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
|
agent: deep-researcher # name of an existing Coyote agent
|
||||||
prompt: "Research {{topic}}" # interpolated against state
|
prompt: "Research {{topic}}" # interpolated against state
|
||||||
timeout: 600 # optional, in seconds (default 300)
|
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)
|
teammates: false # optional; peer messaging with concurrent siblings (see below)
|
||||||
state_updates:
|
state_updates:
|
||||||
findings: "{{output}}"
|
findings: "{{output}}"
|
||||||
@@ -257,12 +261,111 @@ research_topic:
|
|||||||
whole graph fails (no built-in fallback path on agent nodes).
|
whole graph fails (no built-in fallback path on agent nodes).
|
||||||
- **`state_updates`:** Map of `state_key: "{{template}}"`. The child agent's
|
- **`state_updates`:** Map of `state_key: "{{template}}"`. The child agent's
|
||||||
final text is available inside this map as `{{output}}`.
|
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
|
- **`teammates`:** Opt-in peer messaging. When `true` and this node runs
|
||||||
concurrently with other flagged agent nodes (as branches of one `map`
|
concurrently with other flagged agent nodes (as branches of one `map`
|
||||||
fan-out, or alongside other `teammates: true` agent nodes in the same
|
fan-out, or alongside other `teammates: true` agent nodes in the same
|
||||||
super-step), the sibling sub-agents can message each other during the
|
super-step), the sibling sub-agents can message each other during the
|
||||||
run. See [Teammate messaging in fan-outs](#teammate-messaging-in-fan-outs).
|
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
|
## script
|
||||||
@@ -1182,6 +1285,16 @@ contain another `map`, or end in an `end` node; see
|
|||||||
[Branch subgraph rules](#branch-subgraph-rules) and
|
[Branch subgraph rules](#branch-subgraph-rules) and
|
||||||
[Retrying inside a branch](#retrying-inside-a-branch-with-_next).
|
[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
|
## Teammate messaging in fan-outs
|
||||||
|
|
||||||
By default, concurrent sub-agents in a fan-out are mutually invisible:
|
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
|
**Caveat (scripts).** The validator scans templated fields of typed
|
||||||
nodes (`prompt`, `query`, `instructions`, `question`, `default`,
|
nodes (`prompt`, `query`, `instructions`, `question`, `default`,
|
||||||
`output`, `over`, and `state_updates` values). It does not scan
|
`output`, `over`, `state_updates` values, and agent-node `inputs` values).
|
||||||
script bodies, since scripts read state opaquely via `GRAPH_STATE` /
|
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
|
`GRAPH_STATE_FILE` env vars. A Python script in a parallel branch
|
||||||
that does `state.get("sibling_written_key")` will silently see the
|
that does `state.get("sibling_written_key")` will silently see the
|
||||||
**pre**-super-step snapshot. The script's required `state_updates`
|
**pre**-super-step snapshot. The script's required `state_updates`
|
||||||
|
|||||||
Reference in new issue
Block a user