@tanstack/ai 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/activities/chat/adapter.js +23 -16
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
  6. package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
  7. package/dist/esm/activities/chat/cancel.d.ts +40 -0
  8. package/dist/esm/activities/chat/cancel.js +54 -0
  9. package/dist/esm/activities/chat/cancel.js.map +1 -0
  10. package/dist/esm/activities/chat/index.d.ts +28 -21
  11. package/dist/esm/activities/chat/index.js +2100 -1813
  12. package/dist/esm/activities/chat/index.js.map +1 -1
  13. package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
  14. package/dist/esm/activities/chat/mcp/manager.js +90 -77
  15. package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
  16. package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
  17. package/dist/esm/activities/chat/messages.js +397 -346
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/builder.js +17 -15
  20. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  21. package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
  22. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
  23. package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
  24. package/dist/esm/activities/chat/middleware/compose.js +623 -531
  25. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  26. package/dist/esm/activities/chat/middleware/define.js +12 -5
  27. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  28. package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
  29. package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
  30. package/dist/esm/activities/chat/middleware/locks.js +71 -0
  31. package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
  32. package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
  33. package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
  34. package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
  35. package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
  36. package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
  37. package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
  38. package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
  39. package/dist/esm/activities/chat/middleware/run-store.js +176 -0
  40. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
  41. package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
  42. package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
  43. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
  44. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
  45. package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
  46. package/dist/esm/activities/chat/middleware/validate.js +23 -28
  47. package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
  48. package/dist/esm/activities/chat/stream/json-parser.js +39 -25
  49. package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
  50. package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
  51. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  52. package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
  53. package/dist/esm/activities/chat/stream/processor.js +1341 -1542
  54. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  55. package/dist/esm/activities/chat/stream/strategies.js +69 -53
  56. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  57. package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
  58. package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
  59. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
  60. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
  61. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  62. package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
  63. package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
  64. package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
  65. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  66. package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
  67. package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
  68. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  69. package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
  70. package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
  71. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  72. package/dist/esm/activities/error-payload.js +85 -47
  73. package/dist/esm/activities/error-payload.js.map +1 -1
  74. package/dist/esm/activities/generateAudio/adapter.js +22 -15
  75. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  76. package/dist/esm/activities/generateAudio/index.d.ts +4 -0
  77. package/dist/esm/activities/generateAudio/index.js +141 -105
  78. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  79. package/dist/esm/activities/generateImage/adapter.js +22 -15
  80. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  81. package/dist/esm/activities/generateImage/index.d.ts +4 -0
  82. package/dist/esm/activities/generateImage/index.js +155 -111
  83. package/dist/esm/activities/generateImage/index.js.map +1 -1
  84. package/dist/esm/activities/generateSpeech/adapter.js +22 -15
  85. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  86. package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
  87. package/dist/esm/activities/generateSpeech/index.js +159 -110
  88. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  89. package/dist/esm/activities/generateTranscription/adapter.js +22 -15
  90. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  91. package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
  92. package/dist/esm/activities/generateTranscription/index.js +159 -100
  93. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  94. package/dist/esm/activities/generateVideo/adapter.js +36 -29
  95. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  96. package/dist/esm/activities/generateVideo/index.d.ts +143 -19
  97. package/dist/esm/activities/generateVideo/index.js +456 -279
  98. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  99. package/dist/esm/activities/generateVideo/snap.js +60 -48
  100. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  101. package/dist/esm/activities/index.js +8 -34
  102. package/dist/esm/activities/middleware/index.d.ts +1 -1
  103. package/dist/esm/activities/middleware/run.d.ts +10 -0
  104. package/dist/esm/activities/middleware/run.js +53 -29
  105. package/dist/esm/activities/middleware/run.js.map +1 -1
  106. package/dist/esm/activities/middleware/types.d.ts +44 -6
  107. package/dist/esm/activities/stream-generation-result.d.ts +4 -1
  108. package/dist/esm/activities/stream-generation-result.js +79 -44
  109. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  110. package/dist/esm/activities/summarize/adapter.js +22 -15
  111. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  112. package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
  113. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  114. package/dist/esm/activities/summarize/index.d.ts +27 -0
  115. package/dist/esm/activities/summarize/index.js +268 -102
  116. package/dist/esm/activities/summarize/index.js.map +1 -1
  117. package/dist/esm/adapter-internals.d.ts +2 -1
  118. package/dist/esm/adapter-internals.js +4 -11
  119. package/dist/esm/client.d.ts +25 -3
  120. package/dist/esm/client.js +131 -64
  121. package/dist/esm/client.js.map +1 -1
  122. package/dist/esm/custom-events.d.ts +76 -0
  123. package/dist/esm/custom-events.js +37 -0
  124. package/dist/esm/custom-events.js.map +1 -0
  125. package/dist/esm/delivery-detach.d.ts +50 -0
  126. package/dist/esm/delivery-detach.js +71 -0
  127. package/dist/esm/delivery-detach.js.map +1 -0
  128. package/dist/esm/delivery-disconnect.d.ts +62 -0
  129. package/dist/esm/delivery-disconnect.js +81 -0
  130. package/dist/esm/delivery-disconnect.js.map +1 -0
  131. package/dist/esm/extend-adapter.js +19 -17
  132. package/dist/esm/extend-adapter.js.map +1 -1
  133. package/dist/esm/index.d.ts +24 -6
  134. package/dist/esm/index.js +30 -98
  135. package/dist/esm/interrupt-resume.d.ts +71 -0
  136. package/dist/esm/interrupt-resume.js +438 -0
  137. package/dist/esm/interrupt-resume.js.map +1 -0
  138. package/dist/esm/interrupt-serialization.d.ts +12 -0
  139. package/dist/esm/interrupt-serialization.js +178 -0
  140. package/dist/esm/interrupt-serialization.js.map +1 -0
  141. package/dist/esm/interrupts.d.ts +84 -0
  142. package/dist/esm/interrupts.js +31 -0
  143. package/dist/esm/interrupts.js.map +1 -0
  144. package/dist/esm/locks.d.ts +10 -0
  145. package/dist/esm/locks.js +2 -0
  146. package/dist/esm/logger/console-logger.js +101 -78
  147. package/dist/esm/logger/console-logger.js.map +1 -1
  148. package/dist/esm/logger/internal-logger.js +104 -89
  149. package/dist/esm/logger/internal-logger.js.map +1 -1
  150. package/dist/esm/logger/resolve.js +54 -49
  151. package/dist/esm/logger/resolve.js.map +1 -1
  152. package/dist/esm/logger/types.d.ts +1 -1
  153. package/dist/esm/middlewares/content-guard.js +142 -148
  154. package/dist/esm/middlewares/content-guard.js.map +1 -1
  155. package/dist/esm/middlewares/index.js +2 -6
  156. package/dist/esm/middlewares/otel.js +598 -732
  157. package/dist/esm/middlewares/otel.js.map +1 -1
  158. package/dist/esm/middlewares/usage-attributes.js +47 -40
  159. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  160. package/dist/esm/realtime/event-emitter.js +24 -25
  161. package/dist/esm/realtime/event-emitter.js.map +1 -1
  162. package/dist/esm/realtime/index.d.ts +5 -9
  163. package/dist/esm/realtime/index.js +29 -6
  164. package/dist/esm/realtime/index.js.map +1 -1
  165. package/dist/esm/scope.d.ts +47 -0
  166. package/dist/esm/stream-durability.d.ts +171 -0
  167. package/dist/esm/stream-durability.js +295 -0
  168. package/dist/esm/stream-durability.js.map +1 -0
  169. package/dist/esm/stream-to-response.d.ts +178 -13
  170. package/dist/esm/stream-to-response.js +663 -115
  171. package/dist/esm/stream-to-response.js.map +1 -1
  172. package/dist/esm/strip-to-spec-middleware.js +30 -16
  173. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  174. package/dist/esm/system-prompts.js +27 -21
  175. package/dist/esm/system-prompts.js.map +1 -1
  176. package/dist/esm/tool-registry.js +72 -45
  177. package/dist/esm/tool-registry.js.map +1 -1
  178. package/dist/esm/tools/provider-tool.js +14 -5
  179. package/dist/esm/tools/provider-tool.js.map +1 -1
  180. package/dist/esm/types.d.ts +321 -42
  181. package/dist/esm/types.js +2 -0
  182. package/dist/esm/utilities/ag-ui-wire.js +79 -93
  183. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  184. package/dist/esm/utilities/chat-params.d.ts +26 -4
  185. package/dist/esm/utilities/chat-params.js +218 -92
  186. package/dist/esm/utilities/chat-params.js.map +1 -1
  187. package/dist/esm/utilities/errors.js +28 -18
  188. package/dist/esm/utilities/errors.js.map +1 -1
  189. package/dist/esm/utilities/media-prompt.js +46 -41
  190. package/dist/esm/utilities/media-prompt.js.map +1 -1
  191. package/dist/esm/utilities/numbers.js +13 -10
  192. package/dist/esm/utilities/numbers.js.map +1 -1
  193. package/dist/esm/utilities/provider-executed.js +20 -11
  194. package/dist/esm/utilities/provider-executed.js.map +1 -1
  195. package/dist/esm/utilities/sampling-keys.js +31 -19
  196. package/dist/esm/utilities/sampling-keys.js.map +1 -1
  197. package/dist/esm/utilities/tool-result.js +42 -30
  198. package/dist/esm/utilities/tool-result.js.map +1 -1
  199. package/dist/esm/utilities/usage.js +27 -9
  200. package/dist/esm/utilities/usage.js.map +1 -1
  201. package/dist/esm/utils.js +26 -18
  202. package/dist/esm/utils.js.map +1 -1
  203. package/package.json +10 -6
  204. package/skills/ai-core/SKILL.md +69 -18
  205. package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
  206. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
  207. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
  208. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
  209. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
  210. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
  211. package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
  212. package/skills/ai-core/chat-experience/SKILL.md +98 -11
  213. package/skills/ai-core/client-persistence/SKILL.md +277 -0
  214. package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
  215. package/skills/ai-core/debug-logging/SKILL.md +1 -1
  216. package/skills/ai-core/locks/SKILL.md +143 -0
  217. package/skills/ai-core/media-generation/SKILL.md +144 -12
  218. package/skills/ai-core/middleware/SKILL.md +258 -33
  219. package/skills/ai-core/structured-outputs/SKILL.md +1 -1
  220. package/skills/ai-core/tool-calling/SKILL.md +54 -61
  221. package/src/activities/chat/agent-loop-strategies.ts +5 -39
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1091 -200
  224. package/src/activities/chat/mcp/manager.ts +4 -4
  225. package/src/activities/chat/mcp/types.ts +2 -2
  226. package/src/activities/chat/messages.ts +5 -3
  227. package/src/activities/chat/middleware/builder.ts +1 -1
  228. package/src/activities/chat/middleware/compose.ts +186 -9
  229. package/src/activities/chat/middleware/index.ts +26 -0
  230. package/src/activities/chat/middleware/locks.ts +102 -0
  231. package/src/activities/chat/middleware/pending-turn.ts +47 -0
  232. package/src/activities/chat/middleware/run-disconnect.ts +62 -0
  233. package/src/activities/chat/middleware/run-store.ts +412 -0
  234. package/src/activities/chat/middleware/types.ts +62 -1
  235. package/src/activities/chat/stream/processor.ts +189 -5
  236. package/src/activities/chat/tools/approval-schema.ts +205 -0
  237. package/src/activities/chat/tools/tool-calls.ts +106 -13
  238. package/src/activities/chat/tools/tool-definition.ts +210 -39
  239. package/src/activities/generateAudio/index.ts +20 -3
  240. package/src/activities/generateImage/index.ts +20 -3
  241. package/src/activities/generateSpeech/index.ts +25 -3
  242. package/src/activities/generateTranscription/index.ts +26 -3
  243. package/src/activities/generateVideo/index.ts +345 -82
  244. package/src/activities/middleware/index.ts +2 -0
  245. package/src/activities/middleware/run.ts +31 -0
  246. package/src/activities/middleware/types.ts +49 -5
  247. package/src/activities/stream-generation-result.ts +30 -2
  248. package/src/activities/summarize/chat-stream-summarize.ts +5 -0
  249. package/src/activities/summarize/index.ts +200 -10
  250. package/src/adapter-internals.ts +10 -1
  251. package/src/client.ts +244 -0
  252. package/src/custom-events.ts +107 -0
  253. package/src/delivery-detach.ts +72 -0
  254. package/src/delivery-disconnect.ts +84 -0
  255. package/src/index.ts +138 -1
  256. package/src/interrupt-resume.ts +824 -0
  257. package/src/interrupt-serialization.ts +183 -0
  258. package/src/interrupts.ts +146 -0
  259. package/src/locks.ts +17 -0
  260. package/src/logger/types.ts +1 -1
  261. package/src/middlewares/otel.ts +1 -0
  262. package/src/realtime/index.ts +5 -9
  263. package/src/scope.ts +47 -0
  264. package/src/stream-durability.ts +598 -0
  265. package/src/stream-to-response.ts +1051 -95
  266. package/src/strip-to-spec-middleware.ts +3 -3
  267. package/src/types.ts +405 -45
  268. package/src/utilities/chat-params.ts +245 -55
  269. package/dist/esm/activities/index.js.map +0 -1
  270. package/dist/esm/adapter-internals.js.map +0 -1
  271. package/dist/esm/index.js.map +0 -1
  272. package/dist/esm/middlewares/index.js.map +0 -1
@@ -0,0 +1,824 @@
1
+ import {
2
+ INTERRUPT_BINDING_VERSION,
3
+ canonicalizeInterruptResolutions,
4
+ } from './interrupts'
5
+ import {
6
+ canonicalInterruptJson,
7
+ digestInterruptJson,
8
+ } from './interrupt-serialization'
9
+ import {
10
+ hashSchemaInput,
11
+ normalizeApprovalSchema,
12
+ } from './activities/chat/tools/approval-schema'
13
+ import {
14
+ isStandardSchema,
15
+ validateWithStandardSchema,
16
+ } from './activities/chat/tools/schema-converter'
17
+ import type {
18
+ InterruptBinding,
19
+ InterruptSubmissionError,
20
+ ItemInterruptErrorCode,
21
+ ToolApprovalResolution,
22
+ UnopenedInterruptBinding,
23
+ } from './interrupts'
24
+ import type {
25
+ ChatMiddlewareConfig,
26
+ ChatResumeToolState,
27
+ } from './activities/chat/middleware/types'
28
+ import type { Interrupt, RunAgentResumeItem } from './types'
29
+
30
+ /**
31
+ * The `Interrupt.metadata` key under which this package's resume binding
32
+ * travels.
33
+ *
34
+ * Exported so anything that produces an interrupt this package must later
35
+ * resume — an application middleware raising a generic pause, a future
36
+ * workflow-to-AG-UI projection — attaches the binding through
37
+ * {@link withInterruptBinding} rather than copying the string. Everything
38
+ * outside this key is the plain AG-UI envelope and is left untouched.
39
+ */
40
+ export const INTERRUPT_BINDING_METADATA_KEY = 'tanstack:interruptBinding'
41
+
42
+ const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
43
+
44
+ /** The persistence-neutral shape required to validate an interrupt resume. */
45
+ export interface PendingInterruptResumeRecord {
46
+ interruptId: string
47
+ payload: unknown
48
+ binding: InterruptBinding
49
+ }
50
+
51
+ export interface ValidateInterruptResumeBatchInput {
52
+ threadId: string
53
+ interruptedRunId: string
54
+ generation: number
55
+ pending: ReadonlyArray<PendingInterruptResumeRecord>
56
+ resume?: ReadonlyArray<RunAgentResumeItem>
57
+ tools: ChatMiddlewareConfig['tools']
58
+ now?: number
59
+ }
60
+
61
+ export interface ValidatedInterruptResumeBatch {
62
+ errors: ReadonlyArray<InterruptSubmissionError>
63
+ resolutions?: ReadonlyArray<RunAgentResumeItem>
64
+ canonicalResolutions?: string
65
+ fingerprint?: string
66
+ resumeToolState?: ChatResumeToolState
67
+ }
68
+
69
+ export class InterruptResumeValidationError extends Error {
70
+ override readonly name = 'InterruptResumeValidationError'
71
+
72
+ constructor(readonly errors: ReadonlyArray<InterruptSubmissionError>) {
73
+ super(errors.map((error) => error.message).join(' '))
74
+ }
75
+ }
76
+
77
+ function objectValue(value: unknown): Record<string, unknown> | null {
78
+ return value && typeof value === 'object'
79
+ ? (value as Record<string, unknown>)
80
+ : null
81
+ }
82
+
83
+ function stringField(
84
+ value: Record<string, unknown>,
85
+ key: string,
86
+ ): string | undefined {
87
+ return typeof value[key] === 'string' ? value[key] : undefined
88
+ }
89
+
90
+ function normalizeIssuePath(
91
+ path: ReadonlyArray<unknown> | undefined,
92
+ ): ReadonlyArray<string | number> | undefined {
93
+ if (!path) return undefined
94
+ return path.map((segment) => {
95
+ if (typeof segment === 'string' || typeof segment === 'number') {
96
+ return segment
97
+ }
98
+ const record = objectValue(segment)
99
+ const key = record?.key
100
+ return typeof key === 'number' ? key : String(key ?? segment)
101
+ })
102
+ }
103
+
104
+ export function interruptItemError(
105
+ input: Pick<
106
+ ValidateInterruptResumeBatchInput,
107
+ 'threadId' | 'interruptedRunId' | 'generation'
108
+ >,
109
+ interruptId: string,
110
+ code: ItemInterruptErrorCode,
111
+ message: string,
112
+ options?: {
113
+ path?: ReadonlyArray<string | number>
114
+ source?: 'client' | 'server'
115
+ retryable?: boolean
116
+ },
117
+ ): InterruptSubmissionError {
118
+ return {
119
+ scope: 'item',
120
+ threadId: input.threadId,
121
+ interruptedRunId: input.interruptedRunId,
122
+ generation: input.generation,
123
+ interruptId,
124
+ code,
125
+ message,
126
+ source: options?.source ?? 'client',
127
+ retryable: options?.retryable ?? false,
128
+ ...(options?.path ? { path: options.path } : {}),
129
+ }
130
+ }
131
+
132
+ async function validateSchemaValue(input: {
133
+ schema: unknown
134
+ value: unknown
135
+ onIssue: (message: string, path?: ReadonlyArray<string | number>) => void
136
+ }): Promise<void> {
137
+ if (isStandardSchema(input.schema)) {
138
+ const result = await validateWithStandardSchema<unknown>(
139
+ input.schema,
140
+ input.value,
141
+ )
142
+ if (!result.success) {
143
+ for (const issue of result.issues) {
144
+ input.onIssue(issue.message, normalizeIssuePath(issue.path))
145
+ }
146
+ }
147
+ return
148
+ }
149
+
150
+ // A non-Standard-Schema value (a raw JSON Schema, e.g. a generic interrupt's
151
+ // wire responseSchema) is not validated by the library. The application
152
+ // validates the resume value itself if it needs to; otherwise it flows
153
+ // through as-is.
154
+ }
155
+
156
+ type RuntimeTool = ChatMiddlewareConfig['tools'][number] & {
157
+ approvalSchema?: Parameters<typeof normalizeApprovalSchema>[0]
158
+ }
159
+
160
+ function runtimeTool(
161
+ tools: ChatMiddlewareConfig['tools'],
162
+ name: string,
163
+ ): RuntimeTool | undefined {
164
+ return tools.find((tool) => tool.name === name) as RuntimeTool | undefined
165
+ }
166
+
167
+ function descriptorResponseSchema(
168
+ record: PendingInterruptResumeRecord,
169
+ ): unknown {
170
+ return objectValue(record.payload)?.responseSchema
171
+ }
172
+
173
+ function schemaHash(schema: unknown): string {
174
+ return digestInterruptJson(canonicalInterruptJson(schema))
175
+ }
176
+
177
+ async function pushSchemaIssues(input: {
178
+ request: ValidateInterruptResumeBatchInput
179
+ errors: Array<InterruptSubmissionError>
180
+ interruptId: string
181
+ schema: unknown
182
+ value: unknown
183
+ code: ItemInterruptErrorCode
184
+ label: string
185
+ }): Promise<void> {
186
+ try {
187
+ await validateSchemaValue({
188
+ schema: input.schema,
189
+ value: input.value,
190
+ onIssue: (message, path) => {
191
+ input.errors.push(
192
+ interruptItemError(
193
+ input.request,
194
+ input.interruptId,
195
+ input.code,
196
+ `${input.label}: ${message}`,
197
+ { path },
198
+ ),
199
+ )
200
+ },
201
+ })
202
+ } catch (error) {
203
+ input.errors.push(
204
+ interruptItemError(
205
+ input.request,
206
+ input.interruptId,
207
+ 'invalid-response-schema',
208
+ `${input.label} could not be validated: ${error instanceof Error ? error.message : String(error)}`,
209
+ { source: 'server' },
210
+ ),
211
+ )
212
+ }
213
+ }
214
+
215
+ function validateDescriptorSchema(
216
+ input: ValidateInterruptResumeBatchInput,
217
+ record: PendingInterruptResumeRecord,
218
+ binding: InterruptBinding,
219
+ errors: Array<InterruptSubmissionError>,
220
+ ): unknown {
221
+ const schema = descriptorResponseSchema(record)
222
+ if (
223
+ schema === undefined ||
224
+ schemaHash(schema) !== binding.responseSchemaHash
225
+ ) {
226
+ errors.push(
227
+ interruptItemError(
228
+ input,
229
+ record.interruptId,
230
+ 'invalid-response-schema',
231
+ `Interrupt ${record.interruptId} response schema no longer matches its binding.`,
232
+ { source: 'server' },
233
+ ),
234
+ )
235
+ }
236
+ return schema
237
+ }
238
+
239
+ /**
240
+ * Validate and translate a complete interrupt batch before any tool executes.
241
+ * Used by ephemeral chat resume; a durable layer may share the same validator.
242
+ */
243
+ export async function validateInterruptResumeBatch(
244
+ input: ValidateInterruptResumeBatchInput,
245
+ ): Promise<ValidatedInterruptResumeBatch> {
246
+ const grouped = new Map<string, Array<InterruptSubmissionError>>()
247
+ const batchErrors: Array<InterruptSubmissionError> = []
248
+ const group = (interruptId: string): Array<InterruptSubmissionError> => {
249
+ const existing = grouped.get(interruptId)
250
+ if (existing) return existing
251
+ const created: Array<InterruptSubmissionError> = []
252
+ grouped.set(interruptId, created)
253
+ return created
254
+ }
255
+ const pendingById = new Map(
256
+ input.pending.map((record) => [record.interruptId, record]),
257
+ )
258
+ const resumeById = new Map<string, RunAgentResumeItem>()
259
+ const counts = new Map<string, number>()
260
+ for (const entry of input.resume ?? []) {
261
+ counts.set(entry.interruptId, (counts.get(entry.interruptId) ?? 0) + 1)
262
+ if (!resumeById.has(entry.interruptId))
263
+ resumeById.set(entry.interruptId, entry)
264
+ }
265
+
266
+ for (const [interruptId, count] of counts) {
267
+ if (count > 1) {
268
+ group(interruptId).push(
269
+ interruptItemError(
270
+ input,
271
+ interruptId,
272
+ 'conflict',
273
+ `Interrupt ${interruptId} has duplicate resume entries.`,
274
+ ),
275
+ )
276
+ }
277
+ }
278
+
279
+ let incomplete = false
280
+ for (const record of input.pending) {
281
+ const errors = group(record.interruptId)
282
+ const entry = resumeById.get(record.interruptId)
283
+ const binding = record.binding
284
+ if (!entry) {
285
+ incomplete = true
286
+ errors.push(
287
+ interruptItemError(
288
+ input,
289
+ record.interruptId,
290
+ 'unknown-interrupt',
291
+ `Missing resume entry for interrupt ${record.interruptId}.`,
292
+ ),
293
+ )
294
+ }
295
+ if (
296
+ binding.interruptedRunId !== input.interruptedRunId ||
297
+ binding.generation !== input.generation ||
298
+ binding.interruptId !== record.interruptId
299
+ ) {
300
+ errors.push(
301
+ interruptItemError(
302
+ input,
303
+ record.interruptId,
304
+ 'stale',
305
+ `Interrupt ${record.interruptId} has stale correlation metadata.`,
306
+ { source: 'server' },
307
+ ),
308
+ )
309
+ }
310
+ if (
311
+ binding.expiresAt !== undefined &&
312
+ Date.parse(binding.expiresAt) <= (input.now ?? Date.now())
313
+ ) {
314
+ errors.push(
315
+ interruptItemError(
316
+ input,
317
+ record.interruptId,
318
+ 'expired',
319
+ `Interrupt ${record.interruptId} has expired.`,
320
+ { source: 'server' },
321
+ ),
322
+ )
323
+ }
324
+
325
+ const responseSchema = validateDescriptorSchema(
326
+ input,
327
+ record,
328
+ binding,
329
+ errors,
330
+ )
331
+ if (!entry) continue
332
+ const entryStatus: unknown = entry.status
333
+ if (entryStatus !== 'resolved' && entryStatus !== 'cancelled') {
334
+ errors.push(
335
+ interruptItemError(
336
+ input,
337
+ record.interruptId,
338
+ 'invalid-payload',
339
+ `Interrupt ${record.interruptId} has invalid status ${String(entryStatus)}.`,
340
+ ),
341
+ )
342
+ continue
343
+ }
344
+ if (binding.kind === 'generic') {
345
+ if (entry.status === 'cancelled') {
346
+ if (entry.payload !== undefined) {
347
+ errors.push(
348
+ interruptItemError(
349
+ input,
350
+ record.interruptId,
351
+ 'invalid-payload',
352
+ `Cancelled interrupt ${record.interruptId} must not include a payload.`,
353
+ ),
354
+ )
355
+ }
356
+ } else if (responseSchema !== undefined) {
357
+ await pushSchemaIssues({
358
+ request: input,
359
+ errors,
360
+ interruptId: record.interruptId,
361
+ schema: responseSchema,
362
+ value: entry.payload,
363
+ code: 'invalid-payload',
364
+ label: `Interrupt ${record.interruptId} payload is invalid`,
365
+ })
366
+ }
367
+ continue
368
+ }
369
+
370
+ const tool = runtimeTool(input.tools, binding.toolName)
371
+ if (!tool) {
372
+ errors.push(
373
+ interruptItemError(
374
+ input,
375
+ record.interruptId,
376
+ 'stale',
377
+ `Tool ${binding.toolName} is unavailable for interrupt ${record.interruptId}.`,
378
+ { source: 'server' },
379
+ ),
380
+ )
381
+ continue
382
+ }
383
+
384
+ let approval: ReturnType<typeof normalizeApprovalSchema> | undefined
385
+ let schemaDrifted = false
386
+ if (binding.kind === 'client-tool-execution') {
387
+ if (hashSchemaInput(tool.outputSchema) !== binding.outputSchemaHash) {
388
+ errors.push(
389
+ interruptItemError(
390
+ input,
391
+ record.interruptId,
392
+ 'stale',
393
+ `Tool ${binding.toolName} output schema has changed.`,
394
+ { source: 'server' },
395
+ ),
396
+ )
397
+ schemaDrifted = true
398
+ }
399
+ } else {
400
+ try {
401
+ approval = normalizeApprovalSchema(
402
+ tool.approvalSchema,
403
+ tool.inputSchema,
404
+ )
405
+ } catch {
406
+ errors.push(
407
+ interruptItemError(
408
+ input,
409
+ record.interruptId,
410
+ 'stale',
411
+ `Tool ${binding.toolName} approval schema is unavailable.`,
412
+ { source: 'server' },
413
+ ),
414
+ )
415
+ schemaDrifted = true
416
+ }
417
+ if (
418
+ approval !== undefined &&
419
+ (hashSchemaInput(tool.inputSchema) !== binding.inputSchemaHash ||
420
+ approval.approvalSchemaHash !== binding.approvalSchemaHash ||
421
+ approval.responseSchemaHash !== binding.responseSchemaHash)
422
+ ) {
423
+ errors.push(
424
+ interruptItemError(
425
+ input,
426
+ record.interruptId,
427
+ 'stale',
428
+ `Tool ${binding.toolName} approval schema has changed.`,
429
+ { source: 'server' },
430
+ ),
431
+ )
432
+ schemaDrifted = true
433
+ }
434
+ }
435
+
436
+ if (entry.status === 'cancelled') {
437
+ if (entry.payload !== undefined) {
438
+ errors.push(
439
+ interruptItemError(
440
+ input,
441
+ record.interruptId,
442
+ 'invalid-payload',
443
+ `Cancelled interrupt ${record.interruptId} must not include a payload.`,
444
+ ),
445
+ )
446
+ }
447
+ continue
448
+ }
449
+ if (schemaDrifted) continue
450
+
451
+ if (binding.kind === 'client-tool-execution') {
452
+ if (responseSchema !== undefined) {
453
+ await pushSchemaIssues({
454
+ request: input,
455
+ errors,
456
+ interruptId: record.interruptId,
457
+ schema: responseSchema,
458
+ value: entry.payload,
459
+ code: 'invalid-tool-output',
460
+ label: `Tool ${binding.toolName} output is invalid`,
461
+ })
462
+ }
463
+ if (tool.outputSchema !== undefined) {
464
+ await pushSchemaIssues({
465
+ request: input,
466
+ errors,
467
+ interruptId: record.interruptId,
468
+ schema: tool.outputSchema,
469
+ value: entry.payload,
470
+ code: 'invalid-tool-output',
471
+ label: `Tool ${binding.toolName} output is invalid`,
472
+ })
473
+ }
474
+ continue
475
+ }
476
+
477
+ if (approval === undefined) continue
478
+ const envelope = objectValue(entry.payload)
479
+ const approved =
480
+ typeof entry.payload === 'boolean'
481
+ ? entry.payload
482
+ : typeof envelope?.approved === 'boolean'
483
+ ? envelope.approved
484
+ : undefined
485
+ if (approved === undefined) {
486
+ errors.push(
487
+ interruptItemError(
488
+ input,
489
+ record.interruptId,
490
+ 'invalid-payload',
491
+ `Approval ${record.interruptId} must be a boolean or decision envelope.`,
492
+ ),
493
+ )
494
+ continue
495
+ }
496
+ if (envelope) {
497
+ await pushSchemaIssues({
498
+ request: input,
499
+ errors,
500
+ interruptId: record.interruptId,
501
+ schema: approval.responseSchema,
502
+ value: entry.payload,
503
+ code: 'invalid-payload',
504
+ label: `Approval ${record.interruptId} envelope is invalid`,
505
+ })
506
+ }
507
+ if (approved && envelope?.editedArgs !== undefined) {
508
+ if (tool.inputSchema === undefined) {
509
+ errors.push(
510
+ interruptItemError(
511
+ input,
512
+ record.interruptId,
513
+ 'invalid-edited-args',
514
+ `Approval ${record.interruptId} cannot edit arguments without an input schema.`,
515
+ ),
516
+ )
517
+ } else {
518
+ await pushSchemaIssues({
519
+ request: input,
520
+ errors,
521
+ interruptId: record.interruptId,
522
+ schema: tool.inputSchema,
523
+ value: envelope.editedArgs,
524
+ code: 'invalid-edited-args',
525
+ label: `Approval ${record.interruptId} edited arguments are invalid`,
526
+ })
527
+ }
528
+ }
529
+ const branch = approved
530
+ ? approval.branches.approve
531
+ : approval.branches.reject
532
+ if (branch) {
533
+ if (!envelope) {
534
+ errors.push(
535
+ interruptItemError(
536
+ input,
537
+ record.interruptId,
538
+ 'invalid-payload',
539
+ `Approval ${record.interruptId} requires a payload for the ${approved ? 'approve' : 'reject'} decision.`,
540
+ ),
541
+ )
542
+ } else {
543
+ await pushSchemaIssues({
544
+ request: input,
545
+ errors,
546
+ interruptId: record.interruptId,
547
+ schema: branch.source,
548
+ value: envelope.payload,
549
+ code: 'invalid-payload',
550
+ label: `Approval ${record.interruptId} payload is invalid`,
551
+ })
552
+ }
553
+ }
554
+ }
555
+
556
+ for (const entry of input.resume ?? []) {
557
+ if (!pendingById.has(entry.interruptId)) {
558
+ incomplete = true
559
+ group(entry.interruptId).push(
560
+ interruptItemError(
561
+ input,
562
+ entry.interruptId,
563
+ 'unknown-interrupt',
564
+ `Resume entry references unknown interrupt ${entry.interruptId}.`,
565
+ ),
566
+ )
567
+ }
568
+ }
569
+
570
+ if (incomplete) {
571
+ batchErrors.push({
572
+ scope: 'batch',
573
+ threadId: input.threadId,
574
+ interruptedRunId: input.interruptedRunId,
575
+ generation: input.generation,
576
+ code: 'incomplete-batch',
577
+ message:
578
+ 'Resume entries must resolve or cancel the complete interrupt batch.',
579
+ source: 'client',
580
+ retryable: false,
581
+ interruptIds: input.pending.map((record) => record.interruptId),
582
+ })
583
+ }
584
+
585
+ const itemErrors = [...grouped.entries()]
586
+ .sort(([left], [right]) => left.localeCompare(right))
587
+ .flatMap(([, errors]) => errors)
588
+ if (itemErrors.length > 0) {
589
+ batchErrors.push({
590
+ scope: 'batch',
591
+ threadId: input.threadId,
592
+ interruptedRunId: input.interruptedRunId,
593
+ generation: input.generation,
594
+ code: 'item-validation-failed',
595
+ message: 'One or more interrupt resolutions are invalid.',
596
+ source: 'client',
597
+ retryable: false,
598
+ interruptIds: input.pending.map((record) => record.interruptId),
599
+ })
600
+ return { errors: [...itemErrors, ...batchErrors] }
601
+ }
602
+
603
+ const canonical = canonicalizeInterruptResolutions(input.resume ?? [])
604
+ const approvals = new Map<string, ToolApprovalResolution>()
605
+ const clientToolResults = new Map<string, unknown>()
606
+ const genericInterrupts = new Map<
607
+ string,
608
+ | { interruptId: string; status: 'resolved'; payload: unknown }
609
+ | { interruptId: string; status: 'cancelled' }
610
+ >()
611
+ const deniedToolResults = new Map<string, unknown>()
612
+ const cancelledToolCallIds = new Set<string>()
613
+
614
+ for (const record of input.pending) {
615
+ const entry = resumeById.get(record.interruptId)
616
+ if (!entry) continue
617
+ const binding = record.binding
618
+ if (binding.kind === 'generic') {
619
+ genericInterrupts.set(
620
+ record.interruptId,
621
+ entry.status === 'resolved'
622
+ ? {
623
+ interruptId: record.interruptId,
624
+ status: 'resolved',
625
+ payload: entry.payload,
626
+ }
627
+ : { interruptId: record.interruptId, status: 'cancelled' },
628
+ )
629
+ continue
630
+ }
631
+ if (entry.status === 'cancelled') {
632
+ cancelledToolCallIds.add(binding.toolCallId)
633
+ continue
634
+ }
635
+ if (binding.kind === 'client-tool-execution') {
636
+ clientToolResults.set(binding.toolCallId, entry.payload)
637
+ continue
638
+ }
639
+ const envelope = objectValue(entry.payload)
640
+ const resolution: ToolApprovalResolution =
641
+ typeof entry.payload === 'boolean'
642
+ ? entry.payload
643
+ : envelope?.approved === true
644
+ ? {
645
+ approved: true,
646
+ ...(envelope.editedArgs !== undefined
647
+ ? { editedArgs: envelope.editedArgs }
648
+ : {}),
649
+ ...(envelope.payload !== undefined
650
+ ? { payload: envelope.payload }
651
+ : {}),
652
+ }
653
+ : {
654
+ approved: false,
655
+ ...(envelope?.payload !== undefined
656
+ ? { payload: envelope.payload }
657
+ : {}),
658
+ }
659
+ approvals.set(binding.toolCallId, resolution)
660
+ if (
661
+ resolution === false ||
662
+ (typeof resolution === 'object' && !resolution.approved)
663
+ ) {
664
+ deniedToolResults.set(
665
+ binding.toolCallId,
666
+ typeof resolution === 'object' ? resolution.payload : undefined,
667
+ )
668
+ }
669
+ }
670
+
671
+ return {
672
+ errors: [],
673
+ resolutions: canonical.resolutions,
674
+ canonicalResolutions: canonical.canonicalResolutions,
675
+ fingerprint: canonical.fingerprint,
676
+ resumeToolState: {
677
+ approvals,
678
+ clientToolResults,
679
+ genericInterrupts,
680
+ deniedToolResults,
681
+ cancelledToolCallIds,
682
+ },
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Is this a binding written by a version of the protocol we understand?
688
+ *
689
+ * A missing `v` is read as {@link INTERRUPT_BINDING_VERSION} so bindings
690
+ * written before the field existed still resume. A `v` we don't recognise is
691
+ * rejected outright — a newer or foreign producer's binding must not be
692
+ * duck-typed into ours.
693
+ */
694
+ function isSupportedBindingVersion(raw: Record<string, unknown>): boolean {
695
+ const version = raw['v']
696
+ if (version === undefined) return true
697
+ return version === INTERRUPT_BINDING_VERSION
698
+ }
699
+
700
+ export function readUnopenedInterruptBinding(
701
+ descriptor: Interrupt,
702
+ ): UnopenedInterruptBinding | undefined {
703
+ const metadata = objectValue(descriptor.metadata)
704
+ const raw = metadata
705
+ ? objectValue(metadata[interruptBindingMetadataKey])
706
+ : null
707
+ if (!raw || stringField(raw, 'interruptId') !== descriptor.id)
708
+ return undefined
709
+ if (!isSupportedBindingVersion(raw)) return undefined
710
+ const kind = stringField(raw, 'kind')
711
+ const interruptId = stringField(raw, 'interruptId')
712
+ const responseSchemaHash = stringField(raw, 'responseSchemaHash')
713
+ const expiresAt = stringField(raw, 'expiresAt')
714
+ if (!interruptId || !responseSchemaHash) return undefined
715
+ const v = INTERRUPT_BINDING_VERSION
716
+ if (kind === 'generic') {
717
+ return {
718
+ v,
719
+ kind,
720
+ interruptId,
721
+ responseSchemaHash,
722
+ ...(expiresAt ? { expiresAt } : {}),
723
+ }
724
+ }
725
+ const toolName = stringField(raw, 'toolName')
726
+ const toolCallId = stringField(raw, 'toolCallId')
727
+ if (!toolName || !toolCallId) return undefined
728
+ if (kind === 'client-tool-execution') {
729
+ const outputSchemaHash = stringField(raw, 'outputSchemaHash')
730
+ if (!outputSchemaHash) return undefined
731
+ return {
732
+ v,
733
+ kind,
734
+ interruptId,
735
+ toolName,
736
+ toolCallId,
737
+ outputSchemaHash,
738
+ responseSchemaHash,
739
+ ...(expiresAt ? { expiresAt } : {}),
740
+ }
741
+ }
742
+ if (kind === 'tool-approval') {
743
+ const inputSchemaHash = stringField(raw, 'inputSchemaHash')
744
+ const approvalSchemaHash = stringField(raw, 'approvalSchemaHash')
745
+ if (!inputSchemaHash || !approvalSchemaHash) return undefined
746
+ return {
747
+ v,
748
+ kind,
749
+ interruptId,
750
+ toolName,
751
+ toolCallId,
752
+ originalArgs: raw.originalArgs,
753
+ inputSchemaHash,
754
+ approvalSchemaHash,
755
+ responseSchemaHash,
756
+ ...(expiresAt ? { expiresAt } : {}),
757
+ }
758
+ }
759
+ return undefined
760
+ }
761
+
762
+ /**
763
+ * Attach a resume binding to an interrupt descriptor, under
764
+ * {@link INTERRUPT_BINDING_METADATA_KEY}.
765
+ *
766
+ * This is the supported way to make an interrupt resumable by this package.
767
+ * The descriptor keeps its AG-UI shape; only `metadata` gains the namespaced
768
+ * key. Pass the unopened form (no `interruptedRunId` / `generation`) when
769
+ * emitting from inside a run — those fields are stamped as the run finishes.
770
+ */
771
+ export function withInterruptBinding(
772
+ descriptor: Interrupt,
773
+ binding: UnopenedInterruptBinding | InterruptBinding,
774
+ ): Interrupt {
775
+ return {
776
+ ...descriptor,
777
+ metadata: {
778
+ ...descriptor.metadata,
779
+ [interruptBindingMetadataKey]: {
780
+ ...binding,
781
+ v: INTERRUPT_BINDING_VERSION,
782
+ interruptId: descriptor.id,
783
+ },
784
+ },
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Read the opened resume binding off a descriptor, or `undefined` when the
790
+ * descriptor carries no binding of a version we understand.
791
+ *
792
+ * `undefined` means "this interrupt is not ours to resume" — it is not a
793
+ * failure to recover from by inventing a binding.
794
+ */
795
+ export function readInterruptBinding(
796
+ descriptor: Interrupt,
797
+ ): InterruptBinding | undefined {
798
+ const unopened = readUnopenedInterruptBinding(descriptor)
799
+ if (!unopened) return undefined
800
+ const metadata = objectValue(descriptor.metadata)
801
+ const raw = metadata
802
+ ? objectValue(metadata[interruptBindingMetadataKey])
803
+ : null
804
+ if (!raw) return undefined
805
+ const interruptedRunId = stringField(raw, 'interruptedRunId')
806
+ const generation = raw['generation']
807
+ if (
808
+ !interruptedRunId ||
809
+ typeof generation !== 'number' ||
810
+ !Number.isInteger(generation) ||
811
+ generation < 0
812
+ ) {
813
+ return undefined
814
+ }
815
+ return { ...unopened, interruptedRunId, generation }
816
+ }
817
+
818
+ export function withoutInterruptBinding(descriptor: Interrupt): Interrupt {
819
+ const metadata = objectValue(descriptor.metadata)
820
+ if (!metadata || !(interruptBindingMetadataKey in metadata)) return descriptor
821
+ const publicMetadata = { ...metadata }
822
+ delete publicMetadata[interruptBindingMetadataKey]
823
+ return { ...descriptor, metadata: publicMetadata }
824
+ }