From b57aa2c2db72b585a37892d782afe61f818b5099 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 10 Sep 2026 12:48:51 -0600 Subject: [PATCH] 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. --- Graph-Agents.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/Graph-Agents.md b/Graph-Agents.md index 5b92a2e..6dab61d 100644 --- a/Graph-Agents.md +++ b/Graph-Agents.md @@ -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. for 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 '': `inputs:` requires agent '' 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 '': `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`