@sema-agent/core 5.7.0 → 5.9.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 +79 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/cascade.js +24 -0
- package/dist/agents/subagent.d.ts +7 -7
- package/dist/agents/subagent.js +74 -33
- package/dist/agents/teacher.js +3 -3
- package/dist/agents/team.d.ts +1 -1
- package/dist/agents/team.js +1 -1
- package/dist/agents/verify.js +1 -1
- package/dist/brain/anthropic.js +24 -35
- package/dist/brain/open-responses.d.ts +11 -0
- package/dist/brain/open-responses.js +721 -0
- package/dist/brain/openai.js +22 -33
- package/dist/brain/request-params.d.ts +1 -0
- package/dist/brain/request-params.js +16 -0
- package/dist/brain/stream-engine.d.ts +0 -2
- package/dist/brain/stream-engine.js +5 -47
- package/dist/brain/stream-shared.d.ts +0 -10
- package/dist/brain/stream-shared.js +0 -23
- package/dist/brain/terminal-cause.d.ts +0 -1
- package/dist/brain/terminal-cause.js +0 -3
- package/dist/brain/timeout.d.ts +4 -2
- package/dist/brain/timeout.js +4 -14
- package/dist/config/catalog.d.ts +3 -11
- package/dist/config/catalog.js +30 -69
- package/dist/config/defaults.d.ts +0 -3
- package/dist/config/defaults.js +0 -3
- package/dist/core/auto-compaction.d.ts +2 -11
- package/dist/core/auto-compaction.js +3 -67
- package/dist/core/checkpoint-store.d.ts +10 -8
- package/dist/core/checkpoint-store.js +7 -7
- package/dist/core/fs-write-gate-policy.js +2 -2
- package/dist/core/lsp-diagnostics.d.ts +3 -2
- package/dist/core/lsp-diagnostics.js +20 -7
- package/dist/core/memory-engine/file-backend.js +20 -17
- package/dist/core/memory-engine/layout.d.ts +5 -0
- package/dist/core/memory-engine/layout.js +72 -47
- package/dist/core/runner/assemble-result.d.ts +3 -8
- package/dist/core/runner/assemble-result.js +24 -18
- package/dist/core/runner/prepare-task.d.ts +27 -6
- package/dist/core/runner/prepare-task.js +209 -144
- package/dist/core/runner/runtask.d.ts +0 -2
- package/dist/core/runner/runtask.js +228 -266
- package/dist/core/runner/session-rule-policy.d.ts +1 -0
- package/dist/core/runner/session-rule-policy.js +4 -3
- package/dist/core/session-policy-store.d.ts +5 -0
- package/dist/core/session-policy-store.js +3 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
- package/dist/core/task-registry-agent.d.ts +2 -0
- package/dist/core/task-registry-agent.js +33 -1
- package/dist/core/task-registry-shared.d.ts +0 -1
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/task-registry.js +9 -1
- package/dist/core/tool-errors.js +10 -2
- package/dist/core/tool-policy.d.ts +8 -0
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/trace.d.ts +1 -10
- package/dist/core/types.d.ts +27 -30
- package/dist/core/usage-window-store.d.ts +39 -0
- package/dist/core/usage-window-store.js +115 -0
- package/dist/engine/execution-env/node-execution-env.js +0 -22
- package/dist/engine/harness/agent-harness.d.ts +1 -7
- package/dist/engine/harness/agent-harness.js +16 -53
- package/dist/engine/harness/types.d.ts +3 -15
- package/dist/engine/llm/types.d.ts +3 -75
- package/dist/engine/loop/agent-loop.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +7 -173
- package/dist/engine/loop/types.d.ts +1 -18
- package/dist/engine/session/repo-utils.d.ts +1 -2
- package/dist/engine/session/repo-utils.js +0 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +10 -5
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/goal.js +1 -5
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +1 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -6
- package/dist/orchestration/workflow-governance.js +33 -47
- package/dist/orchestration/workflow.js +14 -5
- package/dist/prompt-assembly/event-registry.js +2 -3
- package/dist/stores/cc/lockfile.js +9 -11
- package/dist/stores/cc/mailbox-store.js +49 -38
- package/dist/stores/file/index.d.ts +3 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/usage-window-store.d.ts +9 -0
- package/dist/stores/file/usage-window-store.js +86 -0
- package/dist/tools/fs/fs-bash.d.ts +1 -10
- package/dist/tools/fs/fs-bash.js +20 -60
- package/dist/tools/fs/index.d.ts +0 -2
- package/dist/tools/fs/index.js +1 -2
- package/dist/tools/web.js +20 -19
- package/package.json +4 -5
- package/dist/bin/sema-tb.d.ts +0 -31
- package/dist/bin/sema-tb.js +0 -448
- package/dist/bin/tb-env.d.ts +0 -2
- package/dist/bin/tb-env.js +0 -17
- package/dist/brain/walltime.d.ts +0 -1
- package/dist/brain/walltime.js +0 -1
- package/dist/core/runner/call-cap.d.ts +0 -67
- package/dist/core/runner/call-cap.js +0 -145
- package/dist/core/runner/cut-kill.d.ts +0 -10
- package/dist/core/runner/cut-kill.js +0 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.9.0 (2026-08-04)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A delegated child's permission ask reaches the parent's approval channel.** The parent run's effective approver (`spec.onAsk ?? deps.onAsk`) rides the trusted tool context into every child spec (sync / background / fork lanes share the one construction point), wrapped once with frozen provenance: `AskRequest.delegation` (new, optional, readonly) carries `{ parentToolCallId, depth, agentName? }` so an approval UI can render *which* delegation is asking — the innermost (true-origin) frame wins on grandchildren. New exported type `AskDelegationProvenance`. An approver is consulted once per gated call: the single-frame dedup compares approver identity *through* the wrapper (`askApproverIdentity`), so the provenance wrapper cannot split one consultation into two approval cards. **Consumer flips**: a probe pinning "a child's ask resolves at `deps.onAsk` when the parent supplied `spec.onAsk`" now resolves at the parent's approver; the `delegation` field is additive and ignorable.
|
|
8
|
+
- **`tool_end.errorCode` (new, optional, additive)** — present iff `isError` is true and the harness result's `details` carried a string `code`; a machine-readable discriminator so a consumer never parses the result text. Engine vocabulary today: `"tool.not_found"` and `"gate.parked"` (an abort short-circuit poisoned this call because a durable gate parked the batch — the "Operation aborted" family; a plain user interrupt carries NO marker, pinned both ways). The abort texts are byte-identical to before — an exact-equality anchor on `"Operation aborted"` is unaffected. Frame-shape change, additive: this is the [2513]-process disclosure. Library embedders get the seam directly as `abortResultDetails` on the harness options / loop config.
|
|
9
|
+
|
|
10
|
+
- `createOpenResponsesBrain` (+ `OpenResponsesBrainConfig`) — a Brain for the Open Responses wire form (`POST /responses`: typed input/output items, an SSE stream terminated by `response.completed` / `response.incomplete` / `response.failed` with no `[DONE]` sentinel). One adapter for the whole family of endpoints that speak it, instead of one per vendor. It is **stateless by contract**: `store` / `previous_response_id` / `conversation` are never sent, cannot be injected through `Model.extraBody` (all three are reserved for this API family), and every turn replays the full transcript — this engine stays the single transcript authority. Reasoning arrives as first-class `reasoning` items and is stored as normal `thinking` blocks (item id in `thinkingSignature`), replayed on tool-call turns. Terminal events map onto the existing vocabulary — no new `errorKind` or brain-error code is minted: a `max_output_tokens` cut with no answer text is `length_empty`, a content-filter cut and a streamed refusal are `[refusal]`, a stream with no terminal event is `[stream_torn]`, and `response.failed` maps by code onto `rate_limit` / `auth` / `invalid_request` / `server`. Because the wire form offers no capability discovery and endpoints have been observed to accept-and-ignore parameters, the adapter audits the `response.created` echo against what it requested and records divergences (state held that was never asked for, a reasoning tier not honored, serialized tool calls) as a `responses_capability` diagnostic on the final message. `StreamOptions.stop` has no field in this wire form and is REFUSED rather than silently dropped. Type-surface correction from the landing review: `OpenAIResponsesCompat` now declares exactly the knobs the adapter consumes (`supportsReasoningEffort` / `reasoningEffortLevels`); its two former fields (`sendSessionIdHeader` / `supportsLongCacheRetention`) had zero consumers anywhere in the engine and are removed — type-level only, no runtime behavior existed behind them.
|
|
11
|
+
|
|
12
|
+
### BREAKING
|
|
13
|
+
|
|
14
|
+
- **Declared-only fields removed from the public type surface (type-level only — no runtime behavior existed behind any of them).** A new gate, `gate:field-liveness`, walks every interface reachable from `src/index.ts` (directly exported, or reachable through a field of something that is) and reds on a member no production code under `src/` mentions. Its first run found 59; 34 are removed here. Setting any of them compiled, type-checked, and did nothing.
|
|
15
|
+
- `OpenAICompletionsCompat`: `supportsStore`, `supportsDeveloperRole`, `supportsUsageInStreaming`, `requiresToolResultName`, `requiresAssistantAfterToolResult`, `requiresThinkingAsText`, `openRouterRouting`, `vercelGatewayRouting`, `zaiToolStream`, `supportsStrictMode`, `cacheControlFormat`, `sendSessionAffinityHeaders`, `supportsLongCacheRetention`. Several documented a "Default: auto-detected from URL" that no code performs. The knobs the completions brain does read are unchanged: `supportsReasoningEffort`, `reasoningEffortLevels`, `maxTokensField`, `requiresReasoningContentOnAssistantMessages`, `thinkingFormat`.
|
|
16
|
+
- `AnthropicMessagesCompat`: `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`, `sendSessionAffinityHeaders`, `supportsCacheControlOnTools`. The Anthropic brain performs each of those behaviours unconditionally or not at all, so declaring the opposite changed nothing. `thinkingMode`, `effortLevels`, `supportsTemperature`, `contextManagement`, `interleavedThinking` are unchanged.
|
|
17
|
+
- `OpenRouterRouting` and `VercelGatewayRouting` are removed entirely: both existed only as the types of the two removed routing fields, so nothing they described (fallbacks, ZDR, quantizations, price ceilings, throughput/latency floors, provider order) was ever sent on any request.
|
|
18
|
+
- `TextContent.textSignature` and its payload type `TextSignatureV1` — no brain wrote it and no replay path read it. The live equivalent is `ThinkingContent.thinkingSignature`.
|
|
19
|
+
- `ToolCall.thoughtSignature` — Google-specific, and there is no Google brain in this tree.
|
|
20
|
+
- `Model.mediaInput` — claimed to carry provider media limits "used by attachment preprocessing"; the image pipeline's caps come from `RunnerDeps.mediaByteCapBytes` and the resizer seam.
|
|
21
|
+
- `TraceEvent` (`kind: "brain.call"`): `callCap` and `capThinkingSkipped`, orphans of the design/130 per-call deadline shrink retired in 5.8.0 — the emitter went with the mechanism, the declaration stayed. `stopReason` is unchanged (its JSDoc no longer instructs consumers to judge cap binding by a field that is never emitted).
|
|
22
|
+
- `SemaTaskHandle.outputOffset` — never written by the registry and never read by anything.
|
|
23
|
+
- Migration: setting any removed field is now a compile error at the assignment. There is no runtime change to migrate — the behaviour a consumer sees is exactly what it saw before, because none of these fields ever reached code. The remaining 25 declared-only members are registered with a reason and an owner in `test/field-liveness-allowlist.json` (deployment-implemented adapter contracts, foreign on-disk formats, nominal brand phantoms, and the published recall seam); two of them carry a `PENDING VERDICT` marker with a review date rather than a clearance.
|
|
24
|
+
- `LspDiagnosticsRegistry` is DEPLOYMENT-scoped (one per LSP manager, shared by every task), but its delivered-set had no run boundary: a diagnostic injected into one run was filtered out of every later run's drain for the lifetime of the process — a second session could not see it at all unless it happened to edit that file — and the set grew without bound. The delivered set is now keyed by run: `drain(runIdent)` and `fileEdited(runIdent, uri)` take the run key (both signatures changed), and the new `releaseRun(runIdent)` drops a finished run's keys. The engine passes the session id and releases at the run's terminal; direct callers of the registry must pass a key of their own.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Session permission rules: an `allowDirs` confinement now PATH-CONFINES `NotebookEdit` instead of denying it outright. Previously `createSessionRulePolicy` only knew the `file_path`-keyed write tools, so while `allowDirs` was active every `NotebookEdit` call fell into the "mutating but not path-confinable" arm and was denied — even for a notebook inside an allowed directory — while `createFsWriteGatePolicy` had always confined the same tool by its `notebook_path`. Both faces now share one covered set and one target extractor, so a notebook inside `allowDirs` is allowed, one outside is denied, and a decoy `file_path` never outranks the real `notebook_path` target. **A probe pinning "NotebookEdit is denied under `allowDirs`" will now flip** — re-pin it as a path verdict (in-dir allow / out-of-dir deny). Deliberately unchanged: every other mutating tool that cannot be path-confined (bash, deployment-authored write tools, tools with an unknown effect) is still fail-closed denied under `allowDirs`, and the skill-manifest `allowPaths` face still denies `NotebookEdit` — widening that one is a separate decision.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **A subagent's durable pause is reported as what it is.** The sync and fork report arms minted `status: failed` with "treat this delegation as failed; do not retry" when the child parked at an approval gate — a fabrication that buried the recovery path. They now mint one honest constructor: `status: suspended`, the checkpoint named, the deployment's approval channel named as the only release path, anti-retry guidance kept, `isError: true` now explicit on the report. `details.error` vocabulary: `unexpected.suspended` → `suspended.awaiting_approval`, `unexpected.needs_review` → `suspended.needs_review` (the TaskResult-level `unexpected.*` codes elsewhere are unchanged). The ineligible-background settle keeps its mechanism byte-for-byte but its row/summary/notification faces now say which disposition happened: new `errorCode` values `suspended.awaiting_approval` (checkpoint still pending, resolvable out-of-band) and `suspended.checkpoint_expired` (destroyed by the no-orphans rule — stop waiting). **Consumer flips**: probes pinning the old `status: failed` wording or the `unexpected.*` codes on delegation reports will red; anything pinning a closed set of background-failure `errorCode`s must admit the two new members.
|
|
33
|
+
- Resumed-batch `tool_end` frames (durable-approval resume) carry the result body — `output` / `truncated` / `totalChars` / `structured` from the same projection the live loop uses; the deferred-sibling closes carry the `[DEFERRED]` body the transcript records. A client rendering tool output from frames no longer shows an empty card for every approved call ([2513] W2; zero shape change — these are the frame's existing optional fields, previously absent). The same frames now also carry `eventId` (+ `parentToolCallId`/`sourceTaskId` when running as a subagent) and reach a parent's `forwardSubagentEvents` pane — a child's durable-approved call no longer leaves the parent's viewing pane spinner open forever.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Workflow resilience knobs (`stallMs` / `agentMaxRetries` / `throttleBackoffMs` / `totalTimeoutMs`) refuse non-finite values loudly instead of silently changing behavior — a NaN used to disarm the stall watchdog, zero the retry budget, or turn the total deadline into an instant abort (`stallMs <= 0` stays the documented explicit-off). `WebFetchConfig.maxBytes` likewise refuses non-finite/non-positive values (`config.web_max_bytes_invalid`) — NaN/Infinity used to disable the byte budget entirely, streaming unbounded into memory (±Infinity gets its own message: the cap cannot be turned off, only widened).
|
|
38
|
+
- The WebFetch binary sniffer consumes the shared magic-byte table (`BINARY_MAGIC_SIGNATURES`) instead of a private eight-format if-chain — one data source; edge behavior tightens slightly (full-length signatures) and labels adopt the table's human-readable names.
|
|
39
|
+
- Platform terminals (`env.lifetime_expired` / `usage.window_exhausted`) now carry `salvagedOutput` like every other salvage-eligible terminal (the eligibility set is one closed construction point) — **a consumer pinning "salvagedOutput is empty on every non-completed result" will flip**. `TaskResult.retryAfterMs` (new, optional) rides a `usage.window_exhausted` terminal with the window's reopen hint; presence condition documented on the field — judge the cause by `errorCode`, never by this field's presence.
|
|
40
|
+
- `assertWorkflowDeterminism` reaches the package root alongside its two conformance siblings (the determinism battery an out-of-repo workflow runner needs; additive).
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
- `runCascade` no longer reaches its tail with nothing dispatched. A non-finite `config.maxEscalations` is refused at the door (`Error.code = "config.cascade_invalid"`) instead of making the iteration bound `NaN` and running zero rungs; a `totalTimeoutMs` already spent at entry returns the cascade's own terminal (`status: "failed"`, `errorCode: "cascade.budget_exhausted"`, empty `attempts`, `finalRung: -1`) instead of throwing a bare `TypeError`. `CascadeRunResult.finalRung` can therefore be `-1` (no rung produced the result).
|
|
44
|
+
|
|
45
|
+
## 5.8.0 (2026-08-04)
|
|
46
|
+
|
|
47
|
+
_The limits restructure: time leaves the task-limit axis, tokens become the primary budget, and every default is empty. One release, five construction stages — there is no intermediate version. Old usage fails loudly (compile errors for removed fields; a typed refusal for unknown limit keys at runtime) — nothing is silently ignored._
|
|
48
|
+
|
|
49
|
+
### BREAKING — task limits (`TaskSpec.limits` is the one budget surface)
|
|
50
|
+
|
|
51
|
+
- `limits` now holds every budget knob: `maxTokens` (primary, cumulative per-slice window — promoted from report-only to a GATE) / `maxCostUsd` / `maxTurns` / `maxWalltimeMs` (opt-in, per-slice active clock, **milliseconds**) / `maxOutputTokens` (unchanged, per-request shape) / `approachNotice` / `budgetStreamCancel` / `degrade` / `brainCallGuardrailMs`. The former top-level `maxTokens` / `maxCostUsd` / `budgetStreamCancel` / `degrade` fields are gone.
|
|
52
|
+
- `limits.timeoutSec` is REMOVED (unit and semantics changed → `maxWalltimeMs`). An unknown key on `limits` is refused at prepare with `config.limit_unknown_key` naming the replacement and the unit; a non-finite or negative value refuses with `config.limit_invalid`. Never silently ignored.
|
|
53
|
+
- EVERY default is empty: `DEFAULT_MAX_TURNS=1000`, `FORK_DEFAULT_MAX_TURNS=200`, the session-scoped background child's injected 30-minute wall clock, the resource-suspend `maxSlices=100` default, and the workflow per-agent 300s cap are all retired. No limit is armed unless configured. Anti-zombie coverage for session-scoped background children moved to the stale-running reaper (`TaskRegistry.reapStaleSessionBackgroundAgents` — collects by STALL, not by wall age). Honest consequence, stated plainly: a zero-config task with a non-terminating brain loop now has no task-level backstop — the engineering guardrails (brain-call guardrail, stall detection) keep it from wedging silently, and the deployment governance windows are the burn ceiling.
|
|
54
|
+
- To-limit behavior is two-tier: WITHOUT `resourceSuspend`, hitting any limit is a loud terminal failure — `TaskResult.status` loses `"timeout"`; all four axes end as `failed` with `limits.max_tokens_exceeded` / `limits.max_cost_exceeded` / `limits.max_turns_exceeded` / `limits.max_walltime_exceeded` (the code names the AXIS, replacing `limit.timeout` / `limit.max_turns` / `budget.exceeded` / `budget.precall`), salvage attached. WITH `resourceSuspend` (+ checkpoint infrastructure), a per-slice window exhaustion suspends with a checkpoint and resume grants the next window; only the optional frozen totals (`resourceSuspend.totalTokens` / `totalBudgetUsd`) are hard stops. `ResourceLimitReason` splits `"budget"` into `"budget_tokens"` / `"budget_cost"` — consumers pinning either closed set are affected and named here.
|
|
55
|
+
- Priority when several axes trip at one boundary: tokens > cost > turns > walltime (attribution only — every configured axis stays armed).
|
|
56
|
+
- `limits.approachNotice` replaces `deadlineNudge`: one "start converging" frame at ~80% and one "deliver now" at ~95% of the tightest configured budget, measured in BUDGET (tokens/cost/turns), never in clock; one-shot; `stats.mechanisms.approachNoticesSent`; a continuation gate keeps the notice from extending a run the model had already ended.
|
|
57
|
+
|
|
58
|
+
### BREAKING — retired mechanisms
|
|
59
|
+
|
|
60
|
+
- The design/128/130 deadline family is gone: `deadlineNudge` / `callCapByDeadline` / `gracefulFinalize`, the write-out cushion mathematics, and the per-call deadline shrink.
|
|
61
|
+
- tool-cut is gone end to end: `stats.mechanisms.{toolCuts,toolCutKills,pendingCutTools,finalizeInjected,nudgesSent,capShrinks,callCutoffs,toolClamps}`, `engineCut:"walltime_writeout"`, the bash exec deadline clamp, and `cut-kill.ts`.
|
|
62
|
+
- The brain soft-cutoff is gone: `errorKind:"walltime_cutoff"`, `WALLTIME_CUTOFF_MESSAGE`, `StreamOptions.callDeadlineMs`, the loop's `walltime_cutoff_recovery`.
|
|
63
|
+
- auto-compaction's walltime gate / abort race / throughput EWMA are gone (`clampReason` loses `"walltime"`).
|
|
64
|
+
- Stall detection (connect / first-token / idle) is DECOUPLED from the retired walltime lane and now arms unconditionally (guardrail class; constants renamed `STALL_*`; knob semantics unchanged).
|
|
65
|
+
- The `ExecutionEnv.exec` `onSpawn` / `onDetachAdopted` seam is retired (its only consumer left with tool-cut; zero producers remained).
|
|
66
|
+
- The Terminal-Bench surface is retired: the `sema-tb` bin, `tb-env`, the `tb/` adapter tree, and all thirteen `TB_*` env knobs. No switch survives.
|
|
67
|
+
|
|
68
|
+
### BREAKING — checkpoint / ledger / config
|
|
69
|
+
|
|
70
|
+
- `ResourceLedger.totalWalltimeSec` and `remainingWalltimeMs()` retired; `totalTokens` + `remainingTokens()` added; `spentWalltimeMs` REMAINS (accounting only). Checkpoint version 5 (`TOKEN_CHECKPOINT_VERSION`); a v4 checkpoint carrying the retired wall-clock allocation is refused pre-CAS with `checkpoint.walltime_axis_retired` (the checkpoint stays pending — finish it on the previous release, or start fresh; no silent migration). `resourceSuspend.totalWalltimeSec` → `resourceSuspend.totalTokens`.
|
|
71
|
+
- Config catalog: `limits.{timeoutSec,deadlineNudge,callCapByDeadline,gracefulFinalize}`, `budget.{maxCostUsd,maxTokens,totalWalltimeSec}`, `delegation.{forkMaxTurns,sessionBgTimeoutSec}` retired/moved; `limits.{maxWalltimeMs,approachNotice,maxTokens,maxCostUsd}`, `budget.totalTokens` added. Workflow script `limits` whitelist: `{maxTurns,timeoutSec}` → `{maxTurns,maxWalltimeMs,maxTokens,maxCostUsd}`; `WorkflowChildCaps.perAgentTimeoutSec` → `perAgentMaxWalltimeMs` (no default).
|
|
72
|
+
- Workflow resume journals from 5.7 cache-miss entirely on this version (the spec shape participates in the call identity) — reruns, not errors.
|
|
73
|
+
- Trace: the eight mechanisms fields above are gone; `timerLatenessMs` / `walltimeSyncBackstopFired` REMAIN (the per-slice wall timer survives as `maxWalltimeMs`, so its starvation evidence keeps a producer).
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- memory-engine: partition adoption is a single atomic rename (the delete-then-rename window could destroy a concurrently adopted partition and then disclose the opposite of what happened); fileless-scaffolding cleanup uses `rmdir` only, so a peer's files are unremovable by construction. Both control-plane lock waits are bounded and yield every round — a permanent errno can no longer starve the event loop in a silent spin (the fail-open sidecar lock now honors its documented "proceed anyway" by going lockless after three post-deadline steal rounds).
|
|
78
|
+
- cc stores: the mailbox sweep AND `drop` now judge and delete inside the box's cross-process lock (a compliant writer's freshly committed message can no longer be deleted unseen, and another holder's lock directory is never torn down); the cc lockfile's retry bound is read before every round (three `continue` shortcuts skipped it — a permanently failing reclaim spun forever); the lease table's entry keys fold the same way the table itself does (two store instances over one directory via a symlink could each grant the same lease).
|
|
79
|
+
- session policy: the tighten gate's path-containment fold is win-family-conditional, matching the runtime's — a POSIX sibling directory whose literal name embeds backslashes no longer reads as "within" the confined root (a loosening the gate exists to refuse). The transcript-dir gate's write-tool literal gains a mechanical sync pin against `PATH_WRITE_TOOLS`.
|
|
80
|
+
- `tool-name-aliases` docstring named a compile-time enforcement symbol that never existed; it now states the real division of labor (the prepare-time audit escalates, the compile pass deliberately does not).
|
|
81
|
+
|
|
3
82
|
## 5.7.0 (2026-08-04)
|
|
4
83
|
|
|
5
84
|
_Three collected cars: a parked question is answered by the decision itself (behavior face — see the consumer note), a descendant's terminal notification anchors on the delegation tree, and the env scrub stops deleting infrastructure key names (behavior face)._
|
package/dist/agents/cascade.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type GateVerdict = boolean | {
|
|
|
6
6
|
};
|
|
7
7
|
export interface CascadeRung {
|
|
8
8
|
model: ModelRef;
|
|
9
|
-
overrides?: Partial<Pick<TaskSpec, "limits" | "
|
|
9
|
+
overrides?: Partial<Pick<TaskSpec, "limits" | "systemPrompt">>;
|
|
10
10
|
}
|
|
11
11
|
export interface CascadeConfig {
|
|
12
12
|
onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
|
package/dist/agents/cascade.js
CHANGED
|
@@ -3,6 +3,13 @@ import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
|
3
3
|
import { buildCumulativeStats } from "./cumulative-stats.js";
|
|
4
4
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
5
5
|
const CASCADE_ON_RUNG_SITE = "cascade.onRung";
|
|
6
|
+
const CASCADE_CONFIG_ERROR_CODE = "config.cascade_invalid";
|
|
7
|
+
const CASCADE_NO_DISPATCH_ERROR_CODE = "cascade.budget_exhausted";
|
|
8
|
+
function cascadeConfigError(message) {
|
|
9
|
+
const e = new Error(message);
|
|
10
|
+
e.code = CASCADE_CONFIG_ERROR_CODE;
|
|
11
|
+
return e;
|
|
12
|
+
}
|
|
6
13
|
function createDefaultGate(spec) {
|
|
7
14
|
const requiresStructured = spec.outputSchema != null;
|
|
8
15
|
return (result) => result.status === "completed" && (!requiresStructured || result.structuredOutput !== undefined);
|
|
@@ -13,6 +20,9 @@ export async function runCascade(runner, spec, config) {
|
|
|
13
20
|
throw new Error("runCascade: config.ladder must have at least one rung");
|
|
14
21
|
}
|
|
15
22
|
const maxEscalations = config.maxEscalations ?? ladder.length - 1;
|
|
23
|
+
if (typeof maxEscalations !== "number" || !Number.isFinite(maxEscalations)) {
|
|
24
|
+
throw cascadeConfigError(`runCascade: config.maxEscalations must be a finite number (got ${String(config.maxEscalations)}) — an unevaluable escalation ceiling is not a ceiling, and folding it to a default would run the cascade under a bound nobody chose.`);
|
|
25
|
+
}
|
|
16
26
|
const maxRungs = Math.min(ladder.length, Math.max(0, maxEscalations) + 1);
|
|
17
27
|
const gate = config.gate ?? createDefaultGate(spec);
|
|
18
28
|
const startedAt = Date.now();
|
|
@@ -179,6 +189,20 @@ export async function runCascade(runner, spec, config) {
|
|
|
179
189
|
break;
|
|
180
190
|
}
|
|
181
191
|
}
|
|
192
|
+
if (lastResult === undefined) {
|
|
193
|
+
return {
|
|
194
|
+
taskId: spec.taskId ?? "",
|
|
195
|
+
sessionId: "",
|
|
196
|
+
status: "failed",
|
|
197
|
+
result: `runCascade: the overall wall-clock budget (totalTimeoutMs ${config.totalTimeoutMs}ms) was already spent when the ladder was entered — no rung dispatched`,
|
|
198
|
+
errorCode: CASCADE_NO_DISPATCH_ERROR_CODE,
|
|
199
|
+
stats: { tokens: 0, turns: 0, costMicroUsd: 0 },
|
|
200
|
+
cascadeOutcome: "exhausted",
|
|
201
|
+
escalated: false,
|
|
202
|
+
finalRung: -1,
|
|
203
|
+
attempts,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
182
206
|
const base = lastResult;
|
|
183
207
|
const finalRung = attempts.length - 1;
|
|
184
208
|
return {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Runner } from "../core/runner/runtask.js";
|
|
2
|
-
import type { AgentDefinition, Model, ModelRef, ToolSpec } from "../core/types.js";
|
|
2
|
+
import type { AgentDefinition, Model, ModelRef, TaskLimits, ToolSpec } from "../core/types.js";
|
|
3
3
|
import { type ExecutionEnv } from "../internal/harness.js";
|
|
4
4
|
import type { RunInternals } from "../core/runner/prepare-task.js";
|
|
5
5
|
import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
6
|
-
import {
|
|
7
|
-
export {
|
|
6
|
+
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
|
|
7
|
+
export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
8
8
|
import { SubagentRetainLedger } from "./retain-ledger.js";
|
|
9
9
|
export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
|
|
10
10
|
export declare function notifyResultField(result: string | undefined): string | undefined;
|
|
@@ -43,6 +43,9 @@ export declare function classifySubagentError(child: {
|
|
|
43
43
|
errorKind: SubagentErrorKind;
|
|
44
44
|
retryable: boolean;
|
|
45
45
|
} | undefined;
|
|
46
|
+
export declare const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
|
|
47
|
+
export declare const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
|
|
48
|
+
export declare const SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED = "suspended.checkpoint_expired";
|
|
46
49
|
export interface SubagentSteerHandle {
|
|
47
50
|
parentToolCallId: string;
|
|
48
51
|
agentName?: string;
|
|
@@ -101,10 +104,7 @@ export interface SubagentToolOptions {
|
|
|
101
104
|
systemPrompt?: string;
|
|
102
105
|
name?: string;
|
|
103
106
|
purpose?: string;
|
|
104
|
-
limits?:
|
|
105
|
-
maxTurns?: number;
|
|
106
|
-
timeoutSec?: number;
|
|
107
|
-
};
|
|
107
|
+
limits?: TaskLimits;
|
|
108
108
|
maxDepth?: number;
|
|
109
109
|
retainFailedSessions?: boolean;
|
|
110
110
|
onObserverError?: (err: unknown, info: {
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
|
+
import { withDelegationProvenance } from "../core/tool-policy.js";
|
|
3
4
|
import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
|
|
4
5
|
import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
|
|
5
6
|
import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
|
|
@@ -17,7 +18,7 @@ import { addWorktree } from "../core/git-worktree-env.js";
|
|
|
17
18
|
import { shellQuote } from "../tools/fs/search.js";
|
|
18
19
|
import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
|
|
19
20
|
import { extractErrorCode } from "../brain/errors.js";
|
|
20
|
-
import {
|
|
21
|
+
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
|
|
21
22
|
function rollupDelegatedCost(stats, nested) {
|
|
22
23
|
if (stats.costMicroUsd === undefined)
|
|
23
24
|
return undefined;
|
|
@@ -29,7 +30,7 @@ function delegatedCostField(stats) {
|
|
|
29
30
|
const total = rollupDelegatedCost(stats, stats.nested);
|
|
30
31
|
return total !== undefined ? { costMicroUsd: total } : {};
|
|
31
32
|
}
|
|
32
|
-
export {
|
|
33
|
+
export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
33
34
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
|
|
34
35
|
import { recordRosterSpawn } from "./roster-store.js";
|
|
35
36
|
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
@@ -224,20 +225,55 @@ export function classifySubagentError(child) {
|
|
|
224
225
|
? "rate_limit"
|
|
225
226
|
: code === "server" && (msg.includes("overloaded") || msg.includes("529"))
|
|
226
227
|
? "overloaded"
|
|
227
|
-
: code === "
|
|
228
|
+
: code === "limits.max_walltime_exceeded" || ((code === "network" || code === "server") && msg.includes("timeout"))
|
|
228
229
|
? "timeout"
|
|
229
230
|
: code === "network" || code === "server"
|
|
230
231
|
? "network"
|
|
231
232
|
: "logic";
|
|
232
233
|
return { errorKind, retryable: errorKind !== "logic" };
|
|
233
234
|
}
|
|
235
|
+
export const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
|
|
236
|
+
export const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
|
|
237
|
+
export const SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED = "suspended.checkpoint_expired";
|
|
238
|
+
function durablePauseDelegationReport(reportLabel, child) {
|
|
239
|
+
const needsReview = child.status === "needs_review";
|
|
240
|
+
const token = child.checkpointToken;
|
|
241
|
+
return {
|
|
242
|
+
isError: true,
|
|
243
|
+
content: `[Sub-agent report${reportLabel}]\n` +
|
|
244
|
+
`status: suspended\n` +
|
|
245
|
+
`This delegation is PARKED at an approval gate — it did not fail, and it is not finished. ` +
|
|
246
|
+
`${needsReview ? "A human review" : "An approval"} it cannot grant itself is pending` +
|
|
247
|
+
`${token !== undefined ? `, held by checkpoint ${token}` : " (the run reported no checkpoint token)"}. ` +
|
|
248
|
+
`The gated action has NOT run.\n` +
|
|
249
|
+
`Only the deployment's approval channel can release it: you cannot approve it from here, and no ` +
|
|
250
|
+
`result from it will arrive in this turn. Do not re-issue the same approval-gated action — an ` +
|
|
251
|
+
`identical retry parks again and resolves nothing. Carry on with work that does not depend on ` +
|
|
252
|
+
`this delegation, or report that it is awaiting approval.`,
|
|
253
|
+
details: {
|
|
254
|
+
error: needsReview ? SUBAGENT_SUSPENDED_NEEDS_REVIEW : SUBAGENT_SUSPENDED_AWAITING_APPROVAL,
|
|
255
|
+
...(token !== undefined ? { checkpointToken: token } : {}),
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function unparkedDurablePauseReason(d) {
|
|
260
|
+
return d.kind === "no_park_lane"
|
|
261
|
+
? `the agent hit an approval gate and paused durably, but this delegation cannot be parked ` +
|
|
262
|
+
`(parking needs an explicitly named child plus a durable agent row, a checkpoint store and the ` +
|
|
263
|
+
`durable-session capability). Its checkpoint ${d.checkpointToken} is still committed and the ` +
|
|
264
|
+
`approval must be resolved through the deployment's approval channel; the gated action never ran.`
|
|
265
|
+
: `the agent hit an approval gate and paused durably, but the park did not take (` +
|
|
266
|
+
`${d.cause === "capability_veto" ? "the durable-session capability refused" : "a concurrent stop won the arbitration"}` +
|
|
267
|
+
`), so its checkpoint was expired under the no-orphans rule. Nothing is pending approval and the ` +
|
|
268
|
+
`gated action never ran — re-run this delegation with an approver attached.`;
|
|
269
|
+
}
|
|
234
270
|
function errorKindClause(c) {
|
|
235
271
|
return c !== undefined ? ` (error_kind: ${c.errorKind}, retryable: ${c.retryable})` : "";
|
|
236
272
|
}
|
|
237
273
|
const FAILED_SESSION_RETAIN_TTL_MS = 15 * 60 * 1000;
|
|
238
274
|
const PARTIAL_FINDINGS_MAX_CHARS = 1200;
|
|
239
275
|
const BG_NOTIFY_DRAIN_WINDOW_MS = 2_000;
|
|
240
|
-
function createBgActivityBeat(parentToolCallId, emitTick) {
|
|
276
|
+
function createBgActivityBeat(parentToolCallId, emitTick, noteActivity) {
|
|
241
277
|
let beats = 0;
|
|
242
278
|
let starts = 0;
|
|
243
279
|
return (e) => {
|
|
@@ -248,6 +284,7 @@ function createBgActivityBeat(parentToolCallId, emitTick) {
|
|
|
248
284
|
if (e.type === "tool_start")
|
|
249
285
|
starts += 1;
|
|
250
286
|
beats += 1;
|
|
287
|
+
noteActivity?.();
|
|
251
288
|
if (beats === 1 || beats % RUNNING_AGENT_OBSERVE_EVERY_BEATS === 0)
|
|
252
289
|
emitTick(starts);
|
|
253
290
|
};
|
|
@@ -395,7 +432,7 @@ export function createSubagentResume(deps) {
|
|
|
395
432
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
396
433
|
usage: { toolUses: toolStarts },
|
|
397
434
|
});
|
|
398
|
-
});
|
|
435
|
+
}, deps.registry !== undefined && deps.taskId !== undefined ? () => deps.registry.noteBackgroundAgentActivity(deps.taskId) : undefined);
|
|
399
436
|
stream = childRunner.runTaskStream(resumeSpec, undefined, {
|
|
400
437
|
...entry.internalsSnapshot,
|
|
401
438
|
...(true
|
|
@@ -1149,11 +1186,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1149
1186
|
const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
|
|
1150
1187
|
const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
|
|
1151
1188
|
const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
|
|
1152
|
-
const childLimits = defMaxTurns !== undefined
|
|
1153
|
-
? { ...opts.limits, maxTurns: defMaxTurns }
|
|
1154
|
-
: wantsFork && opts.limits?.maxTurns === undefined
|
|
1155
|
-
? { ...opts.limits, maxTurns: FORK_DEFAULT_MAX_TURNS }
|
|
1156
|
-
: opts.limits;
|
|
1189
|
+
const childLimits = defMaxTurns !== undefined ? { ...opts.limits, maxTurns: defMaxTurns } : opts.limits;
|
|
1157
1190
|
const nextExcluded = def ? new Set([...excluded, def.name]) : excluded;
|
|
1158
1191
|
const defAllowsDelegation = def === undefined || toolNameAllowed(opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME, def.allowTools, def.denyTools);
|
|
1159
1192
|
if (depth + 1 < maxDepth && defAllowsDelegation) {
|
|
@@ -1233,7 +1266,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1233
1266
|
enableBlockedReport: false,
|
|
1234
1267
|
limits: {
|
|
1235
1268
|
maxTurns: typeof observerDef.maxTurns === "number" && Number.isFinite(observerDef.maxTurns) && observerDef.maxTurns > 0 ? observerDef.maxTurns : 8,
|
|
1236
|
-
|
|
1269
|
+
maxWalltimeMs: 120_000,
|
|
1237
1270
|
},
|
|
1238
1271
|
});
|
|
1239
1272
|
const runObserverLeg = async (spec) => {
|
|
@@ -1406,6 +1439,14 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1406
1439
|
...(ctx.onSubagentSpawn ? { onSubagentSpawn: ctx.onSubagentSpawn } : {}),
|
|
1407
1440
|
};
|
|
1408
1441
|
const childThinking = def?.thinking ?? ctx.thinkingLevel;
|
|
1442
|
+
const provenanceAgentName = agentName ?? def?.name;
|
|
1443
|
+
const childOnAsk = ctx.onAsk !== undefined
|
|
1444
|
+
? withDelegationProvenance(ctx.onAsk, {
|
|
1445
|
+
parentToolCallId: ctx.toolCallId,
|
|
1446
|
+
depth: depth + 1,
|
|
1447
|
+
...(provenanceAgentName !== undefined ? { agentName: provenanceAgentName } : {}),
|
|
1448
|
+
})
|
|
1449
|
+
: undefined;
|
|
1409
1450
|
const buildChildSpec = (signal) => ({
|
|
1410
1451
|
objective: prompt,
|
|
1411
1452
|
...(childModel ? { model: childModel } : { modelRole: "subagent" }),
|
|
@@ -1415,6 +1456,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1415
1456
|
...(def?.memory ? { memory: def.memory } : {}),
|
|
1416
1457
|
...(def?.skills?.length ? { skills: def.skills } : {}),
|
|
1417
1458
|
...(ctx.principal !== undefined ? { principal: ctx.principal } : {}),
|
|
1459
|
+
...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
|
|
1418
1460
|
...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
|
|
1419
1461
|
...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
|
|
1420
1462
|
...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
|
|
@@ -1722,9 +1764,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1722
1764
|
});
|
|
1723
1765
|
const forkBgStartedAt = Date.now();
|
|
1724
1766
|
const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective };
|
|
1725
|
-
if (sessionScopedBg && bgForkSpec.limits?.timeoutSec === undefined) {
|
|
1726
|
-
bgForkSpec.limits = { ...(bgForkSpec.limits ?? {}), timeoutSec: SESSION_BG_DEFAULT_TIMEOUT_SEC };
|
|
1727
|
-
}
|
|
1728
1767
|
const s2ForkNotifyReady = (inject) => {
|
|
1729
1768
|
bg.registry.attachAgentNotify(taskId, inject);
|
|
1730
1769
|
};
|
|
@@ -1744,7 +1783,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1744
1783
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
1745
1784
|
usage: { toolUses: toolStarts },
|
|
1746
1785
|
});
|
|
1747
|
-
});
|
|
1786
|
+
}, () => bg.registry.noteBackgroundAgentActivity(taskId));
|
|
1748
1787
|
const bgForkInternals = bgSink
|
|
1749
1788
|
? {
|
|
1750
1789
|
...forkInternals,
|
|
@@ -1977,11 +2016,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1977
2016
|
costMicroUsd: rollupDelegatedCost(forkChild.stats, nsf),
|
|
1978
2017
|
});
|
|
1979
2018
|
if (isDurablePause(forkChild.status)) {
|
|
1980
|
-
return {
|
|
1981
|
-
content: `[Sub-agent report · ${FORK_SUBAGENT_TYPE}]\nstatus: failed\nerror: unexpected durable pause (${forkChild.status}, checkpoint ` +
|
|
1982
|
-
`${String(forkChild.checkpointToken ?? "?")}) — treat this fork as failed; do not retry the same gated action.`,
|
|
1983
|
-
details: { error: forkChild.status === "needs_review" ? "unexpected.needs_review" : "unexpected.suspended", checkpointToken: forkChild.checkpointToken },
|
|
1984
|
-
};
|
|
2019
|
+
return durablePauseDelegationReport(` · ${FORK_SUBAGENT_TYPE}`, forkChild);
|
|
1985
2020
|
}
|
|
1986
2021
|
const wtLine = await finishWorktree();
|
|
1987
2022
|
const forkErr = classifySubagentError(forkChild);
|
|
@@ -2195,9 +2230,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2195
2230
|
...(forwardDurableApproval ? { durableApproval: { ...ctx.durableApprovalForChildren } } : {}),
|
|
2196
2231
|
...(reviveRow !== undefined ? { requireExistingSession: true } : {}),
|
|
2197
2232
|
};
|
|
2198
|
-
if ((sessionScopedBg || reviveRow !== undefined) && bgSpec.limits?.timeoutSec === undefined) {
|
|
2199
|
-
bgSpec.limits = { ...(bgSpec.limits ?? {}), timeoutSec: SESSION_BG_DEFAULT_TIMEOUT_SEC };
|
|
2200
|
-
}
|
|
2201
2233
|
let reviveAttachedResolve;
|
|
2202
2234
|
const reviveAttached = reviveRow !== undefined ? new Promise((r) => (reviveAttachedResolve = r)) : undefined;
|
|
2203
2235
|
const s2NotifyReady = (inject) => {
|
|
@@ -2222,7 +2254,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2222
2254
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
2223
2255
|
usage: { toolUses: toolStarts },
|
|
2224
2256
|
});
|
|
2225
|
-
});
|
|
2257
|
+
}, () => bg.registry.noteBackgroundAgentActivity(taskId));
|
|
2226
2258
|
const bgInternals = bgSink
|
|
2227
2259
|
? {
|
|
2228
2260
|
...childInternals,
|
|
@@ -2417,6 +2449,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2417
2449
|
else
|
|
2418
2450
|
bgRetainLedger?.markSettled(ctx.toolCallId);
|
|
2419
2451
|
}
|
|
2452
|
+
let unparkedPause;
|
|
2420
2453
|
if (isDurablePause(child.status) && child.checkpointToken !== undefined) {
|
|
2421
2454
|
const cpStore = bg.checkpointStore;
|
|
2422
2455
|
const expireByStoreScope = async (token2) => {
|
|
@@ -2461,14 +2494,27 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2461
2494
|
if (parked === "parked")
|
|
2462
2495
|
return;
|
|
2463
2496
|
await expireByStoreScope(token);
|
|
2497
|
+
unparkedPause = { kind: "checkpoint_expired", cause: attested ? "park_lost" : "capability_veto" };
|
|
2464
2498
|
}
|
|
2465
2499
|
}
|
|
2500
|
+
else {
|
|
2501
|
+
unparkedPause = { kind: "no_park_lane", checkpointToken: child.checkpointToken };
|
|
2502
|
+
}
|
|
2466
2503
|
}
|
|
2467
2504
|
const ok = child.status === "completed";
|
|
2468
2505
|
const reaped = !ok && abort.signal.aborted;
|
|
2469
2506
|
const collateral = reaped && bg.registry.getAccessibleTask(taskId, { ...(bgOwner !== undefined ? { owner: bgOwner } : {}), ...(bgScope !== undefined ? { scope: bgScope } : {}) })?.status === "running";
|
|
2470
2507
|
const failedBg = !ok && !reaped;
|
|
2471
|
-
const
|
|
2508
|
+
const unparkedPauseReason = unparkedPause !== undefined && failedBg ? unparkedDurablePauseReason(unparkedPause) : undefined;
|
|
2509
|
+
const errCodeBg = failedBg
|
|
2510
|
+
? unparkedPause !== undefined
|
|
2511
|
+
? unparkedPause.kind === "no_park_lane"
|
|
2512
|
+
? child.status === "needs_review"
|
|
2513
|
+
? SUBAGENT_SUSPENDED_NEEDS_REVIEW
|
|
2514
|
+
: SUBAGENT_SUSPENDED_AWAITING_APPROVAL
|
|
2515
|
+
: SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED
|
|
2516
|
+
: (child.errorCode ?? extractErrorCode(child.errorMessage))
|
|
2517
|
+
: undefined;
|
|
2472
2518
|
const errClassBg = failedBg
|
|
2473
2519
|
? classifySubagentError({ status: "failed", ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}), ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}) })
|
|
2474
2520
|
: undefined;
|
|
@@ -2477,7 +2523,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2477
2523
|
status: ok ? "completed" : reaped ? "killed" : "failed",
|
|
2478
2524
|
seq: seqAtSettle ?? 1,
|
|
2479
2525
|
...resultSettleFields(child.result),
|
|
2480
|
-
...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : child.errorMessage ?? String(child.status) } : {}),
|
|
2526
|
+
...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : unparkedPauseReason ?? child.errorMessage ?? String(child.status) } : {}),
|
|
2481
2527
|
...(errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
|
|
2482
2528
|
...(errClassBg !== undefined ? { retryable: errClassBg.retryable, errorKind: errClassBg.errorKind } : {}),
|
|
2483
2529
|
}) ??
|
|
@@ -2495,7 +2541,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2495
2541
|
const observerNote = observerNoteFor(await startBoundedObserverDrain());
|
|
2496
2542
|
const residual = residualFields();
|
|
2497
2543
|
const resumableBg = bgRetain !== undefined && settled !== "killed";
|
|
2498
|
-
const failReasonBg = settled === "failed" ? child.errorMessage : undefined;
|
|
2544
|
+
const failReasonBg = settled === "failed" ? unparkedPauseReason ?? child.errorMessage : undefined;
|
|
2499
2545
|
const bgTerminalSummary = failReasonBg !== undefined
|
|
2500
2546
|
? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
|
|
2501
2547
|
: `${ccCompletionText(shortDesc, settled, String(child.status), Date.now() - bgStartedAt)}${observerNote}`;
|
|
@@ -2737,12 +2783,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2737
2783
|
if (isDurablePause(child.status)) {
|
|
2738
2784
|
if (retainEntry)
|
|
2739
2785
|
ctx.subagentRetain?.abandon(ctx.toolCallId);
|
|
2740
|
-
return {
|
|
2741
|
-
content: `[Sub-agent report${def ? ` · ${def.name}` : ""}]\n` +
|
|
2742
|
-
`status: failed\nerror: unexpected durable pause (${child.status}, checkpoint ${String(child.checkpointToken ?? "?")}) — ` +
|
|
2743
|
-
`treat this delegation as failed; do not retry with the same approval-gated action.`,
|
|
2744
|
-
details: { error: child.status === "needs_review" ? "unexpected.needs_review" : "unexpected.suspended", checkpointToken: child.checkpointToken },
|
|
2745
|
-
};
|
|
2786
|
+
return durablePauseDelegationReport(def ? ` · ${def.name}` : "", child);
|
|
2746
2787
|
}
|
|
2747
2788
|
let failureRetained = false;
|
|
2748
2789
|
if (!retainEntry) {
|
package/dist/agents/teacher.js
CHANGED
|
@@ -192,7 +192,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
192
192
|
...helperBase(),
|
|
193
193
|
objective: helperObjective,
|
|
194
194
|
enableBlockedReport: false,
|
|
195
|
-
limits: { maxTurns: 1,
|
|
195
|
+
limits: { maxTurns: 1, ...(studentSpec.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: studentSpec.limits.maxWalltimeMs } : {}) },
|
|
196
196
|
signal: extSignal,
|
|
197
197
|
});
|
|
198
198
|
await runner.sessions.release(r.sessionId).catch(() => undefined);
|
|
@@ -341,7 +341,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
341
341
|
...teacherModelFields(),
|
|
342
342
|
systemPrompt: teacher.prompts?.teacher ?? TEACHER_PROMPT,
|
|
343
343
|
enableBlockedReport: false,
|
|
344
|
-
limits: { maxTurns: 4,
|
|
344
|
+
limits: { maxTurns: 4, ...(studentSpec.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: studentSpec.limits.maxWalltimeMs } : {}) },
|
|
345
345
|
signal: extSignal,
|
|
346
346
|
});
|
|
347
347
|
await runner.sessions.release(r.sessionId).catch(() => undefined);
|
|
@@ -363,7 +363,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
363
363
|
else if (result.status === "blocked") {
|
|
364
364
|
trigger = "blocked";
|
|
365
365
|
}
|
|
366
|
-
else if (result.status === "failed"
|
|
366
|
+
else if (result.status === "failed") {
|
|
367
367
|
trigger = "failed";
|
|
368
368
|
}
|
|
369
369
|
else if (!verifyOutput) {
|
package/dist/agents/team.d.ts
CHANGED
package/dist/agents/team.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function runTeamDiscussion(opts) {
|
|
|
63
63
|
: undefined;
|
|
64
64
|
const memberLimits = opts.limits
|
|
65
65
|
? {
|
|
66
|
-
...(opts.limits.
|
|
66
|
+
...(opts.limits.maxWalltimeMs !== undefined ? { maxWalltimeMs: opts.limits.maxWalltimeMs } : {}),
|
|
67
67
|
...(opts.limits.maxTurns !== undefined ? { maxTurns: opts.limits.maxTurns } : {}),
|
|
68
68
|
}
|
|
69
69
|
: undefined;
|
package/dist/agents/verify.js
CHANGED
|
@@ -101,7 +101,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
|
|
|
101
101
|
handsReadOnly: config.verifierHandsReadOnly ?? true,
|
|
102
102
|
outputSchema: VerdictSchema,
|
|
103
103
|
enableBlockedReport: false,
|
|
104
|
-
limits: {
|
|
104
|
+
limits: { ...(specBase.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: specBase.limits.maxWalltimeMs } : {}) },
|
|
105
105
|
getApiKeyAndHeaders: specBase.getApiKeyAndHeaders,
|
|
106
106
|
signal: specBase.signal,
|
|
107
107
|
});
|