@sema-agent/core 5.9.0 → 5.10.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 +40 -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 +30 -0
- package/dist/agents/subagent.js +61 -19
- 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/memory-recall.js +8 -3
- package/dist/core/memory.d.ts +5 -0
- package/dist/core/memory.js +6 -4
- package/dist/core/runner/prepare-task.d.ts +8 -1
- package/dist/core/runner/prepare-task.js +45 -11
- package/dist/core/runner/runtask.d.ts +12 -0
- package/dist/core/runner/runtask.js +127 -22
- 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-output-projection.js +5 -4
- 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/tools.js +9 -1
- package/dist/core/types.d.ts +5 -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/stores/file/memory-store.js +3 -7
- package/dist/tools/fs/fs-bash.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +4 -0
- package/dist/tools/web.js +0 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.10.0 — 2026-08-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`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.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
- **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).
|
|
14
|
+
|
|
15
|
+
- **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).
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
19
|
+
- **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.
|
|
20
|
+
- **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).
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **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.
|
|
27
|
+
|
|
28
|
+
### BREAKING
|
|
29
|
+
|
|
30
|
+
- **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.
|
|
31
|
+
|
|
32
|
+
- **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.
|
|
33
|
+
- **`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`.
|
|
34
|
+
|
|
35
|
+
- **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.**
|
|
36
|
+
- **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.
|
|
37
|
+
- **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.
|
|
38
|
+
- **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.
|
|
39
|
+
- **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.
|
|
40
|
+
- **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).
|
|
41
|
+
- **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.
|
|
42
|
+
|
|
3
43
|
## 5.9.0 (2026-08-04)
|
|
4
44
|
|
|
5
45
|
### 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 {
|
|
@@ -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
|
@@ -232,6 +232,29 @@ export function classifySubagentError(child) {
|
|
|
232
232
|
: "logic";
|
|
233
233
|
return { errorKind, retryable: errorKind !== "logic" };
|
|
234
234
|
}
|
|
235
|
+
export function completedAgentCard(child, extras) {
|
|
236
|
+
return {
|
|
237
|
+
type: "agent",
|
|
238
|
+
...(extras.subagentType !== undefined ? { subagent_type: extras.subagentType } : {}),
|
|
239
|
+
status: child.status,
|
|
240
|
+
taskId: child.taskId,
|
|
241
|
+
sessionId: child.sessionId,
|
|
242
|
+
result: child.result,
|
|
243
|
+
...(child.salvagedOutput !== undefined ? { salvagedOutput: child.salvagedOutput } : {}),
|
|
244
|
+
...(child.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : {}),
|
|
245
|
+
...(child.blockedReason !== undefined ? { blockedReason: child.blockedReason } : {}),
|
|
246
|
+
...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}),
|
|
247
|
+
...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}),
|
|
248
|
+
...(child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
|
|
249
|
+
...(child.degraded !== undefined ? { degraded: child.degraded } : {}),
|
|
250
|
+
...(extras.error ? { error_kind: extras.error.errorKind, retryable: extras.error.retryable } : {}),
|
|
251
|
+
stats: child.stats,
|
|
252
|
+
...(child.model !== undefined ? { model: child.model, resolvedModel: child.model } : {}),
|
|
253
|
+
...(extras.modelFallback !== undefined ? { modelFallback: extras.modelFallback } : {}),
|
|
254
|
+
...(extras.worktreePath !== undefined ? { worktreePath: extras.worktreePath } : {}),
|
|
255
|
+
...(extras.toolStats !== undefined ? { toolStats: extras.toolStats } : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
235
258
|
export const SUBAGENT_SUSPENDED_AWAITING_APPROVAL = "suspended.awaiting_approval";
|
|
236
259
|
export const SUBAGENT_SUSPENDED_NEEDS_REVIEW = "suspended.needs_review";
|
|
237
260
|
export const SUBAGENT_SUSPENDED_CHECKPOINT_EXPIRED = "suspended.checkpoint_expired";
|
|
@@ -374,6 +397,10 @@ export function createSubagentResume(deps) {
|
|
|
374
397
|
objective: createResumePrompt(marker, content),
|
|
375
398
|
sessionId: entry.childSessionId,
|
|
376
399
|
requireExistingSession: true,
|
|
400
|
+
...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
|
|
401
|
+
...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
402
|
+
...(entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
403
|
+
...(entry.specSnapshot.oneShot === true || deps.currentClamps?.oneShot === true ? { oneShot: true } : {}),
|
|
377
404
|
signal: abort.signal,
|
|
378
405
|
};
|
|
379
406
|
if (deps.registry !== undefined && deps.taskId !== undefined && deps.taskAccess !== undefined) {
|
|
@@ -880,7 +907,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
880
907
|
`- Finding a known symbol/class/definition, or searching within 2-3 known files → use Grep/Glob/Read.\n` +
|
|
881
908
|
`- 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
909
|
`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
|
|
910
|
+
`- 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
911
|
`- State exactly what it must RETURN (e.g. "report the file:line of each call site", not "look into the call sites").\n` +
|
|
885
912
|
`- Say explicitly whether you want it to WRITE CODE or only RESEARCH/REPORT — otherwise it may guess wrong.\n` +
|
|
886
913
|
`- 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 +1098,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1071
1098
|
}
|
|
1072
1099
|
}
|
|
1073
1100
|
}
|
|
1101
|
+
const modelFallback = !wantsFork && requestedModel !== undefined && perCallModel === undefined ? "inherit_no_tier_binding" : undefined;
|
|
1074
1102
|
let worktreeDir;
|
|
1075
1103
|
if (reviveClaim === undefined && (a.isolation ?? def?.isolation) === "worktree") {
|
|
1076
1104
|
const iso = ctx.worktreeIsolation;
|
|
@@ -1333,11 +1361,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1333
1361
|
};
|
|
1334
1362
|
const drainAndReleaseObserver = async () => {
|
|
1335
1363
|
if (observerPairing) {
|
|
1364
|
+
let drainRescue;
|
|
1336
1365
|
const timeout = new Promise((resolve) => {
|
|
1337
|
-
|
|
1338
|
-
t.unref?.();
|
|
1366
|
+
drainRescue = setTimeout(() => resolve("timeout"), 60_000);
|
|
1339
1367
|
});
|
|
1340
|
-
|
|
1368
|
+
let outcome;
|
|
1369
|
+
try {
|
|
1370
|
+
outcome = await Promise.race([observerPairing.drain().then(() => "drained"), timeout]);
|
|
1371
|
+
}
|
|
1372
|
+
finally {
|
|
1373
|
+
clearTimeout(drainRescue);
|
|
1374
|
+
}
|
|
1341
1375
|
observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
|
|
1342
1376
|
if (outcome === "timeout")
|
|
1343
1377
|
noteObserverFailure(new Error("observer unresponsive at settle (delivery drain timed out)"));
|
|
@@ -1456,7 +1490,12 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1456
1490
|
...(def?.memory ? { memory: def.memory } : {}),
|
|
1457
1491
|
...(def?.skills?.length ? { skills: def.skills } : {}),
|
|
1458
1492
|
...(ctx.principal !== undefined ? { principal: ctx.principal } : {}),
|
|
1493
|
+
...(ctx.checkpointStoreDisabledForChildren === true ? { checkpointStore: null } : {}),
|
|
1459
1494
|
...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
|
|
1495
|
+
...(ctx.onQuestion !== undefined ? { onQuestion: ctx.onQuestion } : {}),
|
|
1496
|
+
...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
1497
|
+
...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
1498
|
+
...(ctx.oneShot === true ? { oneShot: true } : {}),
|
|
1460
1499
|
...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
|
|
1461
1500
|
...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
|
|
1462
1501
|
...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
|
|
@@ -1473,7 +1512,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1473
1512
|
if (!ledger)
|
|
1474
1513
|
return undefined;
|
|
1475
1514
|
const childSessionId = preMintedSessionId ?? uuidv7();
|
|
1476
|
-
const { signal: _spawnSignal, ...plainSpec } = buildChildSpec(undefined);
|
|
1515
|
+
const { signal: _spawnSignal, onQuestion: _spawnTurnQuestionFace, ...plainSpec } = buildChildSpec(undefined);
|
|
1477
1516
|
let entry = ledger.register(ctx.toolCallId, {
|
|
1478
1517
|
childSessionId,
|
|
1479
1518
|
runner: opts.runner,
|
|
@@ -1553,7 +1592,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1553
1592
|
if (ledger !== undefined && !ledger.disposed) {
|
|
1554
1593
|
if (opts.background)
|
|
1555
1594
|
ensureSessionReapHook(opts.background.registry);
|
|
1556
|
-
const { signal: _spawnSignal, ...plainSpec } = buildChildSpec(undefined);
|
|
1595
|
+
const { signal: _spawnSignal, onQuestion: _spawnTurnQuestionFace, ...plainSpec } = buildChildSpec(undefined);
|
|
1557
1596
|
const entry = ledger.register(ctx.toolCallId, {
|
|
1558
1597
|
childSessionId,
|
|
1559
1598
|
runner: opts.runner,
|
|
@@ -1671,6 +1710,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1671
1710
|
else if (!sessionScopedBg)
|
|
1672
1711
|
ctx.signal.addEventListener("abort", onHostAbort, { once: true });
|
|
1673
1712
|
}
|
|
1713
|
+
const dropTurnBoundQuestionFace = sessionScopedBg ? { onQuestion: undefined } : {};
|
|
1674
1714
|
const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
|
|
1675
1715
|
const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
|
|
1676
1716
|
const bgScope = treeScope;
|
|
@@ -1735,7 +1775,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1735
1775
|
}, { priority: "later" }), "subagent.reapTerminalNotify");
|
|
1736
1776
|
});
|
|
1737
1777
|
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" }));
|
|
1778
|
+
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
1779
|
}
|
|
1740
1780
|
const bgSink = ctx.onBackgroundChildEvent;
|
|
1741
1781
|
const sinkEmit = (event) => {
|
|
@@ -1763,7 +1803,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1763
1803
|
startedAt: Date.now(),
|
|
1764
1804
|
});
|
|
1765
1805
|
const forkBgStartedAt = Date.now();
|
|
1766
|
-
const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective };
|
|
1806
|
+
const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective, ...dropTurnBoundQuestionFace };
|
|
1767
1807
|
const s2ForkNotifyReady = (inject) => {
|
|
1768
1808
|
bg.registry.attachAgentNotify(taskId, inject);
|
|
1769
1809
|
};
|
|
@@ -2039,16 +2079,15 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2039
2079
|
].filter(Boolean);
|
|
2040
2080
|
return {
|
|
2041
2081
|
content: forkLines.join("\n"),
|
|
2042
|
-
details: {
|
|
2043
|
-
|
|
2044
|
-
|
|
2082
|
+
details: completedAgentCard(forkChild, {
|
|
2083
|
+
subagentType: FORK_SUBAGENT_TYPE,
|
|
2084
|
+
error: forkErr,
|
|
2045
2085
|
...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
|
|
2046
|
-
...(forkChild.model !== undefined ? { resolvedModel: forkChild.model } : {}),
|
|
2047
2086
|
...(() => {
|
|
2048
2087
|
const ts = toolStatsCounter.snapshot();
|
|
2049
2088
|
return ts !== undefined ? { toolStats: ts } : {};
|
|
2050
2089
|
})(),
|
|
2051
|
-
},
|
|
2090
|
+
}),
|
|
2052
2091
|
};
|
|
2053
2092
|
}
|
|
2054
2093
|
if (wantsBackground) {
|
|
@@ -2058,12 +2097,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2058
2097
|
const reviveRow = reviveClaim?.row;
|
|
2059
2098
|
const parkedResume = reviveClaim?.parkedResume;
|
|
2060
2099
|
const sessionScopedBg = reviveRow !== undefined ? reviveRow.sessionScoped : ctx.backgroundScope === "session" && ctx.sessionId !== undefined;
|
|
2100
|
+
const outlivesThisRequest = sessionScopedBg || reviveRow !== undefined;
|
|
2061
2101
|
const onHostAbort = () => abort.abort();
|
|
2062
2102
|
const dropHostAbortListener = () => ctx.signal?.removeEventListener("abort", onHostAbort);
|
|
2063
2103
|
if (ctx.signal) {
|
|
2064
2104
|
if (ctx.signal.aborted)
|
|
2065
2105
|
abort.abort();
|
|
2066
|
-
else if (!
|
|
2106
|
+
else if (!outlivesThisRequest)
|
|
2067
2107
|
ctx.signal.addEventListener("abort", onHostAbort, { once: true });
|
|
2068
2108
|
}
|
|
2069
2109
|
const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
|
|
@@ -2229,6 +2269,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2229
2269
|
sessionId: bgChildSessionId,
|
|
2230
2270
|
...(forwardDurableApproval ? { durableApproval: { ...ctx.durableApprovalForChildren } } : {}),
|
|
2231
2271
|
...(reviveRow !== undefined ? { requireExistingSession: true } : {}),
|
|
2272
|
+
...(outlivesThisRequest ? { onQuestion: undefined } : {}),
|
|
2232
2273
|
};
|
|
2233
2274
|
let reviveAttachedResolve;
|
|
2234
2275
|
const reviveAttached = reviveRow !== undefined ? new Promise((r) => (reviveAttachedResolve = r)) : undefined;
|
|
@@ -2729,6 +2770,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2729
2770
|
sink: ctx.onSubagentSpawn,
|
|
2730
2771
|
...(opts.background ? { registry: opts.background.registry } : {}),
|
|
2731
2772
|
...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
|
|
2773
|
+
...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
|
|
2732
2774
|
});
|
|
2733
2775
|
notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
|
|
2734
2776
|
resume,
|
|
@@ -2854,16 +2896,16 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2854
2896
|
].filter(Boolean);
|
|
2855
2897
|
return {
|
|
2856
2898
|
content: lines.join("\n"),
|
|
2857
|
-
details: {
|
|
2858
|
-
...
|
|
2859
|
-
|
|
2899
|
+
details: completedAgentCard(child, {
|
|
2900
|
+
...(def !== undefined ? { subagentType: def.name } : {}),
|
|
2901
|
+
error: errClass,
|
|
2902
|
+
...(modelFallback !== undefined ? { modelFallback } : {}),
|
|
2860
2903
|
...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
|
|
2861
|
-
...(child.model !== undefined ? { resolvedModel: child.model } : {}),
|
|
2862
2904
|
...(() => {
|
|
2863
2905
|
const ts = toolStatsCounter.snapshot();
|
|
2864
2906
|
return ts !== undefined ? { toolStats: ts } : {};
|
|
2865
2907
|
})(),
|
|
2866
|
-
},
|
|
2908
|
+
}),
|
|
2867
2909
|
};
|
|
2868
2910
|
},
|
|
2869
2911
|
};
|
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 ?? "?"}"): ${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",
|
package/dist/brain/openai.js
CHANGED
|
@@ -481,9 +481,17 @@ export function createOpenAIBrain(config = {}) {
|
|
|
481
481
|
finalContent.push({ type: "text", text: textFace });
|
|
482
482
|
const toolCalls = [];
|
|
483
483
|
const malformed = [];
|
|
484
|
+
const unnamed = [];
|
|
485
|
+
let emptyPlaceholders = 0;
|
|
484
486
|
for (const acc of [...toolAccum.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])) {
|
|
485
|
-
if (!acc.name)
|
|
487
|
+
if (!acc.name) {
|
|
488
|
+
if (!acc.id && acc.args === "") {
|
|
489
|
+
emptyPlaceholders++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
unnamed.push(`id="${acc.id || "?"}"(${acc.args.slice(0, 200)})`);
|
|
486
493
|
continue;
|
|
494
|
+
}
|
|
487
495
|
const tc = closeToolCallAccum(acc);
|
|
488
496
|
if (tc) {
|
|
489
497
|
toolCalls.push(tc);
|
|
@@ -508,16 +516,35 @@ export function createOpenAIBrain(config = {}) {
|
|
|
508
516
|
}
|
|
509
517
|
}
|
|
510
518
|
}
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
519
|
+
const emptySlots = `<empty slot>${emptyPlaceholders > 1 ? ` ×${emptyPlaceholders}` : ""}`;
|
|
520
|
+
const sawRealAction = toolCalls.length > 0 || malformed.length > 0 || unnamed.length > 0;
|
|
521
|
+
if (emptyPlaceholders > 0 && sawRealAction) {
|
|
522
|
+
unnamed.push(emptySlots);
|
|
523
|
+
}
|
|
524
|
+
else if (!sawRealAction && finishReason === "tool_calls") {
|
|
525
|
+
unnamed.push(emptyPlaceholders > 0 ? emptySlots : "<no tool_call delta arrived on the stream>");
|
|
526
|
+
}
|
|
527
|
+
const toolErrorParts = [];
|
|
528
|
+
if (malformed.length > 0) {
|
|
529
|
+
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, finish_reason="${finishReason ?? "?"}"): ${malformed.join("; ")}`);
|
|
530
|
+
}
|
|
531
|
+
if (unnamed.length > 0) {
|
|
532
|
+
toolErrorParts.push(`tool call(s) arrived with no tool name and cannot be executed (finish_reason="${finishReason ?? "?"}"): ${unnamed.join("; ")}`);
|
|
533
|
+
}
|
|
534
|
+
const toolError = toolErrorParts.length > 0 ? toolErrorParts.join(" | ") : undefined;
|
|
514
535
|
const noUsableContent = toolCalls.length === 0 && !accumText.trim();
|
|
515
|
-
if (
|
|
536
|
+
if (malformed.length > 0) {
|
|
516
537
|
finalContent.push({
|
|
517
538
|
type: "text",
|
|
518
539
|
text: `\n[note: ${malformed.length} tool call(s) were truncated (finish_reason="${finishReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
|
|
519
540
|
});
|
|
520
541
|
}
|
|
542
|
+
if (unnamed.length > 0) {
|
|
543
|
+
finalContent.push({
|
|
544
|
+
type: "text",
|
|
545
|
+
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("; ")}]`,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
521
548
|
if (malformedFrames > 0) {
|
|
522
549
|
finalContent.push({
|
|
523
550
|
type: "text",
|
package/dist/core/a2a.js
CHANGED
|
@@ -23,7 +23,7 @@ const A2A_CARD_DESCRIPTION_MAX_CHARS = 240;
|
|
|
23
23
|
const A2A_ID_MAX_CHARS = 160;
|
|
24
24
|
const A2A_RESULT_BODY_MAX_CHARS = 100_000;
|
|
25
25
|
const A2A_ERROR_TEXT_MAX_CHARS = 240;
|
|
26
|
-
const A2A_RESULT_FENCE_REASON = "the peer is an
|
|
26
|
+
const A2A_RESULT_FENCE_REASON = "the peer is an agent acting on its own, so its output is worker text, not tool data";
|
|
27
27
|
const A2A_CARD_PATHS = ["/.well-known/agent-card.json", "/.well-known/agent.json"];
|
|
28
28
|
const A2A_PROTOCOL_VERSION = "1.0";
|
|
29
29
|
const A2A_JSONRPC_TRANSPORT = "JSONRPC";
|