docs: name the history inputs wherever linked steps list their flow-local inputs

This commit is contained in:
hugocasa
2026-09-14 18:37:23 +02:00
parent fde63dfb09
commit 39cdb0fbb6
4 changed files with 26 additions and 16 deletions
+2 -1
View File
@@ -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<String>,
/// Binds an agent's tools to *this* flow's context, keyed by tool id then input key, without
+12 -10
View File
@@ -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
@@ -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' }] }
})
})
@@ -177,8 +177,8 @@ type AiAgentValue = Extract<FlowModule['value'], { type: 'aiagent' }>
* 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 {