@tanstack/ai 0.46.0 → 0.47.2

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 +440 -79
  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 +3 -3
  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 +1030 -211
  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
@@ -4,6 +4,7 @@ import type {
4
4
  ContentPart,
5
5
  MessagePart,
6
6
  ModelMessage,
7
+ StructuredOutputPart,
7
8
  TextPart,
8
9
  ToolCallPart,
9
10
  UIMessage,
@@ -26,9 +27,9 @@ function isContentPart(part: MessagePart): part is ContentPart {
26
27
  )
27
28
  }
28
29
 
29
- function safeJsonStringify(value: unknown): string {
30
+ export function safeJsonStringify(value: unknown): string {
30
31
  try {
31
- return JSON.stringify(value)
32
+ return JSON.stringify(value) ?? ''
32
33
  } catch {
33
34
  return ''
34
35
  }
@@ -196,6 +197,7 @@ function buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage {
196
197
  // Accumulator for building an assistant segment (content + tool calls)
197
198
  interface AssistantSegment {
198
199
  contentParts: Array<ContentPart>
200
+ structuredOutput?: StructuredOutputPart
199
201
  toolCalls: Array<{
200
202
  id: string
201
203
  type: 'function'
@@ -255,6 +257,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
255
257
  content,
256
258
  ...(hasToolCalls && { toolCalls: current.toolCalls }),
257
259
  ...(pendingThinking.length > 0 && { thinking: pendingThinking }),
260
+ ...(current.structuredOutput && {
261
+ structuredOutput: current.structuredOutput,
262
+ }),
258
263
  ...(uiMessage.createdAt !== undefined && {
259
264
  createdAt: uiMessage.createdAt,
260
265
  }),
@@ -333,6 +338,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
333
338
  : ''
334
339
  if (serialized !== '') {
335
340
  current.contentParts.push({ type: 'text', content: serialized })
341
+ current.structuredOutput = part
336
342
  }
337
343
  }
338
344
  break
@@ -445,7 +451,9 @@ export function modelMessageToUIMessage(
445
451
 
446
452
  // Handle tool results (when role is "tool") - only produce tool-result part,
447
453
  // not a text part (the content IS the tool result, not display text)
448
- if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
454
+ if (modelMessage.role === 'assistant' && modelMessage.structuredOutput) {
455
+ parts.push(modelMessage.structuredOutput)
456
+ } else if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
449
457
  parts.push({
450
458
  type: 'tool-result',
451
459
  toolCallId: modelMessage.toolCallId,
@@ -1,6 +1,9 @@
1
1
  import type { CapabilityHandle } from './capabilities'
2
2
  import type { AnyChatMiddleware, ChatMiddleware } from './types'
3
3
  import type { DefinedChatMiddleware } from './define'
4
+ import type { InterruptDefinition } from '../../../interrupt-definition'
5
+
6
+ type AnyInterruptDefinition = InterruptDefinition<any, any, any, any>
4
7
 
5
8
  /** Union of capability NAME literals from a tuple of handles. */
6
9
  export type NamesOf<T extends ReadonlyArray<CapabilityHandle>> =
@@ -67,19 +70,41 @@ export type CheckCoverage<TList extends ReadonlyArray<AnyChatMiddleware>> = [
67
70
  export interface ChatMiddlewareBuilder<
68
71
  TList extends ReadonlyArray<AnyChatMiddleware>,
69
72
  TProvided extends string,
73
+ TInterruptDefinitions extends AnyInterruptDefinition = never,
70
74
  > {
71
75
  use: <
72
76
  TRequires extends ReadonlyArray<CapabilityHandle>,
73
77
  TProvides extends ReadonlyArray<CapabilityHandle>,
74
78
  TContext = unknown,
79
+ TMiddlewareInterruptDefinitions extends AnyInterruptDefinition =
80
+ TInterruptDefinitions,
75
81
  >(
76
82
  middleware: [NamesOf<TRequires>] extends [TProvided]
77
- ? DefinedChatMiddleware<TContext, TRequires, TProvides>
78
- : DefinedChatMiddleware<TContext, TRequires, TProvides> &
83
+ ? DefinedChatMiddleware<
84
+ TContext,
85
+ TRequires,
86
+ TProvides,
87
+ TMiddlewareInterruptDefinitions
88
+ >
89
+ : DefinedChatMiddleware<
90
+ TContext,
91
+ TRequires,
92
+ TProvides,
93
+ TMiddlewareInterruptDefinitions
94
+ > &
79
95
  MissingCapabilities<Exclude<NamesOf<TRequires>, TProvided>>,
80
96
  ) => ChatMiddlewareBuilder<
81
- readonly [...TList, DefinedChatMiddleware<TContext, TRequires, TProvides>],
82
- TProvided | NamesOf<TProvides>
97
+ readonly [
98
+ ...TList,
99
+ DefinedChatMiddleware<
100
+ TContext,
101
+ TRequires,
102
+ TProvides,
103
+ TMiddlewareInterruptDefinitions
104
+ >,
105
+ ],
106
+ TProvided | NamesOf<TProvides>,
107
+ TInterruptDefinitions | TMiddlewareInterruptDefinitions
83
108
  >
84
109
 
85
110
  build: () => [...TList]
@@ -10,6 +10,9 @@ import type {
10
10
  ChatMiddlewareContext,
11
11
  ErrorInfo,
12
12
  FinishInfo,
13
+ InterruptBoundaryPhase,
14
+ InterruptResolutionCollection,
15
+ InterruptToolResume,
13
16
  IterationInfo,
14
17
  SandboxFileHookEvent,
15
18
  StructuredOutputMiddlewareConfig,
@@ -17,6 +20,10 @@ import type {
17
20
  ToolPhaseCompleteInfo,
18
21
  UsageInfo,
19
22
  } from './types'
23
+ import type {
24
+ GenericInterruptRequest,
25
+ InterruptDefinition,
26
+ } from '../../../interrupt-definition'
20
27
 
21
28
  /** One middleware's terminal-hook throw, captured instead of propagated. */
22
29
  interface HookFailure {
@@ -25,7 +32,7 @@ interface HookFailure {
25
32
  }
26
33
 
27
34
  /** Check if a middleware should be skipped for instrumentation events. */
28
- function shouldSkipInstrumentation(mw: ChatMiddleware<any>): boolean {
35
+ function shouldSkipInstrumentation(mw: ChatMiddleware<any, any>): boolean {
29
36
  return mw.name === 'devtools' || mw.name === 'strip-to-spec'
30
37
  }
31
38
 
@@ -43,12 +50,18 @@ function instrumentCtx(ctx: ChatMiddlewareContext<any>) {
43
50
  * Internal middleware runner that manages composed execution of middleware hooks.
44
51
  * Created once per chat() invocation.
45
52
  */
46
- export class MiddlewareRunner<TContext = unknown> {
47
- private readonly middlewares: ReadonlyArray<ChatMiddleware<TContext>>
53
+ export class MiddlewareRunner<
54
+ TContext = unknown,
55
+ TInterruptDefinitions extends InterruptDefinition<any, any, any, any> =
56
+ InterruptDefinition<any, any, any, any>,
57
+ > {
58
+ private readonly middlewares: ReadonlyArray<
59
+ ChatMiddleware<TContext, TInterruptDefinitions>
60
+ >
48
61
  private readonly logger: InternalLogger
49
62
 
50
63
  constructor(
51
- middlewares: ReadonlyArray<ChatMiddleware<TContext>>,
64
+ middlewares: ReadonlyArray<ChatMiddleware<TContext, TInterruptDefinitions>>,
52
65
  logger: InternalLogger,
53
66
  ) {
54
67
  this.middlewares = middlewares
@@ -59,6 +72,83 @@ export class MiddlewareRunner<TContext = unknown> {
59
72
  return this.middlewares.length > 0
60
73
  }
61
74
 
75
+ async runOnInterruptBoundary(
76
+ ctx: ChatMiddlewareContext<TContext> & { phase: InterruptBoundaryPhase },
77
+ ): Promise<ReadonlyArray<GenericInterruptRequest<TInterruptDefinitions>>> {
78
+ const requests: Array<GenericInterruptRequest<TInterruptDefinitions>> = []
79
+ for (const mw of this.middlewares) {
80
+ if (mw.onInterruptBoundary) {
81
+ const skip = shouldSkipInstrumentation(mw)
82
+ const start = Date.now()
83
+ const result = await mw.onInterruptBoundary(ctx)
84
+ if (result?.interrupts) requests.push(...result.interrupts)
85
+ if (!skip) {
86
+ this.logger.middleware(
87
+ `hook=onInterruptBoundary middleware=${mw.name ?? 'unnamed'}`,
88
+ {
89
+ middleware: mw.name ?? 'unnamed',
90
+ hook: 'onInterruptBoundary',
91
+ },
92
+ )
93
+ aiEventClient.emit('middleware:hook:executed', {
94
+ ...instrumentCtx(ctx),
95
+ middlewareName: mw.name || 'unnamed',
96
+ hookName: 'onInterruptBoundary',
97
+ iteration: ctx.iteration,
98
+ duration: Date.now() - start,
99
+ hasTransform: result?.interrupts !== undefined,
100
+ })
101
+ }
102
+ }
103
+ }
104
+ return requests
105
+ }
106
+
107
+ async runOnInterruptResolution(
108
+ ctx: ChatMiddlewareContext<TContext>,
109
+ resolutions: InterruptResolutionCollection<TInterruptDefinitions>,
110
+ ): Promise<{ toolResume?: InterruptToolResume }> {
111
+ let toolResume: InterruptToolResume | undefined
112
+ for (const mw of this.middlewares) {
113
+ if (mw.onInterruptResolution) {
114
+ const skip = shouldSkipInstrumentation(mw)
115
+ const start = Date.now()
116
+ const next = await mw.onInterruptResolution(ctx, resolutions)
117
+ if (next?.toolResume !== undefined) {
118
+ const priority: Record<InterruptToolResume, number> = {
119
+ continue: 0,
120
+ cancel: 1,
121
+ stop: 2,
122
+ }
123
+ if (
124
+ toolResume === undefined ||
125
+ priority[next.toolResume] > priority[toolResume]
126
+ ) {
127
+ toolResume = next.toolResume
128
+ }
129
+ }
130
+ if (!skip) {
131
+ this.logger.middleware(
132
+ `hook=onInterruptResolution middleware=${mw.name ?? 'unnamed'}`,
133
+ {
134
+ middleware: mw.name ?? 'unnamed',
135
+ hook: 'onInterruptResolution',
136
+ },
137
+ )
138
+ aiEventClient.emit('middleware:hook:executed', {
139
+ ...instrumentCtx(ctx),
140
+ middlewareName: mw.name || 'unnamed',
141
+ hookName: 'onInterruptResolution',
142
+ iteration: ctx.iteration,
143
+ duration: Date.now() - start,
144
+ hasTransform: next !== undefined,
145
+ })
146
+ }
147
+ }
148
+ }
149
+ return toolResume === undefined ? {} : { toolResume }
150
+ }
151
+
62
152
  /**
63
153
  * Pipe config through all middleware onConfig hooks in order.
64
154
  * Each middleware receives the merged config from previous middleware.
@@ -492,7 +582,7 @@ export class MiddlewareRunner<TContext = unknown> {
492
582
  * {@link runOnError}.
493
583
  */
494
584
  private async captureTerminalHook(
495
- mw: ChatMiddleware<TContext>,
585
+ mw: ChatMiddleware<TContext, TInterruptDefinitions>,
496
586
  hookName: 'onFinish' | 'onAbort' | 'onError',
497
587
  invoke: () => void | Promise<void>,
498
588
  ): Promise<HookFailure | undefined> {
@@ -1,5 +1,8 @@
1
1
  import type { CapabilityHandle } from './capabilities'
2
2
  import type { ChatMiddleware } from './types'
3
+ import type { InterruptDefinition } from '../../../interrupt-definition'
4
+
5
+ type AnyInterruptDefinition = InterruptDefinition<any, any, any, any>
3
6
 
4
7
  /**
5
8
  * A middleware whose `requires`/`provides` tuple types are captured precisely
@@ -9,7 +12,8 @@ export interface DefinedChatMiddleware<
9
12
  TContext,
10
13
  TRequires extends ReadonlyArray<CapabilityHandle>,
11
14
  TProvides extends ReadonlyArray<CapabilityHandle>,
12
- > extends ChatMiddleware<TContext> {
15
+ TInterruptDefinitions extends AnyInterruptDefinition = never,
16
+ > extends ChatMiddleware<TContext, TInterruptDefinitions> {
13
17
  requires?: TRequires
14
18
  provides?: TProvides
15
19
  }
@@ -24,11 +28,17 @@ export function defineChatMiddleware<
24
28
  TContext = unknown,
25
29
  const TRequires extends ReadonlyArray<CapabilityHandle> = readonly [],
26
30
  const TProvides extends ReadonlyArray<CapabilityHandle> = readonly [],
31
+ TInterruptDefinitions extends AnyInterruptDefinition = never,
27
32
  >(
28
- middleware: ChatMiddleware<TContext> & {
33
+ middleware: ChatMiddleware<TContext, TInterruptDefinitions> & {
29
34
  requires?: TRequires
30
35
  provides?: TProvides
31
36
  },
32
- ): DefinedChatMiddleware<TContext, TRequires, TProvides> {
37
+ ): DefinedChatMiddleware<
38
+ TContext,
39
+ TRequires,
40
+ TProvides,
41
+ TInterruptDefinitions
42
+ > {
33
43
  return middleware
34
44
  }
@@ -0,0 +1,26 @@
1
+ import { createCapability } from './capabilities'
2
+ import type { InterruptDefinition } from '../../../interrupt-definition'
3
+
4
+ /**
5
+ * Internal per-run registry of first-party generic interrupt definitions.
6
+ *
7
+ * Persistence uses this bridge while restoring a durable interrupt. It never
8
+ * receives a definition from the stored record; it can only look up one that
9
+ * the current chat call registered.
10
+ */
11
+ export interface GenericInterruptDefinitionRegistry {
12
+ readonly definitions: ReadonlyMap<
13
+ string,
14
+ InterruptDefinition<any, any, any, any>
15
+ >
16
+ }
17
+
18
+ export const GenericInterruptDefinitionRegistryCapability =
19
+ createCapability<GenericInterruptDefinitionRegistry>()(
20
+ 'generic-interrupt-definition-registry',
21
+ )
22
+
23
+ export const [
24
+ getGenericInterruptDefinitionRegistry,
25
+ provideGenericInterruptDefinitionRegistry,
26
+ ] = GenericInterruptDefinitionRegistryCapability
@@ -18,8 +18,23 @@ export type {
18
18
  SandboxFileEvent,
19
19
  SandboxFileHookEvent,
20
20
  ChatSandboxHooks,
21
+ InterruptBoundaryPhase,
22
+ InterruptToolResume,
23
+ InterruptResolutionCollection,
24
+ GenericInterruptResolution,
25
+ InterruptBoundaryResult,
26
+ InterruptResolutionResult,
21
27
  } from './types'
22
28
 
29
+ export { INTERRUPT_BOUNDARY_PHASES, INTERRUPT_TOOL_RESUMES } from './types'
30
+
31
+ export {
32
+ GenericInterruptDefinitionRegistryCapability,
33
+ getGenericInterruptDefinitionRegistry,
34
+ provideGenericInterruptDefinitionRegistry,
35
+ } from './generic-interrupts'
36
+ export type { GenericInterruptDefinitionRegistry } from './generic-interrupts'
37
+
23
38
  export { MiddlewareRunner } from './compose'
24
39
 
25
40
  export { createCapability, CapabilityRegistry } from './capabilities'
@@ -1,3 +1,7 @@
1
+ import type {
2
+ StandardJSONSchemaV1,
3
+ StandardSchemaV1,
4
+ } from '@standard-schema/spec'
1
5
  import type {
2
6
  AgentLoopState,
3
7
  JSONSchema,
@@ -10,6 +14,10 @@ import type {
10
14
  } from '../../../types'
11
15
  import type { SystemPrompt } from '../../../system-prompts'
12
16
  import type { ToolApprovalResolution } from '../../../interrupts'
17
+ import type {
18
+ GenericInterruptRequest,
19
+ InterruptDefinition,
20
+ } from '../../../interrupt-definition'
13
21
  import type {
14
22
  Capability,
15
23
  CapabilityHandle,
@@ -68,6 +76,7 @@ export interface ChatSandboxHooks<TContext = unknown> {
68
76
  * Phase of the chat middleware lifecycle.
69
77
  * - 'init': Initial config transform before the chat engine starts
70
78
  * - 'beforeModel': Before each adapter chatStream call (per agent iteration)
79
+ * - 'afterModel': After each adapter chatStream call (per agent iteration)
71
80
  * - 'modelStream': During model streaming
72
81
  * - 'beforeTools': Before tool execution phase
73
82
  * - 'afterTools': After tool execution phase
@@ -77,11 +86,97 @@ export interface ChatSandboxHooks<TContext = unknown> {
77
86
  export type ChatMiddlewarePhase =
78
87
  | 'init'
79
88
  | 'beforeModel'
89
+ | 'afterModel'
80
90
  | 'modelStream'
81
91
  | 'beforeTools'
82
92
  | 'afterTools'
83
93
  | 'structuredOutput'
84
94
 
95
+ export const INTERRUPT_BOUNDARY_PHASES = [
96
+ 'beforeModel',
97
+ 'afterModel',
98
+ 'beforeTools',
99
+ 'afterTools',
100
+ ] as const
101
+
102
+ export type InterruptBoundaryPhase = (typeof INTERRUPT_BOUNDARY_PHASES)[number]
103
+
104
+ export const INTERRUPT_TOOL_RESUMES = ['continue', 'cancel', 'stop'] as const
105
+
106
+ export type InterruptToolResume = (typeof INTERRUPT_TOOL_RESUMES)[number]
107
+
108
+ type AnyInterruptDefinition = InterruptDefinition<any, any, any, any>
109
+
110
+ type InterruptResponse<TDefinition> =
111
+ TDefinition extends InterruptDefinition<any, any, infer TResponseSchema, any>
112
+ ? TResponseSchema extends StandardSchemaV1<any, infer TResponse>
113
+ ? TResponse
114
+ : TResponseSchema extends StandardJSONSchemaV1<any, infer TResponse>
115
+ ? TResponse
116
+ : unknown
117
+ : unknown
118
+
119
+ export type GenericInterruptResolution<
120
+ TDefinition extends AnyInterruptDefinition,
121
+ > = TDefinition extends AnyInterruptDefinition
122
+ ?
123
+ | {
124
+ readonly request: GenericInterruptRequest<TDefinition>
125
+ readonly status: 'resolved'
126
+ readonly response: InterruptResponse<TDefinition>
127
+ }
128
+ | {
129
+ readonly request: GenericInterruptRequest<TDefinition>
130
+ readonly status: 'cancelled'
131
+ readonly response?: never
132
+ }
133
+ : never
134
+
135
+ export interface InterruptResolutionCollection<
136
+ TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,
137
+ > {
138
+ for: <
139
+ TDefinition extends ([TDefinitions] extends [never]
140
+ ? AnyInterruptDefinition
141
+ : TDefinitions),
142
+ >(
143
+ definition: TDefinition,
144
+ ) => ReadonlyArray<GenericInterruptResolution<TDefinition>>
145
+ all: {
146
+ (): ReadonlyArray<GenericInterruptResolution<TDefinitions>>
147
+ <const TSelected extends ReadonlyArray<TDefinitions>>(
148
+ ...definitions: TSelected
149
+ ): ReadonlyArray<GenericInterruptResolution<TSelected[number]>>
150
+ }
151
+ }
152
+
153
+ type BivariantInterruptResolutionHook<
154
+ TContext,
155
+ TDefinitions extends AnyInterruptDefinition,
156
+ > = InterruptResolutionHookSignature<TContext, TDefinitions>['call']
157
+
158
+ declare abstract class InterruptResolutionHookSignature<
159
+ TContext,
160
+ TDefinitions extends AnyInterruptDefinition,
161
+ > {
162
+ abstract call(
163
+ ctx: ChatMiddlewareContext<TContext>,
164
+ resolutions: InterruptResolutionCollection<TDefinitions>,
165
+ ): InterruptResolutionResult | Promise<InterruptResolutionResult>
166
+ }
167
+
168
+ export type InterruptBoundaryResult<
169
+ TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,
170
+ > =
171
+ | undefined
172
+ | {
173
+ readonly interrupts: ReadonlyArray<GenericInterruptRequest<TDefinitions>>
174
+ }
175
+
176
+ export type InterruptResolutionResult = void | {
177
+ readonly toolResume: InterruptToolResume
178
+ }
179
+
85
180
  /**
86
181
  * Stable context object passed to all middleware hooks.
87
182
  * Created once per chat() invocation and shared across all hooks.
@@ -230,6 +325,13 @@ export interface ChatResumeToolState {
230
325
  genericInterrupts?:
231
326
  | ReadonlyMap<string, ChatResumeGenericResolution>
232
327
  | undefined
328
+ /** Durable generic requests reconstructed by server middleware. */
329
+ genericInterruptRequests?:
330
+ | ReadonlyMap<
331
+ string,
332
+ GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
333
+ >
334
+ | undefined
233
335
  deniedToolResults?: ReadonlyMap<string, unknown> | undefined
234
336
  cancelledToolCallIds?: ReadonlySet<string> | undefined
235
337
  }
@@ -460,10 +562,32 @@ export interface ErrorInfo {
460
562
  * }
461
563
  * ```
462
564
  */
463
- export interface ChatMiddleware<TContext = unknown> {
565
+ export interface ChatMiddleware<
566
+ TContext = unknown,
567
+ TInterruptDefinitions extends AnyInterruptDefinition = never,
568
+ > {
464
569
  /** Optional name for debugging and identification */
465
570
  name?: string
466
571
 
572
+ /**
573
+ * Called at a lifecycle boundary. Return interrupt requests to pause the run.
574
+ * Requests from every middleware in the same boundary form one batch.
575
+ */
576
+ onInterruptBoundary?: (
577
+ ctx: ChatMiddlewareContext<TContext> & { phase: InterruptBoundaryPhase },
578
+ ) =>
579
+ | InterruptBoundaryResult<TInterruptDefinitions>
580
+ | Promise<InterruptBoundaryResult<TInterruptDefinitions>>
581
+
582
+ /**
583
+ * Called on a continuation run after the client answers registered interrupts.
584
+ * Return `toolResume` to decide whether pending tools continue, cancel, or stop.
585
+ */
586
+ onInterruptResolution?: BivariantInterruptResolutionHook<
587
+ TContext,
588
+ TInterruptDefinitions
589
+ >
590
+
467
591
  /**
468
592
  * Capabilities this middleware requires. `chat()` validates that some
469
593
  * middleware (or the adapter) provides each one; unsatisfied requirements are
@@ -653,4 +777,5 @@ export interface ChatMiddleware<TContext = unknown> {
653
777
  }
654
778
 
655
779
  /** A `ChatMiddleware` with a permissive context — for use as a constraint. */
656
- export type AnyChatMiddleware = ChatMiddleware<any>
780
+ /** A permissive middleware constraint that retains the definition parameter. */
781
+ export type AnyChatMiddleware = ChatMiddleware<any, any>
@@ -38,6 +38,7 @@ import {
38
38
  updateToolResultPart,
39
39
  } from './message-updaters'
40
40
  import { ImmediateStrategy } from './strategies'
41
+ import { INTERRUPT_BINDING_METADATA_KEY } from '../../../interrupt-resume'
41
42
  import type {
42
43
  ChunkRecording,
43
44
  ChunkStrategy,
@@ -138,6 +139,22 @@ export interface StreamProcessorOptions {
138
139
 
139
140
  const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12
140
141
 
142
+ function interruptBatchHasGeneric(interrupts: Array<Interrupt>): boolean {
143
+ return interrupts.some((interrupt) => {
144
+ const metadata = interrupt.metadata
145
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
146
+ return false
147
+ }
148
+ const binding = metadata[INTERRUPT_BINDING_METADATA_KEY]
149
+ return (
150
+ binding !== null &&
151
+ typeof binding === 'object' &&
152
+ !Array.isArray(binding) &&
153
+ binding.kind === 'generic'
154
+ )
155
+ })
156
+ }
157
+
141
158
  /**
142
159
  * StreamProcessor - State machine for processing AI response streams
143
160
  *
@@ -1548,6 +1565,7 @@ export class StreamProcessor {
1548
1565
  }
1549
1566
 
1550
1567
  private handleInterrupts(interrupts: Array<Interrupt>): void {
1568
+ const hasGeneric = interruptBatchHasGeneric(interrupts)
1551
1569
  for (const interrupt of interrupts) {
1552
1570
  const metadata =
1553
1571
  interrupt.metadata && typeof interrupt.metadata === 'object'
@@ -1598,6 +1616,9 @@ export class StreamProcessor {
1598
1616
  }
1599
1617
 
1600
1618
  if (kind === 'client_tool' || interrupt.reason === 'client_tool_input') {
1619
+ // Generic interrupts in the same batch decide `toolResume`. Do not
1620
+ // run client tools until that policy is `continue`.
1621
+ if (hasGeneric) continue
1601
1622
  this.events.onToolCall?.({
1602
1623
  toolCallId,
1603
1624
  toolName,
@@ -25,6 +25,26 @@ export {
25
25
  PendingTurnCapability,
26
26
  providePendingTurn,
27
27
  } from './activities/chat/middleware/pending-turn'
28
+ export {
29
+ getGenericInterruptDefinitionRegistry,
30
+ GenericInterruptDefinitionRegistryCapability,
31
+ provideGenericInterruptDefinitionRegistry,
32
+ } from './activities/chat/middleware/generic-interrupts'
33
+ export type { GenericInterruptDefinitionRegistry } from './activities/chat/middleware/generic-interrupts'
34
+ export {
35
+ createInterruptBinding,
36
+ getInterruptRequestInput,
37
+ rehydrateInterruptRequest,
38
+ } from './interrupt-definition'
39
+ export type {
40
+ GenericInterruptRequest,
41
+ InterruptDefinition,
42
+ } from './interrupt-definition'
43
+ export {
44
+ readInterruptBinding,
45
+ validateInterruptResumeBatch,
46
+ } from './interrupt-resume'
47
+ export type { PendingInterruptResumeRecord } from './interrupt-resume'
28
48
  export {
29
49
  assertUniqueToolNames,
30
50
  DuplicateToolNameError,
package/src/client.ts CHANGED
@@ -343,6 +343,26 @@ export {
343
343
  INTERRUPT_BINDING_VERSION,
344
344
  canonicalizeInterruptResolutions,
345
345
  } from './interrupts'
346
+ export {
347
+ defineInterrupt,
348
+ hashInterruptDefinitionSchema,
349
+ INTERRUPT_PAYLOAD_METADATA_KEY,
350
+ } from './interrupt-definition'
351
+ export {
352
+ INTERRUPT_CONTINUATION_METADATA_KEY,
353
+ INTERRUPT_CONTINUATION_VERSION,
354
+ genericInterruptContinuationFromDescriptor,
355
+ readGenericInterruptContinuation,
356
+ wrapGenericInterruptContinuation,
357
+ } from './generic-interrupt-continuation'
358
+ export type {
359
+ GenericInterruptContinuation,
360
+ GenericInterruptContinuationReadResult,
361
+ } from './generic-interrupt-continuation'
362
+ export type {
363
+ GenericInterruptRequest,
364
+ InterruptDefinition,
365
+ } from './interrupt-definition'
346
366
  export type {
347
367
  BatchInterruptError,
348
368
  BatchInterruptErrorCode,