@sema-agent/core 5.7.0 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/agents/cascade.d.ts +1 -1
  3. package/dist/agents/cascade.js +24 -0
  4. package/dist/agents/subagent.d.ts +7 -7
  5. package/dist/agents/subagent.js +74 -33
  6. package/dist/agents/teacher.js +3 -3
  7. package/dist/agents/team.d.ts +1 -1
  8. package/dist/agents/team.js +1 -1
  9. package/dist/agents/verify.js +1 -1
  10. package/dist/brain/anthropic.js +24 -35
  11. package/dist/brain/open-responses.d.ts +11 -0
  12. package/dist/brain/open-responses.js +721 -0
  13. package/dist/brain/openai.js +22 -33
  14. package/dist/brain/request-params.d.ts +1 -0
  15. package/dist/brain/request-params.js +16 -0
  16. package/dist/brain/stream-engine.d.ts +0 -2
  17. package/dist/brain/stream-engine.js +5 -47
  18. package/dist/brain/stream-shared.d.ts +0 -10
  19. package/dist/brain/stream-shared.js +0 -23
  20. package/dist/brain/terminal-cause.d.ts +0 -1
  21. package/dist/brain/terminal-cause.js +0 -3
  22. package/dist/brain/timeout.d.ts +4 -2
  23. package/dist/brain/timeout.js +4 -14
  24. package/dist/config/catalog.d.ts +3 -11
  25. package/dist/config/catalog.js +30 -69
  26. package/dist/config/defaults.d.ts +0 -3
  27. package/dist/config/defaults.js +0 -3
  28. package/dist/core/auto-compaction.d.ts +2 -11
  29. package/dist/core/auto-compaction.js +3 -67
  30. package/dist/core/checkpoint-store.d.ts +10 -8
  31. package/dist/core/checkpoint-store.js +7 -7
  32. package/dist/core/fs-write-gate-policy.js +2 -2
  33. package/dist/core/lsp-diagnostics.d.ts +3 -2
  34. package/dist/core/lsp-diagnostics.js +20 -7
  35. package/dist/core/memory-engine/file-backend.js +20 -17
  36. package/dist/core/memory-engine/layout.d.ts +5 -0
  37. package/dist/core/memory-engine/layout.js +72 -47
  38. package/dist/core/runner/assemble-result.d.ts +3 -8
  39. package/dist/core/runner/assemble-result.js +24 -18
  40. package/dist/core/runner/prepare-task.d.ts +27 -6
  41. package/dist/core/runner/prepare-task.js +209 -144
  42. package/dist/core/runner/runtask.d.ts +0 -2
  43. package/dist/core/runner/runtask.js +228 -266
  44. package/dist/core/runner/session-rule-policy.d.ts +1 -0
  45. package/dist/core/runner/session-rule-policy.js +4 -3
  46. package/dist/core/session-policy-store.d.ts +5 -0
  47. package/dist/core/session-policy-store.js +3 -1
  48. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  49. package/dist/core/task-registry-agent.d.ts +2 -0
  50. package/dist/core/task-registry-agent.js +33 -1
  51. package/dist/core/task-registry-shared.d.ts +0 -1
  52. package/dist/core/task-registry.d.ts +2 -0
  53. package/dist/core/task-registry.js +9 -1
  54. package/dist/core/tool-errors.js +10 -2
  55. package/dist/core/tool-policy.d.ts +8 -0
  56. package/dist/core/tool-policy.js +11 -0
  57. package/dist/core/trace.d.ts +1 -10
  58. package/dist/core/types.d.ts +27 -30
  59. package/dist/core/usage-window-store.d.ts +39 -0
  60. package/dist/core/usage-window-store.js +115 -0
  61. package/dist/engine/execution-env/node-execution-env.js +0 -22
  62. package/dist/engine/harness/agent-harness.d.ts +1 -7
  63. package/dist/engine/harness/agent-harness.js +16 -53
  64. package/dist/engine/harness/types.d.ts +3 -15
  65. package/dist/engine/llm/types.d.ts +3 -75
  66. package/dist/engine/loop/agent-loop.d.ts +1 -1
  67. package/dist/engine/loop/agent-loop.js +7 -173
  68. package/dist/engine/loop/types.d.ts +1 -18
  69. package/dist/engine/session/repo-utils.d.ts +1 -2
  70. package/dist/engine/session/repo-utils.js +0 -7
  71. package/dist/index.d.ts +13 -7
  72. package/dist/index.js +10 -5
  73. package/dist/internal/llm.d.ts +1 -1
  74. package/dist/orchestration/goal.js +1 -5
  75. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  76. package/dist/orchestration/run-workflow-tool.js +1 -2
  77. package/dist/orchestration/workflow-governance.d.ts +6 -6
  78. package/dist/orchestration/workflow-governance.js +33 -47
  79. package/dist/orchestration/workflow.js +14 -5
  80. package/dist/prompt-assembly/event-registry.js +2 -3
  81. package/dist/stores/cc/lockfile.js +9 -11
  82. package/dist/stores/cc/mailbox-store.js +49 -38
  83. package/dist/stores/file/index.d.ts +3 -0
  84. package/dist/stores/file/index.js +4 -0
  85. package/dist/stores/file/usage-window-store.d.ts +9 -0
  86. package/dist/stores/file/usage-window-store.js +86 -0
  87. package/dist/tools/fs/fs-bash.d.ts +1 -10
  88. package/dist/tools/fs/fs-bash.js +20 -60
  89. package/dist/tools/fs/index.d.ts +0 -2
  90. package/dist/tools/fs/index.js +1 -2
  91. package/dist/tools/web.js +20 -19
  92. package/package.json +4 -5
  93. package/dist/bin/sema-tb.d.ts +0 -31
  94. package/dist/bin/sema-tb.js +0 -448
  95. package/dist/bin/tb-env.d.ts +0 -2
  96. package/dist/bin/tb-env.js +0 -17
  97. package/dist/brain/walltime.d.ts +0 -1
  98. package/dist/brain/walltime.js +0 -1
  99. package/dist/core/runner/call-cap.d.ts +0 -67
  100. package/dist/core/runner/call-cap.js +0 -145
  101. package/dist/core/runner/cut-kill.d.ts +0 -10
  102. package/dist/core/runner/cut-kill.js +0 -37
@@ -185,9 +185,8 @@ export class AgentHarness {
185
185
  steeringQueueMode;
186
186
  maxOutputTokens;
187
187
  maxOutputTokensPerCall;
188
- callDeadlineMsPerCall;
189
188
  stallTimeoutsPerCall;
190
- toolDeadline;
189
+ abortResultDetails;
191
190
  loopTrace;
192
191
  resilience;
193
192
  maxToolConcurrency;
@@ -217,9 +216,8 @@ export class AgentHarness {
217
216
  this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
218
217
  this.maxOutputTokens = options.maxOutputTokens;
219
218
  this.maxOutputTokensPerCall = options.maxOutputTokensPerCall;
220
- this.callDeadlineMsPerCall = options.callDeadlineMsPerCall;
221
219
  this.stallTimeoutsPerCall = options.stallTimeoutsPerCall;
222
- this.toolDeadline = options.toolDeadline;
220
+ this.abortResultDetails = options.abortResultDetails;
223
221
  this.loopTrace = options.loopTrace;
224
222
  this.resilience = options.resilience;
225
223
  this.maxToolConcurrency = options.maxToolConcurrency;
@@ -407,7 +405,6 @@ export class AgentHarness {
407
405
  },
408
406
  reasoning: streamOptions?.reasoning,
409
407
  maxTokens: streamOptions?.maxTokens,
410
- callDeadlineMs: streamOptions?.callDeadlineMs,
411
408
  stallTimeouts: streamOptions?.stallTimeouts,
412
409
  maxTokensDynamic: streamOptions?.maxTokensDynamic,
413
410
  staticReasoningCutDowngrade: streamOptions?.staticReasoningCutDowngrade,
@@ -442,12 +439,10 @@ export class AgentHarness {
442
439
  return {
443
440
  model: turnState.model,
444
441
  reasoning: turnState.thinkingLevel,
445
- liveThinkingLevel: () => self.thinkingLevel,
446
442
  ...(this.maxOutputTokens !== undefined ? { maxTokens: this.maxOutputTokens } : {}),
447
443
  ...(this.maxOutputTokensPerCall !== undefined ? { maxTokensPerCall: this.maxOutputTokensPerCall } : {}),
448
- ...(this.callDeadlineMsPerCall !== undefined ? { callDeadlineMsPerCall: this.callDeadlineMsPerCall } : {}),
449
444
  ...(this.stallTimeoutsPerCall !== undefined ? { stallTimeoutsPerCall: this.stallTimeoutsPerCall } : {}),
450
- ...(this.toolDeadline !== undefined ? { toolDeadline: this.toolDeadline } : {}),
445
+ ...(this.abortResultDetails !== undefined ? { abortResultDetails: this.abortResultDetails } : {}),
451
446
  ...(this.resilience !== undefined ? { resilience: this.resilience } : {}),
452
447
  ...(this.maxToolConcurrency !== undefined ? { maxToolConcurrency: this.maxToolConcurrency } : {}),
453
448
  ...(this.streamingToolExecution === true && (this.getHandlers("tool_call")?.size ?? 0) === 0
@@ -511,9 +506,6 @@ export class AgentHarness {
511
506
  get degenerateOutput() {
512
507
  return self.loopRecovery?.degenerateOutput;
513
508
  },
514
- get walltimeCutoff() {
515
- return self.loopRecovery?.walltimeCutoff;
516
- },
517
509
  get promptTooLong() {
518
510
  const ptl = self.loopRecovery?.promptTooLong;
519
511
  if (!ptl)
@@ -681,7 +673,7 @@ export class AgentHarness {
681
673
  for (let i = newMessages.length - 1; i >= 0; i--) {
682
674
  const message = newMessages[i];
683
675
  if (message.role === "assistant") {
684
- return mergeTruncatedOutputChain(newMessages, i, this.loopRecovery?.walltimeCutoff?.detect);
676
+ return mergeTruncatedOutputChain(newMessages, i);
685
677
  }
686
678
  }
687
679
  throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message");
@@ -931,54 +923,25 @@ function isEmptyFailureAssistant(message) {
931
923
  return false;
932
924
  return m.content.every((c) => c.type === "text" ? c.text.trim() === "" : c.type === "thinking" ? c.thinking.trim() === "" : false);
933
925
  }
934
- function mergeTruncatedOutputChain(messages, lastAssistantIndex, isWalltimeCutoff) {
926
+ function mergeTruncatedOutputChain(messages, lastAssistantIndex) {
935
927
  const last = messages[lastAssistantIndex];
936
928
  const lastText = last.content.map((c) => (c.type === "text" ? c.text : "")).join("");
937
929
  const parts = [];
938
930
  let i = lastAssistantIndex;
939
931
  while (i - 2 >= 0) {
940
932
  const mid = messages[i - 1];
941
- const downstreamText = parts.join("") + lastText;
942
- const walltimeGate = last.stopReason === "error" || downstreamText.trim() === "";
943
- let head;
944
- let walltimeStep = false;
945
- let nextI = i - 2;
946
- if (mid.role === "custom") {
947
- const h = messages[i - 2];
948
- if (h.role !== "assistant")
949
- break;
950
- const assistant = h;
951
- const step = (mid.customType === "loop:truncated-output-continue" && assistant.stopReason === "length") ||
952
- (mid.customType === "loop:midstream-partial-continue" && assistant.stopReason === "stop") ||
953
- (mid.customType === "loop:walltime-cutoff-recovery" && assistant.stopReason === "error" && walltimeGate);
954
- if (!step)
955
- break;
956
- head = assistant;
957
- walltimeStep = mid.customType === "loop:walltime-cutoff-recovery";
958
- }
959
- else if (mid.role === "user" && isWalltimeCutoff) {
960
- let j = i - 1;
961
- while (j >= 0 && messages[j].role === "user")
962
- j--;
963
- if (j < 0 || j >= i - 1)
964
- break;
965
- const h = messages[j];
966
- if (h.role !== "assistant")
967
- break;
968
- const assistant = h;
969
- if (!(assistant.stopReason === "error" && isWalltimeCutoff(assistant) && walltimeGate))
970
- break;
971
- head = assistant;
972
- walltimeStep = true;
973
- nextI = j;
974
- }
975
- else {
933
+ if (mid.role !== "custom")
976
934
  break;
977
- }
978
- const headText = head.content.map((c) => (c.type === "text" ? c.text : "")).join("");
979
- const needsBreak = walltimeStep && downstreamText !== "" && headText !== "";
980
- parts.unshift(needsBreak ? `${headText}\n\n` : headText);
981
- i = nextI;
935
+ const h = messages[i - 2];
936
+ if (h.role !== "assistant")
937
+ break;
938
+ const head = h;
939
+ const step = (mid.customType === "loop:truncated-output-continue" && head.stopReason === "length") ||
940
+ (mid.customType === "loop:midstream-partial-continue" && head.stopReason === "stop");
941
+ if (!step)
942
+ break;
943
+ parts.unshift(head.content.map((c) => (c.type === "text" ? c.text : "")).join(""));
944
+ i = i - 2;
982
945
  }
983
946
  if (parts.length === 0) {
984
947
  return last;
@@ -88,11 +88,6 @@ export interface ExecutionEnvExecOptions {
88
88
  onStdout?: (chunk: string) => void;
89
89
  onStderr?: (chunk: string) => void;
90
90
  detachSignal?: AbortSignal;
91
- onSpawn?: (handle: {
92
- pid: number;
93
- kill: () => void;
94
- }) => void;
95
- onDetachAdopted?: () => void;
96
91
  autoBackgroundOnTimeout?: boolean;
97
92
  }
98
93
  export interface FileSystem {
@@ -144,6 +139,8 @@ export interface Shell {
144
139
  cleanup(): Promise<void>;
145
140
  }
146
141
  export interface ExecutionEnv extends FileSystem, Shell {
142
+ readonly lifetimeMs?: number;
143
+ readonly lifetimeStartedAt?: number;
147
144
  }
148
145
  export interface SessionTreeEntryBase {
149
146
  type: string;
@@ -544,17 +541,8 @@ export interface AgentHarnessOptions<TSkill extends Skill = Skill, TPromptTempla
544
541
  followUpMode?: QueueMode;
545
542
  maxOutputTokens?: number;
546
543
  maxOutputTokensPerCall?: () => number | undefined;
547
- callDeadlineMsPerCall?: () => number | undefined;
548
544
  stallTimeoutsPerCall?: () => import("../llm/types.js").StallTimeouts | undefined;
549
- toolDeadline?: {
550
- deadlineMs: () => number | undefined;
551
- onCut?: (info: {
552
- toolCallId: string;
553
- toolName: string;
554
- elapsedMs: number;
555
- settled: Promise<void>;
556
- }) => void;
557
- };
545
+ abortResultDetails?: () => Record<string, unknown> | undefined;
558
546
  loopTrace?: (step: import("../loop/agent-loop.js").LoopStep) => void;
559
547
  resilience?: import("../llm/types.js").ResilienceOptions;
560
548
  maxToolConcurrency?: number;
@@ -45,7 +45,6 @@ export interface StreamOptions {
45
45
  maxTokensDynamic?: boolean;
46
46
  staticReasoningCutDowngrade?: boolean;
47
47
  resilience?: ResilienceOptions;
48
- callDeadlineMs?: number;
49
48
  stallTimeouts?: StallTimeouts;
50
49
  maxRetries?: number;
51
50
  maxRetryDelayMs?: number;
@@ -69,15 +68,9 @@ export interface SimpleStreamOptions extends StreamOptions {
69
68
  }
70
69
  export type StreamFunction<TApi extends Api = Api, TOptions extends StreamOptions = StreamOptions> = (model: Model<TApi>, context: Context, options?: TOptions) => AssistantMessageEventStreamContract;
71
70
  export type ImagesFunction<TApi extends ImagesApi = ImagesApi, TOptions extends ImagesOptions = ImagesOptions> = (model: ImagesModel<TApi>, context: ImagesContext, options?: TOptions) => Promise<AssistantImages>;
72
- export interface TextSignatureV1 {
73
- v: 1;
74
- id: string;
75
- phase?: "commentary" | "final_answer";
76
- }
77
71
  export interface TextContent {
78
72
  type: "text";
79
73
  text: string;
80
- textSignature?: string;
81
74
  }
82
75
  export interface ThinkingContent {
83
76
  type: "thinking";
@@ -100,7 +93,6 @@ export interface ToolCall {
100
93
  id: string;
101
94
  name: string;
102
95
  arguments: Record<string, unknown>;
103
- thoughtSignature?: string;
104
96
  executionMode?: "sequential" | "parallel";
105
97
  }
106
98
  export interface Usage {
@@ -143,7 +135,7 @@ export interface AssistantMessage {
143
135
  usageMissing?: true;
144
136
  stopReason: StopReason;
145
137
  errorMessage?: string;
146
- errorKind?: "length_empty" | "degenerate" | "walltime_cutoff";
138
+ errorKind?: "length_empty" | "degenerate";
147
139
  partialFinalized?: true;
148
140
  repetition?: {
149
141
  cut?: RepetitionEvent;
@@ -260,28 +252,15 @@ export interface AssistantMessageEventStreamLike extends AsyncIterable<Assistant
260
252
  result(): Promise<AssistantMessage>;
261
253
  }
262
254
  export interface OpenAICompletionsCompat {
263
- supportsStore?: boolean;
264
- supportsDeveloperRole?: boolean;
265
255
  supportsReasoningEffort?: boolean;
266
256
  reasoningEffortLevels?: ThinkingLevel[];
267
- supportsUsageInStreaming?: boolean;
268
257
  maxTokensField?: "max_completion_tokens" | "max_tokens";
269
- requiresToolResultName?: boolean;
270
- requiresAssistantAfterToolResult?: boolean;
271
- requiresThinkingAsText?: boolean;
272
258
  requiresReasoningContentOnAssistantMessages?: boolean;
273
259
  thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
274
- openRouterRouting?: OpenRouterRouting;
275
- vercelGatewayRouting?: VercelGatewayRouting;
276
- zaiToolStream?: boolean;
277
- supportsStrictMode?: boolean;
278
- cacheControlFormat?: "anthropic";
279
- sendSessionAffinityHeaders?: boolean;
280
- supportsLongCacheRetention?: boolean;
281
260
  }
282
261
  export interface OpenAIResponsesCompat {
283
- sendSessionIdHeader?: boolean;
284
- supportsLongCacheRetention?: boolean;
262
+ supportsReasoningEffort?: boolean;
263
+ reasoningEffortLevels?: ThinkingLevel[];
285
264
  }
286
265
  export interface AnthropicMessagesCompat {
287
266
  thinkingMode?: "budget" | "adaptive";
@@ -289,48 +268,6 @@ export interface AnthropicMessagesCompat {
289
268
  supportsTemperature?: boolean;
290
269
  contextManagement?: boolean;
291
270
  interleavedThinking?: boolean;
292
- supportsEagerToolInputStreaming?: boolean;
293
- supportsLongCacheRetention?: boolean;
294
- sendSessionAffinityHeaders?: boolean;
295
- supportsCacheControlOnTools?: boolean;
296
- }
297
- export interface OpenRouterRouting {
298
- allow_fallbacks?: boolean;
299
- require_parameters?: boolean;
300
- data_collection?: "deny" | "allow";
301
- zdr?: boolean;
302
- enforce_distillable_text?: boolean;
303
- order?: string[];
304
- only?: string[];
305
- ignore?: string[];
306
- quantizations?: string[];
307
- sort?: string | {
308
- by?: string;
309
- partition?: string | null;
310
- };
311
- max_price?: {
312
- prompt?: number | string;
313
- completion?: number | string;
314
- image?: number | string;
315
- audio?: number | string;
316
- request?: number | string;
317
- };
318
- preferred_min_throughput?: number | {
319
- p50?: number;
320
- p75?: number;
321
- p90?: number;
322
- p99?: number;
323
- };
324
- preferred_max_latency?: number | {
325
- p50?: number;
326
- p75?: number;
327
- p90?: number;
328
- p99?: number;
329
- };
330
- }
331
- export interface VercelGatewayRouting {
332
- only?: string[];
333
- order?: string[];
334
271
  }
335
272
  export interface Model<TApi extends Api = Api> {
336
273
  id: string;
@@ -358,15 +295,6 @@ export interface Model<TApi extends Api = Api> {
358
295
  headers?: Record<string, string>;
359
296
  promptGuidance?: string[];
360
297
  compat?: TApi extends "openai-completions" ? OpenAICompletionsCompat : TApi extends "openai-responses" ? OpenAIResponsesCompat : TApi extends "anthropic-messages" ? AnthropicMessagesCompat : never;
361
- mediaInput?: {
362
- image?: {
363
- maxBytes?: number;
364
- maxPixels?: number;
365
- maxSidePx?: number;
366
- preferredSidePx?: number;
367
- tokenMode?: "tile" | "detail" | "provider";
368
- };
369
- };
370
298
  }
371
299
  export interface ImagesModel<TApi extends ImagesApi = ImagesApi> extends Omit<Model, "api" | "provider" | "reasoning" | "contextWindow" | "maxTokens" | "compat"> {
372
300
  api: TApi;
@@ -1,7 +1,7 @@
1
1
  import { type AgentCoreStreamRuntimeDeps } from "./runtime-deps.js";
2
2
  import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types.js";
3
3
  export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
4
- export type LoopContinueReason = "next_turn" | "steer_injected" | "follow_up_injected" | "reactive_compact_retry" | "max_output_tokens_recovery" | "malformed_tool_use_retry" | "thinking_only_retry" | "midstream_partial_recovery" | "degenerate_output_recovery" | "walltime_cutoff_recovery";
4
+ export type LoopContinueReason = "next_turn" | "steer_injected" | "follow_up_injected" | "reactive_compact_retry" | "max_output_tokens_recovery" | "malformed_tool_use_retry" | "thinking_only_retry" | "midstream_partial_recovery" | "degenerate_output_recovery";
5
5
  export type LoopTerminalReason = "completed" | "aborted_before_stream" | "assistant_error" | "stop_requested" | "malformed_tool_use_exhausted" | "truncated_output_exhausted";
6
6
  export type LoopStep = {
7
7
  kind: "continue";
@@ -111,17 +111,6 @@ function createDegenerateRecoveryNudge() {
111
111
  timestamp: Date.now(),
112
112
  };
113
113
  }
114
- function createWalltimeCutoffRecoveryNudge() {
115
- return {
116
- role: "custom",
117
- customType: "loop:walltime-cutoff-recovery",
118
- provenance: "engine-note",
119
- content: "Your previous response crossed the task's wall-clock deadline and was cut off (the partial text above is preserved). " +
120
- "This is the FINAL turn: write your best current answer / deliverables NOW in one short response — no new work, no long commands.",
121
- display: false,
122
- timestamp: Date.now(),
123
- };
124
- }
125
114
  function createReasoningCutContinueNudge() {
126
115
  return {
127
116
  role: "custom",
@@ -154,7 +143,6 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
154
143
  truncatedOutputContinues: 0,
155
144
  staticReasoningCutRecoveries: initialContext.messages.filter((m) => m.role === "assistant" && m.staticReasoningCut === true).length,
156
145
  degenerateContinues: 0,
157
- walltimeCutoffContinues: 0,
158
146
  malformedToolUseRetries: 0,
159
147
  thinkingOnlyRetries: 0,
160
148
  midstreamPartialContinues: 0,
@@ -282,47 +270,8 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
282
270
  if (message.stopReason !== "error")
283
271
  state.degenerateContinues = 0;
284
272
  }
285
- {
286
- const wc = state.config.recovery?.walltimeCutoff;
287
- if (wc &&
288
- message.stopReason === "error" &&
289
- !signal?.aborted &&
290
- wc.detect(message) &&
291
- (wc.hasLiveWriteoutWindow === undefined || wc.hasLiveWriteoutWindow()) &&
292
- executor.admittedCount === 0 &&
293
- message.content.every((b) => b.type !== "toolCall") &&
294
- state.walltimeCutoffContinues < (wc.maxContinues ?? 1)) {
295
- state.walltimeCutoffContinues++;
296
- await emit({ type: "turn_end", message, toolResults: [] });
297
- const drained = (await state.config.getSteeringMessages?.()) || [];
298
- state.pendingMessages = drained.length > 0 ? drained : [createWalltimeCutoffRecoveryNudge()];
299
- const liveLevel = state.config.liveThinkingLevel?.();
300
- if (liveLevel !== undefined)
301
- state.config = Object.assign({}, state.config, { reasoning: liveLevel });
302
- return { kind: "ran", recovered: "walltime_cutoff_recovery" };
303
- }
304
- }
305
273
  if (message.stopReason === "error" || message.stopReason === "aborted") {
306
274
  const errorFinalResults = await harvestExecutorOnErrorFinal(executor, state, message, signal, emit);
307
- if (state.config.recovery?.walltimeCutoff?.detect(message)) {
308
- const covered = new Set(errorFinalResults.map((r) => r.toolCallId));
309
- for (const tc of message.content.filter((c) => c.type === "toolCall")) {
310
- if (covered.has(tc.id))
311
- continue;
312
- const synthetic = {
313
- role: "toolResult",
314
- toolCallId: tc.id,
315
- toolName: tc.name,
316
- content: [{ type: "text", text: "[call aborted by walltime cutoff — never executed]" }],
317
- isError: true,
318
- timestamp: Date.now(),
319
- };
320
- await emitToolResultMessage(synthetic, emit);
321
- state.context.messages.push(synthetic);
322
- state.newMessages.push(synthetic);
323
- errorFinalResults.push(synthetic);
324
- }
325
- }
326
275
  await emit({ type: "turn_end", message, toolResults: errorFinalResults });
327
276
  await emit({ type: "agent_end", messages: state.newMessages });
328
277
  return { kind: "terminal", reason: "assistant_error" };
@@ -440,12 +389,10 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn,
440
389
  const streamFunction = resolveAgentCoreStreamFn(runtime, streamFn);
441
390
  const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
442
391
  const perCallMaxTokens = config.maxTokensPerCall?.();
443
- const perCallDeadlineMs = config.callDeadlineMsPerCall?.();
444
392
  const perCallStallTimeouts = config.stallTimeoutsPerCall?.();
445
393
  const response = await streamFunction(config.model, llmContext, {
446
394
  ...config,
447
395
  ...(perCallMaxTokens !== undefined ? { maxTokens: perCallMaxTokens, maxTokensDynamic: true } : {}),
448
- ...(perCallDeadlineMs !== undefined ? { callDeadlineMs: perCallDeadlineMs } : {}),
449
396
  ...(perCallStallTimeouts !== undefined ? { stallTimeouts: perCallStallTimeouts } : {}),
450
397
  ...(staticReasoningCutDowngrade === true ? { staticReasoningCutDowngrade: true } : {}),
451
398
  apiKey: resolvedApiKey,
@@ -527,28 +474,6 @@ async function executeToolCalls(currentContext, assistantMessage, config, signal
527
474
  }
528
475
  return executeToolCallsPartitioned(currentContext, assistantMessage, toolCalls, config, signal, emit, executor);
529
476
  }
530
- const engineCutResults = new WeakSet();
531
- function isEngineCutResult(result) {
532
- return engineCutResults.has(result);
533
- }
534
- function createEngineCutNotExecutedOutcome(toolCall) {
535
- return {
536
- toolCall,
537
- result: {
538
- content: [
539
- {
540
- type: "text",
541
- text: `[not executed — engine stopped waiting at the write-out deadline] ${toolCall.name} was NOT executed: ` +
542
- `an earlier tool in this batch was cut at the task's write-out deadline, so the engine closed the batch ` +
543
- `immediately to reach the final write-out turn. This call ran no side effects and is safe to re-issue ` +
544
- `later if still needed — but do not start new long-running work now; write out your best current answer.`,
545
- },
546
- ],
547
- details: { engineCut: "walltime_writeout", notExecuted: true },
548
- },
549
- isError: true,
550
- };
551
- }
552
477
  async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
553
478
  const finalizedCalls = [];
554
479
  const messages = [];
@@ -562,7 +487,6 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
562
487
  });
563
488
  const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
564
489
  let finalized;
565
- let engineCutRaw = false;
566
490
  if (preparation.kind === "immediate") {
567
491
  finalized = {
568
492
  toolCall,
@@ -571,8 +495,7 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
571
495
  };
572
496
  }
573
497
  else {
574
- const executed = await executePreparedToolCall(preparation, config, signal, emit);
575
- engineCutRaw = isEngineCutResult(executed.result);
498
+ const executed = await executePreparedToolCall(preparation, signal, emit, config.abortResultDetails);
576
499
  finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
577
500
  }
578
501
  await emitToolExecutionEnd(finalized, emit);
@@ -580,23 +503,6 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
580
503
  await emitToolResultMessage(toolResultMessage, emit);
581
504
  finalizedCalls.push(finalized);
582
505
  messages.push(toolResultMessage);
583
- if (engineCutRaw) {
584
- for (const later of toolCalls.slice(i + 1)) {
585
- await emit({
586
- type: "tool_execution_start",
587
- toolCallId: later.id,
588
- toolName: later.name,
589
- args: later.arguments,
590
- });
591
- const skipped = createEngineCutNotExecutedOutcome(later);
592
- await emitToolExecutionEnd(skipped, emit);
593
- const skippedMessage = createToolResultMessage(skipped);
594
- await emitToolResultMessage(skippedMessage, emit);
595
- finalizedCalls.push(skipped);
596
- messages.push(skippedMessage);
597
- }
598
- break;
599
- }
600
506
  if (signal?.aborted) {
601
507
  break;
602
508
  }
@@ -668,7 +574,6 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
668
574
  const cap = resolveToolConcurrency(config.maxToolConcurrency);
669
575
  const allFinalized = [];
670
576
  const messages = [];
671
- let engineCutSeen = false;
672
577
  const remainingCalls = [];
673
578
  for (const toolCall of toolCalls) {
674
579
  const held = executor?.take(toolCall.id);
@@ -676,8 +581,6 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
676
581
  remainingCalls.push(toolCall);
677
582
  continue;
678
583
  }
679
- if (held.executed && isEngineCutResult(held.executed.result))
680
- engineCutSeen = true;
681
584
  const finalized = await finalizeStreamEntry(currentContext, assistantMessage, held, config, signal);
682
585
  await emitToolExecutionEnd(finalized, emit);
683
586
  const toolResultMessage = createToolResultMessage(finalized);
@@ -695,23 +598,6 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
695
598
  outer: for (const batch of batches) {
696
599
  if (signal?.aborted)
697
600
  break;
698
- if (engineCutSeen) {
699
- for (const toolCall of batch.calls) {
700
- await emit({
701
- type: "tool_execution_start",
702
- toolCallId: toolCall.id,
703
- toolName: toolCall.name,
704
- args: toolCall.arguments,
705
- });
706
- const skipped = createEngineCutNotExecutedOutcome(toolCall);
707
- await emitToolExecutionEnd(skipped, emit);
708
- const skippedMessage = createToolResultMessage(skipped);
709
- await emitToolResultMessage(skippedMessage, emit);
710
- allFinalized.push(skipped);
711
- messages.push(skippedMessage);
712
- }
713
- continue;
714
- }
715
601
  const entries = [];
716
602
  for (const toolCall of batch.calls) {
717
603
  await emit({
@@ -739,14 +625,7 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
739
625
  return { messages, terminate: shouldTerminateToolBatch(allFinalized) };
740
626
  async function settleBatch(entries, concurrent) {
741
627
  const runOne = (entry) => async () => {
742
- if (engineCutSeen) {
743
- const skipped = createEngineCutNotExecutedOutcome(entry.toolCall);
744
- await emitToolExecutionEnd(skipped, emit);
745
- return skipped;
746
- }
747
- const executed = await executePreparedToolCall(entry.prepared, config, signal, emit);
748
- if (isEngineCutResult(executed.result))
749
- engineCutSeen = true;
628
+ const executed = await executePreparedToolCall(entry.prepared, signal, emit, config.abortResultDetails);
750
629
  const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, entry.prepared, executed, config, signal);
751
630
  await emitToolExecutionEnd(finalized, emit);
752
631
  return finalized;
@@ -828,10 +707,7 @@ class StreamToolExecutor {
828
707
  return;
829
708
  }
830
709
  entry.prepared = preparation;
831
- entry.executed = await executePreparedToolCall(preparation, this.config, this.signal, this.emit);
832
- if (isEngineCutResult(entry.executed.result)) {
833
- this.barrier = true;
834
- }
710
+ entry.executed = await executePreparedToolCall(preparation, this.signal, this.emit, this.config.abortResultDetails);
835
711
  }
836
712
  catch (error) {
837
713
  entry.immediate = {
@@ -953,7 +829,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
953
829
  if (signal?.aborted) {
954
830
  return {
955
831
  kind: "immediate",
956
- result: createErrorToolResult("Operation aborted"),
832
+ result: createErrorToolResult("Operation aborted", { details: config.abortResultDetails?.() }),
957
833
  isError: true,
958
834
  };
959
835
  }
@@ -993,13 +869,12 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
993
869
  };
994
870
  }
995
871
  }
996
- async function executePreparedToolCall(prepared, config, signal, emit) {
872
+ async function executePreparedToolCall(prepared, signal, emit, abortResultDetails) {
997
873
  const updateEvents = [];
998
874
  let acceptingUpdates = true;
999
875
  if (signal?.aborted) {
1000
- return { result: createErrorToolResult("operation aborted before execution"), isError: true };
876
+ return { result: createErrorToolResult("operation aborted before execution", { details: abortResultDetails?.() }), isError: true };
1001
877
  }
1002
- const startedAt = Date.now();
1003
878
  const work = (async () => {
1004
879
  try {
1005
880
  const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
@@ -1027,48 +902,7 @@ async function executePreparedToolCall(prepared, config, signal, emit) {
1027
902
  };
1028
903
  }
1029
904
  })();
1030
- const cutDeadline = config.toolDeadline?.deadlineMs();
1031
- if (cutDeadline === undefined)
1032
- return work;
1033
- let cutTimer;
1034
- const cut = new Promise((resolve) => {
1035
- cutTimer = setTimeout(() => {
1036
- const elapsedMs = Date.now() - startedAt;
1037
- acceptingUpdates = false;
1038
- try {
1039
- config.toolDeadline?.onCut?.({
1040
- toolCallId: prepared.toolCall.id,
1041
- toolName: prepared.toolCall.name,
1042
- elapsedMs,
1043
- settled: work.then(() => undefined, () => undefined),
1044
- });
1045
- }
1046
- catch {
1047
- }
1048
- const cutResult = {
1049
- content: [
1050
- {
1051
- type: "text",
1052
- text: `[tool cut short at the write-out deadline] ${prepared.toolCall.name} had been running for ${Math.round(elapsedMs / 1000)}s ` +
1053
- `when the task's wall-clock write-out window opened — the ENGINE stopped waiting for it so the final answer can still be written out. ` +
1054
- `The tool itself was NOT cancelled and did not fail: it may have completed or may still be running in the background, and its side effects ` +
1055
- `may still land after this message — treat its outcome as UNKNOWN and do NOT blindly re-issue non-idempotent operations. ` +
1056
- `Do not start new long-running work — write out your best current answer now.`,
1057
- },
1058
- ],
1059
- details: { engineCut: "walltime_writeout", elapsedMs },
1060
- };
1061
- engineCutResults.add(cutResult);
1062
- resolve({ result: cutResult, isError: true });
1063
- }, Math.max(0, cutDeadline - Date.now()));
1064
- cutTimer.unref?.();
1065
- });
1066
- try {
1067
- return await Promise.race([work, cut]);
1068
- }
1069
- finally {
1070
- clearTimeout(cutTimer);
1071
- }
905
+ return work;
1072
906
  }
1073
907
  async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
1074
908
  let result = executed.result;
@@ -68,29 +68,12 @@ export interface LoopRecoveryOptions {
68
68
  detect: (message: AssistantMessage) => boolean;
69
69
  maxContinues?: number;
70
70
  };
71
- walltimeCutoff?: {
72
- detect: (message: AssistantMessage) => boolean;
73
- maxContinues?: number;
74
- hasLiveWriteoutWindow?: () => boolean;
75
- };
76
- }
77
- export interface ToolCutInfo {
78
- toolCallId: string;
79
- toolName: string;
80
- elapsedMs: number;
81
- settled: Promise<void>;
82
- }
83
- export interface LoopToolDeadline {
84
- deadlineMs: () => number | undefined;
85
- onCut?: (info: ToolCutInfo) => void;
86
71
  }
87
72
  export interface AgentLoopConfig extends SimpleStreamOptions {
88
73
  model: Model;
89
74
  recovery?: LoopRecoveryOptions;
90
75
  maxTokensPerCall?: () => number | undefined;
91
- callDeadlineMsPerCall?: () => number | undefined;
92
76
  stallTimeoutsPerCall?: () => import("../llm/types.js").StallTimeouts | undefined;
93
- toolDeadline?: LoopToolDeadline;
94
77
  maxToolConcurrency?: number;
95
78
  streamingToolExecution?: boolean;
96
79
  convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
@@ -98,11 +81,11 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
98
81
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
99
82
  shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean | Promise<boolean>;
100
83
  prepareNextTurn?: (context: PrepareNextTurnContext) => AgentLoopTurnUpdate | undefined | Promise<AgentLoopTurnUpdate | undefined>;
101
- liveThinkingLevel?: () => ThinkingLevel;
102
84
  getSteeringMessages?: () => Promise<AgentMessage[]>;
103
85
  getFollowUpMessages?: () => Promise<AgentMessage[]>;
104
86
  toolExecution?: ToolExecutionMode;
105
87
  beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
88
+ abortResultDetails?: () => Record<string, unknown> | undefined;
106
89
  afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
107
90
  }
108
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";