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).
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)
|
||||
|
||||
Reference in new issue
Block a user