@sema-agent/core 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/agents/roster-store.d.ts +1 -0
- package/dist/agents/send-message-tool.js +6 -0
- package/dist/agents/subagent.d.ts +31 -1
- package/dist/agents/subagent.js +67 -21
- package/dist/agents/teacher.js +15 -3
- package/dist/agents/team.js +10 -0
- package/dist/agents/verify.js +7 -0
- package/dist/brain/anthropic.js +27 -10
- package/dist/brain/open-responses.js +19 -4
- package/dist/brain/openai.js +32 -5
- package/dist/core/a2a.js +1 -1
- package/dist/core/background-agent-store.d.ts +2 -1
- package/dist/core/background-agent-store.js +1 -0
- package/dist/core/checkpoint-store.d.ts +2 -0
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/memory-recall.js +8 -3
- package/dist/core/memory.d.ts +5 -0
- package/dist/core/memory.js +6 -4
- package/dist/core/runner/assemble-result.js +9 -0
- package/dist/core/runner/prepare-task.d.ts +12 -1
- package/dist/core/runner/prepare-task.js +110 -38
- package/dist/core/runner/runtask.d.ts +12 -0
- package/dist/core/runner/runtask.js +169 -37
- package/dist/core/runner/session-file-state-replay.d.ts +7 -0
- package/dist/core/runner/session-file-state-replay.js +56 -0
- package/dist/core/runner/synthetic-tools.js +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +24 -9
- package/dist/core/runner/tool-output-projection.js +5 -4
- package/dist/core/runner/turn-attachments.d.ts +2 -0
- package/dist/core/runner/turn-attachments.js +14 -5
- package/dist/core/session-reconcile.d.ts +7 -3
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/strategy-store.d.ts +1 -1
- package/dist/core/strategy-store.js +27 -4
- package/dist/core/task-registry-agent.d.ts +3 -0
- package/dist/core/task-registry-agent.js +9 -2
- package/dist/core/task-registry-shared.d.ts +1 -0
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/tools.js +9 -1
- package/dist/core/trace.d.ts +1 -0
- package/dist/core/types.d.ts +8 -1
- package/dist/engine/loop/agent-loop.js +168 -22
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.js +19 -0
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +4 -1
- package/dist/orchestration/workflow.js +1 -1
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/coordinator.js +1 -1
- package/dist/prompts/default.d.ts +1 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/memory-store.js +3 -7
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +4 -0
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +7 -4
- package/dist/tools/web.d.ts +21 -1
- package/dist/tools/web.js +126 -11
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.11.0 — 2026-08-04
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
|
|
7
|
+
- **The restart-loop suspend cap counts CONSECUTIVE NO-PROGRESS suspends, not total approvals.** The cap's stated purpose is catching a restart-prone model re-issuing the same gated call forever — yet it counted distinct approved-and-executed gates identically, so under a doctrine that asks per call (`shellGate:"classify"` + out-of-root reads) a fully cooperating approver was killed on the 6th legitimate Yes (`suspend.loop`, default `maxSuspends: 5`). The chain base now resets the moment the resume leg starts EXECUTING the approved pending call (an execution that then errors still consumed the approval — it reset the chain); a park whose approved call never ran keeps accumulating. **Consumer flips**: checkpoint `suspendCount` restarts at 1 after each executed approval — probes pinning a monotonically increasing `suspendCount` across approve-execute cycles red; `maxSuspends` now bounds no-progress chains, never the total number of approvals a run may receive.
|
|
8
|
+
- **`SubagentErrorKind` closed set gains a sixth member: `"governance"`.** A FAILED child whose `errorCode` carries the `usage.*` prefix (deployment usage window refused/exhausted) now classifies as `governance` with `retryable: false` — it is DEFERRED-retryable: re-issuing NOW gets the same refusal, but the refusal lifts at the card's `retryAfterMs`. Previously these folded into `logic` ("re-delegating unchanged won't help"), which told a scheduler to abandon a subtask that merely needed to wait. The transient retry set (`rate_limit`/`overloaded`/`timeout`/`network` → `retryable: true`) is untouched. **Consumer flips**: switches over the closed five-member kind set add a `governance` arm; the completed agent card now pairs `error_kind: "governance"` with `retryAfterMs` on window-exhausted children.
|
|
9
|
+
|
|
10
|
+
- **Deferred-tool activation no longer rewrites the provider tools block — `toolMaterializeStrategy` defaults to `"static"`.** Tools serialize FIRST in the provider prompt-cache hierarchy, so the old activation swap (placeholder → full schema via setTools) invalidated the entire conversation cache — 29% of benchmarked trials paid an average ~31k tokens per break. Under the new default the placeholder bytes persist for the whole run; the full schema reaches the model through the ToolSearch result text (existing carrier) and engine-side validation against the REAL schema is unchanged. Ruled GO by a live A/B on the weakest BYOM model (15/15 argument instances correct, three scenario classes incl. nested/optional params). `"swap"` is the explicit opt-back (spec or `SEMA_TOOL_MATERIALIZE_STRATEGY`, invalid env value refused loudly as `config.tool_materialize_invalid`). **Consumer flips**: probes pinning the post-activation full schema in the tools block red — the activation announcement frames and genuine roster changes (MCP refresh) are unchanged.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A shell-gated ask persists the live shellGate doctrine on its risk descriptor.** Field forensics could not answer "was `classify` or `always` live when this gate fired" from any persisted artifact. The mint now stamps `RiskDescriptor.shellGateDoctrine` (`"classify" | "always"`, present only on shell-gated asks — `"off"` never mints) so the checkpoint row self-reports its provenance. Additive optional field.
|
|
15
|
+
- **`TaskSpec.additionalReadDirectories` — a read-only fence widening.** Extra directories the READ faces admit and nothing else: the `shellGate:"classify"` read boundary (a provably read-only command like `head <dir>/settings.json` auto-allows instead of consulting the approver on every call — the interactive kill shape this exists for), plus read_file/grep/glob/repo_map and `bash_readonly` containment. The write faces (edit_file/write_file/notebook_edit) refuse these directories exactly as before — a read grant never silently becomes a write grant (`additionalDirectories` remains the read+write widening). Same canonicalization/fail-closed-skip rules as `additionalDirectories` (the skip trace gains an optional `field` discriminator); announced in `# Environment` as `Additional read-only directories:`; inherited down the delegation tree on the same seat as `additionalDirectories`. Additive — absent ⇒ byte-identical behavior.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **WebFetch stops laundering emptiness into authority (grounding contract).** A shell page (tiny extracted text, or near-zero text ratio) now gets a TRUSTED grounding verdict outside the untrusted fence, the full extracted text appended for verification, and additive `details.grounding{level,textChars,bytes,textRatio}`; an incomplete body is called a retrieved prefix. The summarizer input budget derives from the summary model's window (flat 100k retired; trimmed input disclosed structurally and in the rendered head; NaN model metadata refused loudly). Recovery prose names Bash/curl only when reachable in THIS run. The summary prompt gains a grounding clause (report absence rather than infer; a filename is only evidence about naming) ahead of the byte-fixed CC guidelines tail.
|
|
20
|
+
- **A truncated-tool-arguments disclosure names the request output cap when one was armed** (`request output cap in effect: N tokens`) on both brain lanes — a cut mid-arguments at the cap read as a generic provider error with no budget clue.
|
|
21
|
+
- **agentStream's birth window is steerable**: an immediate `steer()` after `agentStream()` waits (bounded) for the loop's first prompt and lands on turn 1; a finished run keeps the honest refusal.
|
|
22
|
+
- **A long-window zero-byte Bash timeout names its two honest readings** (stalled before producing output / holding it in a block buffer); short windows and abort arms stay bare.
|
|
23
|
+
- **The background faces carry the governance wait hint.** A `usage.window_exhausted` background child's settle dropped `retryAfterMs` — every bg face (poll text, poll details, notification-adjacent durable row) said `retryable:false` with no way to know when the refusal lifts, while the sync report card carried the hint. The classification family gains its fourth seat end-to-end: settle mint → registry row → durable archival row → poll `details.retryAfterMs` → the poll text clause (`(error_kind: governance, retryable: false, retry_after_ms: N)`); a revive clears it with its three siblings. Additive on every face.
|
|
24
|
+
- **The `suspend.loop` terminal diagnoses the every-approval-was-Yes kill.** When every recorded gate decision on the run was ALLOW, the message now names the shape (a gate that keeps asking × an approver that keeps approving consumed the suspend allowance on legitimate work) and the remedies (a live `onAsk` parks nothing; keep benign commands inside the read boundary; raise `maxSuspends` for approval-heavy tasks) instead of reporting a bare restart-loop mystery.
|
|
25
|
+
|
|
26
|
+
## 5.10.0 — 2026-08-04
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`TaskSpec.checkpointStore` is three-valued**: `null` explicitly gives THIS run no durable store, even on a deployment that wired one (`undefined` still inherits `RunnerDeps.checkpointStore`; a store still overrides). All four legs into `status:"suspended"` (approval park / resource-slice / platform `env_lifetime`·`usage_window` / `plan_review`) are gated on a store being present, so a store-null run cannot reach `suspended` by construction and can never leave a pending checkpoint nobody resumes — the machine-started-run form (notification redelivery, maintenance sweeps). Existing no-store semantics apply: asks resolve at a live `onAsk` or fail-closed deny; limits are loud terminals; the plan/question durable faces are not mounted. Delegated children inherit it (sync/bg/fork/workflow spawn lanes). New Runner-filled `ToolExecuteContext.checkpointStoreDisabledForChildren` tells custom delegation tools to copy it. **Consumer notes**: `preempt()` becomes a documented no-op on store-null runs (existing `preempt.ignored` trace — "preemptible" is now two-valued; schedulers keep runs preemptible by leaving the field unset); a resume config carrying `null` refuses loudly (`checkpoint.not_found`) instead of silently resuming against deps.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Eight `tool_end.structured` cards were minted and then dropped before the wire.** The projection allowlist (`details.type`) had never been reconciled against the tools that mint cards, so a host got nothing structured for: `ReportFindings` (worst case — its model face is deliberately compressed to `"N findings reported."`, so the findings reached *neither* the model *nor* the host), `ScheduleWakeup`, `SendMessage`, `AgentTranscript`, the A2A peer tools, the PDF `document` card (whose own 48K base64 budget was written against a size gate it never reached), the `bash_readonly` out-of-root refusal, and the **completed** delegation report — the one card `tool_end.structured`'s own contract names by name, while only the `async_launched` receipt could actually arrive. Registered words added: `report-findings`, `schedule-wakeup`, `send-message`, `agent-transcript`, `a2a`, `document`, `readonly_out_of_root`. Two mint sites gained the `type` discriminant they lacked (`ReportFindings`; the three `bash_readonly` refusal arms, following the `path_not_in_root` precedent) — `details.code` is untouched for existing readers. Model faces are byte-unchanged everywhere; this is purely the host face that was missing. **Consumer notes**: a shell that prose-matched any of these can now read fields; a probe asserting `structured === undefined` for them reds.
|
|
35
|
+
|
|
36
|
+
- **An interrupted tool call's synthetic `tool_end` finally carries a body — and the wake/crash leg emits one at all.** Both reconcile legs closed orphan calls in the transcript with the full `[INTERRUPTED]` explanation the model reads, while the event stream got `isError: true` and nothing else: a consumer rendering tool output from frames showed an empty result for every interrupted call, and the two faces of one call disagreed. The frames now carry `output` (the exact persisted text, same projection as every live tool result) and `errorCode` (`interrupted_never_started` | `interrupted_outcome_unknown` — the persisted `details.errorKind`, so a consumer discriminates on a code instead of prose-matching), plus the tool's display `label` and the usual `eventId`/subagent attribution. Separately, the **wake/crash** leg (a previous PROCESS died holding the calls) used to repair the transcript and tell the stream *nothing*: no `tool_end`, no `message_committed`. It now replays the same pair at run open — the only channel through which that news can ever arrive, since the process that owned those calls is gone. `ReconcileReport.recovered` entries gained `text` and `errorKind` (new exported type `RecoveredOrphan`, `ReconciledErrorKind`). **Consumer flips**: a probe pinning "the interrupted `tool_end` has no `output`" reds; a resumed run on a crashed session now emits one `tool_end` + one `message_committed` per orphan before its first turn, so an exact event-count or first-event assertion on that path reds (there is deliberately no matching `tool_start` — the start belonged to the dead process's stream).
|
|
37
|
+
|
|
38
|
+
- **The idle-park notification redelivery carries task identity.** A notification parked while the session sat idle is redelivered on the next run's stream as the same `task_notification` family — but that push shipped with no `eventId`, and for a subagent no `parentToolCallId`/`sourceTaskId`, so a consumer keying a child's frames by task silently attributed every redelivered frame to the wrong task. The cause was structural rather than a missing field: the identity minter was declared later in the run body than the redelivery drain, so the drain could not call it. It is now minted as soon as the prepared run exists. Live-lane frames are unchanged (they always had identity).
|
|
39
|
+
|
|
40
|
+
- **Read-before-write state follows the SESSION, not just the task.** An ordinary continuation turn used to start with an empty read-state (the seed lane existed only for durable-checkpoint resume), so turn 2 paid a forced re-read for every file turn 1 had read whole. The prepare step now replays the session transcript on continuation turns: whole-file-provable Read/Write records seed entries whose hash comes from what the transcript recorded — never from disk — so staleness is unchanged by construction (a file changed between turns is still refused), and Edit/NotebookEdit records retract their path (a self-edit is never misread as a foreign change). Resume legs, first turns, and post-Edit paths are byte-identical to before. **Consumer flip**: a probe pinning "a continuation-turn edit is always refused as not-read" reds.
|
|
41
|
+
|
|
42
|
+
- **Memory recall: an unknown write time is incomparable, not 1970.** A note whose `ts` failed to parse used to sort as epoch 0 — it lost every `[[name]]` collision (permanently unreachable through links) and rendered a fabricated age ("written 678 months ago"). `MemoryNoteHeader.timestampMissing` (new, optional) is minted at the single parse point; the one recency comparison treats missing as a tie (the incumbent keeps the name — an unknown-time note neither sinks nor poses as newest), and the render face says "write time unknown — verify it's still current". A literal 1970 timestamp string stays a real time (the numeric value is never the signal; negative pins lock this). Bonus: a wire row with NO `ts` key used to throw and kill the whole scope's manifest build. **Consumer flip**: a probe pinning the fabricated-age wording or the sink-to-bottom collision outcome reds.
|
|
43
|
+
- **Three awaited rescue timers are REF'd + cleared-on-settle** (subagent observer drain, teacher abortFallback, WebFetch deadline): unref'd, the process could exit before the rescue fired when the wedged arm held no live handles — the settle never completed. The bg notify drain window keeps its unref by design (a background lane must not pin the process).
|
|
44
|
+
- **A harness-side throw around ONE tool call becomes that call's disclosed error result — the batch survives.** Worker exceptions that are not cancellation (a rejected `tool_execution_update` delivery, a sink failure) used to reject the whole parallel batch, orphan the sibling calls, and on the worst path rewrite a COMMITTED side effect into `[tool execution harness failed: …]` — inviting the model to re-run an already-effective write. Progress deliveries now settle at one point; a delivery failure on an executed tool keeps `content`/`isError` untouched and rides `details.toolProgressDeliveryFailed` (total by construction: hostile accessors or exotic `details` decline the annotation, never the outcome; `afterToolCall` cannot erase it). Cancellation — including one wrapped in the harness's own `AgentHarnessError` (`cause`-chain walk) — stays a hard batch failure on every lane, and the in-stream lane closes admission immediately. The sequential lane shares the same disclosure band. **Consumer flips**: probes pinning the old `[tool execution harness failed]` rewrite or `isError:true` on a delivery-failed success red — re-pin on the new shape plus a retired-dialect tripwire.
|
|
45
|
+
- **openai lane: an evidence-free `tool_calls` fragment no longer fabricates an unnamed-call disclosure.** The evidence gate sits at the finalize disclosure point (slot assignment also routes id-less continuations, so gating there corrupted two real calls); the stream's own verdict wins — `finish_reason:"tool_calls"` with zero survivors, or an empty slot beside real calls, discloses loudly; an isolated terminal fragment with `stop`/`length` stays silent.
|
|
46
|
+
- **A throwing tool keeps its machine-readable marks on BOTH legs.** Two strip points, one class: the `defineTool` adapter rewrapped a ToolSpec throw as a bare `Error` (marks died even LIVE — a ToolSpec throw carrying `details.code` got no `errorCode` lift while a native tool's identical error did), and the durable-resume batch's catch stringified the message alone (breaking the live/resume frame-parity contract). Both carry `details`/`errorKind` through now; message text unchanged.
|
|
47
|
+
- **Cache-break attribution: an aborted / usage-missing row is "unknown", not a cold cache.** Feeding it to the design/31 detector minted a false `server-or-ttl` finding and poisoned the warm baseline for the next real turn. Such rows are skipped; a genuine cacheRead collapse still fires.
|
|
48
|
+
- **A usage-ledger charge that does not settle discloses loudly — and is never abandoned.** Both commit points (turn boundary, end-of-task flush) fire a single deployment-sink disclosure after 10s and keep awaiting (abandoning a sequential ledger write would double-charge on the next flush). A wedged ledger store still wedges what it always wedged, now visibly.
|
|
49
|
+
- **The `abortResultDetails` seam is total.** A throwing host callback degrades to "no details" (the abort text is the contract; the marker is additive) instead of escalating a per-tool abort into an unpaired `tool_execution_start` plus an uncaught task-level rejection.
|
|
50
|
+
|
|
51
|
+
### BREAKING
|
|
52
|
+
|
|
53
|
+
- **The `tool_end.structured` card vocabulary is now a two-way contract — three words leave it, and `type:"agent"` gains a second shape.** Removed: `multiedit`, `memory-saved`, `memory-recall`. No tool in the tree has ever minted any of the three (a batch MultiEdit replay mints `type:"edit"` with an `edits[]` array; core mounts no memory tool at all), so they were a promise to consumers that nothing could keep — a host switching on the closed set had dead branches. Closed-set consumers: **drop those three arms**; a fixture that fabricates one of them to assert it passes the allowlist reds. Separately, `type:"agent"` used to imply "background launch receipt" because the completed delivery was silently dropped; it now carries **both** Agent shapes, told apart by `status` — `"async_launched"` is the receipt, any other value is the completed landing report (`result` / `stats` / `resolvedModel` / `toolStats` / kept `worktreePath` / failure attribution). **Branch on `status`, never on which keys are present.** The completed card is an explicit field whitelist over the child `TaskResult`, not a spread: `checkpointToken` (the resume capability) and `checkpointGate` never leave the process, and a future `TaskResult` field does not join the wire by default. A new test gate walks the tree in both directions (every minted card registered; every registered word produced), so neither direction can drift back.
|
|
54
|
+
|
|
55
|
+
- **Brain lanes disclose an unnamed tool call instead of skipping it in silence.** A `tool_use` block / `tool_calls` slot / `function_call` item arriving with NO tool name now rides the same disclosure path as a call with truncated arguments on all three lanes (anthropic / openai / open-responses): a `[note: N tool call(s) arrived with no tool name …]` block in the assistant content, and — when the turn produced nothing else usable — `stopReason: "error"` with the cause named. Previously such a turn finalized as `[{type:"text",text:""}]` with `stopReason: "stop"` and no error code, so a run whose only action was dropped reported `completed` with an empty answer. **Consumer flips**: a probe pinning the silent empty-`completed` shape reds; a probe asserting assistant content exact-equality gains a note block; "dropped tool call" is now two-cause (`not valid JSON` truncation | `no tool name`) — judge by the named cause, never by the single historic wording; the two causes may be joined with `" | "` in one errorMessage.
|
|
56
|
+
- **`StrategyStore` refuses non-honorable knobs instead of folding them to 0.** `find(scope, query, limit)` requires a non-negative integer or `Infinity` — `Infinity` now means "no cap" and returns everything (it used to return NOTHING); NaN/negative/fractional throw `config.strategy_find_limit_invalid`. `prune(scope, maxSize)` and the `InMemoryStrategyStore` constructor require a non-negative integer and refuse `Infinity` by name (a capacity cap can be widened, not turned off): `config.strategy_max_size_invalid`. A NaN capacity used to silently disable eviction (unbounded growth). Vocabulary additions (closed-set consumers admit two members): `config.strategy_find_limit_invalid`, `config.strategy_max_size_invalid`.
|
|
57
|
+
|
|
58
|
+
- **The three stop clocks gained a crossing contract: `usage_window` > `env_lifetime` > stall.** When the governance window and the environment lifetime come due at the SAME turn boundary, the window wins (loud terminal `usage.window_exhausted` + `retryAfterMs`, or a `usage_window` checkpoint with its resume hint) — it is the only cause carrying an actionable wait; env-first reporting dropped it. With an enforceable env lifetime, the ledger READ races an absolute deadline (a stalled ledger stops the run for the env and disclosingly skips window enforcement for that boundary); the CHARGE is never raced. **Probes pinning "both due ⇒ `env.lifetime_expired`" red.**
|
|
59
|
+
- **Durable-resume deferred siblings are fully paired: each `[DEFERRED]` `tool_end` is preceded by a synthetic `tool_start`** (same id/name/label, the model's real arguments recovered from the transcript, adjacent and unconditional; deliberately NOT in `startedToolCallIds` — the call never executed and interrupt-reconcile still reads it as safe-to-reissue). Exact event-count / first-frame probes on the resume leg red. Delegation tick counters count issued `tool_use` blocks (their declared semantics), so a deferred sibling's start now counts once as issued — the old shape UNDERcounted.
|
|
60
|
+
- **Manual compaction gains two mooted disclosure frames** (`compaction_outcome{mooted, manual, cancelled}` on parked-caller cancel; `{mooted, manual, task_ending}` at run settle/teardown with a parked request), and `TaskStream.compact()` called in a failed run's teardown window returns `"mooted"` instead of hanging forever. "No compaction frame on this path" probes red.
|
|
61
|
+
- **Delegation inheritance seats (one-way tighten, narrowings enumerable):** ① a read-only parent's delegated children carry NO write hands (`handsReadOnly` is a trusted ctx seat, one-way); ② a hard-headless parent's children no longer mount `AskUserQuestion` at all (the content-ask seat `onQuestion` is resolved once at prepare and handed down UNWRAPPED — parent and child share the SAME function object; identity questions read `AskQuestionRequest.sourceTaskId`/`principal`, never closure identity); ③ teacher helper/advisor legs inherit deployment auth/principal/clientContext/promptProfile plus the one-way clamps (a read-only/headless student's teacher legs lose write hands and questions); ④ the verify leg runs hard-headless always (`interactiveTools:false`) with principal + tool-face controls; ⑤ `oneShot` propagates true-ward. The child roster went from "never has AskUserQuestion" to "has it iff the parent face allows" — closed-set tool-roster probes on children red. Legs that OUTLIVE the spawning request (session-scoped/fork background, durable revival, SendMessage wake) never take turn-bound faces; resume rows take the union of clamps.
|
|
62
|
+
- **Every limits door refuses unevaluable values (`config.limit_invalid`), fourth wave of the no-silent-folding rule:** `runTeamDiscussion`'s cumulative `maxTokens`/`maxCostUsd` (NaN silently disarmed the only cumulative stop; refused before any dispatch, 0 stays an honest exhausted window); workflow governance's TRUSTED half (a baseline/`WorkflowChildCaps` NaN/Infinity read as the 0/negative "no ceiling" sentinel and could silently drop an axis — refused loudly, sentinel semantics unchanged); `TaskSpec.resourceSuspend.{totalBudgetUsd,totalTokens,maxSlices,ttlMs}` + non-empty `scope` (NaN blinded even the validated per-slice window through `Math.min`) — refused at the RB-458 door as a failed result before any resource acquisition.
|
|
63
|
+
- **Agent completed card vocabulary additions** (closed-set consumers admit members): `retryAfterMs` (machine-actionable pair of `errorCode` — `retryable` answers "re-issue as-is NOW?", `retryAfterMs` answers "when does the refusal lift"), `degraded` (reduced-quality attribution), and `modelFallback:"inherit_no_tier_binding"` (the requested model word did not bind — no roster / not on the roster — and the child ran on its inherited default; fallback behavior itself unchanged; also stamped on background roster rows). Workflow `agent_end` gains `modelResolved` (the served model, from `TaskResult.model`; `agent_start.model` stays the requested word verbatim — absent on replayed rows and model-less runs).
|
|
64
|
+
- **Two runtime prose strings changed with the retired-scenario vocabulary sweep** (an A2A fence reason and two prompt lines); probes byte-pinning them red. CC-anchored verbatim material (loop sentinels, prompt sections) is exempt by an explicit per-file allowance in the new vocabulary ratchet gate.
|
|
65
|
+
|
|
3
66
|
## 5.9.0 (2026-08-04)
|
|
4
67
|
|
|
5
68
|
### Added
|
|
@@ -528,6 +528,12 @@ export function createSendMessageTool(opts) {
|
|
|
528
528
|
...(row.createdAt !== undefined ? { rowSpawnedAt: row.createdAt } : {}),
|
|
529
529
|
...(opts.onNotifyError !== undefined ? { onNotifyError: opts.onNotifyError } : {}),
|
|
530
530
|
...(opts.notify ? { currentParentNotify: opts.notify } : {}),
|
|
531
|
+
...(ctx.onQuestion !== undefined && row.sessionScoped !== true ? { currentOnQuestion: ctx.onQuestion } : {}),
|
|
532
|
+
currentClamps: {
|
|
533
|
+
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
534
|
+
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
535
|
+
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
536
|
+
},
|
|
531
537
|
});
|
|
532
538
|
const fromPrefix = senderIsChild ? `(message from teammate "${senderLabel}")\n` : "";
|
|
533
539
|
try {
|
|
@@ -34,7 +34,7 @@ export interface SubagentWorktreeIsolation {
|
|
|
34
34
|
}
|
|
35
35
|
export declare function createSubagentWorktreeHelper(baseEnv: ExecutionEnv, repoRoot: string): SubagentWorktreeIsolation;
|
|
36
36
|
export declare const REPORT_FIELD_MAX = 300;
|
|
37
|
-
export type SubagentErrorKind = "rate_limit" | "overloaded" | "timeout" | "network" | "logic";
|
|
37
|
+
export type SubagentErrorKind = "rate_limit" | "overloaded" | "timeout" | "network" | "logic" | "governance";
|
|
38
38
|
export declare function classifySubagentError(child: {
|
|
39
39
|
status: string;
|
|
40
40
|
errorCode?: string;
|
|
@@ -43,6 +43,30 @@ export declare function classifySubagentError(child: {
|
|
|
43
43
|
errorKind: SubagentErrorKind;
|
|
44
44
|
retryable: boolean;
|
|
45
45
|
} | undefined;
|
|
46
|
+
export declare function completedAgentCard(child: {
|
|
47
|
+
taskId: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
status: string;
|
|
50
|
+
model?: string;
|
|
51
|
+
result: string;
|
|
52
|
+
salvagedOutput?: string;
|
|
53
|
+
blockedReason?: string;
|
|
54
|
+
errorMessage?: string;
|
|
55
|
+
errorCode?: string;
|
|
56
|
+
retryAfterMs?: number;
|
|
57
|
+
degraded?: unknown;
|
|
58
|
+
structuredOutput?: unknown;
|
|
59
|
+
stats: unknown;
|
|
60
|
+
}, extras: {
|
|
61
|
+
subagentType?: string;
|
|
62
|
+
error?: {
|
|
63
|
+
errorKind: SubagentErrorKind;
|
|
64
|
+
retryable: boolean;
|
|
65
|
+
} | undefined;
|
|
66
|
+
worktreePath?: string;
|
|
67
|
+
toolStats?: SubagentToolStats;
|
|
68
|
+
modelFallback?: "inherit_no_tier_binding";
|
|
69
|
+
}): Record<string, unknown>;
|
|
46
70
|
export declare const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
|
|
47
71
|
export declare const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
|
|
48
72
|
export declare const SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED = "suspended.checkpoint_expired";
|
|
@@ -66,6 +90,12 @@ export declare function createSubagentResume(deps: {
|
|
|
66
90
|
currentParentNotify?: (n: TaskNotificationPayload, opts?: {
|
|
67
91
|
priority?: "now" | "next" | "later";
|
|
68
92
|
}) => void;
|
|
93
|
+
currentOnQuestion?: import("../core/ask-question.js").OnQuestion;
|
|
94
|
+
currentClamps?: {
|
|
95
|
+
handsReadOnly?: true;
|
|
96
|
+
interactiveTools?: false;
|
|
97
|
+
oneShot?: true;
|
|
98
|
+
};
|
|
69
99
|
taskId?: string;
|
|
70
100
|
taskAccess?: import("../core/task-registry.js").TaskAccess;
|
|
71
101
|
bgSink?: (event: import("../core/types.js").BackgroundChildEvent) => void;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -229,8 +229,33 @@ export function classifySubagentError(child) {
|
|
|
229
229
|
? "timeout"
|
|
230
230
|
: code === "network" || code === "server"
|
|
231
231
|
? "network"
|
|
232
|
-
: "
|
|
233
|
-
|
|
232
|
+
: code !== undefined && code.startsWith("usage.")
|
|
233
|
+
? "governance"
|
|
234
|
+
: "logic";
|
|
235
|
+
return { errorKind, retryable: errorKind !== "logic" && errorKind !== "governance" };
|
|
236
|
+
}
|
|
237
|
+
export function completedAgentCard(child, extras) {
|
|
238
|
+
return {
|
|
239
|
+
type: "agent",
|
|
240
|
+
...(extras.subagentType !== undefined ? { subagent_type: extras.subagentType } : {}),
|
|
241
|
+
status: child.status,
|
|
242
|
+
taskId: child.taskId,
|
|
243
|
+
sessionId: child.sessionId,
|
|
244
|
+
result: child.result,
|
|
245
|
+
...(child.salvagedOutput !== undefined ? { salvagedOutput: child.salvagedOutput } : {}),
|
|
246
|
+
...(child.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : {}),
|
|
247
|
+
...(child.blockedReason !== undefined ? { blockedReason: child.blockedReason } : {}),
|
|
248
|
+
...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}),
|
|
249
|
+
...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}),
|
|
250
|
+
...(child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
|
|
251
|
+
...(child.degraded !== undefined ? { degraded: child.degraded } : {}),
|
|
252
|
+
...(extras.error ? { error_kind: extras.error.errorKind, retryable: extras.error.retryable } : {}),
|
|
253
|
+
stats: child.stats,
|
|
254
|
+
...(child.model !== undefined ? { model: child.model, resolvedModel: child.model } : {}),
|
|
255
|
+
...(extras.modelFallback !== undefined ? { modelFallback: extras.modelFallback } : {}),
|
|
256
|
+
...(extras.worktreePath !== undefined ? { worktreePath: extras.worktreePath } : {}),
|
|
257
|
+
...(extras.toolStats !== undefined ? { toolStats: extras.toolStats } : {}),
|
|
258
|
+
};
|
|
234
259
|
}
|
|
235
260
|
export const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
|
|
236
261
|
export const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
|
|
@@ -374,6 +399,10 @@ export function createSubagentResume(deps) {
|
|
|
374
399
|
objective: createResumePrompt(marker, content),
|
|
375
400
|
sessionId: entry.childSessionId,
|
|
376
401
|
requireExistingSession: true,
|
|
402
|
+
...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
|
|
403
|
+
...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
404
|
+
...(entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
405
|
+
...(entry.specSnapshot.oneShot === true || deps.currentClamps?.oneShot === true ? { oneShot: true } : {}),
|
|
377
406
|
signal: abort.signal,
|
|
378
407
|
};
|
|
379
408
|
if (deps.registry !== undefined && deps.taskId !== undefined && deps.taskAccess !== undefined) {
|
|
@@ -880,7 +909,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
880
909
|
`- Finding a known symbol/class/definition, or searching within 2-3 known files → use Grep/Glob/Read.\n` +
|
|
881
910
|
`- A lookup where you already know what you want and roughly where it is → just do it. Delegate open-ended INVESTIGATIONS (you don't know where the answer is), not lookups.\n\n` +
|
|
882
911
|
`Writing the 'prompt' — brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters:\n` +
|
|
883
|
-
`- Make it fully self-contained and highly detailed: the sub-agent does the whole task
|
|
912
|
+
`- Make it fully self-contained and highly detailed: the sub-agent does the whole task end-to-end in one shot — you can't course-correct mid-run.\n` +
|
|
884
913
|
`- State exactly what it must RETURN (e.g. "report the file:line of each call site", not "look into the call sites").\n` +
|
|
885
914
|
`- Say explicitly whether you want it to WRITE CODE or only RESEARCH/REPORT — otherwise it may guess wrong.\n` +
|
|
886
915
|
`- Never delegate understanding. Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.\n\n` +
|
|
@@ -1071,6 +1100,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1071
1100
|
}
|
|
1072
1101
|
}
|
|
1073
1102
|
}
|
|
1103
|
+
const modelFallback = !wantsFork && requestedModel !== undefined && perCallModel === undefined ? "inherit_no_tier_binding" : undefined;
|
|
1074
1104
|
let worktreeDir;
|
|
1075
1105
|
if (reviveClaim === undefined && (a.isolation ?? def?.isolation) === "worktree") {
|
|
1076
1106
|
const iso = ctx.worktreeIsolation;
|
|
@@ -1333,11 +1363,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1333
1363
|
};
|
|
1334
1364
|
const drainAndReleaseObserver = async () => {
|
|
1335
1365
|
if (observerPairing) {
|
|
1366
|
+
let drainRescue;
|
|
1336
1367
|
const timeout = new Promise((resolve) => {
|
|
1337
|
-
|
|
1338
|
-
t.unref?.();
|
|
1368
|
+
drainRescue = setTimeout(() => resolve("timeout"), 60_000);
|
|
1339
1369
|
});
|
|
1340
|
-
|
|
1370
|
+
let outcome;
|
|
1371
|
+
try {
|
|
1372
|
+
outcome = await Promise.race([observerPairing.drain().then(() => "drained"), timeout]);
|
|
1373
|
+
}
|
|
1374
|
+
finally {
|
|
1375
|
+
clearTimeout(drainRescue);
|
|
1376
|
+
}
|
|
1341
1377
|
observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
|
|
1342
1378
|
if (outcome === "timeout")
|
|
1343
1379
|
noteObserverFailure(new Error("observer unresponsive at settle (delivery drain timed out)"));
|
|
@@ -1456,13 +1492,19 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1456
1492
|
...(def?.memory ? { memory: def.memory } : {}),
|
|
1457
1493
|
...(def?.skills?.length ? { skills: def.skills } : {}),
|
|
1458
1494
|
...(ctx.principal !== undefined ? { principal: ctx.principal } : {}),
|
|
1495
|
+
...(ctx.checkpointStoreDisabledForChildren === true ? { checkpointStore: null } : {}),
|
|
1459
1496
|
...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
|
|
1497
|
+
...(ctx.onQuestion !== undefined ? { onQuestion: ctx.onQuestion } : {}),
|
|
1498
|
+
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
1499
|
+
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
1500
|
+
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
1460
1501
|
...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
|
|
1461
1502
|
...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
|
|
1462
1503
|
...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
|
|
1463
1504
|
...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
|
|
1464
1505
|
...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
|
|
1465
1506
|
...(ctx.additionalDirectories !== undefined ? { additionalDirectories: [...ctx.additionalDirectories] } : {}),
|
|
1507
|
+
...(ctx.additionalReadDirectories !== undefined ? { additionalReadDirectories: [...ctx.additionalReadDirectories] } : {}),
|
|
1466
1508
|
...(ctx.envFacts !== undefined ? { envFacts: { ...ctx.envFacts } } : {}),
|
|
1467
1509
|
...(ctx.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: ctx.getApiKeyAndHeaders } : {}),
|
|
1468
1510
|
enableBlockedReport: true,
|
|
@@ -1473,7 +1515,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1473
1515
|
if (!ledger)
|
|
1474
1516
|
return undefined;
|
|
1475
1517
|
const childSessionId = preMintedSessionId ?? uuidv7();
|
|
1476
|
-
const { signal: _spawnSignal, ...plainSpec } = buildChildSpec(undefined);
|
|
1518
|
+
const { signal: _spawnSignal, onQuestion: _spawnTurnQuestionFace, ...plainSpec } = buildChildSpec(undefined);
|
|
1477
1519
|
let entry = ledger.register(ctx.toolCallId, {
|
|
1478
1520
|
childSessionId,
|
|
1479
1521
|
runner: opts.runner,
|
|
@@ -1553,7 +1595,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1553
1595
|
if (ledger !== undefined && !ledger.disposed) {
|
|
1554
1596
|
if (opts.background)
|
|
1555
1597
|
ensureSessionReapHook(opts.background.registry);
|
|
1556
|
-
const { signal: _spawnSignal, ...plainSpec } = buildChildSpec(undefined);
|
|
1598
|
+
const { signal: _spawnSignal, onQuestion: _spawnTurnQuestionFace, ...plainSpec } = buildChildSpec(undefined);
|
|
1557
1599
|
const entry = ledger.register(ctx.toolCallId, {
|
|
1558
1600
|
childSessionId,
|
|
1559
1601
|
runner: opts.runner,
|
|
@@ -1671,6 +1713,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1671
1713
|
else if (!sessionScopedBg)
|
|
1672
1714
|
ctx.signal.addEventListener("abort", onHostAbort, { once: true });
|
|
1673
1715
|
}
|
|
1716
|
+
const dropTurnBoundQuestionFace = sessionScopedBg ? { onQuestion: undefined } : {};
|
|
1674
1717
|
const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
|
|
1675
1718
|
const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
|
|
1676
1719
|
const bgScope = treeScope;
|
|
@@ -1735,7 +1778,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1735
1778
|
}, { priority: "later" }), "subagent.reapTerminalNotify");
|
|
1736
1779
|
});
|
|
1737
1780
|
if (agentName !== undefined) {
|
|
1738
|
-
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
1781
|
+
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
1739
1782
|
}
|
|
1740
1783
|
const bgSink = ctx.onBackgroundChildEvent;
|
|
1741
1784
|
const sinkEmit = (event) => {
|
|
@@ -1763,7 +1806,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1763
1806
|
startedAt: Date.now(),
|
|
1764
1807
|
});
|
|
1765
1808
|
const forkBgStartedAt = Date.now();
|
|
1766
|
-
const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective };
|
|
1809
|
+
const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective, ...dropTurnBoundQuestionFace };
|
|
1767
1810
|
const s2ForkNotifyReady = (inject) => {
|
|
1768
1811
|
bg.registry.attachAgentNotify(taskId, inject);
|
|
1769
1812
|
};
|
|
@@ -2039,16 +2082,15 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2039
2082
|
].filter(Boolean);
|
|
2040
2083
|
return {
|
|
2041
2084
|
content: forkLines.join("\n"),
|
|
2042
|
-
details: {
|
|
2043
|
-
|
|
2044
|
-
|
|
2085
|
+
details: completedAgentCard(forkChild, {
|
|
2086
|
+
subagentType: FORK_SUBAGENT_TYPE,
|
|
2087
|
+
error: forkErr,
|
|
2045
2088
|
...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
|
|
2046
|
-
...(forkChild.model !== undefined ? { resolvedModel: forkChild.model } : {}),
|
|
2047
2089
|
...(() => {
|
|
2048
2090
|
const ts = toolStatsCounter.snapshot();
|
|
2049
2091
|
return ts !== undefined ? { toolStats: ts } : {};
|
|
2050
2092
|
})(),
|
|
2051
|
-
},
|
|
2093
|
+
}),
|
|
2052
2094
|
};
|
|
2053
2095
|
}
|
|
2054
2096
|
if (wantsBackground) {
|
|
@@ -2058,12 +2100,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2058
2100
|
const reviveRow = reviveClaim?.row;
|
|
2059
2101
|
const parkedResume = reviveClaim?.parkedResume;
|
|
2060
2102
|
const sessionScopedBg = reviveRow !== undefined ? reviveRow.sessionScoped : ctx.backgroundScope === "session" && ctx.sessionId !== undefined;
|
|
2103
|
+
const outlivesThisRequest = sessionScopedBg || reviveRow !== undefined;
|
|
2061
2104
|
const onHostAbort = () => abort.abort();
|
|
2062
2105
|
const dropHostAbortListener = () => ctx.signal?.removeEventListener("abort", onHostAbort);
|
|
2063
2106
|
if (ctx.signal) {
|
|
2064
2107
|
if (ctx.signal.aborted)
|
|
2065
2108
|
abort.abort();
|
|
2066
|
-
else if (!
|
|
2109
|
+
else if (!outlivesThisRequest)
|
|
2067
2110
|
ctx.signal.addEventListener("abort", onHostAbort, { once: true });
|
|
2068
2111
|
}
|
|
2069
2112
|
const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
|
|
@@ -2229,6 +2272,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2229
2272
|
sessionId: bgChildSessionId,
|
|
2230
2273
|
...(forwardDurableApproval ? { durableApproval: { ...ctx.durableApprovalForChildren } } : {}),
|
|
2231
2274
|
...(reviveRow !== undefined ? { requireExistingSession: true } : {}),
|
|
2275
|
+
...(outlivesThisRequest ? { onQuestion: undefined } : {}),
|
|
2232
2276
|
};
|
|
2233
2277
|
let reviveAttachedResolve;
|
|
2234
2278
|
const reviveAttached = reviveRow !== undefined ? new Promise((r) => (reviveAttachedResolve = r)) : undefined;
|
|
@@ -2526,6 +2570,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2526
2570
|
...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : unparkedPauseReason ?? child.errorMessage ?? String(child.status) } : {}),
|
|
2527
2571
|
...(errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
|
|
2528
2572
|
...(errClassBg !== undefined ? { retryable: errClassBg.retryable, errorKind: errClassBg.errorKind } : {}),
|
|
2573
|
+
...(failedBg && child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
|
|
2529
2574
|
}) ??
|
|
2530
2575
|
(abort.signal.aborted ? "killed" : ok ? "completed" : "failed");
|
|
2531
2576
|
if (!bgRetain && reviveRow === undefined && !(await bgRowConfirmed())) {
|
|
@@ -2729,6 +2774,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2729
2774
|
sink: ctx.onSubagentSpawn,
|
|
2730
2775
|
...(opts.background ? { registry: opts.background.registry } : {}),
|
|
2731
2776
|
...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
|
|
2777
|
+
...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
|
|
2732
2778
|
});
|
|
2733
2779
|
notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
|
|
2734
2780
|
resume,
|
|
@@ -2854,16 +2900,16 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2854
2900
|
].filter(Boolean);
|
|
2855
2901
|
return {
|
|
2856
2902
|
content: lines.join("\n"),
|
|
2857
|
-
details: {
|
|
2858
|
-
...
|
|
2859
|
-
|
|
2903
|
+
details: completedAgentCard(child, {
|
|
2904
|
+
...(def !== undefined ? { subagentType: def.name } : {}),
|
|
2905
|
+
error: errClass,
|
|
2906
|
+
...(modelFallback !== undefined ? { modelFallback } : {}),
|
|
2860
2907
|
...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
|
|
2861
|
-
...(child.model !== undefined ? { resolvedModel: child.model } : {}),
|
|
2862
2908
|
...(() => {
|
|
2863
2909
|
const ts = toolStatsCounter.snapshot();
|
|
2864
2910
|
return ts !== undefined ? { toolStats: ts } : {};
|
|
2865
2911
|
})(),
|
|
2866
|
-
},
|
|
2912
|
+
}),
|
|
2867
2913
|
};
|
|
2868
2914
|
},
|
|
2869
2915
|
};
|
package/dist/agents/teacher.js
CHANGED
|
@@ -174,9 +174,21 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
174
174
|
};
|
|
175
175
|
};
|
|
176
176
|
const tools = studentSpec.tools?.map(wrap);
|
|
177
|
+
const inheritedDeploymentConfig = {
|
|
178
|
+
...(studentSpec.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: studentSpec.getApiKeyAndHeaders } : {}),
|
|
179
|
+
...(studentSpec.principal !== undefined ? { principal: studentSpec.principal } : {}),
|
|
180
|
+
...(studentSpec.clientContext !== undefined ? { clientContext: studentSpec.clientContext } : {}),
|
|
181
|
+
...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
|
|
182
|
+
...(studentSpec.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
183
|
+
...(studentSpec.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
184
|
+
...(studentSpec.excludeTools !== undefined ? { excludeTools: [...studentSpec.excludeTools] } : {}),
|
|
185
|
+
...(studentSpec.deferTools !== undefined ? { deferTools: [...studentSpec.deferTools] } : {}),
|
|
186
|
+
...(studentSpec.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...studentSpec.alwaysLoadTools] } : {}),
|
|
187
|
+
...(studentSpec.checkpointStore === null ? { checkpointStore: null } : {}),
|
|
188
|
+
};
|
|
177
189
|
const helperBase = () => teacher.helperModel
|
|
178
|
-
? { model: teacher.helperModel }
|
|
179
|
-
: { model: studentSpec.model, modelRole: studentSpec.modelRole, roles: studentSpec.roles };
|
|
190
|
+
? { ...inheritedDeploymentConfig, model: teacher.helperModel }
|
|
191
|
+
: { ...inheritedDeploymentConfig, model: studentSpec.model, modelRole: studentSpec.modelRole, roles: studentSpec.roles };
|
|
180
192
|
const teacherModelFields = () => {
|
|
181
193
|
if (teacher.model) {
|
|
182
194
|
return { model: teacher.model };
|
|
@@ -317,7 +329,6 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
317
329
|
let timer;
|
|
318
330
|
const promise = new Promise((res) => {
|
|
319
331
|
timer = setTimeout(() => res(syntheticAborted(sid)), 10_000);
|
|
320
|
-
timer.unref?.();
|
|
321
332
|
});
|
|
322
333
|
return { promise, cancel: () => { if (timer)
|
|
323
334
|
clearTimeout(timer); } };
|
|
@@ -338,6 +349,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
338
349
|
const askTeacher = async (trace) => {
|
|
339
350
|
const r = await runner.runTask({
|
|
340
351
|
objective: trace,
|
|
352
|
+
...inheritedDeploymentConfig,
|
|
341
353
|
...teacherModelFields(),
|
|
342
354
|
systemPrompt: teacher.prompts?.teacher ?? TEACHER_PROMPT,
|
|
343
355
|
enableBlockedReport: false,
|
package/dist/agents/team.js
CHANGED
|
@@ -51,6 +51,16 @@ export async function runTeamDiscussion(opts) {
|
|
|
51
51
|
let turns = 0;
|
|
52
52
|
let costMicroUsd = 0;
|
|
53
53
|
let failures = 0;
|
|
54
|
+
for (const key of ["maxTokens", "maxCostUsd"]) {
|
|
55
|
+
const value = opts.limits?.[key];
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
59
|
+
const e = new Error(`TeamDiscussionOptions.limits.${key} must be a finite, non-negative number (got ${String(value)}) — the team's cumulative budget gate cannot evaluate it, and running without the gate would spend under a limit nobody chose.`);
|
|
60
|
+
e.code = "config.limit_invalid";
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
54
64
|
const budgetMaxTokens = opts.limits?.maxTokens;
|
|
55
65
|
const budgetMaxCostMicroUsd = opts.limits?.maxCostUsd !== undefined ? Math.round(opts.limits.maxCostUsd * 1_000_000) : undefined;
|
|
56
66
|
let budgetStop;
|
package/dist/agents/verify.js
CHANGED
|
@@ -99,10 +99,17 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
99
99
|
roles: specBase.roles,
|
|
100
100
|
tools: verifierTools,
|
|
101
101
|
handsReadOnly: config.verifierHandsReadOnly ?? true,
|
|
102
|
+
interactiveTools: false,
|
|
102
103
|
outputSchema: VerdictSchema,
|
|
103
104
|
enableBlockedReport: false,
|
|
104
105
|
limits: { ...(specBase.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: specBase.limits.maxWalltimeMs } : {}) },
|
|
105
106
|
getApiKeyAndHeaders: specBase.getApiKeyAndHeaders,
|
|
107
|
+
...(specBase.principal !== undefined ? { principal: specBase.principal } : {}),
|
|
108
|
+
...(specBase.excludeTools !== undefined ? { excludeTools: [...specBase.excludeTools] } : {}),
|
|
109
|
+
...(specBase.deferTools !== undefined ? { deferTools: [...specBase.deferTools] } : {}),
|
|
110
|
+
...(specBase.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...specBase.alwaysLoadTools] } : {}),
|
|
111
|
+
...(specBase.clientContext !== undefined ? { clientContext: { ...specBase.clientContext } } : {}),
|
|
112
|
+
...(specBase.promptProfile !== undefined ? { promptProfile: specBase.promptProfile } : {}),
|
|
106
113
|
signal: specBase.signal,
|
|
107
114
|
});
|
|
108
115
|
try {
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -516,6 +516,7 @@ export function createAnthropicBrain(config = {}) {
|
|
|
516
516
|
const finalContent = [];
|
|
517
517
|
const toolCalls = [];
|
|
518
518
|
const malformed = [];
|
|
519
|
+
const unnamed = [];
|
|
519
520
|
let anyText = false;
|
|
520
521
|
let reasoningSeen = false;
|
|
521
522
|
for (const [, acc] of [...blocks.entries()].sort((a, b) => a[0] - b[0])) {
|
|
@@ -537,28 +538,44 @@ export function createAnthropicBrain(config = {}) {
|
|
|
537
538
|
anyText = true;
|
|
538
539
|
}
|
|
539
540
|
}
|
|
540
|
-
else if (acc.type === "tool_use"
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
toolCalls.push(tc);
|
|
544
|
-
finalContent.push(tc);
|
|
541
|
+
else if (acc.type === "tool_use") {
|
|
542
|
+
if (!acc.toolName) {
|
|
543
|
+
unnamed.push(`id="${acc.toolId || "?"}"(${acc.toolJson.slice(0, 200)})`);
|
|
545
544
|
}
|
|
546
545
|
else {
|
|
547
|
-
|
|
546
|
+
const tc = closeToolUseBlock(acc);
|
|
547
|
+
if (tc) {
|
|
548
|
+
toolCalls.push(tc);
|
|
549
|
+
finalContent.push(tc);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
malformed.push(`${acc.toolName}(${acc.toolJson.slice(0, 200)})`);
|
|
553
|
+
}
|
|
548
554
|
}
|
|
549
555
|
}
|
|
550
556
|
}
|
|
551
557
|
const doneReason = mapStopReason(stopReason, toolCalls.length > 0);
|
|
552
558
|
const noUsableContent = toolCalls.length === 0 && !anyText;
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
:
|
|
556
|
-
|
|
559
|
+
const toolErrorParts = [];
|
|
560
|
+
if (malformed.length > 0) {
|
|
561
|
+
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, stop_reason="${stopReason ?? "?"}"${sentMaxTokens !== undefined ? `; request output cap in effect: ${sentMaxTokens} tokens — a cut mid-arguments commonly means the cap was hit` : ""}): ${malformed.join("; ")}`);
|
|
562
|
+
}
|
|
563
|
+
if (unnamed.length > 0) {
|
|
564
|
+
toolErrorParts.push(`tool call(s) arrived with no tool name and cannot be executed (stop_reason="${stopReason ?? "?"}"): ${unnamed.join("; ")}`);
|
|
565
|
+
}
|
|
566
|
+
const toolError = toolErrorParts.length > 0 ? toolErrorParts.join(" | ") : undefined;
|
|
567
|
+
if (malformed.length > 0) {
|
|
557
568
|
finalContent.push({
|
|
558
569
|
type: "text",
|
|
559
570
|
text: `\n[note: ${malformed.length} tool call(s) were truncated (stop_reason="${stopReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
|
|
560
571
|
});
|
|
561
572
|
}
|
|
573
|
+
if (unnamed.length > 0) {
|
|
574
|
+
finalContent.push({
|
|
575
|
+
type: "text",
|
|
576
|
+
text: `\n[note: ${unnamed.length} tool call(s) arrived with no tool name and were dropped — re-issue them next turn with an explicit tool name: ${unnamed.join("; ")}]`,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
562
579
|
if (malformedFrames > 0) {
|
|
563
580
|
finalContent.push({
|
|
564
581
|
type: "text",
|
|
@@ -599,6 +599,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
599
599
|
const finalContent = [];
|
|
600
600
|
const toolCalls = [];
|
|
601
601
|
const malformed = [];
|
|
602
|
+
const unnamed = [];
|
|
602
603
|
let anyText = false;
|
|
603
604
|
let reasoningSeen = false;
|
|
604
605
|
for (const [, acc] of [...items.entries()].sort((a, b) => a[0] - b[0])) {
|
|
@@ -629,19 +630,33 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
629
630
|
else if (acc.name) {
|
|
630
631
|
malformed.push(`${acc.name}(${acc.args.slice(0, MALFORMED_ARGS_CHARS)})`);
|
|
631
632
|
}
|
|
633
|
+
else {
|
|
634
|
+
unnamed.push(`call_id="${acc.callId || "?"}"(${acc.args.slice(0, MALFORMED_ARGS_CHARS)})`);
|
|
635
|
+
}
|
|
632
636
|
}
|
|
633
637
|
}
|
|
634
638
|
const noUsableContent = toolCalls.length === 0 && !anyText;
|
|
635
639
|
const terminalLabel = terminalStatus ?? "?";
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
:
|
|
639
|
-
|
|
640
|
+
const toolErrorParts = [];
|
|
641
|
+
if (malformed.length > 0) {
|
|
642
|
+
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, status="${terminalLabel}"): ${malformed.join("; ")}`);
|
|
643
|
+
}
|
|
644
|
+
if (unnamed.length > 0) {
|
|
645
|
+
toolErrorParts.push(`tool call(s) arrived with no tool name and cannot be executed (status="${terminalLabel}"): ${unnamed.join("; ")}`);
|
|
646
|
+
}
|
|
647
|
+
const toolError = toolErrorParts.length > 0 ? toolErrorParts.join(" | ") : undefined;
|
|
648
|
+
if (malformed.length > 0) {
|
|
640
649
|
finalContent.push({
|
|
641
650
|
type: "text",
|
|
642
651
|
text: `\n[note: ${malformed.length} tool call(s) were truncated (status="${terminalLabel}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
|
|
643
652
|
});
|
|
644
653
|
}
|
|
654
|
+
if (unnamed.length > 0) {
|
|
655
|
+
finalContent.push({
|
|
656
|
+
type: "text",
|
|
657
|
+
text: `\n[note: ${unnamed.length} tool call(s) arrived with no tool name and were dropped — re-issue them next turn with an explicit tool name: ${unnamed.join("; ")}]`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
645
660
|
if (malformedFrames > 0) {
|
|
646
661
|
finalContent.push({
|
|
647
662
|
type: "text",
|