@sema-agent/core 5.8.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/agents/cascade.js +24 -0
  3. package/dist/agents/roster-store.d.ts +1 -0
  4. package/dist/agents/send-message-tool.js +6 -0
  5. package/dist/agents/subagent.d.ts +33 -0
  6. package/dist/agents/subagent.js +125 -33
  7. package/dist/agents/teacher.js +15 -3
  8. package/dist/agents/team.js +10 -0
  9. package/dist/agents/verify.js +7 -0
  10. package/dist/brain/anthropic.js +27 -10
  11. package/dist/brain/open-responses.d.ts +11 -0
  12. package/dist/brain/open-responses.js +736 -0
  13. package/dist/brain/openai.js +32 -5
  14. package/dist/brain/request-params.d.ts +1 -0
  15. package/dist/brain/request-params.js +16 -0
  16. package/dist/core/a2a.js +1 -1
  17. package/dist/core/fs-write-gate-policy.js +2 -2
  18. package/dist/core/lsp-diagnostics.d.ts +3 -2
  19. package/dist/core/lsp-diagnostics.js +20 -7
  20. package/dist/core/memory-recall.js +8 -3
  21. package/dist/core/memory.d.ts +5 -0
  22. package/dist/core/memory.js +6 -4
  23. package/dist/core/runner/assemble-result.d.ts +1 -0
  24. package/dist/core/runner/assemble-result.js +14 -7
  25. package/dist/core/runner/prepare-task.d.ts +9 -1
  26. package/dist/core/runner/prepare-task.js +51 -14
  27. package/dist/core/runner/runtask.d.ts +12 -0
  28. package/dist/core/runner/runtask.js +153 -42
  29. package/dist/core/runner/session-file-state-replay.d.ts +7 -0
  30. package/dist/core/runner/session-file-state-replay.js +56 -0
  31. package/dist/core/runner/session-rule-policy.d.ts +1 -0
  32. package/dist/core/runner/session-rule-policy.js +4 -3
  33. package/dist/core/runner/synthetic-tools.js +1 -1
  34. package/dist/core/runner/tool-output-projection.js +5 -4
  35. package/dist/core/session-reconcile.d.ts +7 -3
  36. package/dist/core/session-reconcile.js +3 -2
  37. package/dist/core/strategy-store.d.ts +1 -1
  38. package/dist/core/strategy-store.js +27 -4
  39. package/dist/core/task-registry-shared.d.ts +0 -1
  40. package/dist/core/tool-policy.d.ts +8 -0
  41. package/dist/core/tool-policy.js +11 -0
  42. package/dist/core/tools.js +9 -1
  43. package/dist/core/trace.d.ts +0 -2
  44. package/dist/core/types.d.ts +8 -1
  45. package/dist/engine/harness/agent-harness.d.ts +1 -0
  46. package/dist/engine/harness/agent-harness.js +3 -0
  47. package/dist/engine/harness/types.d.ts +1 -0
  48. package/dist/engine/llm/types.d.ts +2 -73
  49. package/dist/engine/loop/agent-loop.js +168 -22
  50. package/dist/engine/loop/types.d.ts +1 -0
  51. package/dist/engine/session/repo-utils.d.ts +1 -2
  52. package/dist/engine/session/repo-utils.js +0 -7
  53. package/dist/index.d.ts +3 -2
  54. package/dist/index.js +2 -1
  55. package/dist/internal/llm.d.ts +1 -1
  56. package/dist/orchestration/run-workflow-tool.js +1 -1
  57. package/dist/orchestration/workflow-governance.js +19 -0
  58. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  59. package/dist/orchestration/workflow-primitives.js +4 -1
  60. package/dist/orchestration/workflow.js +15 -6
  61. package/dist/prompts/coordinator.d.ts +1 -1
  62. package/dist/prompts/coordinator.js +1 -1
  63. package/dist/stores/file/memory-store.js +3 -7
  64. package/dist/tools/fs/fs-bash.js +3 -3
  65. package/dist/tools/fs/fs-shared.d.ts +1 -0
  66. package/dist/tools/fs/fs-shared.js +4 -0
  67. package/dist/tools/web.js +20 -20
  68. package/package.json +5 -3
@@ -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;
@@ -40,7 +40,6 @@ export interface SemaTaskHandle {
40
40
  createdAt: number;
41
41
  updatedAt: number;
42
42
  outputFile?: string;
43
- outputOffset?: number;
44
43
  completionId?: string;
45
44
  }
46
45
  export declare function mintCompletionId(target: {
@@ -70,6 +70,11 @@ export declare function createTranscriptIntegrityPolicy(opts?: {
70
70
  readAllow?: readonly string[];
71
71
  tools?: string[];
72
72
  }): ToolPolicy;
73
+ export interface AskDelegationProvenance {
74
+ readonly parentToolCallId: string;
75
+ readonly depth: number;
76
+ readonly agentName?: string;
77
+ }
73
78
  export interface AskRequest {
74
79
  toolName: string;
75
80
  toolCallId: string;
@@ -81,12 +86,15 @@ export interface AskRequest {
81
86
  readonly fromSubagent?: true;
82
87
  readonly sourceAgentName?: string;
83
88
  readonly requiresRealApproval?: boolean;
89
+ readonly delegation?: AskDelegationProvenance;
84
90
  }
85
91
  export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal) => AskOutcome | Promise<AskOutcome>);
86
92
  export type AskOutcome = boolean | "unavailable" | {
87
93
  allow: boolean;
88
94
  updatedInput?: unknown;
89
95
  };
96
+ export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDelegationProvenance): OnAsk;
97
+ export declare function askApproverIdentity(onAsk: unknown): unknown;
90
98
  export type ResolvedAsk = PermissionResult & {
91
99
  approverUnavailable?: true;
92
100
  presentedInput?: unknown;
@@ -516,6 +516,17 @@ export function createTranscriptIntegrityPolicy(opts) {
516
516
  },
517
517
  };
518
518
  }
519
+ const delegatedApproverRoot = new WeakMap();
520
+ export function withDelegationProvenance(onAsk, delegation) {
521
+ if (typeof onAsk !== "function")
522
+ return onAsk;
523
+ const wrapper = (req, signal) => req.delegation !== undefined ? onAsk(req, signal) : onAsk({ ...req, delegation }, signal);
524
+ delegatedApproverRoot.set(wrapper, askApproverIdentity(onAsk));
525
+ return wrapper;
526
+ }
527
+ export function askApproverIdentity(onAsk) {
528
+ return typeof onAsk === "function" ? (delegatedApproverRoot.get(onAsk) ?? onAsk) : onAsk;
529
+ }
519
530
  function tryCloneArgs(v) {
520
531
  try {
521
532
  const value = structuredClone(v);
@@ -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)
@@ -134,8 +134,6 @@ export type TraceEvent = {
134
134
  firstTokenMs?: number;
135
135
  callStartedAt?: number;
136
136
  costMicroUsd?: number;
137
- callCap?: number;
138
- capThinkingSkipped?: boolean;
139
137
  stopReason?: string;
140
138
  ts: number;
141
139
  } | {
@@ -98,6 +98,10 @@ export interface ToolExecuteContext {
98
98
  model?: Model;
99
99
  thinkingLevel?: ThinkingLevel;
100
100
  principal?: string;
101
+ onAsk?: import("./tool-policy.js").OnAsk;
102
+ onQuestion?: import("./ask-question.js").OnQuestion;
103
+ handsReadOnly?: true;
104
+ interactiveTools?: false;
101
105
  oneShot?: boolean;
102
106
  clientContext?: TaskSpec["clientContext"];
103
107
  excludeTools?: readonly string[];
@@ -116,6 +120,7 @@ export interface ToolExecuteContext {
116
120
  scope: string;
117
121
  ttlMs?: number;
118
122
  };
123
+ checkpointStoreDisabledForChildren?: true;
119
124
  taskId?: string;
120
125
  sessionId?: string;
121
126
  backgroundScope?: "task" | "session";
@@ -317,7 +322,7 @@ export interface TaskSpec {
317
322
  resilience?: ResilienceOptions;
318
323
  finalVerification?: boolean;
319
324
  maxSuspends?: number;
320
- checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
325
+ checkpointStore?: import("./checkpoint-store.js").CheckpointStore | null;
321
326
  handsReadOnly?: boolean;
322
327
  additionalDirectories?: string[];
323
328
  enablePlanMode?: boolean;
@@ -410,6 +415,7 @@ export interface TaskResult {
410
415
  remoteEnvFailures?: RemoteEnvFailureNote[];
411
416
  errorMessage?: string;
412
417
  errorCode?: string;
418
+ retryAfterMs?: number;
413
419
  degraded?: {
414
420
  from: string;
415
421
  to: string;
@@ -523,6 +529,7 @@ export type TaskEvent = ({
523
529
  isError: boolean;
524
530
  output?: unknown;
525
531
  structured?: unknown;
532
+ errorCode?: string;
526
533
  truncated?: boolean;
527
534
  totalChars?: number;
528
535
  } & TaskEventIdentity) | ({
@@ -50,6 +50,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
50
50
  private maxOutputTokens?;
51
51
  private maxOutputTokensPerCall?;
52
52
  private stallTimeoutsPerCall?;
53
+ private abortResultDetails?;
53
54
  private loopTrace?;
54
55
  private resilience?;
55
56
  private maxToolConcurrency?;
@@ -186,6 +186,7 @@ export class AgentHarness {
186
186
  maxOutputTokens;
187
187
  maxOutputTokensPerCall;
188
188
  stallTimeoutsPerCall;
189
+ abortResultDetails;
189
190
  loopTrace;
190
191
  resilience;
191
192
  maxToolConcurrency;
@@ -216,6 +217,7 @@ export class AgentHarness {
216
217
  this.maxOutputTokens = options.maxOutputTokens;
217
218
  this.maxOutputTokensPerCall = options.maxOutputTokensPerCall;
218
219
  this.stallTimeoutsPerCall = options.stallTimeoutsPerCall;
220
+ this.abortResultDetails = options.abortResultDetails;
219
221
  this.loopTrace = options.loopTrace;
220
222
  this.resilience = options.resilience;
221
223
  this.maxToolConcurrency = options.maxToolConcurrency;
@@ -440,6 +442,7 @@ export class AgentHarness {
440
442
  ...(this.maxOutputTokens !== undefined ? { maxTokens: this.maxOutputTokens } : {}),
441
443
  ...(this.maxOutputTokensPerCall !== undefined ? { maxTokensPerCall: this.maxOutputTokensPerCall } : {}),
442
444
  ...(this.stallTimeoutsPerCall !== undefined ? { stallTimeoutsPerCall: this.stallTimeoutsPerCall } : {}),
445
+ ...(this.abortResultDetails !== undefined ? { abortResultDetails: this.abortResultDetails } : {}),
443
446
  ...(this.resilience !== undefined ? { resilience: this.resilience } : {}),
444
447
  ...(this.maxToolConcurrency !== undefined ? { maxToolConcurrency: this.maxToolConcurrency } : {}),
445
448
  ...(this.streamingToolExecution === true && (this.getHandlers("tool_call")?.size ?? 0) === 0
@@ -542,6 +542,7 @@ export interface AgentHarnessOptions<TSkill extends Skill = Skill, TPromptTempla
542
542
  maxOutputTokens?: number;
543
543
  maxOutputTokensPerCall?: () => number | undefined;
544
544
  stallTimeoutsPerCall?: () => import("../llm/types.js").StallTimeouts | undefined;
545
+ abortResultDetails?: () => Record<string, unknown> | undefined;
545
546
  loopTrace?: (step: import("../loop/agent-loop.js").LoopStep) => void;
546
547
  resilience?: import("../llm/types.js").ResilienceOptions;
547
548
  maxToolConcurrency?: number;
@@ -68,15 +68,9 @@ export interface SimpleStreamOptions extends StreamOptions {
68
68
  }
69
69
  export type StreamFunction<TApi extends Api = Api, TOptions extends StreamOptions = StreamOptions> = (model: Model<TApi>, context: Context, options?: TOptions) => AssistantMessageEventStreamContract;
70
70
  export type ImagesFunction<TApi extends ImagesApi = ImagesApi, TOptions extends ImagesOptions = ImagesOptions> = (model: ImagesModel<TApi>, context: ImagesContext, options?: TOptions) => Promise<AssistantImages>;
71
- export interface TextSignatureV1 {
72
- v: 1;
73
- id: string;
74
- phase?: "commentary" | "final_answer";
75
- }
76
71
  export interface TextContent {
77
72
  type: "text";
78
73
  text: string;
79
- textSignature?: string;
80
74
  }
81
75
  export interface ThinkingContent {
82
76
  type: "thinking";
@@ -99,7 +93,6 @@ export interface ToolCall {
99
93
  id: string;
100
94
  name: string;
101
95
  arguments: Record<string, unknown>;
102
- thoughtSignature?: string;
103
96
  executionMode?: "sequential" | "parallel";
104
97
  }
105
98
  export interface Usage {
@@ -259,28 +252,15 @@ export interface AssistantMessageEventStreamLike extends AsyncIterable<Assistant
259
252
  result(): Promise<AssistantMessage>;
260
253
  }
261
254
  export interface OpenAICompletionsCompat {
262
- supportsStore?: boolean;
263
- supportsDeveloperRole?: boolean;
264
255
  supportsReasoningEffort?: boolean;
265
256
  reasoningEffortLevels?: ThinkingLevel[];
266
- supportsUsageInStreaming?: boolean;
267
257
  maxTokensField?: "max_completion_tokens" | "max_tokens";
268
- requiresToolResultName?: boolean;
269
- requiresAssistantAfterToolResult?: boolean;
270
- requiresThinkingAsText?: boolean;
271
258
  requiresReasoningContentOnAssistantMessages?: boolean;
272
259
  thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
273
- openRouterRouting?: OpenRouterRouting;
274
- vercelGatewayRouting?: VercelGatewayRouting;
275
- zaiToolStream?: boolean;
276
- supportsStrictMode?: boolean;
277
- cacheControlFormat?: "anthropic";
278
- sendSessionAffinityHeaders?: boolean;
279
- supportsLongCacheRetention?: boolean;
280
260
  }
281
261
  export interface OpenAIResponsesCompat {
282
- sendSessionIdHeader?: boolean;
283
- supportsLongCacheRetention?: boolean;
262
+ supportsReasoningEffort?: boolean;
263
+ reasoningEffortLevels?: ThinkingLevel[];
284
264
  }
285
265
  export interface AnthropicMessagesCompat {
286
266
  thinkingMode?: "budget" | "adaptive";
@@ -288,48 +268,6 @@ export interface AnthropicMessagesCompat {
288
268
  supportsTemperature?: boolean;
289
269
  contextManagement?: boolean;
290
270
  interleavedThinking?: boolean;
291
- supportsEagerToolInputStreaming?: boolean;
292
- supportsLongCacheRetention?: boolean;
293
- sendSessionAffinityHeaders?: boolean;
294
- supportsCacheControlOnTools?: boolean;
295
- }
296
- export interface OpenRouterRouting {
297
- allow_fallbacks?: boolean;
298
- require_parameters?: boolean;
299
- data_collection?: "deny" | "allow";
300
- zdr?: boolean;
301
- enforce_distillable_text?: boolean;
302
- order?: string[];
303
- only?: string[];
304
- ignore?: string[];
305
- quantizations?: string[];
306
- sort?: string | {
307
- by?: string;
308
- partition?: string | null;
309
- };
310
- max_price?: {
311
- prompt?: number | string;
312
- completion?: number | string;
313
- image?: number | string;
314
- audio?: number | string;
315
- request?: number | string;
316
- };
317
- preferred_min_throughput?: number | {
318
- p50?: number;
319
- p75?: number;
320
- p90?: number;
321
- p99?: number;
322
- };
323
- preferred_max_latency?: number | {
324
- p50?: number;
325
- p75?: number;
326
- p90?: number;
327
- p99?: number;
328
- };
329
- }
330
- export interface VercelGatewayRouting {
331
- only?: string[];
332
- order?: string[];
333
271
  }
334
272
  export interface Model<TApi extends Api = Api> {
335
273
  id: string;
@@ -357,15 +295,6 @@ export interface Model<TApi extends Api = Api> {
357
295
  headers?: Record<string, string>;
358
296
  promptGuidance?: string[];
359
297
  compat?: TApi extends "openai-completions" ? OpenAICompletionsCompat : TApi extends "openai-responses" ? OpenAIResponsesCompat : TApi extends "anthropic-messages" ? AnthropicMessagesCompat : never;
360
- mediaInput?: {
361
- image?: {
362
- maxBytes?: number;
363
- maxPixels?: number;
364
- maxSidePx?: number;
365
- preferredSidePx?: number;
366
- tokenMode?: "tile" | "detail" | "provider";
367
- };
368
- };
369
298
  }
370
299
  export interface ImagesModel<TApi extends ImagesApi = ImagesApi> extends Omit<Model, "api" | "provider" | "reasoning" | "contextWindow" | "maxTokens" | "compat"> {
371
300
  api: TApi;
@@ -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);
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);
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);
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"),
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) {
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"), 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") {
@@ -85,6 +85,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
85
85
  getFollowUpMessages?: () => Promise<AgentMessage[]>;
86
86
  toolExecution?: ToolExecutionMode;
87
87
  beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
88
+ abortResultDetails?: () => Record<string, unknown> | undefined;
88
89
  afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
89
90
  }
90
91
  export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
@@ -1,9 +1,8 @@
1
- import { type FileError, type Result, type SessionMetadata, type SessionStorage, type SessionTreeEntry } from "../harness/types.js";
1
+ import { type SessionMetadata, type SessionStorage, type SessionTreeEntry } from "../harness/types.js";
2
2
  import type { Session } from "../harness/types.js";
3
3
  export declare function createSessionId(): string;
4
4
  export declare function createTimestamp(): string;
5
5
  export declare function toSession<TMetadata extends SessionMetadata>(storage: SessionStorage<TMetadata>): Session<TMetadata>;
6
- export declare function getFileSystemResultOrThrow<TValue>(result: Result<TValue, FileError>, message: string): TValue;
7
6
  export declare function getEntriesToFork(storage: SessionStorage, options: {
8
7
  entryId?: string;
9
8
  position?: "before" | "at";