@tanstack/ai 0.41.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 +10 -4
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -17
  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 -16
  11. package/dist/esm/activities/chat/index.js +2100 -1744
  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 +23 -5
  134. package/dist/esm/index.js +30 -97
  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 +332 -21
  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 +156 -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 -59
  221. package/src/activities/chat/agent-loop-strategies.ts +10 -4
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1152 -153
  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 -0
  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 +416 -24
  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
@@ -50,6 +50,7 @@ import type {
50
50
  } from './types'
51
51
  import type {
52
52
  ContentPart,
53
+ Interrupt,
53
54
  MessagePart,
54
55
  ModelMessage,
55
56
  StreamChunk,
@@ -917,6 +918,12 @@ export class StreamProcessor {
917
918
  * `tool-call` part is absent from the snapshot, carry the `tool-call` part
918
919
  * forward from the pre-snapshot state (state and output untouched) so a
919
920
  * subsequent `addToolResult(toolCallId)` can still locate the call.
921
+ *
922
+ * Post-pass (c): AG-UI wire snapshots rebuild `tool-call` parts as
923
+ * `input-complete` without `output` (ModelMessage has no result field on
924
+ * the call). After anchoring results, copy each `tool-result` onto its
925
+ * matching `tool-call` (and prefer pre-snapshot complete/output when the
926
+ * snapshot is poorer) so server tools keep the same UI shape as client tools.
920
927
  */
921
928
  private reconcileSnapshotToolCalls(
922
929
  snapshot: Array<UIMessage>,
@@ -1016,7 +1023,87 @@ export class StreamProcessor {
1016
1023
  target.parts = parts
1017
1024
  }
1018
1025
 
1019
- return reconciled
1026
+ return this.enrichSnapshotToolCallsFromResults(reconciled, prevToolCalls)
1027
+ }
1028
+
1029
+ /**
1030
+ * Post-pass (c): fold `tool-result` content into sibling `tool-call` parts
1031
+ * and prefer pre-snapshot complete/output when the snapshot rebuilt a
1032
+ * poorer `input-complete` call (AG-UI ModelMessage has no result on calls).
1033
+ */
1034
+ private enrichSnapshotToolCallsFromResults(
1035
+ messages: Array<UIMessage>,
1036
+ prevToolCalls: Map<string, ToolCallPart>,
1037
+ ): Array<UIMessage> {
1038
+ const resultsByCallId = new Map<string, ToolResultPart>()
1039
+ for (const msg of messages) {
1040
+ for (const part of msg.parts) {
1041
+ if (part.type === 'tool-result') {
1042
+ resultsByCallId.set(part.toolCallId, part)
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ return messages.map((msg) => {
1048
+ const parts = msg.parts.map((part) => {
1049
+ if (part.type !== 'tool-call') return part
1050
+
1051
+ const prev = prevToolCalls.get(part.id)
1052
+ const result = resultsByCallId.get(part.id)
1053
+ let next: ToolCallPart = part
1054
+
1055
+ // Prefer a pre-snapshot call that already carried output/complete —
1056
+ // the client observed TOOL_CALL_END/RESULT before the snapshot wipe.
1057
+ if (
1058
+ prev &&
1059
+ (prev.output !== undefined ||
1060
+ prev.state === 'complete' ||
1061
+ prev.state === 'error') &&
1062
+ (part.output === undefined ||
1063
+ part.state === 'input-complete' ||
1064
+ part.state === 'input-streaming' ||
1065
+ part.state === 'awaiting-input')
1066
+ ) {
1067
+ next = {
1068
+ ...part,
1069
+ ...(prev.output !== undefined ? { output: prev.output } : {}),
1070
+ state: prev.state,
1071
+ ...(prev.approval !== undefined ? { approval: prev.approval } : {}),
1072
+ ...(prev.metadata !== undefined ? { metadata: prev.metadata } : {}),
1073
+ }
1074
+ }
1075
+
1076
+ // Apply sibling tool-result when the call still has no output.
1077
+ if (result && next.output === undefined) {
1078
+ let output: unknown
1079
+ if (Array.isArray(result.content)) {
1080
+ output = result.content
1081
+ } else {
1082
+ try {
1083
+ output = JSON.parse(result.content)
1084
+ } catch {
1085
+ output = result.content
1086
+ }
1087
+ }
1088
+ const errorText =
1089
+ result.state === 'error'
1090
+ ? this.extractToolResultError(output)
1091
+ : undefined
1092
+ next = {
1093
+ ...next,
1094
+ output: errorText ? { error: errorText } : output,
1095
+ state: result.state === 'error' ? 'error' : 'complete',
1096
+ }
1097
+ }
1098
+
1099
+ return next
1100
+ })
1101
+ // Rebuild only when a part object identity changed.
1102
+ const partsChanged = parts.some(
1103
+ (part, index) => part !== msg.parts[index],
1104
+ )
1105
+ return partsChanged ? { ...msg, parts } : msg
1106
+ })
1020
1107
  }
1021
1108
 
1022
1109
  /**
@@ -1449,6 +1536,10 @@ export class StreamProcessor {
1449
1536
  this.finishReason = chunk.finishReason ?? null
1450
1537
  this.activeRuns.delete(chunk.runId)
1451
1538
 
1539
+ if (chunk.outcome?.type === 'interrupt') {
1540
+ this.handleInterrupts(chunk.outcome.interrupts)
1541
+ }
1542
+
1452
1543
  if (this.activeRuns.size === 0) {
1453
1544
  this.isDone = true
1454
1545
  this.completeAllToolCalls()
@@ -1456,6 +1547,74 @@ export class StreamProcessor {
1456
1547
  }
1457
1548
  }
1458
1549
 
1550
+ private handleInterrupts(interrupts: Array<Interrupt>): void {
1551
+ for (const interrupt of interrupts) {
1552
+ const metadata =
1553
+ interrupt.metadata && typeof interrupt.metadata === 'object'
1554
+ ? interrupt.metadata
1555
+ : {}
1556
+ const kind = typeof metadata.kind === 'string' ? metadata.kind : undefined
1557
+ const toolCallId = interrupt.toolCallId
1558
+ if (!toolCallId) continue
1559
+
1560
+ const toolName =
1561
+ typeof metadata.toolName === 'string'
1562
+ ? metadata.toolName
1563
+ : this.findToolCallName(toolCallId)
1564
+ const input = Object.hasOwn(metadata, 'input') ? metadata.input : {}
1565
+
1566
+ if (kind === 'approval' || interrupt.reason === 'approval_required') {
1567
+ // An interrupt terminal arrives after MESSAGES_SNAPSHOT, which may have
1568
+ // rebuilt the message list without the live active-message/tool-call
1569
+ // maps. Fall back to locating the assistant message that actually owns
1570
+ // the tool-call part so the approval UI (part.state) still updates.
1571
+ const resolvedMessageId =
1572
+ this.getActiveAssistantMessageId() ??
1573
+ this.toolCallToMessage.get(toolCallId) ??
1574
+ this.messages.find(
1575
+ (m) =>
1576
+ m.role === 'assistant' &&
1577
+ m.parts.some(
1578
+ (p) => p.type === 'tool-call' && p.id === toolCallId,
1579
+ ),
1580
+ )?.id
1581
+ if (resolvedMessageId) {
1582
+ this.messages = updateToolCallApproval(
1583
+ this.messages,
1584
+ resolvedMessageId,
1585
+ toolCallId,
1586
+ interrupt.id,
1587
+ )
1588
+ this.emitMessagesChange()
1589
+ }
1590
+
1591
+ this.events.onApprovalRequest?.({
1592
+ toolCallId,
1593
+ toolName,
1594
+ input,
1595
+ approvalId: interrupt.id,
1596
+ })
1597
+ continue
1598
+ }
1599
+
1600
+ if (kind === 'client_tool' || interrupt.reason === 'client_tool_input') {
1601
+ this.events.onToolCall?.({
1602
+ toolCallId,
1603
+ toolName,
1604
+ input,
1605
+ })
1606
+ }
1607
+ }
1608
+ }
1609
+
1610
+ private findToolCallName(toolCallId: string): string {
1611
+ for (const state of this.messageStates.values()) {
1612
+ const toolCall = state.toolCalls.get(toolCallId)
1613
+ if (toolCall) return toolCall.name
1614
+ }
1615
+ return ''
1616
+ }
1617
+
1459
1618
  /**
1460
1619
  * Handle RUN_ERROR event
1461
1620
  */
@@ -1676,10 +1835,15 @@ export class StreamProcessor {
1676
1835
  /**
1677
1836
  * Handle CUSTOM event.
1678
1837
  *
1679
- * Handles special custom events emitted by the TextEngine (not adapters):
1680
- * - 'tool-input-available': Client tool needs execution. Fires onToolCall.
1681
- * - 'approval-requested': Tool needs user approval. Updates tool-call part
1682
- * state and fires onApprovalRequest.
1838
+ * Handles custom events consumed by the processor:
1839
+ * - 'tool-input-available': Legacy/replay-compatible input for client tool
1840
+ * execution. Fires onToolCall.
1841
+ * - 'approval-requested': Legacy/replay-compatible input for tool approval.
1842
+ * Updates tool-call part state and fires onApprovalRequest.
1843
+ *
1844
+ * Current core streams represent user-actionable waits through
1845
+ * RUN_FINISHED.outcome.type === 'interrupt'; these custom events are not the
1846
+ * source of truth for new emissions.
1683
1847
  *
1684
1848
  * @see docs/chat-architecture.md#client-tools-and-approval-flows — Full flow details
1685
1849
  */
@@ -1910,6 +2074,13 @@ export class StreamProcessor {
1910
2074
  return
1911
2075
  }
1912
2076
 
2077
+ // RUN_FINISHED interrupt handling can mark the rendered part as waiting on
2078
+ // user action before the completion safety net runs. Keep the parsed
2079
+ // argument bookkeeping above, but do not downgrade that visible state.
2080
+ if (this.isToolCallPartAwaitingUserAction(toolCall.id)) {
2081
+ return
2082
+ }
2083
+
1913
2084
  // Update UIMessage. The arguments are complete now, so surface the parsed
1914
2085
  // input on the part. For adapters that skip TOOL_CALL_ARGS the arguments
1915
2086
  // string was back-filled from TOOL_CALL_END.input, so this parse matches
@@ -1937,6 +2108,19 @@ export class StreamProcessor {
1937
2108
  )
1938
2109
  }
1939
2110
 
2111
+ private isToolCallPartAwaitingUserAction(toolCallId: string): boolean {
2112
+ return this.messages.some((msg) =>
2113
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- `parts` is typed as required, but seeded ModelMessage-shaped messages can lack it at runtime.
2114
+ msg.parts?.some(
2115
+ (part) =>
2116
+ part.type === 'tool-call' &&
2117
+ part.id === toolCallId &&
2118
+ (part.state === 'approval-requested' ||
2119
+ part.state === 'approval-responded'),
2120
+ ),
2121
+ )
2122
+ }
2123
+
1940
2124
  /**
1941
2125
  * Whether the rendered tool-call part for the given id has reached the
1942
2126
  * terminal 'error' state. Used to prevent the completion safety net from
@@ -0,0 +1,205 @@
1
+ import {
2
+ canonicalInterruptJson,
3
+ digestInterruptJson,
4
+ } from '../../../interrupt-serialization'
5
+ import { isStandardJSONSchema, isStandardSchema } from './schema-converter'
6
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
7
+ import type { JSONSchema, SchemaInput } from '../../../types'
8
+ import type { ApprovalSchemaConfig } from './tool-definition'
9
+
10
+ export interface NormalizedSchemaInput {
11
+ source: SchemaInput
12
+ validator?: StandardSchemaV1
13
+ jsonSchema?: JSONSchema
14
+ }
15
+
16
+ export interface NormalizedApprovalSchema {
17
+ branches: {
18
+ approve: NormalizedSchemaInput | null
19
+ reject: NormalizedSchemaInput | null
20
+ }
21
+ responseSchema: JSONSchema
22
+ responseSchemaHash: string
23
+ approvalSchemaHash: string
24
+ }
25
+
26
+ const jsonSchemaKeywords = new Set([
27
+ '$schema',
28
+ '$id',
29
+ '$ref',
30
+ '$defs',
31
+ 'type',
32
+ 'properties',
33
+ 'required',
34
+ 'additionalProperties',
35
+ 'items',
36
+ 'oneOf',
37
+ 'anyOf',
38
+ 'allOf',
39
+ 'enum',
40
+ 'const',
41
+ 'format',
42
+ 'minimum',
43
+ 'maximum',
44
+ 'minLength',
45
+ 'maxLength',
46
+ 'pattern',
47
+ ])
48
+
49
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
50
+ return (
51
+ value !== null &&
52
+ typeof value === 'object' &&
53
+ !Array.isArray(value) &&
54
+ (Object.getPrototypeOf(value) === Object.prototype ||
55
+ Object.getPrototypeOf(value) === null)
56
+ )
57
+ }
58
+
59
+ function isRawJsonSchema(value: unknown): value is JSONSchema {
60
+ return (
61
+ isPlainRecord(value) &&
62
+ Object.keys(value).some((key) => jsonSchemaKeywords.has(key))
63
+ )
64
+ }
65
+
66
+ function isSchemaInput(value: unknown): value is SchemaInput {
67
+ return (
68
+ isStandardSchema(value) ||
69
+ isStandardJSONSchema(value) ||
70
+ isRawJsonSchema(value)
71
+ )
72
+ }
73
+
74
+ function isApprovalBranchMap(
75
+ value: unknown,
76
+ ): value is { approve?: SchemaInput; reject?: SchemaInput } {
77
+ if (!isPlainRecord(value)) return false
78
+ const keys = Object.keys(value)
79
+ return (
80
+ keys.length > 0 &&
81
+ keys.every((key) => key === 'approve' || key === 'reject') &&
82
+ keys.every((key) => isSchemaInput(value[key]))
83
+ )
84
+ }
85
+
86
+ function toJsonSchema(value: Record<string, unknown>): JSONSchema {
87
+ const result: JSONSchema = {}
88
+ for (const [key, item] of Object.entries(value)) {
89
+ result[key] = item
90
+ }
91
+ return result
92
+ }
93
+
94
+ function schemaToWire(schema: SchemaInput): NormalizedSchemaInput {
95
+ if (isStandardSchema(schema)) {
96
+ const jsonSchema = isStandardJSONSchema(schema)
97
+ ? toJsonSchema(
98
+ schema['~standard'].jsonSchema.input({
99
+ target: 'draft-2020-12',
100
+ }),
101
+ )
102
+ : undefined
103
+ return {
104
+ source: schema,
105
+ validator: schema,
106
+ ...(jsonSchema !== undefined && { jsonSchema }),
107
+ }
108
+ }
109
+ if (isRawJsonSchema(schema)) {
110
+ // The library does not compile or validate raw JSON Schema; it is carried
111
+ // on the wire and hashed as-is. Validation is the application's job.
112
+ return { source: schema, jsonSchema: schema }
113
+ }
114
+ throw new TypeError('Expected a supported SchemaInput.')
115
+ }
116
+
117
+ function decisionEnvelope(input: {
118
+ approved: boolean
119
+ payload: NormalizedSchemaInput | null
120
+ inputSchema: NormalizedSchemaInput | null
121
+ }): JSONSchema {
122
+ const properties: Record<string, JSONSchema> = {
123
+ approved: { const: input.approved },
124
+ }
125
+ const required = ['approved']
126
+ if (input.approved && input.inputSchema) {
127
+ properties['editedArgs'] = input.inputSchema.jsonSchema ?? {}
128
+ }
129
+ if (input.payload) {
130
+ properties['payload'] = input.payload.jsonSchema ?? {}
131
+ required.push('payload')
132
+ }
133
+ return {
134
+ type: 'object',
135
+ properties,
136
+ required,
137
+ additionalProperties: false,
138
+ }
139
+ }
140
+
141
+ export function normalizeApprovalSchema(
142
+ approvalSchema: ApprovalSchemaConfig | undefined,
143
+ inputSchema?: SchemaInput,
144
+ ): NormalizedApprovalSchema {
145
+ const normalizedInput =
146
+ inputSchema === undefined ? null : schemaToWire(inputSchema)
147
+ let approve: NormalizedSchemaInput | null = null
148
+ let reject: NormalizedSchemaInput | null = null
149
+
150
+ if (approvalSchema !== undefined) {
151
+ if (isStandardSchema(approvalSchema) || isRawJsonSchema(approvalSchema)) {
152
+ approve = schemaToWire(approvalSchema)
153
+ reject = approve
154
+ } else if (isApprovalBranchMap(approvalSchema)) {
155
+ approve =
156
+ approvalSchema.approve === undefined
157
+ ? null
158
+ : schemaToWire(approvalSchema.approve)
159
+ reject =
160
+ approvalSchema.reject === undefined
161
+ ? null
162
+ : schemaToWire(approvalSchema.reject)
163
+ } else {
164
+ throw new TypeError(
165
+ 'approvalSchema must be a SchemaInput or a nonempty map containing approve or reject.',
166
+ )
167
+ }
168
+ }
169
+
170
+ const responseSchema: JSONSchema = {
171
+ oneOf: [
172
+ decisionEnvelope({
173
+ approved: true,
174
+ payload: approve,
175
+ inputSchema: normalizedInput,
176
+ }),
177
+ decisionEnvelope({
178
+ approved: false,
179
+ payload: reject,
180
+ inputSchema: null,
181
+ }),
182
+ ],
183
+ }
184
+ const responseCanonical = canonicalInterruptJson(responseSchema)
185
+ const approvalCanonical = canonicalInterruptJson({
186
+ approve: approve?.jsonSchema ?? null,
187
+ reject: reject?.jsonSchema ?? null,
188
+ })
189
+
190
+ return {
191
+ branches: { approve, reject },
192
+ responseSchema,
193
+ responseSchemaHash: digestInterruptJson(responseCanonical),
194
+ approvalSchemaHash: digestInterruptJson(approvalCanonical),
195
+ }
196
+ }
197
+
198
+ export function hashSchemaInput(schema: SchemaInput | undefined): string {
199
+ if (schema === undefined) return digestInterruptJson('undefined')
200
+ const normalized = schemaToWire(schema)
201
+ const identity = normalized.jsonSchema ?? {
202
+ standardValidator: 'unserialized',
203
+ }
204
+ return digestInterruptJson(canonicalInterruptJson(identity))
205
+ }