docs: agent nodes can seed a child graph agent's state with inputs
Document the inputs map on agent nodes: overlay order over the child's initial_state, the lone-reference typing rule and the null footgun, the two load-time/runtime rejections, cross-branch read coverage, and the fan-out narrowing example.
1 file changed
+116
-3
+116
-3
@@ -200,7 +200,9 @@ parse_input:
|
||||
...
|
||||
```
|
||||
|
||||
You do not need to (and should not) put `initial_prompt` in `initial_state` as it is overwritten by the dispatcher.
|
||||
You do not need to (and should not) put `initial_prompt` in `initial_state` as it is overwritten by the dispatcher,
|
||||
and `inputs.initial_prompt` on a parent's agent node is rejected (see
|
||||
[Passing state into a child graph agent](#passing-state-into-a-child-graph-agent-inputs)).
|
||||
|
||||
---
|
||||
|
||||
@@ -242,6 +244,8 @@ research_topic:
|
||||
agent: deep-researcher # name of an existing Coyote agent
|
||||
prompt: "Research {{topic}}" # interpolated against state
|
||||
timeout: 600 # optional, in seconds (default 300)
|
||||
inputs: # optional; only when the child is a GRAPH agent
|
||||
max_parallel: "{{inner_width}}" # seeds the child's initial_state (see below)
|
||||
teammates: false # optional; peer messaging with concurrent siblings (see below)
|
||||
state_updates:
|
||||
findings: "{{output}}"
|
||||
@@ -257,12 +261,111 @@ research_topic:
|
||||
whole graph fails (no built-in fallback path on agent nodes).
|
||||
- **`state_updates`:** Map of `state_key: "{{template}}"`. The child agent's
|
||||
final text is available inside this map as `{{output}}`.
|
||||
- **`inputs`:** Map of `child_state_key: "{{template}}"`. Each value is
|
||||
interpolated against the **parent's** state and the resolved map is overlaid
|
||||
on the child graph agent's `initial_state` before its first node runs. Only
|
||||
valid when `agent` names a graph agent (one with a `graph.yaml`). See
|
||||
[Passing state into a child graph agent](#passing-state-into-a-child-graph-agent-inputs).
|
||||
- **`teammates`:** Opt-in peer messaging. When `true` and this node runs
|
||||
concurrently with other flagged agent nodes (as branches of one `map`
|
||||
fan-out, or alongside other `teammates: true` agent nodes in the same
|
||||
super-step), the sibling sub-agents can message each other during the
|
||||
run. See [Teammate messaging in fan-outs](#teammate-messaging-in-fan-outs).
|
||||
|
||||
### Passing state into a child graph agent (inputs)
|
||||
|
||||
`prompt:` is the only thing an agent node hands to a config-only child. When
|
||||
the child is itself a **graph agent**, `inputs:` lets the parent seed any key
|
||||
of the child's `initial_state` as well:
|
||||
|
||||
```yaml
|
||||
research:
|
||||
type: agent
|
||||
agent: deep-research
|
||||
prompt: "{{finding}}"
|
||||
inputs:
|
||||
max_parallel: "{{inner_width}}" # lone {{key}}: the child gets the value as-is
|
||||
local_context: "Repo: {{repo}}" # mixed text: the child gets a string
|
||||
state_updates:
|
||||
verdict: "{{output}}"
|
||||
```
|
||||
|
||||
**Overlay order.** The child's state is seeded in three layers, later layers
|
||||
winning:
|
||||
|
||||
1. the child's own `initial_state` from its `graph.yaml`;
|
||||
2. the resolved `inputs` map (keys the child never declared are simply added);
|
||||
3. `initial_prompt`, which the dispatcher always sets from `prompt:`.
|
||||
|
||||
Nothing is validated against the child's YAML, and nothing about `inputs`
|
||||
persists past the child's executor start: the child sees an ordinary
|
||||
`initial_state` and runs exactly as if those values had been written in its
|
||||
own file.
|
||||
|
||||
**Typing rule.** Every value is a template against the *parent's* state,
|
||||
resolved strictly (a missing key fails the node before the child starts, with
|
||||
`Failed to interpolate inputs.<key> for agent '<agent>'`):
|
||||
|
||||
- A value that is exactly one reference (`"{{inner_width}}"`) hands the
|
||||
parent's JSON value through unchanged: numbers stay numbers, lists stay
|
||||
lists, objects stay objects. Scripts in the child see the real type;
|
||||
the child's own templates stringify it the usual way.
|
||||
- Anything else (`"Repo: {{repo}}"`, `"{{a}}-{{b}}"`) renders to a string.
|
||||
- **`null` is a value, not an absence.** A lone `"{{k}}"` whose parent value
|
||||
is `null` overlays the child's default with `null`. Because `state_updates`
|
||||
cleanup leaves `output: null` behind on the parent, `inputs: {x: "{{output}}"}`
|
||||
after another node's `state_updates` is a footgun: the child gets
|
||||
`x: null`, not its own default.
|
||||
|
||||
**Two things are rejected**, both at load time by the validator and again at
|
||||
runtime for graphs running with `validate_before_run: false`:
|
||||
|
||||
- `inputs:` on a node whose `agent` has a `config.yaml` but no `graph.yaml`
|
||||
(`agent node '<id>': `inputs:` requires agent '<name>' to be a graph agent
|
||||
(no graph.yaml found)`). Presence is what matters: `inputs: {}` on a
|
||||
config-only agent is still an error, while on a graph agent it is a no-op.
|
||||
- An input named `initial_prompt` (`agent node '<id>': `inputs.initial_prompt`
|
||||
is reserved (the dispatcher seeds it from `prompt:`)`). The dispatcher owns
|
||||
that key; put the text in `prompt:`.
|
||||
|
||||
`inputs` values are templated reads like any other, so the
|
||||
[cross-branch read check](#cross-branch-reads-are-not-visible-gotcha) covers
|
||||
them: an `inputs` value that references a key a sibling branch writes in the
|
||||
same super-step is a load-time error.
|
||||
|
||||
**Example: narrowing a child's fan-out.** `deep-research` fans out 4-wide
|
||||
inside each of its runs. An orchestrator that maps over 4 findings and
|
||||
delegates each to `deep-research` therefore runs 16 concurrent LLM calls,
|
||||
and without `inputs` nothing on the parent side can change that. With a
|
||||
templated cap in the child:
|
||||
|
||||
```yaml
|
||||
# child: deep-research/graph.yaml
|
||||
initial_state:
|
||||
max_parallel: 4 # the default when run standalone
|
||||
nodes:
|
||||
fan_out:
|
||||
type: map
|
||||
over: "{{questions}}"
|
||||
max_concurrency: "{{max_parallel}}"
|
||||
...
|
||||
```
|
||||
|
||||
the parent can size it per run:
|
||||
|
||||
```yaml
|
||||
# parent
|
||||
initial_state:
|
||||
inner_width: 2
|
||||
nodes:
|
||||
research:
|
||||
type: agent
|
||||
agent: deep-research
|
||||
prompt: "{{finding}}"
|
||||
inputs:
|
||||
max_parallel: "{{inner_width}}" # 4 items × 2-wide = 8 concurrent, not 16
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## script
|
||||
@@ -1182,6 +1285,16 @@ contain another `map`, or end in an `end` node; see
|
||||
[Branch subgraph rules](#branch-subgraph-rules) and
|
||||
[Retrying inside a branch](#retrying-inside-a-branch-with-_next).
|
||||
|
||||
**Narrowing a child's fan-out.** When a branch delegates to a graph agent
|
||||
that has its own `map`, the fan-outs multiply: 4 items × a 4-wide child is
|
||||
16 concurrent sub-agents. A templated `max_concurrency` sizes *this* graph's
|
||||
fan-out at run time; to size the child's, give the child a templated cap
|
||||
(`max_concurrency: "{{max_parallel}}"` with a default in its
|
||||
`initial_state`) and pass the width from the branch's agent node with
|
||||
`inputs: {max_parallel: "{{inner_width}}"}`. A lone `{{key}}` input keeps
|
||||
the parent's number a number, so the child's cap resolves to an integer.
|
||||
See [Passing state into a child graph agent](#passing-state-into-a-child-graph-agent-inputs).
|
||||
|
||||
## Teammate messaging in fan-outs
|
||||
|
||||
By default, concurrent sub-agents in a fan-out are mutually invisible:
|
||||
@@ -1440,8 +1553,8 @@ super-step (or remove the cross-branch reference).
|
||||
|
||||
**Caveat (scripts).** The validator scans templated fields of typed
|
||||
nodes (`prompt`, `query`, `instructions`, `question`, `default`,
|
||||
`output`, `over`, and `state_updates` values). It does not scan
|
||||
script bodies, since scripts read state opaquely via `GRAPH_STATE` /
|
||||
`output`, `over`, `state_updates` values, and agent-node `inputs` values).
|
||||
It does not scan script bodies, since scripts read state opaquely via `GRAPH_STATE` /
|
||||
`GRAPH_STATE_FILE` env vars. A Python script in a parallel branch
|
||||
that does `state.get("sibling_written_key")` will silently see the
|
||||
**pre**-super-step snapshot. The script's required `state_updates`
|
||||
|
||||
Reference in new issue
Block a user