@librechat/agents 3.3.3 → 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 (274) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +21 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +10 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +793 -111
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/index.cjs +12 -0
  14. package/dist/cjs/hooks/index.cjs.map +1 -1
  15. package/dist/cjs/hooks/types.cjs +1 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -1
  17. package/dist/cjs/langfuse.cjs +8 -0
  18. package/dist/cjs/langfuse.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  20. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  21. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
  22. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  24. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  25. package/dist/cjs/llm/invoke.cjs +331 -5
  26. package/dist/cjs/llm/invoke.cjs.map +1 -1
  27. package/dist/cjs/llm/openai/index.cjs +41 -5
  28. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  29. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  30. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  31. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  32. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  33. package/dist/cjs/llm/preempt.cjs +132 -0
  34. package/dist/cjs/llm/preempt.cjs.map +1 -0
  35. package/dist/cjs/main.cjs +36 -5
  36. package/dist/cjs/messages/alternation.cjs +92 -0
  37. package/dist/cjs/messages/alternation.cjs.map +1 -0
  38. package/dist/cjs/messages/cache.cjs +54 -33
  39. package/dist/cjs/messages/cache.cjs.map +1 -1
  40. package/dist/cjs/messages/contextPruning.cjs +17 -43
  41. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  42. package/dist/cjs/messages/core.cjs +315 -23
  43. package/dist/cjs/messages/core.cjs.map +1 -1
  44. package/dist/cjs/messages/format.cjs +290 -60
  45. package/dist/cjs/messages/format.cjs.map +1 -1
  46. package/dist/cjs/messages/handoffCue.cjs +63 -0
  47. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  48. package/dist/cjs/messages/index.cjs +4 -1
  49. package/dist/cjs/messages/injected.cjs +60 -0
  50. package/dist/cjs/messages/injected.cjs.map +1 -0
  51. package/dist/cjs/messages/prune.cjs +503 -127
  52. package/dist/cjs/messages/prune.cjs.map +1 -1
  53. package/dist/cjs/run.cjs +80 -7
  54. package/dist/cjs/run.cjs.map +1 -1
  55. package/dist/cjs/session/handlers.cjs +18 -9
  56. package/dist/cjs/session/handlers.cjs.map +1 -1
  57. package/dist/cjs/stream.cjs +35 -10
  58. package/dist/cjs/stream.cjs.map +1 -1
  59. package/dist/cjs/summarization/node.cjs +28 -14
  60. package/dist/cjs/summarization/node.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +56 -63
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  64. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  65. package/dist/cjs/tools/search/tool.cjs +9 -1
  66. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  67. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  68. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  69. package/dist/cjs/utils/index.cjs +1 -1
  70. package/dist/cjs/utils/llm.cjs +1 -1
  71. package/dist/cjs/utils/llm.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +307 -20
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/cjs/utils/toolContent.cjs +1514 -0
  75. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  76. package/dist/esm/agents/AgentContext.mjs +2 -1
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/constants.mjs +19 -1
  79. package/dist/esm/common/constants.mjs.map +1 -1
  80. package/dist/esm/events.mjs +10 -1
  81. package/dist/esm/events.mjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +798 -116
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/index.mjs +12 -1
  89. package/dist/esm/hooks/index.mjs.map +1 -1
  90. package/dist/esm/hooks/types.mjs +1 -0
  91. package/dist/esm/hooks/types.mjs.map +1 -1
  92. package/dist/esm/langfuse.mjs +9 -1
  93. package/dist/esm/langfuse.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/google/utils/common.mjs +19 -7
  99. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  100. package/dist/esm/llm/invoke.mjs +332 -8
  101. package/dist/esm/llm/invoke.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +41 -5
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +4 -1
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/preempt.mjs +131 -0
  109. package/dist/esm/llm/preempt.mjs.map +1 -0
  110. package/dist/esm/main.mjs +14 -11
  111. package/dist/esm/messages/alternation.mjs +91 -0
  112. package/dist/esm/messages/alternation.mjs.map +1 -0
  113. package/dist/esm/messages/cache.mjs +55 -34
  114. package/dist/esm/messages/cache.mjs.map +1 -1
  115. package/dist/esm/messages/contextPruning.mjs +17 -43
  116. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  117. package/dist/esm/messages/core.mjs +305 -24
  118. package/dist/esm/messages/core.mjs.map +1 -1
  119. package/dist/esm/messages/format.mjs +290 -61
  120. package/dist/esm/messages/format.mjs.map +1 -1
  121. package/dist/esm/messages/handoffCue.mjs +61 -0
  122. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  123. package/dist/esm/messages/index.mjs +4 -1
  124. package/dist/esm/messages/injected.mjs +60 -0
  125. package/dist/esm/messages/injected.mjs.map +1 -0
  126. package/dist/esm/messages/prune.mjs +500 -128
  127. package/dist/esm/messages/prune.mjs.map +1 -1
  128. package/dist/esm/run.mjs +80 -7
  129. package/dist/esm/run.mjs.map +1 -1
  130. package/dist/esm/session/handlers.mjs +19 -10
  131. package/dist/esm/session/handlers.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +34 -11
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +28 -14
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/ToolNode.mjs +56 -63
  137. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  138. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  139. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  140. package/dist/esm/tools/search/tool.mjs +9 -1
  141. package/dist/esm/tools/search/tool.mjs.map +1 -1
  142. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  143. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  144. package/dist/esm/utils/index.mjs +1 -1
  145. package/dist/esm/utils/llm.mjs +1 -1
  146. package/dist/esm/utils/llm.mjs.map +1 -1
  147. package/dist/esm/utils/tokens.mjs +307 -21
  148. package/dist/esm/utils/tokens.mjs.map +1 -1
  149. package/dist/esm/utils/toolContent.mjs +1503 -0
  150. package/dist/esm/utils/toolContent.mjs.map +1 -0
  151. package/dist/types/common/constants.d.ts +18 -0
  152. package/dist/types/graphs/Graph.d.ts +139 -2
  153. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  154. package/dist/types/hooks/index.d.ts +12 -1
  155. package/dist/types/hooks/types.d.ts +45 -6
  156. package/dist/types/index.d.ts +1 -1
  157. package/dist/types/llm/invoke.d.ts +34 -1
  158. package/dist/types/llm/openai/index.d.ts +4 -0
  159. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  160. package/dist/types/llm/openrouter/index.d.ts +5 -0
  161. package/dist/types/llm/preempt.d.ts +40 -0
  162. package/dist/types/messages/alternation.d.ts +23 -0
  163. package/dist/types/messages/contextPruning.d.ts +2 -1
  164. package/dist/types/messages/core.d.ts +36 -0
  165. package/dist/types/messages/format.d.ts +6 -0
  166. package/dist/types/messages/handoffCue.d.ts +40 -0
  167. package/dist/types/messages/index.d.ts +3 -0
  168. package/dist/types/messages/injected.d.ts +3 -0
  169. package/dist/types/messages/prune.d.ts +16 -12
  170. package/dist/types/run.d.ts +7 -0
  171. package/dist/types/stream.d.ts +14 -0
  172. package/dist/types/tools/ToolNode.d.ts +0 -7
  173. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  174. package/dist/types/tools/search/types.d.ts +31 -2
  175. package/dist/types/types/graph.d.ts +8 -1
  176. package/dist/types/types/run.d.ts +65 -0
  177. package/dist/types/types/stream.d.ts +1 -26
  178. package/dist/types/utils/tokens.d.ts +7 -0
  179. package/dist/types/utils/toolContent.d.ts +107 -0
  180. package/package.json +1 -1
  181. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  182. package/src/agents/AgentContext.ts +1 -0
  183. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  184. package/src/common/constants.ts +21 -0
  185. package/src/events.ts +15 -1
  186. package/src/graphs/Graph.ts +1261 -198
  187. package/src/graphs/MultiAgentGraph.ts +6 -1
  188. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
  189. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  190. package/src/hooks/HookRegistry.ts +40 -0
  191. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  192. package/src/hooks/index.ts +16 -2
  193. package/src/hooks/types.ts +47 -3
  194. package/src/index.ts +1 -1
  195. package/src/langfuse.ts +26 -1
  196. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  197. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  198. package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
  199. package/src/llm/bedrock/utils/message_inputs.ts +100 -24
  200. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  201. package/src/llm/google/utils/common.test.ts +75 -9
  202. package/src/llm/google/utils/common.ts +34 -3
  203. package/src/llm/invoke.alternation.test.ts +87 -0
  204. package/src/llm/invoke.handoffCue.test.ts +112 -0
  205. package/src/llm/invoke.test.ts +488 -0
  206. package/src/llm/invoke.ts +611 -8
  207. package/src/llm/openai/index.ts +106 -6
  208. package/src/llm/openai/utils/index.ts +109 -53
  209. package/src/llm/openai/utils/messages.test.ts +330 -1
  210. package/src/llm/openrouter/index.ts +12 -2
  211. package/src/llm/openrouter/reasoning.test.ts +314 -0
  212. package/src/llm/preempt.test.ts +323 -0
  213. package/src/llm/preempt.ts +178 -0
  214. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  215. package/src/messages/alternation.test.ts +212 -0
  216. package/src/messages/alternation.ts +112 -0
  217. package/src/messages/cache.tail.test.ts +193 -0
  218. package/src/messages/cache.test.ts +113 -0
  219. package/src/messages/cache.ts +92 -60
  220. package/src/messages/contextPruning.test.ts +184 -0
  221. package/src/messages/contextPruning.ts +49 -42
  222. package/src/messages/core.ts +653 -40
  223. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  224. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  225. package/src/messages/format.ts +454 -91
  226. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  227. package/src/messages/formatAgentMessages.test.ts +604 -14
  228. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  229. package/src/messages/handoffCue.test.ts +96 -0
  230. package/src/messages/handoffCue.ts +78 -0
  231. package/src/messages/index.ts +3 -0
  232. package/src/messages/injected.test.ts +90 -0
  233. package/src/messages/injected.ts +74 -0
  234. package/src/messages/prune.ts +996 -183
  235. package/src/run.ts +91 -6
  236. package/src/scripts/preempt-probe.ts +330 -0
  237. package/src/scripts/preempt-scenarios.ts +388 -0
  238. package/src/session/handlers.ts +32 -12
  239. package/src/specs/handoffCue.test.ts +165 -0
  240. package/src/specs/langfuse-callbacks.test.ts +352 -2
  241. package/src/specs/preemptSeal.test.ts +309 -0
  242. package/src/specs/prune.test.ts +1083 -6
  243. package/src/specs/summarization-unit.test.ts +105 -0
  244. package/src/specs/tokens.test.ts +609 -32
  245. package/src/stream.dispatch.test.ts +63 -0
  246. package/src/stream.ts +59 -23
  247. package/src/summarization/__tests__/node.test.ts +77 -0
  248. package/src/summarization/node.ts +46 -18
  249. package/src/tools/ToolNode.ts +150 -99
  250. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  251. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  252. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  253. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  254. package/src/tools/search/keenable-scraper.test.ts +153 -0
  255. package/src/tools/search/keenable-scraper.ts +137 -0
  256. package/src/tools/search/tool.ts +13 -2
  257. package/src/tools/search/types.ts +50 -3
  258. package/src/tools/toolOutputReferences.ts +6 -0
  259. package/src/types/graph.ts +12 -1
  260. package/src/types/run.ts +67 -0
  261. package/src/types/stream.ts +1 -42
  262. package/src/utils/llm.test.ts +18 -0
  263. package/src/utils/llm.ts +4 -1
  264. package/src/utils/tokens.ts +586 -31
  265. package/src/utils/toolContent.test.ts +1432 -0
  266. package/src/utils/toolContent.ts +2707 -0
  267. package/dist/cjs/splitStream.cjs +0 -151
  268. package/dist/cjs/splitStream.cjs.map +0 -1
  269. package/dist/esm/splitStream.mjs +0 -150
  270. package/dist/esm/splitStream.mjs.map +0 -1
  271. package/dist/types/mockStream.d.ts +0 -32
  272. package/dist/types/splitStream.d.ts +0 -37
  273. package/src/mockStream.ts +0 -99
  274. package/src/splitStream.ts +0 -234
@@ -1,5 +1,6 @@
1
1
  import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
2
  import type { TPayload } from '@/types';
3
+ import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
3
4
  import { ContentTypes, Constants, Providers } from '@/common';
4
5
  import { formatAgentMessages } from './format';
5
6
 
@@ -324,3 +325,269 @@ describe('formatAgentMessages steer replay', () => {
324
325
  ]);
325
326
  });
326
327
  });
328
+
329
+ describe('formatAgentMessages trailing-steer anchor', () => {
330
+ const steerPart = (text: string): Record<string, unknown> =>
331
+ ({
332
+ type: ContentTypes.STEER,
333
+ [ContentTypes.STEER]: text,
334
+ }) as unknown as Record<string, unknown>;
335
+
336
+ /**
337
+ * A steer that ends an assistant message leaves the replay on a
338
+ * HumanMessage. Followed by the next turn's user message, that reaches the
339
+ * provider as two adjacent user turns — a hard error on Bedrock and
340
+ * Mistral. Reachable today through abort, not only through preemption.
341
+ */
342
+ it('anchors a trailing steer when another turn follows', () => {
343
+ const payload: TPayload = [
344
+ { role: 'user', content: 'Original request' },
345
+ {
346
+ role: 'assistant',
347
+ content: [
348
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Working on it.' },
349
+ steerPart('Actually, stop and do the other thing.'),
350
+ ],
351
+ },
352
+ { role: 'user', content: 'Next turn' },
353
+ ];
354
+
355
+ const { messages } = formatAgentMessages(payload);
356
+
357
+ const steerIndex = messages.findIndex(
358
+ (message) =>
359
+ message instanceof HumanMessage &&
360
+ message.additional_kwargs.source === 'steer'
361
+ );
362
+ expect(steerIndex).toBeGreaterThan(0);
363
+
364
+ const anchor = messages[steerIndex + 1];
365
+ expect(anchor).toBeInstanceOf(AIMessage);
366
+ expect(anchor.content).not.toBe('');
367
+
368
+ const following = messages[steerIndex + 2];
369
+ expect(following).toBeInstanceOf(HumanMessage);
370
+ expect(following.content).toEqual([{ type: 'text', text: 'Next turn' }]);
371
+ });
372
+
373
+ /**
374
+ * `messagesStateReducer` treats a repeated id as replace-in-place, so an
375
+ * anchor stamped with the shared `sourceMessageId` would overwrite the very
376
+ * steer it exists to protect. It must reach the reducer unstamped.
377
+ */
378
+ it('does not stamp the anchor with the shared source message id', () => {
379
+ const payload: TPayload = [
380
+ { role: 'user', content: 'Original request' },
381
+ {
382
+ messageId: 'assistant_1',
383
+ role: 'assistant',
384
+ content: [
385
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Partial answer.' },
386
+ steerPart('Change of plan.'),
387
+ ],
388
+ } as unknown as TPayload[number],
389
+ { role: 'user', content: 'Next turn' },
390
+ ];
391
+
392
+ const { messages } = formatAgentMessages(payload);
393
+
394
+ const steerIndex = messages.findIndex(
395
+ (message) => message.additional_kwargs.source === 'steer'
396
+ );
397
+ expect(steerIndex).toBeGreaterThan(0);
398
+ expect(messages[steerIndex].id).toBe('assistant_1');
399
+ expect(messages[steerIndex - 1].id).toBe('assistant_1');
400
+
401
+ const anchor = messages[steerIndex + 1];
402
+ expect(anchor).toBeInstanceOf(AIMessage);
403
+ expect(anchor.content).not.toBe('');
404
+ expect(anchor.id).not.toBe('assistant_1');
405
+ });
406
+
407
+ /**
408
+ * The anchor exists to keep the sequence provider-valid, so the assertion
409
+ * that matters is what a provider actually receives. An empty-string
410
+ * assistant turn with no tool calls survives the Anthropic converter
411
+ * verbatim — the empty-text repair covers only array content and tool-call
412
+ * turns — so an empty anchor would trade adjacent user turns for an
413
+ * empty-content 400.
414
+ */
415
+ it('survives the Anthropic converter with non-empty assistant content', () => {
416
+ const payload: TPayload = [
417
+ { role: 'user', content: 'Original request' },
418
+ {
419
+ role: 'assistant',
420
+ content: [
421
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Partial answer.' },
422
+ steerPart('Change of plan.'),
423
+ ],
424
+ },
425
+ { role: 'user', content: 'Next turn' },
426
+ ];
427
+
428
+ const { messages } = formatAgentMessages(payload);
429
+ const { messages: anthropicMessages } =
430
+ _convertMessagesToAnthropicPayload(messages);
431
+
432
+ for (const message of anthropicMessages) {
433
+ if (message.role !== 'assistant') {
434
+ continue;
435
+ }
436
+ if (typeof message.content === 'string') {
437
+ expect(message.content.trim()).not.toBe('');
438
+ continue;
439
+ }
440
+ expect(message.content.length).toBeGreaterThan(0);
441
+ }
442
+
443
+ const roles = anthropicMessages.map((message) => message.role);
444
+ for (let i = 1; i < roles.length; i++) {
445
+ expect(roles[i] === 'user' && roles[i - 1] === 'user').toBe(false);
446
+ }
447
+ });
448
+
449
+ /**
450
+ * The old lookahead only proved a later ENTRY existed, not that it emitted.
451
+ * An entry with empty content is skipped silently, so a trailing steer
452
+ * followed only by such entries would leave the anchor as the final turn —
453
+ * an assistant prefill with no request after it.
454
+ */
455
+ it('omits the anchor when no later payload entry emits a message', () => {
456
+ const payload: TPayload = [
457
+ { role: 'user', content: 'Original request' },
458
+ {
459
+ role: 'assistant',
460
+ content: [
461
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Partial answer.' },
462
+ steerPart('Change of plan.'),
463
+ ],
464
+ },
465
+ { role: 'user', content: [] } as unknown as TPayload[number],
466
+ ];
467
+
468
+ const { messages } = formatAgentMessages(payload);
469
+ const last = messages[messages.length - 1];
470
+ expect(last.additional_kwargs.source).toBe('steer');
471
+ expect(
472
+ messages.some(
473
+ (m) => m instanceof AIMessage && m.content === '_'
474
+ )
475
+ ).toBe(false);
476
+ });
477
+
478
+ it('leaves the user turn last when the steer ends the payload', () => {
479
+ const payload: TPayload = [
480
+ { role: 'user', content: 'Original request' },
481
+ {
482
+ role: 'assistant',
483
+ content: [
484
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Working on it.' },
485
+ steerPart('Actually, stop and do the other thing.'),
486
+ ],
487
+ },
488
+ ];
489
+
490
+ const { messages } = formatAgentMessages(payload);
491
+
492
+ const last = messages[messages.length - 1];
493
+ expect(last).toBeInstanceOf(HumanMessage);
494
+ expect(last.additional_kwargs.source).toBe('steer');
495
+ });
496
+
497
+ it('does not anchor when assistant content follows the steer', () => {
498
+ const payload: TPayload = [
499
+ { role: 'user', content: 'Original request' },
500
+ {
501
+ role: 'assistant',
502
+ content: [
503
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Working on it.' },
504
+ steerPart('Focus on item two.'),
505
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Focusing.' },
506
+ ],
507
+ },
508
+ { role: 'user', content: 'Next turn' },
509
+ ];
510
+
511
+ const { messages } = formatAgentMessages(payload);
512
+
513
+ const emptyAssistants = messages.filter(
514
+ (message) => message instanceof AIMessage && message.content === ''
515
+ );
516
+ expect(emptyAssistants).toHaveLength(0);
517
+ });
518
+
519
+ /**
520
+ * A trailing steer followed by another assistant ENTRY needs no anchor —
521
+ * that entry's own assistant turn IS the separation. Emitting the
522
+ * placeholder anyway would put two assistant turns back to back, which
523
+ * strict-alternation providers can reject and nothing downstream merges
524
+ * (`coalesceAdjacentUserTurns` merges user turns only).
525
+ */
526
+ it('suppresses the anchor when the next payload entry is an assistant turn', () => {
527
+ const payload: TPayload = [
528
+ { role: 'user', content: 'Original request' },
529
+ {
530
+ role: 'assistant',
531
+ content: [
532
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Working on it.' },
533
+ steerPart('Actually, do the other thing.'),
534
+ ],
535
+ },
536
+ {
537
+ role: 'assistant',
538
+ content: [
539
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Doing the other thing.' },
540
+ ],
541
+ },
542
+ ];
543
+
544
+ const { messages } = formatAgentMessages(payload);
545
+
546
+ expect(messages.map((message) => message.constructor.name)).toEqual([
547
+ 'HumanMessage',
548
+ 'AIMessage',
549
+ 'HumanMessage',
550
+ 'AIMessage',
551
+ ]);
552
+ expect(messages[2].additional_kwargs.source).toBe('steer');
553
+ expect(messages[3].content).toEqual([
554
+ { type: 'text', text: 'Doing the other thing.' },
555
+ ]);
556
+ expect(
557
+ messages.some(
558
+ (message) => message instanceof AIMessage && message.content === '_'
559
+ )
560
+ ).toBe(false);
561
+ });
562
+
563
+ it('never emits two adjacent user turns around a trailing steer', () => {
564
+ const payload: TPayload = [
565
+ { role: 'user', content: 'Original request' },
566
+ {
567
+ role: 'assistant',
568
+ content: [
569
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Partial answer.' },
570
+ steerPart('Change of plan.'),
571
+ ],
572
+ },
573
+ { role: 'user', content: 'Next turn' },
574
+ {
575
+ role: 'assistant',
576
+ content: [
577
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Second answer.' },
578
+ steerPart('Change again.'),
579
+ ],
580
+ },
581
+ { role: 'user', content: 'Third turn' },
582
+ ];
583
+
584
+ const { messages } = formatAgentMessages(payload);
585
+
586
+ for (let i = 1; i < messages.length; i++) {
587
+ const adjacentHumans =
588
+ messages[i] instanceof HumanMessage &&
589
+ messages[i - 1] instanceof HumanMessage;
590
+ expect(adjacentHumans).toBe(false);
591
+ }
592
+ });
593
+ });