@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
@@ -0,0 +1,61 @@
1
+ import { HumanMessage } from "@langchain/core/messages";
2
+ //#region src/messages/handoffCue.ts
3
+ /**
4
+ * Bracketed-meta convention, like the handoff path's
5
+ * `[Processed tool result and transferring to …]` bridge. The wording makes
6
+ * two things unambiguous to the model: the assistant turn above is FINISHED,
7
+ * and it belongs to a previous stage — so the successor answers as itself
8
+ * instead of continuing someone else's sentence.
9
+ */
10
+ const PREDECESSOR_HANDOFF_CUE = "[The assistant message above is the completed output of a previous agent. Respond now according to your own role and instructions.]";
11
+ /**
12
+ * Appends a user-turn handoff cue when a payload ends with an assistant turn
13
+ * that THIS RUN produced — which only happens when a different agent in a
14
+ * multi-agent workflow produced it (an agent's own self-loops always re-enter
15
+ * on a tool result or an injected user turn).
16
+ *
17
+ * Why: providers with prefill semantics (Anthropic, Bedrock-Claude) treat a
18
+ * trailing assistant message as a prefill and CONTINUE it. A bare direct-edge
19
+ * successor therefore speaks in its predecessor's voice — or, when the
20
+ * trailing turn reads complete (a preemption steer's short resume, say),
21
+ * returns empty content (danny-avila/agents#345, reproduced live 3/3).
22
+ * Handoff edges with instructions and prompt-instruction edges already break
23
+ * the prefill with a user turn; this closes the same gap for bare edges.
24
+ *
25
+ * Fail-safe OFF by provenance: the trailing payload message must be one the
26
+ * run itself produced (`isRunProduced`, backed by the graph's run-produced id
27
+ * set — immune to summarization compaction, which rewrites the live array
28
+ * and stales index-based boundaries). Host-supplied trailing assistant
29
+ * turns (deliberate prefill flows) never match — the run has not produced
30
+ * them — so single-agent prefill behavior is untouched. Wire-only: the cue is
31
+ * appended to the provider projection, never to graph state or host history.
32
+ */
33
+ function appendPredecessorHandoffCue(messages, isRunProduced) {
34
+ const last = messages.at(-1);
35
+ if (last == null || last.getType() !== "ai") return messages;
36
+ if (isRunProduced == null || !isRunProduced(last)) return messages;
37
+ return [...messages, new HumanMessage({
38
+ content: PREDECESSOR_HANDOFF_CUE,
39
+ additional_kwargs: {
40
+ role: "user",
41
+ isMeta: true,
42
+ source: "handoff"
43
+ }
44
+ })];
45
+ }
46
+ /**
47
+ * Strips a trailing handoff cue. The counterpart for the serving-provider
48
+ * funnel: an Anthropic-like PRIMARY bakes the cue into its measured payload,
49
+ * and a tolerant fallback (OpenAI, Mistral, Bedrock-Nova) re-sending that
50
+ * payload must not ship the Claude-only synthetic turn. Identity on the
51
+ * no-op path.
52
+ */
53
+ function removePredecessorHandoffCue(messages) {
54
+ const last = messages.at(-1);
55
+ if (last == null || last.getType() !== "human" || last.additional_kwargs.source !== "handoff" || last.content !== "[The assistant message above is the completed output of a previous agent. Respond now according to your own role and instructions.]") return messages;
56
+ return messages.slice(0, -1);
57
+ }
58
+ //#endregion
59
+ export { PREDECESSOR_HANDOFF_CUE, appendPredecessorHandoffCue, removePredecessorHandoffCue };
60
+
61
+ //# sourceMappingURL=handoffCue.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoffCue.mjs","names":[],"sources":["../../../src/messages/handoffCue.ts"],"sourcesContent":["// src/messages/handoffCue.ts\nimport { HumanMessage } from '@langchain/core/messages';\nimport type { BaseMessage } from '@langchain/core/messages';\n\n/**\n * Bracketed-meta convention, like the handoff path's\n * `[Processed tool result and transferring to …]` bridge. The wording makes\n * two things unambiguous to the model: the assistant turn above is FINISHED,\n * and it belongs to a previous stage — so the successor answers as itself\n * instead of continuing someone else's sentence.\n */\nexport const PREDECESSOR_HANDOFF_CUE =\n '[The assistant message above is the completed output of a previous ' +\n 'agent. Respond now according to your own role and instructions.]';\n\n/**\n * Appends a user-turn handoff cue when a payload ends with an assistant turn\n * that THIS RUN produced — which only happens when a different agent in a\n * multi-agent workflow produced it (an agent's own self-loops always re-enter\n * on a tool result or an injected user turn).\n *\n * Why: providers with prefill semantics (Anthropic, Bedrock-Claude) treat a\n * trailing assistant message as a prefill and CONTINUE it. A bare direct-edge\n * successor therefore speaks in its predecessor's voice — or, when the\n * trailing turn reads complete (a preemption steer's short resume, say),\n * returns empty content (danny-avila/agents#345, reproduced live 3/3).\n * Handoff edges with instructions and prompt-instruction edges already break\n * the prefill with a user turn; this closes the same gap for bare edges.\n *\n * Fail-safe OFF by provenance: the trailing payload message must be one the\n * run itself produced (`isRunProduced`, backed by the graph's run-produced id\n * set — immune to summarization compaction, which rewrites the live array\n * and stales index-based boundaries). Host-supplied trailing assistant\n * turns (deliberate prefill flows) never match — the run has not produced\n * them — so single-agent prefill behavior is untouched. Wire-only: the cue is\n * appended to the provider projection, never to graph state or host history.\n */\nexport function appendPredecessorHandoffCue(\n messages: BaseMessage[],\n isRunProduced: ((message: BaseMessage) => boolean) | undefined\n): BaseMessage[] {\n const last = messages.at(-1);\n if (last == null || last.getType() !== 'ai') {\n return messages;\n }\n if (isRunProduced == null || !isRunProduced(last)) {\n return messages;\n }\n return [\n ...messages,\n new HumanMessage({\n content: PREDECESSOR_HANDOFF_CUE,\n additional_kwargs: { role: 'user', isMeta: true, source: 'handoff' },\n }),\n ];\n}\n\n/**\n * Strips a trailing handoff cue. The counterpart for the serving-provider\n * funnel: an Anthropic-like PRIMARY bakes the cue into its measured payload,\n * and a tolerant fallback (OpenAI, Mistral, Bedrock-Nova) re-sending that\n * payload must not ship the Claude-only synthetic turn. Identity on the\n * no-op path.\n */\nexport function removePredecessorHandoffCue(\n messages: BaseMessage[]\n): BaseMessage[] {\n const last = messages.at(-1);\n if (\n last == null ||\n last.getType() !== 'human' ||\n last.additional_kwargs.source !== 'handoff' ||\n last.content !== PREDECESSOR_HANDOFF_CUE\n ) {\n return messages;\n }\n return messages.slice(0, -1);\n}\n"],"mappings":";;;;;;;;;AAWA,MAAa,0BACX;;;;;;;;;;;;;;;;;;;;;;;AAyBF,SAAgB,4BACd,UACA,eACe;CACf,MAAM,OAAO,SAAS,GAAG,EAAE;CAC3B,IAAI,QAAQ,QAAQ,KAAK,QAAQ,MAAM,MACrC,OAAO;CAET,IAAI,iBAAiB,QAAQ,CAAC,cAAc,IAAI,GAC9C,OAAO;CAET,OAAO,CACL,GAAG,UACH,IAAI,aAAa;EACf,SAAS;EACT,mBAAmB;GAAE,MAAM;GAAQ,QAAQ;GAAM,QAAQ;EAAU;CACrE,CAAC,CACH;AACF;;;;;;;;AASA,SAAgB,4BACd,UACe;CACf,MAAM,OAAO,SAAS,GAAG,EAAE;CAC3B,IACE,QAAQ,QACR,KAAK,QAAQ,MAAM,WACnB,KAAK,kBAAkB,WAAW,aAClC,KAAK,YAAA,uIAEL,OAAO;CAET,OAAO,SAAS,MAAM,GAAG,EAAE;AAC7B"}
@@ -9,6 +9,9 @@ import "./format.mjs";
9
9
  import "./anthropicToolCache.mjs";
10
10
  import "./content.mjs";
11
11
  import "./tools.mjs";
12
+ import "./injected.mjs";
13
+ import "./alternation.mjs";
14
+ import "./handoffCue.mjs";
12
15
  import "./reducer.mjs";
13
16
  import "./recency.mjs";
14
17
  export {};
@@ -0,0 +1,60 @@
1
+ import "../common/enum.mjs";
2
+ import "../common/index.mjs";
3
+ import { toLangChainContent } from "./langchain.mjs";
4
+ import { HumanMessage } from "@langchain/core/messages";
5
+ //#region src/messages/injected.ts
6
+ /**
7
+ * Converts `InjectedMessage` instances to LangChain `HumanMessage` objects.
8
+ * Both 'user' and 'system' roles become `HumanMessage` to avoid provider
9
+ * rejections (Anthropic/Google reject non-leading SystemMessages). The
10
+ * original role is preserved in `additional_kwargs` for downstream consumers.
11
+ *
12
+ * Shared by both injection boundaries — `ToolNode`'s tool-batch dispatch and
13
+ * `StandardGraph`'s preempt-boundary dispatch. Keeping one implementation is
14
+ * load-bearing rather than tidy: the provider-safety argument for sealing a
15
+ * stream mid-generation rests on the injected turn having byte-identical
16
+ * shape to the one the already-shipped tool boundary emits, so the two sites
17
+ * must not be able to drift.
18
+ */
19
+ /**
20
+ * True when an entry carries nothing a provider will accept as a turn.
21
+ *
22
+ * The public `InjectedMessage` type admits `content: ''` and `content: []`,
23
+ * and a host hook is free to return one. Converting it anyway produces a
24
+ * `HumanMessage` that passes the caller's `length > 0` test, so the graph
25
+ * resumes and sends a trailing EMPTY user turn — which Anthropic and other
26
+ * strict providers reject outright, turning a cooperative seal into a failed
27
+ * run. Permissive providers merely burn a model call.
28
+ *
29
+ * Whitespace counts as empty, matching the standard `canSealPreempt` applies
30
+ * to the assistant side of the same pair. Non-text blocks count as content: a
31
+ * media-only steer is a real turn and must survive.
32
+ */
33
+ function isEmptyInjectedContent(content) {
34
+ if (typeof content === "string") return content.trim() === "";
35
+ if (content.length === 0) return true;
36
+ return content.every((block) => {
37
+ if (block.type !== "text") return false;
38
+ const text = block["text"];
39
+ return typeof text !== "string" || text.trim() === "";
40
+ });
41
+ }
42
+ function convertInjectedMessages(messages) {
43
+ const converted = [];
44
+ for (const msg of messages) {
45
+ if (isEmptyInjectedContent(msg.content)) continue;
46
+ const additional_kwargs = { role: msg.role };
47
+ if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;
48
+ if (msg.source != null) additional_kwargs.source = msg.source;
49
+ if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;
50
+ converted.push(new HumanMessage({
51
+ content: toLangChainContent(msg.content),
52
+ additional_kwargs
53
+ }));
54
+ }
55
+ return converted;
56
+ }
57
+ //#endregion
58
+ export { convertInjectedMessages };
59
+
60
+ //# sourceMappingURL=injected.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injected.mjs","names":[],"sources":["../../../src/messages/injected.ts"],"sourcesContent":["// src/messages/injected.ts\nimport { HumanMessage } from '@langchain/core/messages';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { InjectedMessage } from '@/types/tools';\nimport { ContentTypes } from '@/common';\nimport { toLangChainContent } from './langchain';\n\n/**\n * Converts `InjectedMessage` instances to LangChain `HumanMessage` objects.\n * Both 'user' and 'system' roles become `HumanMessage` to avoid provider\n * rejections (Anthropic/Google reject non-leading SystemMessages). The\n * original role is preserved in `additional_kwargs` for downstream consumers.\n *\n * Shared by both injection boundaries — `ToolNode`'s tool-batch dispatch and\n * `StandardGraph`'s preempt-boundary dispatch. Keeping one implementation is\n * load-bearing rather than tidy: the provider-safety argument for sealing a\n * stream mid-generation rests on the injected turn having byte-identical\n * shape to the one the already-shipped tool boundary emits, so the two sites\n * must not be able to drift.\n */\n/**\n * True when an entry carries nothing a provider will accept as a turn.\n *\n * The public `InjectedMessage` type admits `content: ''` and `content: []`,\n * and a host hook is free to return one. Converting it anyway produces a\n * `HumanMessage` that passes the caller's `length > 0` test, so the graph\n * resumes and sends a trailing EMPTY user turn — which Anthropic and other\n * strict providers reject outright, turning a cooperative seal into a failed\n * run. Permissive providers merely burn a model call.\n *\n * Whitespace counts as empty, matching the standard `canSealPreempt` applies\n * to the assistant side of the same pair. Non-text blocks count as content: a\n * media-only steer is a real turn and must survive.\n */\nfunction isEmptyInjectedContent(content: InjectedMessage['content']): boolean {\n if (typeof content === 'string') {\n return content.trim() === '';\n }\n if (content.length === 0) {\n return true;\n }\n return content.every((block) => {\n if (block.type !== ContentTypes.TEXT) {\n return false;\n }\n const text = block[ContentTypes.TEXT];\n return typeof text !== 'string' || text.trim() === '';\n });\n}\n\nexport function convertInjectedMessages(\n messages: InjectedMessage[]\n): BaseMessage[] {\n const converted: BaseMessage[] = [];\n for (const msg of messages) {\n if (isEmptyInjectedContent(msg.content)) {\n continue;\n }\n const additional_kwargs: Record<string, unknown> = {\n role: msg.role,\n };\n if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;\n if (msg.source != null) additional_kwargs.source = msg.source;\n if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;\n\n converted.push(\n new HumanMessage({\n content: toLangChainContent(msg.content),\n additional_kwargs,\n })\n );\n }\n return converted;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAS,uBAAuB,SAA8C;CAC5E,IAAI,OAAO,YAAY,UACrB,OAAO,QAAQ,KAAK,MAAM;CAE5B,IAAI,QAAQ,WAAW,GACrB,OAAO;CAET,OAAO,QAAQ,OAAO,UAAU;EAC9B,IAAI,MAAM,SAAA,QACR,OAAO;EAET,MAAM,OAAO,MAAA;EACb,OAAO,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM;CACrD,CAAC;AACH;AAEA,SAAgB,wBACd,UACe;CACf,MAAM,YAA2B,CAAC;CAClC,KAAK,MAAM,OAAO,UAAU;EAC1B,IAAI,uBAAuB,IAAI,OAAO,GACpC;EAEF,MAAM,oBAA6C,EACjD,MAAM,IAAI,KACZ;EACA,IAAI,IAAI,UAAU,MAAM,kBAAkB,SAAS,IAAI;EACvD,IAAI,IAAI,UAAU,MAAM,kBAAkB,SAAS,IAAI;EACvD,IAAI,IAAI,aAAa,MAAM,kBAAkB,YAAY,IAAI;EAE7D,UAAU,KACR,IAAI,aAAa;GACf,SAAS,mBAAmB,IAAI,OAAO;GACvC;EACF,CAAC,CACH;CACF;CACA,OAAO;AACT"}
package/dist/esm/run.mjs CHANGED
@@ -4,10 +4,12 @@ import { resolveLangfuseRuntimeScope, withLangfuseRuntimeScope } from "./langfus
4
4
  import { createLangfuseHandler, createLangfuseTraceMetadata, disposeLangfuseHandler, getLangfuseTraceName, isLangfuseCallbackHandler, withLangfuseAttributes } from "./langfuse.mjs";
5
5
  import { ACTIVITY_LABEL_PROMPT, buildActivityLabelPrompt } from "./prompts/activityLabel.mjs";
6
6
  import { appendCallbacks, findCallback } from "./utils/callbacks.mjs";
7
+ import "./common/constants.mjs";
7
8
  import "./common/enum.mjs";
8
9
  import "./common/index.mjs";
9
10
  import { createCompletionTitleRunnable, createTitleRunnable } from "./utils/title.mjs";
10
11
  import { createTokenCounter, encodingForModel } from "./utils/tokens.mjs";
12
+ import { resolveMaxSeals } from "./llm/preempt.mjs";
11
13
  import { initializeLangfuseTracing } from "./instrumentation.mjs";
12
14
  import { isOpenAILike } from "./utils/llm.mjs";
13
15
  import { HandlerRegistry } from "./events.mjs";
@@ -79,6 +81,7 @@ var Run = class Run {
79
81
  interruptingToolNames;
80
82
  toolExecution;
81
83
  subagentUsageSink;
84
+ preemption;
82
85
  indexTokenCountMap;
83
86
  calibrationRatio = 1;
84
87
  graphRunnable;
@@ -125,6 +128,7 @@ var Run = class Run {
125
128
  this.interruptingToolNames = config.interruptingToolNames;
126
129
  this.toolExecution = config.toolExecution;
127
130
  this.subagentUsageSink = config.subagentUsageSink;
131
+ this.preemption = config.preemption;
128
132
  if (!config.graphConfig) throw new Error("Graph config not provided");
129
133
  /** Handle different graph types */
130
134
  if (config.graphConfig.type === "multi-agent") {
@@ -171,7 +175,8 @@ var Run = class Run {
171
175
  tokenCounter: this.tokenCounter,
172
176
  indexTokenCountMap: this.indexTokenCountMap,
173
177
  calibrationRatio: this.calibrationRatio,
174
- subagentUsageSink: this.subagentUsageSink
178
+ subagentUsageSink: this.subagentUsageSink,
179
+ preemption: this.preemption
175
180
  });
176
181
  /** Propagate compile options from graph config */
177
182
  standardGraph.compileOptions = this.applyHITLCheckpointerFallback(config.compileOptions);
@@ -196,7 +201,8 @@ var Run = class Run {
196
201
  tokenCounter: this.tokenCounter,
197
202
  indexTokenCountMap: this.indexTokenCountMap,
198
203
  calibrationRatio: this.calibrationRatio,
199
- subagentUsageSink: this.subagentUsageSink
204
+ subagentUsageSink: this.subagentUsageSink,
205
+ preemption: this.preemption
200
206
  });
201
207
  multiAgentGraph.compileOptions = this.applyHITLCheckpointerFallback(compileOptions);
202
208
  this.hasCheckpointer = multiAgentGraph.compileOptions?.checkpointer != null;
@@ -381,6 +387,17 @@ var Run = class Run {
381
387
  getResolvedInstructionOverhead() {
382
388
  return this.Graph?.getResolvedInstructionOverhead();
383
389
  }
390
+ /**
391
+ * Cooperative-seal counters for this run. `emptyBoundaries` is the one to
392
+ * watch: it counts seals whose `PreemptBoundary` produced nothing to
393
+ * inject, which ends the turn early and leaves the answer unfinished.
394
+ */
395
+ getPreemptStats() {
396
+ return this.Graph?.getPreemptStats() ?? {
397
+ seals: 0,
398
+ emptyBoundaries: 0
399
+ };
400
+ }
384
401
  getToolCount() {
385
402
  return this.Graph?.getToolCount() ?? 0;
386
403
  }
@@ -452,12 +469,34 @@ var Run = class Run {
452
469
  */
453
470
  const isResume = inputs instanceof Command;
454
471
  const stateInputs = isResume ? void 0 : inputs;
472
+ /**
473
+ * Every honored seal costs one extra superstep, so a preemption-enabled
474
+ * run reserves headroom for its whole seal budget. Without it, a
475
+ * tool-heavy agent that gets preempted could hit `GraphRecursionError` —
476
+ * which surfaces as a thrown stream, setting `streamThrew`, firing
477
+ * `StopFailure`, and wiping via `clearHeavyState()` exactly the partial
478
+ * content the seal existed to preserve.
479
+ */
480
+ const recursionLimit = (callerConfig.recursionLimit ?? 50) + (this.preemption != null ? resolveMaxSeals(this.preemption.maxSeals) : 0);
455
481
  const config = {
456
- recursionLimit: 50,
457
482
  ...callerConfig,
483
+ recursionLimit,
458
484
  configurable: { ...callerConfig.configurable }
459
485
  };
460
486
  /**
487
+ * Cancellation can arrive either at graph construction or per-call through
488
+ * `callerConfig.signal`, and boundary hooks need to observe both — for a
489
+ * multi-agent run the construction signal does not exist at all, since
490
+ * `MultiAgentGraphConfig` exposes none. Carried on its own field, assigned
491
+ * unconditionally: writing into `graph.signal` would leak this call's
492
+ * controller into later calls (model-call config and subagent
493
+ * parentSignal read that field, and `clearHeavyState()` is skipped on
494
+ * HITL interrupts), while a conditional write would keep observing a
495
+ * stale controller the host has since aborted. The boundary dispatch
496
+ * composes both channels; see `dispatchPreemptBoundary`.
497
+ */
498
+ graph.callerSignal = callerConfig.signal;
499
+ /**
461
500
  * Skip `resetValues` on resume — we're continuing an in-flight
462
501
  * run, not starting a fresh one. Resetting would wipe the
463
502
  * sidecars (`toolCallStepIds`, `stepKeyIds`, accumulated
@@ -604,10 +643,17 @@ var Run = class Run {
604
643
  * graph doesn't take another model turn after the halting
605
644
  * operation completes.
606
645
  *
607
- * Limitation: the current step (in-flight model call, ongoing
608
- * tool batch) is not aborted only the next step is skipped.
609
- * This matches Claude Code's `continue: false` semantic where
610
- * the active operation finishes before halting takes effect.
646
+ * This `break` is NOT graceful, despite what a `continue: false`
647
+ * reading suggests. Leaving the `for await` calls the iterator's
648
+ * `return()`, which cancels the reader
649
+ * (`@langchain/core/utils/stream`), and langgraph's stream wrapper
650
+ * turns that cancel into `_abortController.abort()`
651
+ * (`pregel/stream.js`). The in-flight model call or tool batch is
652
+ * torn down where it stands — it does not finish first.
653
+ *
654
+ * A halt is therefore the wrong tool for "stop generating but keep
655
+ * what you have". That is what `RunConfig.preemption` is for: it
656
+ * seals the stream at a provider-safe boundary and keeps the run.
611
657
  */
612
658
  const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
613
659
  if (haltSignal != null) {
@@ -631,10 +677,37 @@ var Run = class Run {
631
677
  threadId,
632
678
  agentId: graph.defaultAgentId,
633
679
  messages: graph.getRunMessages() ?? stateInputs?.messages ?? [],
680
+ /**
681
+ * A seal whose boundary ended the turn early must say so. The
682
+ * hook-supplied reason wins when a `PreemptBoundary` hook halted
683
+ * with one — a persistence/audit `Stop` hook should record the
684
+ * actual cause, not the generic label — and `preempt_incomplete`
685
+ * is reserved for the boundary that simply had nothing to inject.
686
+ */
687
+ stopReason: graph.preemptHaltReason ?? (graph.preemptIncomplete ? "preempt_incomplete" : void 0),
634
688
  stopHookActive: false
635
689
  },
636
690
  sessionId: this.id
637
691
  }).catch(() => {});
692
+ /**
693
+ * A `PreemptBoundary` hook that returned `preventContinuation` has its
694
+ * registry halt cleared by the graph — that is what stops the halt from
695
+ * cancelling the stream before the sealed turn commits — so the reason
696
+ * is carried across on the graph instead. Surfaced here, AFTER the
697
+ * `Stop` dispatch above, so the host still receives a completion signal
698
+ * to persist the partial answer with while `getHaltReason()` correctly
699
+ * reports that a hook stopped the run rather than the model finishing.
700
+ *
701
+ * An empty boundary — sealed, but nothing to inject because the host's
702
+ * queue was drained or cancelled in the meantime — cut the answer short
703
+ * just as surely, only without a hook-supplied reason. It surfaces
704
+ * through the same channel under the same name the `Stop` dispatch
705
+ * already used for its `stopReason`, so terminal consumers
706
+ * (`AgentSession` emits `run.halted`, not `run.completed`) cannot
707
+ * finalize a truncated answer as a natural finish.
708
+ */
709
+ if (this._haltedReason == null && graph.preemptHaltReason != null) this._haltedReason = graph.preemptHaltReason;
710
+ else if (this._haltedReason == null && graph.preemptIncomplete) this._haltedReason = "preempt_incomplete";
638
711
  };
639
712
  try {
640
713
  await withLangfuseRuntimeScope(resolveLangfuseRuntimeScope({