@librechat/agents 3.1.57 → 3.1.60

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 (214) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +326 -62
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +13 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +7 -27
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +303 -222
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -4
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +6 -2
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/init.cjs +60 -0
  14. package/dist/cjs/llm/init.cjs.map +1 -0
  15. package/dist/cjs/llm/invoke.cjs +90 -0
  16. package/dist/cjs/llm/invoke.cjs.map +1 -0
  17. package/dist/cjs/llm/openai/index.cjs +2 -0
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +41 -0
  20. package/dist/cjs/llm/request.cjs.map +1 -0
  21. package/dist/cjs/main.cjs +40 -0
  22. package/dist/cjs/main.cjs.map +1 -1
  23. package/dist/cjs/messages/cache.cjs +76 -89
  24. package/dist/cjs/messages/cache.cjs.map +1 -1
  25. package/dist/cjs/messages/contextPruning.cjs +156 -0
  26. package/dist/cjs/messages/contextPruning.cjs.map +1 -0
  27. package/dist/cjs/messages/contextPruningSettings.cjs +53 -0
  28. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -0
  29. package/dist/cjs/messages/core.cjs +23 -37
  30. package/dist/cjs/messages/core.cjs.map +1 -1
  31. package/dist/cjs/messages/format.cjs +156 -11
  32. package/dist/cjs/messages/format.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +1161 -49
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/messages/reducer.cjs +87 -0
  36. package/dist/cjs/messages/reducer.cjs.map +1 -0
  37. package/dist/cjs/run.cjs +81 -42
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +54 -7
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/summarization/index.cjs +75 -0
  42. package/dist/cjs/summarization/index.cjs.map +1 -0
  43. package/dist/cjs/summarization/node.cjs +663 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -0
  45. package/dist/cjs/tools/ToolNode.cjs +16 -8
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +2 -0
  48. package/dist/cjs/tools/handlers.cjs.map +1 -1
  49. package/dist/cjs/utils/errors.cjs +115 -0
  50. package/dist/cjs/utils/errors.cjs.map +1 -0
  51. package/dist/cjs/utils/events.cjs +17 -0
  52. package/dist/cjs/utils/events.cjs.map +1 -1
  53. package/dist/cjs/utils/handlers.cjs +16 -0
  54. package/dist/cjs/utils/handlers.cjs.map +1 -1
  55. package/dist/cjs/utils/llm.cjs +10 -0
  56. package/dist/cjs/utils/llm.cjs.map +1 -1
  57. package/dist/cjs/utils/tokens.cjs +247 -14
  58. package/dist/cjs/utils/tokens.cjs.map +1 -1
  59. package/dist/cjs/utils/truncation.cjs +107 -0
  60. package/dist/cjs/utils/truncation.cjs.map +1 -0
  61. package/dist/esm/agents/AgentContext.mjs +325 -61
  62. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  63. package/dist/esm/common/enum.mjs +13 -0
  64. package/dist/esm/common/enum.mjs.map +1 -1
  65. package/dist/esm/events.mjs +8 -28
  66. package/dist/esm/events.mjs.map +1 -1
  67. package/dist/esm/graphs/Graph.mjs +307 -226
  68. package/dist/esm/graphs/Graph.mjs.map +1 -1
  69. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -4
  70. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  71. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +6 -2
  72. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/init.mjs +58 -0
  74. package/dist/esm/llm/init.mjs.map +1 -0
  75. package/dist/esm/llm/invoke.mjs +87 -0
  76. package/dist/esm/llm/invoke.mjs.map +1 -0
  77. package/dist/esm/llm/openai/index.mjs +2 -0
  78. package/dist/esm/llm/openai/index.mjs.map +1 -1
  79. package/dist/esm/llm/request.mjs +38 -0
  80. package/dist/esm/llm/request.mjs.map +1 -0
  81. package/dist/esm/main.mjs +13 -3
  82. package/dist/esm/main.mjs.map +1 -1
  83. package/dist/esm/messages/cache.mjs +76 -89
  84. package/dist/esm/messages/cache.mjs.map +1 -1
  85. package/dist/esm/messages/contextPruning.mjs +154 -0
  86. package/dist/esm/messages/contextPruning.mjs.map +1 -0
  87. package/dist/esm/messages/contextPruningSettings.mjs +50 -0
  88. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -0
  89. package/dist/esm/messages/core.mjs +23 -37
  90. package/dist/esm/messages/core.mjs.map +1 -1
  91. package/dist/esm/messages/format.mjs +156 -11
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/prune.mjs +1158 -52
  94. package/dist/esm/messages/prune.mjs.map +1 -1
  95. package/dist/esm/messages/reducer.mjs +83 -0
  96. package/dist/esm/messages/reducer.mjs.map +1 -0
  97. package/dist/esm/run.mjs +82 -43
  98. package/dist/esm/run.mjs.map +1 -1
  99. package/dist/esm/stream.mjs +54 -7
  100. package/dist/esm/stream.mjs.map +1 -1
  101. package/dist/esm/summarization/index.mjs +73 -0
  102. package/dist/esm/summarization/index.mjs.map +1 -0
  103. package/dist/esm/summarization/node.mjs +659 -0
  104. package/dist/esm/summarization/node.mjs.map +1 -0
  105. package/dist/esm/tools/ToolNode.mjs +16 -8
  106. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  107. package/dist/esm/tools/handlers.mjs +2 -0
  108. package/dist/esm/tools/handlers.mjs.map +1 -1
  109. package/dist/esm/utils/errors.mjs +111 -0
  110. package/dist/esm/utils/errors.mjs.map +1 -0
  111. package/dist/esm/utils/events.mjs +17 -1
  112. package/dist/esm/utils/events.mjs.map +1 -1
  113. package/dist/esm/utils/handlers.mjs +16 -0
  114. package/dist/esm/utils/handlers.mjs.map +1 -1
  115. package/dist/esm/utils/llm.mjs +10 -1
  116. package/dist/esm/utils/llm.mjs.map +1 -1
  117. package/dist/esm/utils/tokens.mjs +245 -15
  118. package/dist/esm/utils/tokens.mjs.map +1 -1
  119. package/dist/esm/utils/truncation.mjs +102 -0
  120. package/dist/esm/utils/truncation.mjs.map +1 -0
  121. package/dist/types/agents/AgentContext.d.ts +124 -6
  122. package/dist/types/common/enum.d.ts +14 -1
  123. package/dist/types/graphs/Graph.d.ts +22 -27
  124. package/dist/types/index.d.ts +5 -0
  125. package/dist/types/llm/init.d.ts +18 -0
  126. package/dist/types/llm/invoke.d.ts +48 -0
  127. package/dist/types/llm/request.d.ts +14 -0
  128. package/dist/types/messages/contextPruning.d.ts +42 -0
  129. package/dist/types/messages/contextPruningSettings.d.ts +44 -0
  130. package/dist/types/messages/core.d.ts +1 -1
  131. package/dist/types/messages/format.d.ts +17 -1
  132. package/dist/types/messages/index.d.ts +3 -0
  133. package/dist/types/messages/prune.d.ts +162 -1
  134. package/dist/types/messages/reducer.d.ts +18 -0
  135. package/dist/types/run.d.ts +12 -1
  136. package/dist/types/summarization/index.d.ts +20 -0
  137. package/dist/types/summarization/node.d.ts +29 -0
  138. package/dist/types/tools/ToolNode.d.ts +3 -1
  139. package/dist/types/types/graph.d.ts +44 -6
  140. package/dist/types/types/index.d.ts +1 -0
  141. package/dist/types/types/run.d.ts +30 -0
  142. package/dist/types/types/stream.d.ts +31 -4
  143. package/dist/types/types/summarize.d.ts +47 -0
  144. package/dist/types/types/tools.d.ts +7 -0
  145. package/dist/types/utils/errors.d.ts +28 -0
  146. package/dist/types/utils/events.d.ts +13 -0
  147. package/dist/types/utils/index.d.ts +2 -0
  148. package/dist/types/utils/llm.d.ts +4 -0
  149. package/dist/types/utils/tokens.d.ts +14 -1
  150. package/dist/types/utils/truncation.d.ts +49 -0
  151. package/package.json +1 -1
  152. package/src/agents/AgentContext.ts +388 -58
  153. package/src/agents/__tests__/AgentContext.test.ts +265 -5
  154. package/src/common/enum.ts +13 -0
  155. package/src/events.ts +9 -39
  156. package/src/graphs/Graph.ts +468 -331
  157. package/src/index.ts +7 -0
  158. package/src/llm/anthropic/llm.spec.ts +3 -3
  159. package/src/llm/anthropic/utils/message_inputs.ts +6 -4
  160. package/src/llm/bedrock/llm.spec.ts +1 -1
  161. package/src/llm/bedrock/utils/message_inputs.ts +6 -2
  162. package/src/llm/init.ts +63 -0
  163. package/src/llm/invoke.ts +144 -0
  164. package/src/llm/request.ts +55 -0
  165. package/src/messages/__tests__/observationMasking.test.ts +221 -0
  166. package/src/messages/cache.ts +77 -102
  167. package/src/messages/contextPruning.ts +191 -0
  168. package/src/messages/contextPruningSettings.ts +90 -0
  169. package/src/messages/core.ts +32 -53
  170. package/src/messages/ensureThinkingBlock.test.ts +39 -39
  171. package/src/messages/format.ts +227 -15
  172. package/src/messages/formatAgentMessages.test.ts +511 -1
  173. package/src/messages/index.ts +3 -0
  174. package/src/messages/prune.ts +1548 -62
  175. package/src/messages/reducer.ts +22 -0
  176. package/src/run.ts +104 -51
  177. package/src/scripts/bedrock-merge-test.ts +1 -1
  178. package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
  179. package/src/scripts/test-thinking-handoff.ts +1 -1
  180. package/src/scripts/thinking-bedrock.ts +1 -1
  181. package/src/scripts/thinking.ts +1 -1
  182. package/src/specs/anthropic.simple.test.ts +1 -1
  183. package/src/specs/multi-agent-summarization.test.ts +396 -0
  184. package/src/specs/prune.test.ts +1196 -23
  185. package/src/specs/summarization-unit.test.ts +868 -0
  186. package/src/specs/summarization.test.ts +3810 -0
  187. package/src/specs/summarize-prune.test.ts +376 -0
  188. package/src/specs/thinking-handoff.test.ts +10 -10
  189. package/src/specs/thinking-prune.test.ts +7 -4
  190. package/src/specs/token-accounting-e2e.test.ts +1034 -0
  191. package/src/specs/token-accounting-pipeline.test.ts +882 -0
  192. package/src/specs/token-distribution-edge-case.test.ts +25 -26
  193. package/src/splitStream.test.ts +42 -33
  194. package/src/stream.ts +64 -11
  195. package/src/summarization/__tests__/aggregator.test.ts +153 -0
  196. package/src/summarization/__tests__/node.test.ts +708 -0
  197. package/src/summarization/__tests__/trigger.test.ts +50 -0
  198. package/src/summarization/index.ts +102 -0
  199. package/src/summarization/node.ts +982 -0
  200. package/src/tools/ToolNode.ts +25 -3
  201. package/src/types/graph.ts +62 -7
  202. package/src/types/index.ts +1 -0
  203. package/src/types/run.ts +32 -0
  204. package/src/types/stream.ts +45 -5
  205. package/src/types/summarize.ts +58 -0
  206. package/src/types/tools.ts +7 -0
  207. package/src/utils/errors.ts +117 -0
  208. package/src/utils/events.ts +31 -0
  209. package/src/utils/handlers.ts +18 -0
  210. package/src/utils/index.ts +2 -0
  211. package/src/utils/llm.ts +12 -0
  212. package/src/utils/tokens.ts +336 -18
  213. package/src/utils/truncation.ts +124 -0
  214. package/src/scripts/image.ts +0 -180
@@ -75,6 +75,7 @@ describe('Token Distribution Edge Case Tests', () => {
75
75
  startIndex: 0,
76
76
  tokenCounter,
77
77
  indexTokenCountMap: { ...indexTokenCountMap },
78
+ reserveRatio: 0,
78
79
  });
79
80
 
80
81
  // First call to establish lastCutOffIndex
@@ -115,18 +116,21 @@ describe('Token Distribution Edge Case Tests', () => {
115
116
 
116
117
  expect(atLeastOnePrunedMessageUnchanged).toBe(true);
117
118
 
118
- // Verify that the sum of tokens for messages in the context is close to the total_tokens from usageMetadata
119
- // There might be small rounding differences or implementation details that affect the exact sum
120
- const totalContextTokens =
119
+ // Calibration uses input_tokens (30) only (no output), minus instruction overhead (0).
120
+ // Map stays in raw tiktoken space calibrationRatio captures the multiplier.
121
+ // Context messages: indices 0, 3, 4 → raw sum unchanged.
122
+ // calibrationRatio × rawSum should approximate input_tokens (30).
123
+ const rawContextTokens =
121
124
  (result.indexTokenCountMap[0] ?? 0) +
122
125
  (result.indexTokenCountMap[3] ?? 0) +
123
126
  (result.indexTokenCountMap[4] ?? 0);
124
- expect(totalContextTokens).toBeGreaterThan(0);
127
+ expect(rawContextTokens).toBeGreaterThan(0);
125
128
 
126
- // The key thing we're testing is that the token distribution happens for messages in the context
127
- // and that the sum is reasonably close to the expected total
128
- const tokenDifference = Math.abs(totalContextTokens - 50);
129
- expect(tokenDifference).toBeLessThan(20); // Allow for some difference due to implementation details
129
+ const calibratedTotal = Math.round(
130
+ rawContextTokens * (result.calibrationRatio ?? 1)
131
+ );
132
+ const tokenDifference = Math.abs(calibratedTotal - 30);
133
+ expect(tokenDifference).toBeLessThan(10);
130
134
  });
131
135
 
132
136
  it('should handle the case when all messages fit within the token limit', () => {
@@ -174,28 +178,22 @@ describe('Token Distribution Edge Case Tests', () => {
174
178
  usageMetadata,
175
179
  });
176
180
 
177
- // Since all messages fit, all token counts should be adjusted
178
- const initialTotalTokens =
179
- indexTokenCountMap[0] + indexTokenCountMap[1] + indexTokenCountMap[2];
180
- const expectedRatio = 30 / initialTotalTokens;
181
+ // Calibration uses input_tokens (20) only, minus instruction overhead (0).
182
+ // messageTokenSum = 17 + 9 + 10 = 36. ratio = 20/36 = 0.556. Safe.
183
+ // Map stays raw — calibrationRatio captures the multiplier
184
+ expect(result.indexTokenCountMap[0]).toBe(indexTokenCountMap[0]);
185
+ expect(result.indexTokenCountMap[1]).toBe(indexTokenCountMap[1]);
186
+ expect(result.indexTokenCountMap[2]).toBe(indexTokenCountMap[2]);
181
187
 
182
- // Check that all token counts were adjusted
183
- expect(result.indexTokenCountMap[0]).toBe(
184
- Math.round(indexTokenCountMap[0] * expectedRatio)
185
- );
186
- expect(result.indexTokenCountMap[1]).toBe(
187
- Math.round(indexTokenCountMap[1] * expectedRatio)
188
- );
189
- expect(result.indexTokenCountMap[2]).toBe(
190
- Math.round(indexTokenCountMap[2] * expectedRatio)
191
- );
192
-
193
- // Verify that the sum of all tokens equals the total_tokens from usageMetadata
194
- const totalTokens =
188
+ // rawSum × calibrationRatio should approximate input_tokens (20)
189
+ const rawTotal =
195
190
  (result.indexTokenCountMap[0] ?? 0) +
196
191
  (result.indexTokenCountMap[1] ?? 0) +
197
192
  (result.indexTokenCountMap[2] ?? 0);
198
- expect(totalTokens).toBe(30);
193
+ const calibratedTotal = Math.round(
194
+ rawTotal * (result.calibrationRatio ?? 1)
195
+ );
196
+ expect(Math.abs(calibratedTotal - 20)).toBeLessThanOrEqual(3);
199
197
  });
200
198
 
201
199
  it('should handle multiple pruning operations with token redistribution', () => {
@@ -230,6 +228,7 @@ describe('Token Distribution Edge Case Tests', () => {
230
228
  startIndex: 0,
231
229
  tokenCounter,
232
230
  indexTokenCountMap: { ...indexTokenCountMap },
231
+ reserveRatio: 0,
233
232
  });
234
233
 
235
234
  // First pruning operation
@@ -254,7 +254,8 @@ After code.`;
254
254
 
255
255
  const codeBlockPart = contentParts.find(
256
256
  (part) =>
257
- part?.type === ContentTypes.TEXT && part.text.includes('```python')
257
+ part?.type === ContentTypes.TEXT &&
258
+ part.text.includes('```python') === true
258
259
  );
259
260
 
260
261
  expect(codeBlockPart).toBeDefined();
@@ -493,46 +494,51 @@ describe('SplitStreamHandler', () => {
493
494
 
494
495
  // Check that content before <think> was handled as regular text
495
496
  expect(
496
- messageDeltaEvents.some((event) =>
497
- (
498
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
499
- )?.text.includes('Here\'s')
497
+ messageDeltaEvents.some(
498
+ (event) =>
499
+ (
500
+ event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
501
+ )?.text.includes('Here\'s') === true
500
502
  )
501
503
  ).toBe(true);
502
504
 
503
505
  // Check that <think> tag was handled as reasoning
504
506
  expect(
505
- reasoningDeltaEvents.some((event) =>
506
- (
507
- event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
508
- )?.think.includes('<think>')
507
+ reasoningDeltaEvents.some(
508
+ (event) =>
509
+ (
510
+ event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
511
+ )?.think.includes('<think>') === true
509
512
  )
510
513
  ).toBe(true);
511
514
 
512
515
  // Check that content inside <think> tags was handled as reasoning
513
516
  expect(
514
- reasoningDeltaEvents.some((event) =>
515
- (
516
- event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
517
- )?.think.includes('thinking')
517
+ reasoningDeltaEvents.some(
518
+ (event) =>
519
+ (
520
+ event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
521
+ )?.think.includes('thinking') === true
518
522
  )
519
523
  ).toBe(true);
520
524
 
521
525
  // Check that </think> tag was handled as reasoning
522
526
  expect(
523
- reasoningDeltaEvents.some((event) =>
524
- (
525
- event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
526
- )?.think.includes('</think>')
527
+ reasoningDeltaEvents.some(
528
+ (event) =>
529
+ (
530
+ event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
531
+ )?.think.includes('</think>') === true
527
532
  )
528
533
  ).toBe(true);
529
534
 
530
535
  // Check that content after </think> was handled as regular text
531
536
  expect(
532
- messageDeltaEvents.some((event) =>
533
- (
534
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
535
- )?.text.includes('Back')
537
+ messageDeltaEvents.some(
538
+ (event) =>
539
+ (
540
+ event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
541
+ )?.text.includes('Back') === true
536
542
  )
537
543
  ).toBe(true);
538
544
  });
@@ -568,10 +574,11 @@ describe('SplitStreamHandler', () => {
568
574
 
569
575
  // Check that think tags inside code blocks were treated as regular text
570
576
  expect(
571
- messageDeltaEvents.some((event) =>
572
- (
573
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
574
- )?.text.includes('Regular')
577
+ messageDeltaEvents.some(
578
+ (event) =>
579
+ (
580
+ event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
581
+ )?.text.includes('Regular') === true
575
582
  )
576
583
  ).toBe(true);
577
584
 
@@ -622,10 +629,11 @@ describe('SplitStreamHandler', () => {
622
629
 
623
630
  // Check that content before <think> was handled as regular text
624
631
  expect(
625
- messageDeltaEvents.some((event) =>
626
- (
627
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
628
- )?.text.includes('regular')
632
+ messageDeltaEvents.some(
633
+ (event) =>
634
+ (
635
+ event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
636
+ )?.text.includes('regular') === true
629
637
  )
630
638
  ).toBe(true);
631
639
 
@@ -673,10 +681,11 @@ describe('SplitStreamHandler', () => {
673
681
 
674
682
  // Check that content after </think> was handled as regular text
675
683
  expect(
676
- messageDeltaEvents.some((event) =>
677
- (
678
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
679
- )?.text.includes('Back')
684
+ messageDeltaEvents.some(
685
+ (event) =>
686
+ (
687
+ event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
688
+ )?.text.includes('Back') === true
680
689
  )
681
690
  ).toBe(true);
682
691
 
package/src/stream.ts CHANGED
@@ -148,6 +148,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
148
148
  if (!graph.config) {
149
149
  throw new Error('Config not found in graph');
150
150
  }
151
+
151
152
  if (!data.chunk) {
152
153
  console.warn(`No chunk found in ${event} event`);
153
154
  return;
@@ -513,6 +514,18 @@ export function createContentAggregator(): t.ContentAggregatorResult {
513
514
  };
514
515
 
515
516
  contentParts[index] = update;
517
+ } else if (partType === ContentTypes.SUMMARY) {
518
+ const currentSummary = contentParts[index] as
519
+ | t.SummaryContentBlock
520
+ | undefined;
521
+ const incoming = contentPart as t.SummaryContentBlock;
522
+ contentParts[index] = {
523
+ ...incoming,
524
+ content: [
525
+ ...(currentSummary?.content ?? []),
526
+ ...(incoming.content ?? []),
527
+ ],
528
+ };
516
529
  } else if (
517
530
  partType === ContentTypes.IMAGE_URL &&
518
531
  'image_url' in contentPart
@@ -618,9 +631,41 @@ export function createContentAggregator(): t.ContentAggregatorResult {
618
631
  | t.RunStep
619
632
  | t.AgentUpdate
620
633
  | t.MessageDeltaEvent
634
+ | t.ReasoningDeltaEvent
621
635
  | t.RunStepDeltaEvent
636
+ | t.SummarizeDeltaData
637
+ | t.SummarizeCompleteEvent
622
638
  | { result: t.ToolEndEvent };
623
639
  }): void => {
640
+ if (event === GraphEvents.ON_SUMMARIZE_DELTA) {
641
+ const deltaData = data as t.SummarizeDeltaData;
642
+ const runStep = stepMap.get(deltaData.id);
643
+ if (!runStep) {
644
+ console.warn('No run step found for summarize delta event');
645
+ return;
646
+ }
647
+ updateContent(runStep.index, deltaData.delta.summary);
648
+ return;
649
+ }
650
+
651
+ if (event === GraphEvents.ON_SUMMARIZE_COMPLETE) {
652
+ const completeData = data as t.SummarizeCompleteEvent;
653
+ const summary = completeData.summary;
654
+ if (!summary?.boundary) {
655
+ return;
656
+ }
657
+ const runStep = stepMap.get(summary.boundary.messageId);
658
+ if (!runStep) {
659
+ return;
660
+ }
661
+ // Replace accumulated delta text with the authoritative final summary.
662
+ // Multi-stage summarization streams deltas from each chunk, which
663
+ // concatenate in updateContent. This event carries only the correct
664
+ // final text from the last stage.
665
+ contentParts[runStep.index] = summary;
666
+ return;
667
+ }
668
+
624
669
  if (event === GraphEvents.ON_RUN_STEP) {
625
670
  const runStep = data as t.RunStep;
626
671
  stepMap.set(runStep.id, runStep);
@@ -641,7 +686,10 @@ export function createContentAggregator(): t.ContentAggregatorResult {
641
686
  contentMetaMap.set(runStep.index, existingMeta);
642
687
  }
643
688
 
644
- // Store tool call IDs if present
689
+ if (runStep.summary != null) {
690
+ updateContent(runStep.index, runStep.summary);
691
+ }
692
+
645
693
  if (
646
694
  runStep.stepDetails.type === StepTypes.TOOL_CALLS &&
647
695
  runStep.stepDetails.tool_calls
@@ -732,24 +780,29 @@ export function createContentAggregator(): t.ContentAggregatorResult {
732
780
  });
733
781
  }
734
782
  } else if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
735
- const { result } = data as unknown as { result: t.ToolEndEvent };
783
+ const { result } = data as unknown as {
784
+ result:
785
+ | t.ToolEndEvent
786
+ | (t.SummaryCompleted & { id: string; index: number });
787
+ };
736
788
 
737
789
  const { id: stepId } = result;
738
790
 
739
791
  const runStep = stepMap.get(stepId);
740
792
  if (!runStep) {
741
- console.warn(
742
- 'No run step or runId found for completed tool call event'
743
- );
793
+ console.warn('No run step or runId found for completed step event');
744
794
  return;
745
795
  }
746
796
 
747
- const contentPart: t.MessageContentComplex = {
748
- type: ContentTypes.TOOL_CALL,
749
- tool_call: result.tool_call,
750
- };
751
-
752
- updateContent(runStep.index, contentPart, true);
797
+ if (result.type === ContentTypes.SUMMARY && 'summary' in result) {
798
+ contentParts[runStep.index] = result.summary as t.MessageContentComplex;
799
+ } else if ('tool_call' in result) {
800
+ const contentPart: t.MessageContentComplex = {
801
+ type: ContentTypes.TOOL_CALL,
802
+ tool_call: (result as t.ToolEndEvent).tool_call,
803
+ };
804
+ updateContent(runStep.index, contentPart, true);
805
+ }
753
806
  }
754
807
  };
755
808
 
@@ -0,0 +1,153 @@
1
+ import { createContentAggregator } from '@/stream';
2
+ import { ContentTypes, GraphEvents, StepTypes } from '@/common';
3
+ import type * as t from '@/types';
4
+
5
+ describe('createContentAggregator – SUMMARY accumulation', () => {
6
+ it('accumulates text from multiple ON_SUMMARIZE_DELTA events', () => {
7
+ const { aggregateContent, contentParts } = createContentAggregator();
8
+
9
+ // Register a run step with a summary placeholder
10
+ const runStep: t.RunStep = {
11
+ stepIndex: 0,
12
+ id: 'step_sum_1',
13
+ type: StepTypes.MESSAGE_CREATION,
14
+ index: 0,
15
+ stepDetails: {
16
+ type: StepTypes.MESSAGE_CREATION,
17
+ message_creation: { message_id: 'step_sum_1' },
18
+ },
19
+ summary: {
20
+ type: ContentTypes.SUMMARY,
21
+ content: [],
22
+ tokenCount: 0,
23
+ provider: 'openai',
24
+ },
25
+ usage: null,
26
+ };
27
+
28
+ aggregateContent({
29
+ event: GraphEvents.ON_RUN_STEP,
30
+ data: runStep,
31
+ });
32
+
33
+ // The run step registration sets the initial placeholder
34
+ expect(contentParts[0]).toEqual(
35
+ expect.objectContaining({ type: ContentTypes.SUMMARY, content: [] })
36
+ );
37
+
38
+ // Send multiple deltas with content chunks
39
+ aggregateContent({
40
+ event: GraphEvents.ON_SUMMARIZE_DELTA,
41
+ data: {
42
+ id: 'step_sum_1',
43
+ delta: {
44
+ summary: {
45
+ type: ContentTypes.SUMMARY,
46
+ content: [{ type: 'text', text: 'Hello ' }],
47
+ tokenCount: 0,
48
+ provider: 'openai',
49
+ },
50
+ },
51
+ } as t.SummarizeDeltaData,
52
+ });
53
+
54
+ const afterFirst = contentParts[0] as t.SummaryContentBlock;
55
+ expect(afterFirst.content).toHaveLength(1);
56
+ expect((afterFirst.content![0] as { text: string }).text).toBe('Hello ');
57
+
58
+ aggregateContent({
59
+ event: GraphEvents.ON_SUMMARIZE_DELTA,
60
+ data: {
61
+ id: 'step_sum_1',
62
+ delta: {
63
+ summary: {
64
+ type: ContentTypes.SUMMARY,
65
+ content: [{ type: 'text', text: 'world!' }],
66
+ tokenCount: 0,
67
+ provider: 'openai',
68
+ },
69
+ },
70
+ } as t.SummarizeDeltaData,
71
+ });
72
+
73
+ // Should accumulate content blocks, not replace
74
+ const afterSecond = contentParts[0] as t.SummaryContentBlock;
75
+ expect(afterSecond.content).toHaveLength(2);
76
+ expect((afterSecond.content![0] as { text: string }).text).toBe('Hello ');
77
+ expect((afterSecond.content![1] as { text: string }).text).toBe('world!');
78
+ });
79
+
80
+ it('preserves metadata fields from the latest delta', () => {
81
+ const { aggregateContent, contentParts } = createContentAggregator();
82
+
83
+ const runStep: t.RunStep = {
84
+ stepIndex: 0,
85
+ id: 'step_sum_2',
86
+ type: StepTypes.MESSAGE_CREATION,
87
+ index: 0,
88
+ stepDetails: {
89
+ type: StepTypes.MESSAGE_CREATION,
90
+ message_creation: { message_id: 'step_sum_2' },
91
+ },
92
+ summary: {
93
+ type: ContentTypes.SUMMARY,
94
+ content: [],
95
+ tokenCount: 0,
96
+ provider: 'anthropic',
97
+ model: 'claude-sonnet-4-5',
98
+ },
99
+ usage: null,
100
+ };
101
+
102
+ aggregateContent({
103
+ event: GraphEvents.ON_RUN_STEP,
104
+ data: runStep,
105
+ });
106
+
107
+ aggregateContent({
108
+ event: GraphEvents.ON_SUMMARIZE_DELTA,
109
+ data: {
110
+ id: 'step_sum_2',
111
+ delta: {
112
+ summary: {
113
+ type: ContentTypes.SUMMARY,
114
+ content: [{ type: 'text', text: 'chunk' }],
115
+ tokenCount: 0,
116
+ provider: 'anthropic',
117
+ model: 'claude-sonnet-4-5',
118
+ },
119
+ },
120
+ } as t.SummarizeDeltaData,
121
+ });
122
+
123
+ const part = contentParts[0] as t.SummaryContentBlock;
124
+ expect(part.provider).toBe('anthropic');
125
+ expect(part.model).toBe('claude-sonnet-4-5');
126
+ });
127
+
128
+ it('handles delta when no run step exists', () => {
129
+ const { aggregateContent, contentParts } = createContentAggregator();
130
+
131
+ // No run step registered — should warn and not crash
132
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
133
+
134
+ aggregateContent({
135
+ event: GraphEvents.ON_SUMMARIZE_DELTA,
136
+ data: {
137
+ id: 'nonexistent',
138
+ delta: {
139
+ summary: {
140
+ type: ContentTypes.SUMMARY,
141
+ content: [{ type: 'text', text: 'orphan' }],
142
+ tokenCount: 0,
143
+ },
144
+ },
145
+ } as t.SummarizeDeltaData,
146
+ });
147
+
148
+ expect(consoleSpy).toHaveBeenCalled();
149
+ expect(contentParts.length).toBe(0);
150
+
151
+ consoleSpy.mockRestore();
152
+ });
153
+ });