@librechat/agents 3.4.0 → 3.4.1
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/graphs/Graph.cjs +111 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +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 +14 -13
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +82 -15
- 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/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/ToolNode.cjs +316 -94
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +630 -78
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
- package/dist/cjs/tools/subagent/SubagentReplay.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/graphs/Graph.mjs +111 -27
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +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 +11 -11
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +83 -16
- 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/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/ToolNode.mjs +317 -95
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +631 -79
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
- package/dist/esm/tools/subagent/SubagentReplay.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/graphs/Graph.d.ts +16 -1
- 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 +4 -1
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +46 -7
- package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +5 -5
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +6 -2
- package/src/graphs/Graph.ts +246 -52
- 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/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 +164 -18
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/ToolNode.ts +631 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +542 -6
- package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/subagent/SubagentExecutor.ts +1369 -131
- package/src/tools/subagent/SubagentReplay.ts +575 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +5 -5
- 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";
|
|
@@ -14,12 +15,13 @@ import { RunnableCallable } from "../utils/run.mjs";
|
|
|
14
15
|
import "../utils/index.mjs";
|
|
15
16
|
import { stripCodeSessionFileSummary } from "./CodeSessionFileSummary.mjs";
|
|
16
17
|
import { executeHooks } from "../hooks/executeHooks.mjs";
|
|
18
|
+
import { TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from "../hooks/types.mjs";
|
|
17
19
|
import "../hooks/index.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,22 @@ 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
|
+
}
|
|
32
50
|
function createToolErrorOwnership() {
|
|
33
51
|
return {
|
|
34
52
|
handlerOwned: /* @__PURE__ */ new Set(),
|
|
@@ -99,6 +117,25 @@ function describeOfferedShape(value) {
|
|
|
99
117
|
if (Array.isArray(value)) return "array";
|
|
100
118
|
return typeof value;
|
|
101
119
|
}
|
|
120
|
+
function findAssistantBatch(messages) {
|
|
121
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
122
|
+
const message = messages[i];
|
|
123
|
+
if (isAIMessage(message)) return {
|
|
124
|
+
message,
|
|
125
|
+
index: i,
|
|
126
|
+
messageCount: messages.length
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function getAssistantBatchReplayKey(batch, runId, threadId) {
|
|
131
|
+
return JSON.stringify([
|
|
132
|
+
runId ?? null,
|
|
133
|
+
threadId ?? null,
|
|
134
|
+
batch.message.id ?? null,
|
|
135
|
+
batch.index,
|
|
136
|
+
batch.messageCount
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
102
139
|
/**
|
|
103
140
|
* Build the `tool_approval` interrupt payload from the set of pending
|
|
104
141
|
* `ask`-decision entries collected during PreToolUse hook handling.
|
|
@@ -106,9 +143,10 @@ function describeOfferedShape(value) {
|
|
|
106
143
|
* scope. The interrupt itself is raised by the caller (which still
|
|
107
144
|
* needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
|
|
108
145
|
*/
|
|
109
|
-
function buildToolApprovalInterruptPayload(askEntries) {
|
|
146
|
+
function buildToolApprovalInterruptPayload(askEntries, hookSessionId) {
|
|
110
147
|
return {
|
|
111
148
|
type: "tool_approval",
|
|
149
|
+
...hookSessionId == null || hookSessionId === "" ? {} : { hook_session_id: hookSessionId },
|
|
112
150
|
action_requests: askEntries.map(({ entry, reason }) => {
|
|
113
151
|
const request = {
|
|
114
152
|
tool_call_id: entry.call.id,
|
|
@@ -287,6 +325,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
287
325
|
* the Run ends.
|
|
288
326
|
*/
|
|
289
327
|
directPathTurns = /* @__PURE__ */ new Map();
|
|
328
|
+
/** Terminal results from interrupting siblings that must survive a
|
|
329
|
+
* LangGraph replay of the containing ToolNode. Includes the sidecar data
|
|
330
|
+
* the fresh batch needs for hook-context injection and completion events. */
|
|
331
|
+
settledInterruptingResults = /* @__PURE__ */ new Map();
|
|
290
332
|
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
291
333
|
toolRegistry;
|
|
292
334
|
/** Cached programmatic tools (computed once on first PTC call) */
|
|
@@ -520,16 +562,16 @@ var ToolNode = class extends RunnableCallable {
|
|
|
520
562
|
return `Tool "${callName}" not found. Did you mean "${suggestion}"? Handoff tool names must match exactly.`;
|
|
521
563
|
}
|
|
522
564
|
/**
|
|
523
|
-
* Flush
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* call IDs (Codex P2 #33). Hosts can also call this directly if
|
|
565
|
+
* Flush per-Run direct replay state. Called by the Graph at end-of-Run via
|
|
566
|
+
* `clearHeavyState`. The state intentionally survives `run()` re-entry so
|
|
567
|
+
* interrupt + resume keeps both original turn slots and terminal sibling
|
|
568
|
+
* outputs, but it would otherwise grow linearly and could collide across
|
|
569
|
+
* Runs if a provider reused call IDs. Hosts can also call this directly if
|
|
529
570
|
* they reuse a ToolNode across batches outside of a Graph.
|
|
530
571
|
*/
|
|
531
572
|
clearDirectPathTurns() {
|
|
532
573
|
this.directPathTurns.clear();
|
|
574
|
+
this.settledInterruptingResults.clear();
|
|
533
575
|
}
|
|
534
576
|
/**
|
|
535
577
|
* Returns cached programmatic tools, computing once on first access.
|
|
@@ -559,6 +601,29 @@ var ToolNode = class extends RunnableCallable {
|
|
|
559
601
|
getToolUsageCounts() {
|
|
560
602
|
return new Map(this.toolUsageCount);
|
|
561
603
|
}
|
|
604
|
+
createSubagentResumeState() {
|
|
605
|
+
return {
|
|
606
|
+
stateKey: JSON.stringify([
|
|
607
|
+
this.executingAgentId ?? "",
|
|
608
|
+
this.agentId ?? "",
|
|
609
|
+
this.name
|
|
610
|
+
]),
|
|
611
|
+
toolUsageCounts: [...this.toolUsageCount].map(([toolName, count]) => ({
|
|
612
|
+
toolName,
|
|
613
|
+
count
|
|
614
|
+
})),
|
|
615
|
+
directPathTurns: [...this.directPathTurns].map(([toolCallId, turn]) => ({
|
|
616
|
+
toolCallId,
|
|
617
|
+
turn
|
|
618
|
+
}))
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
restoreSubagentResumeState(state) {
|
|
622
|
+
this.toolUsageCount.clear();
|
|
623
|
+
for (const { toolName, count } of state.toolUsageCounts) this.toolUsageCount.set(toolName, count);
|
|
624
|
+
this.directPathTurns.clear();
|
|
625
|
+
for (const { toolCallId, turn } of state.directPathTurns) this.directPathTurns.set(toolCallId, turn);
|
|
626
|
+
}
|
|
562
627
|
recordToolUsageTurn(toolName, turn, callId) {
|
|
563
628
|
this.toolUsageCount.set(toolName, Math.max(this.toolUsageCount.get(toolName) ?? 0, turn + 1));
|
|
564
629
|
if (callId != null && callId !== "") this.toolCallTurns.set(callId, turn);
|
|
@@ -860,9 +925,9 @@ var ToolNode = class extends RunnableCallable {
|
|
|
860
925
|
* `blockDirectCall`, `respond` returns the host-supplied
|
|
861
926
|
* `responseText` as a synthetic success ToolMessage,
|
|
862
927
|
* `edit` re-runs with edited args. LangGraph re-enters
|
|
863
|
-
* ToolNode.run from the start on resume
|
|
864
|
-
* again
|
|
865
|
-
*
|
|
928
|
+
* ToolNode.run from the start on resume. Reusable hooks fire
|
|
929
|
+
* again; a consumed one-shot hook replays its pending approval
|
|
930
|
+
* result. In both cases `interrupt()` consumes the resume value.
|
|
866
931
|
* • When HITL is off: collapses to a fail-closed deny (matches
|
|
867
932
|
* the rest of the SDK's HITL-disabled default). One-time
|
|
868
933
|
* warning logged so hosts notice the gap.
|
|
@@ -882,13 +947,49 @@ var ToolNode = class extends RunnableCallable {
|
|
|
882
947
|
* scopes and is left to a follow-up.
|
|
883
948
|
*/
|
|
884
949
|
async runDirectToolWithLifecycleHooks(call, config, batchContext = {}) {
|
|
950
|
+
const replayController = this.toolMap.get(call.name)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
951
|
+
const replayConfig = replayController == null || batchContext.replayBatchKey == null ? config : {
|
|
952
|
+
...config,
|
|
953
|
+
configurable: {
|
|
954
|
+
...config.configurable,
|
|
955
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: batchContext.replayBatchKey
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const settledOutput = await replayController?.getSettledOutput(call, replayConfig);
|
|
959
|
+
if (settledOutput != null) {
|
|
960
|
+
if (batchContext.additionalContextsSink != null && settledOutput.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...settledOutput.additionalContexts);
|
|
961
|
+
if (batchContext.resolvedArgsByCallId != null && call.id != null && settledOutput.resolvedArgs != null) batchContext.resolvedArgsByCallId.set(call.id, settledOutput.resolvedArgs);
|
|
962
|
+
const refMeta = settledOutput.output.additional_kwargs;
|
|
963
|
+
if (this.toolOutputRegistry != null && refMeta?._refKey != null && settledOutput.referenceContent != null) this.toolOutputRegistry.set(refMeta._refScope, refMeta._refKey, settledOutput.referenceContent);
|
|
964
|
+
return settledOutput.output;
|
|
965
|
+
}
|
|
966
|
+
const replayAdditionalContexts = [];
|
|
967
|
+
const persistOutput = async (output, terminalArgs) => {
|
|
968
|
+
const refMeta = output.additional_kwargs;
|
|
969
|
+
if (terminalArgs != null && call.id != null && batchContext.resolvedArgsByCallId != null) batchContext.resolvedArgsByCallId.set(call.id, terminalArgs);
|
|
970
|
+
const resolvedArgs = terminalArgs ?? (call.id == null ? void 0 : batchContext.resolvedArgsByCallId?.get(call.id));
|
|
971
|
+
const referenceContent = this.toolOutputRegistry == null || refMeta?._refKey == null ? void 0 : this.toolOutputRegistry.get(refMeta._refScope, refMeta._refKey);
|
|
972
|
+
await replayController?.persistSettledOutput(call, replayConfig, {
|
|
973
|
+
output,
|
|
974
|
+
additionalContexts: replayAdditionalContexts,
|
|
975
|
+
...resolvedArgs == null ? {} : { resolvedArgs },
|
|
976
|
+
...referenceContent == null ? {} : { referenceContent }
|
|
977
|
+
});
|
|
978
|
+
return output;
|
|
979
|
+
};
|
|
885
980
|
const runId = config.configurable?.run_id ?? "";
|
|
981
|
+
const threadId = config.configurable?.thread_id;
|
|
886
982
|
const hookRegistry = this.hookRegistry;
|
|
887
983
|
const hasPreHook = hookRegistry?.hasHookFor("PreToolUse", runId) === true;
|
|
984
|
+
const approvalReplayKey = call.id == null || call.id === "" ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", call.id);
|
|
985
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
|
|
986
|
+
const pendingApproval = approvalReplayKey == null ? void 0 : hookRegistry?.getPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
|
|
888
987
|
const hasPostHook = hookRegistry?.hasHookFor("PostToolUse", runId) === true;
|
|
889
988
|
const hasFailureHook = hookRegistry?.hasHookFor("PostToolUseFailure", runId) === true;
|
|
890
|
-
if (hookRegistry == null || !hasPreHook && !hasPostHook && !hasFailureHook)
|
|
891
|
-
|
|
989
|
+
if (hookRegistry == null || !hasPreHook && pendingApproval == null && !hasPostHook && !hasFailureHook) {
|
|
990
|
+
const output = await this.runTool(call, replayConfig, batchContext);
|
|
991
|
+
return output instanceof ToolMessage ? persistOutput(output) : output;
|
|
992
|
+
}
|
|
892
993
|
const registryRunId = batchContext.batchScopeId ?? config.configurable?.run_id;
|
|
893
994
|
let usageCount;
|
|
894
995
|
const cachedTurn = call.id != null && call.id !== "" ? this.directPathTurns.get(call.id) ?? this.toolCallTurns.get(call.id) : void 0;
|
|
@@ -913,7 +1014,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
913
1014
|
resolvedArgs = resolved;
|
|
914
1015
|
}
|
|
915
1016
|
let effectiveCall = call;
|
|
916
|
-
if (hasPreHook) {
|
|
1017
|
+
if (hasPreHook || pendingApproval != null) {
|
|
917
1018
|
const preResult = await executeHooks({
|
|
918
1019
|
registry: hookRegistry,
|
|
919
1020
|
input: {
|
|
@@ -929,34 +1030,48 @@ var ToolNode = class extends RunnableCallable {
|
|
|
929
1030
|
turn
|
|
930
1031
|
},
|
|
931
1032
|
sessionId: runId,
|
|
932
|
-
matchQuery: call.name
|
|
1033
|
+
matchQuery: call.name,
|
|
1034
|
+
onceReplayKey: approvalReplayKey,
|
|
1035
|
+
onceReplaySessionId: approvalReplaySessionId
|
|
933
1036
|
}).catch(() => void 0);
|
|
934
1037
|
if (preResult != null) {
|
|
935
|
-
if (
|
|
1038
|
+
if (preResult.additionalContexts.length > 0) {
|
|
1039
|
+
replayAdditionalContexts.push(...preResult.additionalContexts);
|
|
1040
|
+
batchContext.additionalContextsSink?.push(...preResult.additionalContexts);
|
|
1041
|
+
}
|
|
936
1042
|
if (preResult.updatedInput != null) effectiveCall = {
|
|
937
1043
|
...call,
|
|
938
1044
|
args: preResult.updatedInput
|
|
939
1045
|
};
|
|
940
|
-
if (preResult.decision === "deny") return this.blockDirectCall({
|
|
1046
|
+
if (preResult.decision === "deny") return persistOutput(this.blockDirectCall({
|
|
941
1047
|
call,
|
|
942
1048
|
resolvedArgs,
|
|
943
1049
|
reason: preResult.reason ?? "Blocked by hook",
|
|
944
1050
|
hookRegistry,
|
|
945
1051
|
runId,
|
|
946
1052
|
threadId
|
|
947
|
-
});
|
|
1053
|
+
}), effectiveCall.args);
|
|
948
1054
|
if (preResult.decision === "ask") {
|
|
949
1055
|
if (this.humanInTheLoop?.enabled !== true) {
|
|
950
1056
|
const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
|
|
951
|
-
return this.blockDirectCall({
|
|
1057
|
+
return persistOutput(this.blockDirectCall({
|
|
952
1058
|
call,
|
|
953
1059
|
resolvedArgs,
|
|
954
1060
|
reason,
|
|
955
1061
|
hookRegistry,
|
|
956
1062
|
runId,
|
|
957
1063
|
threadId
|
|
958
|
-
});
|
|
1064
|
+
}), effectiveCall.args);
|
|
959
1065
|
}
|
|
1066
|
+
const toolCallId = call.id;
|
|
1067
|
+
if (toolCallId == null || toolCallId === "" || approvalReplayKey == null) return persistOutput(this.blockDirectCall({
|
|
1068
|
+
call,
|
|
1069
|
+
resolvedArgs,
|
|
1070
|
+
reason: "Tool approval requires a non-empty tool call ID — failing closed",
|
|
1071
|
+
hookRegistry,
|
|
1072
|
+
runId,
|
|
1073
|
+
threadId
|
|
1074
|
+
}), effectiveCall.args);
|
|
960
1075
|
const payload = buildToolApprovalInterruptPayload([{
|
|
961
1076
|
entry: {
|
|
962
1077
|
call: effectiveCall,
|
|
@@ -965,59 +1080,71 @@ var ToolNode = class extends RunnableCallable {
|
|
|
965
1080
|
},
|
|
966
1081
|
reason: preResult.reason,
|
|
967
1082
|
allowedDecisions: preResult.allowedDecisions
|
|
968
|
-
}]);
|
|
1083
|
+
}], runId);
|
|
969
1084
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
|
|
970
|
-
|
|
1085
|
+
hookRegistry.clearPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
|
|
1086
|
+
const decision = normalizeApprovalDecisions([toolCallId], resumeValue).get(toolCallId) ?? {
|
|
971
1087
|
type: "reject",
|
|
972
1088
|
reason: "No decision provided for tool approval"
|
|
973
1089
|
};
|
|
974
1090
|
const declaredType = decision.type;
|
|
975
|
-
if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return this.blockDirectCall({
|
|
1091
|
+
if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return persistOutput(this.blockDirectCall({
|
|
976
1092
|
call,
|
|
977
1093
|
resolvedArgs,
|
|
978
1094
|
reason: `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${preResult.allowedDecisions.join(", ")}] — failing closed`,
|
|
979
1095
|
hookRegistry,
|
|
980
1096
|
runId,
|
|
981
1097
|
threadId
|
|
982
|
-
});
|
|
983
|
-
if (decision.type === "reject") return this.blockDirectCall({
|
|
1098
|
+
}), effectiveCall.args);
|
|
1099
|
+
if (decision.type === "reject") return persistOutput(this.blockDirectCall({
|
|
984
1100
|
call,
|
|
985
1101
|
resolvedArgs,
|
|
986
1102
|
reason: decision.reason ?? preResult.reason ?? "Rejected by user",
|
|
987
1103
|
hookRegistry,
|
|
988
1104
|
runId,
|
|
989
1105
|
threadId
|
|
990
|
-
});
|
|
1106
|
+
}), effectiveCall.args);
|
|
991
1107
|
if (decision.type === "respond") {
|
|
992
1108
|
const responseText = decision.responseText;
|
|
993
|
-
if (typeof responseText !== "string") return this.blockDirectCall({
|
|
1109
|
+
if (typeof responseText !== "string") return persistOutput(this.blockDirectCall({
|
|
994
1110
|
call,
|
|
995
1111
|
resolvedArgs,
|
|
996
1112
|
reason: "Approval payload `respond` was missing a string `responseText`",
|
|
997
1113
|
hookRegistry,
|
|
998
1114
|
runId,
|
|
999
1115
|
threadId
|
|
1000
|
-
});
|
|
1001
|
-
return new ToolMessage({
|
|
1116
|
+
}), effectiveCall.args);
|
|
1117
|
+
return persistOutput(new ToolMessage({
|
|
1002
1118
|
status: "success",
|
|
1003
|
-
content: responseText,
|
|
1119
|
+
content: truncateToolResultContent(responseText, this.maxToolResultChars),
|
|
1004
1120
|
name: call.name,
|
|
1005
1121
|
tool_call_id: call.id ?? ""
|
|
1006
|
-
});
|
|
1122
|
+
}), effectiveCall.args);
|
|
1007
1123
|
}
|
|
1008
1124
|
if (decision.type === "edit") {
|
|
1009
1125
|
const updatedInput = decision.updatedInput;
|
|
1010
|
-
if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return new ToolMessage({
|
|
1126
|
+
if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return persistOutput(new ToolMessage({
|
|
1011
1127
|
status: "error",
|
|
1012
1128
|
content: "Decision \"edit\" missing object updatedInput — failing closed.",
|
|
1013
1129
|
name: call.name,
|
|
1014
1130
|
tool_call_id: call.id ?? ""
|
|
1015
|
-
});
|
|
1131
|
+
}), effectiveCall.args);
|
|
1016
1132
|
effectiveCall = {
|
|
1017
1133
|
...call,
|
|
1018
1134
|
args: updatedInput
|
|
1019
1135
|
};
|
|
1020
1136
|
}
|
|
1137
|
+
if (declaredType !== "approve" && declaredType !== "edit") {
|
|
1138
|
+
const unknownType = typeof declaredType === "string" ? declaredType : "<missing>";
|
|
1139
|
+
return persistOutput(this.blockDirectCall({
|
|
1140
|
+
call,
|
|
1141
|
+
resolvedArgs,
|
|
1142
|
+
reason: `Unknown approval decision type "${unknownType}" — failing closed`,
|
|
1143
|
+
hookRegistry,
|
|
1144
|
+
runId,
|
|
1145
|
+
threadId
|
|
1146
|
+
}), effectiveCall.args);
|
|
1147
|
+
}
|
|
1021
1148
|
}
|
|
1022
1149
|
}
|
|
1023
1150
|
}
|
|
@@ -1031,7 +1158,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1031
1158
|
* which is strictly more accurate.
|
|
1032
1159
|
*/
|
|
1033
1160
|
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,
|
|
1161
|
+
const output = await this.runTool(effectiveCall, replayConfig, {
|
|
1035
1162
|
...batchContext,
|
|
1036
1163
|
usageCount
|
|
1037
1164
|
});
|
|
@@ -1055,8 +1182,11 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1055
1182
|
sessionId: runId,
|
|
1056
1183
|
matchQuery: call.name
|
|
1057
1184
|
}).catch(() => void 0);
|
|
1058
|
-
if (failureResult != null &&
|
|
1059
|
-
|
|
1185
|
+
if (failureResult != null && failureResult.additionalContexts.length > 0) {
|
|
1186
|
+
replayAdditionalContexts.push(...failureResult.additionalContexts);
|
|
1187
|
+
batchContext.additionalContextsSink?.push(...failureResult.additionalContexts);
|
|
1188
|
+
}
|
|
1189
|
+
return persistOutput(output);
|
|
1060
1190
|
}
|
|
1061
1191
|
if (output.status !== "error" && hasPostHook) {
|
|
1062
1192
|
const postResult = await executeHooks({
|
|
@@ -1077,21 +1207,24 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1077
1207
|
sessionId: runId,
|
|
1078
1208
|
matchQuery: call.name
|
|
1079
1209
|
}).catch(() => void 0);
|
|
1080
|
-
if (postResult != null &&
|
|
1210
|
+
if (postResult != null && postResult.additionalContexts.length > 0) {
|
|
1211
|
+
replayAdditionalContexts.push(...postResult.additionalContexts);
|
|
1212
|
+
batchContext.additionalContextsSink?.push(...postResult.additionalContexts);
|
|
1213
|
+
}
|
|
1081
1214
|
if (postResult?.updatedOutput != null) {
|
|
1082
1215
|
if (hasComputerCallOutputMarker(output)) {
|
|
1083
1216
|
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);
|
|
1217
|
+
return persistOutput(cloneToolMessageWithContent(output, postResult.updatedOutput));
|
|
1085
1218
|
}
|
|
1086
1219
|
const refMeta = output.additional_kwargs;
|
|
1087
1220
|
const refKey = refMeta?._refKey;
|
|
1088
1221
|
const refScope = refMeta?._refScope;
|
|
1089
1222
|
const replaced = serializeToolOutputWithinLimits(postResult.updatedOutput, this.maxToolResultChars, this.toolOutputRegistry != null && refKey != null ? this.toolOutputRegistry.perOutputLimit : 0);
|
|
1090
1223
|
if (this.toolOutputRegistry != null && refKey != null) this.toolOutputRegistry.set(refScope, refKey, replaced.registryContent);
|
|
1091
|
-
return cloneToolMessageWithContent(output, replaced.content);
|
|
1224
|
+
return persistOutput(cloneToolMessageWithContent(output, replaced.content));
|
|
1092
1225
|
}
|
|
1093
1226
|
}
|
|
1094
|
-
return output;
|
|
1227
|
+
return persistOutput(output);
|
|
1095
1228
|
}
|
|
1096
1229
|
/**
|
|
1097
1230
|
* `ask` decisions on direct-path tools collapse to fail-closed deny
|
|
@@ -1382,13 +1515,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1382
1515
|
injectedMessages: [],
|
|
1383
1516
|
errors: []
|
|
1384
1517
|
});
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
* to defensively `?.` it across every reference inside.
|
|
1390
|
-
*/
|
|
1391
|
-
const hookRegistry = this.hookRegistry;
|
|
1518
|
+
const hookRegistry = this.hookRegistry;
|
|
1519
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
|
|
1520
|
+
const hasPendingApproval = preToolCalls.some((entry) => entry.call.id != null && hookRegistry?.getPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id)) != null);
|
|
1521
|
+
if (hookRegistry != null && (hookRegistry.hasHookFor("PreToolUse", runId) || hasPendingApproval)) {
|
|
1392
1522
|
/**
|
|
1393
1523
|
* Pull each call's prestarted eager record BEFORE awaiting the hooks:
|
|
1394
1524
|
* an async deny must never race the eager host promise into emitting a
|
|
@@ -1408,23 +1538,29 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1408
1538
|
this.eagerEventToolExecutions.delete(callId);
|
|
1409
1539
|
}
|
|
1410
1540
|
}
|
|
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
|
-
|
|
1541
|
+
const preResults = await Promise.all(preToolCalls.map((entry) => {
|
|
1542
|
+
const toolUseId = entry.call.id;
|
|
1543
|
+
const approvalReplayKey = toolUseId == null ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", toolUseId);
|
|
1544
|
+
return executeHooks({
|
|
1545
|
+
registry: hookRegistry,
|
|
1546
|
+
input: {
|
|
1547
|
+
hook_event_name: "PreToolUse",
|
|
1548
|
+
runId,
|
|
1549
|
+
threadId,
|
|
1550
|
+
agentId: this.agentId,
|
|
1551
|
+
executingAgentId: this.executingAgentId,
|
|
1552
|
+
toolName: entry.call.name,
|
|
1553
|
+
toolInput: entry.args,
|
|
1554
|
+
toolUseId: entry.call.id,
|
|
1555
|
+
stepId: entry.stepId,
|
|
1556
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0
|
|
1557
|
+
},
|
|
1558
|
+
sessionId: runId,
|
|
1559
|
+
matchQuery: entry.call.name,
|
|
1560
|
+
onceReplayKey: approvalReplayKey,
|
|
1561
|
+
onceReplaySessionId: approvalReplaySessionId
|
|
1562
|
+
}).catch(() => HOOK_FALLBACK);
|
|
1563
|
+
}));
|
|
1428
1564
|
/**
|
|
1429
1565
|
* Side effects deferred from `blockEntry` until after any pending
|
|
1430
1566
|
* `interrupt()` resolves. Without deferral, a batch that mixes a
|
|
@@ -1581,12 +1717,11 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1581
1717
|
* pauses, gathers human input, and resumes the run with one
|
|
1582
1718
|
* decision per request. On resume LangGraph re-executes this node
|
|
1583
1719
|
* from the start; `interrupt()` then returns the resume value
|
|
1584
|
-
* instead of throwing
|
|
1585
|
-
*
|
|
1586
|
-
* are pure — see `humanInTheLoop` docs).
|
|
1720
|
+
* instead of throwing. Reusable hooks rebuild their entries, while
|
|
1721
|
+
* consumed one-shot hooks replay their pending approval aggregate.
|
|
1587
1722
|
*/
|
|
1588
1723
|
if (askEntries.length > 0) {
|
|
1589
|
-
const payload = buildToolApprovalInterruptPayload(askEntries);
|
|
1724
|
+
const payload = buildToolApprovalInterruptPayload(askEntries, runId);
|
|
1590
1725
|
/**
|
|
1591
1726
|
* `interrupt()` reads the current `RunnableConfig` from
|
|
1592
1727
|
* AsyncLocalStorage. ToolNode usually runs with tracing disabled
|
|
@@ -1597,6 +1732,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1597
1732
|
* suspend and resume.
|
|
1598
1733
|
*/
|
|
1599
1734
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
|
|
1735
|
+
for (const { entry } of askEntries) hookRegistry.clearPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id));
|
|
1600
1736
|
const decisionByCallId = normalizeApprovalDecisions(askEntries.map(({ entry }) => entry.call.id), resumeValue);
|
|
1601
1737
|
for (const { entry, reason: askReason, allowedDecisions } of askEntries) {
|
|
1602
1738
|
const decision = decisionByCallId.get(entry.call.id) ?? {
|
|
@@ -2187,16 +2323,16 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2187
2323
|
* call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
|
|
2188
2324
|
* whole ToolNode *before* any non-interrupting sibling has started — so a
|
|
2189
2325
|
* sibling with real side effects (send_email, billing) never executes on
|
|
2190
|
-
* the first pass.
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2326
|
+
* the first pass. Terminal interrupting siblings are cached by call id, so
|
|
2327
|
+
* LangGraph replay reuses their complete lifecycle output instead of
|
|
2328
|
+
* repeating model calls, hooks, or side effects.
|
|
2193
2329
|
*
|
|
2194
2330
|
* Without this ordering, a flat `Promise.all` starts every sibling
|
|
2195
2331
|
* concurrently, so a non-idempotent sibling can complete its side effect
|
|
2196
2332
|
* before the interrupt unwinds and then run a SECOND time on resume — the
|
|
2197
|
-
* duplicate side effect this method exists to prevent.
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2333
|
+
* duplicate side effect this method exists to prevent. A tool that actually
|
|
2334
|
+
* suspends re-enters until it reaches a terminal result; siblings that
|
|
2335
|
+
* already settled do not re-enter.
|
|
2200
2336
|
*
|
|
2201
2337
|
* `batchIndices[i]` is `directCalls[i]`'s position within the parent
|
|
2202
2338
|
* ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
|
|
@@ -2205,28 +2341,95 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2205
2341
|
* per-call here.
|
|
2206
2342
|
*/
|
|
2207
2343
|
async runDirectBatchInterruptSafe(directCalls, batchIndices, config, baseContext) {
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2344
|
+
const settledBatchResults = baseContext.replayBatchKey == null ? void 0 : this.settledInterruptingResults.get(baseContext.replayBatchKey);
|
|
2345
|
+
const restoreResult = (call, result) => {
|
|
2346
|
+
baseContext.additionalContextsSink?.push(...result.additionalContexts);
|
|
2347
|
+
if (call.id != null && result.resolvedArgs != null && baseContext.resolvedArgsByCallId != null) baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
|
|
2348
|
+
return result;
|
|
2349
|
+
};
|
|
2350
|
+
const runOne = async (call, position) => {
|
|
2351
|
+
const cachedResult = typeof call.id === "string" ? settledBatchResults?.get(call.id) : void 0;
|
|
2352
|
+
if (cachedResult != null) return restoreResult(call, cachedResult);
|
|
2353
|
+
const additionalContexts = [];
|
|
2354
|
+
const output = await this.runDirectToolWithLifecycleHooks(call, config, {
|
|
2355
|
+
...baseContext,
|
|
2356
|
+
batchIndex: batchIndices[position],
|
|
2357
|
+
additionalContextsSink: additionalContexts
|
|
2358
|
+
});
|
|
2359
|
+
const resolvedArgs = call.id == null ? void 0 : baseContext.resolvedArgsByCallId?.get(call.id);
|
|
2360
|
+
return restoreResult(call, {
|
|
2361
|
+
output,
|
|
2362
|
+
additionalContexts,
|
|
2363
|
+
...resolvedArgs == null ? {} : { resolvedArgs }
|
|
2364
|
+
});
|
|
2365
|
+
};
|
|
2212
2366
|
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)));
|
|
2367
|
+
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
2368
|
const outputs = new Array(directCalls.length);
|
|
2215
2369
|
const interruptingPositions = [];
|
|
2216
2370
|
const regularPositions = [];
|
|
2217
2371
|
for (let i = 0; i < directCalls.length; i++) if (interrupting.has(directCalls[i].name)) interruptingPositions.push(i);
|
|
2218
2372
|
else regularPositions.push(i);
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2373
|
+
const interruptingResults = await Promise.allSettled(interruptingPositions.map((i) => runOne(directCalls[i], i)));
|
|
2374
|
+
/** A sibling may have tripped the run-wide breaker while another raised
|
|
2375
|
+
* a GraphInterrupt. Safety failures take precedence over approval pauses
|
|
2376
|
+
* regardless of input order. */
|
|
2377
|
+
this.throwIfBreakerTripped(config);
|
|
2378
|
+
let hasInterruptingError = false;
|
|
2379
|
+
let interruptingError;
|
|
2380
|
+
for (let i = 0; i < interruptingResults.length; i++) {
|
|
2381
|
+
const result = interruptingResults[i];
|
|
2382
|
+
if (result.status === "rejected") {
|
|
2383
|
+
if (!hasInterruptingError) {
|
|
2384
|
+
hasInterruptingError = true;
|
|
2385
|
+
interruptingError = result.reason;
|
|
2386
|
+
}
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
const position = interruptingPositions[i];
|
|
2390
|
+
outputs[position] = result.value.output;
|
|
2391
|
+
const callId = directCalls[position].id;
|
|
2392
|
+
if (baseContext.replayBatchKey != null && typeof callId === "string" && callId !== "") {
|
|
2393
|
+
const batchResults = this.settledInterruptingResults.get(baseContext.replayBatchKey) ?? /* @__PURE__ */ new Map();
|
|
2394
|
+
batchResults.set(callId, result.value);
|
|
2395
|
+
this.settledInterruptingResults.set(baseContext.replayBatchKey, batchResults);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (hasInterruptingError) {
|
|
2399
|
+
if (isGraphInterrupt(interruptingError)) {
|
|
2400
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
2401
|
+
const parentToolCallIds = /* @__PURE__ */ new Set();
|
|
2402
|
+
for (const call of directCalls) {
|
|
2403
|
+
const controller = this.toolMap.get(call.name)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
2404
|
+
if (controller?.getResumeManifest != null) controllers.add(controller);
|
|
2405
|
+
if (call.id != null && call.id !== "") parentToolCallIds.add(call.id);
|
|
2406
|
+
}
|
|
2407
|
+
const executionsByParentCall = /* @__PURE__ */ new Map();
|
|
2408
|
+
for (const controller of controllers) {
|
|
2409
|
+
const manifest = await controller.getResumeManifest?.(parentToolCallIds);
|
|
2410
|
+
if (manifest != null) for (const execution of manifest.executions) executionsByParentCall.set(execution.parentToolCallId, execution);
|
|
2411
|
+
}
|
|
2412
|
+
const executions = [...executionsByParentCall.values()];
|
|
2413
|
+
if (executions.length > 0) {
|
|
2414
|
+
const manifest = {
|
|
2415
|
+
version: 1,
|
|
2416
|
+
executions
|
|
2417
|
+
};
|
|
2418
|
+
throw new GraphInterrupt(interruptingError.interrupts.map((pendingInterrupt) => ({
|
|
2419
|
+
...pendingInterrupt,
|
|
2420
|
+
value: attachSubagentResumeManifest(pendingInterrupt.value, manifest)
|
|
2421
|
+
})));
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
throw interruptingError;
|
|
2425
|
+
}
|
|
2223
2426
|
/** The breaker can trip while the interrupting group is awaited — e.g.
|
|
2224
2427
|
* a tool that ignores cancellation and completes normally. Recheck
|
|
2225
2428
|
* before starting the non-idempotent siblings. */
|
|
2226
2429
|
this.throwIfBreakerTripped(config);
|
|
2227
2430
|
const regularOutputs = await Promise.all(regularPositions.map((i) => runOne(directCalls[i], i)));
|
|
2228
2431
|
regularPositions.forEach((i, k) => {
|
|
2229
|
-
outputs[i] = regularOutputs[k];
|
|
2432
|
+
outputs[i] = regularOutputs[k].output;
|
|
2230
2433
|
});
|
|
2231
2434
|
return outputs;
|
|
2232
2435
|
}
|
|
@@ -2282,9 +2485,23 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2282
2485
|
const resolvedArgsByCallId = /* @__PURE__ */ new Map();
|
|
2283
2486
|
/** Per-invocation error-completion ownership — see `ToolErrorOwnership`. */
|
|
2284
2487
|
const errorOwnership = createToolErrorOwnership();
|
|
2285
|
-
|
|
2488
|
+
/**
|
|
2489
|
+
* Claim this batch's turn synchronously from the registry (or
|
|
2490
|
+
* fall back to 0 when the feature is disabled). The registry is
|
|
2491
|
+
* partitioned by scope id so overlapping batches cannot
|
|
2492
|
+
* overwrite each other's state even under a shared registry.
|
|
2493
|
+
*
|
|
2494
|
+
* For anonymous callers (no `run_id` in config), mint a unique
|
|
2495
|
+
* per-batch scope id so two concurrent anonymous invocations
|
|
2496
|
+
* don't target the same bucket. The scope is threaded down to
|
|
2497
|
+
* every subsequent registry call on this batch.
|
|
2498
|
+
*/
|
|
2499
|
+
const incomingRunId = config.configurable?.run_id;
|
|
2500
|
+
const incomingThreadId = config.configurable?.thread_id;
|
|
2501
|
+
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
2286
2502
|
const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
2287
2503
|
let outputs;
|
|
2504
|
+
let replayBatchKey;
|
|
2288
2505
|
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
2289
2506
|
* the promotion into handoff updates (same-id state copies there would
|
|
2290
2507
|
* otherwise overwrite the replacement message). */
|
|
@@ -2299,6 +2516,9 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2299
2516
|
});
|
|
2300
2517
|
const directAdditionalContexts = [];
|
|
2301
2518
|
const { lg_tool_call: _sendToolCall, ...sendState } = input;
|
|
2519
|
+
const sendMessages = sendState.messages;
|
|
2520
|
+
const sendAssistantBatch = Array.isArray(sendMessages) ? findAssistantBatch(sendMessages) : void 0;
|
|
2521
|
+
const sendReplayBatchKey = sendAssistantBatch == null ? void 0 : getAssistantBatchReplayKey(sendAssistantBatch, incomingRunId, incomingThreadId);
|
|
2302
2522
|
const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
|
|
2303
2523
|
batchIndex: 0,
|
|
2304
2524
|
turn,
|
|
@@ -2306,6 +2526,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2306
2526
|
resolvedArgsByCallId,
|
|
2307
2527
|
errorOwnership,
|
|
2308
2528
|
additionalContextsSink: directAdditionalContexts,
|
|
2529
|
+
replayBatchKey: sendReplayBatchKey,
|
|
2309
2530
|
runInput: sendState
|
|
2310
2531
|
});
|
|
2311
2532
|
outputs = directAdditionalContexts.length > 0 ? [sendOutput, new HumanMessage({
|
|
@@ -2322,15 +2543,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2322
2543
|
else if (this.isMessagesState(input)) messages = input.messages;
|
|
2323
2544
|
else throw new Error("ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input.");
|
|
2324
2545
|
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.");
|
|
2546
|
+
const assistantBatch = findAssistantBatch(messages);
|
|
2547
|
+
if (assistantBatch == null) throw new Error("ToolNode only accepts AIMessages as input.");
|
|
2548
|
+
const aiMessage = assistantBatch.message;
|
|
2549
|
+
replayBatchKey = getAssistantBatchReplayKey(assistantBatch, incomingRunId, incomingThreadId);
|
|
2334
2550
|
if (this.loadRuntimeTools) {
|
|
2335
2551
|
const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
|
|
2336
2552
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
@@ -2445,6 +2661,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2445
2661
|
errorOwnership,
|
|
2446
2662
|
preBatchSnapshot,
|
|
2447
2663
|
additionalContextsSink: directAdditionalContexts,
|
|
2664
|
+
replayBatchKey,
|
|
2448
2665
|
runInput: input
|
|
2449
2666
|
}) : [];
|
|
2450
2667
|
if (directCalls.length > 0 && directOutputs.length > 0) await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId, errorOwnership);
|
|
@@ -2483,6 +2700,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2483
2700
|
errorOwnership,
|
|
2484
2701
|
preBatchSnapshot,
|
|
2485
2702
|
additionalContextsSink: directAdditionalContexts,
|
|
2703
|
+
replayBatchKey,
|
|
2486
2704
|
runInput: input
|
|
2487
2705
|
});
|
|
2488
2706
|
await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId, errorOwnership);
|
|
@@ -2519,7 +2737,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2519
2737
|
await this.dispatchStepCompleted(result.tool_call_id, result.name ?? "unknown", {}, typeof result.content === "string" ? result.content : "", config);
|
|
2520
2738
|
}
|
|
2521
2739
|
}
|
|
2522
|
-
if (!outputs.some(isCommand))
|
|
2740
|
+
if (!outputs.some(isCommand)) {
|
|
2741
|
+
if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
|
|
2742
|
+
return Array.isArray(input) ? outputs : { messages: outputs };
|
|
2743
|
+
}
|
|
2523
2744
|
/**
|
|
2524
2745
|
* Carry the invalid-call promotion into handoff commands. A handoff
|
|
2525
2746
|
* tool's Command snapshots `update.messages` from the PRE-promotion
|
|
@@ -2606,6 +2827,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2606
2827
|
/** Single handoff - pass through as-is */
|
|
2607
2828
|
combinedOutputs.push(handoffCommands[0]);
|
|
2608
2829
|
if (parentCommand) combinedOutputs.push(parentCommand);
|
|
2830
|
+
if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
|
|
2609
2831
|
return combinedOutputs;
|
|
2610
2832
|
}
|
|
2611
2833
|
isSendInput(input) {
|