diff --git a/Graph-Agents.md b/Graph-Agents.md index d0ed8a8..6366703 100644 --- a/Graph-Agents.md +++ b/Graph-Agents.md @@ -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 '' 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 '' 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 '' 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 `{{}}` 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)