@tanstack/ai 0.35.0 → 0.36.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 (208) hide show
  1. package/package.json +2 -2
  2. package/skills/ai-core/tool-calling/SKILL.md +26 -1
  3. package/src/activities/chat/index.ts +8 -0
  4. package/src/activities/chat/tools/lazy-tool-manager.ts +12 -4
  5. package/src/activities/chat/tools/lazy-tools.ts +33 -0
  6. package/src/index.ts +5 -0
  7. package/src/types.ts +23 -1
  8. package/dist/esm/activities/chat/adapter.d.ts +0 -163
  9. package/dist/esm/activities/chat/adapter.js +0 -17
  10. package/dist/esm/activities/chat/adapter.js.map +0 -1
  11. package/dist/esm/activities/chat/agent-loop-strategies.d.ts +0 -59
  12. package/dist/esm/activities/chat/agent-loop-strategies.js +0 -23
  13. package/dist/esm/activities/chat/agent-loop-strategies.js.map +0 -1
  14. package/dist/esm/activities/chat/index.d.ts +0 -264
  15. package/dist/esm/activities/chat/index.js +0 -1723
  16. package/dist/esm/activities/chat/index.js.map +0 -1
  17. package/dist/esm/activities/chat/mcp/manager.d.ts +0 -25
  18. package/dist/esm/activities/chat/mcp/manager.js +0 -71
  19. package/dist/esm/activities/chat/mcp/manager.js.map +0 -1
  20. package/dist/esm/activities/chat/mcp/types.d.ts +0 -56
  21. package/dist/esm/activities/chat/messages.d.ts +0 -78
  22. package/dist/esm/activities/chat/messages.js +0 -374
  23. package/dist/esm/activities/chat/messages.js.map +0 -1
  24. package/dist/esm/activities/chat/middleware/builder.d.ts +0 -46
  25. package/dist/esm/activities/chat/middleware/builder.js +0 -17
  26. package/dist/esm/activities/chat/middleware/builder.js.map +0 -1
  27. package/dist/esm/activities/chat/middleware/capabilities.d.ts +0 -93
  28. package/dist/esm/activities/chat/middleware/capabilities.js +0 -45
  29. package/dist/esm/activities/chat/middleware/capabilities.js.map +0 -1
  30. package/dist/esm/activities/chat/middleware/compose.d.ts +0 -87
  31. package/dist/esm/activities/chat/middleware/compose.js +0 -510
  32. package/dist/esm/activities/chat/middleware/compose.js.map +0 -1
  33. package/dist/esm/activities/chat/middleware/define.d.ts +0 -20
  34. package/dist/esm/activities/chat/middleware/define.js +0 -7
  35. package/dist/esm/activities/chat/middleware/define.js.map +0 -1
  36. package/dist/esm/activities/chat/middleware/index.d.ts +0 -10
  37. package/dist/esm/activities/chat/middleware/tool-cache-middleware.d.ts +0 -89
  38. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +0 -76
  39. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +0 -1
  40. package/dist/esm/activities/chat/middleware/types.d.ts +0 -405
  41. package/dist/esm/activities/chat/middleware/validate.d.ts +0 -19
  42. package/dist/esm/activities/chat/middleware/validate.js +0 -30
  43. package/dist/esm/activities/chat/middleware/validate.js.map +0 -1
  44. package/dist/esm/activities/chat/runtime-context-types.d.ts +0 -43
  45. package/dist/esm/activities/chat/stream/index.d.ts +0 -11
  46. package/dist/esm/activities/chat/stream/json-parser.d.ts +0 -38
  47. package/dist/esm/activities/chat/stream/json-parser.js +0 -28
  48. package/dist/esm/activities/chat/stream/json-parser.js.map +0 -1
  49. package/dist/esm/activities/chat/stream/message-updaters.d.ts +0 -81
  50. package/dist/esm/activities/chat/stream/message-updaters.js +0 -253
  51. package/dist/esm/activities/chat/stream/message-updaters.js.map +0 -1
  52. package/dist/esm/activities/chat/stream/processor.d.ts +0 -439
  53. package/dist/esm/activities/chat/stream/processor.js +0 -1426
  54. package/dist/esm/activities/chat/stream/processor.js.map +0 -1
  55. package/dist/esm/activities/chat/stream/strategies.d.ts +0 -43
  56. package/dist/esm/activities/chat/stream/strategies.js +0 -54
  57. package/dist/esm/activities/chat/stream/strategies.js.map +0 -1
  58. package/dist/esm/activities/chat/stream/types.d.ts +0 -90
  59. package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +0 -81
  60. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +0 -188
  61. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +0 -1
  62. package/dist/esm/activities/chat/tools/schema-converter.d.ts +0 -140
  63. package/dist/esm/activities/chat/tools/schema-converter.js +0 -167
  64. package/dist/esm/activities/chat/tools/schema-converter.js.map +0 -1
  65. package/dist/esm/activities/chat/tools/tool-calls.d.ts +0 -140
  66. package/dist/esm/activities/chat/tools/tool-calls.js +0 -512
  67. package/dist/esm/activities/chat/tools/tool-calls.js.map +0 -1
  68. package/dist/esm/activities/chat/tools/tool-definition.d.ts +0 -135
  69. package/dist/esm/activities/chat/tools/tool-definition.js +0 -25
  70. package/dist/esm/activities/chat/tools/tool-definition.js.map +0 -1
  71. package/dist/esm/activities/error-payload.d.ts +0 -30
  72. package/dist/esm/activities/error-payload.js +0 -54
  73. package/dist/esm/activities/error-payload.js.map +0 -1
  74. package/dist/esm/activities/generateAudio/adapter.d.ts +0 -62
  75. package/dist/esm/activities/generateAudio/adapter.js +0 -16
  76. package/dist/esm/activities/generateAudio/adapter.js.map +0 -1
  77. package/dist/esm/activities/generateAudio/index.d.ts +0 -85
  78. package/dist/esm/activities/generateAudio/index.js +0 -114
  79. package/dist/esm/activities/generateAudio/index.js.map +0 -1
  80. package/dist/esm/activities/generateImage/adapter.d.ts +0 -78
  81. package/dist/esm/activities/generateImage/adapter.js +0 -16
  82. package/dist/esm/activities/generateImage/adapter.js.map +0 -1
  83. package/dist/esm/activities/generateImage/index.d.ts +0 -134
  84. package/dist/esm/activities/generateImage/index.js +0 -121
  85. package/dist/esm/activities/generateImage/index.js.map +0 -1
  86. package/dist/esm/activities/generateSpeech/adapter.d.ts +0 -62
  87. package/dist/esm/activities/generateSpeech/adapter.js +0 -16
  88. package/dist/esm/activities/generateSpeech/adapter.js.map +0 -1
  89. package/dist/esm/activities/generateSpeech/index.d.ts +0 -96
  90. package/dist/esm/activities/generateSpeech/index.js +0 -119
  91. package/dist/esm/activities/generateSpeech/index.js.map +0 -1
  92. package/dist/esm/activities/generateTranscription/adapter.d.ts +0 -62
  93. package/dist/esm/activities/generateTranscription/adapter.js +0 -16
  94. package/dist/esm/activities/generateTranscription/adapter.js.map +0 -1
  95. package/dist/esm/activities/generateTranscription/index.d.ts +0 -109
  96. package/dist/esm/activities/generateTranscription/index.js +0 -109
  97. package/dist/esm/activities/generateTranscription/index.js.map +0 -1
  98. package/dist/esm/activities/generateVideo/adapter.d.ts +0 -145
  99. package/dist/esm/activities/generateVideo/adapter.js +0 -30
  100. package/dist/esm/activities/generateVideo/adapter.js.map +0 -1
  101. package/dist/esm/activities/generateVideo/index.d.ts +0 -223
  102. package/dist/esm/activities/generateVideo/index.js +0 -295
  103. package/dist/esm/activities/generateVideo/index.js.map +0 -1
  104. package/dist/esm/activities/generateVideo/snap.d.ts +0 -14
  105. package/dist/esm/activities/generateVideo/snap.js +0 -54
  106. package/dist/esm/activities/generateVideo/snap.js.map +0 -1
  107. package/dist/esm/activities/index.d.ts +0 -27
  108. package/dist/esm/activities/index.js +0 -43
  109. package/dist/esm/activities/index.js.map +0 -1
  110. package/dist/esm/activities/middleware/index.d.ts +0 -2
  111. package/dist/esm/activities/middleware/run.d.ts +0 -20
  112. package/dist/esm/activities/middleware/run.js +0 -42
  113. package/dist/esm/activities/middleware/run.js.map +0 -1
  114. package/dist/esm/activities/middleware/types.d.ts +0 -118
  115. package/dist/esm/activities/stream-generation-result.d.ts +0 -15
  116. package/dist/esm/activities/stream-generation-result.js +0 -49
  117. package/dist/esm/activities/stream-generation-result.js.map +0 -1
  118. package/dist/esm/activities/summarize/adapter.d.ts +0 -74
  119. package/dist/esm/activities/summarize/adapter.js +0 -16
  120. package/dist/esm/activities/summarize/adapter.js.map +0 -1
  121. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +0 -45
  122. package/dist/esm/activities/summarize/chat-stream-summarize.js +0 -212
  123. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +0 -1
  124. package/dist/esm/activities/summarize/index.d.ts +0 -108
  125. package/dist/esm/activities/summarize/index.js +0 -113
  126. package/dist/esm/activities/summarize/index.js.map +0 -1
  127. package/dist/esm/adapter-internals.d.ts +0 -5
  128. package/dist/esm/adapter-internals.js +0 -10
  129. package/dist/esm/adapter-internals.js.map +0 -1
  130. package/dist/esm/client.d.ts +0 -44
  131. package/dist/esm/client.js +0 -67
  132. package/dist/esm/client.js.map +0 -1
  133. package/dist/esm/extend-adapter.d.ts +0 -152
  134. package/dist/esm/extend-adapter.js +0 -21
  135. package/dist/esm/extend-adapter.js.map +0 -1
  136. package/dist/esm/index.d.ts +0 -44
  137. package/dist/esm/index.js +0 -106
  138. package/dist/esm/index.js.map +0 -1
  139. package/dist/esm/logger/console-logger.d.ts +0 -29
  140. package/dist/esm/logger/console-logger.js +0 -83
  141. package/dist/esm/logger/console-logger.js.map +0 -1
  142. package/dist/esm/logger/internal-logger.d.ts +0 -41
  143. package/dist/esm/logger/internal-logger.js +0 -86
  144. package/dist/esm/logger/internal-logger.js.map +0 -1
  145. package/dist/esm/logger/resolve.d.ts +0 -14
  146. package/dist/esm/logger/resolve.js +0 -54
  147. package/dist/esm/logger/resolve.js.map +0 -1
  148. package/dist/esm/logger/types.d.ts +0 -75
  149. package/dist/esm/middlewares/content-guard.d.ts +0 -77
  150. package/dist/esm/middlewares/content-guard.js +0 -156
  151. package/dist/esm/middlewares/content-guard.js.map +0 -1
  152. package/dist/esm/middlewares/index.d.ts +0 -2
  153. package/dist/esm/middlewares/index.js +0 -7
  154. package/dist/esm/middlewares/index.js.map +0 -1
  155. package/dist/esm/middlewares/otel.d.ts +0 -81
  156. package/dist/esm/middlewares/otel.js +0 -745
  157. package/dist/esm/middlewares/otel.js.map +0 -1
  158. package/dist/esm/middlewares/tool-cache.d.ts +0 -1
  159. package/dist/esm/middlewares/usage-attributes.d.ts +0 -24
  160. package/dist/esm/middlewares/usage-attributes.js +0 -43
  161. package/dist/esm/middlewares/usage-attributes.js.map +0 -1
  162. package/dist/esm/realtime/index.d.ts +0 -28
  163. package/dist/esm/realtime/index.js +0 -8
  164. package/dist/esm/realtime/index.js.map +0 -1
  165. package/dist/esm/realtime/types.d.ts +0 -282
  166. package/dist/esm/stream-to-response.d.ts +0 -102
  167. package/dist/esm/stream-to-response.js +0 -121
  168. package/dist/esm/stream-to-response.js.map +0 -1
  169. package/dist/esm/strip-to-spec-middleware.d.ts +0 -18
  170. package/dist/esm/strip-to-spec-middleware.js +0 -20
  171. package/dist/esm/strip-to-spec-middleware.js.map +0 -1
  172. package/dist/esm/system-prompts.d.ts +0 -66
  173. package/dist/esm/system-prompts.js +0 -23
  174. package/dist/esm/system-prompts.js.map +0 -1
  175. package/dist/esm/tool-registry.d.ts +0 -81
  176. package/dist/esm/tool-registry.js +0 -49
  177. package/dist/esm/tool-registry.js.map +0 -1
  178. package/dist/esm/tools/provider-tool.d.ts +0 -30
  179. package/dist/esm/tools/provider-tool.js +0 -7
  180. package/dist/esm/tools/provider-tool.js.map +0 -1
  181. package/dist/esm/types.d.ts +0 -1573
  182. package/dist/esm/utilities/ag-ui-wire.d.ts +0 -44
  183. package/dist/esm/utilities/ag-ui-wire.js +0 -107
  184. package/dist/esm/utilities/ag-ui-wire.js.map +0 -1
  185. package/dist/esm/utilities/chat-params.d.ts +0 -85
  186. package/dist/esm/utilities/chat-params.js +0 -100
  187. package/dist/esm/utilities/chat-params.js.map +0 -1
  188. package/dist/esm/utilities/errors.d.ts +0 -13
  189. package/dist/esm/utilities/errors.js +0 -22
  190. package/dist/esm/utilities/errors.js.map +0 -1
  191. package/dist/esm/utilities/media-prompt.d.ts +0 -35
  192. package/dist/esm/utilities/media-prompt.js +0 -43
  193. package/dist/esm/utilities/media-prompt.js.map +0 -1
  194. package/dist/esm/utilities/numbers.d.ts +0 -8
  195. package/dist/esm/utilities/numbers.js +0 -12
  196. package/dist/esm/utilities/numbers.js.map +0 -1
  197. package/dist/esm/utilities/sampling-keys.d.ts +0 -20
  198. package/dist/esm/utilities/sampling-keys.js +0 -20
  199. package/dist/esm/utilities/sampling-keys.js.map +0 -1
  200. package/dist/esm/utilities/tool-result.d.ts +0 -21
  201. package/dist/esm/utilities/tool-result.js +0 -37
  202. package/dist/esm/utilities/tool-result.js.map +0 -1
  203. package/dist/esm/utilities/usage.d.ts +0 -31
  204. package/dist/esm/utilities/usage.js +0 -11
  205. package/dist/esm/utilities/usage.js.map +0 -1
  206. package/dist/esm/utils.d.ts +0 -17
  207. package/dist/esm/utils.js +0 -20
  208. package/dist/esm/utils.js.map +0 -1
@@ -1,1426 +0,0 @@
1
- import { generateMessageId, uiMessageToModelMessages, aguiSnapshotMessageToUIMessage } from "../messages.js";
2
- import { normalizeToolResult } from "../../../utilities/tool-result.js";
3
- import { defaultJSONParser } from "./json-parser.js";
4
- import { updateToolCallWithOutput, updateToolResultPart, updateToolCallApprovalResponse, appendStructuredOutputDelta, updateToolCallPart, errorStructuredOutputPart, updateThinkingPart, completeStructuredOutputPart, updateToolCallApproval, updateTextPart } from "./message-updaters.js";
5
- import { ImmediateStrategy } from "./strategies.js";
6
- const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12;
7
- class StreamProcessor {
8
- chunkStrategy;
9
- events;
10
- jsonParser;
11
- recordingEnabled;
12
- // Message state
13
- messages = [];
14
- // Per-message stream state
15
- messageStates = /* @__PURE__ */ new Map();
16
- activeMessageIds = /* @__PURE__ */ new Set();
17
- toolCallToMessage = /* @__PURE__ */ new Map();
18
- pendingManualMessageId = null;
19
- pendingThinkingStepId = null;
20
- structuredMessageIds = /* @__PURE__ */ new Set();
21
- structuredOutputUpdateBatches = /* @__PURE__ */ new Map();
22
- // Run tracking (for concurrent run safety)
23
- activeRuns = /* @__PURE__ */ new Set();
24
- // Shared stream state
25
- finishReason = null;
26
- hasError = false;
27
- isDone = false;
28
- // Recording
29
- recording = null;
30
- recordingStartTime = 0;
31
- constructor(options = {}) {
32
- this.chunkStrategy = options.chunkStrategy || new ImmediateStrategy();
33
- this.events = options.events || {};
34
- this.jsonParser = options.jsonParser || defaultJSONParser;
35
- this.recordingEnabled = options.recording ?? false;
36
- if (options.initialMessages) {
37
- this.messages = [...options.initialMessages];
38
- }
39
- }
40
- // ============================================
41
- // Message Management Methods
42
- // ============================================
43
- /**
44
- * Set the messages array (e.g., from persisted state)
45
- */
46
- setMessages(messages) {
47
- this.messages = [...messages];
48
- this.emitMessagesChange();
49
- }
50
- /**
51
- * Add a user message to the conversation.
52
- * Supports both simple string content and multimodal content arrays.
53
- *
54
- * @param content - The message content (string or array of content parts)
55
- * @param id - Optional custom message ID (generated if not provided)
56
- * @returns The created UIMessage
57
- *
58
- * @example
59
- * ```ts
60
- * // Simple text message
61
- * processor.addUserMessage('Hello!')
62
- *
63
- * // Multimodal message with image
64
- * processor.addUserMessage([
65
- * { type: 'text', content: 'What is in this image?' },
66
- * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
67
- * ])
68
- *
69
- * // With custom ID
70
- * processor.addUserMessage('Hello!', 'custom-id-123')
71
- * ```
72
- */
73
- addUserMessage(content, id) {
74
- const parts = typeof content === "string" ? [{ type: "text", content }] : content.map((part) => {
75
- return part;
76
- });
77
- const userMessage = {
78
- id: id ?? generateMessageId(),
79
- role: "user",
80
- parts,
81
- createdAt: /* @__PURE__ */ new Date()
82
- };
83
- this.messages = [...this.messages, userMessage];
84
- this.emitMessagesChange();
85
- return userMessage;
86
- }
87
- /**
88
- * Prepare for a new assistant message stream.
89
- * Does NOT create the message immediately -- the message is created lazily
90
- * when the first content-bearing chunk arrives via ensureAssistantMessage().
91
- * This prevents empty assistant messages from flickering in the UI when
92
- * auto-continuation produces no content.
93
- */
94
- prepareAssistantMessage() {
95
- this.resetStreamState();
96
- }
97
- /**
98
- * @deprecated Use prepareAssistantMessage() instead. This eagerly creates
99
- * an assistant message which can cause empty message flicker.
100
- */
101
- startAssistantMessage(messageId) {
102
- this.prepareAssistantMessage();
103
- const { messageId: id } = this.ensureAssistantMessage(messageId);
104
- this.pendingManualMessageId = id;
105
- return id;
106
- }
107
- /**
108
- * Get the current assistant message ID (if one has been created).
109
- * Returns null if prepareAssistantMessage() was called but no content
110
- * has arrived yet.
111
- */
112
- getCurrentAssistantMessageId() {
113
- let lastId = null;
114
- for (const [id, state] of this.messageStates) {
115
- if (state.role === "assistant") {
116
- lastId = id;
117
- }
118
- }
119
- return lastId;
120
- }
121
- /**
122
- * Add a tool result (called by client after handling onToolCall)
123
- */
124
- addToolResult(toolCallId, output, error) {
125
- const messageWithToolCall = this.messages.find(
126
- (msg) => msg.parts.some(
127
- (p) => p.type === "tool-call" && p.id === toolCallId
128
- )
129
- );
130
- if (!messageWithToolCall) {
131
- console.warn(
132
- `[StreamProcessor] Could not find message with tool call ${toolCallId}`
133
- );
134
- return;
135
- }
136
- let updatedMessages = updateToolCallWithOutput(
137
- this.messages,
138
- toolCallId,
139
- output,
140
- error ? "error" : void 0,
141
- error
142
- );
143
- const content = normalizeToolResult(output);
144
- const toolResultState = error ? "error" : "complete";
145
- updatedMessages = updateToolResultPart(
146
- updatedMessages,
147
- messageWithToolCall.id,
148
- toolCallId,
149
- content,
150
- toolResultState,
151
- error
152
- );
153
- this.messages = updatedMessages;
154
- this.emitMessagesChange();
155
- }
156
- /**
157
- * Add an approval response (called by client after handling onApprovalRequest)
158
- */
159
- addToolApprovalResponse(approvalId, approved) {
160
- this.messages = updateToolCallApprovalResponse(
161
- this.messages,
162
- approvalId,
163
- approved
164
- );
165
- this.emitMessagesChange();
166
- }
167
- /**
168
- * Get the conversation as ModelMessages (for sending to LLM)
169
- */
170
- toModelMessages() {
171
- const modelMessages = [];
172
- for (const msg of this.messages) {
173
- modelMessages.push(...uiMessageToModelMessages(msg));
174
- }
175
- return modelMessages;
176
- }
177
- /**
178
- * Get current messages
179
- */
180
- getMessages() {
181
- return this.messages;
182
- }
183
- /**
184
- * Check if all tool calls in the last assistant message are complete
185
- * Useful for auto-continue logic
186
- */
187
- areAllToolsComplete() {
188
- const lastAssistant = this.messages.findLast(
189
- (m) => m.role === "assistant"
190
- );
191
- if (!lastAssistant) return true;
192
- const toolParts = lastAssistant.parts.filter(
193
- (p) => p.type === "tool-call"
194
- );
195
- if (toolParts.length === 0) return true;
196
- const toolResultIds = new Set(
197
- lastAssistant.parts.filter((p) => p.type === "tool-result").map((p) => p.toolCallId)
198
- );
199
- return toolParts.every(
200
- (part) => part.state === "complete" || part.state === "approval-responded" || part.output !== void 0 && !part.approval || toolResultIds.has(part.id)
201
- );
202
- }
203
- /**
204
- * Remove messages after a certain index (for reload/retry)
205
- */
206
- removeMessagesAfter(index) {
207
- const keptIds = new Set(this.messages.slice(0, index + 1).map((m) => m.id));
208
- for (const id of this.structuredMessageIds) {
209
- if (!keptIds.has(id)) this.structuredMessageIds.delete(id);
210
- }
211
- for (const id of this.structuredOutputUpdateBatches.keys()) {
212
- if (!keptIds.has(id)) this.structuredOutputUpdateBatches.delete(id);
213
- }
214
- for (const id of this.messageStates.keys()) {
215
- if (!keptIds.has(id)) this.messageStates.delete(id);
216
- }
217
- for (const [toolCallId, msgId] of this.toolCallToMessage) {
218
- if (!keptIds.has(msgId)) this.toolCallToMessage.delete(toolCallId);
219
- }
220
- for (const id of this.activeMessageIds) {
221
- if (!keptIds.has(id)) this.activeMessageIds.delete(id);
222
- }
223
- this.messages = this.messages.slice(0, index + 1);
224
- this.emitMessagesChange();
225
- }
226
- /**
227
- * Clear all messages
228
- */
229
- clearMessages() {
230
- this.messages = [];
231
- this.messageStates.clear();
232
- this.activeMessageIds.clear();
233
- this.toolCallToMessage.clear();
234
- this.structuredMessageIds.clear();
235
- this.structuredOutputUpdateBatches.clear();
236
- this.pendingManualMessageId = null;
237
- this.emitMessagesChange();
238
- }
239
- // ============================================
240
- // Stream Processing Methods
241
- // ============================================
242
- /**
243
- * Process a stream and emit events through handlers
244
- */
245
- async process(stream) {
246
- this.resetStreamState();
247
- if (this.recordingEnabled) {
248
- this.startRecording();
249
- }
250
- for await (const chunk of stream) {
251
- this.processChunk(chunk);
252
- }
253
- this.finalizeStream();
254
- if (this.recording) {
255
- this.recording.result = this.getResult();
256
- }
257
- return this.getResult();
258
- }
259
- /**
260
- * Process a single chunk from the stream.
261
- *
262
- * Central dispatch for all AG-UI events. Each event type maps to a specific
263
- * handler. Events not listed in the switch are intentionally ignored
264
- * (STEP_STARTED, STATE_SNAPSHOT, STATE_DELTA).
265
- *
266
- * @see docs/chat-architecture.md#adapter-contract — Expected event types and ordering
267
- */
268
- processChunk(chunk) {
269
- if (this.recording) {
270
- this.recording.chunks.push({
271
- chunk,
272
- timestamp: Date.now(),
273
- index: this.recording.chunks.length
274
- });
275
- }
276
- const c = chunk;
277
- switch (c.type) {
278
- // AG-UI Events
279
- case "TEXT_MESSAGE_START":
280
- this.handleTextMessageStartEvent(
281
- chunk
282
- );
283
- break;
284
- case "TEXT_MESSAGE_CONTENT":
285
- this.handleTextMessageContentEvent(
286
- chunk
287
- );
288
- break;
289
- case "TEXT_MESSAGE_END":
290
- this.handleTextMessageEndEvent(
291
- chunk
292
- );
293
- break;
294
- case "TOOL_CALL_START":
295
- this.handleToolCallStartEvent(
296
- chunk
297
- );
298
- break;
299
- case "TOOL_CALL_ARGS":
300
- this.handleToolCallArgsEvent(
301
- chunk
302
- );
303
- break;
304
- case "TOOL_CALL_END":
305
- this.handleToolCallEndEvent(
306
- chunk
307
- );
308
- break;
309
- case "RUN_FINISHED":
310
- this.handleRunFinishedEvent(
311
- chunk
312
- );
313
- break;
314
- case "RUN_ERROR":
315
- this.handleRunErrorEvent(
316
- chunk
317
- );
318
- break;
319
- case "STEP_FINISHED":
320
- this.handleStepFinishedEvent(
321
- chunk
322
- );
323
- break;
324
- case "MESSAGES_SNAPSHOT":
325
- this.handleMessagesSnapshotEvent(
326
- chunk
327
- );
328
- break;
329
- case "CUSTOM":
330
- this.handleCustomEvent(
331
- chunk
332
- );
333
- break;
334
- case "RUN_STARTED":
335
- this.handleRunStartedEvent(
336
- chunk
337
- );
338
- break;
339
- case "REASONING_START":
340
- case "REASONING_MESSAGE_START":
341
- case "REASONING_MESSAGE_END":
342
- case "REASONING_END":
343
- break;
344
- case "REASONING_MESSAGE_CONTENT":
345
- this.handleReasoningMessageContentEvent(
346
- chunk
347
- );
348
- break;
349
- case "TOOL_CALL_RESULT":
350
- this.handleToolCallResultEvent(
351
- chunk
352
- );
353
- break;
354
- case "STEP_STARTED":
355
- this.handleStepStartedEvent(
356
- chunk
357
- );
358
- break;
359
- }
360
- }
361
- // ============================================
362
- // Per-Message State Helpers
363
- // ============================================
364
- /**
365
- * Create a new MessageStreamState for a message
366
- */
367
- createMessageState(messageId, role) {
368
- const state = {
369
- id: messageId,
370
- role,
371
- totalTextContent: "",
372
- currentSegmentText: "",
373
- lastEmittedText: "",
374
- hasSeenReasoningEvents: false,
375
- thinkingSteps: /* @__PURE__ */ new Map(),
376
- thinkingStepSignatures: /* @__PURE__ */ new Map(),
377
- thinkingStepOrder: [],
378
- currentThinkingStepId: null,
379
- toolCalls: /* @__PURE__ */ new Map(),
380
- toolCallOrder: [],
381
- hasToolCallsSinceTextStart: false,
382
- isComplete: false
383
- };
384
- this.messageStates.set(messageId, state);
385
- return state;
386
- }
387
- /**
388
- * Get the MessageStreamState for a message
389
- */
390
- getMessageState(messageId) {
391
- return this.messageStates.get(messageId);
392
- }
393
- /**
394
- * Promote a pending stepId from a STEP_STARTED that fired before the
395
- * assistant message existed onto the given message state, so the next
396
- * thinking event (STEP_FINISHED or REASONING_MESSAGE_CONTENT) attributes
397
- * to the correct step.
398
- */
399
- consumePendingThinkingStep(state) {
400
- if (!this.pendingThinkingStepId) return;
401
- const stepId = this.pendingThinkingStepId;
402
- state.currentThinkingStepId = stepId;
403
- if (!state.thinkingSteps.has(stepId)) {
404
- state.thinkingSteps.set(stepId, "");
405
- state.thinkingStepOrder.push(stepId);
406
- }
407
- this.pendingThinkingStepId = null;
408
- }
409
- /**
410
- * Get the most recent active assistant message ID.
411
- * Used as fallback for events that don't include a messageId.
412
- */
413
- getActiveAssistantMessageId() {
414
- const ids = Array.from(this.activeMessageIds).reverse();
415
- for (const id of ids) {
416
- const state = this.messageStates.get(id);
417
- if (state && state.role === "assistant") {
418
- return id;
419
- }
420
- }
421
- return null;
422
- }
423
- /**
424
- * Ensure an active assistant message exists, creating one if needed.
425
- * Used for backward compat when events arrive without prior TEXT_MESSAGE_START.
426
- *
427
- * On reconnect/resume, a TEXT_MESSAGE_CONTENT may arrive for a message that
428
- * already exists in this.messages (e.g. from initialMessages or a prior
429
- * MESSAGES_SNAPSHOT) but whose transient state was cleared. In that case we
430
- * hydrate state from the existing message rather than creating a duplicate.
431
- */
432
- ensureAssistantMessage(preferredId) {
433
- if (preferredId) {
434
- const state2 = this.getMessageState(preferredId);
435
- if (state2) return { messageId: preferredId, state: state2 };
436
- }
437
- const activeId = this.getActiveAssistantMessageId();
438
- if (activeId) {
439
- const state2 = this.getMessageState(activeId);
440
- if (state2) return { messageId: activeId, state: state2 };
441
- }
442
- if (preferredId) {
443
- const existingMsg = this.messages.find((m) => m.id === preferredId);
444
- if (existingMsg) {
445
- const state2 = this.createMessageState(preferredId, existingMsg.role);
446
- this.activeMessageIds.add(preferredId);
447
- const lastPart = existingMsg.parts.length > 0 ? existingMsg.parts[existingMsg.parts.length - 1] : null;
448
- if (lastPart && lastPart.type === "text") {
449
- state2.currentSegmentText = lastPart.content;
450
- state2.lastEmittedText = lastPart.content;
451
- state2.totalTextContent = lastPart.content;
452
- }
453
- return { messageId: preferredId, state: state2 };
454
- }
455
- }
456
- const id = preferredId || generateMessageId();
457
- const assistantMessage = {
458
- id,
459
- role: "assistant",
460
- parts: [],
461
- createdAt: /* @__PURE__ */ new Date()
462
- };
463
- this.messages = [...this.messages, assistantMessage];
464
- const state = this.createMessageState(id, "assistant");
465
- this.activeMessageIds.add(id);
466
- this.pendingManualMessageId = id;
467
- this.events.onStreamStart?.();
468
- this.emitMessagesChange();
469
- return { messageId: id, state };
470
- }
471
- // ============================================
472
- // Event Handlers
473
- // ============================================
474
- /**
475
- * Handle TEXT_MESSAGE_START event
476
- */
477
- handleTextMessageStartEvent(chunk) {
478
- const { messageId, role } = chunk;
479
- const uiRole = role === "user" || role === "system" ? role : "assistant";
480
- if (this.pendingManualMessageId) {
481
- const pendingId = this.pendingManualMessageId;
482
- this.pendingManualMessageId = null;
483
- if (pendingId !== messageId) {
484
- this.messages = this.messages.map(
485
- (msg) => msg.id === pendingId ? { ...msg, id: messageId } : msg
486
- );
487
- const existingState = this.messageStates.get(pendingId);
488
- if (existingState) {
489
- existingState.id = messageId;
490
- this.messageStates.delete(pendingId);
491
- this.messageStates.set(messageId, existingState);
492
- }
493
- this.activeMessageIds.delete(pendingId);
494
- this.activeMessageIds.add(messageId);
495
- }
496
- if (!this.messageStates.has(messageId)) {
497
- this.createMessageState(messageId, uiRole);
498
- this.activeMessageIds.add(messageId);
499
- }
500
- this.emitMessagesChange();
501
- return;
502
- }
503
- const existingMsg = this.messages.find((m) => m.id === messageId);
504
- if (existingMsg) {
505
- this.activeMessageIds.add(messageId);
506
- const existingState = this.messageStates.get(messageId);
507
- if (!existingState) {
508
- this.createMessageState(messageId, uiRole);
509
- } else {
510
- if (existingState.hasToolCallsSinceTextStart) {
511
- if (existingState.currentSegmentText !== existingState.lastEmittedText) {
512
- this.emitTextUpdateForMessage(messageId);
513
- }
514
- existingState.currentSegmentText = "";
515
- existingState.lastEmittedText = "";
516
- existingState.hasToolCallsSinceTextStart = false;
517
- }
518
- }
519
- return;
520
- }
521
- const newMessage = {
522
- id: messageId,
523
- role: uiRole,
524
- parts: [],
525
- createdAt: /* @__PURE__ */ new Date()
526
- };
527
- this.messages = [...this.messages, newMessage];
528
- this.createMessageState(messageId, uiRole);
529
- this.activeMessageIds.add(messageId);
530
- this.events.onStreamStart?.();
531
- this.emitMessagesChange();
532
- }
533
- /**
534
- * Handle TEXT_MESSAGE_END event
535
- */
536
- handleTextMessageEndEvent(chunk) {
537
- const { messageId } = chunk;
538
- const state = this.getMessageState(messageId);
539
- if (!state) return;
540
- if (state.isComplete) return;
541
- if (state.currentSegmentText !== state.lastEmittedText) {
542
- this.emitTextUpdateForMessage(messageId);
543
- }
544
- this.completeAllToolCallsForMessage(messageId);
545
- }
546
- /**
547
- * Handle MESSAGES_SNAPSHOT event
548
- */
549
- handleMessagesSnapshotEvent(chunk) {
550
- this.resetStreamState();
551
- this.messages = chunk.messages.map(aguiSnapshotMessageToUIMessage);
552
- this.emitMessagesChange();
553
- }
554
- /**
555
- * Handle TEXT_MESSAGE_CONTENT event.
556
- *
557
- * Accumulates delta into both currentSegmentText (for UI emission) and
558
- * totalTextContent (for ProcessorResult). Lazily creates the assistant
559
- * UIMessage on first content. Uses updateTextPart() which replaces the
560
- * last TextPart or creates a new one depending on part ordering.
561
- *
562
- * @see docs/chat-architecture.md#single-shot-text-response — Text accumulation step-by-step
563
- * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
564
- */
565
- handleTextMessageContentEvent(chunk) {
566
- const { messageId, state } = this.ensureAssistantMessage(chunk.messageId);
567
- this.completeAllToolCallsForMessage(messageId);
568
- if (this.structuredMessageIds.has(messageId)) {
569
- let delta2 = chunk.delta || "";
570
- if (delta2 === "" && chunk.content !== void 0 && chunk.content !== "") {
571
- const existingRaw = (this.messages.find((m) => m.id === messageId)?.parts.find(
572
- (p) => p.type === "structured-output"
573
- ) ?? { raw: "" }).raw;
574
- if (chunk.content.startsWith(existingRaw)) {
575
- delta2 = chunk.content.slice(existingRaw.length);
576
- } else if (existingRaw.startsWith(chunk.content)) {
577
- delta2 = "";
578
- } else {
579
- delta2 = chunk.content;
580
- }
581
- }
582
- if (delta2 !== "") {
583
- this.messages = appendStructuredOutputDelta(
584
- this.messages,
585
- messageId,
586
- delta2
587
- );
588
- state.totalTextContent += delta2;
589
- this.queueStructuredOutputUpdate(messageId, delta2);
590
- this.emitMessagesChange();
591
- }
592
- return;
593
- }
594
- const previousSegment = state.currentSegmentText;
595
- const isNewSegment = state.hasToolCallsSinceTextStart && previousSegment.length > 0 && this.isNewTextSegment(chunk, previousSegment);
596
- if (isNewSegment) {
597
- if (previousSegment !== state.lastEmittedText) {
598
- this.emitTextUpdateForMessage(messageId);
599
- }
600
- state.currentSegmentText = "";
601
- state.lastEmittedText = "";
602
- state.hasToolCallsSinceTextStart = false;
603
- }
604
- const currentText = state.currentSegmentText;
605
- let nextText = currentText;
606
- const delta = chunk.delta || "";
607
- if (delta !== "") {
608
- nextText = currentText + delta;
609
- } else if (chunk.content !== void 0 && chunk.content !== "") {
610
- if (chunk.content.startsWith(currentText)) {
611
- nextText = chunk.content;
612
- } else if (currentText.startsWith(chunk.content)) {
613
- nextText = currentText;
614
- } else {
615
- nextText = currentText + chunk.content;
616
- }
617
- }
618
- const textDelta = nextText.slice(currentText.length);
619
- state.currentSegmentText = nextText;
620
- state.totalTextContent += textDelta;
621
- const chunkPortion = chunk.delta || chunk.content || "";
622
- const shouldEmit = this.chunkStrategy.shouldEmit(
623
- chunkPortion,
624
- state.currentSegmentText
625
- );
626
- if (shouldEmit && state.currentSegmentText !== state.lastEmittedText) {
627
- this.emitTextUpdateForMessage(messageId);
628
- }
629
- }
630
- /**
631
- * Handle TOOL_CALL_START event.
632
- *
633
- * Creates a new InternalToolCallState entry in the toolCalls Map and appends
634
- * a ToolCallPart to the UIMessage. Duplicate toolCallId is a no-op.
635
- *
636
- * CRITICAL: This MUST be received before any TOOL_CALL_ARGS for the same
637
- * toolCallId. Args for unknown IDs are silently dropped.
638
- *
639
- * @see docs/chat-architecture.md#single-shot-tool-call-response — Tool call state transitions
640
- * @see docs/chat-architecture.md#parallel-tool-calls-single-shot — Parallel tracking by ID
641
- * @see docs/chat-architecture.md#adapter-contract — Ordering requirements
642
- */
643
- handleToolCallStartEvent(chunk) {
644
- const targetMessageId = chunk.parentMessageId ?? this.getActiveAssistantMessageId();
645
- const { messageId, state } = this.ensureAssistantMessage(
646
- targetMessageId ?? void 0
647
- );
648
- state.hasToolCallsSinceTextStart = true;
649
- const toolCallId = chunk.toolCallId;
650
- const existingToolCall = state.toolCalls.get(toolCallId);
651
- if (!existingToolCall) {
652
- const initialState = "awaiting-input";
653
- const toolName = chunk.toolCallName ?? chunk.toolName;
654
- const chunkMetadata = chunk.metadata;
655
- const newToolCall = {
656
- id: chunk.toolCallId,
657
- name: toolName,
658
- arguments: "",
659
- state: initialState,
660
- parsedArguments: void 0,
661
- index: chunk.index ?? state.toolCalls.size,
662
- ...chunkMetadata !== void 0 && { metadata: chunkMetadata }
663
- };
664
- state.toolCalls.set(toolCallId, newToolCall);
665
- state.toolCallOrder.push(toolCallId);
666
- this.toolCallToMessage.set(toolCallId, messageId);
667
- this.messages = updateToolCallPart(this.messages, messageId, {
668
- id: chunk.toolCallId,
669
- name: toolName,
670
- arguments: "",
671
- state: initialState,
672
- ...chunkMetadata !== void 0 && { metadata: chunkMetadata }
673
- });
674
- this.emitMessagesChange();
675
- this.events.onToolCallStateChange?.(
676
- messageId,
677
- chunk.toolCallId,
678
- initialState,
679
- ""
680
- );
681
- }
682
- }
683
- /**
684
- * Handle TOOL_CALL_ARGS event.
685
- *
686
- * Appends the delta to the tool call's accumulated arguments string.
687
- * Transitions state from awaiting-input → input-streaming on first non-empty delta.
688
- * Attempts partial JSON parse on each update for UI preview.
689
- *
690
- * If toolCallId is not found in the Map (no preceding TOOL_CALL_START),
691
- * this event is silently dropped.
692
- *
693
- * @see docs/chat-architecture.md#single-shot-tool-call-response — Step-by-step tool call processing
694
- */
695
- handleToolCallArgsEvent(chunk) {
696
- const toolCallId = chunk.toolCallId;
697
- const messageId = this.toolCallToMessage.get(toolCallId);
698
- if (!messageId) return;
699
- const state = this.getMessageState(messageId);
700
- if (!state) return;
701
- const existingToolCall = state.toolCalls.get(toolCallId);
702
- if (!existingToolCall) return;
703
- const wasAwaitingInput = existingToolCall.state === "awaiting-input";
704
- existingToolCall.arguments += chunk.delta || "";
705
- if (wasAwaitingInput && chunk.delta) {
706
- existingToolCall.state = "input-streaming";
707
- }
708
- existingToolCall.parsedArguments = this.jsonParser.parse(
709
- existingToolCall.arguments
710
- );
711
- this.messages = updateToolCallPart(this.messages, messageId, {
712
- id: existingToolCall.id,
713
- name: existingToolCall.name,
714
- arguments: existingToolCall.arguments,
715
- state: existingToolCall.state
716
- });
717
- this.emitMessagesChange();
718
- this.events.onToolCallStateChange?.(
719
- messageId,
720
- existingToolCall.id,
721
- existingToolCall.state,
722
- existingToolCall.arguments
723
- );
724
- }
725
- /**
726
- * Handle TOOL_CALL_END event — authoritative signal that a tool call's input is finalized.
727
- *
728
- * This event has a DUAL ROLE:
729
- * - Without `result`: Signals arguments are done (from adapter). Transitions to input-complete.
730
- * - With `result`: Signals tool was executed and result is available (from TextEngine).
731
- * Creates both output on the tool-call part AND a tool-result part.
732
- *
733
- * If `input` is provided, it overrides the accumulated string parse as the
734
- * canonical parsed arguments.
735
- *
736
- * @see docs/chat-architecture.md#tool-results-and-the-tool_call_end-dual-role — Full explanation
737
- * @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
738
- */
739
- handleToolCallEndEvent(chunk) {
740
- const messageId = this.toolCallToMessage.get(chunk.toolCallId);
741
- if (!messageId) return;
742
- const msgState = this.getMessageState(messageId);
743
- if (!msgState) return;
744
- const existingToolCall = msgState.toolCalls.get(chunk.toolCallId);
745
- if (existingToolCall && existingToolCall.state !== "input-complete") {
746
- if (chunk.input !== void 0 && !existingToolCall.arguments) {
747
- existingToolCall.arguments = JSON.stringify(chunk.input);
748
- }
749
- const index = msgState.toolCallOrder.indexOf(chunk.toolCallId);
750
- this.completeToolCall(messageId, index, existingToolCall);
751
- if (chunk.input !== void 0) {
752
- existingToolCall.parsedArguments = chunk.input;
753
- }
754
- }
755
- if (chunk.result) {
756
- let output;
757
- if (Array.isArray(chunk.result)) {
758
- output = chunk.result;
759
- } else {
760
- try {
761
- output = JSON.parse(chunk.result);
762
- } catch {
763
- output = chunk.result;
764
- }
765
- }
766
- this.messages = updateToolCallWithOutput(
767
- this.messages,
768
- chunk.toolCallId,
769
- output,
770
- chunk.state === "output-error" ? "error" : void 0
771
- );
772
- const resultState = chunk.state === "output-error" ? "error" : "complete";
773
- this.messages = updateToolResultPart(
774
- this.messages,
775
- messageId,
776
- chunk.toolCallId,
777
- chunk.result,
778
- resultState,
779
- resultState === "error" ? this.extractToolResultError(output) : void 0
780
- );
781
- this.emitMessagesChange();
782
- }
783
- }
784
- extractToolResultError(output) {
785
- if (output && typeof output === "object" && "error" in output && typeof output.error === "string") {
786
- return output.error;
787
- }
788
- return typeof output === "string" ? output : "Tool execution failed";
789
- }
790
- /**
791
- * Handle TOOL_CALL_RESULT event (AG-UI spec).
792
- *
793
- * Creates a tool-result part and updates the tool-call output field,
794
- * mirroring the logic from TOOL_CALL_END when it carries a result.
795
- * This is the spec-compliant path for delivering tool results to the client.
796
- */
797
- handleToolCallResultEvent(chunk) {
798
- const messageId = this.toolCallToMessage.get(chunk.toolCallId);
799
- if (!messageId) return;
800
- let output;
801
- try {
802
- output = JSON.parse(chunk.content);
803
- } catch {
804
- output = chunk.content;
805
- }
806
- this.messages = updateToolCallWithOutput(
807
- this.messages,
808
- chunk.toolCallId,
809
- output,
810
- chunk.state === "output-error" ? "error" : void 0
811
- );
812
- const resultState = chunk.state === "output-error" ? "error" : "complete";
813
- this.messages = updateToolResultPart(
814
- this.messages,
815
- messageId,
816
- chunk.toolCallId,
817
- chunk.content,
818
- resultState,
819
- resultState === "error" ? this.extractToolResultError(output) : void 0
820
- );
821
- this.emitMessagesChange();
822
- }
823
- /**
824
- * Handle RUN_STARTED event.
825
- *
826
- * Registers the run so that RUN_FINISHED can determine whether other
827
- * runs are still active before finalizing.
828
- */
829
- handleRunStartedEvent(chunk) {
830
- this.activeRuns.add(chunk.runId);
831
- }
832
- /**
833
- * Handle RUN_FINISHED event.
834
- *
835
- * Records the finishReason and removes the run from activeRuns.
836
- * Only finalizes when no more runs are active, so that concurrent
837
- * runs don't interfere with each other.
838
- *
839
- * @see docs/chat-architecture.md#single-shot-tool-call-response — finishReason semantics
840
- * @see docs/chat-architecture.md#adapter-contract — Why RUN_FINISHED is mandatory
841
- */
842
- handleRunFinishedEvent(chunk) {
843
- this.finishReason = chunk.finishReason ?? null;
844
- this.activeRuns.delete(chunk.runId);
845
- if (this.activeRuns.size === 0) {
846
- this.isDone = true;
847
- this.completeAllToolCalls();
848
- this.finalizeStream();
849
- }
850
- }
851
- /**
852
- * Handle RUN_ERROR event
853
- */
854
- handleRunErrorEvent(chunk) {
855
- this.hasError = true;
856
- const runId = "runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0;
857
- if (runId) {
858
- this.activeRuns.delete(runId);
859
- } else {
860
- this.activeRuns.clear();
861
- }
862
- const { messageId } = this.ensureAssistantMessage();
863
- const errorMessage = chunk.message || chunk.error?.message || "An error occurred";
864
- if (!chunk.message && !chunk.error?.message) {
865
- console.error(
866
- "[StreamProcessor] RUN_ERROR with no message; original chunk:",
867
- chunk
868
- );
869
- }
870
- if (this.structuredMessageIds.has(messageId)) {
871
- this.flushStructuredOutputUpdate(messageId);
872
- this.messages = errorStructuredOutputPart(
873
- this.messages,
874
- messageId,
875
- errorMessage
876
- );
877
- this.structuredMessageIds.delete(messageId);
878
- this.emitStructuredOutputChange(messageId, "error");
879
- this.emitMessagesChange();
880
- }
881
- const error = new Error(errorMessage);
882
- const code = chunk.code ?? chunk.error?.code;
883
- if (code !== void 0) {
884
- Object.assign(error, { code });
885
- }
886
- if (chunk.rawEvent !== void 0) {
887
- Object.assign(error, { rawEvent: chunk.rawEvent });
888
- }
889
- this.events.onError?.(error);
890
- }
891
- /**
892
- * Handle STEP_STARTED event (for thinking/reasoning content).
893
- *
894
- * Records the stepId so that subsequent STEP_FINISHED deltas accumulate
895
- * into their own ThinkingPart. Does not create a message — the message
896
- * is lazily created when the first STEP_FINISHED content arrives.
897
- */
898
- handleStepStartedEvent(chunk) {
899
- const stepId = chunk.stepId ?? generateMessageId();
900
- const activeId = this.getActiveAssistantMessageId();
901
- if (activeId) {
902
- const state = this.getMessageState(activeId);
903
- if (state) {
904
- state.currentThinkingStepId = stepId;
905
- if (!state.thinkingSteps.has(stepId)) {
906
- state.thinkingSteps.set(stepId, "");
907
- state.thinkingStepOrder.push(stepId);
908
- }
909
- this.pendingThinkingStepId = null;
910
- return;
911
- }
912
- }
913
- this.pendingThinkingStepId = stepId;
914
- }
915
- /**
916
- * Handle STEP_FINISHED event (for thinking/reasoning content).
917
- *
918
- * Accumulates delta into the current thinking step's content and updates
919
- * the corresponding ThinkingPart in the UIMessage.
920
- *
921
- * @see docs/chat-architecture.md#thinkingreasoning-content — Thinking flow
922
- */
923
- handleStepFinishedEvent(chunk) {
924
- const { messageId, state } = this.ensureAssistantMessage(
925
- this.getActiveAssistantMessageId() ?? void 0
926
- );
927
- if (state.hasSeenReasoningEvents) {
928
- if (chunk.signature) {
929
- const stepId2 = state.currentThinkingStepId ?? chunk.stepId;
930
- if (!stepId2) return;
931
- const thinking = state.thinkingSteps.get(stepId2);
932
- if (thinking !== void 0) {
933
- state.thinkingStepSignatures.set(stepId2, chunk.signature);
934
- this.messages = updateThinkingPart(
935
- this.messages,
936
- messageId,
937
- stepId2,
938
- thinking,
939
- chunk.signature
940
- );
941
- this.emitMessagesChange();
942
- }
943
- }
944
- return;
945
- }
946
- this.consumePendingThinkingStep(state);
947
- const stepId = state.currentThinkingStepId ?? chunk.stepId ?? generateMessageId();
948
- if (!state.thinkingSteps.has(stepId)) {
949
- state.thinkingSteps.set(stepId, "");
950
- state.thinkingStepOrder.push(stepId);
951
- state.currentThinkingStepId = stepId;
952
- }
953
- const previous = state.thinkingSteps.get(stepId) ?? "";
954
- let nextThinking = previous;
955
- if (chunk.delta && chunk.delta !== "") {
956
- nextThinking = previous + chunk.delta;
957
- } else if (chunk.content && chunk.content !== "") {
958
- if (chunk.content.startsWith(previous)) {
959
- nextThinking = chunk.content;
960
- } else if (previous.startsWith(chunk.content)) {
961
- nextThinking = previous;
962
- } else {
963
- nextThinking = previous + chunk.content;
964
- }
965
- }
966
- state.thinkingSteps.set(stepId, nextThinking);
967
- if (chunk.signature) {
968
- state.thinkingStepSignatures.set(stepId, chunk.signature);
969
- }
970
- this.messages = updateThinkingPart(
971
- this.messages,
972
- messageId,
973
- stepId,
974
- nextThinking,
975
- state.thinkingStepSignatures.get(stepId)
976
- );
977
- this.emitMessagesChange();
978
- this.events.onThinkingUpdate?.(messageId, stepId, nextThinking);
979
- }
980
- /**
981
- * Handle REASONING_MESSAGE_CONTENT event (AG-UI reasoning protocol).
982
- *
983
- * Accumulates reasoning delta into thinking content and updates the
984
- * corresponding ThinkingPart in the UIMessage.
985
- */
986
- handleReasoningMessageContentEvent(chunk) {
987
- const { messageId, state } = this.ensureAssistantMessage(
988
- this.getActiveAssistantMessageId() ?? void 0
989
- );
990
- state.hasSeenReasoningEvents = true;
991
- const delta = chunk.delta || "";
992
- this.consumePendingThinkingStep(state);
993
- const stepId = state.currentThinkingStepId ?? chunk.messageId;
994
- if (!state.thinkingSteps.has(stepId)) {
995
- state.thinkingSteps.set(stepId, "");
996
- state.thinkingStepOrder.push(stepId);
997
- state.currentThinkingStepId = stepId;
998
- }
999
- const nextThinking = (state.thinkingSteps.get(stepId) ?? "") + delta;
1000
- state.thinkingSteps.set(stepId, nextThinking);
1001
- this.messages = updateThinkingPart(
1002
- this.messages,
1003
- messageId,
1004
- stepId,
1005
- nextThinking,
1006
- state.thinkingStepSignatures.get(stepId)
1007
- );
1008
- this.emitMessagesChange();
1009
- this.events.onThinkingUpdate?.(messageId, stepId, nextThinking);
1010
- }
1011
- /**
1012
- * Handle CUSTOM event.
1013
- *
1014
- * Handles special custom events emitted by the TextEngine (not adapters):
1015
- * - 'tool-input-available': Client tool needs execution. Fires onToolCall.
1016
- * - 'approval-requested': Tool needs user approval. Updates tool-call part
1017
- * state and fires onApprovalRequest.
1018
- *
1019
- * @see docs/chat-architecture.md#client-tools-and-approval-flows — Full flow details
1020
- */
1021
- handleCustomEvent(chunk) {
1022
- const messageId = this.getActiveAssistantMessageId();
1023
- if (chunk.name === "structured-output.start" && chunk.value) {
1024
- const v = chunk.value;
1025
- const targetId = v.messageId ?? messageId;
1026
- if (targetId) {
1027
- this.ensureAssistantMessage(targetId);
1028
- this.structuredMessageIds.add(targetId);
1029
- this.structuredOutputUpdateBatches.delete(targetId);
1030
- this.events.onStructuredOutputChange?.({
1031
- phase: "start",
1032
- messageId: targetId,
1033
- status: "streaming",
1034
- raw: ""
1035
- });
1036
- }
1037
- return;
1038
- }
1039
- if (chunk.name === "structured-output.complete" && chunk.value) {
1040
- const v = chunk.value;
1041
- const targetId = v.messageId ?? messageId;
1042
- if (targetId) {
1043
- this.flushStructuredOutputUpdate(targetId);
1044
- this.messages = completeStructuredOutputPart(
1045
- this.messages,
1046
- targetId,
1047
- v.object,
1048
- v.raw ?? "",
1049
- v.reasoning
1050
- );
1051
- this.structuredMessageIds.delete(targetId);
1052
- this.emitStructuredOutputChange(targetId, "complete");
1053
- this.emitMessagesChange();
1054
- }
1055
- }
1056
- if (chunk.name === "tool-input-available" && chunk.value) {
1057
- const { toolCallId, toolName, input } = chunk.value;
1058
- this.events.onToolCall?.({
1059
- toolCallId,
1060
- toolName,
1061
- input
1062
- });
1063
- return;
1064
- }
1065
- if (chunk.name === "approval-requested" && chunk.value) {
1066
- const { toolCallId, toolName, input, approval } = chunk.value;
1067
- const resolvedMessageId = messageId ?? this.toolCallToMessage.get(toolCallId);
1068
- if (resolvedMessageId) {
1069
- this.messages = updateToolCallApproval(
1070
- this.messages,
1071
- resolvedMessageId,
1072
- toolCallId,
1073
- approval.id
1074
- );
1075
- this.emitMessagesChange();
1076
- }
1077
- this.events.onApprovalRequest?.({
1078
- toolCallId,
1079
- toolName,
1080
- input,
1081
- approvalId: approval.id
1082
- });
1083
- return;
1084
- }
1085
- if (this.events.onCustomEvent) {
1086
- const toolCallId = chunk.value && typeof chunk.value === "object" ? chunk.value.toolCallId : void 0;
1087
- this.events.onCustomEvent(chunk.name, chunk.value, { toolCallId });
1088
- }
1089
- }
1090
- // ============================================
1091
- // Internal Helpers
1092
- // ============================================
1093
- /**
1094
- * Detect if an incoming content chunk represents a NEW text segment
1095
- */
1096
- isNewTextSegment(chunk, previous) {
1097
- if (chunk.content !== void 0) {
1098
- if (chunk.content.length < previous.length) {
1099
- return true;
1100
- }
1101
- if (!chunk.content.startsWith(previous) && !previous.startsWith(chunk.content)) {
1102
- return true;
1103
- }
1104
- }
1105
- return false;
1106
- }
1107
- /**
1108
- * Complete all tool calls across all active messages — safety net for stream termination.
1109
- *
1110
- * Called by RUN_FINISHED and finalizeStream(). Force-transitions any tool call
1111
- * not yet in input-complete state. Handles cases where TOOL_CALL_END was
1112
- * missed (adapter bug, network error, aborted stream).
1113
- *
1114
- * @see docs/chat-architecture.md#single-shot-tool-call-response — Safety net behavior
1115
- */
1116
- completeAllToolCalls() {
1117
- for (const messageId of this.activeMessageIds) {
1118
- this.completeAllToolCallsForMessage(messageId);
1119
- }
1120
- }
1121
- /**
1122
- * Complete all tool calls for a specific message
1123
- */
1124
- completeAllToolCallsForMessage(messageId) {
1125
- const state = this.getMessageState(messageId);
1126
- if (!state) return;
1127
- state.toolCalls.forEach((toolCall, id) => {
1128
- if (toolCall.state !== "input-complete") {
1129
- const index = state.toolCallOrder.indexOf(id);
1130
- this.completeToolCall(messageId, index, toolCall);
1131
- }
1132
- });
1133
- }
1134
- /**
1135
- * Mark a tool call as complete and emit event
1136
- */
1137
- completeToolCall(messageId, _index, toolCall) {
1138
- toolCall.state = "input-complete";
1139
- toolCall.parsedArguments = this.jsonParser.parse(toolCall.arguments);
1140
- if (this.isToolCallPartErrored(toolCall.id)) {
1141
- return;
1142
- }
1143
- this.messages = updateToolCallPart(this.messages, messageId, {
1144
- id: toolCall.id,
1145
- name: toolCall.name,
1146
- arguments: toolCall.arguments,
1147
- state: "input-complete",
1148
- ...toolCall.metadata !== void 0 && { metadata: toolCall.metadata }
1149
- });
1150
- this.emitMessagesChange();
1151
- this.events.onToolCallStateChange?.(
1152
- messageId,
1153
- toolCall.id,
1154
- "input-complete",
1155
- toolCall.arguments
1156
- );
1157
- }
1158
- /**
1159
- * Whether the rendered tool-call part for the given id has reached the
1160
- * terminal 'error' state. Used to prevent the completion safety net from
1161
- * downgrading a failed call back to 'input-complete'.
1162
- */
1163
- isToolCallPartErrored(toolCallId) {
1164
- return this.messages.some(
1165
- (msg) => msg.parts.some(
1166
- (part) => part.type === "tool-call" && part.id === toolCallId && part.state === "error"
1167
- )
1168
- );
1169
- }
1170
- /**
1171
- * Emit pending text update for a specific message.
1172
- *
1173
- * Calls updateTextPart() which has critical append-vs-replace logic:
1174
- * - If last UIMessage part is TextPart → replaces its content (same segment).
1175
- * - If last part is anything else → pushes new TextPart (new segment after tools).
1176
- *
1177
- * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
1178
- */
1179
- emitTextUpdateForMessage(messageId) {
1180
- const state = this.getMessageState(messageId);
1181
- if (!state) return;
1182
- state.lastEmittedText = state.currentSegmentText;
1183
- this.messages = updateTextPart(
1184
- this.messages,
1185
- messageId,
1186
- state.currentSegmentText
1187
- );
1188
- this.emitMessagesChange();
1189
- this.events.onTextUpdate?.(messageId, state.currentSegmentText);
1190
- }
1191
- queueStructuredOutputUpdate(messageId, delta) {
1192
- const existing = this.structuredOutputUpdateBatches.get(messageId);
1193
- const next = {
1194
- delta: `${existing?.delta ?? ""}${delta}`,
1195
- chunkCount: (existing?.chunkCount ?? 0) + 1
1196
- };
1197
- this.structuredOutputUpdateBatches.set(messageId, next);
1198
- if (next.chunkCount >= STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE) {
1199
- this.flushStructuredOutputUpdate(messageId);
1200
- }
1201
- }
1202
- flushStructuredOutputUpdate(messageId) {
1203
- const batch = this.structuredOutputUpdateBatches.get(messageId);
1204
- if (!batch || batch.chunkCount === 0) return;
1205
- this.structuredOutputUpdateBatches.delete(messageId);
1206
- this.emitStructuredOutputChange(messageId, "update", batch.delta);
1207
- }
1208
- emitStructuredOutputChange(messageId, phase, delta) {
1209
- const part = this.messages.find((message) => message.id === messageId)?.parts.find(
1210
- (messagePart) => messagePart.type === "structured-output"
1211
- );
1212
- if (!part) return;
1213
- this.events.onStructuredOutputChange?.({
1214
- phase,
1215
- messageId,
1216
- status: part.status,
1217
- raw: part.raw,
1218
- ...part.partial !== void 0 ? { partial: part.partial } : {},
1219
- ...part.data !== void 0 ? { data: part.data } : {},
1220
- ...part.reasoning !== void 0 ? { reasoning: part.reasoning } : {},
1221
- ...part.errorMessage !== void 0 ? { errorMessage: part.errorMessage } : {},
1222
- ...delta !== void 0 ? { delta } : {}
1223
- });
1224
- }
1225
- /**
1226
- * Emit messages change event
1227
- */
1228
- emitMessagesChange() {
1229
- this.events.onMessagesChange?.([...this.messages]);
1230
- }
1231
- /**
1232
- * Finalize the stream — complete all pending operations.
1233
- *
1234
- * Called when the async iterable ends (stream closed). Acts as the final
1235
- * safety net: completes any remaining tool calls, flushes un-emitted text,
1236
- * and fires onStreamEnd.
1237
- *
1238
- * @see docs/chat-architecture.md#single-shot-text-response — Finalization step
1239
- */
1240
- finalizeStream() {
1241
- let lastAssistantMessage;
1242
- for (const messageId of this.activeMessageIds) {
1243
- const state = this.getMessageState(messageId);
1244
- if (!state) continue;
1245
- this.completeAllToolCallsForMessage(messageId);
1246
- if (state.currentSegmentText !== state.lastEmittedText) {
1247
- this.emitTextUpdateForMessage(messageId);
1248
- }
1249
- state.isComplete = true;
1250
- const msg = this.messages.find((m) => m.id === messageId);
1251
- if (msg && msg.role === "assistant") {
1252
- lastAssistantMessage = msg;
1253
- }
1254
- }
1255
- for (const messageId of this.structuredMessageIds) {
1256
- this.flushStructuredOutputUpdate(messageId);
1257
- this.messages = errorStructuredOutputPart(
1258
- this.messages,
1259
- messageId,
1260
- "Stream ended without structured-output.complete"
1261
- );
1262
- this.emitStructuredOutputChange(messageId, "error");
1263
- }
1264
- this.structuredMessageIds.clear();
1265
- this.structuredOutputUpdateBatches.clear();
1266
- this.activeMessageIds.clear();
1267
- if (lastAssistantMessage && !this.hasError) {
1268
- if (this.isWhitespaceOnlyMessage(lastAssistantMessage)) {
1269
- this.messages = this.messages.filter(
1270
- (m) => m.id !== lastAssistantMessage.id
1271
- );
1272
- this.emitMessagesChange();
1273
- return;
1274
- }
1275
- }
1276
- if (lastAssistantMessage) {
1277
- this.events.onStreamEnd?.(lastAssistantMessage);
1278
- }
1279
- }
1280
- /**
1281
- * Get completed tool calls in API format (aggregated across all messages)
1282
- */
1283
- getCompletedToolCalls() {
1284
- const result = [];
1285
- for (const state of this.messageStates.values()) {
1286
- for (const tc of state.toolCalls.values()) {
1287
- if (tc.state === "input-complete") {
1288
- result.push({
1289
- id: tc.id,
1290
- type: "function",
1291
- function: {
1292
- name: tc.name,
1293
- arguments: tc.arguments
1294
- },
1295
- // Preserve provider metadata (e.g. Gemini thoughtSignature) on
1296
- // ProcessorResult.toolCalls so callers using process()/getResult()
1297
- // get the same round-trip support as the streaming UI path.
1298
- ...tc.metadata !== void 0 && { metadata: tc.metadata }
1299
- });
1300
- }
1301
- }
1302
- }
1303
- return result;
1304
- }
1305
- /**
1306
- * Get current result (aggregated across all messages)
1307
- */
1308
- getResult() {
1309
- const toolCalls = this.getCompletedToolCalls();
1310
- let content = "";
1311
- let thinking = "";
1312
- for (const state of this.messageStates.values()) {
1313
- content += state.totalTextContent;
1314
- for (const stepId of state.thinkingStepOrder) {
1315
- thinking += state.thinkingSteps.get(stepId) ?? "";
1316
- }
1317
- }
1318
- return {
1319
- content,
1320
- thinking: thinking || void 0,
1321
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1322
- finishReason: this.finishReason
1323
- };
1324
- }
1325
- /**
1326
- * Get current processor state (aggregated across all messages)
1327
- */
1328
- getState() {
1329
- let content = "";
1330
- let thinking = "";
1331
- const toolCalls = /* @__PURE__ */ new Map();
1332
- const toolCallOrder = [];
1333
- for (const state of this.messageStates.values()) {
1334
- content += state.totalTextContent;
1335
- for (const stepId of state.thinkingStepOrder) {
1336
- thinking += state.thinkingSteps.get(stepId) ?? "";
1337
- }
1338
- for (const [id, tc] of state.toolCalls) {
1339
- toolCalls.set(id, tc);
1340
- }
1341
- toolCallOrder.push(...state.toolCallOrder);
1342
- }
1343
- return {
1344
- content,
1345
- thinking,
1346
- toolCalls,
1347
- toolCallOrder,
1348
- finishReason: this.finishReason,
1349
- done: this.isDone
1350
- };
1351
- }
1352
- /**
1353
- * Start recording chunks
1354
- */
1355
- startRecording() {
1356
- this.recordingEnabled = true;
1357
- this.recordingStartTime = Date.now();
1358
- this.recording = {
1359
- version: "1.0",
1360
- timestamp: this.recordingStartTime,
1361
- chunks: []
1362
- };
1363
- }
1364
- /**
1365
- * Get the current recording
1366
- */
1367
- getRecording() {
1368
- return this.recording;
1369
- }
1370
- /**
1371
- * Reset stream state (but keep messages)
1372
- */
1373
- resetStreamState() {
1374
- this.messageStates.clear();
1375
- this.activeMessageIds.clear();
1376
- this.activeRuns.clear();
1377
- this.toolCallToMessage.clear();
1378
- this.structuredMessageIds.clear();
1379
- this.structuredOutputUpdateBatches.clear();
1380
- this.pendingManualMessageId = null;
1381
- this.pendingThinkingStepId = null;
1382
- this.finishReason = null;
1383
- this.hasError = false;
1384
- this.isDone = false;
1385
- this.chunkStrategy.reset?.();
1386
- }
1387
- /**
1388
- * Full reset (including messages)
1389
- */
1390
- reset() {
1391
- this.resetStreamState();
1392
- this.messages = [];
1393
- }
1394
- /**
1395
- * Check if a message contains only whitespace text and no other meaningful parts
1396
- * (no tool calls, tool results, thinking, etc.)
1397
- */
1398
- isWhitespaceOnlyMessage(message) {
1399
- if (message.parts.length === 0) return false;
1400
- return message.parts.every(
1401
- (part) => part.type === "text" && part.content.trim() === ""
1402
- );
1403
- }
1404
- /**
1405
- * Replay a recording through the processor
1406
- */
1407
- static async replay(recording, options) {
1408
- const processor = new StreamProcessor(options);
1409
- return processor.process(createReplayStream(recording));
1410
- }
1411
- }
1412
- function createReplayStream(recording) {
1413
- return {
1414
- // eslint-disable-next-line @typescript-eslint/require-await -- async generator required by AsyncIterable contract; body has no await
1415
- async *[Symbol.asyncIterator]() {
1416
- for (const { chunk } of recording.chunks) {
1417
- yield chunk;
1418
- }
1419
- }
1420
- };
1421
- }
1422
- export {
1423
- StreamProcessor,
1424
- createReplayStream
1425
- };
1426
- //# sourceMappingURL=processor.js.map