@sema-agent/core 5.9.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/agents/roster-store.d.ts +1 -0
  3. package/dist/agents/send-message-tool.js +6 -0
  4. package/dist/agents/subagent.d.ts +31 -1
  5. package/dist/agents/subagent.js +67 -21
  6. package/dist/agents/teacher.js +15 -3
  7. package/dist/agents/team.js +10 -0
  8. package/dist/agents/verify.js +7 -0
  9. package/dist/brain/anthropic.js +27 -10
  10. package/dist/brain/open-responses.js +19 -4
  11. package/dist/brain/openai.js +32 -5
  12. package/dist/core/a2a.js +1 -1
  13. package/dist/core/background-agent-store.d.ts +2 -1
  14. package/dist/core/background-agent-store.js +1 -0
  15. package/dist/core/checkpoint-store.d.ts +2 -0
  16. package/dist/core/checkpoint-store.js +1 -0
  17. package/dist/core/memory-recall.js +8 -3
  18. package/dist/core/memory.d.ts +5 -0
  19. package/dist/core/memory.js +6 -4
  20. package/dist/core/runner/assemble-result.js +9 -0
  21. package/dist/core/runner/prepare-task.d.ts +12 -1
  22. package/dist/core/runner/prepare-task.js +110 -38
  23. package/dist/core/runner/runtask.d.ts +12 -0
  24. package/dist/core/runner/runtask.js +169 -37
  25. package/dist/core/runner/session-file-state-replay.d.ts +7 -0
  26. package/dist/core/runner/session-file-state-replay.js +56 -0
  27. package/dist/core/runner/synthetic-tools.js +1 -1
  28. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  29. package/dist/core/runner/tool-disclosure.js +24 -9
  30. package/dist/core/runner/tool-output-projection.js +5 -4
  31. package/dist/core/runner/turn-attachments.d.ts +2 -0
  32. package/dist/core/runner/turn-attachments.js +14 -5
  33. package/dist/core/session-reconcile.d.ts +7 -3
  34. package/dist/core/session-reconcile.js +3 -2
  35. package/dist/core/strategy-store.d.ts +1 -1
  36. package/dist/core/strategy-store.js +27 -4
  37. package/dist/core/task-registry-agent.d.ts +3 -0
  38. package/dist/core/task-registry-agent.js +9 -2
  39. package/dist/core/task-registry-shared.d.ts +1 -0
  40. package/dist/core/task-registry.d.ts +2 -0
  41. package/dist/core/tools.js +9 -1
  42. package/dist/core/trace.d.ts +1 -0
  43. package/dist/core/types.d.ts +8 -1
  44. package/dist/engine/loop/agent-loop.js +168 -22
  45. package/dist/orchestration/run-workflow-tool.js +1 -1
  46. package/dist/orchestration/workflow-governance.js +19 -0
  47. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  48. package/dist/orchestration/workflow-primitives.js +4 -1
  49. package/dist/orchestration/workflow.js +1 -1
  50. package/dist/prompts/coordinator.d.ts +1 -1
  51. package/dist/prompts/coordinator.js +1 -1
  52. package/dist/prompts/default.d.ts +1 -0
  53. package/dist/prompts/default.js +3 -0
  54. package/dist/stores/file/memory-store.js +3 -7
  55. package/dist/tools/fs/fs-bash.js +7 -4
  56. package/dist/tools/fs/fs-shared.d.ts +1 -0
  57. package/dist/tools/fs/fs-shared.js +4 -0
  58. package/dist/tools/fs/index.d.ts +1 -0
  59. package/dist/tools/fs/index.js +7 -4
  60. package/dist/tools/web.d.ts +21 -1
  61. package/dist/tools/web.js +126 -11
  62. package/package.json +2 -2
@@ -6,10 +6,14 @@ export interface OrphanToolCall {
6
6
  toolName: string;
7
7
  kind?: "result";
8
8
  }
9
+ export type ReconciledErrorKind = "interrupted_never_started" | "interrupted_outcome_unknown";
10
+ export type RecoveredOrphan = OrphanToolCall & {
11
+ entryId: string;
12
+ text: string;
13
+ errorKind: ReconciledErrorKind;
14
+ };
9
15
  export interface ReconcileReport {
10
- recovered: Array<OrphanToolCall & {
11
- entryId: string;
12
- }>;
16
+ recovered: RecoveredOrphan[];
13
17
  }
14
18
  export declare function findOrphanToolCalls(messages: AgentMessage[], suspendedBatch?: ReadonlySet<string>): OrphanToolCall[];
15
19
  export declare function reconcileInterruptedSession(session: Session, toolEffects?: Map<string, ToolEffect>, suspendedBatch?: ReadonlySet<string>, startedToolCallIds?: ReadonlySet<string>): Promise<ReconcileReport>;
@@ -75,16 +75,17 @@ export async function reconcileInterruptedSession(session, toolEffects, suspende
75
75
  const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
76
76
  const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(orphan.toolCallId);
77
77
  const text = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
78
+ const errorKind = neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown";
78
79
  const entryId = await session.appendMessage({
79
80
  role: "toolResult",
80
81
  toolCallId: orphan.toolCallId,
81
82
  toolName: orphan.toolName,
82
83
  content: [{ type: "text", text }],
83
- details: { errorKind: neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown" },
84
+ details: { errorKind },
84
85
  isError: true,
85
86
  timestamp: Date.now(),
86
87
  });
87
- recovered.push({ ...orphan, entryId });
88
+ recovered.push({ ...orphan, entryId, text, errorKind });
88
89
  }
89
90
  return { recovered };
90
91
  }
@@ -14,8 +14,8 @@ export interface StrategyStore {
14
14
  prune?(scope: string, maxSize: number): Promise<void> | void;
15
15
  }
16
16
  export declare class InMemoryStrategyStore implements StrategyStore {
17
- private readonly maxPerScope;
18
17
  private byScope;
18
+ private readonly maxPerScope;
19
19
  constructor(maxPerScope?: number);
20
20
  save(s: StoredStrategy): void;
21
21
  find(scope: string, query: string, limit: number): StoredStrategy[];
@@ -1,3 +1,25 @@
1
+ function invalidKnob(message, code) {
2
+ const e = new Error(message);
3
+ e.code = code;
4
+ return e;
5
+ }
6
+ function resolveFindLimit(limit) {
7
+ if (limit === Number.POSITIVE_INFINITY)
8
+ return limit;
9
+ if (!Number.isInteger(limit) || limit < 0) {
10
+ throw invalidKnob(`StrategyStore.find: limit must be a non-negative integer, or Infinity for "no cap" (got ${String(limit)})`, "config.strategy_find_limit_invalid");
11
+ }
12
+ return limit;
13
+ }
14
+ function resolveCapacityCap(name, cap) {
15
+ if (cap === Number.POSITIVE_INFINITY) {
16
+ throw invalidKnob(`InMemoryStrategyStore ${name} cannot be Infinity — a per-scope capacity cap can be widened but not turned off; pass a large finite integer instead`, "config.strategy_max_size_invalid");
17
+ }
18
+ if (!Number.isInteger(cap) || cap < 0) {
19
+ throw invalidKnob(`InMemoryStrategyStore ${name} must be a non-negative integer (got ${String(cap)})`, "config.strategy_max_size_invalid");
20
+ }
21
+ return cap;
22
+ }
1
23
  const STOP = new Set([
2
24
  "the", "and", "for", "with", "from", "this", "that", "into", "your", "you", "are", "was", "were", "has",
3
25
  "have", "had", "not", "but", "all", "any", "can", "use", "using", "via", "then", "than", "out", "get",
@@ -24,10 +46,10 @@ function score(s) {
24
46
  return (s.confidence + 1) * (1 / (1 + ageDays(s.ts)));
25
47
  }
26
48
  export class InMemoryStrategyStore {
27
- maxPerScope;
28
49
  byScope = new Map();
50
+ maxPerScope;
29
51
  constructor(maxPerScope = 100) {
30
- this.maxPerScope = maxPerScope;
52
+ this.maxPerScope = resolveCapacityCap("maxPerScope", maxPerScope);
31
53
  }
32
54
  save(s) {
33
55
  const arr = this.byScope.get(s.scope) ?? [];
@@ -49,6 +71,7 @@ export class InMemoryStrategyStore {
49
71
  this.byScope.set(s.scope, arr);
50
72
  }
51
73
  find(scope, query, limit) {
74
+ const cap = resolveFindLimit(limit);
52
75
  const arr = this.byScope.get(scope);
53
76
  if (!arr || arr.length === 0) {
54
77
  return [];
@@ -61,11 +84,11 @@ export class InMemoryStrategyStore {
61
84
  const probTokens = new Set(tokens(s.problem));
62
85
  return qSig.every((t) => probTokens.has(t));
63
86
  });
64
- return matches.sort((a, b) => score(b) - score(a)).slice(0, Math.max(0, Number.isFinite(limit) ? limit : 0));
87
+ return matches.sort((a, b) => score(b) - score(a)).slice(0, cap);
65
88
  }
66
89
  prune(scope, maxSize) {
90
+ const n = resolveCapacityCap("maxSize", maxSize);
67
91
  const arr = this.byScope.get(scope);
68
- const n = Math.max(0, maxSize);
69
92
  if (arr && arr.length > n) {
70
93
  arr.sort((a, b) => score(b) - score(a));
71
94
  arr.length = n;
@@ -69,6 +69,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
69
69
  errorCode?: string;
70
70
  retryable?: boolean;
71
71
  errorKind?: string;
72
+ retryAfterMs?: number;
72
73
  stoppedBy?: StopSource;
73
74
  seq?: number;
74
75
  cycle?: number;
@@ -105,6 +106,7 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
105
106
  errorCode?: string;
106
107
  retryable?: boolean;
107
108
  errorKind?: string;
109
+ retryAfterMs?: number;
108
110
  }): "completed" | "failed" | "killed" | undefined;
109
111
  export declare function noteBackgroundAgentActivityLane(core: DurableAgentCore, id: string, now?: number): void;
110
112
  export declare function reapStaleSessionBackgroundAgentsLane(core: DurableAgentCore, staleMs: number, now?: number, onTerminal?: (note: () => void) => void): number;
@@ -137,6 +139,7 @@ export interface AgentPollDetailsInput {
137
139
  error?: string;
138
140
  errorCode?: string;
139
141
  errorRetryable?: boolean;
142
+ errorRetryAfterMs?: number;
140
143
  resultIsPartial?: boolean;
141
144
  completionId?: string;
142
145
  }
@@ -713,6 +713,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
713
713
  handle.errorRetryable = outcome.retryable;
714
714
  if (outcome.errorKind !== undefined)
715
715
  handle.errorKind = outcome.errorKind;
716
+ if (outcome.retryAfterMs !== undefined)
717
+ handle.errorRetryAfterMs = outcome.retryAfterMs;
716
718
  }
717
719
  handle.updatedAt = Date.now();
718
720
  if (outcome.seq !== undefined)
@@ -731,6 +733,7 @@ export function settleBackgroundAgentLane(core, id, outcome) {
731
733
  ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
732
734
  ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
733
735
  ...(handle.errorKind !== undefined ? { errorKind: handle.errorKind } : {}),
736
+ ...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
734
737
  }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
735
738
  return outcome.status;
736
739
  }
@@ -857,6 +860,7 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
857
860
  handle.errorCode = undefined;
858
861
  handle.errorRetryable = undefined;
859
862
  handle.errorKind = undefined;
863
+ handle.errorRetryAfterMs = undefined;
860
864
  handle.resultIsPartial = undefined;
861
865
  handle.stopSource = undefined;
862
866
  handle.completionId = undefined;
@@ -1052,6 +1056,7 @@ export function buildAgentPollDetails(input) {
1052
1056
  ...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
1053
1057
  ...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
1054
1058
  ...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
1059
+ ...(failed && input.errorRetryAfterMs !== undefined ? { retryAfterMs: input.errorRetryAfterMs } : {}),
1055
1060
  ...(input.resultIsPartial === true ? { partial_result: true } : {}),
1056
1061
  ...(input.completionId !== undefined ? { completionId: input.completionId } : {}),
1057
1062
  };
@@ -1070,7 +1075,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1070
1075
  };
1071
1076
  }
1072
1077
  const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
1073
- ? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable})`
1078
+ ? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable}${row.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${row.errorRetryAfterMs}` : ""})`
1074
1079
  : "";
1075
1080
  const body = `status: ${row.status}
1076
1081
  ${row.error ? `error: ${row.error}${kindClause}
@@ -1087,6 +1092,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1087
1092
  ...(row.error !== undefined ? { error: row.error } : {}),
1088
1093
  ...(row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1089
1094
  ...(row.errorRetryable !== undefined ? { errorRetryable: row.errorRetryable } : {}),
1095
+ ...(row.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: row.errorRetryAfterMs } : {}),
1090
1096
  ...(row.resultIsPartial === true ? { resultIsPartial: true } : {}),
1091
1097
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1092
1098
  }),
@@ -1126,7 +1132,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1126
1132
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
1127
1133
  const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
1128
1134
  const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
1129
- ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
1135
+ ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable}${handle.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${handle.errorRetryAfterMs}` : ""})`
1130
1136
  : "";
1131
1137
  const body = running
1132
1138
  ? oneShot === true
@@ -1149,6 +1155,7 @@ ${resultText}` : "(no result text)"}`;
1149
1155
  ...(handle.error !== undefined ? { error: handle.error } : {}),
1150
1156
  ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1151
1157
  ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
1158
+ ...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
1152
1159
  ...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
1153
1160
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1154
1161
  }),
@@ -139,6 +139,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
139
139
  errorCode?: string;
140
140
  errorRetryable?: boolean;
141
141
  errorKind?: string;
142
+ errorRetryAfterMs?: number;
142
143
  resultIsPartial?: boolean;
143
144
  stopSource?: StopSource;
144
145
  stoppedBy?: StopSource;
@@ -141,6 +141,7 @@ export declare class TaskRegistry {
141
141
  errorCode?: string;
142
142
  retryable?: boolean;
143
143
  errorKind?: string;
144
+ retryAfterMs?: number;
144
145
  stoppedBy?: StopSource;
145
146
  seq?: number;
146
147
  cycle?: number;
@@ -180,6 +181,7 @@ export declare class TaskRegistry {
180
181
  errorCode?: string;
181
182
  retryable?: boolean;
182
183
  errorKind?: string;
184
+ retryAfterMs?: number;
183
185
  }): "completed" | "failed" | "killed" | undefined;
184
186
  unmarkRetainedContinuation(id: string): void;
185
187
  attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
@@ -64,7 +64,15 @@ export function defineTool(spec, options) {
64
64
  ret = await spec.execute(params, options?.enrichCtx ? { ...options.enrichCtx(baseCtx), toolCallId, signal } : baseCtx);
65
65
  }
66
66
  catch (err) {
67
- throw new Error(formatToolError(err));
67
+ const wrapped = new Error(formatToolError(err));
68
+ if (err !== null && typeof err === "object") {
69
+ const src = err;
70
+ if (src.details !== undefined)
71
+ wrapped.details = src.details;
72
+ if (typeof src.errorKind === "string")
73
+ wrapped.errorKind = src.errorKind;
74
+ }
75
+ throw wrapped;
68
76
  }
69
77
  const { content, details, terminate, isError } = normalizeContent(ret);
70
78
  const guarded = isEmptyToolContent(content)
@@ -94,6 +94,7 @@ export type TraceEvent = {
94
94
  kind: "config.additional_directory_skipped";
95
95
  version: 1;
96
96
  taskId: string;
97
+ field?: "additionalReadDirectories";
97
98
  entry: string;
98
99
  reason: string;
99
100
  ts: number;
@@ -99,6 +99,9 @@ export interface ToolExecuteContext {
99
99
  thinkingLevel?: ThinkingLevel;
100
100
  principal?: string;
101
101
  onAsk?: import("./tool-policy.js").OnAsk;
102
+ onQuestion?: import("./ask-question.js").OnQuestion;
103
+ handsReadOnly?: true;
104
+ interactiveTools?: false;
102
105
  oneShot?: boolean;
103
106
  clientContext?: TaskSpec["clientContext"];
104
107
  excludeTools?: readonly string[];
@@ -106,6 +109,7 @@ export interface ToolExecuteContext {
106
109
  alwaysLoadTools?: readonly string[];
107
110
  promptProfile?: "simple" | "classic";
108
111
  additionalDirectories?: readonly string[];
112
+ additionalReadDirectories?: readonly string[];
109
113
  envFacts?: TaskSpec["envFacts"];
110
114
  getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
111
115
  activeSkillScope?: () => readonly unknown[];
@@ -117,6 +121,7 @@ export interface ToolExecuteContext {
117
121
  scope: string;
118
122
  ttlMs?: number;
119
123
  };
124
+ checkpointStoreDisabledForChildren?: true;
120
125
  taskId?: string;
121
126
  sessionId?: string;
122
127
  backgroundScope?: "task" | "session";
@@ -293,6 +298,7 @@ export interface TaskSpec {
293
298
  tools?: ToolSpec[];
294
299
  excludeTools?: string[];
295
300
  deferTools?: string[];
301
+ toolMaterializeStrategy?: "swap" | "static";
296
302
  alwaysLoadTools?: string[];
297
303
  deferSelfResolve?: boolean;
298
304
  promptProfile?: "simple" | "classic";
@@ -318,9 +324,10 @@ export interface TaskSpec {
318
324
  resilience?: ResilienceOptions;
319
325
  finalVerification?: boolean;
320
326
  maxSuspends?: number;
321
- checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
327
+ checkpointStore?: import("./checkpoint-store.js").CheckpointStore | null;
322
328
  handsReadOnly?: boolean;
323
329
  additionalDirectories?: string[];
330
+ additionalReadDirectories?: string[];
324
331
  enablePlanMode?: boolean;
325
332
  interactiveTools?: boolean;
326
333
  enableFork?: boolean;
@@ -208,6 +208,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
208
208
  finally {
209
209
  await executor.settle();
210
210
  }
211
+ raiseFatalCancellation(executor);
211
212
  {
212
213
  if (ptl) {
213
214
  const detect = ptlDetect;
@@ -245,6 +246,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
245
246
  finally {
246
247
  await executor.settle();
247
248
  }
249
+ raiseFatalCancellation(executor);
248
250
  }
249
251
  }
250
252
  }
@@ -495,8 +497,7 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
495
497
  };
496
498
  }
497
499
  else {
498
- const executed = await executePreparedToolCall(preparation, signal, emit, config.abortResultDetails);
499
- finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
500
+ finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, preparation, config, signal, emit);
500
501
  }
501
502
  await emitToolExecutionEnd(finalized, emit);
502
503
  const toolResultMessage = createToolResultMessage(finalized);
@@ -559,15 +560,25 @@ function resolveToolConcurrency(configured) {
559
560
  }
560
561
  async function runCapped(thunks, limit) {
561
562
  const results = new Array(thunks.length);
563
+ const failures = new Array(thunks.length);
562
564
  let next = 0;
563
565
  const width = Number.isFinite(limit) ? Math.max(1, Math.min(limit, thunks.length)) : 1;
564
566
  const workers = Array.from({ length: width }, async () => {
565
567
  while (next < thunks.length) {
566
568
  const i = next++;
567
- results[i] = await thunks[i]();
569
+ try {
570
+ results[i] = await thunks[i]();
571
+ }
572
+ catch (error) {
573
+ failures[i] = { error };
574
+ }
568
575
  }
569
576
  });
570
577
  await Promise.all(workers);
578
+ const firstFailure = failures.find((failure) => failure !== undefined);
579
+ if (firstFailure !== undefined) {
580
+ throw firstFailure.error;
581
+ }
571
582
  return results;
572
583
  }
573
584
  async function executeToolCallsPartitioned(currentContext, assistantMessage, toolCalls, config, signal, emit, executor) {
@@ -625,8 +636,11 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
625
636
  return { messages, terminate: shouldTerminateToolBatch(allFinalized) };
626
637
  async function settleBatch(entries, concurrent) {
627
638
  const runOne = (entry) => async () => {
628
- const executed = await executePreparedToolCall(entry.prepared, signal, emit, config.abortResultDetails);
629
- const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, entry.prepared, executed, config, signal);
639
+ const prepared = entry.prepared;
640
+ if (!prepared) {
641
+ throw new Error(`tool call ${entry.toolCall.id} reached execution without a preparation`);
642
+ }
643
+ const finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, prepared, config, signal, emit);
630
644
  await emitToolExecutionEnd(finalized, emit);
631
645
  return finalized;
632
646
  };
@@ -662,6 +676,7 @@ class StreamToolExecutor {
662
676
  admittedOrder = [];
663
677
  barrier = false;
664
678
  inFlight = 0;
679
+ fatalCancellation;
665
680
  constructor(context, config, signal, emit) {
666
681
  this.context = context;
667
682
  this.config = config;
@@ -707,9 +722,11 @@ class StreamToolExecutor {
707
722
  return;
708
723
  }
709
724
  entry.prepared = preparation;
710
- entry.executed = await executePreparedToolCall(preparation, this.signal, this.emit, this.config.abortResultDetails);
725
+ entry.executed = await executePreparedToolCall(preparation, this.signal, this.emit, this.config.abortResultDetails, (error) => this.retainCancellation(error));
711
726
  }
712
727
  catch (error) {
728
+ if (isAbortSemanticsError(error))
729
+ this.retainCancellation(error);
713
730
  entry.immediate = {
714
731
  kind: "immediate",
715
732
  result: createErrorToolResult(error instanceof Error ? error.message : String(error), error),
@@ -724,6 +741,15 @@ class StreamToolExecutor {
724
741
  this.barrier = true;
725
742
  await Promise.all([...this.entries.values()].map((e) => e.promise));
726
743
  }
744
+ retainCancellation(error) {
745
+ this.fatalCancellation ??= error;
746
+ this.barrier = true;
747
+ }
748
+ takeFatalCancellation() {
749
+ const cancellation = this.fatalCancellation;
750
+ this.fatalCancellation = undefined;
751
+ return cancellation;
752
+ }
727
753
  take(id) {
728
754
  const entry = this.entries.get(id);
729
755
  if (entry)
@@ -829,7 +855,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
829
855
  if (signal?.aborted) {
830
856
  return {
831
857
  kind: "immediate",
832
- result: createErrorToolResult("Operation aborted", { details: config.abortResultDetails?.() }),
858
+ result: createErrorToolResult("Operation aborted", { details: safeAbortDetails(config.abortResultDetails) }),
833
859
  isError: true,
834
860
  };
835
861
  }
@@ -869,44 +895,134 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
869
895
  };
870
896
  }
871
897
  }
872
- async function executePreparedToolCall(prepared, signal, emit, abortResultDetails) {
898
+ async function executePreparedToolCall(prepared, signal, emit, abortResultDetails, onLateCancellation) {
873
899
  const updateEvents = [];
874
900
  let acceptingUpdates = true;
875
901
  if (signal?.aborted) {
876
- return { result: createErrorToolResult("operation aborted before execution", { details: abortResultDetails?.() }), isError: true };
902
+ return { result: createErrorToolResult("operation aborted before execution", { details: safeAbortDetails(abortResultDetails) }), isError: true };
877
903
  }
878
904
  const work = (async () => {
905
+ let outcome;
879
906
  try {
880
907
  const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
881
908
  if (!acceptingUpdates) {
882
909
  return;
883
910
  }
884
- updateEvents.push(Promise.resolve(emit({
885
- type: "tool_execution_update",
886
- toolCallId: prepared.toolCall.id,
887
- toolName: prepared.toolCall.name,
888
- args: prepared.toolCall.arguments,
889
- partialResult,
890
- })));
911
+ let delivery;
912
+ try {
913
+ delivery = Promise.resolve(emit({
914
+ type: "tool_execution_update",
915
+ toolCallId: prepared.toolCall.id,
916
+ toolName: prepared.toolCall.name,
917
+ args: prepared.toolCall.arguments,
918
+ partialResult,
919
+ }));
920
+ }
921
+ catch (error) {
922
+ delivery = Promise.reject(error);
923
+ }
924
+ void delivery.catch((reason) => {
925
+ if (isAbortSemanticsError(reason))
926
+ onLateCancellation?.(reason);
927
+ });
928
+ updateEvents.push(delivery);
891
929
  });
892
- acceptingUpdates = false;
893
- await Promise.all(updateEvents);
894
- return { result, isError: result.isError === true };
930
+ outcome = { result, isError: result.isError === true };
895
931
  }
896
932
  catch (error) {
897
- acceptingUpdates = false;
898
- await Promise.all(updateEvents);
899
- return {
933
+ outcome = {
900
934
  result: createErrorToolResult(error instanceof Error ? error.message : String(error), error),
901
935
  isError: true,
902
936
  };
903
937
  }
938
+ acceptingUpdates = false;
939
+ const deliveryFailure = await settleUpdateDeliveries(updateEvents);
940
+ if (!deliveryFailure)
941
+ return outcome;
942
+ const reason = deliveryFailure.reason;
943
+ const details = annotateDeliveryFailure(outcome.result, {
944
+ message: reason instanceof Error ? reason.message : String(reason),
945
+ });
946
+ return details === UNANNOTATED ? outcome : { result: { ...outcome.result, details }, isError: outcome.isError };
904
947
  })();
905
948
  return work;
906
949
  }
950
+ const TOOL_PROGRESS_DELIVERY_FAILED = "toolProgressDeliveryFailed";
951
+ const UNANNOTATED = Symbol("unannotated");
952
+ function annotateDeliveryFailure(result, mark) {
953
+ try {
954
+ const raw = result.details;
955
+ const prior = raw === undefined ? {} : raw;
956
+ const proto = prior === null || typeof prior !== "object" ? undefined : Object.getPrototypeOf(prior);
957
+ if (proto !== Object.prototype && proto !== null)
958
+ return UNANNOTATED;
959
+ const details = { ...prior };
960
+ if (TOOL_PROGRESS_DELIVERY_FAILED in details)
961
+ return UNANNOTATED;
962
+ details[TOOL_PROGRESS_DELIVERY_FAILED] = mark;
963
+ return details;
964
+ }
965
+ catch {
966
+ return UNANNOTATED;
967
+ }
968
+ }
969
+ function readDeliveryFailureMark(details) {
970
+ try {
971
+ if (details === null || typeof details !== "object")
972
+ return undefined;
973
+ const mark = details[TOOL_PROGRESS_DELIVERY_FAILED];
974
+ return mark;
975
+ }
976
+ catch {
977
+ return undefined;
978
+ }
979
+ }
980
+ async function settleUpdateDeliveries(updateEvents) {
981
+ if (updateEvents.length === 0)
982
+ return undefined;
983
+ let reportFailure;
984
+ const firstFailure = new Promise((resolve) => {
985
+ reportFailure = resolve;
986
+ });
987
+ for (const delivery of updateEvents) {
988
+ void delivery.catch((reason) => reportFailure({ reason }));
989
+ }
990
+ const allSettled = Promise.allSettled(updateEvents).then((entries) => {
991
+ for (const entry of entries) {
992
+ if (entry.status === "rejected")
993
+ return { reason: entry.reason };
994
+ }
995
+ return undefined;
996
+ });
997
+ const failure = await Promise.race([firstFailure, allSettled]);
998
+ if (failure && isAbortSemanticsError(failure.reason))
999
+ throw failure.reason;
1000
+ return failure;
1001
+ }
1002
+ function raiseFatalCancellation(executor) {
1003
+ const cancellation = executor.takeFatalCancellation();
1004
+ if (cancellation !== undefined)
1005
+ throw cancellation;
1006
+ }
1007
+ async function executePreparedWithDisclosure(currentContext, assistantMessage, prepared, config, signal, emit) {
1008
+ let executed;
1009
+ try {
1010
+ executed = await executePreparedToolCall(prepared, signal, emit, config.abortResultDetails);
1011
+ }
1012
+ catch (error) {
1013
+ if (isAbortSemanticsError(error))
1014
+ throw error;
1015
+ executed = {
1016
+ result: createErrorToolResult(`[tool execution harness failed: ${error instanceof Error ? error.message : String(error)}]`, error),
1017
+ isError: true,
1018
+ };
1019
+ }
1020
+ return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal);
1021
+ }
907
1022
  async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
908
1023
  let result = executed.result;
909
1024
  let isError = executed.isError;
1025
+ const deliveryMark = readDeliveryFailureMark(executed.result.details);
910
1026
  if (config.afterToolCall) {
911
1027
  try {
912
1028
  const afterResult = await config.afterToolCall({
@@ -930,6 +1046,11 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
930
1046
  const note = `[post-tool processing failed (the tool already executed): ${error instanceof Error ? error.message : String(error)}]`;
931
1047
  result = { ...result, content: [...result.content, { type: "text", text: note }] };
932
1048
  }
1049
+ if (deliveryMark !== undefined && readDeliveryFailureMark(result.details) === undefined) {
1050
+ const details = annotateDeliveryFailure(result, deliveryMark);
1051
+ if (details !== UNANNOTATED)
1052
+ result = { ...result, details };
1053
+ }
933
1054
  }
934
1055
  return {
935
1056
  toolCall: prepared.toolCall,
@@ -937,6 +1058,31 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
937
1058
  isError,
938
1059
  };
939
1060
  }
1061
+ function safeAbortDetails(fn) {
1062
+ try {
1063
+ return fn?.();
1064
+ }
1065
+ catch {
1066
+ return undefined;
1067
+ }
1068
+ }
1069
+ function isAbortSemanticsError(error) {
1070
+ try {
1071
+ let cursor = error;
1072
+ for (let depth = 0; depth < 8 && cursor instanceof Error; depth++) {
1073
+ if (cursor.name === "AbortError")
1074
+ return true;
1075
+ const next = cursor.cause;
1076
+ if (next === cursor)
1077
+ break;
1078
+ cursor = next;
1079
+ }
1080
+ }
1081
+ catch {
1082
+ return false;
1083
+ }
1084
+ return false;
1085
+ }
940
1086
  function createErrorToolResult(message, source) {
941
1087
  let details = {};
942
1088
  if (source !== null && typeof source === "object") {
@@ -380,7 +380,7 @@ export async function createRunWorkflowTool(d) {
380
380
  return structuredError(`workflow script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
381
381
  }
382
382
  const scriptFn = (wfCtx) => {
383
- const primitives = buildWorkflowPrimitives(wfCtx, governance, d.onAgentSpawn, d.parentThinking, principal);
383
+ const primitives = buildWorkflowPrimitives(wfCtx, governance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true);
384
384
  return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result);
385
385
  };
386
386
  if (ctx.signal?.aborted) {
@@ -118,6 +118,25 @@ function pickWhitelist(scriptSpec) {
118
118
  return { safe, modelName };
119
119
  }
120
120
  function clampResourceLimits(safe, base, caps) {
121
+ const trustedCandidates = [
122
+ ["baseline.limits.maxCostUsd", base.limits?.maxCostUsd],
123
+ ["baseline.limits.maxTokens", base.limits?.maxTokens],
124
+ ["baseline.limits.maxWalltimeMs", base.limits?.maxWalltimeMs],
125
+ ["baseline.limits.maxTurns", base.limits?.maxTurns],
126
+ ["caps.childMaxCostUsd", caps?.childMaxCostUsd],
127
+ ["caps.childMaxTokens", caps?.childMaxTokens],
128
+ ["caps.perAgentMaxWalltimeMs", caps?.perAgentMaxWalltimeMs],
129
+ ["caps.childMaxTurns", caps?.childMaxTurns],
130
+ ];
131
+ for (const [label, value] of trustedCandidates) {
132
+ if (value === undefined)
133
+ continue;
134
+ if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
135
+ const e = new Error(`workflow governance: ${label} must be a real number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and dropping it would let a child run this axis unbounded under a limit nobody chose. Use 0 or a negative number to declare "no ceiling from this source" explicitly.`);
136
+ e.code = "config.limit_invalid";
137
+ throw e;
138
+ }
139
+ }
121
140
  const notes = [];
122
141
  const requestedCost = safe.limits?.maxCostUsd;
123
142
  const requestedTokens = safe.limits?.maxTokens;
@@ -8,4 +8,4 @@ export interface WorkflowGovernance {
8
8
  models?: Record<string, Model>;
9
9
  caps?: WorkflowChildCaps;
10
10
  }
11
- export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string): WorkflowPrimitives;
11
+ export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string, parentCheckpointStoreDisabled?: boolean): WorkflowPrimitives;
@@ -22,7 +22,7 @@ function formatResourceClampNote(notes) {
22
22
  const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
23
23
  return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
24
24
  }
25
- export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal) {
25
+ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal, parentCheckpointStoreDisabled) {
26
26
  const agent = (spec, opts) => {
27
27
  if (typeof spec === "string")
28
28
  spec = { objective: spec };
@@ -39,6 +39,9 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
39
39
  if (childSpec.principal === undefined && parentPrincipal !== undefined) {
40
40
  childSpec.principal = parentPrincipal;
41
41
  }
42
+ if (parentCheckpointStoreDisabled === true) {
43
+ childSpec.checkpointStore = null;
44
+ }
42
45
  if (onAgentSpawn) {
43
46
  return ctx.agentStream(childSpec, agentOpts).then((handle) => {
44
47
  onAgentSpawn(handle);