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
{{ message }}
Repository navigation
Commit 247b2ee
Browse filesBrowse the repository at this point in the historyBrowse files
fix(subagent): deliver send_input plainly when no escalation is pending
Models that fill every optional field attach a resolution to ordinary
messages. With no ask pending the resolution has nothing to answer,
yet it routed the call into the escalation path, which rejected it,
so the parent could not message its worker at all.
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -255,7 +255,7 @@ Workers ask the spawning parent with **`ask_director`** (not the human). That pa
255
255
256
256
The ask is never prose-only: `ask_director` requires a typed **`escalation`** object (`src/subagent/escalation-policy.ts`, `ask-director.ts`) with `policyVersion` plus the required facts — `classification`, `blockedOutcome`, `unavailableDirectorPath`, `permittedAlternatives`, `minimumAddition`, and `declineConsequence` (each a non-empty string, byte-bounded). It may also carry `recommendation`, `safeDefault`, `requestedMechanism` versus `minimumAuthority`, and an optional `verification` detail (`verificationOutcome`, `rootCause`, `attemptedNarrowChecks`, `reducedConfidence`). The five `classification` values decide where the ask settles (`decideEscalation`): `routine` never parks — the worker continues with one of its displayed permitted alternatives and the evaluation is recorded for audit (`routineEvaluations`, newest-only cap); it requires at least one alternative with `comparableConfidence: true`. `director_resolvable` also continues internally when such a comparable alternative exists and parks otherwise. `operator_decision_required`, `outcome_blocked`, and `irreversible_or_sensitive` always park — the first and last require an explicit `minimumAuthority`, which must differ from `requestedMechanism` when both are present — and every parked classification requires at least one permitted alternative to show what was already considered.
257
257
258
-
The parent resolves a parked ask with **`send_input`**'s optional structured `resolution` (`questionId`, `kind`, `answer`; `parseEscalationResolution` validates it at the tool boundary). The four kinds are `director_answer` (normal answer), `declined`, `unavailable`, and `minimum_grant_available` (resume by text only — it grants nothing and leaves a denied-call envelope unconsumed). A resolution is bound to the pending `questionId` — a stale or mismatched id is refused so a late answer cannot settle a newer question — and is mutually exclusive with `interrupt`. The resolution's message is delivered to the worker together with the structured answer; neither can silently drop the other. A resolution never grants authority or retries a tool — a denied call still needs the envelope-owned covering-grant retry path described above.
258
+
The parent resolves a parked ask with **`send_input`**'s optional structured `resolution` (`questionId`, `kind`, `answer`; `parseEscalationResolution` validates it at the tool boundary). The four kinds are `director_answer` (normal answer), `declined`, `unavailable`, and `minimum_grant_available` (resume by text only — it grants nothing and leaves a denied-call envelope unconsumed). A resolution is bound to the pending `questionId` — a stale or mismatched id is refused so a late answer cannot settle a newer question — and, while that ask is pending, cannot be combined with `interrupt` (answering resumes the parked turn, interrupting stops it). With no ask pending there is nothing to resolve, so `send_input` ignores the resolution, delivers the message as plain input (interrupting first when `interrupt` is set, exactly as without a resolution), and reports `resolution_ignored` in its result; models that fill every optional field would otherwise fail every plain message. The resolution's message is delivered to the worker together with the structured answer; neither can silently drop the other. A resolution never grants authority or retries a tool — a denied call still needs the envelope-owned covering-grant retry path described above.
259
259
260
260
Projections keep the policy facts visible: wait JSON and `list_agents` carry the pending ask's `question`, `questionId`, and `assessment`, and the session's `terminal_outcome` (`verification_blocked` when a `declined`/`unavailable` resolution lands on an ask that carried a `verification` detail, `declined` for a plain decline/unavailability, or `verified` when the worker reports a successful verification in place of a blocked one) is projected through wait JSON, `list_agents`, and mailbox mail (`src/subagent/agent-fleet.ts`, `fleet-report.ts`).
"Message a running worker only (check list_agents for status) — completed or interrupted workers need resume_agent instead; shutdown workers are gone (see read_agent_trace or spawn anew). Answers a pending ask_director; interrupt:true stops the current turn first and queues message as the next turn.",
463
+
"Message a running worker only (check list_agents for status) — completed or interrupted workers need resume_agent instead; shutdown workers are gone (see read_agent_trace or spawn anew). Answers a pending ask_director; a resolution sent with no ask pending is ignored and the message is delivered plainly (the result carries resolution_ignored). interrupt:true stops the current turn first and queues message as the next turn.",
"Optional explicit pending-escalation resolution (questionId, kind, answer). It never grants or retries a tool. The message is delivered together with the structured answer.",
482
+
"Optional explicit pending-escalation resolution (questionId, kind, answer). Omit it unless answering a pending ask_director; with no ask pending it is ignored and the message is delivered as plain input. It never grants or retries a tool. The message is delivered together with the structured answer.",
481
483
properties: {
482
484
questionId: {
483
485
type: "string",
@@ -534,31 +536,32 @@ export function createSendInputTool(deps: LifecycleToolDeps): AgentTool {
534
536
`(got ${message.length}).`,
535
537
);
536
538
}
537
-
if(parsed.resolution!==undefined){
538
-
if(parsed.interrupt===true){
539
-
returnlifecycleResult(
540
-
call.id,
541
-
"Error: structured escalation resolution cannot interrupt a worker.",
542
-
);
543
-
}
539
+
// Field-filling models send a resolution on every call. With no ask
540
+
// pending it has nothing to answer, so the message goes out as plain
0 commit comments