@librechat/agents 3.3.4 → 3.3.5

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 (159) hide show
  1. package/dist/cjs/common/constants.cjs +21 -0
  2. package/dist/cjs/common/constants.cjs.map +1 -1
  3. package/dist/cjs/events.cjs +10 -1
  4. package/dist/cjs/events.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +456 -7
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/index.cjs +12 -0
  10. package/dist/cjs/hooks/index.cjs.map +1 -1
  11. package/dist/cjs/hooks/types.cjs +1 -0
  12. package/dist/cjs/hooks/types.cjs.map +1 -1
  13. package/dist/cjs/langfuse.cjs +8 -0
  14. package/dist/cjs/langfuse.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +1 -3
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/invoke.cjs +268 -3
  18. package/dist/cjs/llm/invoke.cjs.map +1 -1
  19. package/dist/cjs/llm/preempt.cjs +132 -0
  20. package/dist/cjs/llm/preempt.cjs.map +1 -0
  21. package/dist/cjs/main.cjs +17 -3
  22. package/dist/cjs/messages/alternation.cjs +92 -0
  23. package/dist/cjs/messages/alternation.cjs.map +1 -0
  24. package/dist/cjs/messages/format.cjs +72 -0
  25. package/dist/cjs/messages/format.cjs.map +1 -1
  26. package/dist/cjs/messages/handoffCue.cjs +63 -0
  27. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  28. package/dist/cjs/messages/index.cjs +3 -0
  29. package/dist/cjs/messages/injected.cjs +60 -0
  30. package/dist/cjs/messages/injected.cjs.map +1 -0
  31. package/dist/cjs/run.cjs +80 -7
  32. package/dist/cjs/run.cjs.map +1 -1
  33. package/dist/cjs/session/handlers.cjs +18 -9
  34. package/dist/cjs/session/handlers.cjs.map +1 -1
  35. package/dist/cjs/stream.cjs +28 -7
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +3 -23
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  40. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  41. package/dist/cjs/tools/search/tool.cjs +9 -1
  42. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  43. package/dist/esm/common/constants.mjs +19 -1
  44. package/dist/esm/common/constants.mjs.map +1 -1
  45. package/dist/esm/events.mjs +10 -1
  46. package/dist/esm/events.mjs.map +1 -1
  47. package/dist/esm/graphs/Graph.mjs +455 -6
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  50. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  51. package/dist/esm/hooks/index.mjs +12 -1
  52. package/dist/esm/hooks/index.mjs.map +1 -1
  53. package/dist/esm/hooks/types.mjs +1 -0
  54. package/dist/esm/hooks/types.mjs.map +1 -1
  55. package/dist/esm/langfuse.mjs +9 -1
  56. package/dist/esm/langfuse.mjs.map +1 -1
  57. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +1 -3
  58. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  59. package/dist/esm/llm/invoke.mjs +270 -6
  60. package/dist/esm/llm/invoke.mjs.map +1 -1
  61. package/dist/esm/llm/preempt.mjs +131 -0
  62. package/dist/esm/llm/preempt.mjs.map +1 -0
  63. package/dist/esm/main.mjs +8 -5
  64. package/dist/esm/messages/alternation.mjs +91 -0
  65. package/dist/esm/messages/alternation.mjs.map +1 -0
  66. package/dist/esm/messages/format.mjs +72 -0
  67. package/dist/esm/messages/format.mjs.map +1 -1
  68. package/dist/esm/messages/handoffCue.mjs +61 -0
  69. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  70. package/dist/esm/messages/index.mjs +3 -0
  71. package/dist/esm/messages/injected.mjs +60 -0
  72. package/dist/esm/messages/injected.mjs.map +1 -0
  73. package/dist/esm/run.mjs +80 -7
  74. package/dist/esm/run.mjs.map +1 -1
  75. package/dist/esm/session/handlers.mjs +19 -10
  76. package/dist/esm/session/handlers.mjs.map +1 -1
  77. package/dist/esm/stream.mjs +27 -8
  78. package/dist/esm/stream.mjs.map +1 -1
  79. package/dist/esm/tools/ToolNode.mjs +3 -23
  80. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  81. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  82. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  83. package/dist/esm/tools/search/tool.mjs +9 -1
  84. package/dist/esm/tools/search/tool.mjs.map +1 -1
  85. package/dist/types/common/constants.d.ts +18 -0
  86. package/dist/types/graphs/Graph.d.ts +138 -1
  87. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  88. package/dist/types/hooks/index.d.ts +12 -1
  89. package/dist/types/hooks/types.d.ts +45 -6
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/llm/invoke.d.ts +7 -0
  92. package/dist/types/llm/preempt.d.ts +40 -0
  93. package/dist/types/messages/alternation.d.ts +23 -0
  94. package/dist/types/messages/handoffCue.d.ts +40 -0
  95. package/dist/types/messages/index.d.ts +3 -0
  96. package/dist/types/messages/injected.d.ts +3 -0
  97. package/dist/types/run.d.ts +7 -0
  98. package/dist/types/stream.d.ts +14 -0
  99. package/dist/types/tools/ToolNode.d.ts +0 -7
  100. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  101. package/dist/types/tools/search/types.d.ts +31 -2
  102. package/dist/types/types/graph.d.ts +8 -1
  103. package/dist/types/types/run.d.ts +65 -0
  104. package/dist/types/types/stream.d.ts +0 -25
  105. package/package.json +1 -1
  106. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  107. package/src/common/constants.ts +21 -0
  108. package/src/events.ts +15 -1
  109. package/src/graphs/Graph.ts +568 -3
  110. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
  111. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  112. package/src/hooks/HookRegistry.ts +40 -0
  113. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  114. package/src/hooks/index.ts +16 -2
  115. package/src/hooks/types.ts +47 -3
  116. package/src/index.ts +1 -1
  117. package/src/langfuse.ts +26 -1
  118. package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
  119. package/src/llm/bedrock/utils/message_inputs.ts +14 -11
  120. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  121. package/src/llm/invoke.alternation.test.ts +87 -0
  122. package/src/llm/invoke.handoffCue.test.ts +112 -0
  123. package/src/llm/invoke.ts +417 -8
  124. package/src/llm/preempt.test.ts +323 -0
  125. package/src/llm/preempt.ts +178 -0
  126. package/src/messages/alternation.test.ts +212 -0
  127. package/src/messages/alternation.ts +112 -0
  128. package/src/messages/format.ts +91 -0
  129. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  130. package/src/messages/handoffCue.test.ts +96 -0
  131. package/src/messages/handoffCue.ts +78 -0
  132. package/src/messages/index.ts +3 -0
  133. package/src/messages/injected.test.ts +90 -0
  134. package/src/messages/injected.ts +74 -0
  135. package/src/run.ts +91 -6
  136. package/src/scripts/preempt-probe.ts +330 -0
  137. package/src/scripts/preempt-scenarios.ts +388 -0
  138. package/src/session/handlers.ts +32 -12
  139. package/src/specs/handoffCue.test.ts +165 -0
  140. package/src/specs/langfuse-callbacks.test.ts +352 -2
  141. package/src/specs/preemptSeal.test.ts +309 -0
  142. package/src/stream.dispatch.test.ts +63 -0
  143. package/src/stream.ts +36 -8
  144. package/src/tools/ToolNode.ts +3 -31
  145. package/src/tools/search/keenable-scraper.test.ts +153 -0
  146. package/src/tools/search/keenable-scraper.ts +137 -0
  147. package/src/tools/search/tool.ts +13 -2
  148. package/src/tools/search/types.ts +50 -3
  149. package/src/types/graph.ts +12 -1
  150. package/src/types/run.ts +67 -0
  151. package/src/types/stream.ts +0 -41
  152. package/dist/cjs/splitStream.cjs +0 -151
  153. package/dist/cjs/splitStream.cjs.map +0 -1
  154. package/dist/esm/splitStream.mjs +0 -150
  155. package/dist/esm/splitStream.mjs.map +0 -1
  156. package/dist/types/mockStream.d.ts +0 -32
  157. package/dist/types/splitStream.d.ts +0 -37
  158. package/src/mockStream.ts +0 -99
  159. package/src/splitStream.ts +0 -234
@@ -1,15 +1,6 @@
1
- import { nanoid } from 'nanoid';
2
- import { MessageContentText } from '@langchain/core/messages';
3
1
  import type * as t from '@/types';
4
2
  import { GraphEvents, StepTypes, ContentTypes } from '@/common';
5
3
  import { createContentAggregator } from './stream';
6
- import { SplitStreamHandler } from './splitStream';
7
- import { createMockStream } from './mockStream';
8
-
9
- // Mock sleep to speed up tests
10
- jest.mock('@/utils', () => ({
11
- sleep: (): Promise<void> => Promise.resolve(),
12
- }));
13
4
 
14
5
  const createRunStep = (id: string): t.RunStep => ({
15
6
  id,
@@ -23,156 +14,6 @@ const createRunStep = (id: string): t.RunStep => ({
23
14
  usage: null,
24
15
  });
25
16
 
26
- describe('Stream Generation and Handling', () => {
27
- let mockHandlers: {
28
- [GraphEvents.ON_RUN_STEP]: jest.Mock;
29
- [GraphEvents.ON_MESSAGE_DELTA]: jest.Mock;
30
- };
31
-
32
- beforeEach(() => {
33
- mockHandlers = {
34
- [GraphEvents.ON_RUN_STEP]: jest.fn(),
35
- [GraphEvents.ON_MESSAGE_DELTA]: jest.fn(),
36
- };
37
- });
38
-
39
- it('should properly stream tokens including spaces', async () => {
40
- const stream = createMockStream({
41
- text: 'Hello world!',
42
- streamRate: 0,
43
- })();
44
-
45
- const tokens: string[] = [];
46
- for await (const chunk of stream) {
47
- const content = chunk.choices?.[0]?.delta.content ?? '';
48
- if (content) tokens.push(content);
49
- }
50
-
51
- expect(tokens).toEqual(['Hello', ' ', 'world!']);
52
- });
53
-
54
- it('should handle code blocks without splitting them', async () => {
55
- const runId = nanoid();
56
- const handler = new SplitStreamHandler({
57
- runId,
58
- blockThreshold: 10,
59
- handlers: mockHandlers,
60
- });
61
-
62
- const codeText = `Code:
63
- \`\`\`
64
- const x = 1;
65
- const y = 2;
66
- const z = 2;
67
- const a = 2;
68
- const b = 2;
69
- const c = 2;
70
- const d = 2;
71
- const e = 2;
72
- const f = 2;
73
- const g = 2;
74
- const h = 2;
75
- \`\`\`
76
- End code.`;
77
-
78
- const stream = createMockStream({
79
- text: codeText,
80
- streamRate: 0,
81
- })();
82
-
83
- for await (const chunk of stream) {
84
- handler.handle(chunk);
85
- }
86
-
87
- // Verify that only one message block was created for the code section
88
- const runSteps = mockHandlers[GraphEvents.ON_RUN_STEP].mock.calls;
89
- expect(runSteps.length).toBe(2); // Should only create one message block
90
- });
91
-
92
- it('should split content when exceeding threshold', async () => {
93
- const runId = nanoid();
94
- const handler = new SplitStreamHandler({
95
- runId,
96
- handlers: mockHandlers,
97
- // Set a very low threshold for testing
98
- blockThreshold: 10,
99
- });
100
-
101
- // Make the text longer and ensure it has clear breaking points
102
- const longText =
103
- 'This is the first sentence. And here is another sentence. And yet another one here. Finally one more.';
104
-
105
- const stream = createMockStream({
106
- text: longText,
107
- streamRate: 0,
108
- })();
109
-
110
- // For debugging
111
- // let totalLength = 0;
112
- for await (const chunk of stream) {
113
- handler.handle(chunk);
114
- // For debugging
115
- // const content = chunk.choices?.[0]?.delta.content;
116
- // if (content) {
117
- // totalLength += content.length;
118
- // console.log(`Current length: ${totalLength}, Content: "${content}"`);
119
- // }
120
- }
121
-
122
- // Verify multiple message blocks were created
123
- const runSteps = mockHandlers[GraphEvents.ON_RUN_STEP].mock.calls;
124
- // console.log('Number of run steps:', runSteps.length);
125
- expect(runSteps.length).toEqual(handler.currentIndex + 1);
126
- });
127
-
128
- it('should handle reasoning text separately', async () => {
129
- const runId = nanoid();
130
- new SplitStreamHandler({
131
- runId,
132
- handlers: mockHandlers,
133
- });
134
-
135
- const stream = createMockStream({
136
- text: 'Main content',
137
- reasoningText: 'Reasoning text',
138
- streamRate: 0,
139
- })();
140
-
141
- const reasoningTokens: string[] = [];
142
- const contentTokens: string[] = [];
143
-
144
- for await (const chunk of stream) {
145
- const reasoning = chunk.choices?.[0]?.delta.reasoning_content ?? '';
146
- const content = chunk.choices?.[0]?.delta.content ?? '';
147
-
148
- if (reasoning) reasoningTokens.push(reasoning);
149
- if (content) contentTokens.push(content);
150
- }
151
-
152
- expect(reasoningTokens.length).toBeGreaterThan(0);
153
- expect(contentTokens.length).toBeGreaterThan(0);
154
- });
155
-
156
- it('should preserve empty strings and whitespace', async () => {
157
- const stream = createMockStream({
158
- text: 'Hello world', // Note double space
159
- streamRate: 0,
160
- })();
161
-
162
- const tokens: string[] = [];
163
- for await (const chunk of stream) {
164
- const content = chunk.choices?.[0]?.delta.content ?? '';
165
- if (!content) {
166
- return;
167
- }
168
- tokens.push(content);
169
- }
170
-
171
- expect(tokens).toContain(' ');
172
- expect(tokens.join('')).toBe('Hello world');
173
- });
174
- });
175
-
176
17
  describe('ContentAggregator empty deltas', () => {
177
18
  it('should ignore empty message delta content arrays', () => {
178
19
  const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
@@ -892,9 +733,7 @@ describe('ContentAggregator physical content indices', () => {
892
733
  id: 'step_late_ids',
893
734
  delta: {
894
735
  type: StepTypes.TOOL_CALLS,
895
- tool_calls: [
896
- { index: 2, name: 'first', args: '{"value":"first"}' },
897
- ],
736
+ tool_calls: [{ index: 2, name: 'first', args: '{"value":"first"}' }],
898
737
  },
899
738
  } as t.RunStepDeltaEvent,
900
739
  });
@@ -1155,540 +994,94 @@ describe('ContentAggregator provider-specific parts', () => {
1155
994
  });
1156
995
  });
1157
996
 
1158
- describe('ContentAggregator with SplitStreamHandler', () => {
1159
- it('should aggregate content from multiple message blocks', async () => {
1160
- const runId = nanoid();
997
+ describe('ContentAggregator multi-entry deltas', () => {
998
+ it('concatenates every text entry of a message delta in order', () => {
1161
999
  const { contentParts, aggregateContent } = createContentAggregator();
1162
1000
 
1163
- const handler = new SplitStreamHandler({
1164
- runId,
1165
- handlers: {
1166
- [GraphEvents.ON_RUN_STEP]: aggregateContent,
1167
- [GraphEvents.ON_MESSAGE_DELTA]: aggregateContent,
1168
- },
1169
- blockThreshold: 5,
1170
- });
1171
-
1172
- const text = 'First sentence. Second sentence. Third sentence.';
1173
- const stream = createMockStream({ text, streamRate: 0 })();
1174
-
1175
- for await (const chunk of stream) {
1176
- handler.handle(chunk);
1177
- }
1178
-
1179
- expect(contentParts.length).toBeGreaterThan(0);
1180
- contentParts.forEach((part) => {
1181
- expect(part?.type).toBe(ContentTypes.TEXT);
1182
- if (part?.type === ContentTypes.TEXT) {
1183
- expect(typeof part.text).toBe('string');
1184
- expect(part.text.length).toBeGreaterThan(0);
1185
- }
1001
+ aggregateContent({
1002
+ event: GraphEvents.ON_RUN_STEP,
1003
+ data: createRunStep('step_multi_text'),
1186
1004
  });
1187
1005
 
1188
- const fullText = contentParts
1189
- .filter((part) => part?.type === ContentTypes.TEXT)
1190
- .map((part) => (part?.type === ContentTypes.TEXT ? part.text : ''))
1191
- .join('');
1192
- expect(fullText).toBe(text);
1193
- });
1194
-
1195
- it('should maintain content order across splits', async () => {
1196
- const runId = nanoid();
1197
- const { contentParts, aggregateContent } = createContentAggregator();
1198
-
1199
- const handler = new SplitStreamHandler({
1200
- runId,
1201
- handlers: {
1202
- [GraphEvents.ON_RUN_STEP]: aggregateContent,
1203
- [GraphEvents.ON_MESSAGE_DELTA]: aggregateContent,
1006
+ aggregateContent({
1007
+ event: GraphEvents.ON_MESSAGE_DELTA,
1008
+ data: {
1009
+ id: 'step_multi_text',
1010
+ delta: {
1011
+ content: [
1012
+ { type: ContentTypes.TEXT, text: 'Hello ' },
1013
+ { type: ContentTypes.TEXT, text: 'streaming ' },
1014
+ { type: ContentTypes.TEXT, text: 'world' },
1015
+ ],
1016
+ },
1204
1017
  },
1205
- blockThreshold: 15,
1206
1018
  });
1207
1019
 
1208
- const text = 'First part. Second part. Third part.';
1209
- const stream = createMockStream({ text, streamRate: 0 })();
1210
-
1211
- for await (const chunk of stream) {
1212
- handler.handle(chunk);
1213
- }
1214
-
1215
- const texts = contentParts
1216
- .filter((part) => part?.type === ContentTypes.TEXT)
1217
- .map((part) => (part?.type === ContentTypes.TEXT ? part.text : ''));
1218
-
1219
- expect(texts[0]).toContain('First');
1220
- expect(texts[texts.length - 1]).toContain('Third');
1020
+ expect(contentParts[0]).toEqual({
1021
+ type: ContentTypes.TEXT,
1022
+ text: 'Hello streaming world',
1023
+ });
1221
1024
  });
1222
1025
 
1223
- it('should handle code blocks as single content parts', async () => {
1224
- const runId = nanoid();
1026
+ it('concatenates every think entry of a reasoning delta in order', () => {
1225
1027
  const { contentParts, aggregateContent } = createContentAggregator();
1226
1028
 
1227
- const handler = new SplitStreamHandler({
1228
- runId,
1229
- handlers: {
1230
- [GraphEvents.ON_RUN_STEP]: aggregateContent,
1231
- [GraphEvents.ON_MESSAGE_DELTA]: aggregateContent,
1232
- },
1233
- blockThreshold: 10,
1029
+ aggregateContent({
1030
+ event: GraphEvents.ON_RUN_STEP,
1031
+ data: createRunStep('step_multi_think'),
1234
1032
  });
1235
1033
 
1236
- const text = `Before code.
1237
- \`\`\`python
1238
- def test():
1239
- return True
1240
- \`\`\`
1241
- After code.`;
1242
-
1243
- const stream = createMockStream({ text, streamRate: 0 })();
1244
-
1245
- for await (const chunk of stream) {
1246
- handler.handle(chunk);
1247
- }
1248
-
1249
- const codeBlockPart = contentParts.find(
1250
- (part) =>
1251
- part?.type === ContentTypes.TEXT &&
1252
- part.text.includes('```python') === true
1253
- );
1254
-
1255
- expect(codeBlockPart).toBeDefined();
1256
- if (codeBlockPart?.type === ContentTypes.TEXT) {
1257
- expect(codeBlockPart.text).toContain('def test()');
1258
- expect(codeBlockPart.text).toContain('return True');
1259
- }
1260
- });
1261
-
1262
- it('should properly map steps to their content', async () => {
1263
- const runId = nanoid();
1264
- const { contentParts, aggregateContent, stepMap } =
1265
- createContentAggregator();
1266
-
1267
- const handler = new SplitStreamHandler({
1268
- runId,
1269
- handlers: {
1270
- [GraphEvents.ON_RUN_STEP]: aggregateContent,
1271
- [GraphEvents.ON_MESSAGE_DELTA]: aggregateContent,
1034
+ aggregateContent({
1035
+ event: GraphEvents.ON_REASONING_DELTA,
1036
+ data: {
1037
+ id: 'step_multi_think',
1038
+ delta: {
1039
+ content: [
1040
+ { type: ContentTypes.THINK, think: 'First reasoning block. ' },
1041
+ { type: ContentTypes.THINK, think: 'Second reasoning block.' },
1042
+ ],
1043
+ },
1272
1044
  },
1273
- blockThreshold: 5,
1274
- });
1275
-
1276
- const text = 'Hi. Ok. Yes.';
1277
- const stream = createMockStream({ text, streamRate: 0 })();
1278
-
1279
- for await (const chunk of stream) {
1280
- handler.handle(chunk);
1281
- }
1282
-
1283
- Array.from(stepMap.entries()).forEach(([_stepId, step]) => {
1284
- expect(step?.type).toBe(StepTypes.MESSAGE_CREATION);
1285
- const currentIndex = step?.index ?? -1;
1286
- const stepContent = contentParts[currentIndex];
1287
- if (!stepContent && currentIndex > 0) {
1288
- const prevStepContent = contentParts[currentIndex - 1];
1289
- expect(
1290
- (prevStepContent as MessageContentText | undefined)?.text
1291
- ).toEqual(text);
1292
- } else if (stepContent?.type === ContentTypes.TEXT) {
1293
- expect(stepContent.text.length).toBeGreaterThan(0);
1294
- }
1295
1045
  });
1296
1046
 
1297
- contentParts.forEach((part, index) => {
1298
- const hasMatchingStep = Array.from(stepMap.values()).some(
1299
- (step) => step?.index === index
1300
- );
1301
- expect(hasMatchingStep).toBe(true);
1047
+ expect(contentParts[0]).toEqual({
1048
+ type: ContentTypes.THINK,
1049
+ think: 'First reasoning block. Second reasoning block.',
1302
1050
  });
1303
1051
  });
1304
1052
 
1305
- it('should aggregate content across multiple splits while preserving order', async () => {
1306
- const runId = nanoid();
1053
+ it('accumulates across multi-entry and single-entry deltas alike', () => {
1307
1054
  const { contentParts, aggregateContent } = createContentAggregator();
1308
1055
 
1309
- const handler = new SplitStreamHandler({
1310
- runId,
1311
- handlers: {
1312
- [GraphEvents.ON_RUN_STEP]: aggregateContent,
1313
- [GraphEvents.ON_MESSAGE_DELTA]: aggregateContent,
1314
- },
1315
- blockThreshold: 10,
1316
- });
1317
-
1318
- const text = 'A. B. C. D. E. F.';
1319
- const stream = createMockStream({ text, streamRate: 0 })();
1320
-
1321
- for await (const chunk of stream) {
1322
- handler.handle(chunk);
1323
- }
1324
-
1325
- const letters = ['A', 'B', 'C', 'D', 'E', 'F'];
1326
- let letterIndex = 0;
1327
-
1328
- contentParts.forEach((part) => {
1329
- if (part?.type === ContentTypes.TEXT) {
1330
- while (
1331
- letterIndex < letters.length &&
1332
- part.text.includes(letters[letterIndex]) === true
1333
- ) {
1334
- letterIndex++;
1335
- }
1336
- }
1337
- });
1338
-
1339
- expect(letterIndex).toBe(letters.length);
1340
- });
1341
- });
1342
-
1343
- describe('SplitStreamHandler with Reasoning Tokens', () => {
1344
- it('should apply same splitting rules to both content types', async () => {
1345
- const runId = nanoid();
1346
- const mockHandlers: t.SplitStreamHandlers = {
1347
- [GraphEvents.ON_RUN_STEP]: jest.fn(),
1348
- [GraphEvents.ON_MESSAGE_DELTA]: jest.fn(),
1349
- [GraphEvents.ON_REASONING_DELTA]: jest.fn(),
1350
- };
1351
-
1352
- const handler = new SplitStreamHandler({
1353
- runId,
1354
- handlers: mockHandlers,
1355
- blockThreshold: 3,
1356
- });
1357
-
1358
- const stream = createMockStream({
1359
- text: 'First text. Second text. Third text.',
1360
- reasoningText: 'First thought. Second thought. Third thought.',
1361
- streamRate: 0,
1362
- })();
1363
-
1364
- for await (const chunk of stream) {
1365
- handler.handle(chunk);
1366
- }
1367
-
1368
- const runSteps = (mockHandlers[GraphEvents.ON_RUN_STEP] as jest.Mock).mock
1369
- .calls;
1370
- const reasoningDeltas = (
1371
- mockHandlers[GraphEvents.ON_REASONING_DELTA] as jest.Mock
1372
- ).mock.calls;
1373
- const messageDeltas = (
1374
- mockHandlers[GraphEvents.ON_MESSAGE_DELTA] as jest.Mock
1375
- ).mock.calls;
1376
-
1377
- // Both content types should create multiple blocks
1378
- expect(runSteps.length).toBeGreaterThan(1);
1379
- expect(reasoningDeltas.length).toBeGreaterThan(0);
1380
- expect(messageDeltas.length).toBeGreaterThan(0);
1381
-
1382
- // Verify splitting behavior for both types
1383
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1384
- const getStepTypes = (calls: any[]): string[] =>
1385
- calls
1386
- .map(([{ data }]) =>
1387
- data.stepDetails?.type === StepTypes.MESSAGE_CREATION
1388
- ? data.stepDetails.message_creation.message_id
1389
- : null
1390
- )
1391
- .filter(Boolean);
1392
-
1393
- const messageSteps = getStepTypes(runSteps);
1394
- expect(new Set(messageSteps).size).toBeGreaterThan(1);
1395
- });
1396
-
1397
- it('should properly map steps to their reasoning content', async () => {
1398
- const runId = nanoid();
1399
- const { contentParts, aggregateContent, stepMap } =
1400
- createContentAggregator();
1401
-
1402
- const handler = new SplitStreamHandler({
1403
- runId,
1404
- handlers: {
1405
- [GraphEvents.ON_RUN_STEP]: aggregateContent,
1406
- [GraphEvents.ON_MESSAGE_DELTA]: aggregateContent,
1407
- [GraphEvents.ON_REASONING_DELTA]: aggregateContent,
1408
- },
1409
- blockThreshold: 5,
1410
- });
1411
-
1412
- const text = 'Main content.';
1413
- const reasoningText = 'First thought. Second thought. Third thought.';
1414
- const stream = createMockStream({
1415
- text,
1416
- reasoningText,
1417
- streamRate: 0,
1418
- })();
1419
-
1420
- for await (const chunk of stream) {
1421
- handler.handle(chunk);
1422
- }
1423
-
1424
- Array.from(stepMap.entries()).forEach(([_stepId, step]) => {
1425
- expect(step?.type).toBe(StepTypes.MESSAGE_CREATION);
1426
- const currentIndex = step?.index ?? -1;
1427
- const stepContent = contentParts[currentIndex];
1428
-
1429
- if (stepContent?.type === ContentTypes.THINK) {
1430
- // Verify reasoning content structure
1431
- expect(stepContent).toHaveProperty('think');
1432
- expect(typeof stepContent.think).toBe('string');
1433
- expect(stepContent.think.length).toBeGreaterThan(0);
1434
- }
1435
- });
1436
-
1437
- // Verify at least one reasoning content part exists
1438
- const reasoningParts = contentParts.filter(
1439
- (part) => part?.type === ContentTypes.THINK
1440
- );
1441
- expect(reasoningParts.length).toBeGreaterThan(0);
1442
-
1443
- // Verify the content order (reasoning should come before main content)
1444
- const contentTypes = contentParts
1445
- .filter((part) => part !== undefined)
1446
- .map((part) => part.type);
1447
-
1448
- expect(contentTypes).toContain(ContentTypes.THINK);
1449
- expect(contentTypes).toContain(ContentTypes.TEXT);
1450
-
1451
- // Verify the complete reasoning content is preserved
1452
- const fullReasoningText = reasoningParts
1453
- .map((part) => (part?.type === ContentTypes.THINK ? part.think : ''))
1454
- .join('');
1455
- expect(fullReasoningText).toBe(reasoningText);
1456
- });
1457
- });
1458
-
1459
- describe('SplitStreamHandler', () => {
1460
- it('should handle think blocks correctly', async () => {
1461
- const runId = nanoid();
1462
- const messageDeltaEvents: t.MessageDeltaEvent[] = [];
1463
- const reasoningDeltaEvents: t.ReasoningDeltaEvent[] = [];
1464
-
1465
- const streamHandler = new SplitStreamHandler({
1466
- runId,
1467
- handlers: {
1468
- [GraphEvents.ON_MESSAGE_DELTA]: ({ data }): void => {
1469
- messageDeltaEvents.push(data);
1470
- },
1471
- [GraphEvents.ON_REASONING_DELTA]: ({ data }): void => {
1472
- reasoningDeltaEvents.push(data);
1473
- },
1474
- },
1056
+ aggregateContent({
1057
+ event: GraphEvents.ON_RUN_STEP,
1058
+ data: createRunStep('step_mixed_cadence'),
1475
1059
  });
1476
1060
 
1477
- const content =
1478
- 'Here\'s some regular text. <think>Now I\'m thinking deeply about something important. This should all be reasoning.</think> Back to regular text.';
1479
-
1480
- const stream = createMockStream({
1481
- text: content,
1482
- streamRate: 5,
1483
- })();
1484
-
1485
- for await (const chunk of stream) {
1486
- streamHandler.handle(chunk);
1487
- }
1488
-
1489
- // Check that content before <think> was handled as regular text
1490
- expect(
1491
- messageDeltaEvents.some(
1492
- (event) =>
1493
- (
1494
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
1495
- )?.text.includes('Here\'s') === true
1496
- )
1497
- ).toBe(true);
1498
-
1499
- // Check that <think> tag was handled as reasoning
1500
- expect(
1501
- reasoningDeltaEvents.some(
1502
- (event) =>
1503
- (
1504
- event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
1505
- )?.think.includes('<think>') === true
1506
- )
1507
- ).toBe(true);
1508
-
1509
- // Check that content inside <think> tags was handled as reasoning
1510
- expect(
1511
- reasoningDeltaEvents.some(
1512
- (event) =>
1513
- (
1514
- event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
1515
- )?.think.includes('thinking') === true
1516
- )
1517
- ).toBe(true);
1518
-
1519
- // Check that </think> tag was handled as reasoning
1520
- expect(
1521
- reasoningDeltaEvents.some(
1522
- (event) =>
1523
- (
1524
- event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined
1525
- )?.think.includes('</think>') === true
1526
- )
1527
- ).toBe(true);
1528
-
1529
- // Check that content after </think> was handled as regular text
1530
- expect(
1531
- messageDeltaEvents.some(
1532
- (event) =>
1533
- (
1534
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
1535
- )?.text.includes('Back') === true
1536
- )
1537
- ).toBe(true);
1538
- });
1539
-
1540
- it('should ignore think tags inside code blocks', async () => {
1541
- const runId = nanoid();
1542
- const messageDeltaEvents: t.MessageDeltaEvent[] = [];
1543
- const reasoningDeltaEvents: t.ReasoningDeltaEvent[] = [];
1544
-
1545
- const streamHandler = new SplitStreamHandler({
1546
- runId,
1547
- handlers: {
1548
- [GraphEvents.ON_MESSAGE_DELTA]: ({ data }): void => {
1549
- messageDeltaEvents.push(data);
1550
- },
1551
- [GraphEvents.ON_REASONING_DELTA]: ({ data }): void => {
1552
- reasoningDeltaEvents.push(data);
1061
+ aggregateContent({
1062
+ event: GraphEvents.ON_REASONING_DELTA,
1063
+ data: {
1064
+ id: 'step_mixed_cadence',
1065
+ delta: {
1066
+ content: [
1067
+ { type: ContentTypes.THINK, think: 'One. ' },
1068
+ { type: ContentTypes.THINK, think: 'Two. ' },
1069
+ ],
1553
1070
  },
1554
1071
  },
1555
1072
  });
1556
1073
 
1557
- const content =
1558
- 'Regular text. ```<think>This should stay as code</think>``` More text.';
1559
-
1560
- const stream = createMockStream({
1561
- text: content,
1562
- streamRate: 5,
1563
- })();
1564
-
1565
- for await (const chunk of stream) {
1566
- streamHandler.handle(chunk);
1567
- }
1568
-
1569
- // Check that think tags inside code blocks were treated as regular text
1570
- expect(
1571
- messageDeltaEvents.some(
1572
- (event) =>
1573
- (
1574
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
1575
- )?.text.includes('Regular') === true
1576
- )
1577
- ).toBe(true);
1578
-
1579
- // Verify no reasoning events were generated
1580
- expect(reasoningDeltaEvents.length).toBe(0);
1581
- });
1582
-
1583
- it('should properly split content with think tags while maintaining context', async () => {
1584
- const runId = nanoid();
1585
- const messageDeltaEvents: t.MessageDeltaEvent[] = [];
1586
- const reasoningDeltaEvents: t.ReasoningDeltaEvent[] = [];
1587
- const runStepEvents: t.RunStep[] = [];
1588
- const { contentParts, aggregateContent } = createContentAggregator();
1589
-
1590
- const streamHandler = new SplitStreamHandler({
1591
- runId,
1592
- blockThreshold: 20, // Small threshold to force splits
1593
- handlers: {
1594
- [GraphEvents.ON_MESSAGE_DELTA]: (event): void => {
1595
- messageDeltaEvents.push(event.data);
1596
- aggregateContent(event);
1597
- },
1598
- [GraphEvents.ON_REASONING_DELTA]: (event): void => {
1599
- reasoningDeltaEvents.push(event.data);
1600
- aggregateContent(event);
1601
- },
1602
- [GraphEvents.ON_RUN_STEP]: (event): void => {
1603
- runStepEvents.push(event.data);
1604
- aggregateContent(event);
1605
- },
1074
+ aggregateContent({
1075
+ event: GraphEvents.ON_REASONING_DELTA,
1076
+ data: {
1077
+ id: 'step_mixed_cadence',
1078
+ delta: { content: [{ type: ContentTypes.THINK, think: 'Three.' }] },
1606
1079
  },
1607
1080
  });
1608
1081
 
1609
- const content =
1610
- 'Here\'s some regular text. <think>Now I\'m thinking deeply about something important. This is a long thought that should be split into multiple parts. We want to ensure the splitting works correctly.</think> Back to regular text after thinking.';
1611
-
1612
- const stream = createMockStream({
1613
- text: content,
1614
- streamRate: 5,
1615
- })();
1616
-
1617
- for await (const chunk of stream) {
1618
- streamHandler.handle(chunk);
1619
- }
1620
-
1621
- // Verify that multiple message blocks were created
1622
- expect(runStepEvents.length).toBeGreaterThan(2);
1623
-
1624
- // Check that content before <think> was handled as regular text
1625
- expect(
1626
- messageDeltaEvents.some(
1627
- (event) =>
1628
- (
1629
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
1630
- )?.text.includes('regular') === true
1631
- )
1632
- ).toBe(true);
1633
-
1634
- // Verify that reasoning content was split into multiple parts
1635
- const reasoningParts = reasoningDeltaEvents
1636
- .map(
1637
- (event) =>
1638
- (event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined)
1639
- ?.think
1640
- )
1641
- .filter(Boolean);
1642
- expect(reasoningParts.length).toBeGreaterThan(1);
1643
-
1644
- // Verify that the complete reasoning content is preserved when joined
1645
- const fullReasoningContent = reasoningParts.join('');
1646
- expect(fullReasoningContent).toContain('thinking');
1647
- expect(fullReasoningContent).toContain('should');
1648
- expect(fullReasoningContent).toContain('be');
1649
- expect(fullReasoningContent).toContain('split');
1650
-
1651
- // Check that each reasoning part maintains proper think context
1652
- let seenThinkOpen = false;
1653
- let seenThinkClose = false;
1654
- reasoningParts.forEach((part) => {
1655
- if (part == null) return;
1656
- if (part.includes('<think>')) {
1657
- seenThinkOpen = true;
1658
- }
1659
- if (part.includes('</think>')) {
1660
- seenThinkClose = true;
1661
- }
1662
- // Middle parts should be handled as reasoning even without explicit think tags
1663
- if (!part.includes('<think>') && !part.includes('</think>')) {
1664
- expect(
1665
- reasoningDeltaEvents.some(
1666
- (event) =>
1667
- (event.delta.content?.[0] as t.ReasoningDeltaUpdate | undefined)
1668
- ?.think === part
1669
- )
1670
- ).toBe(true);
1671
- }
1082
+ expect(contentParts[0]).toEqual({
1083
+ type: ContentTypes.THINK,
1084
+ think: 'One. Two. Three.',
1672
1085
  });
1673
- expect(seenThinkOpen).toBe(true);
1674
- expect(seenThinkClose).toBe(true);
1675
-
1676
- // Check that content after </think> was handled as regular text
1677
- expect(
1678
- messageDeltaEvents.some(
1679
- (event) =>
1680
- (
1681
- event.delta.content?.[0] as t.MessageDeltaUpdate | undefined
1682
- )?.text.includes('Back') === true
1683
- )
1684
- ).toBe(true);
1685
-
1686
- const thinkingBlocks = contentParts.filter(
1687
- (part) => part?.type === ContentTypes.THINK
1688
- );
1689
- expect(thinkingBlocks.length).toBeGreaterThan(0);
1690
- expect(
1691
- (thinkingBlocks[0] as t.ReasoningContentText).think.startsWith('<think>')
1692
- ).toBeTruthy();
1693
1086
  });
1694
1087
  });