You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
candidateOutput?:unknown; // from RecoverableNodeError — the malformed value, redacted+truncated
66
66
inputs:Record<string, unknown>; // the invocation's input values, redacted+truncated
67
+
declaredOutputs:Record<string, string>; // the node's declared output types, per slot
67
68
attempt:number; // 1-based, per (nodeId, invocationKey)
68
69
spentCostUsd:number; // provider cost recorded by this invocation
69
70
createdAssets:boolean;
@@ -87,6 +88,13 @@ export type Verdict =
87
88
88
89
`applyTo: "signature"` is the sticky form: the handle caches the verdict keyed by `(nodeId, failureSignature)` and resolves later escalations that match without waking the agent (PRD scenario 2 — 7 identical failures, 1 LLM call). Keying by signature rather than node keeps one login-required error from silently skipping later, unrelated timeouts on the same node. A signature exists only when the error carries a **stable categorical code** — HTTP status, provider error code, validation path — extracted by a small registry of error-shape recognizers. A plain `Error` gets **no** signature (error class alone is not one; every generic throw would collide), and with no signature stickiness is simply off: each such failure escalates individually. Signatures never derive from message text with embedded values. Only `skip` and `fail` may stick; a sticky `retry` or `substitute` would blindly replay a decision made against different inputs.
89
90
91
+
`declaredOutputs` is what makes `substitute` authorable at all: a repair has to
92
+
be typed per slot, and the escalation is the only thing the supervisor sees.
93
+
It is also what the agent-side acceptance hook validates against before the
94
+
verdict is allowed to become terminal, so the model learns its repair was the
95
+
wrong shape while its conversation is still open. Type metadata, so nothing to
96
+
redact.
97
+
90
98
`RecoverableNodeError` (node-sdk) is how a node hands the supervisor the thing that needs repairing: a parser that throws on broken JSON attaches the raw response as `candidateOutput` instead of losing it. **No `candidateOutput`, no `substitute`** — without the broken value in hand, "repair" is fabrication: the model would invent a structurally valid output the runtime validator cannot semantically vet. A plain thrown error offers retry/skip/fail only.
91
99
92
100
New `ProcessingMessage` variants: `supervisor_escalation` and `supervisor_decision` (escalation + verdict + `decidedBy: "agent" | "sticky" | "bounds" | "default"` + cost). Both flow through the existing `_emit` path.
Copy file name to clipboardExpand all lines: docs/workflow-supervisor-implementation-plan.md
+14-1Lines changed: 14 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -55,10 +55,23 @@ Still no LLM. Depends on PR 1.
55
55
56
56
## Phase B — the agent and the CLI
57
57
58
-
### PR 3 — SupervisorAgent
58
+
### PR 3 — SupervisorAgent — **shipped**
59
59
60
60
Depends on PR 2.
61
61
62
+
Two additions the plan did not name, both forced by what the agent has to see:
63
+
`Escalation.declaredOutputs` (a repair cannot be typed, or accepted, without
64
+
the node's declared output types) and a kernel-side **`RunStateReader`** handed
65
+
to the handle via `SupervisorHandle.attach()` — the tools are pull-based over
66
+
runner state, and a handle is configured before the runner exists. Output
67
+
recording for `read_node_output` is bounded per node and happens only on a
68
+
supervised run.
69
+
70
+
Host-side schema validation checks the schema **as the caller wrote it**, not
71
+
the sanitized copy sent to the provider: the sanitizer injects
72
+
`additionalProperties: false` for strict structured-output modes, and enforcing
73
+
that would reject results no author ever forbade.
74
+
62
75
-`packages/agents/src/supervisor/supervisor-agent.ts`: `SupervisorHandle` via one `StepExecutor` per decision, verdict `outputSchema` (mirroring the escalation's allowed set), serialized queue; the `decide()` signal threads through `StepExecutor` into `provider.generateLoop({signal})` so abort kills the in-flight LLM request.
63
76
-**`TurnBudget` in the runtime provider layer** (design §6): `reserve/commit` object accepted via `generateLoop` options, honored before every model turn by the base loop and by native loop overrides (Claude Agent SDK provider explicitly). Worst-case reservation from the pricing catalog with explicit `supervisorMaxOutputTokens` (default 2048, never unset); no pricing entry ⇒ fail closed. This is a `packages/runtime` change with its own provider-contract test, not a wrapper in the agents package — a wrapper around `generateLoop()` cannot see individual turns.
64
77
-**`StepExecutor` extensions** (design §6): full host-side JSON-schema validation for `finish_step` (enums, nested fields, `additionalProperties`, discriminated unions — today it checks top-level type and required keys only) and an injectable **async acceptance callback** so a `substitute` becomes terminal only after the runtime validator (PR 2, storage resolution included) resolves; failures return as tool errors into the still-open conversation, `MAX_REPAIR_ROUNDS` (3) then `fail`. Both extensions are general `StepExecutor` features with their own tests.
retry: "Run this invocation again, unchanged. Only useful when the failure was transient — a timeout, a rate limit, a 5xx. Offered only when the invocation spent nothing and wrote nothing, so it is never a way to pay twice.",
13
+
substitute:
14
+
"Replace the node's output with a corrected value you supply. For a node that produced something almost right — malformed JSON, a near-miss shape. The value is type-checked against the node's declared outputs before it enters the graph; a repair that fails the check is not a repair.",
15
+
skip: "Retire this invocation. Downstream nodes see it as producing nothing — an item drops out of the batch. Correct when one item is genuinely unprocessable and the rest of the run is still worth having.",
16
+
end_stream:
17
+
"End this streaming node's output where it stopped. What it already emitted stands; nothing more comes. The only recovery available once a node has emitted.",
18
+
fail: "Let the run fail, as it would without a supervisor. The right answer whenever recovery would be a guess."
19
+
};
20
+
21
+
exportconstSUPERVISOR_PROMPT_PREAMBLE=`# Role
22
+
You supervise a running workflow. A node invocation has failed. Decide what happens to it.
23
+
24
+
# What you are deciding
25
+
One invocation, not the run. Everything else in the graph is still executing. Your verdict applies to this invocation alone.
26
+
27
+
# Duties
28
+
- Diagnose before you decide. \`get_run_state\` shows how the run is going; \`read_node_output\` shows what a node upstream of this failure produced. Read them when the error alone does not tell you what happened.
29
+
- Give a one-line rationale with every verdict. It goes in the run report, and it is what a person reads later to understand why an item is missing.
30
+
- Distrust \`skip\`. Skipping is data loss, and it is silent — the run reports success with a smaller result. Use it when an item is genuinely unprocessable, not when a failure is merely inconvenient. If you skip, name the item in your rationale.
31
+
- \`fail\` is not a defeat. A guessed repair that passes validation and is wrong is worse than a failed run: the run reports success and the wrong answer flows downstream.
32
+
33
+
# What the verbs mean`;
34
+
35
+
/** The prompt for one decision: verbs the escalation actually allows, then it. */
? `\n\nNot available for this failure: ${omitted.map((a)=>`\`${a}\``).join(", ")}. The kernel computed that from what this node is and what this invocation already did; asking for one of them gets you \`fail\`.`
0 commit comments