diff --git a/backend/windmill-ai/src/types.rs b/backend/windmill-ai/src/types.rs index b4eb629451..77772d74a3 100644 --- a/backend/windmill-ai/src/types.rs +++ b/backend/windmill-ai/src/types.rs @@ -137,8 +137,8 @@ struct AIAgentArgsRaw { // nothing runs stateless instead of falling back to the run's memory id. #[serde(default, deserialize_with = "deserialize_present")] memory_id: Option, - // Same distinction for an authored previous messages expression: null replaces a legacy manual list with - // no history, where an absent key keeps that list. + // Same distinction for an authored previous messages expression: null replaces a legacy manual + // list with no history, where an absent key keeps that list. #[serde(default, deserialize_with = "deserialize_present_messages")] previous_messages: Option>>, enabled_tools: Option>, diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index 7fe3dc95fe..366a6145ac 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -144,8 +144,8 @@ fn keep_authored_history_args( /// Reconciles the step's history inputs, the agent's memory policy and the run's memory id, for /// every shape a flow or agent resource may still carry. Managed memory reads only a memory id, and -/// memory that is off reads only previous messages. Also returns lines for the job log: a step input that -/// went unused, or a policy that remembers ending up stateless. +/// memory that is off reads only previous messages. Also returns lines for the job log: a step +/// input that went unused, or a policy that remembers ending up stateless. fn resolve_history_source<'a>( args: &'a AIAgentArgs, run_memory_id: Option, @@ -2133,8 +2133,8 @@ mod tests { } } - /// Empty previous messages a form leaves on a step never replace a legacy list; an expression does, even - /// when it evaluates to null. + /// Empty previous messages a form leaves on a step never replace a legacy list; an expression + /// does, even when it evaluates to null. #[test] fn only_an_expression_can_empty_a_legacy_message_list() { let run = Uuid::from_u128(1); diff --git a/docs/reusable-ai-agents.md b/docs/reusable-ai-agents.md index a1cdc4981a..6dfc17ee8a 100644 --- a/docs/reusable-ai-agents.md +++ b/docs/reusable-ai-agents.md @@ -17,10 +17,11 @@ every workspace via the standard cached-resource-type sync, like other built-in (`windmill-worker/src/ai_executor.rs`): the brain is interpolated, so a nested provider `$res:` credential resolves automatically. - The step keeps only the flow-local inputs (`user_message`, `user_attachments`, `enabled_tools`, - and the history inputs `memory_id` and `previous_messages`) in its own `input_transforms`; the brain and - tools stay in the resource (read-only in the step). `enabled_tools` says which of the roster this - step may call, narrowing one use of a shared agent without touching the agent: an absent field - carries every tool, a list carries the ones it names, and an empty list carries none. + and the history inputs `memory_id` and `previous_messages`) in its own `input_transforms`; the + brain and tools stay in the resource (read-only in the step). `enabled_tools` says which of the + roster this step may call, narrowing one use of a shared agent without touching the agent: an + absent field carries every tool, a list carries the ones it names, and an empty list carries + none. - The agent carries its tools' default input bindings verbatim as authored (static, AI-filled, or flow expressions), so saving round-trips losslessly. Each host flow overrides what it needs: `tool_inputs` stores per-tool overrides (a diff from the resource tool's own @@ -53,20 +54,21 @@ which memory it is: webhooks, evals and plain runs pass no id and run stateless. - **Step: history inputs.** Flow-local, so they stay on a linked step. Each is read in one memory state only, and the editor offers it only there, the memory id behind a *Custom* toggle that - writes the key only once it is on. With managed memory on, `memory_id` overrides - the run's id, hashed the same way: a fixed value is one memory shared by every run, an expression - such as `flow_input.customer_id` one memory per key, and an expression that evaluates to nothing - runs stateless rather than falling back to the run's id. With memory off, `previous_messages` supplies the - history itself. The editor never seeds a placeholder for either, because a present key is the + writes the key only once it is on. With managed memory on, `memory_id` overrides the run's id, + hashed the same way: a fixed value is one memory shared by every run, an expression such as + `flow_input.customer_id` one memory per key, and an expression that evaluates to nothing runs + stateless rather than falling back to the run's id. With memory off, `previous_messages` supplies + the history itself. The editor never seeds a placeholder for either, because a present key is the step's choice, and a static empty value reads as unset. The worker reconciles them once per agent invocation, nested agent tools included, in `resolve_history_source` (`windmill-worker/src/ai_executor.rs`): -1. Memory off, or a legacy `manual` memory: the history is `previous_messages`, else the `manual` list, - else nothing. Memory is neither read nor written, and a step `memory_id` is ignored. -2. Managed memory: a step `previous_messages` is ignored. The memory id is the step's, else the run's, else - a legacy id baked into the `auto` object. With no memory id the agent runs stateless. +1. Memory off, or a legacy `manual` memory: the history is `previous_messages`, else the `manual` + list, else nothing. Memory is neither read nor written, and a step `memory_id` is ignored. +2. Managed memory: a step `previous_messages` is ignored. The memory id is the step's, else the + run's, else a legacy id baked into the `auto` object. With no memory id the agent runs + stateless. Each ignored input and each stateless fallback is written to the job log. diff --git a/frontend/src/lib/components/flows/content/AgentMemoryNotes.svelte b/frontend/src/lib/components/flows/content/AgentMemoryNotes.svelte index 5653c8a6c5..1b43f58381 100644 --- a/frontend/src/lib/components/flows/content/AgentMemoryNotes.svelte +++ b/frontend/src/lib/components/flows/content/AgentMemoryNotes.svelte @@ -7,8 +7,8 @@ * it moves into. */ args: Record chatInputEnabled?: boolean - /** Whether the step's own memory id and previous messages are on this form. A saved agent has neither: - * they belong to each step linking it. */ + /** Whether the step's own memory id and previous messages are on this form. A saved agent has + * neither: they belong to each step linking it. */ historyOnStep?: boolean s3StorageConfigured?: boolean } diff --git a/frontend/src/lib/components/flows/linkedAgentDrafts.ts b/frontend/src/lib/components/flows/linkedAgentDrafts.ts index d78e1849a8..967efe85a0 100644 --- a/frontend/src/lib/components/flows/linkedAgentDrafts.ts +++ b/frontend/src/lib/components/flows/linkedAgentDrafts.ts @@ -178,8 +178,9 @@ type AiAgentValue = Extract * * The overlay order is the worker's (`ai_executor.rs`): its linked branch interpolates the whole * resource brain and only then writes the flow-local inputs (`user_message`, `user_attachments`, - * `enabled_tools`, `memory_id`, `previous_messages`) back from the step's own args. `tool_inputs` stays untouched — the worker overlays it onto the tools in both branches, so - * an inlined step keeps the host flow's tool bindings. + * `enabled_tools`, `memory_id`, `previous_messages`) back from the step's own args. `tool_inputs` + * stays untouched — the worker overlays it onto the tools in both branches, so an inlined step + * keeps the host flow's tool bindings. */ export function inlineAgentDraft(value: AiAgentValue, args: AIAgentConfig): AiAgentValue { const { agent: _agent, ...rest } = value