@librechat/agents 3.4.0 → 3.4.2
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/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +165 -57
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
- package/dist/cjs/graphs/createGraph.cjs +15 -0
- package/dist/cjs/graphs/createGraph.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/main.cjs +34 -24
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +130 -53
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +321 -95
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/search/rerankers.cjs +154 -1
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
- package/dist/cjs/tools/subagent/index.cjs +1 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +165 -57
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
- package/dist/esm/graphs/createGraph.mjs +15 -0
- package/dist/esm/graphs/createGraph.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/main.mjs +21 -19
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +131 -54
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +2 -2
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +322 -96
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
- package/dist/esm/tools/search/rerankers.mjs +154 -1
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
- package/dist/esm/tools/subagent/index.mjs +1 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +22 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
- package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
- package/dist/types/graphs/createGraph.d.ts +13 -0
- package/dist/types/graphs/graphFactory.d.ts +13 -0
- package/dist/types/graphs/index.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/run.d.ts +6 -3
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/search/rerankers.d.ts +28 -0
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
- package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
- package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
- package/dist/types/tools/subagent/index.d.ts +1 -1
- package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +189 -24
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +8 -2
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +349 -104
- package/src/graphs/MultiAgentGraph.ts +149 -47
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
- package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
- package/src/graphs/__tests__/createGraph.test.ts +206 -0
- package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
- package/src/graphs/createGraph.ts +32 -0
- package/src/graphs/graphFactory.ts +12 -0
- package/src/graphs/index.ts +2 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +214 -56
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/specs/graph-subagent.live.test.ts +242 -0
- package/src/specs/graph-subagent.test.ts +322 -0
- package/src/specs/langfuse-routing.integration.test.ts +120 -1
- package/src/specs/subagent.test.ts +130 -4
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +643 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
- package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/search/rag-api-reranker.test.ts +891 -0
- package/src/tools/search/rerankers.ts +260 -0
- package/src/tools/search/tool.ts +6 -0
- package/src/tools/search/types.ts +45 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
- package/src/tools/subagent/SubagentExecutor.ts +2232 -312
- package/src/tools/subagent/SubagentReplay.ts +584 -0
- package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
- package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
- package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
- package/src/tools/subagent/childGraphConfig.ts +694 -0
- package/src/tools/subagent/index.ts +6 -0
- package/src/tools/subagent/runtimeLimits.ts +7 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +222 -24
- package/src/types/hitl.ts +16 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SUBAGENT_PARENT_BATCH_CONFIG_KEY, SUBAGENT_REPLAY_CONTROLLER, attachSubagentResumeManifest } from "./subagent/SubagentReplay.mjs";
|
|
1
2
|
import { composeAbortSignals } from "../utils/misc.mjs";
|
|
2
3
|
import { resolveLangfuseRuntimeScope, withLangfuseRuntimeScope } from "../langfuseRuntimeScope.mjs";
|
|
3
4
|
import { CODE_EXECUTION_TOOLS } from "../common/enum.mjs";
|
|
@@ -12,14 +13,15 @@ import { INTENT_ARG, isIntentLabelProperty, outcomeFieldsFromResult, readOutcome
|
|
|
12
13
|
import { ToolOutputReferenceRegistry, buildReferenceKey } from "./toolOutputReferences.mjs";
|
|
13
14
|
import { RunnableCallable } from "../utils/run.mjs";
|
|
14
15
|
import "../utils/index.mjs";
|
|
15
|
-
import { stripCodeSessionFileSummary } from "./CodeSessionFileSummary.mjs";
|
|
16
16
|
import { executeHooks } from "../hooks/executeHooks.mjs";
|
|
17
|
+
import { TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from "../hooks/types.mjs";
|
|
17
18
|
import "../hooks/index.mjs";
|
|
19
|
+
import { stripCodeSessionFileSummary } from "./CodeSessionFileSummary.mjs";
|
|
18
20
|
import { resolveLocalExecutionTools, resolveLocalToolRegistry } from "./local/resolveLocalExecutionTools.mjs";
|
|
19
21
|
import "./local/index.mjs";
|
|
20
22
|
import { nanoid } from "nanoid";
|
|
21
23
|
import { AIMessage, HumanMessage, ToolMessage, isAIMessage, isBaseMessage } from "@langchain/core/messages";
|
|
22
|
-
import { Command, END, Send, interrupt, isCommand, isGraphInterrupt } from "@langchain/langgraph";
|
|
24
|
+
import { Command, END, GraphInterrupt, Send, interrupt, isCommand, isGraphInterrupt } from "@langchain/langgraph";
|
|
23
25
|
import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
|
|
24
26
|
//#region src/tools/ToolNode.ts
|
|
25
27
|
/** Host-facing batch requests must not carry the batch's breaker scope —
|
|
@@ -29,6 +31,32 @@ function stripRunBreakerScope(configurable) {
|
|
|
29
31
|
const { [RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = configurable;
|
|
30
32
|
return rest;
|
|
31
33
|
}
|
|
34
|
+
function createToolApprovalReplayKey(config, agentId, toolUseId) {
|
|
35
|
+
const configuredScope = config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
|
|
36
|
+
const threadId = config.configurable?.thread_id;
|
|
37
|
+
let executionScope = "";
|
|
38
|
+
if (typeof configuredScope === "string") executionScope = configuredScope;
|
|
39
|
+
else if (typeof threadId === "string") executionScope = threadId;
|
|
40
|
+
return {
|
|
41
|
+
executionScope,
|
|
42
|
+
agentId,
|
|
43
|
+
toolUseId
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function getToolApprovalReplaySessionId(config, hookSessionId) {
|
|
47
|
+
const configuredScope = config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
|
|
48
|
+
return typeof configuredScope === "string" && configuredScope.length > 0 ? configuredScope : hookSessionId;
|
|
49
|
+
}
|
|
50
|
+
function withSubagentReplayBatch(config, replayBatchKey) {
|
|
51
|
+
if (replayBatchKey == null) return config;
|
|
52
|
+
return {
|
|
53
|
+
...config,
|
|
54
|
+
configurable: {
|
|
55
|
+
...config.configurable,
|
|
56
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: replayBatchKey
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
32
60
|
function createToolErrorOwnership() {
|
|
33
61
|
return {
|
|
34
62
|
handlerOwned: /* @__PURE__ */ new Set(),
|
|
@@ -99,6 +127,25 @@ function describeOfferedShape(value) {
|
|
|
99
127
|
if (Array.isArray(value)) return "array";
|
|
100
128
|
return typeof value;
|
|
101
129
|
}
|
|
130
|
+
function findAssistantBatch(messages) {
|
|
131
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
132
|
+
const message = messages[i];
|
|
133
|
+
if (isAIMessage(message)) return {
|
|
134
|
+
message,
|
|
135
|
+
index: i,
|
|
136
|
+
messageCount: messages.length
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function getAssistantBatchReplayKey(batch, runId, threadId) {
|
|
141
|
+
return JSON.stringify([
|
|
142
|
+
runId ?? null,
|
|
143
|
+
threadId ?? null,
|
|
144
|
+
batch.message.id ?? null,
|
|
145
|
+
batch.index,
|
|
146
|
+
batch.messageCount
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
102
149
|
/**
|
|
103
150
|
* Build the `tool_approval` interrupt payload from the set of pending
|
|
104
151
|
* `ask`-decision entries collected during PreToolUse hook handling.
|
|
@@ -106,9 +153,10 @@ function describeOfferedShape(value) {
|
|
|
106
153
|
* scope. The interrupt itself is raised by the caller (which still
|
|
107
154
|
* needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
|
|
108
155
|
*/
|
|
109
|
-
function buildToolApprovalInterruptPayload(askEntries) {
|
|
156
|
+
function buildToolApprovalInterruptPayload(askEntries, hookSessionId) {
|
|
110
157
|
return {
|
|
111
158
|
type: "tool_approval",
|
|
159
|
+
...hookSessionId == null || hookSessionId === "" ? {} : { hook_session_id: hookSessionId },
|
|
112
160
|
action_requests: askEntries.map(({ entry, reason }) => {
|
|
113
161
|
const request = {
|
|
114
162
|
tool_call_id: entry.call.id,
|
|
@@ -287,6 +335,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
287
335
|
* the Run ends.
|
|
288
336
|
*/
|
|
289
337
|
directPathTurns = /* @__PURE__ */ new Map();
|
|
338
|
+
/** Terminal results from interrupting siblings that must survive a
|
|
339
|
+
* LangGraph replay of the containing ToolNode. Includes the sidecar data
|
|
340
|
+
* the fresh batch needs for hook-context injection and completion events. */
|
|
341
|
+
settledInterruptingResults = /* @__PURE__ */ new Map();
|
|
290
342
|
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
291
343
|
toolRegistry;
|
|
292
344
|
/** Cached programmatic tools (computed once on first PTC call) */
|
|
@@ -520,16 +572,16 @@ var ToolNode = class extends RunnableCallable {
|
|
|
520
572
|
return `Tool "${callName}" not found. Did you mean "${suggestion}"? Handoff tool names must match exactly.`;
|
|
521
573
|
}
|
|
522
574
|
/**
|
|
523
|
-
* Flush
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* call IDs (Codex P2 #33). Hosts can also call this directly if
|
|
575
|
+
* Flush per-Run direct replay state. Called by the Graph at end-of-Run via
|
|
576
|
+
* `clearHeavyState`. The state intentionally survives `run()` re-entry so
|
|
577
|
+
* interrupt + resume keeps both original turn slots and terminal sibling
|
|
578
|
+
* outputs, but it would otherwise grow linearly and could collide across
|
|
579
|
+
* Runs if a provider reused call IDs. Hosts can also call this directly if
|
|
529
580
|
* they reuse a ToolNode across batches outside of a Graph.
|
|
530
581
|
*/
|
|
531
582
|
clearDirectPathTurns() {
|
|
532
583
|
this.directPathTurns.clear();
|
|
584
|
+
this.settledInterruptingResults.clear();
|
|
533
585
|
}
|
|
534
586
|
/**
|
|
535
587
|
* Returns cached programmatic tools, computing once on first access.
|
|
@@ -559,6 +611,29 @@ var ToolNode = class extends RunnableCallable {
|
|
|
559
611
|
getToolUsageCounts() {
|
|
560
612
|
return new Map(this.toolUsageCount);
|
|
561
613
|
}
|
|
614
|
+
createSubagentResumeState() {
|
|
615
|
+
return {
|
|
616
|
+
stateKey: JSON.stringify([
|
|
617
|
+
this.executingAgentId ?? "",
|
|
618
|
+
this.agentId ?? "",
|
|
619
|
+
this.name
|
|
620
|
+
]),
|
|
621
|
+
toolUsageCounts: [...this.toolUsageCount].map(([toolName, count]) => ({
|
|
622
|
+
toolName,
|
|
623
|
+
count
|
|
624
|
+
})),
|
|
625
|
+
directPathTurns: [...this.directPathTurns].map(([toolCallId, turn]) => ({
|
|
626
|
+
toolCallId,
|
|
627
|
+
turn
|
|
628
|
+
}))
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
restoreSubagentResumeState(state) {
|
|
632
|
+
this.toolUsageCount.clear();
|
|
633
|
+
for (const { toolName, count } of state.toolUsageCounts) this.toolUsageCount.set(toolName, count);
|
|
634
|
+
this.directPathTurns.clear();
|
|
635
|
+
for (const { toolCallId, turn } of state.directPathTurns) this.directPathTurns.set(toolCallId, turn);
|
|
636
|
+
}
|
|
562
637
|
recordToolUsageTurn(toolName, turn, callId) {
|
|
563
638
|
this.toolUsageCount.set(toolName, Math.max(this.toolUsageCount.get(toolName) ?? 0, turn + 1));
|
|
564
639
|
if (callId != null && callId !== "") this.toolCallTurns.set(callId, turn);
|
|
@@ -860,9 +935,9 @@ var ToolNode = class extends RunnableCallable {
|
|
|
860
935
|
* `blockDirectCall`, `respond` returns the host-supplied
|
|
861
936
|
* `responseText` as a synthetic success ToolMessage,
|
|
862
937
|
* `edit` re-runs with edited args. LangGraph re-enters
|
|
863
|
-
* ToolNode.run from the start on resume
|
|
864
|
-
* again
|
|
865
|
-
*
|
|
938
|
+
* ToolNode.run from the start on resume. Reusable hooks fire
|
|
939
|
+
* again; a consumed one-shot hook replays its pending approval
|
|
940
|
+
* result. In both cases `interrupt()` consumes the resume value.
|
|
866
941
|
* • When HITL is off: collapses to a fail-closed deny (matches
|
|
867
942
|
* the rest of the SDK's HITL-disabled default). One-time
|
|
868
943
|
* warning logged so hosts notice the gap.
|
|
@@ -882,13 +957,43 @@ var ToolNode = class extends RunnableCallable {
|
|
|
882
957
|
* scopes and is left to a follow-up.
|
|
883
958
|
*/
|
|
884
959
|
async runDirectToolWithLifecycleHooks(call, config, batchContext = {}) {
|
|
960
|
+
const replayController = this.toolMap.get(call.name)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
961
|
+
const replayConfig = replayController == null ? config : withSubagentReplayBatch(config, batchContext.replayBatchKey);
|
|
962
|
+
const settledOutput = await replayController?.getSettledOutput(call, replayConfig);
|
|
963
|
+
if (settledOutput != null) {
|
|
964
|
+
if (batchContext.additionalContextsSink != null && settledOutput.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...settledOutput.additionalContexts);
|
|
965
|
+
if (batchContext.resolvedArgsByCallId != null && call.id != null && settledOutput.resolvedArgs != null) batchContext.resolvedArgsByCallId.set(call.id, settledOutput.resolvedArgs);
|
|
966
|
+
const refMeta = settledOutput.output.additional_kwargs;
|
|
967
|
+
if (this.toolOutputRegistry != null && refMeta?._refKey != null && settledOutput.referenceContent != null) this.toolOutputRegistry.set(refMeta._refScope, refMeta._refKey, settledOutput.referenceContent);
|
|
968
|
+
return settledOutput.output;
|
|
969
|
+
}
|
|
970
|
+
const replayAdditionalContexts = [];
|
|
971
|
+
const persistOutput = async (output, terminalArgs) => {
|
|
972
|
+
const refMeta = output.additional_kwargs;
|
|
973
|
+
if (terminalArgs != null && call.id != null && batchContext.resolvedArgsByCallId != null) batchContext.resolvedArgsByCallId.set(call.id, terminalArgs);
|
|
974
|
+
const resolvedArgs = terminalArgs ?? (call.id == null ? void 0 : batchContext.resolvedArgsByCallId?.get(call.id));
|
|
975
|
+
const referenceContent = this.toolOutputRegistry == null || refMeta?._refKey == null ? void 0 : this.toolOutputRegistry.get(refMeta._refScope, refMeta._refKey);
|
|
976
|
+
await replayController?.persistSettledOutput(call, replayConfig, {
|
|
977
|
+
output,
|
|
978
|
+
additionalContexts: replayAdditionalContexts,
|
|
979
|
+
...resolvedArgs == null ? {} : { resolvedArgs },
|
|
980
|
+
...referenceContent == null ? {} : { referenceContent }
|
|
981
|
+
});
|
|
982
|
+
return output;
|
|
983
|
+
};
|
|
885
984
|
const runId = config.configurable?.run_id ?? "";
|
|
985
|
+
const threadId = config.configurable?.thread_id;
|
|
886
986
|
const hookRegistry = this.hookRegistry;
|
|
887
987
|
const hasPreHook = hookRegistry?.hasHookFor("PreToolUse", runId) === true;
|
|
988
|
+
const approvalReplayKey = call.id == null || call.id === "" ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", call.id);
|
|
989
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
|
|
990
|
+
const pendingApproval = approvalReplayKey == null ? void 0 : hookRegistry?.getPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
|
|
888
991
|
const hasPostHook = hookRegistry?.hasHookFor("PostToolUse", runId) === true;
|
|
889
992
|
const hasFailureHook = hookRegistry?.hasHookFor("PostToolUseFailure", runId) === true;
|
|
890
|
-
if (hookRegistry == null || !hasPreHook && !hasPostHook && !hasFailureHook)
|
|
891
|
-
|
|
993
|
+
if (hookRegistry == null || !hasPreHook && pendingApproval == null && !hasPostHook && !hasFailureHook) {
|
|
994
|
+
const output = await this.runTool(call, replayConfig, batchContext);
|
|
995
|
+
return output instanceof ToolMessage ? persistOutput(output) : output;
|
|
996
|
+
}
|
|
892
997
|
const registryRunId = batchContext.batchScopeId ?? config.configurable?.run_id;
|
|
893
998
|
let usageCount;
|
|
894
999
|
const cachedTurn = call.id != null && call.id !== "" ? this.directPathTurns.get(call.id) ?? this.toolCallTurns.get(call.id) : void 0;
|
|
@@ -913,7 +1018,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
913
1018
|
resolvedArgs = resolved;
|
|
914
1019
|
}
|
|
915
1020
|
let effectiveCall = call;
|
|
916
|
-
if (hasPreHook) {
|
|
1021
|
+
if (hasPreHook || pendingApproval != null) {
|
|
917
1022
|
const preResult = await executeHooks({
|
|
918
1023
|
registry: hookRegistry,
|
|
919
1024
|
input: {
|
|
@@ -929,34 +1034,48 @@ var ToolNode = class extends RunnableCallable {
|
|
|
929
1034
|
turn
|
|
930
1035
|
},
|
|
931
1036
|
sessionId: runId,
|
|
932
|
-
matchQuery: call.name
|
|
1037
|
+
matchQuery: call.name,
|
|
1038
|
+
onceReplayKey: approvalReplayKey,
|
|
1039
|
+
onceReplaySessionId: approvalReplaySessionId
|
|
933
1040
|
}).catch(() => void 0);
|
|
934
1041
|
if (preResult != null) {
|
|
935
|
-
if (
|
|
1042
|
+
if (preResult.additionalContexts.length > 0) {
|
|
1043
|
+
replayAdditionalContexts.push(...preResult.additionalContexts);
|
|
1044
|
+
batchContext.additionalContextsSink?.push(...preResult.additionalContexts);
|
|
1045
|
+
}
|
|
936
1046
|
if (preResult.updatedInput != null) effectiveCall = {
|
|
937
1047
|
...call,
|
|
938
1048
|
args: preResult.updatedInput
|
|
939
1049
|
};
|
|
940
|
-
if (preResult.decision === "deny") return this.blockDirectCall({
|
|
1050
|
+
if (preResult.decision === "deny") return persistOutput(this.blockDirectCall({
|
|
941
1051
|
call,
|
|
942
1052
|
resolvedArgs,
|
|
943
1053
|
reason: preResult.reason ?? "Blocked by hook",
|
|
944
1054
|
hookRegistry,
|
|
945
1055
|
runId,
|
|
946
1056
|
threadId
|
|
947
|
-
});
|
|
1057
|
+
}), effectiveCall.args);
|
|
948
1058
|
if (preResult.decision === "ask") {
|
|
949
1059
|
if (this.humanInTheLoop?.enabled !== true) {
|
|
950
1060
|
const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
|
|
951
|
-
return this.blockDirectCall({
|
|
1061
|
+
return persistOutput(this.blockDirectCall({
|
|
952
1062
|
call,
|
|
953
1063
|
resolvedArgs,
|
|
954
1064
|
reason,
|
|
955
1065
|
hookRegistry,
|
|
956
1066
|
runId,
|
|
957
1067
|
threadId
|
|
958
|
-
});
|
|
1068
|
+
}), effectiveCall.args);
|
|
959
1069
|
}
|
|
1070
|
+
const toolCallId = call.id;
|
|
1071
|
+
if (toolCallId == null || toolCallId === "" || approvalReplayKey == null) return persistOutput(this.blockDirectCall({
|
|
1072
|
+
call,
|
|
1073
|
+
resolvedArgs,
|
|
1074
|
+
reason: "Tool approval requires a non-empty tool call ID — failing closed",
|
|
1075
|
+
hookRegistry,
|
|
1076
|
+
runId,
|
|
1077
|
+
threadId
|
|
1078
|
+
}), effectiveCall.args);
|
|
960
1079
|
const payload = buildToolApprovalInterruptPayload([{
|
|
961
1080
|
entry: {
|
|
962
1081
|
call: effectiveCall,
|
|
@@ -965,59 +1084,71 @@ var ToolNode = class extends RunnableCallable {
|
|
|
965
1084
|
},
|
|
966
1085
|
reason: preResult.reason,
|
|
967
1086
|
allowedDecisions: preResult.allowedDecisions
|
|
968
|
-
}]);
|
|
1087
|
+
}], runId);
|
|
969
1088
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
|
|
970
|
-
|
|
1089
|
+
hookRegistry.clearPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
|
|
1090
|
+
const decision = normalizeApprovalDecisions([toolCallId], resumeValue).get(toolCallId) ?? {
|
|
971
1091
|
type: "reject",
|
|
972
1092
|
reason: "No decision provided for tool approval"
|
|
973
1093
|
};
|
|
974
1094
|
const declaredType = decision.type;
|
|
975
|
-
if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return this.blockDirectCall({
|
|
1095
|
+
if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return persistOutput(this.blockDirectCall({
|
|
976
1096
|
call,
|
|
977
1097
|
resolvedArgs,
|
|
978
1098
|
reason: `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${preResult.allowedDecisions.join(", ")}] — failing closed`,
|
|
979
1099
|
hookRegistry,
|
|
980
1100
|
runId,
|
|
981
1101
|
threadId
|
|
982
|
-
});
|
|
983
|
-
if (decision.type === "reject") return this.blockDirectCall({
|
|
1102
|
+
}), effectiveCall.args);
|
|
1103
|
+
if (decision.type === "reject") return persistOutput(this.blockDirectCall({
|
|
984
1104
|
call,
|
|
985
1105
|
resolvedArgs,
|
|
986
1106
|
reason: decision.reason ?? preResult.reason ?? "Rejected by user",
|
|
987
1107
|
hookRegistry,
|
|
988
1108
|
runId,
|
|
989
1109
|
threadId
|
|
990
|
-
});
|
|
1110
|
+
}), effectiveCall.args);
|
|
991
1111
|
if (decision.type === "respond") {
|
|
992
1112
|
const responseText = decision.responseText;
|
|
993
|
-
if (typeof responseText !== "string") return this.blockDirectCall({
|
|
1113
|
+
if (typeof responseText !== "string") return persistOutput(this.blockDirectCall({
|
|
994
1114
|
call,
|
|
995
1115
|
resolvedArgs,
|
|
996
1116
|
reason: "Approval payload `respond` was missing a string `responseText`",
|
|
997
1117
|
hookRegistry,
|
|
998
1118
|
runId,
|
|
999
1119
|
threadId
|
|
1000
|
-
});
|
|
1001
|
-
return new ToolMessage({
|
|
1120
|
+
}), effectiveCall.args);
|
|
1121
|
+
return persistOutput(new ToolMessage({
|
|
1002
1122
|
status: "success",
|
|
1003
|
-
content: responseText,
|
|
1123
|
+
content: truncateToolResultContent(responseText, this.maxToolResultChars),
|
|
1004
1124
|
name: call.name,
|
|
1005
1125
|
tool_call_id: call.id ?? ""
|
|
1006
|
-
});
|
|
1126
|
+
}), effectiveCall.args);
|
|
1007
1127
|
}
|
|
1008
1128
|
if (decision.type === "edit") {
|
|
1009
1129
|
const updatedInput = decision.updatedInput;
|
|
1010
|
-
if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return new ToolMessage({
|
|
1130
|
+
if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return persistOutput(new ToolMessage({
|
|
1011
1131
|
status: "error",
|
|
1012
1132
|
content: "Decision \"edit\" missing object updatedInput — failing closed.",
|
|
1013
1133
|
name: call.name,
|
|
1014
1134
|
tool_call_id: call.id ?? ""
|
|
1015
|
-
});
|
|
1135
|
+
}), effectiveCall.args);
|
|
1016
1136
|
effectiveCall = {
|
|
1017
1137
|
...call,
|
|
1018
1138
|
args: updatedInput
|
|
1019
1139
|
};
|
|
1020
1140
|
}
|
|
1141
|
+
if (declaredType !== "approve" && declaredType !== "edit") {
|
|
1142
|
+
const unknownType = typeof declaredType === "string" ? declaredType : "<missing>";
|
|
1143
|
+
return persistOutput(this.blockDirectCall({
|
|
1144
|
+
call,
|
|
1145
|
+
resolvedArgs,
|
|
1146
|
+
reason: `Unknown approval decision type "${unknownType}" — failing closed`,
|
|
1147
|
+
hookRegistry,
|
|
1148
|
+
runId,
|
|
1149
|
+
threadId
|
|
1150
|
+
}), effectiveCall.args);
|
|
1151
|
+
}
|
|
1021
1152
|
}
|
|
1022
1153
|
}
|
|
1023
1154
|
}
|
|
@@ -1031,7 +1162,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1031
1162
|
* which is strictly more accurate.
|
|
1032
1163
|
*/
|
|
1033
1164
|
if (effectiveCall !== call && batchContext.resolvedArgsByCallId != null && call.id != null && call.id !== "") batchContext.resolvedArgsByCallId.set(call.id, effectiveCall.args);
|
|
1034
|
-
const output = await this.runTool(effectiveCall,
|
|
1165
|
+
const output = await this.runTool(effectiveCall, replayConfig, {
|
|
1035
1166
|
...batchContext,
|
|
1036
1167
|
usageCount
|
|
1037
1168
|
});
|
|
@@ -1055,8 +1186,11 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1055
1186
|
sessionId: runId,
|
|
1056
1187
|
matchQuery: call.name
|
|
1057
1188
|
}).catch(() => void 0);
|
|
1058
|
-
if (failureResult != null &&
|
|
1059
|
-
|
|
1189
|
+
if (failureResult != null && failureResult.additionalContexts.length > 0) {
|
|
1190
|
+
replayAdditionalContexts.push(...failureResult.additionalContexts);
|
|
1191
|
+
batchContext.additionalContextsSink?.push(...failureResult.additionalContexts);
|
|
1192
|
+
}
|
|
1193
|
+
return persistOutput(output);
|
|
1060
1194
|
}
|
|
1061
1195
|
if (output.status !== "error" && hasPostHook) {
|
|
1062
1196
|
const postResult = await executeHooks({
|
|
@@ -1077,21 +1211,24 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1077
1211
|
sessionId: runId,
|
|
1078
1212
|
matchQuery: call.name
|
|
1079
1213
|
}).catch(() => void 0);
|
|
1080
|
-
if (postResult != null &&
|
|
1214
|
+
if (postResult != null && postResult.additionalContexts.length > 0) {
|
|
1215
|
+
replayAdditionalContexts.push(...postResult.additionalContexts);
|
|
1216
|
+
batchContext.additionalContextsSink?.push(...postResult.additionalContexts);
|
|
1217
|
+
}
|
|
1081
1218
|
if (postResult?.updatedOutput != null) {
|
|
1082
1219
|
if (hasComputerCallOutputMarker(output)) {
|
|
1083
1220
|
if (!isComputerCallOutputContent(postResult.updatedOutput)) throw new Error("PostToolUse updatedOutput for a computer call must be a valid screenshot URL or screenshot content block.");
|
|
1084
|
-
return cloneToolMessageWithContent(output, postResult.updatedOutput);
|
|
1221
|
+
return persistOutput(cloneToolMessageWithContent(output, postResult.updatedOutput));
|
|
1085
1222
|
}
|
|
1086
1223
|
const refMeta = output.additional_kwargs;
|
|
1087
1224
|
const refKey = refMeta?._refKey;
|
|
1088
1225
|
const refScope = refMeta?._refScope;
|
|
1089
1226
|
const replaced = serializeToolOutputWithinLimits(postResult.updatedOutput, this.maxToolResultChars, this.toolOutputRegistry != null && refKey != null ? this.toolOutputRegistry.perOutputLimit : 0);
|
|
1090
1227
|
if (this.toolOutputRegistry != null && refKey != null) this.toolOutputRegistry.set(refScope, refKey, replaced.registryContent);
|
|
1091
|
-
return cloneToolMessageWithContent(output, replaced.content);
|
|
1228
|
+
return persistOutput(cloneToolMessageWithContent(output, replaced.content));
|
|
1092
1229
|
}
|
|
1093
1230
|
}
|
|
1094
|
-
return output;
|
|
1231
|
+
return persistOutput(output);
|
|
1095
1232
|
}
|
|
1096
1233
|
/**
|
|
1097
1234
|
* `ask` decisions on direct-path tools collapse to fail-closed deny
|
|
@@ -1382,13 +1519,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1382
1519
|
injectedMessages: [],
|
|
1383
1520
|
errors: []
|
|
1384
1521
|
});
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
* to defensively `?.` it across every reference inside.
|
|
1390
|
-
*/
|
|
1391
|
-
const hookRegistry = this.hookRegistry;
|
|
1522
|
+
const hookRegistry = this.hookRegistry;
|
|
1523
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
|
|
1524
|
+
const hasPendingApproval = preToolCalls.some((entry) => entry.call.id != null && hookRegistry?.getPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id)) != null);
|
|
1525
|
+
if (hookRegistry != null && (hookRegistry.hasHookFor("PreToolUse", runId) || hasPendingApproval)) {
|
|
1392
1526
|
/**
|
|
1393
1527
|
* Pull each call's prestarted eager record BEFORE awaiting the hooks:
|
|
1394
1528
|
* an async deny must never race the eager host promise into emitting a
|
|
@@ -1408,23 +1542,29 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1408
1542
|
this.eagerEventToolExecutions.delete(callId);
|
|
1409
1543
|
}
|
|
1410
1544
|
}
|
|
1411
|
-
const preResults = await Promise.all(preToolCalls.map((entry) =>
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1545
|
+
const preResults = await Promise.all(preToolCalls.map((entry) => {
|
|
1546
|
+
const toolUseId = entry.call.id;
|
|
1547
|
+
const approvalReplayKey = toolUseId == null ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", toolUseId);
|
|
1548
|
+
return executeHooks({
|
|
1549
|
+
registry: hookRegistry,
|
|
1550
|
+
input: {
|
|
1551
|
+
hook_event_name: "PreToolUse",
|
|
1552
|
+
runId,
|
|
1553
|
+
threadId,
|
|
1554
|
+
agentId: this.agentId,
|
|
1555
|
+
executingAgentId: this.executingAgentId,
|
|
1556
|
+
toolName: entry.call.name,
|
|
1557
|
+
toolInput: entry.args,
|
|
1558
|
+
toolUseId: entry.call.id,
|
|
1559
|
+
stepId: entry.stepId,
|
|
1560
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0
|
|
1561
|
+
},
|
|
1562
|
+
sessionId: runId,
|
|
1563
|
+
matchQuery: entry.call.name,
|
|
1564
|
+
onceReplayKey: approvalReplayKey,
|
|
1565
|
+
onceReplaySessionId: approvalReplaySessionId
|
|
1566
|
+
}).catch(() => HOOK_FALLBACK);
|
|
1567
|
+
}));
|
|
1428
1568
|
/**
|
|
1429
1569
|
* Side effects deferred from `blockEntry` until after any pending
|
|
1430
1570
|
* `interrupt()` resolves. Without deferral, a batch that mixes a
|
|
@@ -1581,12 +1721,11 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1581
1721
|
* pauses, gathers human input, and resumes the run with one
|
|
1582
1722
|
* decision per request. On resume LangGraph re-executes this node
|
|
1583
1723
|
* from the start; `interrupt()` then returns the resume value
|
|
1584
|
-
* instead of throwing
|
|
1585
|
-
*
|
|
1586
|
-
* are pure — see `humanInTheLoop` docs).
|
|
1724
|
+
* instead of throwing. Reusable hooks rebuild their entries, while
|
|
1725
|
+
* consumed one-shot hooks replay their pending approval aggregate.
|
|
1587
1726
|
*/
|
|
1588
1727
|
if (askEntries.length > 0) {
|
|
1589
|
-
const payload = buildToolApprovalInterruptPayload(askEntries);
|
|
1728
|
+
const payload = buildToolApprovalInterruptPayload(askEntries, runId);
|
|
1590
1729
|
/**
|
|
1591
1730
|
* `interrupt()` reads the current `RunnableConfig` from
|
|
1592
1731
|
* AsyncLocalStorage. ToolNode usually runs with tracing disabled
|
|
@@ -1597,6 +1736,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1597
1736
|
* suspend and resume.
|
|
1598
1737
|
*/
|
|
1599
1738
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
|
|
1739
|
+
for (const { entry } of askEntries) hookRegistry.clearPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id));
|
|
1600
1740
|
const decisionByCallId = normalizeApprovalDecisions(askEntries.map(({ entry }) => entry.call.id), resumeValue);
|
|
1601
1741
|
for (const { entry, reason: askReason, allowedDecisions } of askEntries) {
|
|
1602
1742
|
const decision = decisionByCallId.get(entry.call.id) ?? {
|
|
@@ -2187,16 +2327,16 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2187
2327
|
* call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
|
|
2188
2328
|
* whole ToolNode *before* any non-interrupting sibling has started — so a
|
|
2189
2329
|
* sibling with real side effects (send_email, billing) never executes on
|
|
2190
|
-
* the first pass.
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2330
|
+
* the first pass. Terminal interrupting siblings are cached by call id, so
|
|
2331
|
+
* LangGraph replay reuses their complete lifecycle output instead of
|
|
2332
|
+
* repeating model calls, hooks, or side effects.
|
|
2193
2333
|
*
|
|
2194
2334
|
* Without this ordering, a flat `Promise.all` starts every sibling
|
|
2195
2335
|
* concurrently, so a non-idempotent sibling can complete its side effect
|
|
2196
2336
|
* before the interrupt unwinds and then run a SECOND time on resume — the
|
|
2197
|
-
* duplicate side effect this method exists to prevent.
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2337
|
+
* duplicate side effect this method exists to prevent. A tool that actually
|
|
2338
|
+
* suspends re-enters until it reaches a terminal result; siblings that
|
|
2339
|
+
* already settled do not re-enter.
|
|
2200
2340
|
*
|
|
2201
2341
|
* `batchIndices[i]` is `directCalls[i]`'s position within the parent
|
|
2202
2342
|
* ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
|
|
@@ -2205,28 +2345,95 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2205
2345
|
* per-call here.
|
|
2206
2346
|
*/
|
|
2207
2347
|
async runDirectBatchInterruptSafe(directCalls, batchIndices, config, baseContext) {
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2348
|
+
const settledBatchResults = baseContext.replayBatchKey == null ? void 0 : this.settledInterruptingResults.get(baseContext.replayBatchKey);
|
|
2349
|
+
const restoreResult = (call, result) => {
|
|
2350
|
+
baseContext.additionalContextsSink?.push(...result.additionalContexts);
|
|
2351
|
+
if (call.id != null && result.resolvedArgs != null && baseContext.resolvedArgsByCallId != null) baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
|
|
2352
|
+
return result;
|
|
2353
|
+
};
|
|
2354
|
+
const runOne = async (call, position) => {
|
|
2355
|
+
const cachedResult = typeof call.id === "string" ? settledBatchResults?.get(call.id) : void 0;
|
|
2356
|
+
if (cachedResult != null) return restoreResult(call, cachedResult);
|
|
2357
|
+
const additionalContexts = [];
|
|
2358
|
+
const output = await this.runDirectToolWithLifecycleHooks(call, config, {
|
|
2359
|
+
...baseContext,
|
|
2360
|
+
batchIndex: batchIndices[position],
|
|
2361
|
+
additionalContextsSink: additionalContexts
|
|
2362
|
+
});
|
|
2363
|
+
const resolvedArgs = call.id == null ? void 0 : baseContext.resolvedArgsByCallId?.get(call.id);
|
|
2364
|
+
return restoreResult(call, {
|
|
2365
|
+
output,
|
|
2366
|
+
additionalContexts,
|
|
2367
|
+
...resolvedArgs == null ? {} : { resolvedArgs }
|
|
2368
|
+
});
|
|
2369
|
+
};
|
|
2212
2370
|
const interrupting = this.interruptingToolNames;
|
|
2213
|
-
if (!(interrupting != null && directCalls.some((call) => interrupting.has(call.name)))) return Promise.all(directCalls.map((call, i) => runOne(call, i)));
|
|
2371
|
+
if (!(interrupting != null && directCalls.some((call) => interrupting.has(call.name)))) return (await Promise.all(directCalls.map((call, i) => runOne(call, i)))).map((result) => result.output);
|
|
2214
2372
|
const outputs = new Array(directCalls.length);
|
|
2215
2373
|
const interruptingPositions = [];
|
|
2216
2374
|
const regularPositions = [];
|
|
2217
2375
|
for (let i = 0; i < directCalls.length; i++) if (interrupting.has(directCalls[i].name)) interruptingPositions.push(i);
|
|
2218
2376
|
else regularPositions.push(i);
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2377
|
+
const interruptingResults = await Promise.allSettled(interruptingPositions.map((i) => runOne(directCalls[i], i)));
|
|
2378
|
+
/** A sibling may have tripped the run-wide breaker while another raised
|
|
2379
|
+
* a GraphInterrupt. Safety failures take precedence over approval pauses
|
|
2380
|
+
* regardless of input order. */
|
|
2381
|
+
this.throwIfBreakerTripped(config);
|
|
2382
|
+
let hasInterruptingError = false;
|
|
2383
|
+
let interruptingError;
|
|
2384
|
+
for (let i = 0; i < interruptingResults.length; i++) {
|
|
2385
|
+
const result = interruptingResults[i];
|
|
2386
|
+
if (result.status === "rejected") {
|
|
2387
|
+
if (!hasInterruptingError) {
|
|
2388
|
+
hasInterruptingError = true;
|
|
2389
|
+
interruptingError = result.reason;
|
|
2390
|
+
}
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
const position = interruptingPositions[i];
|
|
2394
|
+
outputs[position] = result.value.output;
|
|
2395
|
+
const callId = directCalls[position].id;
|
|
2396
|
+
if (baseContext.replayBatchKey != null && typeof callId === "string" && callId !== "") {
|
|
2397
|
+
const batchResults = this.settledInterruptingResults.get(baseContext.replayBatchKey) ?? /* @__PURE__ */ new Map();
|
|
2398
|
+
batchResults.set(callId, result.value);
|
|
2399
|
+
this.settledInterruptingResults.set(baseContext.replayBatchKey, batchResults);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (hasInterruptingError) {
|
|
2403
|
+
if (isGraphInterrupt(interruptingError)) {
|
|
2404
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
2405
|
+
const parentToolCallIds = /* @__PURE__ */ new Set();
|
|
2406
|
+
for (const call of directCalls) {
|
|
2407
|
+
const controller = this.toolMap.get(call.name)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
2408
|
+
if (controller?.getResumeManifest != null) controllers.add(controller);
|
|
2409
|
+
if (call.id != null && call.id !== "") parentToolCallIds.add(call.id);
|
|
2410
|
+
}
|
|
2411
|
+
const executionsByParentCall = /* @__PURE__ */ new Map();
|
|
2412
|
+
for (const controller of controllers) {
|
|
2413
|
+
const manifest = await controller.getResumeManifest?.(parentToolCallIds, withSubagentReplayBatch(config, baseContext.replayBatchKey));
|
|
2414
|
+
if (manifest != null) for (const execution of manifest.executions) executionsByParentCall.set(execution.parentToolCallId, execution);
|
|
2415
|
+
}
|
|
2416
|
+
const executions = [...executionsByParentCall.values()];
|
|
2417
|
+
if (executions.length > 0) {
|
|
2418
|
+
const manifest = {
|
|
2419
|
+
version: 1,
|
|
2420
|
+
executions
|
|
2421
|
+
};
|
|
2422
|
+
throw new GraphInterrupt(interruptingError.interrupts.map((pendingInterrupt) => ({
|
|
2423
|
+
...pendingInterrupt,
|
|
2424
|
+
value: attachSubagentResumeManifest(pendingInterrupt.value, manifest)
|
|
2425
|
+
})));
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
throw interruptingError;
|
|
2429
|
+
}
|
|
2223
2430
|
/** The breaker can trip while the interrupting group is awaited — e.g.
|
|
2224
2431
|
* a tool that ignores cancellation and completes normally. Recheck
|
|
2225
2432
|
* before starting the non-idempotent siblings. */
|
|
2226
2433
|
this.throwIfBreakerTripped(config);
|
|
2227
2434
|
const regularOutputs = await Promise.all(regularPositions.map((i) => runOne(directCalls[i], i)));
|
|
2228
2435
|
regularPositions.forEach((i, k) => {
|
|
2229
|
-
outputs[i] = regularOutputs[k];
|
|
2436
|
+
outputs[i] = regularOutputs[k].output;
|
|
2230
2437
|
});
|
|
2231
2438
|
return outputs;
|
|
2232
2439
|
}
|
|
@@ -2282,9 +2489,23 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2282
2489
|
const resolvedArgsByCallId = /* @__PURE__ */ new Map();
|
|
2283
2490
|
/** Per-invocation error-completion ownership — see `ToolErrorOwnership`. */
|
|
2284
2491
|
const errorOwnership = createToolErrorOwnership();
|
|
2285
|
-
|
|
2492
|
+
/**
|
|
2493
|
+
* Claim this batch's turn synchronously from the registry (or
|
|
2494
|
+
* fall back to 0 when the feature is disabled). The registry is
|
|
2495
|
+
* partitioned by scope id so overlapping batches cannot
|
|
2496
|
+
* overwrite each other's state even under a shared registry.
|
|
2497
|
+
*
|
|
2498
|
+
* For anonymous callers (no `run_id` in config), mint a unique
|
|
2499
|
+
* per-batch scope id so two concurrent anonymous invocations
|
|
2500
|
+
* don't target the same bucket. The scope is threaded down to
|
|
2501
|
+
* every subsequent registry call on this batch.
|
|
2502
|
+
*/
|
|
2503
|
+
const incomingRunId = config.configurable?.run_id;
|
|
2504
|
+
const incomingThreadId = config.configurable?.thread_id;
|
|
2505
|
+
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
2286
2506
|
const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
2287
2507
|
let outputs;
|
|
2508
|
+
let replayBatchKey;
|
|
2288
2509
|
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
2289
2510
|
* the promotion into handoff updates (same-id state copies there would
|
|
2290
2511
|
* otherwise overwrite the replacement message). */
|
|
@@ -2299,6 +2520,9 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2299
2520
|
});
|
|
2300
2521
|
const directAdditionalContexts = [];
|
|
2301
2522
|
const { lg_tool_call: _sendToolCall, ...sendState } = input;
|
|
2523
|
+
const sendMessages = sendState.messages;
|
|
2524
|
+
const sendAssistantBatch = Array.isArray(sendMessages) ? findAssistantBatch(sendMessages) : void 0;
|
|
2525
|
+
const sendReplayBatchKey = sendAssistantBatch == null ? void 0 : getAssistantBatchReplayKey(sendAssistantBatch, incomingRunId, incomingThreadId);
|
|
2302
2526
|
const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
|
|
2303
2527
|
batchIndex: 0,
|
|
2304
2528
|
turn,
|
|
@@ -2306,6 +2530,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2306
2530
|
resolvedArgsByCallId,
|
|
2307
2531
|
errorOwnership,
|
|
2308
2532
|
additionalContextsSink: directAdditionalContexts,
|
|
2533
|
+
replayBatchKey: sendReplayBatchKey,
|
|
2309
2534
|
runInput: sendState
|
|
2310
2535
|
});
|
|
2311
2536
|
outputs = directAdditionalContexts.length > 0 ? [sendOutput, new HumanMessage({
|
|
@@ -2322,15 +2547,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2322
2547
|
else if (this.isMessagesState(input)) messages = input.messages;
|
|
2323
2548
|
else throw new Error("ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input.");
|
|
2324
2549
|
const toolMessageIds = new Set(messages.filter((msg) => msg._getType() === "tool").map((msg) => msg.tool_call_id));
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
aiMessage = message;
|
|
2330
|
-
break;
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
if (aiMessage == null || !isAIMessage(aiMessage)) throw new Error("ToolNode only accepts AIMessages as input.");
|
|
2550
|
+
const assistantBatch = findAssistantBatch(messages);
|
|
2551
|
+
if (assistantBatch == null) throw new Error("ToolNode only accepts AIMessages as input.");
|
|
2552
|
+
const aiMessage = assistantBatch.message;
|
|
2553
|
+
replayBatchKey = getAssistantBatchReplayKey(assistantBatch, incomingRunId, incomingThreadId);
|
|
2334
2554
|
if (this.loadRuntimeTools) {
|
|
2335
2555
|
const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
|
|
2336
2556
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
@@ -2445,6 +2665,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2445
2665
|
errorOwnership,
|
|
2446
2666
|
preBatchSnapshot,
|
|
2447
2667
|
additionalContextsSink: directAdditionalContexts,
|
|
2668
|
+
replayBatchKey,
|
|
2448
2669
|
runInput: input
|
|
2449
2670
|
}) : [];
|
|
2450
2671
|
if (directCalls.length > 0 && directOutputs.length > 0) await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId, errorOwnership);
|
|
@@ -2483,6 +2704,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2483
2704
|
errorOwnership,
|
|
2484
2705
|
preBatchSnapshot,
|
|
2485
2706
|
additionalContextsSink: directAdditionalContexts,
|
|
2707
|
+
replayBatchKey,
|
|
2486
2708
|
runInput: input
|
|
2487
2709
|
});
|
|
2488
2710
|
await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId, errorOwnership);
|
|
@@ -2519,7 +2741,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2519
2741
|
await this.dispatchStepCompleted(result.tool_call_id, result.name ?? "unknown", {}, typeof result.content === "string" ? result.content : "", config);
|
|
2520
2742
|
}
|
|
2521
2743
|
}
|
|
2522
|
-
if (!outputs.some(isCommand))
|
|
2744
|
+
if (!outputs.some(isCommand)) {
|
|
2745
|
+
if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
|
|
2746
|
+
return Array.isArray(input) ? outputs : { messages: outputs };
|
|
2747
|
+
}
|
|
2523
2748
|
/**
|
|
2524
2749
|
* Carry the invalid-call promotion into handoff commands. A handoff
|
|
2525
2750
|
* tool's Command snapshots `update.messages` from the PRE-promotion
|
|
@@ -2606,6 +2831,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2606
2831
|
/** Single handoff - pass through as-is */
|
|
2607
2832
|
combinedOutputs.push(handoffCommands[0]);
|
|
2608
2833
|
if (parentCommand) combinedOutputs.push(parentCommand);
|
|
2834
|
+
if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
|
|
2609
2835
|
return combinedOutputs;
|
|
2610
2836
|
}
|
|
2611
2837
|
isSendInput(input) {
|