diff --git a/backend/windmill-types/src/flows.rs b/backend/windmill-types/src/flows.rs index cb1c53a579..4b45c55e6a 100644 --- a/backend/windmill-types/src/flows.rs +++ b/backend/windmill-types/src/flows.rs @@ -1095,7 +1095,8 @@ pub enum FlowModuleValue { omit_output_from_conversation: bool, /// When set, the agent brain config (provider/model/system prompt/etc.) and tools are /// resolved at runtime from this `ai_agent` resource path (hybrid linking). The module's - /// `input_transforms` then only carry the flow-local inputs (user_message/user_attachments). + /// `input_transforms` then only carry the flow-local inputs: user_message, + /// user_attachments and the history inputs memory_id and messages. #[serde(default, skip_serializing_if = "Option::is_none")] agent: Option, /// Binds an agent's tools to *this* flow's context, keyed by tool id then input key, without diff --git a/docs/reusable-ai-agents.md b/docs/reusable-ai-agents.md index 93e59a77b1..430770a739 100644 --- a/docs/reusable-ai-agents.md +++ b/docs/reusable-ai-agents.md @@ -49,11 +49,12 @@ which memory it is: names two memories. A uuid is used as is. Nothing is generated at save time, so schedules, webhooks, evals and plain runs pass no id and run stateless. - **Step: history inputs.** Flow-local, so they stay on a linked step. `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 + 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. `messages` supplies the history itself and - bypasses memory; an expression that evaluates to null sends no history and still bypasses it. The editor writes at most one of them and never seeds a placeholder for - either, because a present key is the step's choice; if both are present, `messages` wins. + bypasses memory; an expression that evaluates to null sends no history and still bypasses it. The + editor writes at most one of them and never seeds a placeholder for either, because a present key + is the step's choice; if both are present, `messages` wins. The worker reconciles them once per agent invocation, nested agent tools included, in `resolve_history_source` (`windmill-worker/src/ai_executor.rs`): @@ -63,9 +64,9 @@ The worker reconciles them once per agent invocation, nested agent tools include 3. The memory id is the step's, else the run's, else a legacy id baked into the `auto` object. 4. With no memory id the agent runs stateless and says so in the job log. -Memory is stored per (memory id, step id), in `ai_agent_memory` or S3 at -`memory/{workspace}/{memory id}/{step}.json`. The chat transcript (`flow_conversation_message`) -always follows the run's id, even when a step sets its own. Nothing expires stored memory: deleting a chat conversation deletes +Memory is stored per (memory id, step id), in `ai_agent_memory` or S3 at `memory/{workspace}/{memory +id}/{step}.json`. The chat transcript (`flow_conversation_message`) always follows the run's id, +even when a step sets its own. Nothing expires stored memory: deleting a chat conversation deletes its memory, and a memory named by a string id stays until it is overwritten. Compatibility runs one way. New workers read every older shape. The editor rewrites a legacy step @@ -87,9 +88,10 @@ A flow does not wait for that deploy to see the draft: - Testing the flow, or a single linked step, runs the draft. `runFlowPreview` and `ModuleTest` substitute each linked step for the standalone step the draft would run as (`linkedAgentDrafts.ts`): `agent` cleared, the draft's brain as static input transforms, the - draft's tools on the step, and the step's own `user_message`/`user_attachments` kept on top — - the same overlay order `ai_executor.rs` applies to a linked step. `tool_inputs` is untouched, - since the worker overlays it in both branches. + draft's tools on the step, and the step's own flow-local inputs (`user_message`, + `user_attachments`, `memory_id`, `messages`) kept on top — the same overlay order `ai_executor.rs` + applies to a linked step. `tool_inputs` is untouched, since the worker overlays it in both + branches. - The step's linked card and the graph's tool nodes show the draft, with a *Draft* badge, so the editor describes what a test would run. Read-only surfaces (the deployed flow page, the run viewer) stay on the deployed agent: they resolve tools through `publishLinkedAgentTools` without diff --git a/frontend/src/lib/components/flows/agentResourceUtils.test.ts b/frontend/src/lib/components/flows/agentResourceUtils.test.ts index aaefaabae3..dba2c05fb8 100644 --- a/frontend/src/lib/components/flows/agentResourceUtils.test.ts +++ b/frontend/src/lib/components/flows/agentResourceUtils.test.ts @@ -143,16 +143,23 @@ describe('nonStaticBrainKeys', () => { }) describe('flowLocalInputs', () => { - it('keeps only user_message/user_attachments, dropping brain transforms', () => { + // Linking keeps exactly these on the step, history inputs included: dropping one would silently + // move a linked step onto the run's memory. + it('keeps the user message, attachments and history inputs, dropping brain transforms', () => { expect( flowLocalInputs({ provider: { type: 'static', value: {} }, + memory: { type: 'static', value: { kind: 'window', context_length: 10 } }, user_message: { type: 'static', value: 'hi' }, - user_attachments: { type: 'static', value: [] } + user_attachments: { type: 'static', value: [] }, + memory_id: { type: 'javascript', expr: 'flow_input.customer_id' }, + messages: { type: 'static', value: [{ role: 'user', content: 'earlier' }] } } as any) ).toEqual({ user_message: { type: 'static', value: 'hi' }, - user_attachments: { type: 'static', value: [] } + user_attachments: { type: 'static', value: [] }, + memory_id: { type: 'javascript', expr: 'flow_input.customer_id' }, + messages: { type: 'static', value: [{ role: 'user', content: 'earlier' }] } }) }) diff --git a/frontend/src/lib/components/flows/linkedAgentDrafts.ts b/frontend/src/lib/components/flows/linkedAgentDrafts.ts index 8da732e7ff..47c7f2e658 100644 --- a/frontend/src/lib/components/flows/linkedAgentDrafts.ts +++ b/frontend/src/lib/components/flows/linkedAgentDrafts.ts @@ -177,8 +177,8 @@ type AiAgentValue = Extract * the step's own flow-local inputs kept on top. * * The overlay order is the worker's (`ai_executor.rs`): its linked branch interpolates the whole - * resource brain and only then writes `user_message`/`user_attachments` back from the step's own - * args. `tool_inputs` stays untouched — the worker overlays it onto the tools in both branches, so + * resource brain and only then writes the flow-local inputs (`user_message`, `user_attachments`, + * `memory_id`, `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 {