illuma-agents 1.0.2 → 1.0.4

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 (225) hide show
  1. package/LICENSE +25 -21
  2. package/dist/cjs/agents/AgentContext.cjs +222 -0
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -0
  4. package/dist/cjs/common/enum.cjs +5 -4
  5. package/dist/cjs/common/enum.cjs.map +1 -1
  6. package/dist/cjs/events.cjs +7 -5
  7. package/dist/cjs/events.cjs.map +1 -1
  8. package/dist/cjs/graphs/Graph.cjs +328 -207
  9. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs +507 -0
  11. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -0
  12. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  13. package/dist/cjs/llm/google/index.cjs.map +1 -1
  14. package/dist/cjs/llm/ollama/index.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +35 -0
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/utils/index.cjs +3 -1
  18. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  20. package/dist/cjs/llm/providers.cjs +0 -2
  21. package/dist/cjs/llm/providers.cjs.map +1 -1
  22. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +12 -1
  24. package/dist/cjs/main.cjs.map +1 -1
  25. package/dist/cjs/messages/cache.cjs +123 -0
  26. package/dist/cjs/messages/cache.cjs.map +1 -0
  27. package/dist/cjs/messages/content.cjs +53 -0
  28. package/dist/cjs/messages/content.cjs.map +1 -0
  29. package/dist/cjs/messages/format.cjs +17 -29
  30. package/dist/cjs/messages/format.cjs.map +1 -1
  31. package/dist/cjs/run.cjs +119 -74
  32. package/dist/cjs/run.cjs.map +1 -1
  33. package/dist/cjs/stream.cjs +77 -73
  34. package/dist/cjs/stream.cjs.map +1 -1
  35. package/dist/cjs/tools/Calculator.cjs +45 -0
  36. package/dist/cjs/tools/Calculator.cjs.map +1 -0
  37. package/dist/cjs/tools/CodeExecutor.cjs +22 -22
  38. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +5 -3
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/handlers.cjs +20 -20
  42. package/dist/cjs/tools/handlers.cjs.map +1 -1
  43. package/dist/cjs/utils/events.cjs +31 -0
  44. package/dist/cjs/utils/events.cjs.map +1 -0
  45. package/dist/cjs/utils/handlers.cjs +70 -0
  46. package/dist/cjs/utils/handlers.cjs.map +1 -0
  47. package/dist/cjs/utils/tokens.cjs +54 -7
  48. package/dist/cjs/utils/tokens.cjs.map +1 -1
  49. package/dist/esm/agents/AgentContext.mjs +220 -0
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -0
  51. package/dist/esm/common/enum.mjs +5 -4
  52. package/dist/esm/common/enum.mjs.map +1 -1
  53. package/dist/esm/events.mjs +7 -5
  54. package/dist/esm/events.mjs.map +1 -1
  55. package/dist/esm/graphs/Graph.mjs +330 -209
  56. package/dist/esm/graphs/Graph.mjs.map +1 -1
  57. package/dist/esm/graphs/MultiAgentGraph.mjs +505 -0
  58. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -0
  59. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  60. package/dist/esm/llm/google/index.mjs.map +1 -1
  61. package/dist/esm/llm/ollama/index.mjs.map +1 -1
  62. package/dist/esm/llm/openai/index.mjs +35 -0
  63. package/dist/esm/llm/openai/index.mjs.map +1 -1
  64. package/dist/esm/llm/openai/utils/index.mjs +3 -1
  65. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  66. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  67. package/dist/esm/llm/providers.mjs +0 -2
  68. package/dist/esm/llm/providers.mjs.map +1 -1
  69. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  70. package/dist/esm/main.mjs +7 -2
  71. package/dist/esm/main.mjs.map +1 -1
  72. package/dist/esm/messages/cache.mjs +120 -0
  73. package/dist/esm/messages/cache.mjs.map +1 -0
  74. package/dist/esm/messages/content.mjs +51 -0
  75. package/dist/esm/messages/content.mjs.map +1 -0
  76. package/dist/esm/messages/format.mjs +18 -29
  77. package/dist/esm/messages/format.mjs.map +1 -1
  78. package/dist/esm/run.mjs +119 -74
  79. package/dist/esm/run.mjs.map +1 -1
  80. package/dist/esm/stream.mjs +77 -73
  81. package/dist/esm/stream.mjs.map +1 -1
  82. package/dist/esm/tools/Calculator.mjs +24 -0
  83. package/dist/esm/tools/Calculator.mjs.map +1 -0
  84. package/dist/esm/tools/CodeExecutor.mjs +22 -22
  85. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  86. package/dist/esm/tools/ToolNode.mjs +5 -3
  87. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  88. package/dist/esm/tools/handlers.mjs +20 -20
  89. package/dist/esm/tools/handlers.mjs.map +1 -1
  90. package/dist/esm/utils/events.mjs +29 -0
  91. package/dist/esm/utils/events.mjs.map +1 -0
  92. package/dist/esm/utils/handlers.mjs +68 -0
  93. package/dist/esm/utils/handlers.mjs.map +1 -0
  94. package/dist/esm/utils/tokens.mjs +54 -8
  95. package/dist/esm/utils/tokens.mjs.map +1 -1
  96. package/dist/types/agents/AgentContext.d.ts +94 -0
  97. package/dist/types/common/enum.d.ts +7 -5
  98. package/dist/types/events.d.ts +3 -3
  99. package/dist/types/graphs/Graph.d.ts +60 -66
  100. package/dist/types/graphs/MultiAgentGraph.d.ts +47 -0
  101. package/dist/types/graphs/index.d.ts +1 -0
  102. package/dist/types/index.d.ts +1 -0
  103. package/dist/types/llm/openai/index.d.ts +10 -0
  104. package/dist/types/messages/cache.d.ts +20 -0
  105. package/dist/types/messages/content.d.ts +7 -0
  106. package/dist/types/messages/format.d.ts +1 -7
  107. package/dist/types/messages/index.d.ts +2 -0
  108. package/dist/types/messages/reducer.d.ts +9 -0
  109. package/dist/types/run.d.ts +16 -10
  110. package/dist/types/stream.d.ts +4 -3
  111. package/dist/types/tools/Calculator.d.ts +8 -0
  112. package/dist/types/tools/ToolNode.d.ts +1 -1
  113. package/dist/types/tools/handlers.d.ts +9 -7
  114. package/dist/types/tools/search/tool.d.ts +4 -4
  115. package/dist/types/types/graph.d.ts +124 -11
  116. package/dist/types/types/llm.d.ts +13 -9
  117. package/dist/types/types/messages.d.ts +4 -0
  118. package/dist/types/types/run.d.ts +46 -8
  119. package/dist/types/types/stream.d.ts +3 -2
  120. package/dist/types/utils/events.d.ts +6 -0
  121. package/dist/types/utils/handlers.d.ts +34 -0
  122. package/dist/types/utils/index.d.ts +1 -0
  123. package/dist/types/utils/tokens.d.ts +24 -0
  124. package/package.json +162 -145
  125. package/src/agents/AgentContext.ts +323 -0
  126. package/src/common/enum.ts +177 -176
  127. package/src/events.ts +197 -191
  128. package/src/graphs/Graph.ts +1058 -846
  129. package/src/graphs/MultiAgentGraph.ts +598 -0
  130. package/src/graphs/index.ts +2 -1
  131. package/src/index.ts +25 -24
  132. package/src/llm/anthropic/index.ts +413 -413
  133. package/src/llm/google/index.ts +222 -222
  134. package/src/llm/google/utils/zod_to_genai_parameters.ts +86 -88
  135. package/src/llm/ollama/index.ts +92 -92
  136. package/src/llm/openai/index.ts +894 -853
  137. package/src/llm/openai/utils/index.ts +920 -918
  138. package/src/llm/openrouter/index.ts +60 -60
  139. package/src/llm/providers.ts +55 -57
  140. package/src/llm/vertexai/index.ts +360 -360
  141. package/src/messages/cache.test.ts +461 -0
  142. package/src/messages/cache.ts +151 -0
  143. package/src/messages/content.test.ts +362 -0
  144. package/src/messages/content.ts +63 -0
  145. package/src/messages/format.ts +611 -625
  146. package/src/messages/formatAgentMessages.test.ts +1144 -917
  147. package/src/messages/index.ts +6 -4
  148. package/src/messages/reducer.ts +80 -0
  149. package/src/run.ts +447 -381
  150. package/src/scripts/abort.ts +157 -138
  151. package/src/scripts/ant_web_search.ts +158 -158
  152. package/src/scripts/cli.ts +172 -167
  153. package/src/scripts/cli2.ts +133 -125
  154. package/src/scripts/cli3.ts +184 -178
  155. package/src/scripts/cli4.ts +191 -184
  156. package/src/scripts/cli5.ts +191 -184
  157. package/src/scripts/code_exec.ts +213 -214
  158. package/src/scripts/code_exec_simple.ts +147 -129
  159. package/src/scripts/content.ts +138 -120
  160. package/src/scripts/handoff-test.ts +135 -0
  161. package/src/scripts/multi-agent-chain.ts +278 -0
  162. package/src/scripts/multi-agent-conditional.ts +220 -0
  163. package/src/scripts/multi-agent-document-review-chain.ts +197 -0
  164. package/src/scripts/multi-agent-hybrid-flow.ts +310 -0
  165. package/src/scripts/multi-agent-parallel.ts +343 -0
  166. package/src/scripts/multi-agent-sequence.ts +212 -0
  167. package/src/scripts/multi-agent-supervisor.ts +364 -0
  168. package/src/scripts/multi-agent-test.ts +186 -0
  169. package/src/scripts/search.ts +146 -150
  170. package/src/scripts/simple.ts +225 -225
  171. package/src/scripts/stream.ts +140 -122
  172. package/src/scripts/test-custom-prompt-key.ts +145 -0
  173. package/src/scripts/test-handoff-input.ts +170 -0
  174. package/src/scripts/test-multi-agent-list-handoff.ts +261 -0
  175. package/src/scripts/test-tools-before-handoff.ts +222 -0
  176. package/src/scripts/tools.ts +153 -155
  177. package/src/specs/agent-handoffs.test.ts +889 -0
  178. package/src/specs/anthropic.simple.test.ts +320 -317
  179. package/src/specs/azure.simple.test.ts +325 -316
  180. package/src/specs/openai.simple.test.ts +311 -316
  181. package/src/specs/openrouter.simple.test.ts +107 -0
  182. package/src/specs/prune.test.ts +758 -763
  183. package/src/specs/reasoning.test.ts +201 -165
  184. package/src/specs/thinking-prune.test.ts +769 -703
  185. package/src/specs/token-memoization.test.ts +39 -0
  186. package/src/stream.ts +664 -651
  187. package/src/tools/Calculator.test.ts +278 -0
  188. package/src/tools/Calculator.ts +25 -0
  189. package/src/tools/CodeExecutor.ts +220 -220
  190. package/src/tools/ToolNode.ts +170 -170
  191. package/src/tools/handlers.ts +341 -336
  192. package/src/types/graph.ts +372 -185
  193. package/src/types/llm.ts +141 -140
  194. package/src/types/messages.ts +4 -0
  195. package/src/types/run.ts +128 -89
  196. package/src/types/stream.ts +401 -400
  197. package/src/utils/events.ts +32 -0
  198. package/src/utils/handlers.ts +107 -0
  199. package/src/utils/index.ts +6 -5
  200. package/src/utils/llmConfig.ts +183 -183
  201. package/src/utils/tokens.ts +129 -70
  202. package/dist/types/scripts/abort.d.ts +0 -1
  203. package/dist/types/scripts/ant_web_search.d.ts +0 -1
  204. package/dist/types/scripts/args.d.ts +0 -7
  205. package/dist/types/scripts/caching.d.ts +0 -1
  206. package/dist/types/scripts/cli.d.ts +0 -1
  207. package/dist/types/scripts/cli2.d.ts +0 -1
  208. package/dist/types/scripts/cli3.d.ts +0 -1
  209. package/dist/types/scripts/cli4.d.ts +0 -1
  210. package/dist/types/scripts/cli5.d.ts +0 -1
  211. package/dist/types/scripts/code_exec.d.ts +0 -1
  212. package/dist/types/scripts/code_exec_files.d.ts +0 -1
  213. package/dist/types/scripts/code_exec_simple.d.ts +0 -1
  214. package/dist/types/scripts/content.d.ts +0 -1
  215. package/dist/types/scripts/empty_input.d.ts +0 -1
  216. package/dist/types/scripts/image.d.ts +0 -1
  217. package/dist/types/scripts/memory.d.ts +0 -1
  218. package/dist/types/scripts/search.d.ts +0 -1
  219. package/dist/types/scripts/simple.d.ts +0 -1
  220. package/dist/types/scripts/stream.d.ts +0 -1
  221. package/dist/types/scripts/thinking.d.ts +0 -1
  222. package/dist/types/scripts/tools.d.ts +0 -1
  223. package/dist/types/specs/spec.utils.d.ts +0 -1
  224. package/dist/types/tools/example.d.ts +0 -78
  225. package/src/tools/example.ts +0 -129
package/src/stream.ts CHANGED
@@ -1,651 +1,664 @@
1
- // src/stream.ts
2
- import type { ChatOpenAIReasoningSummary } from '@langchain/openai';
3
- import type { AIMessageChunk } from '@langchain/core/messages';
4
- import type { ToolCall } from '@langchain/core/messages/tool';
5
- import type { Graph } from '@/graphs';
6
- import type * as t from '@/types';
7
- import {
8
- ToolCallTypes,
9
- ContentTypes,
10
- GraphEvents,
11
- StepTypes,
12
- Providers,
13
- } from '@/common';
14
- import {
15
- handleServerToolResult,
16
- handleToolCallChunks,
17
- handleToolCalls,
18
- } from '@/tools/handlers';
19
- import { getMessageId } from '@/messages';
20
-
21
- /**
22
- * Parses content to extract thinking sections enclosed in <think> tags using string operations
23
- * @param content The content to parse
24
- * @returns An object with separated text and thinking content
25
- */
26
- function parseThinkingContent(content: string): {
27
- text: string;
28
- thinking: string;
29
- } {
30
- // If no think tags, return the original content as text
31
- if (!content.includes('<think>')) {
32
- return { text: content, thinking: '' };
33
- }
34
-
35
- let textResult = '';
36
- const thinkingResult: string[] = [];
37
- let position = 0;
38
-
39
- while (position < content.length) {
40
- const thinkStart = content.indexOf('<think>', position);
41
-
42
- if (thinkStart === -1) {
43
- // No more think tags, add the rest and break
44
- textResult += content.slice(position);
45
- break;
46
- }
47
-
48
- // Add text before the think tag
49
- textResult += content.slice(position, thinkStart);
50
-
51
- const thinkEnd = content.indexOf('</think>', thinkStart);
52
- if (thinkEnd === -1) {
53
- // Malformed input, no closing tag
54
- textResult += content.slice(thinkStart);
55
- break;
56
- }
57
-
58
- // Add the thinking content
59
- const thinkContent = content.slice(thinkStart + 7, thinkEnd);
60
- thinkingResult.push(thinkContent);
61
-
62
- // Move position to after the think tag
63
- position = thinkEnd + 8; // 8 is the length of '</think>'
64
- }
65
-
66
- return {
67
- text: textResult.trim(),
68
- thinking: thinkingResult.join('\n').trim(),
69
- };
70
- }
71
-
72
- function getNonEmptyValue(possibleValues: string[]): string | undefined {
73
- for (const value of possibleValues) {
74
- if (value && value.trim() !== '') {
75
- return value;
76
- }
77
- }
78
- return undefined;
79
- }
80
-
81
- export function getChunkContent({
82
- chunk,
83
- provider,
84
- reasoningKey,
85
- }: {
86
- chunk?: Partial<AIMessageChunk>;
87
- provider?: Providers;
88
- reasoningKey: 'reasoning_content' | 'reasoning';
89
- }): string | t.MessageContentComplex[] | undefined {
90
- if (
91
- (provider === Providers.OPENAI || provider === Providers.AZURE) &&
92
- (
93
- chunk?.additional_kwargs?.reasoning as
94
- | Partial<ChatOpenAIReasoningSummary>
95
- | undefined
96
- )?.summary?.[0]?.text != null &&
97
- ((
98
- chunk?.additional_kwargs?.reasoning as
99
- | Partial<ChatOpenAIReasoningSummary>
100
- | undefined
101
- )?.summary?.[0]?.text?.length ?? 0) > 0
102
- ) {
103
- return (
104
- chunk?.additional_kwargs?.reasoning as
105
- | Partial<ChatOpenAIReasoningSummary>
106
- | undefined
107
- )?.summary?.[0]?.text;
108
- }
109
- return (
110
- ((chunk?.additional_kwargs?.[reasoningKey] as string | undefined) ?? '') ||
111
- chunk?.content
112
- );
113
- }
114
-
115
- export class ChatModelStreamHandler implements t.EventHandler {
116
- handle(
117
- event: string,
118
- data: t.StreamEventData,
119
- metadata?: Record<string, unknown>,
120
- graph?: Graph
121
- ): void {
122
- if (!graph) {
123
- throw new Error('Graph not found');
124
- }
125
- if (!graph.config) {
126
- throw new Error('Config not found in graph');
127
- }
128
- if (!data.chunk) {
129
- console.warn(`No chunk found in ${event} event`);
130
- return;
131
- }
132
-
133
- const chunk = data.chunk as Partial<AIMessageChunk>;
134
- const content = getChunkContent({
135
- chunk,
136
- reasoningKey: graph.reasoningKey,
137
- provider: metadata?.provider as Providers,
138
- });
139
- const skipHandling = handleServerToolResult({
140
- content,
141
- metadata,
142
- graph,
143
- });
144
- if (skipHandling) {
145
- return;
146
- }
147
- this.handleReasoning(chunk, graph, metadata?.provider as Providers);
148
- let hasToolCalls = false;
149
- if (
150
- chunk.tool_calls &&
151
- chunk.tool_calls.length > 0 &&
152
- chunk.tool_calls.every((tc) => tc.id != null && tc.id !== '')
153
- ) {
154
- hasToolCalls = true;
155
- handleToolCalls(chunk.tool_calls, metadata, graph);
156
- }
157
-
158
- const hasToolCallChunks =
159
- (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) ?? false;
160
- const isEmptyContent =
161
- typeof content === 'undefined' ||
162
- !content.length ||
163
- (typeof content === 'string' && !content);
164
- const isEmptyChunk = isEmptyContent && !hasToolCallChunks;
165
- const chunkId = chunk.id ?? '';
166
- if (isEmptyChunk && chunkId && chunkId.startsWith('msg')) {
167
- if (graph.messageIdsByStepKey.has(chunkId)) {
168
- return;
169
- } else if (graph.prelimMessageIdsByStepKey.has(chunkId)) {
170
- return;
171
- }
172
-
173
- const stepKey = graph.getStepKey(metadata);
174
- graph.prelimMessageIdsByStepKey.set(stepKey, chunkId);
175
- return;
176
- } else if (isEmptyChunk) {
177
- return;
178
- }
179
-
180
- const stepKey = graph.getStepKey(metadata);
181
-
182
- if (
183
- hasToolCallChunks &&
184
- chunk.tool_call_chunks &&
185
- chunk.tool_call_chunks.length &&
186
- typeof chunk.tool_call_chunks[0]?.index === 'number'
187
- ) {
188
- handleToolCallChunks({
189
- graph,
190
- stepKey,
191
- toolCallChunks: chunk.tool_call_chunks,
192
- });
193
- }
194
-
195
- if (isEmptyContent) {
196
- return;
197
- }
198
-
199
- const message_id = getMessageId(stepKey, graph) ?? '';
200
- if (message_id) {
201
- graph.dispatchRunStep(stepKey, {
202
- type: StepTypes.MESSAGE_CREATION,
203
- message_creation: {
204
- message_id,
205
- },
206
- });
207
- }
208
-
209
- const stepId = graph.getStepIdByKey(stepKey);
210
- const runStep = graph.getRunStep(stepId);
211
- if (!runStep) {
212
- console.warn(`\n
213
- ==============================================================
214
-
215
-
216
- Run step for ${stepId} does not exist, cannot dispatch delta event.
217
-
218
- event: ${event}
219
- stepId: ${stepId}
220
- stepKey: ${stepKey}
221
- message_id: ${message_id}
222
- hasToolCalls: ${hasToolCalls}
223
- hasToolCallChunks: ${hasToolCallChunks}
224
-
225
- ==============================================================
226
- \n`);
227
- return;
228
- }
229
-
230
- /* Note: tool call chunks may have non-empty content that matches the current tool chunk generation */
231
- if (typeof content === 'string' && runStep.type === StepTypes.TOOL_CALLS) {
232
- return;
233
- } else if (
234
- hasToolCallChunks &&
235
- (chunk.tool_call_chunks?.some((tc) => tc.args === content) ?? false)
236
- ) {
237
- return;
238
- } else if (typeof content === 'string') {
239
- if (graph.currentTokenType === ContentTypes.TEXT) {
240
- graph.dispatchMessageDelta(stepId, {
241
- content: [
242
- {
243
- type: ContentTypes.TEXT,
244
- text: content,
245
- },
246
- ],
247
- });
248
- } else if (graph.currentTokenType === 'think_and_text') {
249
- const { text, thinking } = parseThinkingContent(content);
250
- if (thinking) {
251
- graph.dispatchReasoningDelta(stepId, {
252
- content: [
253
- {
254
- type: ContentTypes.THINK,
255
- think: thinking,
256
- },
257
- ],
258
- });
259
- }
260
- if (text) {
261
- graph.currentTokenType = ContentTypes.TEXT;
262
- graph.tokenTypeSwitch = 'content';
263
- const newStepKey = graph.getStepKey(metadata);
264
- const message_id = getMessageId(newStepKey, graph) ?? '';
265
- graph.dispatchRunStep(newStepKey, {
266
- type: StepTypes.MESSAGE_CREATION,
267
- message_creation: {
268
- message_id,
269
- },
270
- });
271
-
272
- const newStepId = graph.getStepIdByKey(newStepKey);
273
- graph.dispatchMessageDelta(newStepId, {
274
- content: [
275
- {
276
- type: ContentTypes.TEXT,
277
- text: text,
278
- },
279
- ],
280
- });
281
- }
282
- } else {
283
- graph.dispatchReasoningDelta(stepId, {
284
- content: [
285
- {
286
- type: ContentTypes.THINK,
287
- think: content,
288
- },
289
- ],
290
- });
291
- }
292
- } else if (
293
- content.every((c) => c.type?.startsWith(ContentTypes.TEXT) ?? false)
294
- ) {
295
- graph.dispatchMessageDelta(stepId, {
296
- content,
297
- });
298
- } else if (
299
- content.every(
300
- (c) =>
301
- (c.type?.startsWith(ContentTypes.THINKING) ?? false) ||
302
- (c.type?.startsWith(ContentTypes.REASONING) ?? false) ||
303
- (c.type?.startsWith(ContentTypes.REASONING_CONTENT) ?? false)
304
- )
305
- ) {
306
- graph.dispatchReasoningDelta(stepId, {
307
- content: content.map((c) => ({
308
- type: ContentTypes.THINK,
309
- think:
310
- (c as t.ThinkingContentText).thinking ??
311
- (c as Partial<t.GoogleReasoningContentText>).reasoning ??
312
- (c as Partial<t.BedrockReasoningContentText>).reasoningText?.text ??
313
- '',
314
- })),
315
- });
316
- }
317
- }
318
- handleReasoning(
319
- chunk: Partial<AIMessageChunk>,
320
- graph: Graph,
321
- provider?: Providers
322
- ): void {
323
- let reasoning_content = chunk.additional_kwargs?.[graph.reasoningKey] as
324
- | string
325
- | Partial<ChatOpenAIReasoningSummary>
326
- | undefined;
327
- if (
328
- Array.isArray(chunk.content) &&
329
- (chunk.content[0]?.type === ContentTypes.THINKING ||
330
- chunk.content[0]?.type === ContentTypes.REASONING ||
331
- chunk.content[0]?.type === ContentTypes.REASONING_CONTENT)
332
- ) {
333
- reasoning_content = 'valid';
334
- } else if (
335
- (provider === Providers.OPENAI || provider === Providers.AZURE) &&
336
- reasoning_content != null &&
337
- typeof reasoning_content !== 'string' &&
338
- reasoning_content.summary?.[0]?.text != null &&
339
- reasoning_content.summary[0].text
340
- ) {
341
- reasoning_content = 'valid';
342
- }
343
- if (
344
- reasoning_content != null &&
345
- reasoning_content !== '' &&
346
- (chunk.content == null ||
347
- chunk.content === '' ||
348
- reasoning_content === 'valid')
349
- ) {
350
- graph.currentTokenType = ContentTypes.THINK;
351
- graph.tokenTypeSwitch = 'reasoning';
352
- return;
353
- } else if (
354
- graph.tokenTypeSwitch === 'reasoning' &&
355
- graph.currentTokenType !== ContentTypes.TEXT &&
356
- ((chunk.content != null && chunk.content !== '') ||
357
- (chunk.tool_calls?.length ?? 0) > 0)
358
- ) {
359
- graph.currentTokenType = ContentTypes.TEXT;
360
- graph.tokenTypeSwitch = 'content';
361
- } else if (
362
- chunk.content != null &&
363
- typeof chunk.content === 'string' &&
364
- chunk.content.includes('<think>') &&
365
- chunk.content.includes('</think>')
366
- ) {
367
- graph.currentTokenType = 'think_and_text';
368
- graph.tokenTypeSwitch = 'content';
369
- } else if (
370
- chunk.content != null &&
371
- typeof chunk.content === 'string' &&
372
- chunk.content.includes('<think>')
373
- ) {
374
- graph.currentTokenType = ContentTypes.THINK;
375
- graph.tokenTypeSwitch = 'content';
376
- } else if (
377
- graph.lastToken != null &&
378
- graph.lastToken.includes('</think>')
379
- ) {
380
- graph.currentTokenType = ContentTypes.TEXT;
381
- graph.tokenTypeSwitch = 'content';
382
- }
383
- if (typeof chunk.content !== 'string') {
384
- return;
385
- }
386
- graph.lastToken = chunk.content;
387
- }
388
- }
389
-
390
- export function createContentAggregator(): t.ContentAggregatorResult {
391
- const contentParts: Array<t.MessageContentComplex | undefined> = [];
392
- const stepMap = new Map<string, t.RunStep>();
393
- const toolCallIdMap = new Map<string, string>();
394
-
395
- const updateContent = (
396
- index: number,
397
- contentPart?: t.MessageContentComplex,
398
- finalUpdate = false
399
- ): void => {
400
- if (!contentPart) {
401
- console.warn('No content part found in \'updateContent\'');
402
- return;
403
- }
404
- const partType = contentPart.type ?? '';
405
- if (!partType) {
406
- console.warn('No content type found in content part');
407
- return;
408
- }
409
-
410
- if (!contentParts[index]) {
411
- contentParts[index] = { type: partType };
412
- }
413
-
414
- if (!partType.startsWith(contentParts[index]?.type ?? '')) {
415
- console.warn('Content type mismatch');
416
- return;
417
- }
418
-
419
- if (
420
- partType.startsWith(ContentTypes.TEXT) &&
421
- ContentTypes.TEXT in contentPart &&
422
- typeof contentPart.text === 'string'
423
- ) {
424
- // TODO: update this!!
425
- const currentContent = contentParts[index] as t.MessageDeltaUpdate;
426
- const update: t.MessageDeltaUpdate = {
427
- type: ContentTypes.TEXT,
428
- text: (currentContent.text || '') + contentPart.text,
429
- };
430
-
431
- if (contentPart.tool_call_ids) {
432
- update.tool_call_ids = contentPart.tool_call_ids;
433
- }
434
- contentParts[index] = update;
435
- } else if (
436
- partType.startsWith(ContentTypes.THINK) &&
437
- ContentTypes.THINK in contentPart &&
438
- typeof contentPart.think === 'string'
439
- ) {
440
- const currentContent = contentParts[index] as t.ReasoningDeltaUpdate;
441
- const update: t.ReasoningDeltaUpdate = {
442
- type: ContentTypes.THINK,
443
- think: (currentContent.think || '') + contentPart.think,
444
- };
445
- contentParts[index] = update;
446
- } else if (
447
- partType.startsWith(ContentTypes.AGENT_UPDATE) &&
448
- ContentTypes.AGENT_UPDATE in contentPart &&
449
- contentPart.agent_update != null
450
- ) {
451
- const update: t.AgentUpdate = {
452
- type: ContentTypes.AGENT_UPDATE,
453
- agent_update: contentPart.agent_update,
454
- };
455
-
456
- contentParts[index] = update;
457
- } else if (
458
- partType === ContentTypes.IMAGE_URL &&
459
- 'image_url' in contentPart
460
- ) {
461
- const currentContent = contentParts[index] as {
462
- type: 'image_url';
463
- image_url: string;
464
- };
465
- contentParts[index] = {
466
- ...currentContent,
467
- };
468
- } else if (
469
- partType === ContentTypes.TOOL_CALL &&
470
- 'tool_call' in contentPart
471
- ) {
472
- const existingContent = contentParts[index] as
473
- | (Omit<t.ToolCallContent, 'tool_call'> & {
474
- tool_call?: t.ToolCallPart;
475
- })
476
- | undefined;
477
-
478
- const toolCallArgs = (contentPart.tool_call as t.ToolCallPart).args;
479
- /** When args are a valid object, they are likely already invoked */
480
- let args =
481
- finalUpdate ||
482
- typeof existingContent?.tool_call?.args === 'object' ||
483
- typeof toolCallArgs === 'object'
484
- ? contentPart.tool_call.args
485
- : (existingContent?.tool_call?.args ?? '') + (toolCallArgs ?? '');
486
- if (
487
- finalUpdate &&
488
- args == null &&
489
- existingContent?.tool_call?.args != null
490
- ) {
491
- args = existingContent.tool_call.args;
492
- }
493
-
494
- const id =
495
- getNonEmptyValue([
496
- contentPart.tool_call.id,
497
- existingContent?.tool_call?.id,
498
- ]) ?? '';
499
- const name =
500
- getNonEmptyValue([
501
- contentPart.tool_call.name,
502
- existingContent?.tool_call?.name,
503
- ]) ?? '';
504
-
505
- const newToolCall: ToolCall & t.PartMetadata = {
506
- id,
507
- name,
508
- args,
509
- type: ToolCallTypes.TOOL_CALL,
510
- };
511
-
512
- if (finalUpdate) {
513
- newToolCall.progress = 1;
514
- newToolCall.output = contentPart.tool_call.output;
515
- }
516
-
517
- contentParts[index] = {
518
- type: ContentTypes.TOOL_CALL,
519
- tool_call: newToolCall,
520
- };
521
- }
522
- };
523
-
524
- const aggregateContent = ({
525
- event,
526
- data,
527
- }: {
528
- event: GraphEvents;
529
- data:
530
- | t.RunStep
531
- | t.AgentUpdate
532
- | t.MessageDeltaEvent
533
- | t.RunStepDeltaEvent
534
- | { result: t.ToolEndEvent };
535
- }): void => {
536
- if (event === GraphEvents.ON_RUN_STEP) {
537
- const runStep = data as t.RunStep;
538
- stepMap.set(runStep.id, runStep);
539
-
540
- // Store tool call IDs if present
541
- if (
542
- runStep.stepDetails.type === StepTypes.TOOL_CALLS &&
543
- runStep.stepDetails.tool_calls
544
- ) {
545
- (runStep.stepDetails.tool_calls as ToolCall[]).forEach((toolCall) => {
546
- const toolCallId = toolCall.id ?? '';
547
- if ('id' in toolCall && toolCallId) {
548
- toolCallIdMap.set(runStep.id, toolCallId);
549
- }
550
- const contentPart: t.MessageContentComplex = {
551
- type: ContentTypes.TOOL_CALL,
552
- tool_call: {
553
- args: toolCall.args,
554
- name: toolCall.name,
555
- id: toolCallId,
556
- },
557
- };
558
-
559
- updateContent(runStep.index, contentPart);
560
- });
561
- }
562
- } else if (event === GraphEvents.ON_MESSAGE_DELTA) {
563
- const messageDelta = data as t.MessageDeltaEvent;
564
- const runStep = stepMap.get(messageDelta.id);
565
- if (!runStep) {
566
- console.warn('No run step or runId found for message delta event');
567
- return;
568
- }
569
-
570
- if (messageDelta.delta.content) {
571
- const contentPart = Array.isArray(messageDelta.delta.content)
572
- ? messageDelta.delta.content[0]
573
- : messageDelta.delta.content;
574
-
575
- updateContent(runStep.index, contentPart);
576
- }
577
- } else if (
578
- event === GraphEvents.ON_AGENT_UPDATE &&
579
- (data as t.AgentUpdate | undefined)?.agent_update
580
- ) {
581
- const contentPart = data as t.AgentUpdate | undefined;
582
- if (!contentPart) {
583
- return;
584
- }
585
- updateContent(contentPart.agent_update.index, contentPart);
586
- } else if (event === GraphEvents.ON_REASONING_DELTA) {
587
- const reasoningDelta = data as t.ReasoningDeltaEvent;
588
- const runStep = stepMap.get(reasoningDelta.id);
589
- if (!runStep) {
590
- console.warn('No run step or runId found for reasoning delta event');
591
- return;
592
- }
593
-
594
- if (reasoningDelta.delta.content) {
595
- const contentPart = Array.isArray(reasoningDelta.delta.content)
596
- ? reasoningDelta.delta.content[0]
597
- : reasoningDelta.delta.content;
598
-
599
- updateContent(runStep.index, contentPart);
600
- }
601
- } else if (event === GraphEvents.ON_RUN_STEP_DELTA) {
602
- const runStepDelta = data as t.RunStepDeltaEvent;
603
- const runStep = stepMap.get(runStepDelta.id);
604
- if (!runStep) {
605
- console.warn('No run step or runId found for run step delta event');
606
- return;
607
- }
608
-
609
- if (
610
- runStepDelta.delta.type === StepTypes.TOOL_CALLS &&
611
- runStepDelta.delta.tool_calls
612
- ) {
613
- runStepDelta.delta.tool_calls.forEach((toolCallDelta) => {
614
- const toolCallId = toolCallIdMap.get(runStepDelta.id);
615
-
616
- const contentPart: t.MessageContentComplex = {
617
- type: ContentTypes.TOOL_CALL,
618
- tool_call: {
619
- args: toolCallDelta.args ?? '',
620
- name: toolCallDelta.name,
621
- id: toolCallId,
622
- },
623
- };
624
-
625
- updateContent(runStep.index, contentPart);
626
- });
627
- }
628
- } else if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
629
- const { result } = data as unknown as { result: t.ToolEndEvent };
630
-
631
- const { id: stepId } = result;
632
-
633
- const runStep = stepMap.get(stepId);
634
- if (!runStep) {
635
- console.warn(
636
- 'No run step or runId found for completed tool call event'
637
- );
638
- return;
639
- }
640
-
641
- const contentPart: t.MessageContentComplex = {
642
- type: ContentTypes.TOOL_CALL,
643
- tool_call: result.tool_call,
644
- };
645
-
646
- updateContent(runStep.index, contentPart, true);
647
- }
648
- };
649
-
650
- return { contentParts, aggregateContent, stepMap };
651
- }
1
+ // src/stream.ts
2
+ import type { ChatOpenAIReasoningSummary } from '@langchain/openai';
3
+ import type { AIMessageChunk } from '@langchain/core/messages';
4
+ import type { ToolCall } from '@langchain/core/messages/tool';
5
+ import type { AgentContext } from '@/agents/AgentContext';
6
+ import type { StandardGraph } from '@/graphs';
7
+ import type * as t from '@/types';
8
+ import {
9
+ ToolCallTypes,
10
+ ContentTypes,
11
+ GraphEvents,
12
+ StepTypes,
13
+ Providers,
14
+ } from '@/common';
15
+ import {
16
+ handleServerToolResult,
17
+ handleToolCallChunks,
18
+ handleToolCalls,
19
+ } from '@/tools/handlers';
20
+ import { getMessageId } from '@/messages';
21
+
22
+ /**
23
+ * Parses content to extract thinking sections enclosed in <think> tags using string operations
24
+ * @param content The content to parse
25
+ * @returns An object with separated text and thinking content
26
+ */
27
+ function parseThinkingContent(content: string): {
28
+ text: string;
29
+ thinking: string;
30
+ } {
31
+ // If no think tags, return the original content as text
32
+ if (!content.includes('<think>')) {
33
+ return { text: content, thinking: '' };
34
+ }
35
+
36
+ let textResult = '';
37
+ const thinkingResult: string[] = [];
38
+ let position = 0;
39
+
40
+ while (position < content.length) {
41
+ const thinkStart = content.indexOf('<think>', position);
42
+
43
+ if (thinkStart === -1) {
44
+ // No more think tags, add the rest and break
45
+ textResult += content.slice(position);
46
+ break;
47
+ }
48
+
49
+ // Add text before the think tag
50
+ textResult += content.slice(position, thinkStart);
51
+
52
+ const thinkEnd = content.indexOf('</think>', thinkStart);
53
+ if (thinkEnd === -1) {
54
+ // Malformed input, no closing tag
55
+ textResult += content.slice(thinkStart);
56
+ break;
57
+ }
58
+
59
+ // Add the thinking content
60
+ const thinkContent = content.slice(thinkStart + 7, thinkEnd);
61
+ thinkingResult.push(thinkContent);
62
+
63
+ // Move position to after the think tag
64
+ position = thinkEnd + 8; // 8 is the length of '</think>'
65
+ }
66
+
67
+ return {
68
+ text: textResult.trim(),
69
+ thinking: thinkingResult.join('\n').trim(),
70
+ };
71
+ }
72
+
73
+ function getNonEmptyValue(possibleValues: string[]): string | undefined {
74
+ for (const value of possibleValues) {
75
+ if (value && value.trim() !== '') {
76
+ return value;
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ export function getChunkContent({
83
+ chunk,
84
+ provider,
85
+ reasoningKey,
86
+ }: {
87
+ chunk?: Partial<AIMessageChunk>;
88
+ provider?: Providers;
89
+ reasoningKey: 'reasoning_content' | 'reasoning';
90
+ }): string | t.MessageContentComplex[] | undefined {
91
+ if (
92
+ (provider === Providers.OPENAI || provider === Providers.AZURE) &&
93
+ (
94
+ chunk?.additional_kwargs?.reasoning as
95
+ | Partial<ChatOpenAIReasoningSummary>
96
+ | undefined
97
+ )?.summary?.[0]?.text != null &&
98
+ ((
99
+ chunk?.additional_kwargs?.reasoning as
100
+ | Partial<ChatOpenAIReasoningSummary>
101
+ | undefined
102
+ )?.summary?.[0]?.text?.length ?? 0) > 0
103
+ ) {
104
+ return (
105
+ chunk?.additional_kwargs?.reasoning as
106
+ | Partial<ChatOpenAIReasoningSummary>
107
+ | undefined
108
+ )?.summary?.[0]?.text;
109
+ }
110
+ return (
111
+ ((chunk?.additional_kwargs?.[reasoningKey] as string | undefined) ?? '') ||
112
+ chunk?.content
113
+ );
114
+ }
115
+
116
+ export class ChatModelStreamHandler implements t.EventHandler {
117
+ async handle(
118
+ event: string,
119
+ data: t.StreamEventData,
120
+ metadata?: Record<string, unknown>,
121
+ graph?: StandardGraph
122
+ ): Promise<void> {
123
+ if (!graph) {
124
+ throw new Error('Graph not found');
125
+ }
126
+ if (!graph.config) {
127
+ throw new Error('Config not found in graph');
128
+ }
129
+ if (!data.chunk) {
130
+ console.warn(`No chunk found in ${event} event`);
131
+ return;
132
+ }
133
+
134
+ const agentContext = graph.getAgentContext(metadata);
135
+
136
+ const chunk = data.chunk as Partial<AIMessageChunk>;
137
+ const content = getChunkContent({
138
+ chunk,
139
+ reasoningKey: agentContext.reasoningKey,
140
+ provider: agentContext.provider,
141
+ });
142
+ const skipHandling = await handleServerToolResult({
143
+ graph,
144
+ content,
145
+ metadata,
146
+ agentContext,
147
+ });
148
+ if (skipHandling) {
149
+ return;
150
+ }
151
+ this.handleReasoning(chunk, agentContext);
152
+ let hasToolCalls = false;
153
+ if (
154
+ chunk.tool_calls &&
155
+ chunk.tool_calls.length > 0 &&
156
+ chunk.tool_calls.every(
157
+ (tc) =>
158
+ tc.id != null && tc.id !== '' && tc.name != null && tc.name !== ''
159
+ )
160
+ ) {
161
+ hasToolCalls = true;
162
+ await handleToolCalls(chunk.tool_calls, metadata, graph);
163
+ }
164
+
165
+ const hasToolCallChunks =
166
+ (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) ?? false;
167
+ const isEmptyContent =
168
+ typeof content === 'undefined' ||
169
+ !content.length ||
170
+ (typeof content === 'string' && !content);
171
+
172
+ /** Set a preliminary message ID if found in empty chunk */
173
+ const isEmptyChunk = isEmptyContent && !hasToolCallChunks;
174
+ if (
175
+ isEmptyChunk &&
176
+ (chunk.id ?? '') !== '' &&
177
+ !graph.prelimMessageIdsByStepKey.has(chunk.id ?? '')
178
+ ) {
179
+ const stepKey = graph.getStepKey(metadata);
180
+ graph.prelimMessageIdsByStepKey.set(stepKey, chunk.id ?? '');
181
+ } else if (isEmptyChunk) {
182
+ return;
183
+ }
184
+
185
+ const stepKey = graph.getStepKey(metadata);
186
+
187
+ if (
188
+ hasToolCallChunks &&
189
+ chunk.tool_call_chunks &&
190
+ chunk.tool_call_chunks.length &&
191
+ typeof chunk.tool_call_chunks[0]?.index === 'number'
192
+ ) {
193
+ await handleToolCallChunks({
194
+ graph,
195
+ stepKey,
196
+ toolCallChunks: chunk.tool_call_chunks,
197
+ });
198
+ }
199
+
200
+ if (isEmptyContent) {
201
+ return;
202
+ }
203
+
204
+ const message_id = getMessageId(stepKey, graph) ?? '';
205
+ if (message_id) {
206
+ await graph.dispatchRunStep(stepKey, {
207
+ type: StepTypes.MESSAGE_CREATION,
208
+ message_creation: {
209
+ message_id,
210
+ },
211
+ });
212
+ }
213
+
214
+ const stepId = graph.getStepIdByKey(stepKey);
215
+ const runStep = graph.getRunStep(stepId);
216
+ if (!runStep) {
217
+ console.warn(`\n
218
+ ==============================================================
219
+
220
+
221
+ Run step for ${stepId} does not exist, cannot dispatch delta event.
222
+
223
+ event: ${event}
224
+ stepId: ${stepId}
225
+ stepKey: ${stepKey}
226
+ message_id: ${message_id}
227
+ hasToolCalls: ${hasToolCalls}
228
+ hasToolCallChunks: ${hasToolCallChunks}
229
+
230
+ ==============================================================
231
+ \n`);
232
+ return;
233
+ }
234
+
235
+ /* Note: tool call chunks may have non-empty content that matches the current tool chunk generation */
236
+ if (typeof content === 'string' && runStep.type === StepTypes.TOOL_CALLS) {
237
+ return;
238
+ } else if (
239
+ hasToolCallChunks &&
240
+ (chunk.tool_call_chunks?.some((tc) => tc.args === content) ?? false)
241
+ ) {
242
+ return;
243
+ } else if (typeof content === 'string') {
244
+ if (agentContext.currentTokenType === ContentTypes.TEXT) {
245
+ await graph.dispatchMessageDelta(stepId, {
246
+ content: [
247
+ {
248
+ type: ContentTypes.TEXT,
249
+ text: content,
250
+ },
251
+ ],
252
+ });
253
+ } else if (agentContext.currentTokenType === 'think_and_text') {
254
+ const { text, thinking } = parseThinkingContent(content);
255
+ if (thinking) {
256
+ await graph.dispatchReasoningDelta(stepId, {
257
+ content: [
258
+ {
259
+ type: ContentTypes.THINK,
260
+ think: thinking,
261
+ },
262
+ ],
263
+ });
264
+ }
265
+ if (text) {
266
+ agentContext.currentTokenType = ContentTypes.TEXT;
267
+ agentContext.tokenTypeSwitch = 'content';
268
+ const newStepKey = graph.getStepKey(metadata);
269
+ const message_id = getMessageId(newStepKey, graph) ?? '';
270
+ await graph.dispatchRunStep(newStepKey, {
271
+ type: StepTypes.MESSAGE_CREATION,
272
+ message_creation: {
273
+ message_id,
274
+ },
275
+ });
276
+
277
+ const newStepId = graph.getStepIdByKey(newStepKey);
278
+ await graph.dispatchMessageDelta(newStepId, {
279
+ content: [
280
+ {
281
+ type: ContentTypes.TEXT,
282
+ text: text,
283
+ },
284
+ ],
285
+ });
286
+ }
287
+ } else {
288
+ await graph.dispatchReasoningDelta(stepId, {
289
+ content: [
290
+ {
291
+ type: ContentTypes.THINK,
292
+ think: content,
293
+ },
294
+ ],
295
+ });
296
+ }
297
+ } else if (
298
+ content.every((c) => c.type?.startsWith(ContentTypes.TEXT) ?? false)
299
+ ) {
300
+ await graph.dispatchMessageDelta(stepId, {
301
+ content,
302
+ });
303
+ } else if (
304
+ content.every(
305
+ (c) =>
306
+ (c.type?.startsWith(ContentTypes.THINKING) ?? false) ||
307
+ (c.type?.startsWith(ContentTypes.REASONING) ?? false) ||
308
+ (c.type?.startsWith(ContentTypes.REASONING_CONTENT) ?? false)
309
+ )
310
+ ) {
311
+ await graph.dispatchReasoningDelta(stepId, {
312
+ content: content.map((c) => ({
313
+ type: ContentTypes.THINK,
314
+ think:
315
+ (c as t.ThinkingContentText).thinking ??
316
+ (c as Partial<t.GoogleReasoningContentText>).reasoning ??
317
+ (c as Partial<t.BedrockReasoningContentText>).reasoningText?.text ??
318
+ '',
319
+ })),
320
+ });
321
+ }
322
+ }
323
+ handleReasoning(
324
+ chunk: Partial<AIMessageChunk>,
325
+ agentContext: AgentContext
326
+ ): void {
327
+ let reasoning_content = chunk.additional_kwargs?.[
328
+ agentContext.reasoningKey
329
+ ] as string | Partial<ChatOpenAIReasoningSummary> | undefined;
330
+ if (
331
+ Array.isArray(chunk.content) &&
332
+ (chunk.content[0]?.type === ContentTypes.THINKING ||
333
+ chunk.content[0]?.type === ContentTypes.REASONING ||
334
+ chunk.content[0]?.type === ContentTypes.REASONING_CONTENT)
335
+ ) {
336
+ reasoning_content = 'valid';
337
+ } else if (
338
+ (agentContext.provider === Providers.OPENAI ||
339
+ agentContext.provider === Providers.AZURE) &&
340
+ reasoning_content != null &&
341
+ typeof reasoning_content !== 'string' &&
342
+ reasoning_content.summary?.[0]?.text != null &&
343
+ reasoning_content.summary[0].text
344
+ ) {
345
+ reasoning_content = 'valid';
346
+ }
347
+ if (
348
+ reasoning_content != null &&
349
+ reasoning_content !== '' &&
350
+ (chunk.content == null ||
351
+ chunk.content === '' ||
352
+ reasoning_content === 'valid')
353
+ ) {
354
+ agentContext.currentTokenType = ContentTypes.THINK;
355
+ agentContext.tokenTypeSwitch = 'reasoning';
356
+ return;
357
+ } else if (
358
+ agentContext.tokenTypeSwitch === 'reasoning' &&
359
+ agentContext.currentTokenType !== ContentTypes.TEXT &&
360
+ ((chunk.content != null && chunk.content !== '') ||
361
+ (chunk.tool_calls?.length ?? 0) > 0 ||
362
+ (chunk.tool_call_chunks?.length ?? 0) > 0)
363
+ ) {
364
+ agentContext.currentTokenType = ContentTypes.TEXT;
365
+ agentContext.tokenTypeSwitch = 'content';
366
+ } else if (
367
+ chunk.content != null &&
368
+ typeof chunk.content === 'string' &&
369
+ chunk.content.includes('<think>') &&
370
+ chunk.content.includes('</think>')
371
+ ) {
372
+ agentContext.currentTokenType = 'think_and_text';
373
+ agentContext.tokenTypeSwitch = 'content';
374
+ } else if (
375
+ chunk.content != null &&
376
+ typeof chunk.content === 'string' &&
377
+ chunk.content.includes('<think>')
378
+ ) {
379
+ agentContext.currentTokenType = ContentTypes.THINK;
380
+ agentContext.tokenTypeSwitch = 'content';
381
+ } else if (
382
+ agentContext.lastToken != null &&
383
+ agentContext.lastToken.includes('</think>')
384
+ ) {
385
+ agentContext.currentTokenType = ContentTypes.TEXT;
386
+ agentContext.tokenTypeSwitch = 'content';
387
+ }
388
+ if (typeof chunk.content !== 'string') {
389
+ return;
390
+ }
391
+ agentContext.lastToken = chunk.content;
392
+ }
393
+ }
394
+
395
+ export function createContentAggregator(): t.ContentAggregatorResult {
396
+ const contentParts: Array<t.MessageContentComplex | undefined> = [];
397
+ const stepMap = new Map<string, t.RunStep>();
398
+ const toolCallIdMap = new Map<string, string>();
399
+
400
+ const updateContent = (
401
+ index: number,
402
+ contentPart?: t.MessageContentComplex,
403
+ finalUpdate = false
404
+ ): void => {
405
+ if (!contentPart) {
406
+ console.warn('No content part found in \'updateContent\'');
407
+ return;
408
+ }
409
+ const partType = contentPart.type ?? '';
410
+ if (!partType) {
411
+ console.warn('No content type found in content part');
412
+ return;
413
+ }
414
+
415
+ if (!contentParts[index]) {
416
+ contentParts[index] = { type: partType };
417
+ }
418
+
419
+ if (!partType.startsWith(contentParts[index]?.type ?? '')) {
420
+ console.warn('Content type mismatch');
421
+ return;
422
+ }
423
+
424
+ if (
425
+ partType.startsWith(ContentTypes.TEXT) &&
426
+ ContentTypes.TEXT in contentPart &&
427
+ typeof contentPart.text === 'string'
428
+ ) {
429
+ // TODO: update this!!
430
+ const currentContent = contentParts[index] as t.MessageDeltaUpdate;
431
+ const update: t.MessageDeltaUpdate = {
432
+ type: ContentTypes.TEXT,
433
+ text: (currentContent.text || '') + contentPart.text,
434
+ };
435
+
436
+ if (contentPart.tool_call_ids) {
437
+ update.tool_call_ids = contentPart.tool_call_ids;
438
+ }
439
+ contentParts[index] = update;
440
+ } else if (
441
+ partType.startsWith(ContentTypes.THINK) &&
442
+ ContentTypes.THINK in contentPart &&
443
+ typeof contentPart.think === 'string'
444
+ ) {
445
+ const currentContent = contentParts[index] as t.ReasoningDeltaUpdate;
446
+ const update: t.ReasoningDeltaUpdate = {
447
+ type: ContentTypes.THINK,
448
+ think: (currentContent.think || '') + contentPart.think,
449
+ };
450
+ contentParts[index] = update;
451
+ } else if (
452
+ partType.startsWith(ContentTypes.AGENT_UPDATE) &&
453
+ ContentTypes.AGENT_UPDATE in contentPart &&
454
+ contentPart.agent_update != null
455
+ ) {
456
+ const update: t.AgentUpdate = {
457
+ type: ContentTypes.AGENT_UPDATE,
458
+ agent_update: contentPart.agent_update,
459
+ };
460
+
461
+ contentParts[index] = update;
462
+ } else if (
463
+ partType === ContentTypes.IMAGE_URL &&
464
+ 'image_url' in contentPart
465
+ ) {
466
+ const currentContent = contentParts[index] as {
467
+ type: 'image_url';
468
+ image_url: string;
469
+ };
470
+ contentParts[index] = {
471
+ ...currentContent,
472
+ };
473
+ } else if (
474
+ partType === ContentTypes.TOOL_CALL &&
475
+ 'tool_call' in contentPart
476
+ ) {
477
+ const incomingName = contentPart.tool_call.name;
478
+ const incomingId = contentPart.tool_call.id;
479
+ const toolCallArgs = (contentPart.tool_call as t.ToolCallPart).args;
480
+
481
+ // When we receive a tool call with a name, it's the complete tool call
482
+ // Consolidate with any previously accumulated args from chunks
483
+ const hasValidName = incomingName != null && incomingName !== '';
484
+
485
+ // Only process if incoming has a valid name (complete tool call)
486
+ // or if we're doing a final update with complete data
487
+ if (!hasValidName && !finalUpdate) {
488
+ return;
489
+ }
490
+
491
+ const existingContent = contentParts[index] as
492
+ | (Omit<t.ToolCallContent, 'tool_call'> & {
493
+ tool_call?: t.ToolCallPart;
494
+ })
495
+ | undefined;
496
+
497
+ /** When args are a valid object, they are likely already invoked */
498
+ let args =
499
+ finalUpdate ||
500
+ typeof existingContent?.tool_call?.args === 'object' ||
501
+ typeof toolCallArgs === 'object'
502
+ ? contentPart.tool_call.args
503
+ : (existingContent?.tool_call?.args ?? '') + (toolCallArgs ?? '');
504
+ if (
505
+ finalUpdate &&
506
+ args == null &&
507
+ existingContent?.tool_call?.args != null
508
+ ) {
509
+ args = existingContent.tool_call.args;
510
+ }
511
+
512
+ const id =
513
+ getNonEmptyValue([incomingId, existingContent?.tool_call?.id]) ?? '';
514
+ const name =
515
+ getNonEmptyValue([incomingName, existingContent?.tool_call?.name]) ??
516
+ '';
517
+
518
+ const newToolCall: ToolCall & t.PartMetadata = {
519
+ id,
520
+ name,
521
+ args,
522
+ type: ToolCallTypes.TOOL_CALL,
523
+ };
524
+
525
+ if (finalUpdate) {
526
+ newToolCall.progress = 1;
527
+ newToolCall.output = contentPart.tool_call.output;
528
+ }
529
+
530
+ contentParts[index] = {
531
+ type: ContentTypes.TOOL_CALL,
532
+ tool_call: newToolCall,
533
+ };
534
+ }
535
+ };
536
+
537
+ const aggregateContent = ({
538
+ event,
539
+ data,
540
+ }: {
541
+ event: GraphEvents;
542
+ data:
543
+ | t.RunStep
544
+ | t.AgentUpdate
545
+ | t.MessageDeltaEvent
546
+ | t.RunStepDeltaEvent
547
+ | { result: t.ToolEndEvent };
548
+ }): void => {
549
+ if (event === GraphEvents.ON_RUN_STEP) {
550
+ const runStep = data as t.RunStep;
551
+ stepMap.set(runStep.id, runStep);
552
+
553
+ // Store tool call IDs if present
554
+ if (
555
+ runStep.stepDetails.type === StepTypes.TOOL_CALLS &&
556
+ runStep.stepDetails.tool_calls
557
+ ) {
558
+ (runStep.stepDetails.tool_calls as ToolCall[]).forEach((toolCall) => {
559
+ const toolCallId = toolCall.id ?? '';
560
+ if ('id' in toolCall && toolCallId) {
561
+ toolCallIdMap.set(runStep.id, toolCallId);
562
+ }
563
+ const contentPart: t.MessageContentComplex = {
564
+ type: ContentTypes.TOOL_CALL,
565
+ tool_call: {
566
+ args: toolCall.args,
567
+ name: toolCall.name,
568
+ id: toolCallId,
569
+ },
570
+ };
571
+
572
+ updateContent(runStep.index, contentPart);
573
+ });
574
+ }
575
+ } else if (event === GraphEvents.ON_MESSAGE_DELTA) {
576
+ const messageDelta = data as t.MessageDeltaEvent;
577
+ const runStep = stepMap.get(messageDelta.id);
578
+ if (!runStep) {
579
+ console.warn('No run step or runId found for message delta event');
580
+ return;
581
+ }
582
+
583
+ if (messageDelta.delta.content) {
584
+ const contentPart = Array.isArray(messageDelta.delta.content)
585
+ ? messageDelta.delta.content[0]
586
+ : messageDelta.delta.content;
587
+
588
+ updateContent(runStep.index, contentPart);
589
+ }
590
+ } else if (
591
+ event === GraphEvents.ON_AGENT_UPDATE &&
592
+ (data as t.AgentUpdate | undefined)?.agent_update
593
+ ) {
594
+ const contentPart = data as t.AgentUpdate | undefined;
595
+ if (!contentPart) {
596
+ return;
597
+ }
598
+ updateContent(contentPart.agent_update.index, contentPart);
599
+ } else if (event === GraphEvents.ON_REASONING_DELTA) {
600
+ const reasoningDelta = data as t.ReasoningDeltaEvent;
601
+ const runStep = stepMap.get(reasoningDelta.id);
602
+ if (!runStep) {
603
+ console.warn('No run step or runId found for reasoning delta event');
604
+ return;
605
+ }
606
+
607
+ if (reasoningDelta.delta.content) {
608
+ const contentPart = Array.isArray(reasoningDelta.delta.content)
609
+ ? reasoningDelta.delta.content[0]
610
+ : reasoningDelta.delta.content;
611
+
612
+ updateContent(runStep.index, contentPart);
613
+ }
614
+ } else if (event === GraphEvents.ON_RUN_STEP_DELTA) {
615
+ const runStepDelta = data as t.RunStepDeltaEvent;
616
+ const runStep = stepMap.get(runStepDelta.id);
617
+ if (!runStep) {
618
+ console.warn('No run step or runId found for run step delta event');
619
+ return;
620
+ }
621
+
622
+ if (
623
+ runStepDelta.delta.type === StepTypes.TOOL_CALLS &&
624
+ runStepDelta.delta.tool_calls
625
+ ) {
626
+ runStepDelta.delta.tool_calls.forEach((toolCallDelta) => {
627
+ const toolCallId = toolCallIdMap.get(runStepDelta.id);
628
+
629
+ const contentPart: t.MessageContentComplex = {
630
+ type: ContentTypes.TOOL_CALL,
631
+ tool_call: {
632
+ args: toolCallDelta.args ?? '',
633
+ name: toolCallDelta.name,
634
+ id: toolCallId,
635
+ },
636
+ };
637
+
638
+ updateContent(runStep.index, contentPart);
639
+ });
640
+ }
641
+ } else if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
642
+ const { result } = data as unknown as { result: t.ToolEndEvent };
643
+
644
+ const { id: stepId } = result;
645
+
646
+ const runStep = stepMap.get(stepId);
647
+ if (!runStep) {
648
+ console.warn(
649
+ 'No run step or runId found for completed tool call event'
650
+ );
651
+ return;
652
+ }
653
+
654
+ const contentPart: t.MessageContentComplex = {
655
+ type: ContentTypes.TOOL_CALL,
656
+ tool_call: result.tool_call,
657
+ };
658
+
659
+ updateContent(runStep.index, contentPart, true);
660
+ }
661
+ };
662
+
663
+ return { contentParts, aggregateContent, stepMap };
664
+ }