@librechat/agents 3.8.0 → 3.8.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 +13 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +176 -55
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +16 -6
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hitl/approvalReview.cjs +89 -0
- package/dist/cjs/hitl/approvalReview.cjs.map +1 -0
- package/dist/cjs/langfuse.cjs +42 -0
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +42 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +7 -1
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +6 -10
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +2 -7
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/contextPressureMeter.cjs +51 -10
- package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/llm/openai/index.cjs +4 -3
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +5 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/preempt.cjs +3 -2
- package/dist/cjs/llm/preempt.cjs.map +1 -1
- package/dist/cjs/llm/prepareProviderRequest.cjs +9 -7
- package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs +1 -1
- package/dist/cjs/main.cjs +14 -8
- package/dist/cjs/messages/alternation.cjs +1 -5
- package/dist/cjs/messages/alternation.cjs.map +1 -1
- package/dist/cjs/messages/budget.cjs +206 -7
- package/dist/cjs/messages/budget.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +3 -9
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +26 -38
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +132 -43
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +2 -0
- package/dist/cjs/messages/prune.cjs +14 -9
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/reasoningTypes.cjs +21 -0
- package/dist/cjs/messages/reasoningTypes.cjs.map +1 -0
- package/dist/cjs/messages/recency.cjs +15 -94
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/messages/toolHistoryProjection.cjs +243 -0
- package/dist/cjs/messages/toolHistoryProjection.cjs.map +1 -0
- package/dist/cjs/messages/toolResultTypes.cjs +145 -4
- package/dist/cjs/messages/toolResultTypes.cjs.map +1 -1
- package/dist/cjs/run.cjs +22 -10
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +7 -5
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +3 -0
- package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
- package/dist/cjs/session/index.cjs +1 -1
- package/dist/cjs/session/sessionProjection.cjs +75 -0
- package/dist/cjs/session/sessionProjection.cjs.map +1 -0
- package/dist/cjs/stream.cjs +20 -19
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/index.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +19 -9
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/summarization/shared.cjs +9 -0
- package/dist/cjs/summarization/shared.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +256 -84
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +24 -9
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/tools/toolBatchReplay.cjs +187 -0
- package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +12 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/types/hitl.cjs +4 -0
- package/dist/cjs/types/hitl.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +13 -0
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +105 -0
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +13 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +178 -57
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +16 -6
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hitl/approvalReview.mjs +83 -0
- package/dist/esm/hitl/approvalReview.mjs.map +1 -0
- package/dist/esm/langfuse.mjs +43 -2
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +41 -1
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +7 -1
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +6 -10
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +2 -7
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/contextPressureMeter.mjs +52 -11
- package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/llm/openai/index.mjs +2 -1
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +5 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/preempt.mjs +2 -1
- package/dist/esm/llm/preempt.mjs.map +1 -1
- package/dist/esm/llm/prepareProviderRequest.mjs +9 -7
- package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs +1 -1
- package/dist/esm/main.mjs +13 -10
- package/dist/esm/messages/alternation.mjs +2 -6
- package/dist/esm/messages/alternation.mjs.map +1 -1
- package/dist/esm/messages/budget.mjs +206 -8
- package/dist/esm/messages/budget.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +3 -9
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +24 -34
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +133 -44
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +2 -0
- package/dist/esm/messages/prune.mjs +14 -9
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/reasoningTypes.mjs +20 -0
- package/dist/esm/messages/reasoningTypes.mjs.map +1 -0
- package/dist/esm/messages/recency.mjs +16 -95
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/messages/toolHistoryProjection.mjs +237 -0
- package/dist/esm/messages/toolHistoryProjection.mjs.map +1 -0
- package/dist/esm/messages/toolResultTypes.mjs +141 -4
- package/dist/esm/messages/toolResultTypes.mjs.map +1 -1
- package/dist/esm/run.mjs +23 -11
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +7 -5
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +3 -0
- package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
- package/dist/esm/session/index.mjs +1 -1
- package/dist/esm/session/sessionProjection.mjs +72 -0
- package/dist/esm/session/sessionProjection.mjs.map +1 -0
- package/dist/esm/stream.mjs +17 -16
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/index.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +20 -10
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/summarization/shared.mjs +9 -1
- package/dist/esm/summarization/shared.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +256 -84
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +24 -9
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/tools/toolBatchReplay.mjs +176 -0
- package/dist/esm/tools/toolBatchReplay.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +12 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/types/hitl.mjs +4 -1
- package/dist/esm/types/hitl.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +13 -0
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +105 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +13 -1
- package/dist/types/graphs/Graph.d.ts +27 -0
- package/dist/types/hitl/approvalReview.d.ts +32 -0
- package/dist/types/hooks/types.d.ts +4 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/langfuse.d.ts +5 -0
- package/dist/types/langfuseToolOutputTracing.d.ts +2 -0
- package/dist/types/llm/contextPressureMeter.d.ts +2 -1
- package/dist/types/llm/prepareProviderRequest.d.ts +7 -1
- package/dist/types/messages/budget.d.ts +15 -10
- package/dist/types/messages/core.d.ts +8 -17
- package/dist/types/messages/format.d.ts +3 -2
- package/dist/types/messages/index.d.ts +1 -1
- package/dist/types/messages/prune.d.ts +1 -1
- package/dist/types/messages/reasoningTypes.d.ts +7 -0
- package/dist/types/messages/recency.d.ts +3 -0
- package/dist/types/messages/toolHistoryProjection.d.ts +65 -0
- package/dist/types/messages/toolResultTypes.d.ts +31 -1
- package/dist/types/session/sessionProjection.d.ts +10 -0
- package/dist/types/summarization/index.d.ts +2 -1
- package/dist/types/summarization/node.d.ts +1 -0
- package/dist/types/summarization/shared.d.ts +12 -0
- package/dist/types/tools/ToolNode.d.ts +12 -6
- package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -0
- package/dist/types/tools/toolBatchReplay.d.ts +54 -0
- package/dist/types/tools/toolOutputReferences.d.ts +2 -0
- package/dist/types/types/graph.d.ts +20 -0
- package/dist/types/types/run.d.ts +4 -0
- package/dist/types/types/summarize.d.ts +4 -1
- package/dist/types/utils/tokens.d.ts +2 -1
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +34 -3
- package/src/graphs/Graph.ts +465 -147
- package/src/graphs/MultiAgentGraph.ts +28 -6
- package/src/hitl/approvalReview.ts +209 -0
- package/src/hooks/types.ts +4 -1
- package/src/index.ts +1 -0
- package/src/langfuse.ts +67 -9
- package/src/langfuseToolOutputTracing.ts +91 -0
- package/src/langfuseTraceShaping.ts +17 -4
- package/src/llm/anthropic/utils/message_inputs.ts +5 -10
- package/src/llm/bedrock/utils/message_inputs.ts +2 -8
- package/src/llm/contextPressureMeter.ts +80 -12
- package/src/llm/openai/utils/index.ts +5 -4
- package/src/llm/prepareProviderRequest.ts +21 -16
- package/src/messages/alternation.ts +2 -15
- package/src/messages/budget.ts +439 -23
- package/src/messages/cache.ts +8 -9
- package/src/messages/core.ts +65 -95
- package/src/messages/format.ts +262 -78
- package/src/messages/index.ts +1 -1
- package/src/messages/prune.ts +31 -12
- package/src/messages/reasoningTypes.ts +23 -0
- package/src/messages/recency.ts +35 -179
- package/src/messages/toolHistoryProjection.ts +460 -0
- package/src/messages/toolResultTypes.ts +331 -100
- package/src/run.ts +44 -22
- package/src/session/AgentSession.ts +33 -16
- package/src/session/JsonlSessionStore.ts +6 -0
- package/src/session/sessionProjection.ts +126 -0
- package/src/stream.ts +14 -19
- package/src/summarization/index.ts +2 -0
- package/src/summarization/node.ts +62 -13
- package/src/summarization/shared.ts +23 -0
- package/src/tools/ToolNode.ts +518 -184
- package/src/tools/subagent/SubagentExecutor.ts +35 -6
- package/src/tools/subagent/SubagentReplay.ts +2 -2
- package/src/tools/toolBatchReplay.ts +395 -0
- package/src/tools/toolOutputReferences.ts +16 -0
- package/src/types/graph.ts +20 -0
- package/src/types/run.ts +4 -0
- package/src/types/summarize.ts +4 -1
- package/src/utils/events.ts +19 -0
- package/src/utils/tokens.ts +183 -0
package/src/tools/ToolNode.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
2
3
|
import { ToolCall } from '@langchain/core/messages/tool';
|
|
3
4
|
import { RunnableLambda } from '@langchain/core/runnables';
|
|
4
5
|
import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
|
|
@@ -31,6 +32,7 @@ import type { LangGraphRunnableConfig } from '@langchain/langgraph';
|
|
|
31
32
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
32
33
|
import type {
|
|
33
34
|
ToolOutputResolveView,
|
|
35
|
+
ToolOutputReferenceState,
|
|
34
36
|
PreResolvedArgsMap,
|
|
35
37
|
ResolvedArgsByCallId,
|
|
36
38
|
ResolveResult,
|
|
@@ -48,6 +50,7 @@ import type {
|
|
|
48
50
|
ToolApprovalReplayKey,
|
|
49
51
|
} from '@/hooks';
|
|
50
52
|
import type { PreparedSubagents } from '@/tools/preparedSubagents';
|
|
53
|
+
import type { SettledToolBatchResult } from '@/tools/toolBatchReplay';
|
|
51
54
|
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
52
55
|
import type * as t from '@/types';
|
|
53
56
|
import {
|
|
@@ -67,12 +70,20 @@ import {
|
|
|
67
70
|
serializeStructuredValueBounded,
|
|
68
71
|
serializeToolContentBounded,
|
|
69
72
|
} from '@/utils/toolContent';
|
|
73
|
+
import {
|
|
74
|
+
getReviewedToolApproval,
|
|
75
|
+
getToolApprovalReviewEvidence,
|
|
76
|
+
toolApprovalPayloadMatches,
|
|
77
|
+
toolApprovalProposalMatches,
|
|
78
|
+
TOOL_APPROVAL_REVIEW_CONFIG_KEY,
|
|
79
|
+
} from '@/hitl/approvalReview';
|
|
70
80
|
import {
|
|
71
81
|
buildToolExecutionRequestPlan,
|
|
72
82
|
coerceArgsForSchema,
|
|
73
83
|
coerceRecordArgs,
|
|
74
84
|
resolveRuntimeSessionHint,
|
|
75
85
|
recordArgsEqual,
|
|
86
|
+
stableStringify,
|
|
76
87
|
} from '@/tools/eagerEventExecution';
|
|
77
88
|
import {
|
|
78
89
|
attachSubagentResumeManifest,
|
|
@@ -110,16 +121,47 @@ import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
|
110
121
|
import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
|
|
111
122
|
import { PreparedSubagentError } from '@/tools/preparedSubagents';
|
|
112
123
|
import { attachRunStepResumeState } from '@/tools/runStepResume';
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
import {
|
|
125
|
+
TOOL_BATCH_REPLAY_KEY,
|
|
126
|
+
getToolBatchReplayOwner,
|
|
127
|
+
getToolBatchReplayScope,
|
|
128
|
+
attachToolBatchReplayState,
|
|
129
|
+
restoreToolBatchReplayState,
|
|
130
|
+
getToolBatchReplayState,
|
|
131
|
+
isChildToolApprovalOwner,
|
|
132
|
+
} from '@/tools/toolBatchReplay';
|
|
133
|
+
|
|
134
|
+
function stripToolApprovalReviewConfig(
|
|
117
135
|
configurable: Record<string, unknown> | undefined
|
|
118
136
|
): Record<string, unknown> | undefined {
|
|
119
|
-
if (
|
|
137
|
+
if (
|
|
138
|
+
configurable == null ||
|
|
139
|
+
!(TOOL_APPROVAL_REVIEW_CONFIG_KEY in configurable) &&
|
|
140
|
+
!(TOOL_BATCH_REPLAY_KEY in configurable)
|
|
141
|
+
) {
|
|
120
142
|
return configurable;
|
|
121
143
|
}
|
|
122
|
-
const {
|
|
144
|
+
const {
|
|
145
|
+
[TOOL_APPROVAL_REVIEW_CONFIG_KEY]: _review,
|
|
146
|
+
[TOOL_BATCH_REPLAY_KEY]: _replay,
|
|
147
|
+
...rest
|
|
148
|
+
} = configurable;
|
|
149
|
+
return rest;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Host-facing requests must not carry SDK-private execution state — hosts
|
|
153
|
+
* spread `configurable` into their own run configs. */
|
|
154
|
+
function stripHostExecutionConfig(
|
|
155
|
+
configurable: Record<string, unknown> | undefined
|
|
156
|
+
): Record<string, unknown> | undefined {
|
|
157
|
+
const withoutReview = stripToolApprovalReviewConfig(configurable);
|
|
158
|
+
if (
|
|
159
|
+
withoutReview == null ||
|
|
160
|
+
!(RUN_BREAKER_SCOPE_CONFIG_KEY in withoutReview)
|
|
161
|
+
) {
|
|
162
|
+
return withoutReview;
|
|
163
|
+
}
|
|
164
|
+
const { [RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = withoutReview;
|
|
123
165
|
return rest;
|
|
124
166
|
}
|
|
125
167
|
import { convertInjectedMessages } from '@/messages/injected';
|
|
@@ -209,6 +251,10 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
209
251
|
additionalContextsSink?: string[];
|
|
210
252
|
/** Stable identity of the assistant tool-call batch across HITL replay. */
|
|
211
253
|
replayBatchKey?: string;
|
|
254
|
+
/** Positions in the original proposal, before completed calls are filtered. */
|
|
255
|
+
replayCallPositions?: ReadonlyMap<ToolCall, number>;
|
|
256
|
+
/** Tool-call identities owned by this ToolNode invocation. */
|
|
257
|
+
approvalScopeCallIds?: ReadonlySet<string>;
|
|
212
258
|
/**
|
|
213
259
|
* Graph state the ToolNode was invoked with, threaded from `run()`
|
|
214
260
|
* so `tool.invoke` can forward it as langgraph 1.4's `runtime.state`
|
|
@@ -236,11 +282,14 @@ function withSubagentReplayBatch(
|
|
|
236
282
|
};
|
|
237
283
|
}
|
|
238
284
|
|
|
239
|
-
type SettledDirectToolResult =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
285
|
+
type SettledDirectToolResult = SettledToolBatchResult;
|
|
286
|
+
|
|
287
|
+
/** Positional identity is safe within the immutable full-proposal batch key. */
|
|
288
|
+
function directReplayKey(call: ToolCall, batchIndex: number): string {
|
|
289
|
+
return call.id == null || call.id === ''
|
|
290
|
+
? JSON.stringify(['position', batchIndex])
|
|
291
|
+
: JSON.stringify(['id', call.id]);
|
|
292
|
+
}
|
|
244
293
|
|
|
245
294
|
/**
|
|
246
295
|
* Batch-local record of who owns each failed call's completion event.
|
|
@@ -403,8 +452,6 @@ function describeOfferedShape(value: unknown): string {
|
|
|
403
452
|
|
|
404
453
|
type AssistantBatch = {
|
|
405
454
|
message: AIMessage;
|
|
406
|
-
index: number;
|
|
407
|
-
messageCount: number;
|
|
408
455
|
};
|
|
409
456
|
|
|
410
457
|
function findAssistantBatch(
|
|
@@ -413,7 +460,7 @@ function findAssistantBatch(
|
|
|
413
460
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
414
461
|
const message = messages[i];
|
|
415
462
|
if (isAIMessage(message)) {
|
|
416
|
-
return { message
|
|
463
|
+
return { message };
|
|
417
464
|
}
|
|
418
465
|
}
|
|
419
466
|
return undefined;
|
|
@@ -421,15 +468,12 @@ function findAssistantBatch(
|
|
|
421
468
|
|
|
422
469
|
function getAssistantBatchReplayKey(
|
|
423
470
|
batch: AssistantBatch,
|
|
424
|
-
runId: string | undefined,
|
|
425
471
|
threadId: string | undefined
|
|
426
472
|
): string {
|
|
427
473
|
return JSON.stringify([
|
|
428
|
-
runId ?? null,
|
|
429
474
|
threadId ?? null,
|
|
430
|
-
batch.message.id
|
|
431
|
-
batch.
|
|
432
|
-
batch.messageCount,
|
|
475
|
+
batch.message.id == null || batch.message.id === '' ? null : batch.message.id,
|
|
476
|
+
createHash('sha256').update(stableStringify(batch.message.tool_calls ?? [])).digest('hex'),
|
|
433
477
|
]);
|
|
434
478
|
}
|
|
435
479
|
|
|
@@ -697,10 +741,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
697
741
|
* the Run ends.
|
|
698
742
|
*/
|
|
699
743
|
private directPathTurns: Map<string, number> = new Map();
|
|
700
|
-
/** Terminal results
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
private
|
|
744
|
+
/** Terminal direct results that must survive a LangGraph replay of the
|
|
745
|
+
* containing ToolNode. Includes the sidecar data the fresh batch needs for
|
|
746
|
+
* hook-context injection and completion-event deduplication. */
|
|
747
|
+
private settledDirectResultsByBatch = new Map<
|
|
704
748
|
string,
|
|
705
749
|
Map<string, SettledDirectToolResult>
|
|
706
750
|
>();
|
|
@@ -854,23 +898,78 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
854
898
|
name: name ?? TOOL_NODE_RUN_NAME,
|
|
855
899
|
tags,
|
|
856
900
|
func: async (input, config) => {
|
|
901
|
+
const messages = Array.isArray(input) ? input as BaseMessage[] :
|
|
902
|
+
(input as { messages?: BaseMessage[] }).messages;
|
|
903
|
+
const assistantBatch = messages == null ? undefined : findAssistantBatch(messages);
|
|
904
|
+
const activeReplayKey = assistantBatch == null || getToolBatchReplayScope(config) == null ? undefined : getAssistantBatchReplayKey(
|
|
905
|
+
assistantBatch, getToolBatchReplayScope(config)
|
|
906
|
+
);
|
|
907
|
+
const replayOwner = getToolBatchReplayOwner(
|
|
908
|
+
config,
|
|
909
|
+
this.executingAgentId ?? this.agentId ?? ''
|
|
910
|
+
);
|
|
911
|
+
const reviewEvidence = getToolApprovalReviewEvidence(config);
|
|
912
|
+
if (reviewEvidence?.owner != null && reviewEvidence.owner !== replayOwner) {
|
|
913
|
+
const calls = this.isSendInput(input) ? [input.lg_tool_call] : assistantBatch?.message.tool_calls ?? [];
|
|
914
|
+
const trustedParentCallIds = new Set(calls.filter((call) =>
|
|
915
|
+
call.name === Constants.SUBAGENT &&
|
|
916
|
+
(this.toolMap.get(call.name) as ReplayableSubagentTool | undefined)?.[SUBAGENT_REPLAY_CONTROLLER] != null
|
|
917
|
+
).map((call) => call.id ?? ''));
|
|
918
|
+
const ownsParentBatch = getToolBatchReplayState(config.configurable)?.records.some(
|
|
919
|
+
(record) => record.owner === replayOwner && record.batch === activeReplayKey
|
|
920
|
+
) === true;
|
|
921
|
+
if (!ownsParentBatch || !isChildToolApprovalOwner(config, reviewEvidence.owner, trustedParentCallIds)) {
|
|
922
|
+
throw new Error('Tool approval execution owner changed before resume — failing closed');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const referenceReplay: { state?: ToolOutputReferenceState } = {};
|
|
926
|
+
const restoredBatches = await restoreToolBatchReplayState(config, replayOwner);
|
|
927
|
+
if (restoredBatches.length > 0 && !restoredBatches.some(({ batch }) => batch === activeReplayKey)) {
|
|
928
|
+
throw new Error('Tool batch proposal changed before approval replay — failing closed');
|
|
929
|
+
}
|
|
930
|
+
if (activeReplayKey != null && getToolBatchReplayState(config.configurable) != null) {
|
|
931
|
+
this.settledDirectResultsByBatch.delete(activeReplayKey);
|
|
932
|
+
}
|
|
933
|
+
for (const { batch, results, turnState, referenceState } of restoredBatches) {
|
|
934
|
+
if (batch !== activeReplayKey) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
this.settledDirectResultsByBatch.set(batch, new Map(results));
|
|
938
|
+
if (turnState != null) {
|
|
939
|
+
this.restoreSubagentResumeState(turnState);
|
|
940
|
+
}
|
|
941
|
+
referenceReplay.state = referenceState;
|
|
942
|
+
}
|
|
857
943
|
const state = input as T & Pick<t.BaseGraphState, 'runStepState'>;
|
|
858
944
|
restoreRunStepResumeState?.(state.runStepState, config);
|
|
859
945
|
let result: T;
|
|
860
946
|
try {
|
|
861
|
-
result = await this.run(input, config);
|
|
947
|
+
result = await this.run(input, config, referenceReplay);
|
|
862
948
|
} catch (error) {
|
|
863
|
-
if (!isGraphInterrupt(error)
|
|
949
|
+
if (!isGraphInterrupt(error)) {
|
|
864
950
|
throw error;
|
|
865
951
|
}
|
|
866
|
-
const resumeState = createRunStepResumeState();
|
|
952
|
+
const resumeState = createRunStepResumeState?.();
|
|
953
|
+
const activeResults = activeReplayKey == null ? undefined : this.settledDirectResultsByBatch.get(activeReplayKey);
|
|
954
|
+
const activeBatches = activeReplayKey == null
|
|
955
|
+
? new Map<string, Map<string, SettledDirectToolResult>>()
|
|
956
|
+
: new Map([[activeReplayKey, activeResults ?? new Map<string, SettledDirectToolResult>()]]);
|
|
957
|
+
const turnState = this.createSubagentResumeState();
|
|
958
|
+
const activeCallIds = new Set(assistantBatch?.message.tool_calls?.map((call) => call.id));
|
|
959
|
+
turnState.directPathTurns = turnState.directPathTurns.filter(({ toolCallId }) => activeCallIds.has(toolCallId));
|
|
867
960
|
throw new GraphInterrupt(
|
|
868
|
-
error.interrupts.map((pendingInterrupt) =>
|
|
869
|
-
|
|
870
|
-
value: attachRunStepResumeState(
|
|
961
|
+
await Promise.all(error.interrupts.map(async (pendingInterrupt) => {
|
|
962
|
+
const value = await attachToolBatchReplayState(
|
|
871
963
|
pendingInterrupt.value,
|
|
872
|
-
|
|
873
|
-
|
|
964
|
+
replayOwner,
|
|
965
|
+
activeBatches,
|
|
966
|
+
turnState,
|
|
967
|
+
referenceReplay.state
|
|
968
|
+
);
|
|
969
|
+
return {
|
|
970
|
+
...pendingInterrupt,
|
|
971
|
+
value: resumeState == null ? value : attachRunStepResumeState(value, resumeState),
|
|
972
|
+
};
|
|
874
973
|
}))
|
|
875
974
|
);
|
|
876
975
|
}
|
|
@@ -1146,14 +1245,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1146
1245
|
*/
|
|
1147
1246
|
clearDirectPathTurns(): void {
|
|
1148
1247
|
this.directPathTurns.clear();
|
|
1149
|
-
this.
|
|
1248
|
+
this.settledDirectResultsByBatch.clear();
|
|
1150
1249
|
}
|
|
1151
1250
|
|
|
1152
1251
|
/** Returns the live caller projection used by direct and event execution. */
|
|
1153
1252
|
private getCallerCapabilityProjection(): CallerCapabilityProjection {
|
|
1154
|
-
const discoveredToolNames = new Set(
|
|
1155
|
-
this.getDiscoveredToolNames?.() ?? []
|
|
1156
|
-
);
|
|
1253
|
+
const discoveredToolNames = new Set(this.getDiscoveredToolNames?.() ?? []);
|
|
1157
1254
|
return resolveCallerCapabilityProjection(
|
|
1158
1255
|
mergeCallerCapabilityDefinitions(
|
|
1159
1256
|
this.toolDefinitions?.values(),
|
|
@@ -1194,9 +1291,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1194
1291
|
private getProgrammaticTools(): t.ProgrammaticCache {
|
|
1195
1292
|
const toolMap: t.ToolMap = new Map();
|
|
1196
1293
|
const toolDefs: t.LCTool[] = [];
|
|
1197
|
-
const discoveredToolNames = new Set(
|
|
1198
|
-
this.getDiscoveredToolNames?.() ?? []
|
|
1199
|
-
);
|
|
1294
|
+
const discoveredToolNames = new Set(this.getDiscoveredToolNames?.() ?? []);
|
|
1200
1295
|
const executableCapabilities = resolveCallerCapabilityProjection(
|
|
1201
1296
|
this.toolRegistry?.values() ?? [],
|
|
1202
1297
|
(toolDef) => isToolDefinitionActive(toolDef, discoveredToolNames)
|
|
@@ -1307,11 +1402,22 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1307
1402
|
runInput?: RunToolBatchContext<T>['runInput']
|
|
1308
1403
|
): Promise<unknown> {
|
|
1309
1404
|
const lgConfig = config as LangGraphRunnableConfig;
|
|
1310
|
-
const
|
|
1405
|
+
const isTrustedSubagentHop =
|
|
1406
|
+
call.name === Constants.SUBAGENT &&
|
|
1407
|
+
(tool as ReplayableSubagentTool)[SUBAGENT_REPLAY_CONTROLLER] != null;
|
|
1408
|
+
const toolConfig: RunnableConfig = {
|
|
1311
1409
|
...config,
|
|
1410
|
+
configurable: isTrustedSubagentHop
|
|
1411
|
+
? config.configurable
|
|
1412
|
+
: stripToolApprovalReviewConfig(
|
|
1413
|
+
config.configurable as Record<string, unknown> | undefined
|
|
1414
|
+
),
|
|
1415
|
+
};
|
|
1416
|
+
const runtime: ToolRuntime<T> = {
|
|
1417
|
+
...toolConfig,
|
|
1312
1418
|
state: runInput as ToolRuntime<T>['state'],
|
|
1313
1419
|
toolCallId: call.id ?? '',
|
|
1314
|
-
config,
|
|
1420
|
+
config: toolConfig,
|
|
1315
1421
|
context: lgConfig.context as ToolRuntime<T>['context'],
|
|
1316
1422
|
store: (lgConfig.store ?? null) as ToolRuntime<T>['store'],
|
|
1317
1423
|
writer:
|
|
@@ -1533,6 +1639,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1533
1639
|
}
|
|
1534
1640
|
return next;
|
|
1535
1641
|
})();
|
|
1642
|
+
this.recordToolUsageTurn(call.name, usageCount, call.id);
|
|
1643
|
+
if (batchContext.replayBatchKey != null && call.id != null && call.id !== '') {
|
|
1644
|
+
this.directPathTurns.set(call.id, usageCount);
|
|
1645
|
+
}
|
|
1536
1646
|
let args = call.args;
|
|
1537
1647
|
if (resolveFn != null) {
|
|
1538
1648
|
const { resolved, unresolved } = resolveFn(runId, args);
|
|
@@ -1915,17 +2025,22 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1915
2025
|
batchContext: RunToolBatchContext<T> = {}
|
|
1916
2026
|
): Promise<BaseMessage | Command> {
|
|
1917
2027
|
if (
|
|
1918
|
-
this.preparedSubagents?.owns(this.executingAgentId ?? '', call) ===
|
|
2028
|
+
this.preparedSubagents?.owns(this.executingAgentId ?? '', call) ===
|
|
2029
|
+
true &&
|
|
1919
2030
|
this.hookRegistry?.hasResultAlteringHooks(
|
|
1920
2031
|
(config.configurable?.run_id as string | undefined) ?? ''
|
|
1921
2032
|
) === true
|
|
1922
2033
|
) {
|
|
1923
2034
|
this.preparedSubagents.clear();
|
|
1924
|
-
throw new PreparedSubagentError(
|
|
2035
|
+
throw new PreparedSubagentError(
|
|
2036
|
+
'Tool hooks changed after a subagent started.'
|
|
2037
|
+
);
|
|
1925
2038
|
}
|
|
1926
2039
|
const replayController = (
|
|
1927
2040
|
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
1928
2041
|
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
2042
|
+
const isTrustedSubagentReviewHop =
|
|
2043
|
+
call.name === Constants.SUBAGENT && replayController != null;
|
|
1929
2044
|
const replayConfig =
|
|
1930
2045
|
replayController == null
|
|
1931
2046
|
? config
|
|
@@ -2024,16 +2139,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2024
2139
|
approvalReplaySessionId,
|
|
2025
2140
|
approvalReplayKey
|
|
2026
2141
|
);
|
|
2142
|
+
const approvalReviewEvidence = getToolApprovalReviewEvidence(config,
|
|
2143
|
+
getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ''));
|
|
2144
|
+
const reviewedApproval = getReviewedToolApproval(
|
|
2145
|
+
approvalReviewEvidence?.payload,
|
|
2146
|
+
call.id
|
|
2147
|
+
);
|
|
2148
|
+
const approvalEvidenceAppliesToBatch =
|
|
2149
|
+
approvalReviewEvidence?.payload.action_requests.some((request) =>
|
|
2150
|
+
batchContext.approvalScopeCallIds == null
|
|
2151
|
+
? request.tool_call_id === call.id
|
|
2152
|
+
: batchContext.approvalScopeCallIds.has(request.tool_call_id)
|
|
2153
|
+
) === true;
|
|
2027
2154
|
const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
2028
2155
|
const hasFailureHook =
|
|
2029
2156
|
hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
2030
2157
|
|
|
2031
2158
|
if (
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2159
|
+
!hasPreHook &&
|
|
2160
|
+
pendingApproval == null &&
|
|
2161
|
+
reviewedApproval == null &&
|
|
2162
|
+
approvalReviewEvidence == null &&
|
|
2163
|
+
!hasPostHook &&
|
|
2164
|
+
!hasFailureHook
|
|
2037
2165
|
) {
|
|
2038
2166
|
const output = await this.runTool(call, replayConfig, batchContext);
|
|
2039
2167
|
return output instanceof ToolMessage ? persistOutput(output) : output;
|
|
@@ -2086,46 +2214,74 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2086
2214
|
// value the PreToolUse hook observes matches the value the
|
|
2087
2215
|
// (later-awaited) `runTool` will actually run with — both are
|
|
2088
2216
|
// anchored to the pre-batch registry state.
|
|
2089
|
-
let resolvedArgs = call.args as Record<string, unknown>;
|
|
2090
2217
|
const hookResolveOptions = {
|
|
2091
2218
|
substituteIntentKey: this.toolDeclaresBusinessIntent(call.name),
|
|
2092
2219
|
};
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2220
|
+
const resolveDirectArgs = (
|
|
2221
|
+
args: Record<string, unknown>
|
|
2222
|
+
): Record<string, unknown> => {
|
|
2223
|
+
if (batchContext.preBatchSnapshot != null) {
|
|
2224
|
+
return batchContext.preBatchSnapshot.resolve(args, hookResolveOptions)
|
|
2225
|
+
.resolved as Record<string, unknown>;
|
|
2226
|
+
}
|
|
2227
|
+
if (this.toolOutputRegistry != null) {
|
|
2228
|
+
return this.toolOutputRegistry.resolve(
|
|
2229
|
+
registryRunId,
|
|
2230
|
+
args,
|
|
2231
|
+
hookResolveOptions
|
|
2232
|
+
).resolved as Record<string, unknown>;
|
|
2233
|
+
}
|
|
2234
|
+
return args;
|
|
2235
|
+
};
|
|
2236
|
+
const originalResolvedArgs = resolveDirectArgs(
|
|
2237
|
+
call.args as Record<string, unknown>
|
|
2238
|
+
);
|
|
2239
|
+
/** Reusable and pending hooks must replay from the original proposal so
|
|
2240
|
+
* non-idempotent rewrites are applied exactly once. A rebuilt runtime
|
|
2241
|
+
* without that policy instead falls back to the resolved arguments the
|
|
2242
|
+
* user actually reviewed. */
|
|
2243
|
+
let resolvedArgs =
|
|
2244
|
+
hasPreHook || pendingApproval != null
|
|
2245
|
+
? originalResolvedArgs
|
|
2246
|
+
: (reviewedApproval?.request.arguments ?? originalResolvedArgs);
|
|
2107
2247
|
|
|
2108
2248
|
let effectiveCall = call;
|
|
2109
|
-
if (
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2249
|
+
if (
|
|
2250
|
+
hasPreHook ||
|
|
2251
|
+
pendingApproval != null ||
|
|
2252
|
+
reviewedApproval != null ||
|
|
2253
|
+
approvalReviewEvidence != null
|
|
2254
|
+
) {
|
|
2255
|
+
const preResult: AggregatedHookResult | undefined =
|
|
2256
|
+
hookRegistry == null
|
|
2257
|
+
? {
|
|
2258
|
+
additionalContexts: [],
|
|
2259
|
+
injectedMessages: [],
|
|
2260
|
+
errors: [],
|
|
2261
|
+
}
|
|
2262
|
+
: await executeHooks({
|
|
2263
|
+
registry: hookRegistry,
|
|
2264
|
+
input: {
|
|
2265
|
+
hook_event_name: 'PreToolUse',
|
|
2266
|
+
runId,
|
|
2267
|
+
threadId,
|
|
2268
|
+
agentId: this.agentId,
|
|
2269
|
+
executingAgentId: this.executingAgentId,
|
|
2270
|
+
toolName: call.name,
|
|
2271
|
+
toolInput: resolvedArgs,
|
|
2272
|
+
toolUseId: call.id ?? '',
|
|
2273
|
+
stepId,
|
|
2274
|
+
turn,
|
|
2275
|
+
},
|
|
2276
|
+
sessionId: runId,
|
|
2277
|
+
matchQuery: call.name,
|
|
2278
|
+
onceReplayKey: approvalReplayKey,
|
|
2279
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
2280
|
+
}).catch((): AggregatedHookResult | undefined => approvalReviewEvidence == null ? undefined : {
|
|
2281
|
+
decision: 'deny',
|
|
2282
|
+
reason: 'Approval policy could not be evaluated on resume',
|
|
2283
|
+
additionalContexts: [], injectedMessages: [], errors: [],
|
|
2284
|
+
});
|
|
2129
2285
|
|
|
2130
2286
|
if (preResult != null) {
|
|
2131
2287
|
// Forward any additionalContext strings hooks returned into
|
|
@@ -2142,9 +2298,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2142
2298
|
// a valid combination (one matcher sanitises args, another asks
|
|
2143
2299
|
// for approval); the reviewer should see the sanitised args.
|
|
2144
2300
|
if (preResult.updatedInput != null) {
|
|
2301
|
+
resolvedArgs = resolveDirectArgs(
|
|
2302
|
+
preResult.updatedInput as Record<string, unknown>
|
|
2303
|
+
);
|
|
2304
|
+
effectiveCall = {
|
|
2305
|
+
...call,
|
|
2306
|
+
args: resolvedArgs,
|
|
2307
|
+
};
|
|
2308
|
+
} else if (reviewedApproval != null) {
|
|
2309
|
+
resolvedArgs = reviewedApproval.request.arguments;
|
|
2145
2310
|
effectiveCall = {
|
|
2146
2311
|
...call,
|
|
2147
|
-
args:
|
|
2312
|
+
args: resolvedArgs,
|
|
2148
2313
|
};
|
|
2149
2314
|
}
|
|
2150
2315
|
|
|
@@ -2162,7 +2327,30 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2162
2327
|
);
|
|
2163
2328
|
}
|
|
2164
2329
|
|
|
2165
|
-
|
|
2330
|
+
/** A sibling absent from restored evidence may have raised a parallel
|
|
2331
|
+
* approval that LangGraph did not preserve. A current hook can ask
|
|
2332
|
+
* again; otherwise never let that unreviewed sibling execute. */
|
|
2333
|
+
if (
|
|
2334
|
+
approvalEvidenceAppliesToBatch &&
|
|
2335
|
+
reviewedApproval == null &&
|
|
2336
|
+
!isTrustedSubagentReviewHop &&
|
|
2337
|
+
preResult.decision !== 'ask'
|
|
2338
|
+
) {
|
|
2339
|
+
return persistOutput(
|
|
2340
|
+
this.blockDirectCall({
|
|
2341
|
+
call,
|
|
2342
|
+
resolvedArgs,
|
|
2343
|
+
reason:
|
|
2344
|
+
'Tool call was not included in the restored approval batch — failing closed',
|
|
2345
|
+
hookRegistry,
|
|
2346
|
+
runId,
|
|
2347
|
+
threadId,
|
|
2348
|
+
}),
|
|
2349
|
+
resolvedArgs
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
if (preResult.decision === 'ask' || reviewedApproval != null) {
|
|
2166
2354
|
if (this.humanInTheLoop?.enabled !== true) {
|
|
2167
2355
|
// Fail-closed: no HITL UI configured, so we can't actually
|
|
2168
2356
|
// ask. Logged once via the existing helper.
|
|
@@ -2213,14 +2401,25 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2213
2401
|
// same ask entry without dispatching the consumed hook again.
|
|
2214
2402
|
// ToolNode disables LangSmith tracing, so the AsyncLocalStorage
|
|
2215
2403
|
// frame must be re-established here.
|
|
2404
|
+
const allowedDecisions =
|
|
2405
|
+
preResult.decision === 'ask'
|
|
2406
|
+
? preResult.allowedDecisions
|
|
2407
|
+
: reviewedApproval?.reviewConfig.allowed_decisions;
|
|
2408
|
+
const askReason =
|
|
2409
|
+
preResult.decision === 'ask'
|
|
2410
|
+
? preResult.reason
|
|
2411
|
+
: reviewedApproval?.request.description;
|
|
2412
|
+
if (effectiveCall === call) {
|
|
2413
|
+
effectiveCall = { ...call, args: resolvedArgs };
|
|
2414
|
+
}
|
|
2216
2415
|
const askEntry: AskEntry = {
|
|
2217
2416
|
entry: {
|
|
2218
2417
|
call: effectiveCall,
|
|
2219
2418
|
args: effectiveCall.args as Record<string, unknown>,
|
|
2220
2419
|
stepId,
|
|
2221
2420
|
},
|
|
2222
|
-
reason:
|
|
2223
|
-
allowedDecisions
|
|
2421
|
+
reason: askReason,
|
|
2422
|
+
allowedDecisions,
|
|
2224
2423
|
};
|
|
2225
2424
|
const payload = buildToolApprovalInterruptPayload([askEntry], runId);
|
|
2226
2425
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
@@ -2231,10 +2430,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2231
2430
|
t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap
|
|
2232
2431
|
>(payload)
|
|
2233
2432
|
);
|
|
2234
|
-
hookRegistry
|
|
2433
|
+
hookRegistry?.clearPendingToolApproval(
|
|
2235
2434
|
approvalReplaySessionId,
|
|
2236
2435
|
approvalReplayKey
|
|
2237
2436
|
);
|
|
2437
|
+
const currentApproval = getReviewedToolApproval(payload, toolCallId);
|
|
2438
|
+
if (
|
|
2439
|
+
reviewedApproval != null &&
|
|
2440
|
+
(currentApproval == null ||
|
|
2441
|
+
!toolApprovalProposalMatches(currentApproval, reviewedApproval))
|
|
2442
|
+
) {
|
|
2443
|
+
return persistOutput(
|
|
2444
|
+
this.blockDirectCall({
|
|
2445
|
+
call,
|
|
2446
|
+
resolvedArgs,
|
|
2447
|
+
reason:
|
|
2448
|
+
'Reviewed tool proposal changed before resume; retry approval',
|
|
2449
|
+
hookRegistry,
|
|
2450
|
+
runId,
|
|
2451
|
+
threadId,
|
|
2452
|
+
}),
|
|
2453
|
+
effectiveCall.args as Record<string, unknown>
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2238
2456
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
2239
2457
|
[toolCallId],
|
|
2240
2458
|
resumeValue
|
|
@@ -2246,9 +2464,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2246
2464
|
const declaredType = (decision as { type?: unknown }).type;
|
|
2247
2465
|
|
|
2248
2466
|
if (
|
|
2249
|
-
|
|
2467
|
+
allowedDecisions != null &&
|
|
2250
2468
|
(typeof declaredType !== 'string' ||
|
|
2251
|
-
!
|
|
2469
|
+
!allowedDecisions.includes(
|
|
2252
2470
|
declaredType as t.ToolApprovalDecisionType
|
|
2253
2471
|
))
|
|
2254
2472
|
) {
|
|
@@ -2256,7 +2474,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2256
2474
|
this.blockDirectCall({
|
|
2257
2475
|
call,
|
|
2258
2476
|
resolvedArgs,
|
|
2259
|
-
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${
|
|
2477
|
+
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${allowedDecisions.join(', ')}] — failing closed`,
|
|
2260
2478
|
hookRegistry,
|
|
2261
2479
|
runId,
|
|
2262
2480
|
threadId,
|
|
@@ -2270,8 +2488,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2270
2488
|
this.blockDirectCall({
|
|
2271
2489
|
call,
|
|
2272
2490
|
resolvedArgs,
|
|
2273
|
-
reason:
|
|
2274
|
-
decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
2491
|
+
reason: decision.reason ?? askReason ?? 'Rejected by user',
|
|
2275
2492
|
hookRegistry,
|
|
2276
2493
|
runId,
|
|
2277
2494
|
threadId,
|
|
@@ -2546,12 +2763,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2546
2763
|
call: ToolCall;
|
|
2547
2764
|
resolvedArgs: Record<string, unknown>;
|
|
2548
2765
|
reason: string;
|
|
2549
|
-
hookRegistry: HookRegistry;
|
|
2766
|
+
hookRegistry: HookRegistry | undefined;
|
|
2550
2767
|
runId: string;
|
|
2551
2768
|
threadId: string | undefined;
|
|
2552
2769
|
}): ToolMessage {
|
|
2553
2770
|
const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
|
|
2554
|
-
if (hookRegistry
|
|
2771
|
+
if (hookRegistry?.hasHookFor('PermissionDenied', runId) === true) {
|
|
2555
2772
|
executeHooks({
|
|
2556
2773
|
registry: hookRegistry,
|
|
2557
2774
|
input: {
|
|
@@ -2881,7 +3098,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2881
3098
|
const signal = config.signal;
|
|
2882
3099
|
if (
|
|
2883
3100
|
signal?.aborted === true &&
|
|
2884
|
-
(signal.reason instanceof StreamLimitExceededError ||
|
|
3101
|
+
(signal.reason instanceof StreamLimitExceededError ||
|
|
3102
|
+
signal.reason instanceof PreparedSubagentError)
|
|
2885
3103
|
) {
|
|
2886
3104
|
throw signal.reason;
|
|
2887
3105
|
}
|
|
@@ -2932,11 +3150,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2932
3150
|
unresolvedByCallId.set(call.id, pre.unresolved);
|
|
2933
3151
|
}
|
|
2934
3152
|
} else if (registry != null) {
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2937
|
-
originalArgs,
|
|
2938
|
-
|
|
2939
|
-
);
|
|
3153
|
+
const options = { substituteIntentKey: this.toolDeclaresBusinessIntent(call.name) };
|
|
3154
|
+
const { resolved, unresolved } = preBatchSnapshot != null
|
|
3155
|
+
? preBatchSnapshot.resolve(originalArgs, options)
|
|
3156
|
+
: registry.resolve(registryRunId, originalArgs, options);
|
|
2940
3157
|
resolvedArgs = resolved as Record<string, unknown>;
|
|
2941
3158
|
if (unresolved.length > 0 && call.id != null) {
|
|
2942
3159
|
unresolvedByCallId.set(call.id, unresolved);
|
|
@@ -2984,6 +3201,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2984
3201
|
config,
|
|
2985
3202
|
runId
|
|
2986
3203
|
);
|
|
3204
|
+
const approvalReviewEvidence = getToolApprovalReviewEvidence(config,
|
|
3205
|
+
getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ''));
|
|
2987
3206
|
const hasPendingApproval = preToolCalls.some(
|
|
2988
3207
|
(entry) =>
|
|
2989
3208
|
entry.call.id != null &&
|
|
@@ -2997,8 +3216,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2997
3216
|
) != null
|
|
2998
3217
|
);
|
|
2999
3218
|
if (
|
|
3000
|
-
hookRegistry
|
|
3001
|
-
|
|
3219
|
+
hookRegistry?.hasHookFor('PreToolUse', runId) === true ||
|
|
3220
|
+
hasPendingApproval ||
|
|
3221
|
+
approvalReviewEvidence != null
|
|
3002
3222
|
) {
|
|
3003
3223
|
/**
|
|
3004
3224
|
* Pull each call's prestarted eager record BEFORE awaiting the hooks:
|
|
@@ -3026,38 +3246,41 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3026
3246
|
}
|
|
3027
3247
|
}
|
|
3028
3248
|
}
|
|
3029
|
-
const preResults =
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3249
|
+
const preResults =
|
|
3250
|
+
hookRegistry == null
|
|
3251
|
+
? preToolCalls.map(() => HOOK_FALLBACK)
|
|
3252
|
+
: await Promise.all(
|
|
3253
|
+
preToolCalls.map((entry) => {
|
|
3254
|
+
const toolUseId = entry.call.id;
|
|
3255
|
+
const approvalReplayKey =
|
|
3256
|
+
toolUseId == null
|
|
3257
|
+
? undefined
|
|
3258
|
+
: createToolApprovalReplayKey(
|
|
3259
|
+
config,
|
|
3260
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
3261
|
+
toolUseId
|
|
3262
|
+
);
|
|
3263
|
+
return executeHooks({
|
|
3264
|
+
registry: hookRegistry,
|
|
3265
|
+
input: {
|
|
3266
|
+
hook_event_name: 'PreToolUse',
|
|
3267
|
+
runId,
|
|
3268
|
+
threadId,
|
|
3269
|
+
agentId: this.agentId,
|
|
3270
|
+
executingAgentId: this.executingAgentId,
|
|
3271
|
+
toolName: entry.call.name,
|
|
3272
|
+
toolInput: entry.args,
|
|
3273
|
+
toolUseId: entry.call.id!,
|
|
3274
|
+
stepId: entry.stepId,
|
|
3275
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0,
|
|
3276
|
+
},
|
|
3277
|
+
sessionId: runId,
|
|
3278
|
+
matchQuery: entry.call.name,
|
|
3279
|
+
onceReplayKey: approvalReplayKey,
|
|
3280
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
3281
|
+
}).catch((): AggregatedHookResult => HOOK_FALLBACK);
|
|
3282
|
+
})
|
|
3283
|
+
);
|
|
3061
3284
|
|
|
3062
3285
|
type PendingEntry = (typeof preToolCalls)[number];
|
|
3063
3286
|
|
|
@@ -3138,7 +3361,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3138
3361
|
item.contentString,
|
|
3139
3362
|
config
|
|
3140
3363
|
);
|
|
3141
|
-
if (hookRegistry
|
|
3364
|
+
if (hookRegistry?.hasHookFor('PermissionDenied', runId) === true) {
|
|
3142
3365
|
executeHooks({
|
|
3143
3366
|
registry: hookRegistry,
|
|
3144
3367
|
input: {
|
|
@@ -3252,6 +3475,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3252
3475
|
*/
|
|
3253
3476
|
if (hookResult.updatedInput != null) {
|
|
3254
3477
|
applyInputOverride(entry, hookResult.updatedInput);
|
|
3478
|
+
} else {
|
|
3479
|
+
const reviewed = getReviewedToolApproval(approvalReviewEvidence?.payload, entry.call.id);
|
|
3480
|
+
if (reviewed != null) {
|
|
3481
|
+
entry.args = this.coerceEventToolArgs(entry.call.name, reviewed.request.arguments);
|
|
3482
|
+
if (entry.call.id != null) {
|
|
3483
|
+
unresolvedByCallId.delete(entry.call.id);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3255
3486
|
}
|
|
3256
3487
|
askEntries.push({
|
|
3257
3488
|
entry,
|
|
@@ -3264,6 +3495,27 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3264
3495
|
if (hookResult.updatedInput != null) {
|
|
3265
3496
|
applyInputOverride(entry, hookResult.updatedInput);
|
|
3266
3497
|
}
|
|
3498
|
+
const reviewedApproval = getReviewedToolApproval(
|
|
3499
|
+
approvalReviewEvidence?.payload,
|
|
3500
|
+
entry.call.id
|
|
3501
|
+
);
|
|
3502
|
+
if (reviewedApproval != null) {
|
|
3503
|
+
if (hookResult.updatedInput == null) {
|
|
3504
|
+
entry.args = this.coerceEventToolArgs(
|
|
3505
|
+
entry.call.name,
|
|
3506
|
+
reviewedApproval.request.arguments
|
|
3507
|
+
);
|
|
3508
|
+
if (entry.call.id != null) {
|
|
3509
|
+
unresolvedByCallId.delete(entry.call.id);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
askEntries.push({
|
|
3513
|
+
entry,
|
|
3514
|
+
reason: reviewedApproval.request.description,
|
|
3515
|
+
allowedDecisions: reviewedApproval.reviewConfig.allowed_decisions,
|
|
3516
|
+
});
|
|
3517
|
+
continue;
|
|
3518
|
+
}
|
|
3267
3519
|
if (entry.call.id != null) {
|
|
3268
3520
|
const preempted = preemptedEagerRecords.get(entry.call.id);
|
|
3269
3521
|
if (preempted != null) {
|
|
@@ -3304,7 +3556,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3304
3556
|
);
|
|
3305
3557
|
|
|
3306
3558
|
for (const { entry } of askEntries) {
|
|
3307
|
-
hookRegistry
|
|
3559
|
+
hookRegistry?.clearPendingToolApproval(
|
|
3308
3560
|
approvalReplaySessionId,
|
|
3309
3561
|
createToolApprovalReplayKey(
|
|
3310
3562
|
config,
|
|
@@ -3314,8 +3566,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3314
3566
|
);
|
|
3315
3567
|
}
|
|
3316
3568
|
|
|
3569
|
+
const proposalMatches =
|
|
3570
|
+
approvalReviewEvidence == null ||
|
|
3571
|
+
toolApprovalPayloadMatches(payload, approvalReviewEvidence.payload);
|
|
3572
|
+
if (!proposalMatches) {
|
|
3573
|
+
for (const { entry } of askEntries) {
|
|
3574
|
+
blockEntry(
|
|
3575
|
+
entry,
|
|
3576
|
+
'Reviewed tool proposal changed before resume; retry approval'
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
const decisionEntries = proposalMatches ? askEntries : [];
|
|
3317
3581
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
3318
|
-
|
|
3582
|
+
decisionEntries.map(({ entry }) => entry.call.id!),
|
|
3319
3583
|
resumeValue
|
|
3320
3584
|
);
|
|
3321
3585
|
|
|
@@ -3323,7 +3587,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3323
3587
|
entry,
|
|
3324
3588
|
reason: askReason,
|
|
3325
3589
|
allowedDecisions,
|
|
3326
|
-
} of
|
|
3590
|
+
} of decisionEntries) {
|
|
3327
3591
|
const decision = decisionByCallId.get(entry.call.id!) ?? {
|
|
3328
3592
|
type: 'reject' as const,
|
|
3329
3593
|
reason: 'No decision provided for tool approval',
|
|
@@ -3688,8 +3952,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3688
3952
|
// match it at the top level too.
|
|
3689
3953
|
agentId: this.executingAgentId,
|
|
3690
3954
|
callerCapabilityProjection:
|
|
3691
|
-
|
|
3692
|
-
configurable:
|
|
3955
|
+
this.getCallerCapabilityProjectionSnapshot(),
|
|
3956
|
+
configurable: stripHostExecutionConfig(
|
|
3693
3957
|
config.configurable as Record<string, unknown> | undefined
|
|
3694
3958
|
),
|
|
3695
3959
|
metadata: config.metadata as
|
|
@@ -4348,12 +4612,44 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4348
4612
|
const settledBatchResults =
|
|
4349
4613
|
baseContext.replayBatchKey == null
|
|
4350
4614
|
? undefined
|
|
4351
|
-
: this.
|
|
4615
|
+
: (this.settledDirectResultsByBatch.get(baseContext.replayBatchKey) ??
|
|
4616
|
+
new Map<string, SettledDirectToolResult>());
|
|
4617
|
+
const cacheResult = (
|
|
4618
|
+
call: ToolCall,
|
|
4619
|
+
position: number,
|
|
4620
|
+
result: SettledDirectToolResult
|
|
4621
|
+
): void => {
|
|
4622
|
+
if (
|
|
4623
|
+
baseContext.replayBatchKey == null ||
|
|
4624
|
+
settledBatchResults == null
|
|
4625
|
+
) {
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
settledBatchResults.set(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]), result);
|
|
4629
|
+
this.settledDirectResultsByBatch.set(
|
|
4630
|
+
baseContext.replayBatchKey,
|
|
4631
|
+
settledBatchResults
|
|
4632
|
+
);
|
|
4633
|
+
};
|
|
4352
4634
|
const restoreResult = (
|
|
4353
4635
|
call: ToolCall,
|
|
4354
4636
|
result: SettledDirectToolResult
|
|
4355
4637
|
): SettledDirectToolResult => {
|
|
4356
4638
|
baseContext.additionalContextsSink?.push(...result.additionalContexts);
|
|
4639
|
+
const turn = result.turn;
|
|
4640
|
+
if (turn != null) {
|
|
4641
|
+
this.recordToolUsageTurn(call.name, turn, call.id);
|
|
4642
|
+
}
|
|
4643
|
+
const refMeta = isBaseMessage(result.output) ? result.output.additional_kwargs as t.ToolMessageRefMetadata : undefined;
|
|
4644
|
+
if (refMeta?._refKey != null && result.referenceContent != null) {
|
|
4645
|
+
this.toolOutputRegistry?.set(baseContext.batchScopeId, refMeta._refKey, result.referenceContent);
|
|
4646
|
+
if (isBaseMessage(result.output)) {
|
|
4647
|
+
result = { ...result, output: new ToolMessage({
|
|
4648
|
+
...(result.output as ToolMessage),
|
|
4649
|
+
additional_kwargs: { ...result.output.additional_kwargs, _refScope: baseContext.batchScopeId },
|
|
4650
|
+
}) };
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4357
4653
|
if (
|
|
4358
4654
|
call.id != null &&
|
|
4359
4655
|
result.resolvedArgs != null &&
|
|
@@ -4367,11 +4663,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4367
4663
|
call: ToolCall,
|
|
4368
4664
|
position: number
|
|
4369
4665
|
): Promise<SettledDirectToolResult> => {
|
|
4370
|
-
const cachedResult =
|
|
4371
|
-
typeof call.id === 'string'
|
|
4372
|
-
? settledBatchResults?.get(call.id)
|
|
4373
|
-
: undefined;
|
|
4666
|
+
const cachedResult = settledBatchResults?.get(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]));
|
|
4374
4667
|
if (cachedResult != null) {
|
|
4668
|
+
if (cachedResult.proposal.name !== call.name ||
|
|
4669
|
+
!recordArgsEqual(cachedResult.proposal.args, call.args as Record<string, unknown>)) {
|
|
4670
|
+
throw new Error('Cannot change an already completed tool call during approval replay');
|
|
4671
|
+
}
|
|
4375
4672
|
return restoreResult(call, cachedResult);
|
|
4376
4673
|
}
|
|
4377
4674
|
const additionalContexts: string[] = [];
|
|
@@ -4384,11 +4681,19 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4384
4681
|
call.id == null
|
|
4385
4682
|
? undefined
|
|
4386
4683
|
: baseContext.resolvedArgsByCallId?.get(call.id);
|
|
4387
|
-
|
|
4684
|
+
const result: SettledDirectToolResult = {
|
|
4685
|
+
proposal: structuredClone({ name: call.name, args: call.args as Record<string, unknown> }),
|
|
4686
|
+
turn: call.id == null ? undefined : this.toolCallTurns.get(call.id) ?? this.directPathTurns.get(call.id),
|
|
4388
4687
|
output,
|
|
4389
4688
|
additionalContexts,
|
|
4390
4689
|
...(resolvedArgs == null ? {} : { resolvedArgs }),
|
|
4391
|
-
}
|
|
4690
|
+
};
|
|
4691
|
+
const refMeta = isBaseMessage(output) ? output.additional_kwargs as t.ToolMessageRefMetadata : undefined;
|
|
4692
|
+
if (refMeta?._refKey != null) {
|
|
4693
|
+
result.referenceContent = this.toolOutputRegistry?.get(refMeta._refScope, refMeta._refKey);
|
|
4694
|
+
}
|
|
4695
|
+
cacheResult(call, position, result);
|
|
4696
|
+
return restoreResult(call, result);
|
|
4392
4697
|
};
|
|
4393
4698
|
|
|
4394
4699
|
const interrupting = this.interruptingToolNames;
|
|
@@ -4397,10 +4702,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4397
4702
|
directCalls.some((call) => interrupting.has(call.name));
|
|
4398
4703
|
|
|
4399
4704
|
if (!hasInterrupting) {
|
|
4400
|
-
const results = await Promise.
|
|
4705
|
+
const results = await Promise.allSettled(
|
|
4401
4706
|
directCalls.map((call, i) => runOne(call, i))
|
|
4402
4707
|
);
|
|
4403
|
-
|
|
4708
|
+
this.throwIfBreakerTripped(config);
|
|
4709
|
+
const failed = results.find((result) => result.status === 'rejected' && !isGraphInterrupt(result.reason)) ??
|
|
4710
|
+
results.find((result) => result.status === 'rejected');
|
|
4711
|
+
if (failed?.status === 'rejected') {
|
|
4712
|
+
throw failed.reason;
|
|
4713
|
+
}
|
|
4714
|
+
return results.map((result) => {
|
|
4715
|
+
if (result.status === 'rejected') {
|
|
4716
|
+
throw result.reason;
|
|
4717
|
+
}
|
|
4718
|
+
return result.value.output;
|
|
4719
|
+
});
|
|
4404
4720
|
}
|
|
4405
4721
|
|
|
4406
4722
|
const outputs: (BaseMessage | Command)[] = new Array(directCalls.length);
|
|
@@ -4437,21 +4753,6 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4437
4753
|
}
|
|
4438
4754
|
const position = interruptingPositions[i];
|
|
4439
4755
|
outputs[position] = result.value.output;
|
|
4440
|
-
const callId = directCalls[position].id;
|
|
4441
|
-
if (
|
|
4442
|
-
baseContext.replayBatchKey != null &&
|
|
4443
|
-
typeof callId === 'string' &&
|
|
4444
|
-
callId !== ''
|
|
4445
|
-
) {
|
|
4446
|
-
const batchResults =
|
|
4447
|
-
this.settledInterruptingResults.get(baseContext.replayBatchKey) ??
|
|
4448
|
-
new Map<string, SettledDirectToolResult>();
|
|
4449
|
-
batchResults.set(callId, result.value);
|
|
4450
|
-
this.settledInterruptingResults.set(
|
|
4451
|
-
baseContext.replayBatchKey,
|
|
4452
|
-
batchResults
|
|
4453
|
-
);
|
|
4454
|
-
}
|
|
4455
4756
|
}
|
|
4456
4757
|
if (hasInterruptingError) {
|
|
4457
4758
|
if (isGraphInterrupt(interruptingError)) {
|
|
@@ -4549,7 +4850,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4549
4850
|
}
|
|
4550
4851
|
|
|
4551
4852
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4552
|
-
protected async run(input: any, config: RunnableConfig): Promise<T> {
|
|
4853
|
+
protected async run(input: any, config: RunnableConfig, referenceReplay: { state?: ToolOutputReferenceState } = {}): Promise<T> {
|
|
4553
4854
|
/**
|
|
4554
4855
|
* Breaker read once at batch entry: every downstream path (direct
|
|
4555
4856
|
* `tool.invoke` runtimes and the ON_TOOL_EXECUTE dispatch) inherits this
|
|
@@ -4581,7 +4882,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4581
4882
|
* otherwise perform side effects on a failed run. */
|
|
4582
4883
|
if (
|
|
4583
4884
|
composedSignal?.aborted === true &&
|
|
4584
|
-
(composedSignal.reason instanceof StreamLimitExceededError ||
|
|
4885
|
+
(composedSignal.reason instanceof StreamLimitExceededError ||
|
|
4886
|
+
composedSignal.reason instanceof PreparedSubagentError)
|
|
4585
4887
|
) {
|
|
4586
4888
|
throw composedSignal.reason;
|
|
4587
4889
|
}
|
|
@@ -4606,11 +4908,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4606
4908
|
* every subsequent registry call on this batch.
|
|
4607
4909
|
*/
|
|
4608
4910
|
const incomingRunId = config.configurable?.run_id as string | undefined;
|
|
4609
|
-
const incomingThreadId = config.configurable?.thread_id as
|
|
4610
|
-
| string
|
|
4611
|
-
| undefined;
|
|
4612
4911
|
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
4613
|
-
const
|
|
4912
|
+
const resumedReferenceState = referenceReplay.state;
|
|
4913
|
+
const replayInputSnapshot = resumedReferenceState == null ? undefined :
|
|
4914
|
+
this.toolOutputRegistry?.resumeBatch(batchScopeId, resumedReferenceState);
|
|
4915
|
+
referenceReplay.state = resumedReferenceState ?? this.toolOutputRegistry?.snapshotState(batchScopeId);
|
|
4916
|
+
const turn = resumedReferenceState?.turnCounter ?? this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
4614
4917
|
let outputs: (BaseMessage | Command)[];
|
|
4615
4918
|
let replayBatchKey: string | undefined;
|
|
4616
4919
|
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
@@ -4628,6 +4931,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4628
4931
|
batchIndices: [0],
|
|
4629
4932
|
turn,
|
|
4630
4933
|
batchScopeId,
|
|
4934
|
+
preBatchSnapshot: replayInputSnapshot,
|
|
4631
4935
|
});
|
|
4632
4936
|
}
|
|
4633
4937
|
// Same per-batch sink the message-state branches use so
|
|
@@ -4645,12 +4949,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4645
4949
|
? findAssistantBatch(sendMessages as BaseMessage[])
|
|
4646
4950
|
: undefined;
|
|
4647
4951
|
const sendReplayBatchKey =
|
|
4648
|
-
sendAssistantBatch == null
|
|
4952
|
+
sendAssistantBatch == null || getToolBatchReplayScope(config) == null
|
|
4649
4953
|
? undefined
|
|
4650
4954
|
: getAssistantBatchReplayKey(
|
|
4651
4955
|
sendAssistantBatch,
|
|
4652
|
-
|
|
4653
|
-
incomingThreadId
|
|
4956
|
+
getToolBatchReplayScope(config)
|
|
4654
4957
|
);
|
|
4655
4958
|
const sendOutput = await this.runDirectToolWithLifecycleHooks(
|
|
4656
4959
|
input.lg_tool_call,
|
|
@@ -4659,6 +4962,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4659
4962
|
batchIndex: 0,
|
|
4660
4963
|
turn,
|
|
4661
4964
|
batchScopeId,
|
|
4965
|
+
preBatchSnapshot: replayInputSnapshot,
|
|
4662
4966
|
resolvedArgsByCallId,
|
|
4663
4967
|
errorOwnership,
|
|
4664
4968
|
additionalContextsSink: directAdditionalContexts,
|
|
@@ -4713,10 +5017,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4713
5017
|
throw new Error('ToolNode only accepts AIMessages as input.');
|
|
4714
5018
|
}
|
|
4715
5019
|
const aiMessage = assistantBatch.message;
|
|
4716
|
-
replayBatchKey = getAssistantBatchReplayKey(
|
|
5020
|
+
replayBatchKey = getToolBatchReplayScope(config) == null ? undefined : getAssistantBatchReplayKey(
|
|
4717
5021
|
assistantBatch,
|
|
4718
|
-
|
|
4719
|
-
incomingThreadId
|
|
5022
|
+
getToolBatchReplayScope(config)
|
|
4720
5023
|
);
|
|
4721
5024
|
|
|
4722
5025
|
if (this.loadRuntimeTools) {
|
|
@@ -4728,6 +5031,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4728
5031
|
this.applyToolExecutionOverrides();
|
|
4729
5032
|
}
|
|
4730
5033
|
|
|
5034
|
+
const replayCallPositions = new Map((aiMessage.tool_calls ?? []).map((call, i) => [call, i]));
|
|
4731
5035
|
const filteredCalls =
|
|
4732
5036
|
aiMessage.tool_calls?.filter((call) => {
|
|
4733
5037
|
/**
|
|
@@ -4737,7 +5041,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4737
5041
|
* which are executed by the provider's API and don't require invocation
|
|
4738
5042
|
*/
|
|
4739
5043
|
return (
|
|
4740
|
-
(call.id == null || !toolMessageIds.has(call.id)) &&
|
|
5044
|
+
(call.id == null || call.id === '' || !toolMessageIds.has(call.id)) &&
|
|
4741
5045
|
!(
|
|
4742
5046
|
call.id?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) ??
|
|
4743
5047
|
false
|
|
@@ -4865,6 +5169,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4865
5169
|
batchIndices: eventEntries.map((entry) => entry.batchIndex),
|
|
4866
5170
|
turn,
|
|
4867
5171
|
batchScopeId,
|
|
5172
|
+
preBatchSnapshot: replayInputSnapshot,
|
|
4868
5173
|
});
|
|
4869
5174
|
}
|
|
4870
5175
|
|
|
@@ -4872,6 +5177,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4872
5177
|
const directIndices = directEntries.map((e) => e.batchIndex);
|
|
4873
5178
|
const eventCalls = eventEntries.map((e) => e.call);
|
|
4874
5179
|
const eventIndices = eventEntries.map((e) => e.batchIndex);
|
|
5180
|
+
const approvalScopeCallIds = new Set(
|
|
5181
|
+
filteredCalls.flatMap((call) => (call.id == null ? [] : [call.id]))
|
|
5182
|
+
);
|
|
4875
5183
|
|
|
4876
5184
|
/**
|
|
4877
5185
|
* Snapshot the event calls' args against the *pre-batch*
|
|
@@ -4898,7 +5206,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4898
5206
|
* same-turn direct outputs.
|
|
4899
5207
|
*/
|
|
4900
5208
|
const preBatchSnapshot =
|
|
4901
|
-
this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
5209
|
+
replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
4902
5210
|
if (preBatchSnapshot != null) {
|
|
4903
5211
|
for (const entry of eventEntries) {
|
|
4904
5212
|
if (entry.call.id != null) {
|
|
@@ -4937,19 +5245,40 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4937
5245
|
preBatchSnapshot,
|
|
4938
5246
|
additionalContextsSink: directAdditionalContexts,
|
|
4939
5247
|
replayBatchKey,
|
|
5248
|
+
replayCallPositions,
|
|
5249
|
+
approvalScopeCallIds,
|
|
4940
5250
|
runInput: input as T,
|
|
4941
5251
|
}
|
|
4942
5252
|
)
|
|
4943
5253
|
: [];
|
|
4944
5254
|
|
|
4945
|
-
|
|
5255
|
+
const settledDirectResults =
|
|
5256
|
+
replayBatchKey == null ? undefined : this.settledDirectResultsByBatch.get(replayBatchKey);
|
|
5257
|
+
const unhandledDirectCalls: ToolCall[] = [];
|
|
5258
|
+
const unhandledDirectOutputs: (BaseMessage | Command)[] = [];
|
|
5259
|
+
for (let i = 0; i < directCalls.length; i++) {
|
|
5260
|
+
const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])!));
|
|
5261
|
+
if (settled?.completionHandled === true) {
|
|
5262
|
+
continue;
|
|
5263
|
+
}
|
|
5264
|
+
unhandledDirectCalls.push(directCalls[i]);
|
|
5265
|
+
unhandledDirectOutputs.push(directOutputs[i]);
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
if (unhandledDirectCalls.length > 0) {
|
|
4946
5269
|
await this.handleRunToolCompletions(
|
|
4947
|
-
|
|
4948
|
-
|
|
5270
|
+
unhandledDirectCalls,
|
|
5271
|
+
unhandledDirectOutputs,
|
|
4949
5272
|
config,
|
|
4950
5273
|
resolvedArgsByCallId,
|
|
4951
5274
|
errorOwnership
|
|
4952
5275
|
);
|
|
5276
|
+
for (let i = 0; i < directCalls.length; i++) {
|
|
5277
|
+
const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])!));
|
|
5278
|
+
if (settled != null) {
|
|
5279
|
+
settled.completionHandled = true;
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
4953
5282
|
}
|
|
4954
5283
|
|
|
4955
5284
|
const eventResult =
|
|
@@ -4998,8 +5327,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4998
5327
|
// leak a sibling's just-registered output into a sister
|
|
4999
5328
|
// call's args mid-await (Codex P1 #18).
|
|
5000
5329
|
const preBatchSnapshot =
|
|
5001
|
-
this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
5330
|
+
replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
5002
5331
|
const directAdditionalContexts: string[] = [];
|
|
5332
|
+
const approvalScopeCallIds = new Set(
|
|
5333
|
+
filteredCalls.flatMap((call) => (call.id == null ? [] : [call.id]))
|
|
5334
|
+
);
|
|
5003
5335
|
const toolOutputs = await this.runDirectBatchInterruptSafe(
|
|
5004
5336
|
filteredCalls,
|
|
5005
5337
|
filteredCalls.map((_call, i) => i),
|
|
@@ -5012,6 +5344,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
5012
5344
|
preBatchSnapshot,
|
|
5013
5345
|
additionalContextsSink: directAdditionalContexts,
|
|
5014
5346
|
replayBatchKey,
|
|
5347
|
+
replayCallPositions,
|
|
5348
|
+
approvalScopeCallIds,
|
|
5015
5349
|
runInput: input as T,
|
|
5016
5350
|
}
|
|
5017
5351
|
);
|
|
@@ -5071,7 +5405,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
5071
5405
|
|
|
5072
5406
|
if (!outputs.some(isCommand)) {
|
|
5073
5407
|
if (replayBatchKey != null) {
|
|
5074
|
-
this.
|
|
5408
|
+
this.settledDirectResultsByBatch.delete(replayBatchKey);
|
|
5075
5409
|
}
|
|
5076
5410
|
return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
|
|
5077
5411
|
}
|
|
@@ -5214,7 +5548,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
5214
5548
|
}
|
|
5215
5549
|
|
|
5216
5550
|
if (replayBatchKey != null) {
|
|
5217
|
-
this.
|
|
5551
|
+
this.settledDirectResultsByBatch.delete(replayBatchKey);
|
|
5218
5552
|
}
|
|
5219
5553
|
return combinedOutputs as T;
|
|
5220
5554
|
}
|