docs(graph): Ctrl-C aborts a graph run from anywhere; map as/output_key must differ; max_concurrency template is exactly one {{key}}

Adds an 'Abort and cancellation' subsection under the settings block
(per-node cancellation semantics, nesting, headless exit status, raw-mode
prompt caveat, subagents-cancelled / jobs-remain note) and rewrites the
Ctrl-C mentions under max_loop_iterations, timeout, the llm Turn cap part
(d), the self-loop rationale, and the map retry notes. The map field
reference now states that as and output_key must differ and that a string
max_concurrency must be exactly one {{key}} (whitespace around it allowed).
Dark-Alex-17 committed 2026-09-10 17:59:49 -06:00
1 parent ae40aa20c2
commit d6a754f6f5
1 file changed
+67 -21
+67 -21
@@ -159,8 +159,10 @@ nodes:
`0` when the list is discovered mid-run or the loop needs `approval`/`input`
gates (disallowed inside branches). With the cap off, a non-converging loop
is stopped only by `settings.timeout`, an enclosing agent node's `timeout`,
or Ctrl-C; if you also set `timeout: 0`, nothing bounds the run but
completion. The validator warns on the explicit `0`. The `0 = no bound`
or Ctrl-C (which aborts the run promptly from anywhere, see
[Abort and cancellation](#abort-and-cancellation)); if you also set
`timeout: 0`, nothing but Ctrl-C bounds the run before completion. The
validator warns on the explicit `0`. The `0 = no bound`
rule applies to `timeout`, `max_loop_iterations`, and an `llm` node's
`max_iterations`; `max_concurrency: 0` is a validation error.
- **`timeout`:** Wall-clock cap on the entire graph run. Checked before each
@@ -182,10 +184,13 @@ nodes:
`timeout: 0` does not lift a child's non-zero `settings.timeout`; the
child's own executor enforces it. A root graph run directly is bounded
only by its own `settings.timeout`. An unbounded run ends only on
completion, error, or an enclosing bound; Ctrl-C is observed between
super-steps, and an in-flight agent step only stops when its own loop
observes the interrupt (a script step is killed only when its future is
dropped). The 300s agent default was a de-facto safety net for a wedged
completion, error, an enclosing bound, or Ctrl-C. Ctrl-C aborts the run
promptly: the graph flags abort, an in-flight model call is interrupted, a
running tool is killed within ~100 ms, in-flight branches and map items are
cancelled, and the run fails with `Graph … aborted before super-step` or
`Graph … aborted during super-step` (either means "aborted"; see
[Abort and cancellation](#abort-and-cancellation)).
The 300s agent default was a de-facto safety net for a wedged
child; `timeout: 0` is the explicit opt-out of that net.
- **`initial_state`:** A JSON-compatible object. Values are seeded into
state before any node runs and are referenced from any node via `{{key}}`
@@ -208,6 +213,39 @@ nodes:
load time. See [Skills in Graph Agents](Skills#skills-in-graph-agents) for
the full per-node story.
### Abort and cancellation
A graph run observes a user interrupt from anywhere, not just between
super-steps. Ctrl-C (or the enclosing turn being aborted) sets the run's
abort flag; the frontier join reacts to it exactly like a graph timeout:
every in-flight branch task is cancelled, and the run fails with
`Graph '<name>' aborted during super-step with frontier [...]` (or, if the
abort and the join land in the same tick, `… aborted before super-step …`
one iteration later; either means "aborted"). What "cancelled" means per
node type:
- **`llm`:** an in-flight model call is interrupted; every turn also checks
the flag before it creates a client, so a tool result returned after the
abort never reaches the model. The node fails with `llm node aborted` and
the run ends before any `fallback:` route gets to run.
- **Tools inside `llm` nodes:** a running tool process is killed within
~100 ms and reports `Tool call '<name>' aborted`.
- **`script`:** the shell is killed with the branch task.
- **`map`:** cancelling the map cancels every in-flight item chain with it.
- **`agent`:** the child graph inherits the session's interrupt, so Ctrl-C
reaches every nested graph agent directly. A child graph is also cancelled
when its parent aborts or times out; its in-flight synchronous tool then
runs to `tool_timeout` before the child unwinds. A child agent's own
`agent__spawn`ed subagents are cancelled with it, but its `job__start`ed
jobs stay in `job__list` until the next prompt's Ctrl-C.
- **Teammates:** a cancelled branch or item still retires its identity, so
peers see it as finished.
Headless runs (`coyote --agent … "prompt"`) end with the `Graph … aborted`
error (exit status 1) instead of dying on the signal (130). `approval` and
`input` prompts run in raw mode: a Ctrl-C there is a key event that fails
the node, not a SIGINT.
### `{{initial_prompt}}`: Automatically Seeded
When Coyote invokes a graph agent with a user prompt (whether from the
@@ -710,16 +748,18 @@ converges on an external signal, not on a turn count. Rules:
quadratically in the number of turns and the node eventually fails with
`Exceed max_input_tokens limit` (routes to `fallback` if declared), which is
an error unrelated to anything you configured.
- (d) Ctrl-C, **only while `Generating…` is showing** (an in-flight model
call, on a TTY). During a tool's sleep it is swallowed: the tool exits with
code 130, the model sees a failed poll, and the watcher polls again. When it
does land it fails the *node* (→ `fallback`); it does not abort the graph.
- (d) Ctrl-C, from anywhere in the loop: during a model call (the call is
interrupted), during a tool (the tool is killed within ~100 ms and reports
`Tool call '<name>' aborted`), or between turns (the turn-top check fires
before the next call). The node fails with `llm node aborted` and the run
ends with `Graph … aborted` before any `fallback:` route gets to run. See
[Abort and cancellation](#abort-and-cancellation).
**For long-lived watchers, prefer a graph-level self-loop:** an `llm` check
node with the default cap, a `script` gate that emits `_next: check | done`,
and `settings.max_loop_iterations: 0`, with the wait in the script or the
tool. Each visit starts with a fresh conversation (flat context), Ctrl-C is
honored at every step boundary, and `settings.timeout` bounds the whole run.
tool. Each visit starts with a fresh conversation (flat context) and
`settings.timeout` bounds the whole run.
```yaml
settings:
@@ -949,7 +989,7 @@ fan_out_subjects:
branch: research_subject # required; entry node of the per-item chain
output_key: finding # optional; state key the chain writes (default "output")
collect_into: research_results # required; state key to receive the array of results
max_concurrency: 5 # optional; integer or "{{key}}" template
max_concurrency: 5 # optional; integer or exactly one "{{key}}"
next: rank # required; where to go after the map collects
research_subject: # chain step 1 (runs once per item)
@@ -969,14 +1009,16 @@ research_subject: # chain step 1 (runs once per item)
(which is correct semantically. I.e. mapping over nothing yields nothing).
- **`as`:** The state key under which each item is bound inside its
sub-branch. Every node in the chain can read it via `{{<as>}}` in its
prompt / templates (or from `GRAPH_STATE` in a script).
prompt / templates (or from `GRAPH_STATE` in a script). Must differ from
`output_key`; the validator rejects a map whose `as` and `output_key` are
the same key (the item binding would be collected as the chain's result).
- **`branch`:** The id of the chain's **entry** node. The branch subgraph is
that node plus everything reachable from it through declared `next:` and
`fallback:` edges; the validator checks the whole subgraph (see
[Branch subgraph rules](#branch-subgraph-rules)).
- **`output_key`:** The state key whose value in the item's **final** fork
state becomes the item's result. Default `"output"`. The chain must
write it; see [Output contract](#output-contract).
write it; see [Output contract](#output-contract). Must differ from `as`.
- **`collect_into`:** The state key in the **parent's** state that
receives the array of all sub-branch outputs, **in input-list order
regardless of which sub-branch finished first**. This is the map's
@@ -984,13 +1026,15 @@ research_subject: # chain step 1 (runs once per item)
- **`max_concurrency`:** Cap on items in flight. One slot is held for an
item's **whole chain**, so the cap counts items, not steps. Falls back to
`settings.max_concurrency` (default 8) when unset. Accepts either an
integer (`>= 1`) or a `"{{key}}"` template that must resolve at run time
to a positive integer: a JSON number or a numeric string, so a script can
integer (`>= 1`) or a template that is exactly one `{{key}}` (surrounding
whitespace allowed, nothing else) and must resolve at run time to a
positive integer: a JSON number or a numeric string, so a script can
emit `{"review_budget": "3"}`. Anything else (`0`, `"high"`, a missing
key) fails the map node with an error naming the template and the value
it resolved to. A quoted string with no `{{...}}` in it (e.g. `"4"`) is
rejected at load time: write an integer or a template.
`settings.max_concurrency` itself cannot be templated.
it resolved to. A quoted string that is not exactly one template (`"4"`,
`"n={{k}}"`, `"{{ key }}"`, `"{{a}}{{b}}"`) is rejected at load time: write
an integer or exactly one `{{key}}`. `settings.max_concurrency` itself
cannot be templated.
- **`next`:** Required. Where the parent super-step continues after the
map collects.
@@ -1134,7 +1178,9 @@ Rules for the loop:
fails the item and names the node and the count. Keep your own budget
(`retry_count`) under that hard cap. `0` disables that cap too; since there
is no per-item timeout, a runaway `_next` loop in a branch is then stopped
only by `settings.timeout`, the enclosing agent node's `timeout`, or Ctrl-C.
only by `settings.timeout`, the enclosing agent node's `timeout`, or Ctrl-C
(which aborts the run promptly from anywhere, cancelling every in-flight
item chain; see [Abort and cancellation](#abort-and-cancellation)).
- Item errors are fail-fast and carry the chain position:
`map node 'review_each_file': sub-branch [2] failed at node 'check_review' (step 2): ...`.
- A `script` step that fails consults its `fallback:` (a branch-local node)