@tanstack/ai 0.46.0 → 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 (66) hide show
  1. package/dist/esm/activities/chat/index.d.ts +36 -11
  2. package/dist/esm/activities/chat/index.js +435 -76
  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/adapter-internals.d.ts +6 -0
  24. package/dist/esm/adapter-internals.js +4 -1
  25. package/dist/esm/client.d.ts +4 -0
  26. package/dist/esm/client.js +3 -1
  27. package/dist/esm/client.js.map +1 -1
  28. package/dist/esm/generic-interrupt-continuation.d.ts +45 -0
  29. package/dist/esm/generic-interrupt-continuation.js +80 -0
  30. package/dist/esm/generic-interrupt-continuation.js.map +1 -0
  31. package/dist/esm/index.d.ts +6 -1
  32. package/dist/esm/index.js +4 -1
  33. package/dist/esm/interrupt-definition.d.ts +113 -0
  34. package/dist/esm/interrupt-definition.js +169 -0
  35. package/dist/esm/interrupt-definition.js.map +1 -0
  36. package/dist/esm/interrupt-resume.d.ts +3 -0
  37. package/dist/esm/interrupt-resume.js +77 -16
  38. package/dist/esm/interrupt-resume.js.map +1 -1
  39. package/dist/esm/interrupts.d.ts +12 -3
  40. package/dist/esm/interrupts.js.map +1 -1
  41. package/dist/esm/types.d.ts +11 -3
  42. package/dist/esm/utilities/chat-params.js +10 -1
  43. package/dist/esm/utilities/chat-params.js.map +1 -1
  44. package/package.json +1 -1
  45. package/skills/ai-core/media-generation/SKILL.md +4 -1
  46. package/skills/ai-core/middleware/SKILL.md +53 -44
  47. package/skills/ai-core/structured-outputs/SKILL.md +59 -55
  48. package/skills/ai-core/tool-calling/SKILL.md +54 -1
  49. package/src/activities/chat/index.ts +1024 -203
  50. package/src/activities/chat/messages.ts +11 -3
  51. package/src/activities/chat/middleware/builder.ts +29 -4
  52. package/src/activities/chat/middleware/compose.ts +95 -5
  53. package/src/activities/chat/middleware/define.ts +13 -3
  54. package/src/activities/chat/middleware/generic-interrupts.ts +26 -0
  55. package/src/activities/chat/middleware/index.ts +15 -0
  56. package/src/activities/chat/middleware/types.ts +127 -2
  57. package/src/activities/chat/stream/processor.ts +21 -0
  58. package/src/adapter-internals.ts +20 -0
  59. package/src/client.ts +20 -0
  60. package/src/generic-interrupt-continuation.ts +162 -0
  61. package/src/index.ts +34 -0
  62. package/src/interrupt-definition.ts +581 -0
  63. package/src/interrupt-resume.ts +156 -25
  64. package/src/interrupts.ts +13 -3
  65. package/src/types.ts +11 -3
  66. package/src/utilities/chat-params.ts +16 -3
@@ -14,10 +14,21 @@ import { EventType } from '../../types'
14
14
  import {
15
15
  INTERRUPT_BINDING_METADATA_KEY,
16
16
  InterruptResumeValidationError,
17
+ readInterruptBinding,
17
18
  readUnopenedInterruptBinding,
18
19
  validateInterruptResumeBatch,
19
20
  } from '../../interrupt-resume'
20
21
  import { INTERRUPT_BINDING_VERSION } from '../../interrupts'
22
+ import {
23
+ INTERRUPT_PAYLOAD_METADATA_KEY,
24
+ createInterruptBinding,
25
+ rehydrateInterruptRequest,
26
+ } from '../../interrupt-definition'
27
+ import { readGenericInterruptContinuation } from '../../generic-interrupt-continuation'
28
+ import type {
29
+ GenericInterruptRequest,
30
+ InterruptDefinition,
31
+ } from '../../interrupt-definition'
21
32
  import {
22
33
  canonicalInterruptJson,
23
34
  digestInterruptJson,
@@ -47,6 +58,7 @@ import {
47
58
  convertMessagesToModelMessages,
48
59
  generateMessageId,
49
60
  modelMessageToUIMessage,
61
+ safeJsonStringify,
50
62
  } from './messages'
51
63
  import { MiddlewareRunner } from './middleware/compose'
52
64
  import { getRunDetached } from './middleware/run-store'
@@ -89,6 +101,7 @@ import type {
89
101
  SchemaInput,
90
102
  StreamChunk,
91
103
  StructuredOutputCompleteEvent,
104
+ StructuredOutputPart,
92
105
  StructuredOutputStream,
93
106
  TextMessageContentEvent,
94
107
  TextOptions,
@@ -104,10 +117,13 @@ import type {
104
117
  ChatMiddleware,
105
118
  ChatMiddlewareConfig,
106
119
  ChatMiddlewareContext,
120
+ ChatResumeGenericResolution,
107
121
  ChatResumeToolState,
122
+ InterruptResolutionCollection,
108
123
  SandboxFileHookEvent,
109
124
  StructuredOutputMiddlewareConfig,
110
125
  } from './middleware/types'
126
+ import { provideGenericInterruptDefinitionRegistry } from './middleware/generic-interrupts'
111
127
  import type { CheckCoverage } from './middleware/builder'
112
128
  import type { SystemPrompt } from '../../system-prompts'
113
129
  import type { InternalLogger } from '../../logger/internal-logger'
@@ -204,74 +220,11 @@ function normalizePublicInterruptBinding(
204
220
  value: unknown,
205
221
  expectedInterruptId: string,
206
222
  ): InterruptBinding | undefined {
207
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
208
- return undefined
209
- }
210
- const binding: Record<string, unknown> = Object.fromEntries(
211
- Object.entries(value),
212
- )
213
- if (
214
- binding.interruptId !== expectedInterruptId ||
215
- // A binding version we don't recognise belongs to another producer. Drop
216
- // it rather than reading our fields out of it.
217
- (binding.v !== undefined && binding.v !== INTERRUPT_BINDING_VERSION) ||
218
- typeof binding.interruptedRunId !== 'string' ||
219
- typeof binding.generation !== 'number' ||
220
- !Number.isInteger(binding.generation) ||
221
- binding.generation < 0 ||
222
- typeof binding.responseSchemaHash !== 'string' ||
223
- (binding.expiresAt !== undefined && typeof binding.expiresAt !== 'string')
224
- ) {
225
- return undefined
226
- }
227
- const base = {
228
- v: INTERRUPT_BINDING_VERSION,
229
- interruptId: binding.interruptId,
230
- interruptedRunId: binding.interruptedRunId,
231
- generation: binding.generation,
232
- responseSchemaHash: binding.responseSchemaHash,
233
- ...(typeof binding.expiresAt === 'string'
234
- ? { expiresAt: binding.expiresAt }
235
- : {}),
236
- }
237
- if (binding.kind === 'generic') {
238
- return { kind: binding.kind, ...base }
239
- }
240
- if (
241
- typeof binding.toolName !== 'string' ||
242
- typeof binding.toolCallId !== 'string'
243
- ) {
244
- return undefined
245
- }
246
- if (
247
- binding.kind === 'client-tool-execution' &&
248
- typeof binding.outputSchemaHash === 'string'
249
- ) {
250
- return {
251
- kind: binding.kind,
252
- ...base,
253
- toolName: binding.toolName,
254
- toolCallId: binding.toolCallId,
255
- outputSchemaHash: binding.outputSchemaHash,
256
- }
257
- }
258
- if (
259
- binding.kind === 'tool-approval' &&
260
- Object.prototype.hasOwnProperty.call(binding, 'originalArgs') &&
261
- typeof binding.inputSchemaHash === 'string' &&
262
- typeof binding.approvalSchemaHash === 'string'
263
- ) {
264
- return {
265
- kind: binding.kind,
266
- ...base,
267
- toolName: binding.toolName,
268
- toolCallId: binding.toolCallId,
269
- originalArgs: binding.originalArgs,
270
- inputSchemaHash: binding.inputSchemaHash,
271
- approvalSchemaHash: binding.approvalSchemaHash,
272
- }
273
- }
274
- return undefined
223
+ return readInterruptBinding({
224
+ id: expectedInterruptId,
225
+ reason: '',
226
+ metadata: { [INTERRUPT_BINDING_METADATA_KEY]: value },
227
+ })
275
228
  }
276
229
 
277
230
  // The leaf context-inference primitives (KnownContext, MergeContext,
@@ -310,33 +263,133 @@ type InferredContext<TTools, TMiddleware> = [
310
263
  ? unknown
311
264
  : ContextFromInputs<TTools, TMiddleware>
312
265
 
313
- type RequiredContextFromInputs<TTools, TMiddleware> = [
314
- ContextFromInputs<TTools, TMiddleware>,
266
+ type RegistryInterrupt<
267
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
268
+ > = [TInterrupts[number]] extends [never] ? never : TInterrupts[number]
269
+
270
+ type DuplicateInterruptDefinitionId<
271
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
272
+ TSeenIds extends string = never,
273
+ > = TInterrupts extends readonly [infer THead, ...infer TTail]
274
+ ? THead extends InterruptDefinition<infer TId, any, any, any>
275
+ ? string extends TId
276
+ ? TTail extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
277
+ ? DuplicateInterruptDefinitionId<TTail, TSeenIds>
278
+ : never
279
+ : TId extends TSeenIds
280
+ ? TId
281
+ : TTail extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
282
+ ? DuplicateInterruptDefinitionId<TTail, TSeenIds | TId>
283
+ : never
284
+ : never
285
+ : never
286
+
287
+ type CheckUniqueInterruptDefinitions<
288
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
289
+ > = [DuplicateInterruptDefinitionId<TInterrupts>] extends [never]
290
+ ? unknown
291
+ : {
292
+ readonly '✖ Duplicate interrupt definition id in chat({ interrupts }).': never
293
+ }
294
+
295
+ type InlineChatContext<TTools, TContext> = MergeContext<
296
+ ContextFromArray<NonNullable<TTools>>,
297
+ TContext
298
+ >
299
+
300
+ type RegistryChatMiddleware<
301
+ TContext,
302
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
303
+ > = ChatMiddleware<TContext, RegistryInterrupt<TInterrupts>>
304
+
305
+ type MiddlewareInterruptDefinitions<TMiddleware> =
306
+ TMiddleware extends ReadonlyArray<infer TMiddlewareItem>
307
+ ? TMiddlewareItem extends ChatMiddleware<any, infer TDefinitions>
308
+ ? TDefinitions
309
+ : never
310
+ : never
311
+
312
+ type IsAny<TValue> = 0 extends 1 & TValue ? true : false
313
+
314
+ type CheckInterruptRegistry<
315
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
316
+ TMiddleware,
317
+ > =
318
+ IsAny<MiddlewareInterruptDefinitions<TMiddleware>> extends true
319
+ ? unknown
320
+ : [MiddlewareInterruptDefinitions<TMiddleware>] extends [never]
321
+ ? unknown
322
+ : [
323
+ Exclude<
324
+ MiddlewareInterruptDefinitions<TMiddleware>,
325
+ RegistryInterrupt<TInterrupts>
326
+ >,
327
+ ] extends [never]
328
+ ? unknown
329
+ : {
330
+ readonly '✖ Middleware emits an interrupt definition that is not registered in chat({ interrupts }).': never
331
+ }
332
+
333
+ type RuntimeContextOption<TTools, TMiddleware, TContext> = [
334
+ MergeContext<ContextFromInputs<TTools, TMiddleware>, TContext>,
315
335
  ] extends [never]
316
- ? { context?: unknown }
317
- : undefined extends ContextFromInputs<TTools, TMiddleware>
318
- ? { context?: ContextFromInputs<TTools, TMiddleware> }
319
- : { context: ContextFromInputs<TTools, TMiddleware> }
336
+ ? { context?: TContext }
337
+ : undefined extends MergeContext<
338
+ ContextFromInputs<TTools, TMiddleware>,
339
+ TContext
340
+ >
341
+ ? {
342
+ context?: MergeContext<ContextFromInputs<TTools, TMiddleware>, TContext>
343
+ }
344
+ : {
345
+ context: MergeContext<ContextFromInputs<TTools, TMiddleware>, TContext>
346
+ }
347
+
348
+ type ExactMiddlewareOption<
349
+ TTools,
350
+ TContext,
351
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
352
+ TMiddleware extends Array<unknown> | undefined,
353
+ > = [TMiddleware] extends [undefined]
354
+ ? Array<
355
+ RegistryChatMiddleware<
356
+ InlineChatContext<TTools, TContext>,
357
+ NoInfer<TInterrupts>
358
+ >
359
+ >
360
+ : TMiddleware &
361
+ (TMiddleware extends Array<
362
+ RegistryChatMiddleware<
363
+ InlineChatContext<TTools, NoInfer<TContext>>,
364
+ NoInfer<TInterrupts>
365
+ >
366
+ >
367
+ ? Array<
368
+ RegistryChatMiddleware<
369
+ InlineChatContext<TTools, TContext>,
370
+ NoInfer<TInterrupts>
371
+ >
372
+ >
373
+ : CheckInterruptRegistry<TInterrupts, TMiddleware>) &
374
+ CheckCoverage<Extract<TMiddleware, ReadonlyArray<AnyChatMiddleware>>>
320
375
 
321
376
  type TextActivityOptionsWithContext<
322
377
  TAdapter extends AnyTextAdapter,
323
378
  TSchema extends SchemaInput | undefined,
324
379
  TStream extends boolean,
325
380
  TTools extends TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
326
- TMiddleware extends TextActivityOptions<
327
- TAdapter,
328
- TSchema,
329
- TStream,
330
- any
331
- >['middleware'],
381
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
382
+ [],
383
+ TContext = unknown,
384
+ TMiddleware extends Array<unknown> | undefined = undefined,
332
385
  > = Omit<
333
386
  TextActivityOptions<TAdapter, TSchema, TStream, any>,
334
- 'tools' | 'middleware' | 'context'
387
+ 'tools' | 'middleware' | 'context' | 'interrupts'
335
388
  > & {
336
389
  tools?: TTools
337
- middleware?: TMiddleware &
338
- CheckCoverage<Extract<TMiddleware, ReadonlyArray<AnyChatMiddleware>>>
339
- } & RequiredContextFromInputs<TTools, TMiddleware>
390
+ interrupts?: TInterrupts & CheckUniqueInterruptDefinitions<TInterrupts>
391
+ middleware?: ExactMiddlewareOption<TTools, TContext, TInterrupts, TMiddleware>
392
+ } & RuntimeContextOption<TTools, TMiddleware, TContext>
340
393
 
341
394
  // ===========================
342
395
  // Activity Options Type
@@ -494,6 +547,11 @@ export interface TextActivityOptions<
494
547
  * ```
495
548
  */
496
549
  middleware?: Array<ChatMiddleware<TContext>>
550
+ /**
551
+ * First-party generic interrupt definitions for this chat call.
552
+ * Register the same definitions on the client to type payloads and answers.
553
+ */
554
+ interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>>
497
555
  /**
498
556
  * Runtime context value passed to middleware hooks and server tools.
499
557
  */
@@ -534,28 +592,21 @@ export function createChatOptions<
534
592
  TStream,
535
593
  any
536
594
  >['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
537
- const TMiddleware extends TextActivityOptions<
538
- TAdapter,
539
- TSchema,
540
- TStream,
541
- any
542
- >['middleware'] = TextActivityOptions<
543
- TAdapter,
544
- TSchema,
545
- TStream,
546
- any
547
- >['middleware'],
595
+ const TInterrupts extends ReadonlyArray<
596
+ InterruptDefinition<any, any, any, any>
597
+ > = [],
598
+ TContext = unknown,
599
+ const TMiddleware extends Array<unknown> | undefined = undefined,
548
600
  >(
549
601
  options: TextActivityOptionsWithContext<
550
602
  TAdapter,
551
603
  TSchema,
552
604
  TStream,
553
605
  TTools,
606
+ TInterrupts,
607
+ TContext,
554
608
  TMiddleware
555
609
  >,
556
- // Preserve the concrete `tools` tuple on the returned options (so a later
557
- // `chat({ ...opts })` still narrows tool-call events to the tool names)
558
- // while threading the inferred runtime context like the bare options type.
559
610
  ): Omit<
560
611
  TextActivityOptions<
561
612
  TAdapter,
@@ -563,8 +614,12 @@ export function createChatOptions<
563
614
  TStream,
564
615
  InferredContext<TTools, TMiddleware>
565
616
  >,
566
- 'tools'
567
- > & { tools?: TTools } {
617
+ 'tools' | 'middleware' | 'interrupts'
618
+ > & {
619
+ tools?: TTools
620
+ interrupts?: TInterrupts
621
+ middleware?: ExactMiddlewareOption<TTools, TContext, TInterrupts, TMiddleware>
622
+ } {
568
623
  return options
569
624
  }
570
625
 
@@ -628,7 +683,7 @@ interface TextEngineConfig<
628
683
  adapter: TAdapter
629
684
  systemPrompts?: Array<SystemPrompt>
630
685
  params: TParams
631
- middleware?: Array<ChatMiddleware<TContext>>
686
+ middleware?: Array<AnyChatMiddleware>
632
687
  context?: TContext
633
688
  /**
634
689
  * If set, after the agent loop finishes the engine runs a
@@ -712,12 +767,18 @@ class TextEngine<
712
767
  >,
713
768
  > {
714
769
  private readonly adapter: TAdapter
770
+ private readonly interruptDefinitions: ReadonlyMap<
771
+ string,
772
+ InterruptDefinition<any, any, any, any>
773
+ >
715
774
  private params: TParams
716
775
  private systemPrompts: Array<SystemPrompt>
717
776
  private tools: Array<AnyRuntimeTool>
718
777
  private readonly loopStrategy: AgentLoopStrategy
719
778
  private toolCallManager: ToolCallManager<ReadonlyArray<AnyTool>, TContext>
720
779
  private readonly lazyToolManager: LazyToolManager
780
+ /** A public interruption terminal must always have this run's start event. */
781
+ private hasPublicRunStarted = false
721
782
  private readonly initialMessageCount: number
722
783
  private readonly requestId: string
723
784
  private readonly streamId: string
@@ -749,6 +810,9 @@ class TextEngine<
749
810
  private finishedEvent: RunFinishedEvent | null = null
750
811
  private readonly streamedToolErrorResults = new Map<string, ToolResult>()
751
812
  private deferredToolCallRunFinishedChunks: Array<StreamChunk> = []
813
+ /** The model terminal is held until afterModel can choose an interrupt. */
814
+ private deferredModelRunFinishedChunks: Array<StreamChunk> = []
815
+
752
816
  private earlyTermination = false
753
817
  private toolPhase: ToolPhaseResult = 'continue'
754
818
  private cyclePhase: CyclePhase = 'processText'
@@ -759,6 +823,14 @@ class TextEngine<
759
823
  private readonly resumeClientToolResults = new Map<string, any>()
760
824
  private readonly resumeDeniedToolResults = new Map<string, unknown>()
761
825
  private readonly resumeCancelledToolCallIds = new Set<string>()
826
+ private readonly resumeGenericInterrupts = new Map<
827
+ string,
828
+ ChatResumeGenericResolution
829
+ >()
830
+ private readonly resumeGenericInterruptRequests = new Map<
831
+ string,
832
+ GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
833
+ >()
762
834
 
763
835
  // AG-UI protocol IDs
764
836
  private readonly threadId: string
@@ -766,7 +838,10 @@ class TextEngine<
766
838
  private readonly parentRunIdOverride?: string
767
839
 
768
840
  // Middleware support
769
- private readonly middlewareRunner: MiddlewareRunner<TContext>
841
+ private readonly middlewareRunner: MiddlewareRunner<
842
+ TContext,
843
+ InterruptDefinition<any, any, any, any>
844
+ >
770
845
  private readonly middlewareCtx: ChatMiddlewareContext<TContext>
771
846
  private readonly sandboxFileQueue: Array<StreamChunk> = []
772
847
  private readonly deferredPromises: Array<Promise<unknown>> = []
@@ -788,8 +863,13 @@ class TextEngine<
788
863
  private readonly logger: InternalLogger
789
864
 
790
865
  // Structured-output finalization state (populated by runStructuredFinalization)
791
- private structuredOutputResult: { data: unknown; rawText: string } | null =
792
- null
866
+ private structuredOutputResult: {
867
+ data: unknown
868
+ rawText: string
869
+ reasoning?: string
870
+ } | null = null
871
+ private structuredOutputMessageId: string | null = null
872
+ private structuredOutputMessageCreatedAt: Date | null = null
793
873
  // Native combined mode: tracks whether we've already emitted the synthetic
794
874
  // `structured-output.start` event before the schema-constrained final-turn
795
875
  // text begins streaming. The event must precede the first
@@ -826,6 +906,15 @@ class TextEngine<
826
906
  ) {
827
907
  this.logger = logger
828
908
  this.adapter = config.adapter
909
+ this.interruptDefinitions = new Map(
910
+ (
911
+ (
912
+ config.params as TParams & {
913
+ interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>>
914
+ }
915
+ ).interrupts ?? []
916
+ ).map((definition) => [definition.id, definition]),
917
+ )
829
918
  this.finalStructuredOutput = config.finalStructuredOutput
830
919
  this.params = config.params
831
920
  this.systemPrompts = config.params.systemPrompts || []
@@ -878,7 +967,9 @@ class TextEngine<
878
967
  // handleStreamChunk processes raw chunks BEFORE middleware, so internal
879
968
  // state management sees extended fields (finishReason, delta, toolCallName, etc.).
880
969
  // The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
881
- const allMiddleware: Array<ChatMiddleware<TContext>> = [
970
+ const allMiddleware: Array<
971
+ ChatMiddleware<TContext, InterruptDefinition<any, any, any, any>>
972
+ > = [
882
973
  devtoolsMiddleware(),
883
974
  ...(config.middleware || []),
884
975
  stripToSpecMiddleware(),
@@ -957,6 +1048,10 @@ class TextEngine<
957
1048
  },
958
1049
  })
959
1050
 
1051
+ provideGenericInterruptDefinitionRegistry(this.middlewareCtx, {
1052
+ definitions: this.interruptDefinitions,
1053
+ })
1054
+
960
1055
  // Provide the internal SandboxRuntime capability so harness adapters and
961
1056
  // sandbox middleware can emit file events. The sink logs, fans the event
962
1057
  // out through the middleware `onFile*` hooks (fire-and-forget), and queues
@@ -1048,10 +1143,25 @@ class TextEngine<
1048
1143
  )
1049
1144
  this.applyMiddlewareConfig(transformedConfig)
1050
1145
  await this.applyEphemeralInterruptResume(transformedConfig)
1146
+ await this.applyDurableGenericInterruptResolution()
1051
1147
 
1052
1148
  // Run onStart (devtools middleware emits text:request:started and initial messages here)
1053
1149
  await this.middlewareRunner.runOnStart(this.middlewareCtx)
1054
1150
 
1151
+ if (this.earlyTermination) {
1152
+ yield* this.emitSuccessfulEarlyTermination()
1153
+ if (!this.terminalHookCalled) {
1154
+ this.terminalHookCalled = true
1155
+ await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
1156
+ finishReason: this.lastFinishReason,
1157
+ duration: Date.now() - this.streamStartTime,
1158
+ content: this.accumulatedContent,
1159
+ usage: this.finishedEvent?.usage,
1160
+ })
1161
+ }
1162
+ return
1163
+ }
1164
+
1055
1165
  const pendingPhase = yield* this.checkForPendingToolCalls()
1056
1166
  if (pendingPhase === 'wait') {
1057
1167
  return
@@ -1095,7 +1205,35 @@ class TextEngine<
1095
1205
  )
1096
1206
  this.applyMiddlewareConfig(iterTransformedConfig)
1097
1207
 
1208
+ if (
1209
+ yield* this.emitBoundaryInterrupts(
1210
+ 'beforeModel',
1211
+ this.createSyntheticFinishedEvent(),
1212
+ )
1213
+ ) {
1214
+ this.setToolPhase('wait')
1215
+ return
1216
+ }
1217
+
1098
1218
  yield* this.streamModelResponse()
1219
+
1220
+ if (
1221
+ yield* this.emitBoundaryInterrupts(
1222
+ 'afterModel',
1223
+ this.finishedEvent ?? this.createSyntheticFinishedEvent(),
1224
+ )
1225
+ ) {
1226
+ this.setToolPhase('wait')
1227
+ return
1228
+ }
1229
+ if (this.shouldExecuteToolPhase()) {
1230
+ this.deferredToolCallRunFinishedChunks.push(
1231
+ ...this.deferredModelRunFinishedChunks,
1232
+ )
1233
+ this.deferredModelRunFinishedChunks = []
1234
+ } else {
1235
+ yield* this.flushDeferredModelRunFinishedChunks()
1236
+ }
1099
1237
  } else {
1100
1238
  yield* this.processToolCalls()
1101
1239
  }
@@ -1159,7 +1297,7 @@ class TextEngine<
1159
1297
  duration: Date.now() - this.streamStartTime,
1160
1298
  })
1161
1299
  } else {
1162
- this.addTerminalReasoningMessage()
1300
+ this.addTerminalAssistantMessages()
1163
1301
  this.terminalHookCalled = true
1164
1302
  await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
1165
1303
  finishReason: this.lastFinishReason,
@@ -1403,6 +1541,7 @@ class TextEngine<
1403
1541
  typeof startValue.messageId === 'string'
1404
1542
  ) {
1405
1543
  this.combinedStructuredMessageId = startValue.messageId
1544
+ this.captureStructuredOutputMessageIdentity(startValue.messageId)
1406
1545
  }
1407
1546
  }
1408
1547
 
@@ -1420,6 +1559,11 @@ class TextEngine<
1420
1559
  this.structuredOutputResult = { data: object, rawText: parsed.raw }
1421
1560
  this.combinedCompleteEmitted = true
1422
1561
  const value = chunk.value
1562
+ const completeMessageId = readCustomEventMessageId(value)
1563
+ if (completeMessageId) {
1564
+ this.combinedStructuredMessageId = completeMessageId
1565
+ this.captureStructuredOutputMessageIdentity(completeMessageId)
1566
+ }
1423
1567
  if (object !== parsed.object && value && typeof value === 'object') {
1424
1568
  outboundChunk = { ...chunk, value: { ...value, object } }
1425
1569
  }
@@ -1482,10 +1626,17 @@ class TextEngine<
1482
1626
  ) {
1483
1627
  continue
1484
1628
  }
1629
+ if (outputChunk.type === EventType.RUN_FINISHED) {
1630
+ this.deferredModelRunFinishedChunks.push(outputChunk)
1631
+ continue
1632
+ }
1485
1633
  if (this.shouldDeferToolCallRunFinished(outputChunk)) {
1486
1634
  this.deferredToolCallRunFinishedChunks.push(outputChunk)
1487
1635
  continue
1488
1636
  }
1637
+ if (outputChunk.type === EventType.RUN_STARTED) {
1638
+ this.hasPublicRunStarted = true
1639
+ }
1489
1640
  this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk })
1490
1641
  yield outputChunk
1491
1642
  this.middlewareCtx.chunkIndex++
@@ -1586,6 +1737,11 @@ class TextEngine<
1586
1737
  }
1587
1738
  }
1588
1739
 
1740
+ private captureStructuredOutputMessageIdentity(messageId: string): void {
1741
+ this.structuredOutputMessageId = messageId
1742
+ this.structuredOutputMessageCreatedAt ??= new Date()
1743
+ }
1744
+
1589
1745
  private handleToolCallStartEvent(chunk: ToolCallStartEvent): void {
1590
1746
  if (
1591
1747
  typeof chunk.parentMessageId === 'string' &&
@@ -1763,6 +1919,18 @@ class TextEngine<
1763
1919
  return 'continue'
1764
1920
  }
1765
1921
 
1922
+ this.middlewareCtx.phase = 'beforeTools'
1923
+ if (
1924
+ yield* this.emitBoundaryInterrupts(
1925
+ 'beforeTools',
1926
+ finishEvent,
1927
+ executablePendingCalls,
1928
+ )
1929
+ ) {
1930
+ this.setToolPhase('wait')
1931
+ return 'wait'
1932
+ }
1933
+
1766
1934
  const { approvals, clientToolResults } = this.collectClientState()
1767
1935
 
1768
1936
  const generator = executeToolCalls(
@@ -1930,6 +2098,17 @@ class TextEngine<
1930
2098
  }
1931
2099
  this.middlewareCtx.phase = 'beforeTools'
1932
2100
 
2101
+ if (
2102
+ yield* this.emitBoundaryInterrupts(
2103
+ 'beforeTools',
2104
+ finishEvent,
2105
+ executableToolCalls,
2106
+ )
2107
+ ) {
2108
+ this.setToolPhase('wait')
2109
+ return
2110
+ }
2111
+
1933
2112
  const { approvals, clientToolResults } = this.collectClientState()
1934
2113
 
1935
2114
  const generator = executeToolCalls(
@@ -1997,15 +2176,36 @@ class TextEngine<
1997
2176
  needsClientExecution: executionResult.needsClientExecution,
1998
2177
  })
1999
2178
 
2179
+ const afterToolBoundaryChunks = this.buildToolResultChunks(
2180
+ allResults,
2181
+ finishEvent,
2182
+ )
2183
+ const afterToolRequests =
2184
+ await this.middlewareRunner.runOnInterruptBoundary(
2185
+ this.middlewareCtx as ChatMiddlewareContext<TContext> & {
2186
+ phase: 'afterTools'
2187
+ },
2188
+ )
2189
+ if (afterToolRequests.length > 0) {
2190
+ for (const chunk of afterToolBoundaryChunks) {
2191
+ yield* this.pipeThroughMiddleware(chunk)
2192
+ }
2193
+ yield* this.emitBoundaryInterrupts(
2194
+ 'afterTools',
2195
+ finishEvent,
2196
+ toolCalls,
2197
+ afterToolRequests,
2198
+ )
2199
+ this.setToolPhase('wait')
2200
+ return
2201
+ }
2202
+
2000
2203
  if (
2001
2204
  executionResult.needsApproval.length > 0 ||
2002
2205
  executionResult.needsClientExecution.length > 0
2003
2206
  ) {
2004
2207
  if (allResults.length > 0) {
2005
- for (const chunk of this.buildToolResultChunks(
2006
- allResults,
2007
- finishEvent,
2008
- )) {
2208
+ for (const chunk of afterToolBoundaryChunks) {
2009
2209
  yield* this.pipeThroughMiddleware(chunk)
2010
2210
  }
2011
2211
  }
@@ -2021,7 +2221,7 @@ class TextEngine<
2021
2221
 
2022
2222
  yield* this.flushDeferredToolCallRunFinishedChunks()
2023
2223
 
2024
- const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent)
2224
+ const toolResultChunks = afterToolBoundaryChunks
2025
2225
 
2026
2226
  for (const chunk of toolResultChunks) {
2027
2227
  yield* this.pipeThroughMiddleware(chunk)
@@ -2061,6 +2261,48 @@ class TextEngine<
2061
2261
  this.deferredToolCallRunFinishedChunks = []
2062
2262
  }
2063
2263
 
2264
+ private *flushDeferredModelRunFinishedChunks(): Generator<StreamChunk> {
2265
+ for (const chunk of this.deferredModelRunFinishedChunks) {
2266
+ this.logger.output(`type=${chunk.type}`, { chunk })
2267
+ yield chunk
2268
+ this.middlewareCtx.chunkIndex++
2269
+ }
2270
+ this.deferredModelRunFinishedChunks = []
2271
+ }
2272
+
2273
+ private async *emitSyntheticRunStarted(
2274
+ finishEvent: RunFinishedEvent,
2275
+ ): AsyncGenerator<StreamChunk, void, void> {
2276
+ if (this.hasPublicRunStarted) return
2277
+ yield* this.pipeThroughMiddleware({
2278
+ type: EventType.RUN_STARTED,
2279
+ runId: finishEvent.runId,
2280
+ threadId: finishEvent.threadId,
2281
+ model: finishEvent.model,
2282
+ timestamp: Date.now(),
2283
+ })
2284
+ }
2285
+
2286
+ private async *emitSuccessfulEarlyTermination(): AsyncGenerator<
2287
+ StreamChunk,
2288
+ void,
2289
+ void
2290
+ > {
2291
+ // `stop` is a finished run, not another tool cycle. `tool_calls` here
2292
+ // makes the client auto-send after afterTools, so reject looks stuck.
2293
+ this.lastFinishReason = 'stop'
2294
+ const finishEvent = {
2295
+ ...this.createSyntheticFinishedEvent(),
2296
+ finishReason: 'stop' as const,
2297
+ }
2298
+ yield* this.emitSyntheticRunStarted(finishEvent)
2299
+ yield* this.pipeThroughMiddleware({
2300
+ ...finishEvent,
2301
+ timestamp: Date.now(),
2302
+ outcome: { type: 'success' },
2303
+ })
2304
+ }
2305
+
2064
2306
  private discardDeferredToolCallRunFinishedChunks(): void {
2065
2307
  this.deferredToolCallRunFinishedChunks = []
2066
2308
  }
@@ -2092,27 +2334,104 @@ class TextEngine<
2092
2334
  this.middlewareCtx.messages = this.messages
2093
2335
  }
2094
2336
 
2095
- private addTerminalReasoningMessage(): void {
2337
+ private addTerminalAssistantMessages(): void {
2096
2338
  this.finalizeCurrentThinkingStep()
2097
- if (this.accumulatedThinking.length === 0) return
2098
2339
 
2099
- const messages = this.middlewareCtx.messages
2100
- const alreadyPresent = messages.some(
2340
+ const structuredResult = this.structuredOutputResult
2341
+ const raw = structuredResult
2342
+ ? structuredResult.rawText || safeJsonStringify(structuredResult.data)
2343
+ : ''
2344
+ const structuredOutput: StructuredOutputPart | undefined = structuredResult
2345
+ ? {
2346
+ type: 'structured-output',
2347
+ status: 'complete',
2348
+ data: structuredResult.data,
2349
+ partial: structuredResult.data,
2350
+ raw,
2351
+ ...(structuredResult.reasoning !== undefined
2352
+ ? { reasoning: structuredResult.reasoning }
2353
+ : {}),
2354
+ }
2355
+ : undefined
2356
+ const nativeCombined = this.finalStructuredOutput?.nativeCombined === true
2357
+ const eventSourced = this.finalStructuredOutput?.source === 'event'
2358
+ const structuredId =
2359
+ this.structuredOutputMessageId ??
2360
+ this.combinedStructuredMessageId ??
2361
+ this.currentMessageId ??
2362
+ this.createId('msg')
2363
+ // Codex, OpenCode, ACP, and grok-build reuse the last text messageId
2364
+ // on structured-output.complete. Split only when the event uses a
2365
+ // different id (Claude Code). Same-id output stays on one message.
2366
+ const splitStructuredMessage =
2367
+ Boolean(structuredOutput) &&
2368
+ (!nativeCombined || eventSourced) &&
2369
+ this.currentMessageId != null &&
2370
+ structuredId !== this.currentMessageId
2371
+ const messages = [...this.middlewareCtx.messages]
2372
+ const existingStructuredIndex = messages.findIndex(
2373
+ (message) => message.role === 'assistant' && message.id === structuredId,
2374
+ )
2375
+ const currentTurnAlreadyRecorded = messages.some(
2101
2376
  (message) =>
2102
2377
  message.role === 'assistant' && message.id === this.currentMessageId,
2103
2378
  )
2104
- if (alreadyPresent) return
2379
+ const thinking =
2380
+ this.accumulatedThinking.length > 0 ? this.accumulatedThinking : undefined
2381
+ const startedLength = messages.length
2382
+
2383
+ if (structuredOutput && existingStructuredIndex >= 0) {
2384
+ const existing = messages[existingStructuredIndex]
2385
+ if (existing) {
2386
+ messages[existingStructuredIndex] = {
2387
+ ...existing,
2388
+ content: raw || existing.content,
2389
+ structuredOutput,
2390
+ }
2391
+ }
2392
+ } else if (structuredOutput && !splitStructuredMessage) {
2393
+ if (!currentTurnAlreadyRecorded) {
2394
+ messages.push({
2395
+ role: 'assistant',
2396
+ content: this.accumulatedContent || raw || null,
2397
+ id: structuredId,
2398
+ createdAt:
2399
+ this.currentMessageCreatedAt ??
2400
+ this.structuredOutputMessageCreatedAt ??
2401
+ new Date(),
2402
+ structuredOutput,
2403
+ ...(thinking ? { thinking } : {}),
2404
+ })
2405
+ }
2406
+ } else {
2407
+ if (
2408
+ !currentTurnAlreadyRecorded &&
2409
+ (this.accumulatedContent !== '' || thinking)
2410
+ ) {
2411
+ messages.push({
2412
+ role: 'assistant',
2413
+ content: this.accumulatedContent || null,
2414
+ id: this.currentMessageId ?? this.createId('msg'),
2415
+ createdAt: this.currentMessageCreatedAt ?? new Date(),
2416
+ ...(thinking ? { thinking } : {}),
2417
+ })
2418
+ }
2419
+ if (structuredOutput) {
2420
+ messages.push({
2421
+ role: 'assistant',
2422
+ content: raw || null,
2423
+ id: structuredId,
2424
+ createdAt: this.structuredOutputMessageCreatedAt ?? new Date(),
2425
+ structuredOutput,
2426
+ })
2427
+ }
2428
+ }
2105
2429
 
2106
- this.messages = [
2107
- ...messages,
2108
- {
2109
- role: 'assistant',
2110
- content: this.accumulatedContent || null,
2111
- id: this.currentMessageId ?? undefined,
2112
- createdAt: this.currentMessageCreatedAt ?? undefined,
2113
- thinking: this.accumulatedThinking,
2114
- },
2115
- ]
2430
+ if (messages.length === startedLength && existingStructuredIndex < 0) {
2431
+ return
2432
+ }
2433
+
2434
+ this.messages = messages
2116
2435
  this.middlewareCtx.messages = this.messages
2117
2436
  }
2118
2437
 
@@ -2206,9 +2525,17 @@ class TextEngine<
2206
2525
  return { approvals, clientToolResults }
2207
2526
  }
2208
2527
 
2528
+ private genericInterruptId(): string {
2529
+ return this.createId('interrupt')
2530
+ }
2531
+
2209
2532
  private buildActionableInterrupts(
2210
2533
  approvals: Array<ApprovalRequest>,
2211
2534
  clientRequests: Array<ClientToolRequest>,
2535
+ genericRequests: ReadonlyArray<
2536
+ GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2537
+ > = [],
2538
+ genericInterruptIds: ReadonlyArray<string> = [],
2212
2539
  ): Array<Interrupt> {
2213
2540
  const interrupts: Array<Interrupt> = []
2214
2541
 
@@ -2278,6 +2605,64 @@ class TextEngine<
2278
2605
  })
2279
2606
  }
2280
2607
 
2608
+ for (const [index, request] of genericRequests.entries()) {
2609
+ const batchIndex = interrupts.length
2610
+ const id = genericInterruptIds[index]
2611
+ if (!id) throw new Error('Generic interrupt id is unavailable.')
2612
+ const preEmission = createInterruptBinding(request, { batchIndex })
2613
+ interrupts.push({
2614
+ id,
2615
+ reason: request.reason,
2616
+ message: request.message,
2617
+ ...(preEmission.descriptor.responseSchemaCanonicalJson !== undefined
2618
+ ? {
2619
+ responseSchema: JSON.parse(
2620
+ preEmission.descriptor.responseSchemaCanonicalJson,
2621
+ ),
2622
+ }
2623
+ : {}),
2624
+ ...(request.expiresAt !== undefined
2625
+ ? { expiresAt: request.expiresAt }
2626
+ : {}),
2627
+ metadata: {
2628
+ [interruptBindingMetadataKey]: {
2629
+ v: INTERRUPT_BINDING_VERSION,
2630
+ kind: 'generic',
2631
+ interruptId: id,
2632
+ definitionId: preEmission.descriptor.definitionId,
2633
+ key: preEmission.descriptor.key,
2634
+ batchIndex,
2635
+ ...(request.expiresAt !== undefined
2636
+ ? { expiresAt: request.expiresAt }
2637
+ : {}),
2638
+ ...(preEmission.descriptor.payloadSchemaHash
2639
+ ? {
2640
+ payloadSchemaHash: preEmission.descriptor.payloadSchemaHash,
2641
+ }
2642
+ : {}),
2643
+ ...(preEmission.descriptor.responseSchemaHash !== undefined
2644
+ ? {
2645
+ responseSchemaHash: preEmission.descriptor.responseSchemaHash,
2646
+ }
2647
+ : {}),
2648
+ },
2649
+ ...(preEmission.payload !== undefined
2650
+ ? { [INTERRUPT_PAYLOAD_METADATA_KEY]: preEmission.payload }
2651
+ : {}),
2652
+ },
2653
+ })
2654
+ }
2655
+
2656
+ const ids = new Set<string>()
2657
+ for (const interrupt of interrupts) {
2658
+ if (ids.has(interrupt.id)) {
2659
+ throw new Error(
2660
+ `Duplicate interrupt id in final batch: ${interrupt.id}`,
2661
+ )
2662
+ }
2663
+ ids.add(interrupt.id)
2664
+ }
2665
+
2281
2666
  return interrupts
2282
2667
  }
2283
2668
 
@@ -2285,13 +2670,22 @@ class TextEngine<
2285
2670
  finishEvent: RunFinishedEvent,
2286
2671
  approvals: Array<ApprovalRequest>,
2287
2672
  clientRequests: Array<ClientToolRequest>,
2673
+ genericRequests: ReadonlyArray<
2674
+ GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2675
+ > = [],
2676
+ genericInterruptIds?: ReadonlyArray<string>,
2288
2677
  ): StreamChunk {
2289
2678
  return {
2290
2679
  ...finishEvent,
2291
2680
  timestamp: Date.now(),
2292
2681
  outcome: {
2293
2682
  type: 'interrupt',
2294
- interrupts: this.buildActionableInterrupts(approvals, clientRequests),
2683
+ interrupts: this.buildActionableInterrupts(
2684
+ approvals,
2685
+ clientRequests,
2686
+ genericRequests,
2687
+ genericInterruptIds,
2688
+ ),
2295
2689
  },
2296
2690
  }
2297
2691
  }
@@ -2309,7 +2703,8 @@ class TextEngine<
2309
2703
  message.id ||
2310
2704
  `snapshot_${this.runIdOverride ?? this.requestId}_${index}`
2311
2705
  const parts =
2312
- message.role === 'assistant' && message.thinking?.length
2706
+ message.role === 'assistant' &&
2707
+ (message.thinking?.length || message.structuredOutput)
2313
2708
  ? modelMessageToUIMessage(message, id).parts
2314
2709
  : undefined
2315
2710
  return {
@@ -2440,9 +2835,22 @@ class TextEngine<
2440
2835
  finishEvent: RunFinishedEvent,
2441
2836
  approvals: Array<ApprovalRequest>,
2442
2837
  clientRequests: Array<ClientToolRequest>,
2838
+ genericRequests: ReadonlyArray<
2839
+ GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2840
+ > = [],
2443
2841
  ): AsyncGenerator<StreamChunk, boolean, void> {
2842
+ yield* this.emitSyntheticRunStarted(finishEvent)
2843
+ const genericInterruptIds = genericRequests.map(() =>
2844
+ this.genericInterruptId(),
2845
+ )
2444
2846
  const terminal = this.completeEphemeralInterruptBindings(
2445
- this.buildInterruptFinishedChunk(finishEvent, approvals, clientRequests),
2847
+ this.buildInterruptFinishedChunk(
2848
+ finishEvent,
2849
+ approvals,
2850
+ clientRequests,
2851
+ genericRequests,
2852
+ genericInterruptIds,
2853
+ ),
2446
2854
  )
2447
2855
  let terminalOutputs: Array<StreamChunk>
2448
2856
  try {
@@ -2471,6 +2879,103 @@ class TextEngine<
2471
2879
  return true
2472
2880
  }
2473
2881
 
2882
+ private async *emitBoundaryInterrupts(
2883
+ phase: 'beforeModel' | 'afterModel' | 'beforeTools' | 'afterTools',
2884
+ finishEvent: RunFinishedEvent,
2885
+ toolCalls: ReadonlyArray<ToolCall> = [],
2886
+ requests?: ReadonlyArray<
2887
+ GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2888
+ >,
2889
+ ): AsyncGenerator<StreamChunk, boolean, void> {
2890
+ this.middlewareCtx.phase = phase
2891
+ const boundaryRequests =
2892
+ requests ??
2893
+ (await this.middlewareRunner.runOnInterruptBoundary(
2894
+ this.middlewareCtx as ChatMiddlewareContext<TContext> & {
2895
+ phase: typeof phase
2896
+ },
2897
+ ))
2898
+ if (boundaryRequests.length === 0) return false
2899
+ for (const request of boundaryRequests) {
2900
+ if (
2901
+ this.interruptDefinitions.get(request.definition.id) !==
2902
+ request.definition
2903
+ ) {
2904
+ throw new Error(
2905
+ `Generic interrupt definition ${request.definition.id} is not registered on this chat.`,
2906
+ )
2907
+ }
2908
+ }
2909
+ if (phase === 'afterModel') {
2910
+ if (this.toolCallManager.hasToolCalls()) {
2911
+ this.addAssistantToolCallMessage(this.toolCallManager.getToolCalls())
2912
+ } else {
2913
+ this.addAssistantTextMessageForInterrupt()
2914
+ }
2915
+ }
2916
+ const actionable = this.getBoundaryActionableToolRequests(toolCalls)
2917
+ yield* this.emitActionableInterruptBoundary(
2918
+ finishEvent,
2919
+ actionable.approvals,
2920
+ actionable.clientRequests,
2921
+ boundaryRequests,
2922
+ )
2923
+ return true
2924
+ }
2925
+
2926
+ private addAssistantTextMessageForInterrupt(): void {
2927
+ if (this.accumulatedContent.length === 0) return
2928
+ this.messages = [
2929
+ ...this.messages,
2930
+ { role: 'assistant', content: this.accumulatedContent },
2931
+ ]
2932
+ this.middlewareCtx.messages = this.messages
2933
+ }
2934
+
2935
+ private getBoundaryActionableToolRequests(
2936
+ toolCalls: ReadonlyArray<ToolCall>,
2937
+ ): {
2938
+ approvals: Array<ApprovalRequest>
2939
+ clientRequests: Array<ClientToolRequest>
2940
+ } {
2941
+ const { approvals, clientToolResults } = this.collectClientState()
2942
+ const approvalRequests: Array<ApprovalRequest> = []
2943
+ const clientRequests: Array<ClientToolRequest> = []
2944
+ for (const toolCall of toolCalls) {
2945
+ const tool = this.resolveExecutableTools([toolCall]).find(
2946
+ (candidate) => candidate.name === toolCall.function.name,
2947
+ ) as RuntimeToolWithApproval | undefined
2948
+ if (!tool) continue
2949
+ let input: unknown = {}
2950
+ try {
2951
+ const parsed = JSON.parse(toolCall.function.arguments.trim() || '{}')
2952
+ input = parsed && typeof parsed === 'object' ? parsed : {}
2953
+ } catch {
2954
+ input = {}
2955
+ }
2956
+ const approvalId = `approval_${toolCall.id}`
2957
+ if (tool.needsApproval && !approvals.has(approvalId)) {
2958
+ approvalRequests.push({
2959
+ toolCallId: toolCall.id,
2960
+ toolName: toolCall.function.name,
2961
+ input,
2962
+ approvalId,
2963
+ })
2964
+ } else if (
2965
+ !tool.execute &&
2966
+ !clientToolResults.has(toolCall.id) &&
2967
+ !this.resumeCancelledToolCallIds.has(toolCall.id)
2968
+ ) {
2969
+ clientRequests.push({
2970
+ toolCallId: toolCall.id,
2971
+ toolName: toolCall.function.name,
2972
+ input,
2973
+ })
2974
+ }
2975
+ }
2976
+ return { approvals: approvalRequests, clientRequests }
2977
+ }
2978
+
2474
2979
  private completeEphemeralInterruptBindings(chunk: StreamChunk): StreamChunk {
2475
2980
  if (
2476
2981
  chunk.type !== EventType.RUN_FINISHED ||
@@ -2718,7 +3223,7 @@ class TextEngine<
2718
3223
  private createSyntheticFinishedEvent(): RunFinishedEvent {
2719
3224
  return {
2720
3225
  type: 'RUN_FINISHED',
2721
- runId: this.createId('pending'),
3226
+ runId: this.runIdOverride ?? this.requestId,
2722
3227
  threadId: this.threadId,
2723
3228
  model: this.params.model,
2724
3229
  timestamp: Date.now(),
@@ -2997,6 +3502,7 @@ class TextEngine<
2997
3502
  const buildSynthesizedStart = (timestamp = Date.now()): StreamChunk => {
2998
3503
  const idForStart = structuredMessageId ?? generateMessageId()
2999
3504
  structuredMessageId = idForStart
3505
+ this.captureStructuredOutputMessageIdentity(idForStart)
3000
3506
  return {
3001
3507
  type: EventType.CUSTOM,
3002
3508
  name: 'structured-output.start',
@@ -3037,7 +3543,10 @@ class TextEngine<
3037
3543
  // synthesized start (when needed) uses the SAME id the deltas carry
3038
3544
  if (!structuredMessageId) {
3039
3545
  const extracted = extractMessageId(chunk)
3040
- if (extracted) structuredMessageId = extracted
3546
+ if (extracted) {
3547
+ structuredMessageId = extracted
3548
+ this.captureStructuredOutputMessageIdentity(extracted)
3549
+ }
3041
3550
  }
3042
3551
 
3043
3552
  // Synthesis only matters for the streaming client path — the agentic
@@ -3104,7 +3613,13 @@ class TextEngine<
3104
3613
  const object = this.finalStructuredOutput.normalize
3105
3614
  ? this.finalStructuredOutput.normalize(parsed.object)
3106
3615
  : parsed.object
3107
- this.structuredOutputResult = { data: object, rawText: parsed.raw }
3616
+ this.structuredOutputResult = {
3617
+ data: object,
3618
+ rawText: parsed.raw,
3619
+ ...(parsed.reasoning !== undefined
3620
+ ? { reasoning: parsed.reasoning }
3621
+ : {}),
3622
+ }
3108
3623
  // Rewrite the outbound event so the yielded chunk carries the
3109
3624
  // normalized object (the original `chunk.value` still holds the
3110
3625
  // widened one). Preserve every other field — `raw`, `reasoning` —
@@ -3555,7 +4070,15 @@ class TextEngine<
3555
4070
  }
3556
4071
  }
3557
4072
 
3558
- const pending = this.buildActionableInterrupts(
4073
+ const genericPending = this.getGenericContinuationPending(interruptedRunId)
4074
+ const pending: Array<{
4075
+ interruptId: string
4076
+ payload: unknown
4077
+ binding: InterruptBinding
4078
+ genericRequest?: GenericInterruptRequest<
4079
+ InterruptDefinition<any, any, any, any>
4080
+ >
4081
+ }> = this.buildActionableInterrupts(
3559
4082
  approvalRequests,
3560
4083
  clientRequests,
3561
4084
  ).flatMap((descriptor) => {
@@ -3574,6 +4097,7 @@ class TextEngine<
3574
4097
  ]
3575
4098
  : []
3576
4099
  })
4100
+ pending.push(...genericPending)
3577
4101
  const validated = await validateInterruptResumeBatch({
3578
4102
  threadId: this.threadId,
3579
4103
  interruptedRunId,
@@ -3601,6 +4125,185 @@ class TextEngine<
3601
4125
  ...validated.resumeToolState,
3602
4126
  approvals,
3603
4127
  })
4128
+
4129
+ const genericResolutions = validated.resumeToolState.genericInterrupts
4130
+ if (genericPending.length > 0 && genericResolutions) {
4131
+ const resolutions = genericPending
4132
+ .sort((left, right) => {
4133
+ const leftIndex =
4134
+ left.binding.kind === 'generic' ? (left.binding.batchIndex ?? 0) : 0
4135
+ const rightIndex =
4136
+ right.binding.kind === 'generic'
4137
+ ? (right.binding.batchIndex ?? 0)
4138
+ : 0
4139
+ return leftIndex - rightIndex
4140
+ })
4141
+ .flatMap((record) => {
4142
+ const resolution = genericResolutions.get(record.interruptId)
4143
+ if (!resolution || !record.genericRequest) return []
4144
+ return [
4145
+ resolution.status === 'resolved'
4146
+ ? {
4147
+ request: record.genericRequest,
4148
+ status: 'resolved' as const,
4149
+ response: resolution.payload,
4150
+ }
4151
+ : {
4152
+ request: record.genericRequest,
4153
+ status: 'cancelled' as const,
4154
+ },
4155
+ ]
4156
+ })
4157
+ const collection: InterruptResolutionCollection = {
4158
+ for: (definition) =>
4159
+ resolutions.filter(
4160
+ (resolution) => resolution.request.definition === definition,
4161
+ ) as never,
4162
+ all: (
4163
+ ...definitions: Array<InterruptDefinition<any, any, any, any>>
4164
+ ) =>
4165
+ definitions.length === 0
4166
+ ? resolutions
4167
+ : resolutions.filter((resolution) =>
4168
+ definitions.includes(resolution.request.definition),
4169
+ ),
4170
+ }
4171
+ const policy = await this.middlewareRunner.runOnInterruptResolution(
4172
+ this.middlewareCtx,
4173
+ collection,
4174
+ )
4175
+ if (policy.toolResume === 'stop') {
4176
+ this.earlyTermination = true
4177
+ } else if (policy.toolResume === 'cancel') {
4178
+ for (const request of pendingToolCalls) {
4179
+ this.resumeCancelledToolCallIds.add(request.id)
4180
+ }
4181
+ }
4182
+ }
4183
+ }
4184
+
4185
+ private getGenericContinuationPending(interruptedRunId: string): Array<{
4186
+ interruptId: string
4187
+ payload: unknown
4188
+ binding: InterruptBinding
4189
+ genericRequest: GenericInterruptRequest<
4190
+ InterruptDefinition<any, any, any, any>
4191
+ >
4192
+ }> {
4193
+ const fail = (message: string): never => {
4194
+ throw new InterruptResumeValidationError([
4195
+ {
4196
+ scope: 'batch',
4197
+ threadId: this.threadId,
4198
+ interruptedRunId,
4199
+ generation: 0,
4200
+ interruptIds: [],
4201
+ code: 'stale',
4202
+ message,
4203
+ source: 'server',
4204
+ retryable: false,
4205
+ },
4206
+ ])
4207
+ }
4208
+ const pending: Array<{
4209
+ interruptId: string
4210
+ payload: unknown
4211
+ binding: InterruptBinding
4212
+ genericRequest: GenericInterruptRequest<
4213
+ InterruptDefinition<any, any, any, any>
4214
+ >
4215
+ }> = []
4216
+ const ids = new Set<string>()
4217
+ const batchIndexes = new Set<number>()
4218
+ for (const resumeItem of this.params.resume ?? []) {
4219
+ const parsed = readGenericInterruptContinuation(resumeItem.metadata)
4220
+ if (parsed.status === 'absent') continue
4221
+ if (parsed.status === 'invalid') {
4222
+ return fail(parsed.message)
4223
+ }
4224
+ const entry = parsed.value
4225
+ const id = resumeItem.interruptId
4226
+ const definition = this.interruptDefinitions.get(entry.definitionId)
4227
+ if (!definition) {
4228
+ return fail(
4229
+ `Generic interrupt definition ${entry.definitionId} is unavailable.`,
4230
+ )
4231
+ }
4232
+ if (ids.has(id) || batchIndexes.has(entry.batchIndex)) {
4233
+ return fail(
4234
+ 'Generic interrupt continuation contains duplicate entries.',
4235
+ )
4236
+ }
4237
+ ids.add(id)
4238
+ batchIndexes.add(entry.batchIndex)
4239
+ let request: GenericInterruptRequest<
4240
+ InterruptDefinition<any, any, any, any>
4241
+ >
4242
+ try {
4243
+ request = rehydrateInterruptRequest(definition, {
4244
+ key: entry.key,
4245
+ reason: entry.reason,
4246
+ message: entry.message,
4247
+ ...(typeof entry.expiresAt === 'string'
4248
+ ? { expiresAt: entry.expiresAt }
4249
+ : {}),
4250
+ ...(Object.prototype.hasOwnProperty.call(entry, 'payload')
4251
+ ? { payload: entry.payload }
4252
+ : {}),
4253
+ })
4254
+ } catch (error) {
4255
+ return fail(
4256
+ `Generic interrupt continuation ${id} is invalid: ${
4257
+ error instanceof Error ? error.message : String(error)
4258
+ }`,
4259
+ )
4260
+ }
4261
+ const emitted = createInterruptBinding(request, {
4262
+ batchIndex: entry.batchIndex,
4263
+ })
4264
+ if (
4265
+ entry.responseSchemaHash !== emitted.descriptor.responseSchemaHash ||
4266
+ entry.payloadSchemaHash !== emitted.descriptor.payloadSchemaHash
4267
+ ) {
4268
+ return fail(
4269
+ `Generic interrupt continuation ${id} does not match its definition.`,
4270
+ )
4271
+ }
4272
+ pending.push({
4273
+ interruptId: id,
4274
+ payload: {
4275
+ id,
4276
+ ...(emitted.descriptor.responseSchemaCanonicalJson !== undefined
4277
+ ? {
4278
+ responseSchema: JSON.parse(
4279
+ emitted.descriptor.responseSchemaCanonicalJson,
4280
+ ),
4281
+ }
4282
+ : {}),
4283
+ },
4284
+ binding: {
4285
+ v: INTERRUPT_BINDING_VERSION,
4286
+ kind: 'generic',
4287
+ interruptId: id,
4288
+ interruptedRunId,
4289
+ generation: 0,
4290
+ definitionId: entry.definitionId,
4291
+ key: entry.key,
4292
+ batchIndex: entry.batchIndex,
4293
+ ...(typeof entry.expiresAt === 'string'
4294
+ ? { expiresAt: entry.expiresAt }
4295
+ : {}),
4296
+ ...(emitted.descriptor.payloadSchemaHash
4297
+ ? { payloadSchemaHash: emitted.descriptor.payloadSchemaHash }
4298
+ : {}),
4299
+ ...(entry.responseSchemaHash !== undefined
4300
+ ? { responseSchemaHash: entry.responseSchemaHash }
4301
+ : {}),
4302
+ },
4303
+ genericRequest: request,
4304
+ })
4305
+ }
4306
+ return pending
3604
4307
  }
3605
4308
 
3606
4309
  private applyResumeToolState(state: ChatResumeToolState | undefined): void {
@@ -3624,6 +4327,58 @@ class TextEngine<
3624
4327
  this.resumeCancelledToolCallIds.add(toolCallId)
3625
4328
  }
3626
4329
  }
4330
+ if (state?.genericInterrupts) {
4331
+ for (const [interruptId, resolution] of state.genericInterrupts) {
4332
+ this.resumeGenericInterrupts.set(interruptId, resolution)
4333
+ }
4334
+ }
4335
+ if (state?.genericInterruptRequests) {
4336
+ for (const [interruptId, request] of state.genericInterruptRequests) {
4337
+ this.resumeGenericInterruptRequests.set(interruptId, request)
4338
+ }
4339
+ }
4340
+ }
4341
+
4342
+ private async applyDurableGenericInterruptResolution(): Promise<void> {
4343
+ if (this.resumeGenericInterruptRequests.size === 0) return
4344
+ const resolutions = [
4345
+ ...this.resumeGenericInterruptRequests.entries(),
4346
+ ].flatMap(([interruptId, request]) => {
4347
+ const resolution = this.resumeGenericInterrupts.get(interruptId)
4348
+ if (!resolution) return []
4349
+ return [
4350
+ resolution.status === 'resolved'
4351
+ ? {
4352
+ request,
4353
+ status: 'resolved' as const,
4354
+ response: resolution.payload,
4355
+ }
4356
+ : { request, status: 'cancelled' as const },
4357
+ ]
4358
+ })
4359
+ const collection: InterruptResolutionCollection = {
4360
+ for: (definition) =>
4361
+ resolutions.filter(
4362
+ (resolution) => resolution.request.definition === definition,
4363
+ ) as never,
4364
+ all: (...definitions: Array<InterruptDefinition<any, any, any, any>>) =>
4365
+ definitions.length === 0
4366
+ ? resolutions
4367
+ : resolutions.filter((resolution) =>
4368
+ definitions.includes(resolution.request.definition),
4369
+ ),
4370
+ }
4371
+ const policy = await this.middlewareRunner.runOnInterruptResolution(
4372
+ this.middlewareCtx,
4373
+ collection,
4374
+ )
4375
+ if (policy.toolResume === 'stop') {
4376
+ this.earlyTermination = true
4377
+ } else if (policy.toolResume === 'cancel') {
4378
+ for (const toolCall of this.getPendingToolCallsFromMessages()) {
4379
+ this.resumeCancelledToolCallIds.add(toolCall.id)
4380
+ }
4381
+ }
3627
4382
  }
3628
4383
 
3629
4384
  private applyMiddlewareConfig(config: ChatMiddlewareConfig): void {
@@ -3662,6 +4417,9 @@ class TextEngine<
3662
4417
  chunk,
3663
4418
  )
3664
4419
  for (const outputChunk of outputChunks) {
4420
+ if (outputChunk.type === EventType.RUN_STARTED) {
4421
+ this.hasPublicRunStarted = true
4422
+ }
3665
4423
  yield outputChunk
3666
4424
  this.middlewareCtx.chunkIndex++
3667
4425
  }
@@ -3802,67 +4560,136 @@ export function chat<
3802
4560
  TStream,
3803
4561
  any
3804
4562
  >['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
3805
- const TMiddleware extends TextActivityOptions<
3806
- TAdapter,
3807
- TSchema,
3808
- TStream,
3809
- any
3810
- >['middleware'] = TextActivityOptions<
3811
- TAdapter,
3812
- TSchema,
3813
- TStream,
3814
- any
3815
- >['middleware'],
4563
+ const TInterrupts extends ReadonlyArray<
4564
+ InterruptDefinition<any, any, any, any>
4565
+ > = [],
4566
+ TContext = unknown,
4567
+ const TMiddleware extends Array<unknown> | undefined = undefined,
3816
4568
  >(
3817
4569
  options: TextActivityOptionsWithContext<
3818
4570
  TAdapter,
3819
4571
  TSchema,
3820
4572
  TStream,
3821
4573
  TTools,
4574
+ TInterrupts,
4575
+ TContext,
3822
4576
  TMiddleware
3823
4577
  >,
3824
4578
  ): TextActivityResult<TSchema, TStream, TTools> {
3825
- validateCapabilities(options.middleware ?? [], options.adapter)
4579
+ validateInterruptDefinitions(options.interrupts)
4580
+ validateCapabilities(
4581
+ readRuntimeMiddleware(options.middleware) ?? [],
4582
+ options.adapter,
4583
+ )
3826
4584
  if (options.tools) {
3827
4585
  assertUniqueToolNames(options.tools)
3828
4586
  }
3829
4587
 
3830
4588
  const { outputSchema, stream } = options
3831
4589
 
3832
- // outputSchema + stream:true is the only branch that streams structured
3833
- // output. Without an explicit `stream: true`, schema-bearing calls run the
3834
- // agent loop and resolve to a typed Promise<InferSchemaType<TSchema>>.
3835
4590
  if (outputSchema && stream === true) {
3836
- return runStreamingStructuredOutput({
3837
- ...options,
3838
- outputSchema,
3839
- stream,
3840
- }) as TextActivityResult<TSchema, TStream, TTools>
4591
+ return runStreamingStructuredOutput(
4592
+ toRuntimeTextActivityOptions(options, {
4593
+ outputSchema,
4594
+ stream: true,
4595
+ }),
4596
+ ) as TextActivityResult<TSchema, TStream, TTools>
3841
4597
  }
3842
4598
 
3843
- // If outputSchema is provided, run agentic structured output (Promise<T>)
3844
4599
  if (outputSchema) {
3845
- return runAgenticStructuredOutput({
3846
- ...options,
3847
- outputSchema,
3848
- }) as TextActivityResult<TSchema, TStream, TTools>
4600
+ return runAgenticStructuredOutput(
4601
+ toRuntimeTextActivityOptions(options, {
4602
+ outputSchema,
4603
+ stream: false,
4604
+ }),
4605
+ ) as TextActivityResult<TSchema, TStream, TTools>
3849
4606
  }
3850
4607
 
3851
- // If stream is explicitly false, run non-streaming text
3852
4608
  if (stream === false) {
3853
- return runNonStreamingText({
3854
- ...options,
4609
+ return runNonStreamingText(
4610
+ toRuntimeTextActivityOptions(options, {
4611
+ outputSchema: undefined,
4612
+ stream: false,
4613
+ }),
4614
+ ) as TextActivityResult<TSchema, TStream, TTools>
4615
+ }
4616
+
4617
+ return runStreamingText(
4618
+ toRuntimeTextActivityOptions(options, {
3855
4619
  outputSchema: undefined,
3856
- stream,
3857
- }) as TextActivityResult<TSchema, TStream, TTools>
4620
+ stream: true,
4621
+ }),
4622
+ ) as TextActivityResult<TSchema, TStream, TTools>
4623
+ }
4624
+
4625
+ type RuntimeTextActivityOptions<
4626
+ TAdapter extends AnyTextAdapter,
4627
+ TSchema extends SchemaInput | undefined,
4628
+ TStream extends boolean,
4629
+ > = Omit<TextActivityOptions<TAdapter, TSchema, TStream, any>, 'middleware'> & {
4630
+ middleware?: Array<AnyChatMiddleware>
4631
+ }
4632
+
4633
+ function readRuntimeMiddleware(
4634
+ middleware: unknown,
4635
+ ): Array<AnyChatMiddleware> | undefined {
4636
+ if (middleware === undefined) return undefined
4637
+ if (!Array.isArray(middleware)) {
4638
+ throw new TypeError('Chat middleware must be an array.')
3858
4639
  }
4640
+ return middleware
4641
+ }
3859
4642
 
3860
- // Otherwise, run streaming text (default)
3861
- return runStreamingText({
3862
- ...options,
3863
- outputSchema: undefined,
3864
- stream,
3865
- }) as TextActivityResult<TSchema, TStream, TTools>
4643
+ function toRuntimeTextActivityOptions<
4644
+ TAdapter extends AnyTextAdapter,
4645
+ TInputSchema extends SchemaInput | undefined,
4646
+ TInputStream extends boolean,
4647
+ TOutputSchema extends SchemaInput | undefined,
4648
+ TOutputStream extends boolean,
4649
+ TTools extends TextActivityOptions<
4650
+ TAdapter,
4651
+ TInputSchema,
4652
+ TInputStream,
4653
+ any
4654
+ >['tools'],
4655
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
4656
+ TContext,
4657
+ TMiddleware extends Array<unknown> | undefined,
4658
+ >(
4659
+ options: TextActivityOptionsWithContext<
4660
+ TAdapter,
4661
+ TInputSchema,
4662
+ TInputStream,
4663
+ TTools,
4664
+ TInterrupts,
4665
+ TContext,
4666
+ TMiddleware
4667
+ >,
4668
+ overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
4669
+ ): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
4670
+ const { middleware, ...rest } = options
4671
+ return {
4672
+ ...rest,
4673
+ ...overrides,
4674
+ ...(middleware === undefined
4675
+ ? {}
4676
+ : { middleware: readRuntimeMiddleware(middleware) }),
4677
+ }
4678
+ }
4679
+
4680
+ function validateInterruptDefinitions(
4681
+ definitions:
4682
+ | ReadonlyArray<InterruptDefinition<any, any, any, any>>
4683
+ | undefined,
4684
+ ): void {
4685
+ if (!definitions) return
4686
+ const seen = new Set<string>()
4687
+ for (const definition of definitions) {
4688
+ if (seen.has(definition.id)) {
4689
+ throw new Error(`Duplicate interrupt definition id: ${definition.id}`)
4690
+ }
4691
+ seen.add(definition.id)
4692
+ }
3866
4693
  }
3867
4694
 
3868
4695
  /**
@@ -3914,8 +4741,8 @@ function publishDeliverySeams(
3914
4741
  * returns, so the identity has to be minted out here and the engine reached back
3915
4742
  * through `engineRef`, which the body fills as soon as its engine exists.
3916
4743
  */
3917
- function runStreamingText<TContext = unknown>(
3918
- options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
4744
+ function runStreamingText(
4745
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
3919
4746
  ): AsyncIterable<StreamChunk> {
3920
4747
  const engineRef: DeliveryEngineRef = {}
3921
4748
  const stream = streamTextChunks(options, engineRef)
@@ -3923,8 +4750,8 @@ function runStreamingText<TContext = unknown>(
3923
4750
  return stream
3924
4751
  }
3925
4752
 
3926
- async function* streamTextChunks<TContext = unknown>(
3927
- options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
4753
+ async function* streamTextChunks(
4754
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
3928
4755
  engineRef: DeliveryEngineRef,
3929
4756
  ): AsyncIterable<StreamChunk> {
3930
4757
  const { adapter, middleware, context, debug, mcp, ...textOptions } = options
@@ -3943,7 +4770,7 @@ async function* streamTextChunks<TContext = unknown>(
3943
4770
  params: { ...textOptions, model, logger } as TextOptions<
3944
4771
  Record<string, any>,
3945
4772
  Record<string, any>,
3946
- TContext
4773
+ any
3947
4774
  >,
3948
4775
  middleware,
3949
4776
  context,
@@ -3965,19 +4792,13 @@ async function* streamTextChunks<TContext = unknown>(
3965
4792
  * Run non-streaming text - collects all content and returns as a string.
3966
4793
  * Runs the full agentic loop (if tools are provided) but returns collected text.
3967
4794
  */
3968
- function runNonStreamingText<TContext = unknown>(
3969
- options: TextActivityOptions<AnyTextAdapter, undefined, false, TContext>,
4795
+ function runNonStreamingText(
4796
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, false>,
3970
4797
  ): Promise<string> {
3971
- // Run the streaming text and collect all text using streamToText.
3972
- const stream = runStreamingText(
3973
- // oxlint-disable-next-line eslint-js/no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
3974
- options as unknown as TextActivityOptions<
3975
- AnyTextAdapter,
3976
- undefined,
3977
- true,
3978
- TContext
3979
- >,
3980
- )
4798
+ const stream = runStreamingText({
4799
+ ...options,
4800
+ stream: true,
4801
+ })
3981
4802
 
3982
4803
  return streamToText(stream)
3983
4804
  }
@@ -3988,11 +4809,8 @@ function runNonStreamingText<TContext = unknown>(
3988
4809
  * 2. Once complete, call adapter.structuredOutput with the conversation context
3989
4810
  * 3. Validate and return the structured result
3990
4811
  */
3991
- async function runAgenticStructuredOutput<
3992
- TSchema extends SchemaInput,
3993
- TContext = unknown,
3994
- >(
3995
- options: TextActivityOptions<AnyTextAdapter, TSchema, boolean, TContext>,
4812
+ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
4813
+ options: RuntimeTextActivityOptions<AnyTextAdapter, TSchema, boolean>,
3996
4814
  ): Promise<InferSchemaType<TSchema>> {
3997
4815
  const {
3998
4816
  adapter,
@@ -4063,7 +4881,7 @@ async function runAgenticStructuredOutput<
4063
4881
  params: { ...textOptions, model, logger } as TextOptions<
4064
4882
  Record<string, unknown>,
4065
4883
  Record<string, unknown>,
4066
- TContext
4884
+ any
4067
4885
  >,
4068
4886
  middleware,
4069
4887
  context,
@@ -4126,6 +4944,15 @@ async function runAgenticStructuredOutput<
4126
4944
  * Uses an `unknown`-input runtime check rather than `as` casts so the engine
4127
4945
  * stays cast-free in its hot path.
4128
4946
  */
4947
+ function readCustomEventMessageId(value: unknown): string | undefined {
4948
+ if (typeof value !== 'object' || value === null) return undefined
4949
+ if (!('messageId' in value)) return undefined
4950
+ const messageId = value.messageId
4951
+ return typeof messageId === 'string' && messageId !== ''
4952
+ ? messageId
4953
+ : undefined
4954
+ }
4955
+
4129
4956
  function readStructuredOutputCompleteValue(
4130
4957
  value: unknown,
4131
4958
  ): { object: unknown; raw: string; reasoning?: string } | null {
@@ -4271,11 +5098,8 @@ async function* fallbackStructuredOutputStream(
4271
5098
  * synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
4272
5099
  * those are programmer errors, not runtime conditions.
4273
5100
  */
4274
- function runStreamingStructuredOutput<
4275
- TSchema extends SchemaInput,
4276
- TContext = unknown,
4277
- >(
4278
- options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
5101
+ function runStreamingStructuredOutput<TSchema extends SchemaInput>(
5102
+ options: RuntimeTextActivityOptions<AnyTextAdapter, TSchema, true>,
4279
5103
  ): StructuredOutputStream<InferSchemaType<TSchema>> {
4280
5104
  const { outputSchema } = options
4281
5105
 
@@ -4336,11 +5160,8 @@ type StructuredOutputStreamInternal<T> = AsyncIterable<
4336
5160
  StreamChunk | StructuredOutputCompleteEvent<T>
4337
5161
  >
4338
5162
 
4339
- async function* runStreamingStructuredOutputImpl<
4340
- TSchema extends SchemaInput,
4341
- TContext = unknown,
4342
- >(
4343
- options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
5163
+ async function* runStreamingStructuredOutputImpl<TSchema extends SchemaInput>(
5164
+ options: RuntimeTextActivityOptions<AnyTextAdapter, TSchema, true>,
4344
5165
  jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
4345
5166
  normalize: (data: unknown) => unknown,
4346
5167
  engineRef: DeliveryEngineRef,
@@ -4383,7 +5204,7 @@ async function* runStreamingStructuredOutputImpl<
4383
5204
  params: { ...textOptions, model, logger } as TextOptions<
4384
5205
  Record<string, unknown>,
4385
5206
  Record<string, unknown>,
4386
- TContext
5207
+ any
4387
5208
  >,
4388
5209
  middleware,
4389
5210
  context,