@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
package/src/tools/ToolNode.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
END,
|
|
13
13
|
Send,
|
|
14
14
|
Command,
|
|
15
|
+
GraphInterrupt,
|
|
15
16
|
isCommand,
|
|
16
17
|
interrupt,
|
|
17
18
|
isGraphInterrupt,
|
|
@@ -25,8 +26,8 @@ import type {
|
|
|
25
26
|
ToolRuntime,
|
|
26
27
|
StructuredToolInterface,
|
|
27
28
|
} from '@langchain/core/tools';
|
|
28
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
29
29
|
import type { LangGraphRunnableConfig } from '@langchain/langgraph';
|
|
30
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
30
31
|
import type {
|
|
31
32
|
ToolOutputResolveView,
|
|
32
33
|
PreResolvedArgsMap,
|
|
@@ -34,11 +35,18 @@ import type {
|
|
|
34
35
|
ResolveResult,
|
|
35
36
|
ResolveOptions,
|
|
36
37
|
} from '@/tools/toolOutputReferences';
|
|
38
|
+
import type {
|
|
39
|
+
ReplayableSubagentTool,
|
|
40
|
+
SubagentResumeManifest,
|
|
41
|
+
SubagentToolNodeResumeState,
|
|
42
|
+
} from '@/tools/subagent/SubagentReplay';
|
|
37
43
|
import type {
|
|
38
44
|
HookRegistry,
|
|
39
45
|
AggregatedHookResult,
|
|
40
46
|
PostToolBatchEntry,
|
|
47
|
+
ToolApprovalReplayKey,
|
|
41
48
|
} from '@/hooks';
|
|
49
|
+
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
42
50
|
import type * as t from '@/types';
|
|
43
51
|
import {
|
|
44
52
|
cloneToolMessageWithContent,
|
|
@@ -49,6 +57,11 @@ import {
|
|
|
49
57
|
serializeStructuredValueBounded,
|
|
50
58
|
serializeToolContentBounded,
|
|
51
59
|
} from '@/utils/toolContent';
|
|
60
|
+
import {
|
|
61
|
+
attachSubagentResumeManifest,
|
|
62
|
+
SUBAGENT_PARENT_BATCH_CONFIG_KEY,
|
|
63
|
+
SUBAGENT_REPLAY_CONTROLLER,
|
|
64
|
+
} from '@/tools/subagent/SubagentReplay';
|
|
52
65
|
import {
|
|
53
66
|
INTENT_ARG,
|
|
54
67
|
readOutcomeFields,
|
|
@@ -73,17 +86,16 @@ import {
|
|
|
73
86
|
calculateMaxToolResultChars,
|
|
74
87
|
truncateToolResultContent,
|
|
75
88
|
} from '@/utils/truncation';
|
|
89
|
+
import {
|
|
90
|
+
StreamLimitExceededError,
|
|
91
|
+
RUN_BREAKER_SCOPE_CONFIG_KEY,
|
|
92
|
+
} from '@/llm/streamLimits';
|
|
76
93
|
import {
|
|
77
94
|
resolveLocalToolRegistry,
|
|
78
95
|
resolveLocalExecutionTools,
|
|
79
96
|
} from '@/tools/local';
|
|
80
97
|
import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
81
98
|
import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
|
|
82
|
-
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
83
|
-
import {
|
|
84
|
-
StreamLimitExceededError,
|
|
85
|
-
RUN_BREAKER_SCOPE_CONFIG_KEY,
|
|
86
|
-
} from '@/llm/streamLimits';
|
|
87
99
|
|
|
88
100
|
/** Host-facing batch requests must not carry the batch's breaker scope —
|
|
89
101
|
* hosts spread `configurable` into their own run configs. */
|
|
@@ -99,7 +111,42 @@ function stripRunBreakerScope(
|
|
|
99
111
|
import { convertInjectedMessages } from '@/messages/injected';
|
|
100
112
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
101
113
|
import { RunnableCallable, composeAbortSignals } from '@/utils';
|
|
102
|
-
import {
|
|
114
|
+
import {
|
|
115
|
+
executeHooks,
|
|
116
|
+
TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
|
|
117
|
+
} from '@/hooks';
|
|
118
|
+
|
|
119
|
+
function createToolApprovalReplayKey(
|
|
120
|
+
config: RunnableConfig,
|
|
121
|
+
agentId: string,
|
|
122
|
+
toolUseId: string
|
|
123
|
+
): ToolApprovalReplayKey {
|
|
124
|
+
const configuredScope =
|
|
125
|
+
config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
|
|
126
|
+
const threadId = config.configurable?.thread_id;
|
|
127
|
+
let executionScope = '';
|
|
128
|
+
if (typeof configuredScope === 'string') {
|
|
129
|
+
executionScope = configuredScope;
|
|
130
|
+
} else if (typeof threadId === 'string') {
|
|
131
|
+
executionScope = threadId;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
executionScope,
|
|
135
|
+
agentId,
|
|
136
|
+
toolUseId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getToolApprovalReplaySessionId(
|
|
141
|
+
config: RunnableConfig,
|
|
142
|
+
hookSessionId: string
|
|
143
|
+
): string {
|
|
144
|
+
const configuredScope =
|
|
145
|
+
config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
|
|
146
|
+
return typeof configuredScope === 'string' && configuredScope.length > 0
|
|
147
|
+
? configuredScope
|
|
148
|
+
: hookSessionId;
|
|
149
|
+
}
|
|
103
150
|
|
|
104
151
|
/**
|
|
105
152
|
* Per-call batch context for `runTool`. Bundles every optional
|
|
@@ -145,6 +192,8 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
145
192
|
* contract for hosts relying on it for policy / recovery guidance.
|
|
146
193
|
*/
|
|
147
194
|
additionalContextsSink?: string[];
|
|
195
|
+
/** Stable identity of the assistant tool-call batch across HITL replay. */
|
|
196
|
+
replayBatchKey?: string;
|
|
148
197
|
/**
|
|
149
198
|
* Graph state the ToolNode was invoked with, threaded from `run()`
|
|
150
199
|
* so `tool.invoke` can forward it as langgraph 1.4's `runtime.state`
|
|
@@ -156,6 +205,28 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
156
205
|
errorOwnership?: ToolErrorOwnership;
|
|
157
206
|
};
|
|
158
207
|
|
|
208
|
+
function withSubagentReplayBatch(
|
|
209
|
+
config: RunnableConfig,
|
|
210
|
+
replayBatchKey: string | undefined
|
|
211
|
+
): RunnableConfig {
|
|
212
|
+
if (replayBatchKey == null) {
|
|
213
|
+
return config;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
...config,
|
|
217
|
+
configurable: {
|
|
218
|
+
...config.configurable,
|
|
219
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: replayBatchKey,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type SettledDirectToolResult = {
|
|
225
|
+
output: BaseMessage | Command;
|
|
226
|
+
additionalContexts: string[];
|
|
227
|
+
resolvedArgs?: Record<string, unknown>;
|
|
228
|
+
};
|
|
229
|
+
|
|
159
230
|
/**
|
|
160
231
|
* Batch-local record of who owns each failed call's completion event.
|
|
161
232
|
*
|
|
@@ -315,6 +386,38 @@ function describeOfferedShape(value: unknown): string {
|
|
|
315
386
|
return typeof value;
|
|
316
387
|
}
|
|
317
388
|
|
|
389
|
+
type AssistantBatch = {
|
|
390
|
+
message: AIMessage;
|
|
391
|
+
index: number;
|
|
392
|
+
messageCount: number;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
function findAssistantBatch(
|
|
396
|
+
messages: BaseMessage[]
|
|
397
|
+
): AssistantBatch | undefined {
|
|
398
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
399
|
+
const message = messages[i];
|
|
400
|
+
if (isAIMessage(message)) {
|
|
401
|
+
return { message, index: i, messageCount: messages.length };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getAssistantBatchReplayKey(
|
|
408
|
+
batch: AssistantBatch,
|
|
409
|
+
runId: string | undefined,
|
|
410
|
+
threadId: string | undefined
|
|
411
|
+
): string {
|
|
412
|
+
return JSON.stringify([
|
|
413
|
+
runId ?? null,
|
|
414
|
+
threadId ?? null,
|
|
415
|
+
batch.message.id ?? null,
|
|
416
|
+
batch.index,
|
|
417
|
+
batch.messageCount,
|
|
418
|
+
]);
|
|
419
|
+
}
|
|
420
|
+
|
|
318
421
|
/**
|
|
319
422
|
* Per-entry record collected during PreToolUse hook handling for tool
|
|
320
423
|
* calls that need human approval. Carries everything
|
|
@@ -342,10 +445,14 @@ type AskEntry = {
|
|
|
342
445
|
* needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
|
|
343
446
|
*/
|
|
344
447
|
function buildToolApprovalInterruptPayload(
|
|
345
|
-
askEntries: ReadonlyArray<AskEntry
|
|
448
|
+
askEntries: ReadonlyArray<AskEntry>,
|
|
449
|
+
hookSessionId?: string
|
|
346
450
|
): t.ToolApprovalInterruptPayload {
|
|
347
451
|
return {
|
|
348
452
|
type: 'tool_approval',
|
|
453
|
+
...(hookSessionId == null || hookSessionId === ''
|
|
454
|
+
? {}
|
|
455
|
+
: { hook_session_id: hookSessionId }),
|
|
349
456
|
action_requests: askEntries.map(({ entry, reason }) => {
|
|
350
457
|
const request: t.ToolApprovalRequest = {
|
|
351
458
|
tool_call_id: entry.call.id!,
|
|
@@ -574,6 +681,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
574
681
|
* the Run ends.
|
|
575
682
|
*/
|
|
576
683
|
private directPathTurns: Map<string, number> = new Map();
|
|
684
|
+
/** Terminal results from interrupting siblings that must survive a
|
|
685
|
+
* LangGraph replay of the containing ToolNode. Includes the sidecar data
|
|
686
|
+
* the fresh batch needs for hook-context injection and completion events. */
|
|
687
|
+
private settledInterruptingResults = new Map<
|
|
688
|
+
string,
|
|
689
|
+
Map<string, SettledDirectToolResult>
|
|
690
|
+
>();
|
|
577
691
|
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
578
692
|
private toolRegistry?: t.LCToolRegistry;
|
|
579
693
|
/** Cached programmatic tools (computed once on first PTC call) */
|
|
@@ -931,16 +1045,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
931
1045
|
}
|
|
932
1046
|
|
|
933
1047
|
/**
|
|
934
|
-
* Flush
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
* call IDs (Codex P2 #33). Hosts can also call this directly if
|
|
1048
|
+
* Flush per-Run direct replay state. Called by the Graph at end-of-Run via
|
|
1049
|
+
* `clearHeavyState`. The state intentionally survives `run()` re-entry so
|
|
1050
|
+
* interrupt + resume keeps both original turn slots and terminal sibling
|
|
1051
|
+
* outputs, but it would otherwise grow linearly and could collide across
|
|
1052
|
+
* Runs if a provider reused call IDs. Hosts can also call this directly if
|
|
940
1053
|
* they reuse a ToolNode across batches outside of a Graph.
|
|
941
1054
|
*/
|
|
942
1055
|
clearDirectPathTurns(): void {
|
|
943
1056
|
this.directPathTurns.clear();
|
|
1057
|
+
this.settledInterruptingResults.clear();
|
|
944
1058
|
}
|
|
945
1059
|
|
|
946
1060
|
/**
|
|
@@ -977,6 +1091,35 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
977
1091
|
return new Map(this.toolUsageCount); // Return a copy
|
|
978
1092
|
}
|
|
979
1093
|
|
|
1094
|
+
createSubagentResumeState(): SubagentToolNodeResumeState {
|
|
1095
|
+
return {
|
|
1096
|
+
stateKey: JSON.stringify([
|
|
1097
|
+
this.executingAgentId ?? '',
|
|
1098
|
+
this.agentId ?? '',
|
|
1099
|
+
this.name,
|
|
1100
|
+
]),
|
|
1101
|
+
toolUsageCounts: [...this.toolUsageCount].map(([toolName, count]) => ({
|
|
1102
|
+
toolName,
|
|
1103
|
+
count,
|
|
1104
|
+
})),
|
|
1105
|
+
directPathTurns: [...this.directPathTurns].map(([toolCallId, turn]) => ({
|
|
1106
|
+
toolCallId,
|
|
1107
|
+
turn,
|
|
1108
|
+
})),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
restoreSubagentResumeState(state: SubagentToolNodeResumeState): void {
|
|
1113
|
+
this.toolUsageCount.clear();
|
|
1114
|
+
for (const { toolName, count } of state.toolUsageCounts) {
|
|
1115
|
+
this.toolUsageCount.set(toolName, count);
|
|
1116
|
+
}
|
|
1117
|
+
this.directPathTurns.clear();
|
|
1118
|
+
for (const { toolCallId, turn } of state.directPathTurns) {
|
|
1119
|
+
this.directPathTurns.set(toolCallId, turn);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
980
1123
|
private recordToolUsageTurn(
|
|
981
1124
|
toolName: string,
|
|
982
1125
|
turn: number,
|
|
@@ -1475,9 +1618,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1475
1618
|
* `blockDirectCall`, `respond` returns the host-supplied
|
|
1476
1619
|
* `responseText` as a synthetic success ToolMessage,
|
|
1477
1620
|
* `edit` re-runs with edited args. LangGraph re-enters
|
|
1478
|
-
* ToolNode.run from the start on resume
|
|
1479
|
-
* again
|
|
1480
|
-
*
|
|
1621
|
+
* ToolNode.run from the start on resume. Reusable hooks fire
|
|
1622
|
+
* again; a consumed one-shot hook replays its pending approval
|
|
1623
|
+
* result. In both cases `interrupt()` consumes the resume value.
|
|
1481
1624
|
* • When HITL is off: collapses to a fail-closed deny (matches
|
|
1482
1625
|
* the rest of the SDK's HITL-disabled default). One-time
|
|
1483
1626
|
* warning logged so hosts notice the gap.
|
|
@@ -1501,21 +1644,122 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1501
1644
|
config: RunnableConfig,
|
|
1502
1645
|
batchContext: RunToolBatchContext<T> = {}
|
|
1503
1646
|
): Promise<BaseMessage | Command> {
|
|
1647
|
+
const replayController = (
|
|
1648
|
+
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
1649
|
+
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
1650
|
+
const replayConfig =
|
|
1651
|
+
replayController == null
|
|
1652
|
+
? config
|
|
1653
|
+
: withSubagentReplayBatch(config, batchContext.replayBatchKey);
|
|
1654
|
+
const settledOutput = await replayController?.getSettledOutput(
|
|
1655
|
+
call,
|
|
1656
|
+
replayConfig
|
|
1657
|
+
);
|
|
1658
|
+
if (settledOutput != null) {
|
|
1659
|
+
if (
|
|
1660
|
+
batchContext.additionalContextsSink != null &&
|
|
1661
|
+
settledOutput.additionalContexts.length > 0
|
|
1662
|
+
) {
|
|
1663
|
+
batchContext.additionalContextsSink.push(
|
|
1664
|
+
...settledOutput.additionalContexts
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
if (
|
|
1668
|
+
batchContext.resolvedArgsByCallId != null &&
|
|
1669
|
+
call.id != null &&
|
|
1670
|
+
settledOutput.resolvedArgs != null
|
|
1671
|
+
) {
|
|
1672
|
+
batchContext.resolvedArgsByCallId.set(
|
|
1673
|
+
call.id,
|
|
1674
|
+
settledOutput.resolvedArgs
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
const refMeta = settledOutput.output.additional_kwargs as
|
|
1678
|
+
| t.ToolMessageRefMetadata
|
|
1679
|
+
| undefined;
|
|
1680
|
+
if (
|
|
1681
|
+
this.toolOutputRegistry != null &&
|
|
1682
|
+
refMeta?._refKey != null &&
|
|
1683
|
+
settledOutput.referenceContent != null
|
|
1684
|
+
) {
|
|
1685
|
+
this.toolOutputRegistry.set(
|
|
1686
|
+
refMeta._refScope,
|
|
1687
|
+
refMeta._refKey,
|
|
1688
|
+
settledOutput.referenceContent
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
return settledOutput.output;
|
|
1692
|
+
}
|
|
1693
|
+
const replayAdditionalContexts: string[] = [];
|
|
1694
|
+
const persistOutput = async (
|
|
1695
|
+
output: ToolMessage,
|
|
1696
|
+
terminalArgs?: Record<string, unknown>
|
|
1697
|
+
): Promise<ToolMessage> => {
|
|
1698
|
+
const refMeta = output.additional_kwargs as
|
|
1699
|
+
| t.ToolMessageRefMetadata
|
|
1700
|
+
| undefined;
|
|
1701
|
+
if (
|
|
1702
|
+
terminalArgs != null &&
|
|
1703
|
+
call.id != null &&
|
|
1704
|
+
batchContext.resolvedArgsByCallId != null
|
|
1705
|
+
) {
|
|
1706
|
+
batchContext.resolvedArgsByCallId.set(call.id, terminalArgs);
|
|
1707
|
+
}
|
|
1708
|
+
const resolvedArgs =
|
|
1709
|
+
terminalArgs ??
|
|
1710
|
+
(call.id == null
|
|
1711
|
+
? undefined
|
|
1712
|
+
: batchContext.resolvedArgsByCallId?.get(call.id));
|
|
1713
|
+
const referenceContent =
|
|
1714
|
+
this.toolOutputRegistry == null || refMeta?._refKey == null
|
|
1715
|
+
? undefined
|
|
1716
|
+
: this.toolOutputRegistry.get(refMeta._refScope, refMeta._refKey);
|
|
1717
|
+
await replayController?.persistSettledOutput(call, replayConfig, {
|
|
1718
|
+
output,
|
|
1719
|
+
additionalContexts: replayAdditionalContexts,
|
|
1720
|
+
...(resolvedArgs == null ? {} : { resolvedArgs }),
|
|
1721
|
+
...(referenceContent == null ? {} : { referenceContent }),
|
|
1722
|
+
});
|
|
1723
|
+
return output;
|
|
1724
|
+
};
|
|
1504
1725
|
const runId = (config.configurable?.run_id as string | undefined) ?? '';
|
|
1726
|
+
const threadId = config.configurable?.thread_id as string | undefined;
|
|
1505
1727
|
const hookRegistry = this.hookRegistry;
|
|
1506
1728
|
const hasPreHook = hookRegistry?.hasHookFor('PreToolUse', runId) === true;
|
|
1729
|
+
const approvalReplayKey =
|
|
1730
|
+
call.id == null || call.id === ''
|
|
1731
|
+
? undefined
|
|
1732
|
+
: createToolApprovalReplayKey(
|
|
1733
|
+
config,
|
|
1734
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
1735
|
+
call.id
|
|
1736
|
+
);
|
|
1737
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(
|
|
1738
|
+
config,
|
|
1739
|
+
runId
|
|
1740
|
+
);
|
|
1741
|
+
const pendingApproval =
|
|
1742
|
+
approvalReplayKey == null
|
|
1743
|
+
? undefined
|
|
1744
|
+
: hookRegistry?.getPendingToolApproval(
|
|
1745
|
+
approvalReplaySessionId,
|
|
1746
|
+
approvalReplayKey
|
|
1747
|
+
);
|
|
1507
1748
|
const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
1508
1749
|
const hasFailureHook =
|
|
1509
1750
|
hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
1510
1751
|
|
|
1511
1752
|
if (
|
|
1512
1753
|
hookRegistry == null ||
|
|
1513
|
-
(!hasPreHook &&
|
|
1754
|
+
(!hasPreHook &&
|
|
1755
|
+
pendingApproval == null &&
|
|
1756
|
+
!hasPostHook &&
|
|
1757
|
+
!hasFailureHook)
|
|
1514
1758
|
) {
|
|
1515
|
-
|
|
1759
|
+
const output = await this.runTool(call, replayConfig, batchContext);
|
|
1760
|
+
return output instanceof ToolMessage ? persistOutput(output) : output;
|
|
1516
1761
|
}
|
|
1517
1762
|
|
|
1518
|
-
const threadId = config.configurable?.thread_id as string | undefined;
|
|
1519
1763
|
const registryRunId =
|
|
1520
1764
|
batchContext.batchScopeId ??
|
|
1521
1765
|
(config.configurable?.run_id as string | undefined);
|
|
@@ -1583,7 +1827,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1583
1827
|
}
|
|
1584
1828
|
|
|
1585
1829
|
let effectiveCall = call;
|
|
1586
|
-
if (hasPreHook) {
|
|
1830
|
+
if (hasPreHook || pendingApproval != null) {
|
|
1587
1831
|
const preResult = await executeHooks({
|
|
1588
1832
|
registry: hookRegistry,
|
|
1589
1833
|
input: {
|
|
@@ -1600,6 +1844,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1600
1844
|
},
|
|
1601
1845
|
sessionId: runId,
|
|
1602
1846
|
matchQuery: call.name,
|
|
1847
|
+
onceReplayKey: approvalReplayKey,
|
|
1848
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
1603
1849
|
}).catch(() => undefined);
|
|
1604
1850
|
|
|
1605
1851
|
if (preResult != null) {
|
|
@@ -1607,11 +1853,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1607
1853
|
// the per-batch sink so the caller materializes them as a
|
|
1608
1854
|
// HumanMessage for the next model turn — same shape as the
|
|
1609
1855
|
// event-driven path's `injected[]`. Codex P2 #39.
|
|
1610
|
-
if (
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
) {
|
|
1614
|
-
batchContext.additionalContextsSink.push(
|
|
1856
|
+
if (preResult.additionalContexts.length > 0) {
|
|
1857
|
+
replayAdditionalContexts.push(...preResult.additionalContexts);
|
|
1858
|
+
batchContext.additionalContextsSink?.push(
|
|
1615
1859
|
...preResult.additionalContexts
|
|
1616
1860
|
);
|
|
1617
1861
|
}
|
|
@@ -1626,14 +1870,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1626
1870
|
}
|
|
1627
1871
|
|
|
1628
1872
|
if (preResult.decision === 'deny') {
|
|
1629
|
-
return
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1873
|
+
return persistOutput(
|
|
1874
|
+
this.blockDirectCall({
|
|
1875
|
+
call,
|
|
1876
|
+
resolvedArgs,
|
|
1877
|
+
reason: preResult.reason ?? 'Blocked by hook',
|
|
1878
|
+
hookRegistry,
|
|
1879
|
+
runId,
|
|
1880
|
+
threadId,
|
|
1881
|
+
}),
|
|
1882
|
+
effectiveCall.args as Record<string, unknown>
|
|
1883
|
+
);
|
|
1637
1884
|
}
|
|
1638
1885
|
|
|
1639
1886
|
if (preResult.decision === 'ask') {
|
|
@@ -1644,27 +1891,49 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1644
1891
|
preResult.reason,
|
|
1645
1892
|
call.name
|
|
1646
1893
|
);
|
|
1647
|
-
return
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1894
|
+
return persistOutput(
|
|
1895
|
+
this.blockDirectCall({
|
|
1896
|
+
call,
|
|
1897
|
+
resolvedArgs,
|
|
1898
|
+
reason,
|
|
1899
|
+
hookRegistry,
|
|
1900
|
+
runId,
|
|
1901
|
+
threadId,
|
|
1902
|
+
}),
|
|
1903
|
+
effectiveCall.args as Record<string, unknown>
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
const toolCallId = call.id;
|
|
1907
|
+
if (
|
|
1908
|
+
toolCallId == null ||
|
|
1909
|
+
toolCallId === '' ||
|
|
1910
|
+
approvalReplayKey == null
|
|
1911
|
+
) {
|
|
1912
|
+
return persistOutput(
|
|
1913
|
+
this.blockDirectCall({
|
|
1914
|
+
call,
|
|
1915
|
+
resolvedArgs,
|
|
1916
|
+
reason:
|
|
1917
|
+
'Tool approval requires a non-empty tool call ID — failing closed',
|
|
1918
|
+
hookRegistry,
|
|
1919
|
+
runId,
|
|
1920
|
+
threadId,
|
|
1921
|
+
}),
|
|
1922
|
+
effectiveCall.args as Record<string, unknown>
|
|
1923
|
+
);
|
|
1655
1924
|
}
|
|
1656
1925
|
|
|
1657
1926
|
// Raise a single-tool tool_approval interrupt. LangGraph
|
|
1658
1927
|
// throws on the first execution (host gets the interrupt)
|
|
1659
1928
|
// and returns the resume value on re-entry. Because direct
|
|
1660
|
-
// tools re-enter the entire ToolNode.run on resume
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1663
|
-
// distinguishes "first call asking" from "second call after
|
|
1664
|
-
// approve/reject". We anchor `interrupt()` against the
|
|
1929
|
+
// tools re-enter the entire ToolNode.run on resume. Reusable
|
|
1930
|
+
// hooks fire again; a consumed one-shot hook instead replays
|
|
1931
|
+
// the pending approval. We anchor `interrupt()` against the
|
|
1665
1932
|
// node's RunnableConfig the same way `dispatchToolEvents`
|
|
1666
|
-
// does
|
|
1667
|
-
//
|
|
1933
|
+
// does. A one-shot hook's pending contribution reconstructs this
|
|
1934
|
+
// same ask entry without dispatching the consumed hook again.
|
|
1935
|
+
// ToolNode disables LangSmith tracing, so the AsyncLocalStorage
|
|
1936
|
+
// frame must be re-established here.
|
|
1668
1937
|
const askEntry: AskEntry = {
|
|
1669
1938
|
entry: {
|
|
1670
1939
|
call: effectiveCall,
|
|
@@ -1674,7 +1943,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1674
1943
|
reason: preResult.reason,
|
|
1675
1944
|
allowedDecisions: preResult.allowedDecisions,
|
|
1676
1945
|
};
|
|
1677
|
-
const payload = buildToolApprovalInterruptPayload([askEntry]);
|
|
1946
|
+
const payload = buildToolApprovalInterruptPayload([askEntry], runId);
|
|
1678
1947
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
1679
1948
|
config,
|
|
1680
1949
|
() =>
|
|
@@ -1683,11 +1952,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1683
1952
|
t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap
|
|
1684
1953
|
>(payload)
|
|
1685
1954
|
);
|
|
1955
|
+
hookRegistry.clearPendingToolApproval(
|
|
1956
|
+
approvalReplaySessionId,
|
|
1957
|
+
approvalReplayKey
|
|
1958
|
+
);
|
|
1686
1959
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
1687
|
-
[
|
|
1960
|
+
[toolCallId],
|
|
1688
1961
|
resumeValue
|
|
1689
1962
|
);
|
|
1690
|
-
const decision = decisionByCallId.get(
|
|
1963
|
+
const decision = decisionByCallId.get(toolCallId) ?? {
|
|
1691
1964
|
type: 'reject' as const,
|
|
1692
1965
|
reason: 'No decision provided for tool approval',
|
|
1693
1966
|
};
|
|
@@ -1700,47 +1973,63 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1700
1973
|
declaredType as t.ToolApprovalDecisionType
|
|
1701
1974
|
))
|
|
1702
1975
|
) {
|
|
1703
|
-
return
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1976
|
+
return persistOutput(
|
|
1977
|
+
this.blockDirectCall({
|
|
1978
|
+
call,
|
|
1979
|
+
resolvedArgs,
|
|
1980
|
+
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${preResult.allowedDecisions.join(', ')}] — failing closed`,
|
|
1981
|
+
hookRegistry,
|
|
1982
|
+
runId,
|
|
1983
|
+
threadId,
|
|
1984
|
+
}),
|
|
1985
|
+
effectiveCall.args as Record<string, unknown>
|
|
1986
|
+
);
|
|
1711
1987
|
}
|
|
1712
1988
|
|
|
1713
1989
|
if (decision.type === 'reject') {
|
|
1714
|
-
return
|
|
1715
|
-
|
|
1716
|
-
resolvedArgs,
|
|
1717
|
-
reason: decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
1718
|
-
hookRegistry,
|
|
1719
|
-
runId,
|
|
1720
|
-
threadId,
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
if (decision.type === 'respond') {
|
|
1725
|
-
const responseText = (decision as { responseText?: unknown })
|
|
1726
|
-
.responseText;
|
|
1727
|
-
if (typeof responseText !== 'string') {
|
|
1728
|
-
return this.blockDirectCall({
|
|
1990
|
+
return persistOutput(
|
|
1991
|
+
this.blockDirectCall({
|
|
1729
1992
|
call,
|
|
1730
1993
|
resolvedArgs,
|
|
1731
1994
|
reason:
|
|
1732
|
-
|
|
1995
|
+
decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
1733
1996
|
hookRegistry,
|
|
1734
1997
|
runId,
|
|
1735
1998
|
threadId,
|
|
1736
|
-
})
|
|
1999
|
+
}),
|
|
2000
|
+
effectiveCall.args as Record<string, unknown>
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
if (decision.type === 'respond') {
|
|
2005
|
+
const responseText = (decision as { responseText?: unknown })
|
|
2006
|
+
.responseText;
|
|
2007
|
+
if (typeof responseText !== 'string') {
|
|
2008
|
+
return persistOutput(
|
|
2009
|
+
this.blockDirectCall({
|
|
2010
|
+
call,
|
|
2011
|
+
resolvedArgs,
|
|
2012
|
+
reason:
|
|
2013
|
+
'Approval payload `respond` was missing a string `responseText`',
|
|
2014
|
+
hookRegistry,
|
|
2015
|
+
runId,
|
|
2016
|
+
threadId,
|
|
2017
|
+
}),
|
|
2018
|
+
effectiveCall.args as Record<string, unknown>
|
|
2019
|
+
);
|
|
1737
2020
|
}
|
|
1738
|
-
return
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2021
|
+
return persistOutput(
|
|
2022
|
+
new ToolMessage({
|
|
2023
|
+
status: 'success',
|
|
2024
|
+
content: truncateToolResultContent(
|
|
2025
|
+
responseText,
|
|
2026
|
+
this.maxToolResultChars
|
|
2027
|
+
),
|
|
2028
|
+
name: call.name,
|
|
2029
|
+
tool_call_id: call.id ?? '',
|
|
2030
|
+
}),
|
|
2031
|
+
effectiveCall.args as Record<string, unknown>
|
|
2032
|
+
);
|
|
1744
2033
|
}
|
|
1745
2034
|
|
|
1746
2035
|
if (decision.type === 'edit') {
|
|
@@ -1759,13 +2048,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1759
2048
|
typeof updatedInput !== 'object' ||
|
|
1760
2049
|
Array.isArray(updatedInput)
|
|
1761
2050
|
) {
|
|
1762
|
-
return
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2051
|
+
return persistOutput(
|
|
2052
|
+
new ToolMessage({
|
|
2053
|
+
status: 'error',
|
|
2054
|
+
content:
|
|
2055
|
+
'Decision "edit" missing object updatedInput — failing closed.',
|
|
2056
|
+
name: call.name,
|
|
2057
|
+
tool_call_id: call.id ?? '',
|
|
2058
|
+
}),
|
|
2059
|
+
effectiveCall.args as Record<string, unknown>
|
|
2060
|
+
);
|
|
1769
2061
|
}
|
|
1770
2062
|
effectiveCall = {
|
|
1771
2063
|
...call,
|
|
@@ -1773,6 +2065,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1773
2065
|
};
|
|
1774
2066
|
// fall through to executing the edited call
|
|
1775
2067
|
}
|
|
2068
|
+
if (declaredType !== 'approve' && declaredType !== 'edit') {
|
|
2069
|
+
const unknownType =
|
|
2070
|
+
typeof declaredType === 'string' ? declaredType : '<missing>';
|
|
2071
|
+
return persistOutput(
|
|
2072
|
+
this.blockDirectCall({
|
|
2073
|
+
call,
|
|
2074
|
+
resolvedArgs,
|
|
2075
|
+
reason: `Unknown approval decision type "${unknownType}" — failing closed`,
|
|
2076
|
+
hookRegistry,
|
|
2077
|
+
runId,
|
|
2078
|
+
threadId,
|
|
2079
|
+
}),
|
|
2080
|
+
effectiveCall.args as Record<string, unknown>
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
1776
2083
|
// 'approve' (or 'edit' after applying edits) → fall through
|
|
1777
2084
|
}
|
|
1778
2085
|
}
|
|
@@ -1799,7 +2106,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1799
2106
|
);
|
|
1800
2107
|
}
|
|
1801
2108
|
|
|
1802
|
-
const output = await this.runTool(effectiveCall,
|
|
2109
|
+
const output = await this.runTool(effectiveCall, replayConfig, {
|
|
1803
2110
|
...batchContext,
|
|
1804
2111
|
usageCount,
|
|
1805
2112
|
});
|
|
@@ -1839,14 +2146,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1839
2146
|
}).catch(() => undefined);
|
|
1840
2147
|
if (
|
|
1841
2148
|
failureResult != null &&
|
|
1842
|
-
batchContext.additionalContextsSink != null &&
|
|
1843
2149
|
failureResult.additionalContexts.length > 0
|
|
1844
2150
|
) {
|
|
1845
|
-
|
|
2151
|
+
replayAdditionalContexts.push(...failureResult.additionalContexts);
|
|
2152
|
+
batchContext.additionalContextsSink?.push(
|
|
1846
2153
|
...failureResult.additionalContexts
|
|
1847
2154
|
);
|
|
1848
2155
|
}
|
|
1849
|
-
return output;
|
|
2156
|
+
return persistOutput(output);
|
|
1850
2157
|
}
|
|
1851
2158
|
|
|
1852
2159
|
if (output.status !== 'error' && hasPostHook) {
|
|
@@ -1871,12 +2178,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1871
2178
|
|
|
1872
2179
|
// Forward additionalContexts from the PostToolUse hook into
|
|
1873
2180
|
// the per-batch sink (Codex P2 #39).
|
|
1874
|
-
if (
|
|
1875
|
-
postResult
|
|
1876
|
-
batchContext.additionalContextsSink
|
|
1877
|
-
postResult.additionalContexts.length > 0
|
|
1878
|
-
) {
|
|
1879
|
-
batchContext.additionalContextsSink.push(
|
|
2181
|
+
if (postResult != null && postResult.additionalContexts.length > 0) {
|
|
2182
|
+
replayAdditionalContexts.push(...postResult.additionalContexts);
|
|
2183
|
+
batchContext.additionalContextsSink?.push(
|
|
1880
2184
|
...postResult.additionalContexts
|
|
1881
2185
|
);
|
|
1882
2186
|
}
|
|
@@ -1888,7 +2192,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1888
2192
|
'PostToolUse updatedOutput for a computer call must be a valid screenshot URL or screenshot content block.'
|
|
1889
2193
|
);
|
|
1890
2194
|
}
|
|
1891
|
-
return
|
|
2195
|
+
return persistOutput(
|
|
2196
|
+
cloneToolMessageWithContent(output, postResult.updatedOutput)
|
|
2197
|
+
);
|
|
1892
2198
|
}
|
|
1893
2199
|
// Keep the tool-output registry in sync with what the model
|
|
1894
2200
|
// actually sees. Without this, `runTool` already registered
|
|
@@ -1918,11 +2224,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1918
2224
|
replaced.registryContent
|
|
1919
2225
|
);
|
|
1920
2226
|
}
|
|
1921
|
-
return
|
|
2227
|
+
return persistOutput(
|
|
2228
|
+
cloneToolMessageWithContent(output, replaced.content)
|
|
2229
|
+
);
|
|
1922
2230
|
}
|
|
1923
2231
|
}
|
|
1924
2232
|
|
|
1925
|
-
return output;
|
|
2233
|
+
return persistOutput(output);
|
|
1926
2234
|
}
|
|
1927
2235
|
|
|
1928
2236
|
/**
|
|
@@ -2381,13 +2689,27 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2381
2689
|
errors: [] as string[],
|
|
2382
2690
|
});
|
|
2383
2691
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2692
|
+
const hookRegistry = this.hookRegistry;
|
|
2693
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(
|
|
2694
|
+
config,
|
|
2695
|
+
runId
|
|
2696
|
+
);
|
|
2697
|
+
const hasPendingApproval = preToolCalls.some(
|
|
2698
|
+
(entry) =>
|
|
2699
|
+
entry.call.id != null &&
|
|
2700
|
+
hookRegistry?.getPendingToolApproval(
|
|
2701
|
+
approvalReplaySessionId,
|
|
2702
|
+
createToolApprovalReplayKey(
|
|
2703
|
+
config,
|
|
2704
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
2705
|
+
entry.call.id
|
|
2706
|
+
)
|
|
2707
|
+
) != null
|
|
2708
|
+
);
|
|
2709
|
+
if (
|
|
2710
|
+
hookRegistry != null &&
|
|
2711
|
+
(hookRegistry.hasHookFor('PreToolUse', runId) || hasPendingApproval)
|
|
2712
|
+
) {
|
|
2391
2713
|
/**
|
|
2392
2714
|
* Pull each call's prestarted eager record BEFORE awaiting the hooks:
|
|
2393
2715
|
* an async deny must never race the eager host promise into emitting a
|
|
@@ -2415,8 +2737,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2415
2737
|
}
|
|
2416
2738
|
}
|
|
2417
2739
|
const preResults = await Promise.all(
|
|
2418
|
-
preToolCalls.map((entry) =>
|
|
2419
|
-
|
|
2740
|
+
preToolCalls.map((entry) => {
|
|
2741
|
+
const toolUseId = entry.call.id;
|
|
2742
|
+
const approvalReplayKey =
|
|
2743
|
+
toolUseId == null
|
|
2744
|
+
? undefined
|
|
2745
|
+
: createToolApprovalReplayKey(
|
|
2746
|
+
config,
|
|
2747
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
2748
|
+
toolUseId
|
|
2749
|
+
);
|
|
2750
|
+
return executeHooks({
|
|
2420
2751
|
registry: hookRegistry,
|
|
2421
2752
|
input: {
|
|
2422
2753
|
hook_event_name: 'PreToolUse',
|
|
@@ -2432,8 +2763,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2432
2763
|
},
|
|
2433
2764
|
sessionId: runId,
|
|
2434
2765
|
matchQuery: entry.call.name,
|
|
2435
|
-
|
|
2436
|
-
|
|
2766
|
+
onceReplayKey: approvalReplayKey,
|
|
2767
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
2768
|
+
}).catch((): AggregatedHookResult => HOOK_FALLBACK);
|
|
2769
|
+
})
|
|
2437
2770
|
);
|
|
2438
2771
|
|
|
2439
2772
|
type PendingEntry = (typeof preToolCalls)[number];
|
|
@@ -2653,12 +2986,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2653
2986
|
* pauses, gathers human input, and resumes the run with one
|
|
2654
2987
|
* decision per request. On resume LangGraph re-executes this node
|
|
2655
2988
|
* from the start; `interrupt()` then returns the resume value
|
|
2656
|
-
* instead of throwing
|
|
2657
|
-
*
|
|
2658
|
-
* are pure — see `humanInTheLoop` docs).
|
|
2989
|
+
* instead of throwing. Reusable hooks rebuild their entries, while
|
|
2990
|
+
* consumed one-shot hooks replay their pending approval aggregate.
|
|
2659
2991
|
*/
|
|
2660
2992
|
if (askEntries.length > 0) {
|
|
2661
|
-
const payload = buildToolApprovalInterruptPayload(askEntries);
|
|
2993
|
+
const payload = buildToolApprovalInterruptPayload(askEntries, runId);
|
|
2662
2994
|
|
|
2663
2995
|
/**
|
|
2664
2996
|
* `interrupt()` reads the current `RunnableConfig` from
|
|
@@ -2678,6 +3010,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2678
3010
|
>(payload)
|
|
2679
3011
|
);
|
|
2680
3012
|
|
|
3013
|
+
for (const { entry } of askEntries) {
|
|
3014
|
+
hookRegistry.clearPendingToolApproval(
|
|
3015
|
+
approvalReplaySessionId,
|
|
3016
|
+
createToolApprovalReplayKey(
|
|
3017
|
+
config,
|
|
3018
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
3019
|
+
entry.call.id!
|
|
3020
|
+
)
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2681
3024
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
2682
3025
|
askEntries.map(({ entry }) => entry.call.id!),
|
|
2683
3026
|
resumeValue
|
|
@@ -3051,7 +3394,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3051
3394
|
// match it at the top level too.
|
|
3052
3395
|
agentId: this.executingAgentId,
|
|
3053
3396
|
configurable: stripRunBreakerScope(
|
|
3054
|
-
|
|
3397
|
+
config.configurable as Record<string, unknown> | undefined
|
|
3055
3398
|
),
|
|
3056
3399
|
metadata: config.metadata as
|
|
3057
3400
|
| Record<string, unknown>
|
|
@@ -3680,16 +4023,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3680
4023
|
* call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
|
|
3681
4024
|
* whole ToolNode *before* any non-interrupting sibling has started — so a
|
|
3682
4025
|
* sibling with real side effects (send_email, billing) never executes on
|
|
3683
|
-
* the first pass.
|
|
3684
|
-
*
|
|
3685
|
-
*
|
|
4026
|
+
* the first pass. Terminal interrupting siblings are cached by call id, so
|
|
4027
|
+
* LangGraph replay reuses their complete lifecycle output instead of
|
|
4028
|
+
* repeating model calls, hooks, or side effects.
|
|
3686
4029
|
*
|
|
3687
4030
|
* Without this ordering, a flat `Promise.all` starts every sibling
|
|
3688
4031
|
* concurrently, so a non-idempotent sibling can complete its side effect
|
|
3689
4032
|
* before the interrupt unwinds and then run a SECOND time on resume — the
|
|
3690
|
-
* duplicate side effect this method exists to prevent.
|
|
3691
|
-
*
|
|
3692
|
-
*
|
|
4033
|
+
* duplicate side effect this method exists to prevent. A tool that actually
|
|
4034
|
+
* suspends re-enters until it reaches a terminal result; siblings that
|
|
4035
|
+
* already settled do not re-enter.
|
|
3693
4036
|
*
|
|
3694
4037
|
* `batchIndices[i]` is `directCalls[i]`'s position within the parent
|
|
3695
4038
|
* ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
|
|
@@ -3703,14 +4046,51 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3703
4046
|
config: RunnableConfig,
|
|
3704
4047
|
baseContext: Omit<RunToolBatchContext<T>, 'batchIndex'>
|
|
3705
4048
|
): Promise<(BaseMessage | Command)[]> {
|
|
3706
|
-
const
|
|
4049
|
+
const settledBatchResults =
|
|
4050
|
+
baseContext.replayBatchKey == null
|
|
4051
|
+
? undefined
|
|
4052
|
+
: this.settledInterruptingResults.get(baseContext.replayBatchKey);
|
|
4053
|
+
const restoreResult = (
|
|
4054
|
+
call: ToolCall,
|
|
4055
|
+
result: SettledDirectToolResult
|
|
4056
|
+
): SettledDirectToolResult => {
|
|
4057
|
+
baseContext.additionalContextsSink?.push(...result.additionalContexts);
|
|
4058
|
+
if (
|
|
4059
|
+
call.id != null &&
|
|
4060
|
+
result.resolvedArgs != null &&
|
|
4061
|
+
baseContext.resolvedArgsByCallId != null
|
|
4062
|
+
) {
|
|
4063
|
+
baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
|
|
4064
|
+
}
|
|
4065
|
+
return result;
|
|
4066
|
+
};
|
|
4067
|
+
const runOne = async (
|
|
3707
4068
|
call: ToolCall,
|
|
3708
4069
|
position: number
|
|
3709
|
-
): Promise<
|
|
3710
|
-
|
|
4070
|
+
): Promise<SettledDirectToolResult> => {
|
|
4071
|
+
const cachedResult =
|
|
4072
|
+
typeof call.id === 'string'
|
|
4073
|
+
? settledBatchResults?.get(call.id)
|
|
4074
|
+
: undefined;
|
|
4075
|
+
if (cachedResult != null) {
|
|
4076
|
+
return restoreResult(call, cachedResult);
|
|
4077
|
+
}
|
|
4078
|
+
const additionalContexts: string[] = [];
|
|
4079
|
+
const output = await this.runDirectToolWithLifecycleHooks(call, config, {
|
|
3711
4080
|
...baseContext,
|
|
3712
4081
|
batchIndex: batchIndices[position],
|
|
4082
|
+
additionalContextsSink: additionalContexts,
|
|
4083
|
+
});
|
|
4084
|
+
const resolvedArgs =
|
|
4085
|
+
call.id == null
|
|
4086
|
+
? undefined
|
|
4087
|
+
: baseContext.resolvedArgsByCallId?.get(call.id);
|
|
4088
|
+
return restoreResult(call, {
|
|
4089
|
+
output,
|
|
4090
|
+
additionalContexts,
|
|
4091
|
+
...(resolvedArgs == null ? {} : { resolvedArgs }),
|
|
3713
4092
|
});
|
|
4093
|
+
};
|
|
3714
4094
|
|
|
3715
4095
|
const interrupting = this.interruptingToolNames;
|
|
3716
4096
|
const hasInterrupting =
|
|
@@ -3718,7 +4098,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3718
4098
|
directCalls.some((call) => interrupting.has(call.name));
|
|
3719
4099
|
|
|
3720
4100
|
if (!hasInterrupting) {
|
|
3721
|
-
|
|
4101
|
+
const results = await Promise.all(
|
|
4102
|
+
directCalls.map((call, i) => runOne(call, i))
|
|
4103
|
+
);
|
|
4104
|
+
return results.map((result) => result.output);
|
|
3722
4105
|
}
|
|
3723
4106
|
|
|
3724
4107
|
const outputs: (BaseMessage | Command)[] = new Array(directCalls.length);
|
|
@@ -3732,14 +4115,96 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3732
4115
|
}
|
|
3733
4116
|
}
|
|
3734
4117
|
|
|
3735
|
-
// Interrupting group first.
|
|
3736
|
-
//
|
|
3737
|
-
|
|
4118
|
+
// Interrupting group first. Wait for every sibling to settle before
|
|
4119
|
+
// propagating an interrupt so approved subagents cannot keep running in
|
|
4120
|
+
// the background while the parent exposes the next pending approval.
|
|
4121
|
+
const interruptingResults = await Promise.allSettled(
|
|
3738
4122
|
interruptingPositions.map((i) => runOne(directCalls[i], i))
|
|
3739
4123
|
);
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
4124
|
+
/** A sibling may have tripped the run-wide breaker while another raised
|
|
4125
|
+
* a GraphInterrupt. Safety failures take precedence over approval pauses
|
|
4126
|
+
* regardless of input order. */
|
|
4127
|
+
this.throwIfBreakerTripped(config);
|
|
4128
|
+
let hasInterruptingError = false;
|
|
4129
|
+
let interruptingError: unknown;
|
|
4130
|
+
for (let i = 0; i < interruptingResults.length; i++) {
|
|
4131
|
+
const result = interruptingResults[i];
|
|
4132
|
+
if (result.status === 'rejected') {
|
|
4133
|
+
if (!hasInterruptingError) {
|
|
4134
|
+
hasInterruptingError = true;
|
|
4135
|
+
interruptingError = result.reason;
|
|
4136
|
+
}
|
|
4137
|
+
continue;
|
|
4138
|
+
}
|
|
4139
|
+
const position = interruptingPositions[i];
|
|
4140
|
+
outputs[position] = result.value.output;
|
|
4141
|
+
const callId = directCalls[position].id;
|
|
4142
|
+
if (
|
|
4143
|
+
baseContext.replayBatchKey != null &&
|
|
4144
|
+
typeof callId === 'string' &&
|
|
4145
|
+
callId !== ''
|
|
4146
|
+
) {
|
|
4147
|
+
const batchResults =
|
|
4148
|
+
this.settledInterruptingResults.get(baseContext.replayBatchKey) ??
|
|
4149
|
+
new Map<string, SettledDirectToolResult>();
|
|
4150
|
+
batchResults.set(callId, result.value);
|
|
4151
|
+
this.settledInterruptingResults.set(
|
|
4152
|
+
baseContext.replayBatchKey,
|
|
4153
|
+
batchResults
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
if (hasInterruptingError) {
|
|
4158
|
+
if (isGraphInterrupt(interruptingError)) {
|
|
4159
|
+
const controllers = new Set<
|
|
4160
|
+
NonNullable<ReplayableSubagentTool[typeof SUBAGENT_REPLAY_CONTROLLER]>
|
|
4161
|
+
>();
|
|
4162
|
+
const parentToolCallIds = new Set<string>();
|
|
4163
|
+
for (const call of directCalls) {
|
|
4164
|
+
const controller = (
|
|
4165
|
+
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
4166
|
+
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
4167
|
+
if (controller?.getResumeManifest != null) {
|
|
4168
|
+
controllers.add(controller);
|
|
4169
|
+
}
|
|
4170
|
+
if (call.id != null && call.id !== '') {
|
|
4171
|
+
parentToolCallIds.add(call.id);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
const executionsByParentCall = new Map<
|
|
4175
|
+
string,
|
|
4176
|
+
SubagentResumeManifest['executions'][number]
|
|
4177
|
+
>();
|
|
4178
|
+
for (const controller of controllers) {
|
|
4179
|
+
const manifest = await controller.getResumeManifest?.(
|
|
4180
|
+
parentToolCallIds,
|
|
4181
|
+
withSubagentReplayBatch(config, baseContext.replayBatchKey)
|
|
4182
|
+
);
|
|
4183
|
+
if (manifest != null) {
|
|
4184
|
+
for (const execution of manifest.executions) {
|
|
4185
|
+
executionsByParentCall.set(execution.parentToolCallId, execution);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
const executions = [...executionsByParentCall.values()];
|
|
4190
|
+
if (executions.length > 0) {
|
|
4191
|
+
const manifest: SubagentResumeManifest = {
|
|
4192
|
+
version: 1,
|
|
4193
|
+
executions,
|
|
4194
|
+
};
|
|
4195
|
+
throw new GraphInterrupt(
|
|
4196
|
+
interruptingError.interrupts.map((pendingInterrupt) => ({
|
|
4197
|
+
...pendingInterrupt,
|
|
4198
|
+
value: attachSubagentResumeManifest(
|
|
4199
|
+
pendingInterrupt.value,
|
|
4200
|
+
manifest
|
|
4201
|
+
),
|
|
4202
|
+
}))
|
|
4203
|
+
);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
throw interruptingError;
|
|
4207
|
+
}
|
|
3743
4208
|
|
|
3744
4209
|
/** The breaker can trip while the interrupting group is awaited — e.g.
|
|
3745
4210
|
* a tool that ignores cancellation and completes normally. Recheck
|
|
@@ -3751,7 +4216,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3751
4216
|
regularPositions.map((i) => runOne(directCalls[i], i))
|
|
3752
4217
|
);
|
|
3753
4218
|
regularPositions.forEach((i, k) => {
|
|
3754
|
-
outputs[i] = regularOutputs[k];
|
|
4219
|
+
outputs[i] = regularOutputs[k].output;
|
|
3755
4220
|
});
|
|
3756
4221
|
|
|
3757
4222
|
return outputs;
|
|
@@ -3842,9 +4307,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3842
4307
|
* every subsequent registry call on this batch.
|
|
3843
4308
|
*/
|
|
3844
4309
|
const incomingRunId = config.configurable?.run_id as string | undefined;
|
|
4310
|
+
const incomingThreadId = config.configurable?.thread_id as
|
|
4311
|
+
| string
|
|
4312
|
+
| undefined;
|
|
3845
4313
|
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
3846
4314
|
const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
3847
4315
|
let outputs: (BaseMessage | Command)[];
|
|
4316
|
+
let replayBatchKey: string | undefined;
|
|
3848
4317
|
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
3849
4318
|
* the promotion into handoff updates (same-id state copies there would
|
|
3850
4319
|
* otherwise overwrite the replacement message). */
|
|
@@ -3872,6 +4341,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3872
4341
|
// Mirror langgraph's prebuilt ToolNode: the Send-input state is
|
|
3873
4342
|
// the input minus the `lg_tool_call` envelope key.
|
|
3874
4343
|
const { lg_tool_call: _sendToolCall, ...sendState } = input;
|
|
4344
|
+
const sendMessages = (sendState as { messages?: unknown }).messages;
|
|
4345
|
+
const sendAssistantBatch = Array.isArray(sendMessages)
|
|
4346
|
+
? findAssistantBatch(sendMessages as BaseMessage[])
|
|
4347
|
+
: undefined;
|
|
4348
|
+
const sendReplayBatchKey =
|
|
4349
|
+
sendAssistantBatch == null
|
|
4350
|
+
? undefined
|
|
4351
|
+
: getAssistantBatchReplayKey(
|
|
4352
|
+
sendAssistantBatch,
|
|
4353
|
+
incomingRunId,
|
|
4354
|
+
incomingThreadId
|
|
4355
|
+
);
|
|
3875
4356
|
const sendOutput = await this.runDirectToolWithLifecycleHooks(
|
|
3876
4357
|
input.lg_tool_call,
|
|
3877
4358
|
config,
|
|
@@ -3882,6 +4363,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3882
4363
|
resolvedArgsByCallId,
|
|
3883
4364
|
errorOwnership,
|
|
3884
4365
|
additionalContextsSink: directAdditionalContexts,
|
|
4366
|
+
replayBatchKey: sendReplayBatchKey,
|
|
3885
4367
|
runInput: sendState as T,
|
|
3886
4368
|
}
|
|
3887
4369
|
);
|
|
@@ -3925,18 +4407,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3925
4407
|
.map((msg) => (msg as ToolMessage).tool_call_id)
|
|
3926
4408
|
);
|
|
3927
4409
|
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
const message = messages[i];
|
|
3931
|
-
if (isAIMessage(message)) {
|
|
3932
|
-
aiMessage = message;
|
|
3933
|
-
break;
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3936
|
-
|
|
3937
|
-
if (aiMessage == null || !isAIMessage(aiMessage)) {
|
|
4410
|
+
const assistantBatch = findAssistantBatch(messages);
|
|
4411
|
+
if (assistantBatch == null) {
|
|
3938
4412
|
throw new Error('ToolNode only accepts AIMessages as input.');
|
|
3939
4413
|
}
|
|
4414
|
+
const aiMessage = assistantBatch.message;
|
|
4415
|
+
replayBatchKey = getAssistantBatchReplayKey(
|
|
4416
|
+
assistantBatch,
|
|
4417
|
+
incomingRunId,
|
|
4418
|
+
incomingThreadId
|
|
4419
|
+
);
|
|
3940
4420
|
|
|
3941
4421
|
if (this.loadRuntimeTools) {
|
|
3942
4422
|
const { tools, toolMap } = this.loadRuntimeTools(
|
|
@@ -3999,9 +4479,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3999
4479
|
: (aiMessage.invalid_tool_calls ?? []).filter(
|
|
4000
4480
|
(call) =>
|
|
4001
4481
|
call.id != null &&
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4482
|
+
call.id !== '' &&
|
|
4483
|
+
!toolMessageIds.has(call.id) &&
|
|
4484
|
+
!call.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
|
|
4005
4485
|
);
|
|
4006
4486
|
invalidCallResults = attributableInvalidCalls.map(
|
|
4007
4487
|
(call) =>
|
|
@@ -4156,6 +4636,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4156
4636
|
errorOwnership,
|
|
4157
4637
|
preBatchSnapshot,
|
|
4158
4638
|
additionalContextsSink: directAdditionalContexts,
|
|
4639
|
+
replayBatchKey,
|
|
4159
4640
|
runInput: input as T,
|
|
4160
4641
|
}
|
|
4161
4642
|
)
|
|
@@ -4228,6 +4709,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4228
4709
|
errorOwnership,
|
|
4229
4710
|
preBatchSnapshot,
|
|
4230
4711
|
additionalContextsSink: directAdditionalContexts,
|
|
4712
|
+
replayBatchKey,
|
|
4231
4713
|
runInput: input as T,
|
|
4232
4714
|
}
|
|
4233
4715
|
);
|
|
@@ -4240,7 +4722,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4240
4722
|
);
|
|
4241
4723
|
// Append accumulated additionalContexts as a single
|
|
4242
4724
|
// HumanMessage so the next model turn sees them. Codex P2 #39.
|
|
4243
|
-
const promotedPrefix =
|
|
4725
|
+
const promotedPrefix =
|
|
4726
|
+
promotedAiMessage != null ? [promotedAiMessage] : [];
|
|
4244
4727
|
outputs =
|
|
4245
4728
|
directAdditionalContexts.length > 0
|
|
4246
4729
|
? [
|
|
@@ -4283,6 +4766,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4283
4766
|
}
|
|
4284
4767
|
|
|
4285
4768
|
if (!outputs.some(isCommand)) {
|
|
4769
|
+
if (replayBatchKey != null) {
|
|
4770
|
+
this.settledInterruptingResults.delete(replayBatchKey);
|
|
4771
|
+
}
|
|
4286
4772
|
return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
|
|
4287
4773
|
}
|
|
4288
4774
|
|
|
@@ -4301,8 +4787,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4301
4787
|
isCommand(output)
|
|
4302
4788
|
? patchCommandUpdateForPromotedInvalidCalls(
|
|
4303
4789
|
output,
|
|
4304
|
-
|
|
4305
|
-
|
|
4790
|
+
promotedAiMessage!,
|
|
4791
|
+
invalidCallResults
|
|
4306
4792
|
)
|
|
4307
4793
|
: output
|
|
4308
4794
|
);
|
|
@@ -4423,6 +4909,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4423
4909
|
combinedOutputs.push(parentCommand);
|
|
4424
4910
|
}
|
|
4425
4911
|
|
|
4912
|
+
if (replayBatchKey != null) {
|
|
4913
|
+
this.settledInterruptingResults.delete(replayBatchKey);
|
|
4914
|
+
}
|
|
4426
4915
|
return combinedOutputs as T;
|
|
4427
4916
|
}
|
|
4428
4917
|
|
|
@@ -4498,8 +4987,7 @@ function sanitizeInvalidToolUseBlocks(
|
|
|
4498
4987
|
/** `name` normalizes with the SAME fallback the promoted `tool_calls`
|
|
4499
4988
|
* entry uses — a nameless block would fail provider validation on its
|
|
4500
4989
|
* own even with a valid input. */
|
|
4501
|
-
const nameIsValid =
|
|
4502
|
-
typeof toolUse.name === 'string' && toolUse.name !== '';
|
|
4990
|
+
const nameIsValid = typeof toolUse.name === 'string' && toolUse.name !== '';
|
|
4503
4991
|
if (inputIsObject && nameIsValid) {
|
|
4504
4992
|
return block;
|
|
4505
4993
|
}
|
|
@@ -4571,9 +5059,7 @@ function patchCommandUpdateForPromotedInvalidCalls(
|
|
|
4571
5059
|
if (!isAIMessage(msg) || msg.id !== promoted.id) {
|
|
4572
5060
|
return msg;
|
|
4573
5061
|
}
|
|
4574
|
-
const existingIds = new Set(
|
|
4575
|
-
(msg.tool_calls ?? []).map((call) => call.id)
|
|
4576
|
-
);
|
|
5062
|
+
const existingIds = new Set((msg.tool_calls ?? []).map((call) => call.id));
|
|
4577
5063
|
const promotedEntries = invalidResults
|
|
4578
5064
|
.filter((result) => !existingIds.has(result.tool_call_id))
|
|
4579
5065
|
.map((result) => ({
|