@tanstack/ai 0.45.1 → 0.47.1

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 (82) hide show
  1. package/dist/esm/activities/chat/index.d.ts +36 -11
  2. package/dist/esm/activities/chat/index.js +462 -66
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.d.ts +1 -0
  5. package/dist/esm/activities/chat/messages.js +12 -7
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/middleware/builder.d.ts +7 -2
  8. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  9. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -3
  10. package/dist/esm/activities/chat/middleware/compose.js +55 -0
  11. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  12. package/dist/esm/activities/chat/middleware/define.d.ts +6 -3
  13. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/generic-interrupts.d.ts +13 -0
  15. package/dist/esm/activities/chat/middleware/generic-interrupts.js +8 -0
  16. package/dist/esm/activities/chat/middleware/generic-interrupts.js.map +1 -0
  17. package/dist/esm/activities/chat/middleware/index.d.ts +4 -1
  18. package/dist/esm/activities/chat/middleware/types.d.ts +54 -3
  19. package/dist/esm/activities/chat/middleware/types.js +16 -0
  20. package/dist/esm/activities/chat/middleware/types.js.map +1 -0
  21. package/dist/esm/activities/chat/stream/processor.js +18 -5
  22. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  23. package/dist/esm/activities/chat/tools/unique-tool-names.d.ts +20 -0
  24. package/dist/esm/activities/chat/tools/unique-tool-names.js +57 -0
  25. package/dist/esm/activities/chat/tools/unique-tool-names.js.map +1 -0
  26. package/dist/esm/adapter-internals.d.ts +7 -0
  27. package/dist/esm/adapter-internals.js +5 -1
  28. package/dist/esm/client.d.ts +4 -0
  29. package/dist/esm/client.js +3 -1
  30. package/dist/esm/client.js.map +1 -1
  31. package/dist/esm/generic-interrupt-continuation.d.ts +45 -0
  32. package/dist/esm/generic-interrupt-continuation.js +80 -0
  33. package/dist/esm/generic-interrupt-continuation.js.map +1 -0
  34. package/dist/esm/index.d.ts +9 -1
  35. package/dist/esm/index.js +7 -2
  36. package/dist/esm/interrupt-definition.d.ts +113 -0
  37. package/dist/esm/interrupt-definition.js +169 -0
  38. package/dist/esm/interrupt-definition.js.map +1 -0
  39. package/dist/esm/interrupt-resume.d.ts +3 -0
  40. package/dist/esm/interrupt-resume.js +77 -16
  41. package/dist/esm/interrupt-resume.js.map +1 -1
  42. package/dist/esm/interrupts.d.ts +12 -3
  43. package/dist/esm/interrupts.js.map +1 -1
  44. package/dist/esm/middlewares/usage-attributes.d.ts +2 -2
  45. package/dist/esm/middlewares/usage-attributes.js +9 -2
  46. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  47. package/dist/esm/stream-to-response.d.ts +26 -0
  48. package/dist/esm/stream-to-response.js +1 -1
  49. package/dist/esm/stream-to-response.js.map +1 -1
  50. package/dist/esm/stream-to-websocket.d.ts +123 -0
  51. package/dist/esm/stream-to-websocket.js +249 -0
  52. package/dist/esm/stream-to-websocket.js.map +1 -0
  53. package/dist/esm/types.d.ts +19 -10
  54. package/dist/esm/utilities/chat-params.js +10 -1
  55. package/dist/esm/utilities/chat-params.js.map +1 -1
  56. package/package.json +2 -2
  57. package/skills/ai-core/media-generation/SKILL.md +13 -9
  58. package/skills/ai-core/middleware/SKILL.md +53 -44
  59. package/skills/ai-core/structured-outputs/SKILL.md +59 -55
  60. package/skills/ai-core/tool-calling/SKILL.md +54 -1
  61. package/src/activities/chat/index.ts +1076 -194
  62. package/src/activities/chat/messages.ts +11 -3
  63. package/src/activities/chat/middleware/builder.ts +29 -4
  64. package/src/activities/chat/middleware/compose.ts +95 -5
  65. package/src/activities/chat/middleware/define.ts +13 -3
  66. package/src/activities/chat/middleware/generic-interrupts.ts +26 -0
  67. package/src/activities/chat/middleware/index.ts +15 -0
  68. package/src/activities/chat/middleware/types.ts +127 -2
  69. package/src/activities/chat/stream/processor.ts +21 -0
  70. package/src/activities/chat/tools/unique-tool-names.ts +73 -0
  71. package/src/adapter-internals.ts +24 -0
  72. package/src/client.ts +20 -0
  73. package/src/generic-interrupt-continuation.ts +162 -0
  74. package/src/index.ts +51 -0
  75. package/src/interrupt-definition.ts +581 -0
  76. package/src/interrupt-resume.ts +156 -25
  77. package/src/interrupts.ts +13 -3
  78. package/src/middlewares/usage-attributes.ts +12 -2
  79. package/src/stream-to-response.ts +2 -2
  80. package/src/stream-to-websocket.ts +418 -0
  81. package/src/types.ts +21 -8
  82. package/src/utilities/chat-params.ts +16 -3
@@ -11,18 +11,22 @@ import { canonicalInterruptJson, digestInterruptJson } from "../../interrupt-ser
11
11
  import "../../interrupts.js";
12
12
  import { convertSchemaForStructuredOutput, convertSchemaToJsonSchema, isStandardSchema, parseWithStandardSchema } from "./tools/schema-converter.js";
13
13
  import { hashSchemaInput, normalizeApprovalSchema } from "./tools/approval-schema.js";
14
- import { INTERRUPT_BINDING_METADATA_KEY, InterruptResumeValidationError, readUnopenedInterruptBinding, validateInterruptResumeBatch } from "../../interrupt-resume.js";
14
+ import { INTERRUPT_BINDING_METADATA_KEY, InterruptResumeValidationError, readInterruptBinding, readUnopenedInterruptBinding, validateInterruptResumeBatch } from "../../interrupt-resume.js";
15
+ import { INTERRUPT_PAYLOAD_METADATA_KEY, createInterruptBinding, rehydrateInterruptRequest } from "../../interrupt-definition.js";
16
+ import { readGenericInterruptContinuation } from "../../generic-interrupt-continuation.js";
15
17
  import { normalizeToolResult } from "../../utilities/tool-result.js";
16
18
  import { isProviderExecutedToolCall } from "../../utilities/provider-executed.js";
17
19
  import { LazyToolManager } from "./tools/lazy-tool-manager.js";
20
+ import { assertUniqueToolNames } from "./tools/unique-tool-names.js";
18
21
  import { MiddlewareAbortError, ToolCallManager, executeToolCalls } from "./tools/tool-calls.js";
19
22
  import { maxIterations } from "./agent-loop-strategies.js";
20
- import { convertMessagesToModelMessages, generateMessageId } from "./messages.js";
23
+ import { convertMessagesToModelMessages, generateMessageId, modelMessageToUIMessage, safeJsonStringify } from "./messages.js";
21
24
  import { MiddlewareRunner } from "./middleware/compose.js";
22
25
  import { provideSandboxRuntime } from "./middleware/sandbox-runtime.js";
23
26
  import { provideRunDisconnect } from "./middleware/run-disconnect.js";
24
27
  import { validateCapabilities } from "./middleware/validate.js";
25
28
  import { MCPManager } from "./mcp/manager.js";
29
+ import { provideGenericInterruptDefinitionRegistry } from "./middleware/generic-interrupts.js";
26
30
  import "./adapter.js";
27
31
  import { devtoolsMiddleware } from "@tanstack/ai-event-client";
28
32
  import { undoNullWidening } from "@tanstack/ai-utils";
@@ -50,38 +54,11 @@ function structuralInterruptFailure(error) {
50
54
  };
51
55
  }
52
56
  function normalizePublicInterruptBinding(value, expectedInterruptId) {
53
- if (value === null || typeof value !== "object" || Array.isArray(value)) return;
54
- const binding = Object.fromEntries(Object.entries(value));
55
- if (binding.interruptId !== expectedInterruptId || binding.v !== void 0 && binding.v !== 1 || typeof binding.interruptedRunId !== "string" || typeof binding.generation !== "number" || !Number.isInteger(binding.generation) || binding.generation < 0 || typeof binding.responseSchemaHash !== "string" || binding.expiresAt !== void 0 && typeof binding.expiresAt !== "string") return;
56
- const base = {
57
- v: 1,
58
- interruptId: binding.interruptId,
59
- interruptedRunId: binding.interruptedRunId,
60
- generation: binding.generation,
61
- responseSchemaHash: binding.responseSchemaHash,
62
- ...typeof binding.expiresAt === "string" ? { expiresAt: binding.expiresAt } : {}
63
- };
64
- if (binding.kind === "generic") return {
65
- kind: binding.kind,
66
- ...base
67
- };
68
- if (typeof binding.toolName !== "string" || typeof binding.toolCallId !== "string") return;
69
- if (binding.kind === "client-tool-execution" && typeof binding.outputSchemaHash === "string") return {
70
- kind: binding.kind,
71
- ...base,
72
- toolName: binding.toolName,
73
- toolCallId: binding.toolCallId,
74
- outputSchemaHash: binding.outputSchemaHash
75
- };
76
- if (binding.kind === "tool-approval" && Object.prototype.hasOwnProperty.call(binding, "originalArgs") && typeof binding.inputSchemaHash === "string" && typeof binding.approvalSchemaHash === "string") return {
77
- kind: binding.kind,
78
- ...base,
79
- toolName: binding.toolName,
80
- toolCallId: binding.toolCallId,
81
- originalArgs: binding.originalArgs,
82
- inputSchemaHash: binding.inputSchemaHash,
83
- approvalSchemaHash: binding.approvalSchemaHash
84
- };
57
+ return readInterruptBinding({
58
+ id: expectedInterruptId,
59
+ reason: "",
60
+ metadata: { [INTERRUPT_BINDING_METADATA_KEY]: value }
61
+ });
85
62
  }
86
63
  /**
87
64
  * Create typed options for the chat() function without executing.
@@ -119,12 +96,15 @@ function combineAbortSignals(a, b) {
119
96
  }
120
97
  var TextEngine = class {
121
98
  adapter;
99
+ interruptDefinitions;
122
100
  params;
123
101
  systemPrompts;
124
102
  tools;
125
103
  loopStrategy;
126
104
  toolCallManager;
127
105
  lazyToolManager;
106
+ /** A public interruption terminal must always have this run's start event. */
107
+ hasPublicRunStarted = false;
128
108
  initialMessageCount;
129
109
  requestId;
130
110
  streamId;
@@ -148,11 +128,14 @@ var TextEngine = class {
148
128
  accumulatedThinking = [];
149
129
  currentThinkingContent = "";
150
130
  currentThinkingSignature = "";
131
+ hasSeenReasoningEvents = false;
151
132
  eventOptions;
152
133
  eventToolNames;
153
134
  finishedEvent = null;
154
135
  streamedToolErrorResults = /* @__PURE__ */ new Map();
155
136
  deferredToolCallRunFinishedChunks = [];
137
+ /** The model terminal is held until afterModel can choose an interrupt. */
138
+ deferredModelRunFinishedChunks = [];
156
139
  earlyTermination = false;
157
140
  toolPhase = "continue";
158
141
  cyclePhase = "processText";
@@ -162,6 +145,8 @@ var TextEngine = class {
162
145
  resumeClientToolResults = /* @__PURE__ */ new Map();
163
146
  resumeDeniedToolResults = /* @__PURE__ */ new Map();
164
147
  resumeCancelledToolCallIds = /* @__PURE__ */ new Set();
148
+ resumeGenericInterrupts = /* @__PURE__ */ new Map();
149
+ resumeGenericInterruptRequests = /* @__PURE__ */ new Map();
165
150
  threadId;
166
151
  runIdOverride;
167
152
  parentRunIdOverride;
@@ -183,6 +168,8 @@ var TextEngine = class {
183
168
  disconnectListeners = [];
184
169
  logger;
185
170
  structuredOutputResult = null;
171
+ structuredOutputMessageId = null;
172
+ structuredOutputMessageCreatedAt = null;
186
173
  combinedStartEmitted = false;
187
174
  combinedStructuredMessageId = null;
188
175
  validatedStructuredOutput = void 0;
@@ -193,6 +180,7 @@ var TextEngine = class {
193
180
  constructor(config, logger) {
194
181
  this.logger = logger;
195
182
  this.adapter = config.adapter;
183
+ this.interruptDefinitions = new Map((config.params.interrupts ?? []).map((definition) => [definition.id, definition]));
196
184
  this.finalStructuredOutput = config.finalStructuredOutput;
197
185
  this.params = config.params;
198
186
  this.systemPrompts = config.params.systemPrompts || [];
@@ -202,6 +190,7 @@ var TextEngine = class {
202
190
  this.initialApprovals = approvals;
203
191
  this.initialClientToolResults = clientToolResults;
204
192
  this.messages = convertMessagesToModelMessages(config.params.messages);
193
+ assertUniqueToolNames(config.params.tools || []);
205
194
  this.lazyToolManager = new LazyToolManager(config.params.tools || [], this.messages, config.params.lazyToolsConfig);
206
195
  this.tools = this.lazyToolManager.getActiveTools();
207
196
  this.toolCallManager = new ToolCallManager(this.tools);
@@ -263,6 +252,7 @@ var TextEngine = class {
263
252
  this.disconnectListeners.push(listener);
264
253
  if (this.disconnected) this.runDisconnectListener(listener);
265
254
  } });
255
+ provideGenericInterruptDefinitionRegistry(this.middlewareCtx, { definitions: this.interruptDefinitions });
266
256
  provideSandboxRuntime(this.middlewareCtx, {
267
257
  logger: this.logger,
268
258
  emit: (event) => {
@@ -321,7 +311,21 @@ var TextEngine = class {
321
311
  const transformedConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, initialConfig);
322
312
  this.applyMiddlewareConfig(transformedConfig);
323
313
  await this.applyEphemeralInterruptResume(transformedConfig);
314
+ await this.applyDurableGenericInterruptResolution();
324
315
  await this.middlewareRunner.runOnStart(this.middlewareCtx);
316
+ if (this.earlyTermination) {
317
+ yield* this.emitSuccessfulEarlyTermination();
318
+ if (!this.terminalHookCalled) {
319
+ this.terminalHookCalled = true;
320
+ await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
321
+ finishReason: this.lastFinishReason,
322
+ duration: Date.now() - this.streamStartTime,
323
+ content: this.accumulatedContent,
324
+ usage: this.finishedEvent?.usage
325
+ });
326
+ }
327
+ return;
328
+ }
325
329
  if ((yield* this.checkForPendingToolCalls()) === "wait") return;
326
330
  if (!(!!this.finalStructuredOutput && this.tools.length === 0 && this.finalStructuredOutput.nativeCombined !== true)) do {
327
331
  if (this.earlyTermination || this.isCancelled()) return;
@@ -333,7 +337,19 @@ var TextEngine = class {
333
337
  const iterConfig = this.buildMiddlewareConfig();
334
338
  const iterTransformedConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, iterConfig);
335
339
  this.applyMiddlewareConfig(iterTransformedConfig);
340
+ if (yield* this.emitBoundaryInterrupts("beforeModel", this.createSyntheticFinishedEvent())) {
341
+ this.setToolPhase("wait");
342
+ return;
343
+ }
336
344
  yield* this.streamModelResponse();
345
+ if (yield* this.emitBoundaryInterrupts("afterModel", this.finishedEvent ?? this.createSyntheticFinishedEvent())) {
346
+ this.setToolPhase("wait");
347
+ return;
348
+ }
349
+ if (this.shouldExecuteToolPhase()) {
350
+ this.deferredToolCallRunFinishedChunks.push(...this.deferredModelRunFinishedChunks);
351
+ this.deferredModelRunFinishedChunks = [];
352
+ } else yield* this.flushDeferredModelRunFinishedChunks();
337
353
  } else yield* this.processToolCalls();
338
354
  this.endCycle();
339
355
  } while (await this.shouldContinue());
@@ -355,6 +371,7 @@ var TextEngine = class {
355
371
  duration: Date.now() - this.streamStartTime
356
372
  });
357
373
  } else {
374
+ this.addTerminalAssistantMessages();
358
375
  this.terminalHookCalled = true;
359
376
  await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
360
377
  finishReason: this.lastFinishReason,
@@ -458,6 +475,7 @@ var TextEngine = class {
458
475
  this.accumulatedThinking = [];
459
476
  this.currentThinkingContent = "";
460
477
  this.currentThinkingSignature = "";
478
+ this.hasSeenReasoningEvents = false;
461
479
  this.finishedEvent = null;
462
480
  this.streamedToolErrorResults.clear();
463
481
  this.middlewareCtx.currentMessageId = this.currentMessageId;
@@ -508,7 +526,10 @@ var TextEngine = class {
508
526
  if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") {
509
527
  this.combinedStartEmitted = true;
510
528
  const startValue = chunk.value;
511
- if (startValue && typeof startValue === "object" && "messageId" in startValue && typeof startValue.messageId === "string") this.combinedStructuredMessageId = startValue.messageId;
529
+ if (startValue && typeof startValue === "object" && "messageId" in startValue && typeof startValue.messageId === "string") {
530
+ this.combinedStructuredMessageId = startValue.messageId;
531
+ this.captureStructuredOutputMessageIdentity(startValue.messageId);
532
+ }
512
533
  }
513
534
  let outboundChunk = chunk;
514
535
  if (this.finalStructuredOutput?.source === "event" && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
@@ -521,6 +542,11 @@ var TextEngine = class {
521
542
  };
522
543
  this.combinedCompleteEmitted = true;
523
544
  const value = chunk.value;
545
+ const completeMessageId = readCustomEventMessageId(value);
546
+ if (completeMessageId) {
547
+ this.combinedStructuredMessageId = completeMessageId;
548
+ this.captureStructuredOutputMessageIdentity(completeMessageId);
549
+ }
524
550
  if (object !== parsed.object && value && typeof value === "object") outboundChunk = {
525
551
  ...chunk,
526
552
  value: {
@@ -553,10 +579,15 @@ var TextEngine = class {
553
579
  const suppressAgentLifecycle = !!this.finalStructuredOutput && this.finalStructuredOutput.yieldChunks && this.finalStructuredOutput.nativeCombined !== true;
554
580
  for (const outputChunk of outputChunks) {
555
581
  if (suppressAgentLifecycle && (outputChunk.type === EventType.RUN_STARTED || outputChunk.type === EventType.RUN_FINISHED)) continue;
582
+ if (outputChunk.type === EventType.RUN_FINISHED) {
583
+ this.deferredModelRunFinishedChunks.push(outputChunk);
584
+ continue;
585
+ }
556
586
  if (this.shouldDeferToolCallRunFinished(outputChunk)) {
557
587
  this.deferredToolCallRunFinishedChunks.push(outputChunk);
558
588
  continue;
559
589
  }
590
+ if (outputChunk.type === EventType.RUN_STARTED) this.hasPublicRunStarted = true;
560
591
  this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk });
561
592
  yield outputChunk;
562
593
  this.middlewareCtx.chunkIndex++;
@@ -593,7 +624,10 @@ var TextEngine = class {
593
624
  case "STEP_STARTED":
594
625
  this.handleStepStartedEvent();
595
626
  break;
596
- case "STEP_FINISHED": this.handleStepFinishedEvent(chunk);
627
+ case "STEP_FINISHED":
628
+ this.handleStepFinishedEvent(chunk);
629
+ break;
630
+ case "REASONING_MESSAGE_CONTENT": this.handleReasoningMessageContentEvent(chunk);
597
631
  }
598
632
  }
599
633
  handleTextMessageContentEvent(chunk) {
@@ -609,6 +643,10 @@ var TextEngine = class {
609
643
  this.streamIdentityCaptured = true;
610
644
  }
611
645
  }
646
+ captureStructuredOutputMessageIdentity(messageId) {
647
+ this.structuredOutputMessageId = messageId;
648
+ this.structuredOutputMessageCreatedAt ??= /* @__PURE__ */ new Date();
649
+ }
612
650
  handleToolCallStartEvent(chunk) {
613
651
  if (typeof chunk.parentMessageId === "string" && chunk.parentMessageId !== "") this.captureStreamMessageIdentity(chunk.parentMessageId);
614
652
  this.toolCallManager.addToolCallStartEvent(chunk);
@@ -657,9 +695,19 @@ var TextEngine = class {
657
695
  this.finalizeCurrentThinkingStep();
658
696
  }
659
697
  handleStepFinishedEvent(chunk) {
660
- if (chunk.delta) this.currentThinkingContent += chunk.delta;
698
+ if (!this.hasSeenReasoningEvents) {
699
+ if (chunk.delta) this.currentThinkingContent += chunk.delta;
700
+ else if (chunk.content) {
701
+ if (chunk.content.startsWith(this.currentThinkingContent)) this.currentThinkingContent = chunk.content;
702
+ else if (!this.currentThinkingContent.startsWith(chunk.content)) this.currentThinkingContent += chunk.content;
703
+ }
704
+ }
661
705
  if (chunk.signature) this.currentThinkingSignature = chunk.signature;
662
706
  }
707
+ handleReasoningMessageContentEvent(chunk) {
708
+ this.hasSeenReasoningEvents = true;
709
+ this.currentThinkingContent += chunk.delta;
710
+ }
663
711
  /**
664
712
  * Tools available for execution this turn. The discovery tool is dropped
665
713
  * from the advertised set (`this.tools`) once every lazy tool is discovered,
@@ -696,6 +744,11 @@ var TextEngine = class {
696
744
  if (deferredErrorResults.length > 0) for (const chunk of this.buildToolResultChunks(deferredErrorResults, finishEvent, argsMap)) yield* this.pipeThroughMiddleware(chunk);
697
745
  return "continue";
698
746
  }
747
+ this.middlewareCtx.phase = "beforeTools";
748
+ if (yield* this.emitBoundaryInterrupts("beforeTools", finishEvent, executablePendingCalls)) {
749
+ this.setToolPhase("wait");
750
+ return "wait";
751
+ }
699
752
  const { approvals, clientToolResults } = this.collectClientState();
700
753
  const generator = executeToolCalls(executablePendingCalls, this.resolveExecutableTools(executablePendingCalls), approvals, clientToolResults, (eventName, data) => this.createCustomEventChunk(eventName, data), {
701
754
  onBeforeToolCall: async (toolCall, tool, args) => {
@@ -784,6 +837,10 @@ var TextEngine = class {
784
837
  return;
785
838
  }
786
839
  this.middlewareCtx.phase = "beforeTools";
840
+ if (yield* this.emitBoundaryInterrupts("beforeTools", finishEvent, executableToolCalls)) {
841
+ this.setToolPhase("wait");
842
+ return;
843
+ }
787
844
  const { approvals, clientToolResults } = this.collectClientState();
788
845
  const generator = executeToolCalls(executableToolCalls, this.resolveExecutableTools(executableToolCalls), approvals, clientToolResults, (eventName, data) => this.createCustomEventChunk(eventName, data), {
789
846
  onBeforeToolCall: async (toolCall, tool, args) => {
@@ -824,14 +881,22 @@ var TextEngine = class {
824
881
  needsApproval: executionResult.needsApproval,
825
882
  needsClientExecution: executionResult.needsClientExecution
826
883
  });
884
+ const afterToolBoundaryChunks = this.buildToolResultChunks(allResults, finishEvent);
885
+ const afterToolRequests = await this.middlewareRunner.runOnInterruptBoundary(this.middlewareCtx);
886
+ if (afterToolRequests.length > 0) {
887
+ for (const chunk of afterToolBoundaryChunks) yield* this.pipeThroughMiddleware(chunk);
888
+ yield* this.emitBoundaryInterrupts("afterTools", finishEvent, toolCalls, afterToolRequests);
889
+ this.setToolPhase("wait");
890
+ return;
891
+ }
827
892
  if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
828
- if (allResults.length > 0) for (const chunk of this.buildToolResultChunks(allResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
893
+ if (allResults.length > 0) for (const chunk of afterToolBoundaryChunks) yield* this.pipeThroughMiddleware(chunk);
829
894
  const emitted = yield* this.emitActionableInterruptBoundary(finishEvent, executionResult.needsApproval, executionResult.needsClientExecution);
830
895
  this.setToolPhase(emitted ? "wait" : "stop");
831
896
  return;
832
897
  }
833
898
  yield* this.flushDeferredToolCallRunFinishedChunks();
834
- const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
899
+ const toolResultChunks = afterToolBoundaryChunks;
835
900
  for (const chunk of toolResultChunks) yield* this.pipeThroughMiddleware(chunk);
836
901
  if (this.lazyToolManager.hasNewlyDiscoveredTools()) {
837
902
  this.tools = this.lazyToolManager.getActiveTools();
@@ -853,6 +918,37 @@ var TextEngine = class {
853
918
  }
854
919
  this.deferredToolCallRunFinishedChunks = [];
855
920
  }
921
+ *flushDeferredModelRunFinishedChunks() {
922
+ for (const chunk of this.deferredModelRunFinishedChunks) {
923
+ this.logger.output(`type=${chunk.type}`, { chunk });
924
+ yield chunk;
925
+ this.middlewareCtx.chunkIndex++;
926
+ }
927
+ this.deferredModelRunFinishedChunks = [];
928
+ }
929
+ async *emitSyntheticRunStarted(finishEvent) {
930
+ if (this.hasPublicRunStarted) return;
931
+ yield* this.pipeThroughMiddleware({
932
+ type: EventType.RUN_STARTED,
933
+ runId: finishEvent.runId,
934
+ threadId: finishEvent.threadId,
935
+ model: finishEvent.model,
936
+ timestamp: Date.now()
937
+ });
938
+ }
939
+ async *emitSuccessfulEarlyTermination() {
940
+ this.lastFinishReason = "stop";
941
+ const finishEvent = {
942
+ ...this.createSyntheticFinishedEvent(),
943
+ finishReason: "stop"
944
+ };
945
+ yield* this.emitSyntheticRunStarted(finishEvent);
946
+ yield* this.pipeThroughMiddleware({
947
+ ...finishEvent,
948
+ timestamp: Date.now(),
949
+ outcome: { type: "success" }
950
+ });
951
+ }
856
952
  discardDeferredToolCallRunFinishedChunks() {
857
953
  this.deferredToolCallRunFinishedChunks = [];
858
954
  }
@@ -871,6 +967,63 @@ var TextEngine = class {
871
967
  }];
872
968
  this.middlewareCtx.messages = this.messages;
873
969
  }
970
+ addTerminalAssistantMessages() {
971
+ this.finalizeCurrentThinkingStep();
972
+ const structuredResult = this.structuredOutputResult;
973
+ const raw = structuredResult ? structuredResult.rawText || safeJsonStringify(structuredResult.data) : "";
974
+ const structuredOutput = structuredResult ? {
975
+ type: "structured-output",
976
+ status: "complete",
977
+ data: structuredResult.data,
978
+ partial: structuredResult.data,
979
+ raw,
980
+ ...structuredResult.reasoning !== void 0 ? { reasoning: structuredResult.reasoning } : {}
981
+ } : void 0;
982
+ const nativeCombined = this.finalStructuredOutput?.nativeCombined === true;
983
+ const eventSourced = this.finalStructuredOutput?.source === "event";
984
+ const structuredId = this.structuredOutputMessageId ?? this.combinedStructuredMessageId ?? this.currentMessageId ?? this.createId("msg");
985
+ const splitStructuredMessage = Boolean(structuredOutput) && (!nativeCombined || eventSourced) && this.currentMessageId != null && structuredId !== this.currentMessageId;
986
+ const messages = [...this.middlewareCtx.messages];
987
+ const existingStructuredIndex = messages.findIndex((message) => message.role === "assistant" && message.id === structuredId);
988
+ const currentTurnAlreadyRecorded = messages.some((message) => message.role === "assistant" && message.id === this.currentMessageId);
989
+ const thinking = this.accumulatedThinking.length > 0 ? this.accumulatedThinking : void 0;
990
+ const startedLength = messages.length;
991
+ if (structuredOutput && existingStructuredIndex >= 0) {
992
+ const existing = messages[existingStructuredIndex];
993
+ if (existing) messages[existingStructuredIndex] = {
994
+ ...existing,
995
+ content: raw || existing.content,
996
+ structuredOutput
997
+ };
998
+ } else if (structuredOutput && !splitStructuredMessage) {
999
+ if (!currentTurnAlreadyRecorded) messages.push({
1000
+ role: "assistant",
1001
+ content: this.accumulatedContent || raw || null,
1002
+ id: structuredId,
1003
+ createdAt: this.currentMessageCreatedAt ?? this.structuredOutputMessageCreatedAt ?? /* @__PURE__ */ new Date(),
1004
+ structuredOutput,
1005
+ ...thinking ? { thinking } : {}
1006
+ });
1007
+ } else {
1008
+ if (!currentTurnAlreadyRecorded && (this.accumulatedContent !== "" || thinking)) messages.push({
1009
+ role: "assistant",
1010
+ content: this.accumulatedContent || null,
1011
+ id: this.currentMessageId ?? this.createId("msg"),
1012
+ createdAt: this.currentMessageCreatedAt ?? /* @__PURE__ */ new Date(),
1013
+ ...thinking ? { thinking } : {}
1014
+ });
1015
+ if (structuredOutput) messages.push({
1016
+ role: "assistant",
1017
+ content: raw || null,
1018
+ id: structuredId,
1019
+ createdAt: this.structuredOutputMessageCreatedAt ?? /* @__PURE__ */ new Date(),
1020
+ structuredOutput
1021
+ });
1022
+ }
1023
+ if (messages.length === startedLength && existingStructuredIndex < 0) return;
1024
+ this.messages = messages;
1025
+ this.middlewareCtx.messages = this.messages;
1026
+ }
874
1027
  /**
875
1028
  * Extract client state (approvals and client tool results) from original messages.
876
1029
  * This is called in the constructor BEFORE converting to ModelMessage format,
@@ -911,7 +1064,10 @@ var TextEngine = class {
911
1064
  clientToolResults
912
1065
  };
913
1066
  }
914
- buildActionableInterrupts(approvals, clientRequests) {
1067
+ genericInterruptId() {
1068
+ return this.createId("interrupt");
1069
+ }
1070
+ buildActionableInterrupts(approvals, clientRequests, genericRequests = [], genericInterruptIds = []) {
915
1071
  const interrupts = [];
916
1072
  for (const approval of approvals) {
917
1073
  const tool = this.tools.find((candidate) => candidate.name === approval.toolName);
@@ -965,25 +1121,60 @@ var TextEngine = class {
965
1121
  }
966
1122
  });
967
1123
  }
1124
+ for (const [index, request] of genericRequests.entries()) {
1125
+ const batchIndex = interrupts.length;
1126
+ const id = genericInterruptIds[index];
1127
+ if (!id) throw new Error("Generic interrupt id is unavailable.");
1128
+ const preEmission = createInterruptBinding(request, { batchIndex });
1129
+ interrupts.push({
1130
+ id,
1131
+ reason: request.reason,
1132
+ message: request.message,
1133
+ ...preEmission.descriptor.responseSchemaCanonicalJson !== void 0 ? { responseSchema: JSON.parse(preEmission.descriptor.responseSchemaCanonicalJson) } : {},
1134
+ ...request.expiresAt !== void 0 ? { expiresAt: request.expiresAt } : {},
1135
+ metadata: {
1136
+ [interruptBindingMetadataKey]: {
1137
+ v: 1,
1138
+ kind: "generic",
1139
+ interruptId: id,
1140
+ definitionId: preEmission.descriptor.definitionId,
1141
+ key: preEmission.descriptor.key,
1142
+ batchIndex,
1143
+ ...request.expiresAt !== void 0 ? { expiresAt: request.expiresAt } : {},
1144
+ ...preEmission.descriptor.payloadSchemaHash ? { payloadSchemaHash: preEmission.descriptor.payloadSchemaHash } : {},
1145
+ ...preEmission.descriptor.responseSchemaHash !== void 0 ? { responseSchemaHash: preEmission.descriptor.responseSchemaHash } : {}
1146
+ },
1147
+ ...preEmission.payload !== void 0 ? { [INTERRUPT_PAYLOAD_METADATA_KEY]: preEmission.payload } : {}
1148
+ }
1149
+ });
1150
+ }
1151
+ const ids = /* @__PURE__ */ new Set();
1152
+ for (const interrupt of interrupts) {
1153
+ if (ids.has(interrupt.id)) throw new Error(`Duplicate interrupt id in final batch: ${interrupt.id}`);
1154
+ ids.add(interrupt.id);
1155
+ }
968
1156
  return interrupts;
969
1157
  }
970
- buildInterruptFinishedChunk(finishEvent, approvals, clientRequests) {
1158
+ buildInterruptFinishedChunk(finishEvent, approvals, clientRequests, genericRequests = [], genericInterruptIds) {
971
1159
  return {
972
1160
  ...finishEvent,
973
1161
  timestamp: Date.now(),
974
1162
  outcome: {
975
1163
  type: "interrupt",
976
- interrupts: this.buildActionableInterrupts(approvals, clientRequests)
1164
+ interrupts: this.buildActionableInterrupts(approvals, clientRequests, genericRequests, genericInterruptIds)
977
1165
  }
978
1166
  };
979
1167
  }
980
1168
  buildMessagesSnapshotChunk() {
981
1169
  const messages = this.messages.map((message, index) => {
982
1170
  const content = typeof message.content === "string" ? message.content : message.content === null ? void 0 : JSON.stringify(message.content);
1171
+ const id = message.id || `snapshot_${this.runIdOverride ?? this.requestId}_${index}`;
1172
+ const parts = message.role === "assistant" && (message.thinking?.length || message.structuredOutput) ? modelMessageToUIMessage(message, id).parts : void 0;
983
1173
  return {
984
- id: message.id || `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
1174
+ id,
985
1175
  role: message.role,
986
1176
  ...content !== void 0 ? { content } : {},
1177
+ ...parts ? { parts } : {},
987
1178
  ..."toolCalls" in message && message.toolCalls ? { toolCalls: message.toolCalls } : {},
988
1179
  ..."toolCallId" in message && message.toolCallId ? { toolCallId: message.toolCallId } : {}
989
1180
  };
@@ -1062,8 +1253,10 @@ var TextEngine = class {
1062
1253
  };
1063
1254
  yield* this.pipeThroughMiddleware(this.buildInterruptRunErrorChunk(error));
1064
1255
  }
1065
- async *emitActionableInterruptBoundary(finishEvent, approvals, clientRequests) {
1066
- const terminal = this.completeEphemeralInterruptBindings(this.buildInterruptFinishedChunk(finishEvent, approvals, clientRequests));
1256
+ async *emitActionableInterruptBoundary(finishEvent, approvals, clientRequests, genericRequests = []) {
1257
+ yield* this.emitSyntheticRunStarted(finishEvent);
1258
+ const genericInterruptIds = genericRequests.map(() => this.genericInterruptId());
1259
+ const terminal = this.completeEphemeralInterruptBindings(this.buildInterruptFinishedChunk(finishEvent, approvals, clientRequests, genericRequests, genericInterruptIds));
1067
1260
  let terminalOutputs;
1068
1261
  try {
1069
1262
  terminalOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, terminal);
@@ -1084,6 +1277,59 @@ var TextEngine = class {
1084
1277
  }
1085
1278
  return true;
1086
1279
  }
1280
+ async *emitBoundaryInterrupts(phase, finishEvent, toolCalls = [], requests) {
1281
+ this.middlewareCtx.phase = phase;
1282
+ const boundaryRequests = requests ?? await this.middlewareRunner.runOnInterruptBoundary(this.middlewareCtx);
1283
+ if (boundaryRequests.length === 0) return false;
1284
+ for (const request of boundaryRequests) if (this.interruptDefinitions.get(request.definition.id) !== request.definition) throw new Error(`Generic interrupt definition ${request.definition.id} is not registered on this chat.`);
1285
+ if (phase === "afterModel") {
1286
+ if (this.toolCallManager.hasToolCalls()) this.addAssistantToolCallMessage(this.toolCallManager.getToolCalls());
1287
+ else this.addAssistantTextMessageForInterrupt();
1288
+ }
1289
+ const actionable = this.getBoundaryActionableToolRequests(toolCalls);
1290
+ yield* this.emitActionableInterruptBoundary(finishEvent, actionable.approvals, actionable.clientRequests, boundaryRequests);
1291
+ return true;
1292
+ }
1293
+ addAssistantTextMessageForInterrupt() {
1294
+ if (this.accumulatedContent.length === 0) return;
1295
+ this.messages = [...this.messages, {
1296
+ role: "assistant",
1297
+ content: this.accumulatedContent
1298
+ }];
1299
+ this.middlewareCtx.messages = this.messages;
1300
+ }
1301
+ getBoundaryActionableToolRequests(toolCalls) {
1302
+ const { approvals, clientToolResults } = this.collectClientState();
1303
+ const approvalRequests = [];
1304
+ const clientRequests = [];
1305
+ for (const toolCall of toolCalls) {
1306
+ const tool = this.resolveExecutableTools([toolCall]).find((candidate) => candidate.name === toolCall.function.name);
1307
+ if (!tool) continue;
1308
+ let input = {};
1309
+ try {
1310
+ const parsed = JSON.parse(toolCall.function.arguments.trim() || "{}");
1311
+ input = parsed && typeof parsed === "object" ? parsed : {};
1312
+ } catch {
1313
+ input = {};
1314
+ }
1315
+ const approvalId = `approval_${toolCall.id}`;
1316
+ if (tool.needsApproval && !approvals.has(approvalId)) approvalRequests.push({
1317
+ toolCallId: toolCall.id,
1318
+ toolName: toolCall.function.name,
1319
+ input,
1320
+ approvalId
1321
+ });
1322
+ else if (!tool.execute && !clientToolResults.has(toolCall.id) && !this.resumeCancelledToolCallIds.has(toolCall.id)) clientRequests.push({
1323
+ toolCallId: toolCall.id,
1324
+ toolName: toolCall.function.name,
1325
+ input
1326
+ });
1327
+ }
1328
+ return {
1329
+ approvals: approvalRequests,
1330
+ clientRequests
1331
+ };
1332
+ }
1087
1333
  completeEphemeralInterruptBindings(chunk) {
1088
1334
  if (chunk.type !== EventType.RUN_FINISHED || chunk.outcome?.type !== "interrupt") return chunk;
1089
1335
  const interruptedRunId = this.runIdOverride ?? this.requestId;
@@ -1230,7 +1476,7 @@ var TextEngine = class {
1230
1476
  createSyntheticFinishedEvent() {
1231
1477
  return {
1232
1478
  type: "RUN_FINISHED",
1233
- runId: this.createId("pending"),
1479
+ runId: this.runIdOverride ?? this.requestId,
1234
1480
  threadId: this.threadId,
1235
1481
  model: this.params.model,
1236
1482
  timestamp: Date.now(),
@@ -1416,6 +1662,7 @@ var TextEngine = class {
1416
1662
  const buildSynthesizedStart = (timestamp = Date.now()) => {
1417
1663
  const idForStart = structuredMessageId ?? generateMessageId();
1418
1664
  structuredMessageId = idForStart;
1665
+ this.captureStructuredOutputMessageIdentity(idForStart);
1419
1666
  return {
1420
1667
  type: EventType.CUSTOM,
1421
1668
  name: "structured-output.start",
@@ -1433,7 +1680,10 @@ var TextEngine = class {
1433
1680
  if (!startEmitted && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") startEmitted = true;
1434
1681
  if (!structuredMessageId) {
1435
1682
  const extracted = extractMessageId(chunk);
1436
- if (extracted) structuredMessageId = extracted;
1683
+ if (extracted) {
1684
+ structuredMessageId = extracted;
1685
+ this.captureStructuredOutputMessageIdentity(extracted);
1686
+ }
1437
1687
  }
1438
1688
  if (this.finalStructuredOutput.yieldChunks) {
1439
1689
  if (!startEmitted && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT || chunk.type === EventType.TEXT_MESSAGE_END)) {
@@ -1460,7 +1710,8 @@ var TextEngine = class {
1460
1710
  const object = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed.object) : parsed.object;
1461
1711
  this.structuredOutputResult = {
1462
1712
  data: object,
1463
- rawText: parsed.raw
1713
+ rawText: parsed.raw,
1714
+ ...parsed.reasoning !== void 0 ? { reasoning: parsed.reasoning } : {}
1464
1715
  };
1465
1716
  const value = chunk.value;
1466
1717
  if (object !== parsed.object && value && typeof value === "object") outboundChunk = {
@@ -1717,6 +1968,7 @@ var TextEngine = class {
1717
1968
  input: toolInputs.get(toolCall.id) ?? {}
1718
1969
  });
1719
1970
  }
1971
+ const genericPending = this.getGenericContinuationPending(interruptedRunId);
1720
1972
  const pending = this.buildActionableInterrupts(approvalRequests, clientRequests).flatMap((descriptor) => {
1721
1973
  const unopened = readUnopenedInterruptBinding(descriptor);
1722
1974
  return unopened ? [{
@@ -1729,6 +1981,7 @@ var TextEngine = class {
1729
1981
  }
1730
1982
  }] : [];
1731
1983
  });
1984
+ pending.push(...genericPending);
1732
1985
  const validated = await validateInterruptResumeBatch({
1733
1986
  threadId: this.threadId,
1734
1987
  interruptedRunId,
@@ -1744,17 +1997,130 @@ var TextEngine = class {
1744
1997
  ...validated.resumeToolState,
1745
1998
  approvals
1746
1999
  });
2000
+ const genericResolutions = validated.resumeToolState.genericInterrupts;
2001
+ if (genericPending.length > 0 && genericResolutions) {
2002
+ const resolutions = genericPending.sort((left, right) => {
2003
+ return (left.binding.kind === "generic" ? left.binding.batchIndex ?? 0 : 0) - (right.binding.kind === "generic" ? right.binding.batchIndex ?? 0 : 0);
2004
+ }).flatMap((record) => {
2005
+ const resolution = genericResolutions.get(record.interruptId);
2006
+ if (!resolution || !record.genericRequest) return [];
2007
+ return [resolution.status === "resolved" ? {
2008
+ request: record.genericRequest,
2009
+ status: "resolved",
2010
+ response: resolution.payload
2011
+ } : {
2012
+ request: record.genericRequest,
2013
+ status: "cancelled"
2014
+ }];
2015
+ });
2016
+ const policy = await this.middlewareRunner.runOnInterruptResolution(this.middlewareCtx, {
2017
+ for: (definition) => resolutions.filter((resolution) => resolution.request.definition === definition),
2018
+ all: (...definitions) => definitions.length === 0 ? resolutions : resolutions.filter((resolution) => definitions.includes(resolution.request.definition))
2019
+ });
2020
+ if (policy.toolResume === "stop") this.earlyTermination = true;
2021
+ else if (policy.toolResume === "cancel") for (const request of pendingToolCalls) this.resumeCancelledToolCallIds.add(request.id);
2022
+ }
2023
+ }
2024
+ getGenericContinuationPending(interruptedRunId) {
2025
+ const fail = (message) => {
2026
+ throw new InterruptResumeValidationError([{
2027
+ scope: "batch",
2028
+ threadId: this.threadId,
2029
+ interruptedRunId,
2030
+ generation: 0,
2031
+ interruptIds: [],
2032
+ code: "stale",
2033
+ message,
2034
+ source: "server",
2035
+ retryable: false
2036
+ }]);
2037
+ };
2038
+ const pending = [];
2039
+ const ids = /* @__PURE__ */ new Set();
2040
+ const batchIndexes = /* @__PURE__ */ new Set();
2041
+ for (const resumeItem of this.params.resume ?? []) {
2042
+ const parsed = readGenericInterruptContinuation(resumeItem.metadata);
2043
+ if (parsed.status === "absent") continue;
2044
+ if (parsed.status === "invalid") return fail(parsed.message);
2045
+ const entry = parsed.value;
2046
+ const id = resumeItem.interruptId;
2047
+ const definition = this.interruptDefinitions.get(entry.definitionId);
2048
+ if (!definition) return fail(`Generic interrupt definition ${entry.definitionId} is unavailable.`);
2049
+ if (ids.has(id) || batchIndexes.has(entry.batchIndex)) return fail("Generic interrupt continuation contains duplicate entries.");
2050
+ ids.add(id);
2051
+ batchIndexes.add(entry.batchIndex);
2052
+ let request;
2053
+ try {
2054
+ request = rehydrateInterruptRequest(definition, {
2055
+ key: entry.key,
2056
+ reason: entry.reason,
2057
+ message: entry.message,
2058
+ ...typeof entry.expiresAt === "string" ? { expiresAt: entry.expiresAt } : {},
2059
+ ...Object.prototype.hasOwnProperty.call(entry, "payload") ? { payload: entry.payload } : {}
2060
+ });
2061
+ } catch (error) {
2062
+ return fail(`Generic interrupt continuation ${id} is invalid: ${error instanceof Error ? error.message : String(error)}`);
2063
+ }
2064
+ const emitted = createInterruptBinding(request, { batchIndex: entry.batchIndex });
2065
+ if (entry.responseSchemaHash !== emitted.descriptor.responseSchemaHash || entry.payloadSchemaHash !== emitted.descriptor.payloadSchemaHash) return fail(`Generic interrupt continuation ${id} does not match its definition.`);
2066
+ pending.push({
2067
+ interruptId: id,
2068
+ payload: {
2069
+ id,
2070
+ ...emitted.descriptor.responseSchemaCanonicalJson !== void 0 ? { responseSchema: JSON.parse(emitted.descriptor.responseSchemaCanonicalJson) } : {}
2071
+ },
2072
+ binding: {
2073
+ v: 1,
2074
+ kind: "generic",
2075
+ interruptId: id,
2076
+ interruptedRunId,
2077
+ generation: 0,
2078
+ definitionId: entry.definitionId,
2079
+ key: entry.key,
2080
+ batchIndex: entry.batchIndex,
2081
+ ...typeof entry.expiresAt === "string" ? { expiresAt: entry.expiresAt } : {},
2082
+ ...emitted.descriptor.payloadSchemaHash ? { payloadSchemaHash: emitted.descriptor.payloadSchemaHash } : {},
2083
+ ...entry.responseSchemaHash !== void 0 ? { responseSchemaHash: entry.responseSchemaHash } : {}
2084
+ },
2085
+ genericRequest: request
2086
+ });
2087
+ }
2088
+ return pending;
1747
2089
  }
1748
2090
  applyResumeToolState(state) {
1749
2091
  if (state?.approvals) for (const [approvalId, resolution] of state.approvals) this.resumeApprovals.set(approvalId, resolution);
1750
2092
  if (state?.clientToolResults) for (const [toolCallId, result] of state.clientToolResults) this.resumeClientToolResults.set(toolCallId, result);
1751
2093
  if (state?.deniedToolResults) for (const [toolCallId, result] of state.deniedToolResults) this.resumeDeniedToolResults.set(toolCallId, result);
1752
2094
  if (state?.cancelledToolCallIds) for (const toolCallId of state.cancelledToolCallIds) this.resumeCancelledToolCallIds.add(toolCallId);
2095
+ if (state?.genericInterrupts) for (const [interruptId, resolution] of state.genericInterrupts) this.resumeGenericInterrupts.set(interruptId, resolution);
2096
+ if (state?.genericInterruptRequests) for (const [interruptId, request] of state.genericInterruptRequests) this.resumeGenericInterruptRequests.set(interruptId, request);
2097
+ }
2098
+ async applyDurableGenericInterruptResolution() {
2099
+ if (this.resumeGenericInterruptRequests.size === 0) return;
2100
+ const resolutions = [...this.resumeGenericInterruptRequests.entries()].flatMap(([interruptId, request]) => {
2101
+ const resolution = this.resumeGenericInterrupts.get(interruptId);
2102
+ if (!resolution) return [];
2103
+ return [resolution.status === "resolved" ? {
2104
+ request,
2105
+ status: "resolved",
2106
+ response: resolution.payload
2107
+ } : {
2108
+ request,
2109
+ status: "cancelled"
2110
+ }];
2111
+ });
2112
+ const policy = await this.middlewareRunner.runOnInterruptResolution(this.middlewareCtx, {
2113
+ for: (definition) => resolutions.filter((resolution) => resolution.request.definition === definition),
2114
+ all: (...definitions) => definitions.length === 0 ? resolutions : resolutions.filter((resolution) => definitions.includes(resolution.request.definition))
2115
+ });
2116
+ if (policy.toolResume === "stop") this.earlyTermination = true;
2117
+ else if (policy.toolResume === "cancel") for (const toolCall of this.getPendingToolCallsFromMessages()) this.resumeCancelledToolCallIds.add(toolCall.id);
1753
2118
  }
1754
2119
  applyMiddlewareConfig(config) {
1755
2120
  this.applyResumeToolState(config.resumeToolState);
1756
2121
  this.messages = config.messages;
1757
2122
  this.systemPrompts = config.systemPrompts;
2123
+ assertUniqueToolNames(config.tools);
1758
2124
  this.tools = config.tools;
1759
2125
  this.params = {
1760
2126
  ...this.params,
@@ -1777,6 +2143,7 @@ var TextEngine = class {
1777
2143
  async *pipeThroughMiddleware(chunk) {
1778
2144
  const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, chunk);
1779
2145
  for (const outputChunk of outputChunks) {
2146
+ if (outputChunk.type === EventType.RUN_STARTED) this.hasPublicRunStarted = true;
1780
2147
  yield outputChunk;
1781
2148
  this.middlewareCtx.chunkIndex++;
1782
2149
  }
@@ -1878,27 +2245,47 @@ var TextEngine = class {
1878
2245
  * ```
1879
2246
  */
1880
2247
  function chat(options) {
1881
- validateCapabilities(options.middleware ?? [], options.adapter);
2248
+ validateInterruptDefinitions(options.interrupts);
2249
+ validateCapabilities(readRuntimeMiddleware(options.middleware) ?? [], options.adapter);
2250
+ if (options.tools) assertUniqueToolNames(options.tools);
1882
2251
  const { outputSchema, stream } = options;
1883
- if (outputSchema && stream === true) return runStreamingStructuredOutput({
1884
- ...options,
2252
+ if (outputSchema && stream === true) return runStreamingStructuredOutput(toRuntimeTextActivityOptions(options, {
1885
2253
  outputSchema,
1886
- stream
1887
- });
1888
- if (outputSchema) return runAgenticStructuredOutput({
1889
- ...options,
1890
- outputSchema
1891
- });
1892
- if (stream === false) return runNonStreamingText({
1893
- ...options,
2254
+ stream: true
2255
+ }));
2256
+ if (outputSchema) return runAgenticStructuredOutput(toRuntimeTextActivityOptions(options, {
2257
+ outputSchema,
2258
+ stream: false
2259
+ }));
2260
+ if (stream === false) return runNonStreamingText(toRuntimeTextActivityOptions(options, {
1894
2261
  outputSchema: void 0,
1895
- stream
1896
- });
1897
- return runStreamingText({
1898
- ...options,
2262
+ stream: false
2263
+ }));
2264
+ return runStreamingText(toRuntimeTextActivityOptions(options, {
1899
2265
  outputSchema: void 0,
1900
- stream
1901
- });
2266
+ stream: true
2267
+ }));
2268
+ }
2269
+ function readRuntimeMiddleware(middleware) {
2270
+ if (middleware === void 0) return void 0;
2271
+ if (!Array.isArray(middleware)) throw new TypeError("Chat middleware must be an array.");
2272
+ return middleware;
2273
+ }
2274
+ function toRuntimeTextActivityOptions(options, overrides) {
2275
+ const { middleware, ...rest } = options;
2276
+ return {
2277
+ ...rest,
2278
+ ...overrides,
2279
+ ...middleware === void 0 ? {} : { middleware: readRuntimeMiddleware(middleware) }
2280
+ };
2281
+ }
2282
+ function validateInterruptDefinitions(definitions) {
2283
+ if (!definitions) return;
2284
+ const seen = /* @__PURE__ */ new Set();
2285
+ for (const definition of definitions) {
2286
+ if (seen.has(definition.id)) throw new Error(`Duplicate interrupt definition id: ${definition.id}`);
2287
+ seen.add(definition.id);
2288
+ }
1902
2289
  }
1903
2290
  /**
1904
2291
  * Publish both delivery-side seams for `stream`.
@@ -1959,7 +2346,10 @@ async function* streamTextChunks(options, engineRef) {
1959
2346
  * Runs the full agentic loop (if tools are provided) but returns collected text.
1960
2347
  */
1961
2348
  function runNonStreamingText(options) {
1962
- const stream = runStreamingText(options);
2349
+ const stream = runStreamingText({
2350
+ ...options,
2351
+ stream: true
2352
+ });
1963
2353
  return streamToText(stream);
1964
2354
  }
1965
2355
  /**
@@ -2027,6 +2417,12 @@ async function runAgenticStructuredOutput(options) {
2027
2417
  * Uses an `unknown`-input runtime check rather than `as` casts so the engine
2028
2418
  * stays cast-free in its hot path.
2029
2419
  */
2420
+ function readCustomEventMessageId(value) {
2421
+ if (typeof value !== "object" || value === null) return void 0;
2422
+ if (!("messageId" in value)) return void 0;
2423
+ const messageId = value.messageId;
2424
+ return typeof messageId === "string" && messageId !== "" ? messageId : void 0;
2425
+ }
2030
2426
  function readStructuredOutputCompleteValue(value) {
2031
2427
  if (typeof value !== "object" || value === null) return null;
2032
2428
  if (!("object" in value) || !("raw" in value)) return null;