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` `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 gates (disallowed inside branches). With the cap off, a non-converging loop
is stopped only by `settings.timeout`, an enclosing agent node's `timeout`, 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 or Ctrl-C (which aborts the run promptly from anywhere, see
completion. The validator warns on the explicit `0`. The `0 = no bound` [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 rule applies to `timeout`, `max_loop_iterations`, and an `llm` node's
`max_iterations`; `max_concurrency: 0` is a validation error. `max_iterations`; `max_concurrency: 0` is a validation error.
- **`timeout`:** Wall-clock cap on the entire graph run. Checked before each - **`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 `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 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 only by its own `settings.timeout`. An unbounded run ends only on
completion, error, or an enclosing bound; Ctrl-C is observed between completion, error, an enclosing bound, or Ctrl-C. Ctrl-C aborts the run
super-steps, and an in-flight agent step only stops when its own loop promptly: the graph flags abort, an in-flight model call is interrupted, a
observes the interrupt (a script step is killed only when its future is running tool is killed within ~100 ms, in-flight branches and map items are
dropped). The 300s agent default was a de-facto safety net for a wedged 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. child; `timeout: 0` is the explicit opt-out of that net.
- **`initial_state`:** A JSON-compatible object. Values are seeded into - **`initial_state`:** A JSON-compatible object. Values are seeded into
state before any node runs and are referenced from any node via `{{key}}` 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 load time. See [Skills in Graph Agents](Skills#skills-in-graph-agents) for
the full per-node story. 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 ### `{{initial_prompt}}`: Automatically Seeded
When Coyote invokes a graph agent with a user prompt (whether from the 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 quadratically in the number of turns and the node eventually fails with
`Exceed max_input_tokens limit` (routes to `fallback` if declared), which is `Exceed max_input_tokens limit` (routes to `fallback` if declared), which is
an error unrelated to anything you configured. an error unrelated to anything you configured.
- (d) Ctrl-C, **only while `Generating…` is showing** (an in-flight model - (d) Ctrl-C, from anywhere in the loop: during a model call (the call is
call, on a TTY). During a tool's sleep it is swallowed: the tool exits with interrupted), during a tool (the tool is killed within ~100 ms and reports
code 130, the model sees a failed poll, and the watcher polls again. When it `Tool call '<name>' aborted`), or between turns (the turn-top check fires
does land it fails the *node* (→ `fallback`); it does not abort the graph. 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 **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`, 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 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 tool. Each visit starts with a fresh conversation (flat context) and
honored at every step boundary, and `settings.timeout` bounds the whole run. `settings.timeout` bounds the whole run.
```yaml ```yaml
settings: settings:
@@ -949,7 +989,7 @@ fan_out_subjects:
branch: research_subject # required; entry node of the per-item chain branch: research_subject # required; entry node of the per-item chain
output_key: finding # optional; state key the chain writes (default "output") output_key: finding # optional; state key the chain writes (default "output")
collect_into: research_results # required; state key to receive the array of results 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 next: rank # required; where to go after the map collects
research_subject: # chain step 1 (runs once per item) 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). (which is correct semantically. I.e. mapping over nothing yields nothing).
- **`as`:** The state key under which each item is bound inside its - **`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 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 - **`branch`:** The id of the chain's **entry** node. The branch subgraph is
that node plus everything reachable from it through declared `next:` and that node plus everything reachable from it through declared `next:` and
`fallback:` edges; the validator checks the whole subgraph (see `fallback:` edges; the validator checks the whole subgraph (see
[Branch subgraph rules](#branch-subgraph-rules)). [Branch subgraph rules](#branch-subgraph-rules)).
- **`output_key`:** The state key whose value in the item's **final** fork - **`output_key`:** The state key whose value in the item's **final** fork
state becomes the item's result. Default `"output"`. The chain must 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 - **`collect_into`:** The state key in the **parent's** state that
receives the array of all sub-branch outputs, **in input-list order receives the array of all sub-branch outputs, **in input-list order
regardless of which sub-branch finished first**. This is the map's 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 - **`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 item's **whole chain**, so the cap counts items, not steps. Falls back to
`settings.max_concurrency` (default 8) when unset. Accepts either an `settings.max_concurrency` (default 8) when unset. Accepts either an
integer (`>= 1`) or a `"{{key}}"` template that must resolve at run time integer (`>= 1`) or a template that is exactly one `{{key}}` (surrounding
to a positive integer: a JSON number or a numeric string, so a script can 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 emit `{"review_budget": "3"}`. Anything else (`0`, `"high"`, a missing
key) fails the map node with an error naming the template and the value 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 it resolved to. A quoted string that is not exactly one template (`"4"`,
rejected at load time: write an integer or a template. `"n={{k}}"`, `"{{ key }}"`, `"{{a}}{{b}}"`) is rejected at load time: write
`settings.max_concurrency` itself cannot be templated. an integer or exactly one `{{key}}`. `settings.max_concurrency` itself
cannot be templated.
- **`next`:** Required. Where the parent super-step continues after the - **`next`:** Required. Where the parent super-step continues after the
map collects. map collects.
@@ -1134,7 +1178,9 @@ Rules for the loop:
fails the item and names the node and the count. Keep your own budget 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 (`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 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: - 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): ...`. `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) - A `script` step that fails consults its `fallback:` (a branch-local node)