@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.
- package/dist/cjs/common/constants.cjs +21 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/events.cjs +10 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +456 -7
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +25 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +8 -0
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +1 -3
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +268 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/preempt.cjs +132 -0
- package/dist/cjs/llm/preempt.cjs.map +1 -0
- package/dist/cjs/main.cjs +17 -3
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- package/dist/cjs/messages/format.cjs +72 -0
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +63 -0
- package/dist/cjs/messages/handoffCue.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +3 -0
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/run.cjs +80 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +18 -9
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +28 -7
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -23
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +9 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/common/constants.mjs +19 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/events.mjs +10 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +455 -6
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +25 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +9 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +1 -3
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +270 -6
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/preempt.mjs +131 -0
- package/dist/esm/llm/preempt.mjs.map +1 -0
- package/dist/esm/main.mjs +8 -5
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- package/dist/esm/messages/format.mjs +72 -0
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +61 -0
- package/dist/esm/messages/handoffCue.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +3 -0
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/run.mjs +80 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +19 -10
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +27 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -23
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +9 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +138 -1
- package/dist/types/hooks/HookRegistry.d.ts +15 -0
- package/dist/types/hooks/index.d.ts +12 -1
- package/dist/types/hooks/types.d.ts +45 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +7 -0
- package/dist/types/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -0
- package/dist/types/messages/handoffCue.d.ts +40 -0
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/injected.d.ts +3 -0
- package/dist/types/run.d.ts +7 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +0 -7
- package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
- package/dist/types/tools/search/types.d.ts +31 -2
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/run.d.ts +65 -0
- package/dist/types/types/stream.d.ts +0 -25
- package/package.json +1 -1
- package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
- package/src/common/constants.ts +21 -0
- package/src/events.ts +15 -1
- package/src/graphs/Graph.ts +568 -3
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
- package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
- package/src/hooks/HookRegistry.ts +40 -0
- package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
- package/src/hooks/index.ts +16 -2
- package/src/hooks/types.ts +47 -3
- package/src/index.ts +1 -1
- package/src/langfuse.ts +26 -1
- package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
- package/src/llm/bedrock/utils/message_inputs.ts +14 -11
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.ts +417 -8
- package/src/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- package/src/messages/format.ts +91 -0
- package/src/messages/formatAgentMessages.steer.test.ts +267 -0
- package/src/messages/handoffCue.test.ts +96 -0
- package/src/messages/handoffCue.ts +78 -0
- package/src/messages/index.ts +3 -0
- package/src/messages/injected.test.ts +90 -0
- package/src/messages/injected.ts +74 -0
- package/src/run.ts +91 -6
- package/src/scripts/preempt-probe.ts +330 -0
- package/src/scripts/preempt-scenarios.ts +388 -0
- package/src/session/handlers.ts +32 -12
- package/src/specs/handoffCue.test.ts +165 -0
- package/src/specs/langfuse-callbacks.test.ts +352 -2
- package/src/specs/preemptSeal.test.ts +309 -0
- package/src/stream.dispatch.test.ts +63 -0
- package/src/stream.ts +36 -8
- package/src/tools/ToolNode.ts +3 -31
- package/src/tools/search/keenable-scraper.test.ts +153 -0
- package/src/tools/search/keenable-scraper.ts +137 -0
- package/src/tools/search/tool.ts +13 -2
- package/src/tools/search/types.ts +50 -3
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +0 -41
- package/dist/cjs/splitStream.cjs +0 -151
- package/dist/cjs/splitStream.cjs.map +0 -1
- package/dist/esm/splitStream.mjs +0 -150
- package/dist/esm/splitStream.mjs.map +0 -1
- package/dist/types/mockStream.d.ts +0 -32
- package/dist/types/splitStream.d.ts +0 -37
- package/src/mockStream.ts +0 -99
- 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
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
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({
|