@librechat/agents 3.8.1 → 3.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +2 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/hitl/approvalReview.cjs +89 -0
- package/dist/cjs/hitl/approvalReview.cjs.map +1 -0
- package/dist/cjs/hooks/index.cjs +2 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -1
- package/dist/cjs/main.cjs +5 -3
- package/dist/cjs/messages/core.cjs +1 -1
- package/dist/cjs/messages/format.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +2 -2
- package/dist/cjs/run.cjs +17 -6
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/stream.cjs +2 -2
- package/dist/cjs/summarization/node.cjs +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -3
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +63 -11
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +8 -7
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +271 -84
- package/dist/cjs/tools/ToolNode.cjs.map +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 +198 -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/esm/graphs/Graph.mjs +2 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/hitl/approvalReview.mjs +83 -0
- package/dist/esm/hitl/approvalReview.mjs.map +1 -0
- package/dist/esm/hooks/index.mjs +2 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
- package/dist/esm/main.mjs +6 -6
- package/dist/esm/messages/core.mjs +1 -1
- package/dist/esm/messages/format.mjs +1 -1
- package/dist/esm/messages/prune.mjs +2 -2
- package/dist/esm/run.mjs +18 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/stream.mjs +2 -2
- package/dist/esm/summarization/node.mjs +1 -1
- package/dist/esm/tools/BashExecutor.mjs +2 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +63 -12
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -7
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +271 -84
- package/dist/esm/tools/ToolNode.mjs.map +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 +186 -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/types/hitl/approvalReview.d.ts +32 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/hooks/types.d.ts +6 -0
- package/dist/types/tools/CodeExecutor.d.ts +2 -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 +63 -0
- package/dist/types/tools/toolOutputReferences.d.ts +2 -0
- package/package.json +1 -1
- package/src/hitl/approvalReview.ts +209 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/types.ts +6 -0
- package/src/run.ts +34 -20
- package/src/tools/BashExecutor.ts +2 -2
- package/src/tools/BashProgrammaticToolCalling.ts +5 -3
- package/src/tools/CodeExecutor.ts +111 -17
- package/src/tools/ProgrammaticToolCalling.ts +10 -8
- package/src/tools/ToolNode.ts +536 -184
- package/src/tools/subagent/SubagentExecutor.ts +35 -6
- package/src/tools/subagent/SubagentReplay.ts +2 -2
- package/src/tools/toolBatchReplay.ts +423 -0
- package/src/tools/toolOutputReferences.ts +16 -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,48 @@ 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
|
+
isChildToolReplayOwner,
|
|
132
|
+
isToolReplayResume,
|
|
133
|
+
} from '@/tools/toolBatchReplay';
|
|
134
|
+
|
|
135
|
+
function stripToolApprovalReviewConfig(
|
|
117
136
|
configurable: Record<string, unknown> | undefined
|
|
118
137
|
): Record<string, unknown> | undefined {
|
|
119
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
configurable == null ||
|
|
140
|
+
!(TOOL_APPROVAL_REVIEW_CONFIG_KEY in configurable) &&
|
|
141
|
+
!(TOOL_BATCH_REPLAY_KEY in configurable)
|
|
142
|
+
) {
|
|
120
143
|
return configurable;
|
|
121
144
|
}
|
|
122
|
-
const {
|
|
145
|
+
const {
|
|
146
|
+
[TOOL_APPROVAL_REVIEW_CONFIG_KEY]: _review,
|
|
147
|
+
[TOOL_BATCH_REPLAY_KEY]: _replay,
|
|
148
|
+
...rest
|
|
149
|
+
} = configurable;
|
|
150
|
+
return rest;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Host-facing requests must not carry SDK-private execution state — hosts
|
|
154
|
+
* spread `configurable` into their own run configs. */
|
|
155
|
+
function stripHostExecutionConfig(
|
|
156
|
+
configurable: Record<string, unknown> | undefined
|
|
157
|
+
): Record<string, unknown> | undefined {
|
|
158
|
+
const withoutReview = stripToolApprovalReviewConfig(configurable);
|
|
159
|
+
if (
|
|
160
|
+
withoutReview == null ||
|
|
161
|
+
!(RUN_BREAKER_SCOPE_CONFIG_KEY in withoutReview)
|
|
162
|
+
) {
|
|
163
|
+
return withoutReview;
|
|
164
|
+
}
|
|
165
|
+
const { [RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = withoutReview;
|
|
123
166
|
return rest;
|
|
124
167
|
}
|
|
125
168
|
import { convertInjectedMessages } from '@/messages/injected';
|
|
@@ -209,6 +252,10 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
209
252
|
additionalContextsSink?: string[];
|
|
210
253
|
/** Stable identity of the assistant tool-call batch across HITL replay. */
|
|
211
254
|
replayBatchKey?: string;
|
|
255
|
+
/** Positions in the original proposal, before completed calls are filtered. */
|
|
256
|
+
replayCallPositions?: ReadonlyMap<ToolCall, number>;
|
|
257
|
+
/** Tool-call identities owned by this ToolNode invocation. */
|
|
258
|
+
approvalScopeCallIds?: ReadonlySet<string>;
|
|
212
259
|
/**
|
|
213
260
|
* Graph state the ToolNode was invoked with, threaded from `run()`
|
|
214
261
|
* so `tool.invoke` can forward it as langgraph 1.4's `runtime.state`
|
|
@@ -236,11 +283,14 @@ function withSubagentReplayBatch(
|
|
|
236
283
|
};
|
|
237
284
|
}
|
|
238
285
|
|
|
239
|
-
type SettledDirectToolResult =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
type SettledDirectToolResult = SettledToolBatchResult;
|
|
287
|
+
|
|
288
|
+
/** Positional identity is safe within the immutable full-proposal batch key. */
|
|
289
|
+
function directReplayKey(call: ToolCall, batchIndex: number): string {
|
|
290
|
+
return call.id == null || call.id === ''
|
|
291
|
+
? JSON.stringify(['position', batchIndex])
|
|
292
|
+
: JSON.stringify(['id', call.id]);
|
|
293
|
+
}
|
|
244
294
|
|
|
245
295
|
/**
|
|
246
296
|
* Batch-local record of who owns each failed call's completion event.
|
|
@@ -403,8 +453,6 @@ function describeOfferedShape(value: unknown): string {
|
|
|
403
453
|
|
|
404
454
|
type AssistantBatch = {
|
|
405
455
|
message: AIMessage;
|
|
406
|
-
index: number;
|
|
407
|
-
messageCount: number;
|
|
408
456
|
};
|
|
409
457
|
|
|
410
458
|
function findAssistantBatch(
|
|
@@ -413,7 +461,7 @@ function findAssistantBatch(
|
|
|
413
461
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
414
462
|
const message = messages[i];
|
|
415
463
|
if (isAIMessage(message)) {
|
|
416
|
-
return { message
|
|
464
|
+
return { message };
|
|
417
465
|
}
|
|
418
466
|
}
|
|
419
467
|
return undefined;
|
|
@@ -421,15 +469,12 @@ function findAssistantBatch(
|
|
|
421
469
|
|
|
422
470
|
function getAssistantBatchReplayKey(
|
|
423
471
|
batch: AssistantBatch,
|
|
424
|
-
runId: string | undefined,
|
|
425
472
|
threadId: string | undefined
|
|
426
473
|
): string {
|
|
427
474
|
return JSON.stringify([
|
|
428
|
-
runId ?? null,
|
|
429
475
|
threadId ?? null,
|
|
430
|
-
batch.message.id
|
|
431
|
-
batch.
|
|
432
|
-
batch.messageCount,
|
|
476
|
+
batch.message.id == null || batch.message.id === '' ? null : batch.message.id,
|
|
477
|
+
createHash('sha256').update(stableStringify(batch.message.tool_calls ?? [])).digest('hex'),
|
|
433
478
|
]);
|
|
434
479
|
}
|
|
435
480
|
|
|
@@ -697,10 +742,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
697
742
|
* the Run ends.
|
|
698
743
|
*/
|
|
699
744
|
private directPathTurns: Map<string, number> = new Map();
|
|
700
|
-
/** Terminal results
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
private
|
|
745
|
+
/** Terminal direct results that must survive a LangGraph replay of the
|
|
746
|
+
* containing ToolNode. Includes the sidecar data the fresh batch needs for
|
|
747
|
+
* hook-context injection and completion-event deduplication. */
|
|
748
|
+
private settledDirectResultsByBatch = new Map<
|
|
704
749
|
string,
|
|
705
750
|
Map<string, SettledDirectToolResult>
|
|
706
751
|
>();
|
|
@@ -854,23 +899,95 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
854
899
|
name: name ?? TOOL_NODE_RUN_NAME,
|
|
855
900
|
tags,
|
|
856
901
|
func: async (input, config) => {
|
|
902
|
+
const messages = Array.isArray(input) ? input as BaseMessage[] :
|
|
903
|
+
(input as { messages?: BaseMessage[] }).messages;
|
|
904
|
+
const assistantBatch = messages == null ? undefined : findAssistantBatch(messages);
|
|
905
|
+
const activeReplayKey = assistantBatch == null || getToolBatchReplayScope(config) == null ? undefined : getAssistantBatchReplayKey(
|
|
906
|
+
assistantBatch, getToolBatchReplayScope(config)
|
|
907
|
+
);
|
|
908
|
+
const replayOwner = getToolBatchReplayOwner(
|
|
909
|
+
config,
|
|
910
|
+
this.executingAgentId ?? this.agentId ?? ''
|
|
911
|
+
);
|
|
912
|
+
let reviewEvidence = getToolApprovalReviewEvidence(config);
|
|
913
|
+
const replayState = getToolBatchReplayState(config.configurable);
|
|
914
|
+
const ownsParentBatch = replayState?.records.some(
|
|
915
|
+
(record) => record.owner === replayOwner && record.batch === activeReplayKey
|
|
916
|
+
) === true;
|
|
917
|
+
let isChildReplay = false;
|
|
918
|
+
if (ownsParentBatch) {
|
|
919
|
+
const calls = this.isSendInput(input) ? [input.lg_tool_call] : assistantBatch?.message.tool_calls ?? [];
|
|
920
|
+
const trustedParentCallIds = new Set(calls.filter((call) =>
|
|
921
|
+
call.name === Constants.SUBAGENT &&
|
|
922
|
+
(this.toolMap.get(call.name) as ReplayableSubagentTool | undefined)?.[SUBAGENT_REPLAY_CONTROLLER] != null
|
|
923
|
+
).map((call) => call.id ?? ''));
|
|
924
|
+
isChildReplay = reviewEvidence?.owner != null
|
|
925
|
+
? isChildToolReplayOwner(config, reviewEvidence.owner, trustedParentCallIds)
|
|
926
|
+
: replayState.records.some((record) => isChildToolReplayOwner(config, record.owner, trustedParentCallIds));
|
|
927
|
+
}
|
|
928
|
+
if ((reviewEvidence?.owner != null || replayState != null) &&
|
|
929
|
+
!isToolReplayResume(config, reviewEvidence?.interruptId ?? replayState?.interruptId, isChildReplay)) {
|
|
930
|
+
config = {
|
|
931
|
+
...config,
|
|
932
|
+
configurable: {
|
|
933
|
+
...config.configurable,
|
|
934
|
+
[TOOL_APPROVAL_REVIEW_CONFIG_KEY]: undefined,
|
|
935
|
+
[TOOL_BATCH_REPLAY_KEY]: undefined,
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
reviewEvidence = undefined;
|
|
939
|
+
}
|
|
940
|
+
if (reviewEvidence?.owner != null && reviewEvidence.owner !== replayOwner && !isChildReplay) {
|
|
941
|
+
throw new Error('Tool approval execution owner changed before resume — failing closed');
|
|
942
|
+
}
|
|
943
|
+
const referenceReplay: { state?: ToolOutputReferenceState } = {};
|
|
944
|
+
const restoredBatches = await restoreToolBatchReplayState(config, replayOwner);
|
|
945
|
+
if (restoredBatches.length > 0 && !restoredBatches.some(({ batch }) => batch === activeReplayKey)) {
|
|
946
|
+
throw new Error('Tool batch proposal changed before approval replay — failing closed');
|
|
947
|
+
}
|
|
948
|
+
if (activeReplayKey != null && getToolBatchReplayState(config.configurable) != null) {
|
|
949
|
+
this.settledDirectResultsByBatch.delete(activeReplayKey);
|
|
950
|
+
}
|
|
951
|
+
for (const { batch, results, turnState, referenceState } of restoredBatches) {
|
|
952
|
+
if (batch !== activeReplayKey) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
this.settledDirectResultsByBatch.set(batch, new Map(results));
|
|
956
|
+
if (turnState != null) {
|
|
957
|
+
this.restoreSubagentResumeState(turnState);
|
|
958
|
+
}
|
|
959
|
+
referenceReplay.state = referenceState;
|
|
960
|
+
}
|
|
857
961
|
const state = input as T & Pick<t.BaseGraphState, 'runStepState'>;
|
|
858
962
|
restoreRunStepResumeState?.(state.runStepState, config);
|
|
859
963
|
let result: T;
|
|
860
964
|
try {
|
|
861
|
-
result = await this.run(input, config);
|
|
965
|
+
result = await this.run(input, config, referenceReplay);
|
|
862
966
|
} catch (error) {
|
|
863
|
-
if (!isGraphInterrupt(error)
|
|
967
|
+
if (!isGraphInterrupt(error)) {
|
|
864
968
|
throw error;
|
|
865
969
|
}
|
|
866
|
-
const resumeState = createRunStepResumeState();
|
|
970
|
+
const resumeState = createRunStepResumeState?.();
|
|
971
|
+
const activeResults = activeReplayKey == null ? undefined : this.settledDirectResultsByBatch.get(activeReplayKey);
|
|
972
|
+
const activeBatches = activeReplayKey == null
|
|
973
|
+
? new Map<string, Map<string, SettledDirectToolResult>>()
|
|
974
|
+
: new Map([[activeReplayKey, activeResults ?? new Map<string, SettledDirectToolResult>()]]);
|
|
975
|
+
const turnState = this.createSubagentResumeState();
|
|
976
|
+
const activeCallIds = new Set(assistantBatch?.message.tool_calls?.map((call) => call.id));
|
|
977
|
+
turnState.directPathTurns = turnState.directPathTurns.filter(({ toolCallId }) => activeCallIds.has(toolCallId));
|
|
867
978
|
throw new GraphInterrupt(
|
|
868
|
-
error.interrupts.map((pendingInterrupt) =>
|
|
869
|
-
|
|
870
|
-
value: attachRunStepResumeState(
|
|
979
|
+
await Promise.all(error.interrupts.map(async (pendingInterrupt) => {
|
|
980
|
+
const value = await attachToolBatchReplayState(
|
|
871
981
|
pendingInterrupt.value,
|
|
872
|
-
|
|
873
|
-
|
|
982
|
+
replayOwner,
|
|
983
|
+
activeBatches,
|
|
984
|
+
turnState,
|
|
985
|
+
referenceReplay.state
|
|
986
|
+
);
|
|
987
|
+
return {
|
|
988
|
+
...pendingInterrupt,
|
|
989
|
+
value: resumeState == null ? value : attachRunStepResumeState(value, resumeState),
|
|
990
|
+
};
|
|
874
991
|
}))
|
|
875
992
|
);
|
|
876
993
|
}
|
|
@@ -1146,14 +1263,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1146
1263
|
*/
|
|
1147
1264
|
clearDirectPathTurns(): void {
|
|
1148
1265
|
this.directPathTurns.clear();
|
|
1149
|
-
this.
|
|
1266
|
+
this.settledDirectResultsByBatch.clear();
|
|
1150
1267
|
}
|
|
1151
1268
|
|
|
1152
1269
|
/** Returns the live caller projection used by direct and event execution. */
|
|
1153
1270
|
private getCallerCapabilityProjection(): CallerCapabilityProjection {
|
|
1154
|
-
const discoveredToolNames = new Set(
|
|
1155
|
-
this.getDiscoveredToolNames?.() ?? []
|
|
1156
|
-
);
|
|
1271
|
+
const discoveredToolNames = new Set(this.getDiscoveredToolNames?.() ?? []);
|
|
1157
1272
|
return resolveCallerCapabilityProjection(
|
|
1158
1273
|
mergeCallerCapabilityDefinitions(
|
|
1159
1274
|
this.toolDefinitions?.values(),
|
|
@@ -1194,9 +1309,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1194
1309
|
private getProgrammaticTools(): t.ProgrammaticCache {
|
|
1195
1310
|
const toolMap: t.ToolMap = new Map();
|
|
1196
1311
|
const toolDefs: t.LCTool[] = [];
|
|
1197
|
-
const discoveredToolNames = new Set(
|
|
1198
|
-
this.getDiscoveredToolNames?.() ?? []
|
|
1199
|
-
);
|
|
1312
|
+
const discoveredToolNames = new Set(this.getDiscoveredToolNames?.() ?? []);
|
|
1200
1313
|
const executableCapabilities = resolveCallerCapabilityProjection(
|
|
1201
1314
|
this.toolRegistry?.values() ?? [],
|
|
1202
1315
|
(toolDef) => isToolDefinitionActive(toolDef, discoveredToolNames)
|
|
@@ -1307,11 +1420,22 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1307
1420
|
runInput?: RunToolBatchContext<T>['runInput']
|
|
1308
1421
|
): Promise<unknown> {
|
|
1309
1422
|
const lgConfig = config as LangGraphRunnableConfig;
|
|
1310
|
-
const
|
|
1423
|
+
const isTrustedSubagentHop =
|
|
1424
|
+
call.name === Constants.SUBAGENT &&
|
|
1425
|
+
(tool as ReplayableSubagentTool)[SUBAGENT_REPLAY_CONTROLLER] != null;
|
|
1426
|
+
const toolConfig: RunnableConfig = {
|
|
1311
1427
|
...config,
|
|
1428
|
+
configurable: isTrustedSubagentHop
|
|
1429
|
+
? config.configurable
|
|
1430
|
+
: stripToolApprovalReviewConfig(
|
|
1431
|
+
config.configurable as Record<string, unknown> | undefined
|
|
1432
|
+
),
|
|
1433
|
+
};
|
|
1434
|
+
const runtime: ToolRuntime<T> = {
|
|
1435
|
+
...toolConfig,
|
|
1312
1436
|
state: runInput as ToolRuntime<T>['state'],
|
|
1313
1437
|
toolCallId: call.id ?? '',
|
|
1314
|
-
config,
|
|
1438
|
+
config: toolConfig,
|
|
1315
1439
|
context: lgConfig.context as ToolRuntime<T>['context'],
|
|
1316
1440
|
store: (lgConfig.store ?? null) as ToolRuntime<T>['store'],
|
|
1317
1441
|
writer:
|
|
@@ -1533,6 +1657,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1533
1657
|
}
|
|
1534
1658
|
return next;
|
|
1535
1659
|
})();
|
|
1660
|
+
this.recordToolUsageTurn(call.name, usageCount, call.id);
|
|
1661
|
+
if (batchContext.replayBatchKey != null && call.id != null && call.id !== '') {
|
|
1662
|
+
this.directPathTurns.set(call.id, usageCount);
|
|
1663
|
+
}
|
|
1536
1664
|
let args = call.args;
|
|
1537
1665
|
if (resolveFn != null) {
|
|
1538
1666
|
const { resolved, unresolved } = resolveFn(runId, args);
|
|
@@ -1915,17 +2043,22 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1915
2043
|
batchContext: RunToolBatchContext<T> = {}
|
|
1916
2044
|
): Promise<BaseMessage | Command> {
|
|
1917
2045
|
if (
|
|
1918
|
-
this.preparedSubagents?.owns(this.executingAgentId ?? '', call) ===
|
|
2046
|
+
this.preparedSubagents?.owns(this.executingAgentId ?? '', call) ===
|
|
2047
|
+
true &&
|
|
1919
2048
|
this.hookRegistry?.hasResultAlteringHooks(
|
|
1920
2049
|
(config.configurable?.run_id as string | undefined) ?? ''
|
|
1921
2050
|
) === true
|
|
1922
2051
|
) {
|
|
1923
2052
|
this.preparedSubagents.clear();
|
|
1924
|
-
throw new PreparedSubagentError(
|
|
2053
|
+
throw new PreparedSubagentError(
|
|
2054
|
+
'Tool hooks changed after a subagent started.'
|
|
2055
|
+
);
|
|
1925
2056
|
}
|
|
1926
2057
|
const replayController = (
|
|
1927
2058
|
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
1928
2059
|
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
2060
|
+
const isTrustedSubagentReviewHop =
|
|
2061
|
+
call.name === Constants.SUBAGENT && replayController != null;
|
|
1929
2062
|
const replayConfig =
|
|
1930
2063
|
replayController == null
|
|
1931
2064
|
? config
|
|
@@ -2024,16 +2157,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2024
2157
|
approvalReplaySessionId,
|
|
2025
2158
|
approvalReplayKey
|
|
2026
2159
|
);
|
|
2160
|
+
const approvalReviewEvidence = getToolApprovalReviewEvidence(config,
|
|
2161
|
+
getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ''));
|
|
2162
|
+
const reviewedApproval = getReviewedToolApproval(
|
|
2163
|
+
approvalReviewEvidence?.payload,
|
|
2164
|
+
call.id
|
|
2165
|
+
);
|
|
2166
|
+
const approvalEvidenceAppliesToBatch =
|
|
2167
|
+
approvalReviewEvidence?.payload.action_requests.some((request) =>
|
|
2168
|
+
batchContext.approvalScopeCallIds == null
|
|
2169
|
+
? request.tool_call_id === call.id
|
|
2170
|
+
: batchContext.approvalScopeCallIds.has(request.tool_call_id)
|
|
2171
|
+
) === true;
|
|
2027
2172
|
const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
2028
2173
|
const hasFailureHook =
|
|
2029
2174
|
hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
2030
2175
|
|
|
2031
2176
|
if (
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2177
|
+
!hasPreHook &&
|
|
2178
|
+
pendingApproval == null &&
|
|
2179
|
+
reviewedApproval == null &&
|
|
2180
|
+
approvalReviewEvidence == null &&
|
|
2181
|
+
!hasPostHook &&
|
|
2182
|
+
!hasFailureHook
|
|
2037
2183
|
) {
|
|
2038
2184
|
const output = await this.runTool(call, replayConfig, batchContext);
|
|
2039
2185
|
return output instanceof ToolMessage ? persistOutput(output) : output;
|
|
@@ -2086,46 +2232,74 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2086
2232
|
// value the PreToolUse hook observes matches the value the
|
|
2087
2233
|
// (later-awaited) `runTool` will actually run with — both are
|
|
2088
2234
|
// anchored to the pre-batch registry state.
|
|
2089
|
-
let resolvedArgs = call.args as Record<string, unknown>;
|
|
2090
2235
|
const hookResolveOptions = {
|
|
2091
2236
|
substituteIntentKey: this.toolDeclaresBusinessIntent(call.name),
|
|
2092
2237
|
};
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2238
|
+
const resolveDirectArgs = (
|
|
2239
|
+
args: Record<string, unknown>
|
|
2240
|
+
): Record<string, unknown> => {
|
|
2241
|
+
if (batchContext.preBatchSnapshot != null) {
|
|
2242
|
+
return batchContext.preBatchSnapshot.resolve(args, hookResolveOptions)
|
|
2243
|
+
.resolved as Record<string, unknown>;
|
|
2244
|
+
}
|
|
2245
|
+
if (this.toolOutputRegistry != null) {
|
|
2246
|
+
return this.toolOutputRegistry.resolve(
|
|
2247
|
+
registryRunId,
|
|
2248
|
+
args,
|
|
2249
|
+
hookResolveOptions
|
|
2250
|
+
).resolved as Record<string, unknown>;
|
|
2251
|
+
}
|
|
2252
|
+
return args;
|
|
2253
|
+
};
|
|
2254
|
+
const originalResolvedArgs = resolveDirectArgs(
|
|
2255
|
+
call.args as Record<string, unknown>
|
|
2256
|
+
);
|
|
2257
|
+
/** Reusable and pending hooks must replay from the original proposal so
|
|
2258
|
+
* non-idempotent rewrites are applied exactly once. A rebuilt runtime
|
|
2259
|
+
* without that policy instead falls back to the resolved arguments the
|
|
2260
|
+
* user actually reviewed. */
|
|
2261
|
+
let resolvedArgs =
|
|
2262
|
+
hasPreHook || pendingApproval != null
|
|
2263
|
+
? originalResolvedArgs
|
|
2264
|
+
: (reviewedApproval?.request.arguments ?? originalResolvedArgs);
|
|
2107
2265
|
|
|
2108
2266
|
let effectiveCall = call;
|
|
2109
|
-
if (
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2267
|
+
if (
|
|
2268
|
+
hasPreHook ||
|
|
2269
|
+
pendingApproval != null ||
|
|
2270
|
+
reviewedApproval != null ||
|
|
2271
|
+
approvalReviewEvidence != null
|
|
2272
|
+
) {
|
|
2273
|
+
const preResult: AggregatedHookResult | undefined =
|
|
2274
|
+
hookRegistry == null
|
|
2275
|
+
? {
|
|
2276
|
+
additionalContexts: [],
|
|
2277
|
+
injectedMessages: [],
|
|
2278
|
+
errors: [],
|
|
2279
|
+
}
|
|
2280
|
+
: await executeHooks({
|
|
2281
|
+
registry: hookRegistry,
|
|
2282
|
+
input: {
|
|
2283
|
+
hook_event_name: 'PreToolUse',
|
|
2284
|
+
runId,
|
|
2285
|
+
threadId,
|
|
2286
|
+
agentId: this.agentId,
|
|
2287
|
+
executingAgentId: this.executingAgentId,
|
|
2288
|
+
toolName: call.name,
|
|
2289
|
+
toolInput: resolvedArgs,
|
|
2290
|
+
toolUseId: call.id ?? '',
|
|
2291
|
+
stepId,
|
|
2292
|
+
turn,
|
|
2293
|
+
},
|
|
2294
|
+
sessionId: runId,
|
|
2295
|
+
matchQuery: call.name,
|
|
2296
|
+
onceReplayKey: approvalReplayKey,
|
|
2297
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
2298
|
+
}).catch((): AggregatedHookResult | undefined => approvalReviewEvidence == null ? undefined : {
|
|
2299
|
+
decision: 'deny',
|
|
2300
|
+
reason: 'Approval policy could not be evaluated on resume',
|
|
2301
|
+
additionalContexts: [], injectedMessages: [], errors: [],
|
|
2302
|
+
});
|
|
2129
2303
|
|
|
2130
2304
|
if (preResult != null) {
|
|
2131
2305
|
// Forward any additionalContext strings hooks returned into
|
|
@@ -2142,9 +2316,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2142
2316
|
// a valid combination (one matcher sanitises args, another asks
|
|
2143
2317
|
// for approval); the reviewer should see the sanitised args.
|
|
2144
2318
|
if (preResult.updatedInput != null) {
|
|
2319
|
+
resolvedArgs = resolveDirectArgs(
|
|
2320
|
+
preResult.updatedInput as Record<string, unknown>
|
|
2321
|
+
);
|
|
2322
|
+
effectiveCall = {
|
|
2323
|
+
...call,
|
|
2324
|
+
args: resolvedArgs,
|
|
2325
|
+
};
|
|
2326
|
+
} else if (reviewedApproval != null) {
|
|
2327
|
+
resolvedArgs = reviewedApproval.request.arguments;
|
|
2145
2328
|
effectiveCall = {
|
|
2146
2329
|
...call,
|
|
2147
|
-
args:
|
|
2330
|
+
args: resolvedArgs,
|
|
2148
2331
|
};
|
|
2149
2332
|
}
|
|
2150
2333
|
|
|
@@ -2162,7 +2345,30 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2162
2345
|
);
|
|
2163
2346
|
}
|
|
2164
2347
|
|
|
2165
|
-
|
|
2348
|
+
/** A sibling absent from restored evidence may have raised a parallel
|
|
2349
|
+
* approval that LangGraph did not preserve. A current hook can ask
|
|
2350
|
+
* again; otherwise never let that unreviewed sibling execute. */
|
|
2351
|
+
if (
|
|
2352
|
+
approvalEvidenceAppliesToBatch &&
|
|
2353
|
+
reviewedApproval == null &&
|
|
2354
|
+
!isTrustedSubagentReviewHop &&
|
|
2355
|
+
preResult.decision !== 'ask'
|
|
2356
|
+
) {
|
|
2357
|
+
return persistOutput(
|
|
2358
|
+
this.blockDirectCall({
|
|
2359
|
+
call,
|
|
2360
|
+
resolvedArgs,
|
|
2361
|
+
reason:
|
|
2362
|
+
'Tool call was not included in the restored approval batch — failing closed',
|
|
2363
|
+
hookRegistry,
|
|
2364
|
+
runId,
|
|
2365
|
+
threadId,
|
|
2366
|
+
}),
|
|
2367
|
+
resolvedArgs
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
if (preResult.decision === 'ask' || reviewedApproval != null) {
|
|
2166
2372
|
if (this.humanInTheLoop?.enabled !== true) {
|
|
2167
2373
|
// Fail-closed: no HITL UI configured, so we can't actually
|
|
2168
2374
|
// ask. Logged once via the existing helper.
|
|
@@ -2213,14 +2419,25 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2213
2419
|
// same ask entry without dispatching the consumed hook again.
|
|
2214
2420
|
// ToolNode disables LangSmith tracing, so the AsyncLocalStorage
|
|
2215
2421
|
// frame must be re-established here.
|
|
2422
|
+
const allowedDecisions =
|
|
2423
|
+
preResult.decision === 'ask'
|
|
2424
|
+
? preResult.allowedDecisions
|
|
2425
|
+
: reviewedApproval?.reviewConfig.allowed_decisions;
|
|
2426
|
+
const askReason =
|
|
2427
|
+
preResult.decision === 'ask'
|
|
2428
|
+
? preResult.reason
|
|
2429
|
+
: reviewedApproval?.request.description;
|
|
2430
|
+
if (effectiveCall === call) {
|
|
2431
|
+
effectiveCall = { ...call, args: resolvedArgs };
|
|
2432
|
+
}
|
|
2216
2433
|
const askEntry: AskEntry = {
|
|
2217
2434
|
entry: {
|
|
2218
2435
|
call: effectiveCall,
|
|
2219
2436
|
args: effectiveCall.args as Record<string, unknown>,
|
|
2220
2437
|
stepId,
|
|
2221
2438
|
},
|
|
2222
|
-
reason:
|
|
2223
|
-
allowedDecisions
|
|
2439
|
+
reason: askReason,
|
|
2440
|
+
allowedDecisions,
|
|
2224
2441
|
};
|
|
2225
2442
|
const payload = buildToolApprovalInterruptPayload([askEntry], runId);
|
|
2226
2443
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
@@ -2231,10 +2448,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2231
2448
|
t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap
|
|
2232
2449
|
>(payload)
|
|
2233
2450
|
);
|
|
2234
|
-
hookRegistry
|
|
2451
|
+
hookRegistry?.clearPendingToolApproval(
|
|
2235
2452
|
approvalReplaySessionId,
|
|
2236
2453
|
approvalReplayKey
|
|
2237
2454
|
);
|
|
2455
|
+
const currentApproval = getReviewedToolApproval(payload, toolCallId);
|
|
2456
|
+
if (
|
|
2457
|
+
reviewedApproval != null &&
|
|
2458
|
+
(currentApproval == null ||
|
|
2459
|
+
!toolApprovalProposalMatches(currentApproval, reviewedApproval))
|
|
2460
|
+
) {
|
|
2461
|
+
return persistOutput(
|
|
2462
|
+
this.blockDirectCall({
|
|
2463
|
+
call,
|
|
2464
|
+
resolvedArgs,
|
|
2465
|
+
reason:
|
|
2466
|
+
'Reviewed tool proposal changed before resume; retry approval',
|
|
2467
|
+
hookRegistry,
|
|
2468
|
+
runId,
|
|
2469
|
+
threadId,
|
|
2470
|
+
}),
|
|
2471
|
+
effectiveCall.args as Record<string, unknown>
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2238
2474
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
2239
2475
|
[toolCallId],
|
|
2240
2476
|
resumeValue
|
|
@@ -2246,9 +2482,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2246
2482
|
const declaredType = (decision as { type?: unknown }).type;
|
|
2247
2483
|
|
|
2248
2484
|
if (
|
|
2249
|
-
|
|
2485
|
+
allowedDecisions != null &&
|
|
2250
2486
|
(typeof declaredType !== 'string' ||
|
|
2251
|
-
!
|
|
2487
|
+
!allowedDecisions.includes(
|
|
2252
2488
|
declaredType as t.ToolApprovalDecisionType
|
|
2253
2489
|
))
|
|
2254
2490
|
) {
|
|
@@ -2256,7 +2492,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2256
2492
|
this.blockDirectCall({
|
|
2257
2493
|
call,
|
|
2258
2494
|
resolvedArgs,
|
|
2259
|
-
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${
|
|
2495
|
+
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${allowedDecisions.join(', ')}] — failing closed`,
|
|
2260
2496
|
hookRegistry,
|
|
2261
2497
|
runId,
|
|
2262
2498
|
threadId,
|
|
@@ -2270,8 +2506,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2270
2506
|
this.blockDirectCall({
|
|
2271
2507
|
call,
|
|
2272
2508
|
resolvedArgs,
|
|
2273
|
-
reason:
|
|
2274
|
-
decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
2509
|
+
reason: decision.reason ?? askReason ?? 'Rejected by user',
|
|
2275
2510
|
hookRegistry,
|
|
2276
2511
|
runId,
|
|
2277
2512
|
threadId,
|
|
@@ -2546,12 +2781,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2546
2781
|
call: ToolCall;
|
|
2547
2782
|
resolvedArgs: Record<string, unknown>;
|
|
2548
2783
|
reason: string;
|
|
2549
|
-
hookRegistry: HookRegistry;
|
|
2784
|
+
hookRegistry: HookRegistry | undefined;
|
|
2550
2785
|
runId: string;
|
|
2551
2786
|
threadId: string | undefined;
|
|
2552
2787
|
}): ToolMessage {
|
|
2553
2788
|
const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
|
|
2554
|
-
if (hookRegistry
|
|
2789
|
+
if (hookRegistry?.hasHookFor('PermissionDenied', runId) === true) {
|
|
2555
2790
|
executeHooks({
|
|
2556
2791
|
registry: hookRegistry,
|
|
2557
2792
|
input: {
|
|
@@ -2881,7 +3116,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2881
3116
|
const signal = config.signal;
|
|
2882
3117
|
if (
|
|
2883
3118
|
signal?.aborted === true &&
|
|
2884
|
-
(signal.reason instanceof StreamLimitExceededError ||
|
|
3119
|
+
(signal.reason instanceof StreamLimitExceededError ||
|
|
3120
|
+
signal.reason instanceof PreparedSubagentError)
|
|
2885
3121
|
) {
|
|
2886
3122
|
throw signal.reason;
|
|
2887
3123
|
}
|
|
@@ -2932,11 +3168,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2932
3168
|
unresolvedByCallId.set(call.id, pre.unresolved);
|
|
2933
3169
|
}
|
|
2934
3170
|
} else if (registry != null) {
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2937
|
-
originalArgs,
|
|
2938
|
-
|
|
2939
|
-
);
|
|
3171
|
+
const options = { substituteIntentKey: this.toolDeclaresBusinessIntent(call.name) };
|
|
3172
|
+
const { resolved, unresolved } = preBatchSnapshot != null
|
|
3173
|
+
? preBatchSnapshot.resolve(originalArgs, options)
|
|
3174
|
+
: registry.resolve(registryRunId, originalArgs, options);
|
|
2940
3175
|
resolvedArgs = resolved as Record<string, unknown>;
|
|
2941
3176
|
if (unresolved.length > 0 && call.id != null) {
|
|
2942
3177
|
unresolvedByCallId.set(call.id, unresolved);
|
|
@@ -2984,6 +3219,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2984
3219
|
config,
|
|
2985
3220
|
runId
|
|
2986
3221
|
);
|
|
3222
|
+
const approvalReviewEvidence = getToolApprovalReviewEvidence(config,
|
|
3223
|
+
getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ''));
|
|
2987
3224
|
const hasPendingApproval = preToolCalls.some(
|
|
2988
3225
|
(entry) =>
|
|
2989
3226
|
entry.call.id != null &&
|
|
@@ -2997,8 +3234,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2997
3234
|
) != null
|
|
2998
3235
|
);
|
|
2999
3236
|
if (
|
|
3000
|
-
hookRegistry
|
|
3001
|
-
|
|
3237
|
+
hookRegistry?.hasHookFor('PreToolUse', runId) === true ||
|
|
3238
|
+
hasPendingApproval ||
|
|
3239
|
+
approvalReviewEvidence != null
|
|
3002
3240
|
) {
|
|
3003
3241
|
/**
|
|
3004
3242
|
* Pull each call's prestarted eager record BEFORE awaiting the hooks:
|
|
@@ -3026,38 +3264,41 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3026
3264
|
}
|
|
3027
3265
|
}
|
|
3028
3266
|
}
|
|
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
|
-
|
|
3267
|
+
const preResults =
|
|
3268
|
+
hookRegistry == null
|
|
3269
|
+
? preToolCalls.map(() => HOOK_FALLBACK)
|
|
3270
|
+
: await Promise.all(
|
|
3271
|
+
preToolCalls.map((entry) => {
|
|
3272
|
+
const toolUseId = entry.call.id;
|
|
3273
|
+
const approvalReplayKey =
|
|
3274
|
+
toolUseId == null
|
|
3275
|
+
? undefined
|
|
3276
|
+
: createToolApprovalReplayKey(
|
|
3277
|
+
config,
|
|
3278
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
3279
|
+
toolUseId
|
|
3280
|
+
);
|
|
3281
|
+
return executeHooks({
|
|
3282
|
+
registry: hookRegistry,
|
|
3283
|
+
input: {
|
|
3284
|
+
hook_event_name: 'PreToolUse',
|
|
3285
|
+
runId,
|
|
3286
|
+
threadId,
|
|
3287
|
+
agentId: this.agentId,
|
|
3288
|
+
executingAgentId: this.executingAgentId,
|
|
3289
|
+
toolName: entry.call.name,
|
|
3290
|
+
toolInput: entry.args,
|
|
3291
|
+
toolUseId: entry.call.id!,
|
|
3292
|
+
stepId: entry.stepId,
|
|
3293
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0,
|
|
3294
|
+
},
|
|
3295
|
+
sessionId: runId,
|
|
3296
|
+
matchQuery: entry.call.name,
|
|
3297
|
+
onceReplayKey: approvalReplayKey,
|
|
3298
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
3299
|
+
}).catch((): AggregatedHookResult => HOOK_FALLBACK);
|
|
3300
|
+
})
|
|
3301
|
+
);
|
|
3061
3302
|
|
|
3062
3303
|
type PendingEntry = (typeof preToolCalls)[number];
|
|
3063
3304
|
|
|
@@ -3138,7 +3379,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3138
3379
|
item.contentString,
|
|
3139
3380
|
config
|
|
3140
3381
|
);
|
|
3141
|
-
if (hookRegistry
|
|
3382
|
+
if (hookRegistry?.hasHookFor('PermissionDenied', runId) === true) {
|
|
3142
3383
|
executeHooks({
|
|
3143
3384
|
registry: hookRegistry,
|
|
3144
3385
|
input: {
|
|
@@ -3252,6 +3493,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3252
3493
|
*/
|
|
3253
3494
|
if (hookResult.updatedInput != null) {
|
|
3254
3495
|
applyInputOverride(entry, hookResult.updatedInput);
|
|
3496
|
+
} else {
|
|
3497
|
+
const reviewed = getReviewedToolApproval(approvalReviewEvidence?.payload, entry.call.id);
|
|
3498
|
+
if (reviewed != null) {
|
|
3499
|
+
entry.args = this.coerceEventToolArgs(entry.call.name, reviewed.request.arguments);
|
|
3500
|
+
if (entry.call.id != null) {
|
|
3501
|
+
unresolvedByCallId.delete(entry.call.id);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3255
3504
|
}
|
|
3256
3505
|
askEntries.push({
|
|
3257
3506
|
entry,
|
|
@@ -3264,6 +3513,27 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3264
3513
|
if (hookResult.updatedInput != null) {
|
|
3265
3514
|
applyInputOverride(entry, hookResult.updatedInput);
|
|
3266
3515
|
}
|
|
3516
|
+
const reviewedApproval = getReviewedToolApproval(
|
|
3517
|
+
approvalReviewEvidence?.payload,
|
|
3518
|
+
entry.call.id
|
|
3519
|
+
);
|
|
3520
|
+
if (reviewedApproval != null) {
|
|
3521
|
+
if (hookResult.updatedInput == null) {
|
|
3522
|
+
entry.args = this.coerceEventToolArgs(
|
|
3523
|
+
entry.call.name,
|
|
3524
|
+
reviewedApproval.request.arguments
|
|
3525
|
+
);
|
|
3526
|
+
if (entry.call.id != null) {
|
|
3527
|
+
unresolvedByCallId.delete(entry.call.id);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
askEntries.push({
|
|
3531
|
+
entry,
|
|
3532
|
+
reason: reviewedApproval.request.description,
|
|
3533
|
+
allowedDecisions: reviewedApproval.reviewConfig.allowed_decisions,
|
|
3534
|
+
});
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3267
3537
|
if (entry.call.id != null) {
|
|
3268
3538
|
const preempted = preemptedEagerRecords.get(entry.call.id);
|
|
3269
3539
|
if (preempted != null) {
|
|
@@ -3304,7 +3574,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3304
3574
|
);
|
|
3305
3575
|
|
|
3306
3576
|
for (const { entry } of askEntries) {
|
|
3307
|
-
hookRegistry
|
|
3577
|
+
hookRegistry?.clearPendingToolApproval(
|
|
3308
3578
|
approvalReplaySessionId,
|
|
3309
3579
|
createToolApprovalReplayKey(
|
|
3310
3580
|
config,
|
|
@@ -3314,8 +3584,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3314
3584
|
);
|
|
3315
3585
|
}
|
|
3316
3586
|
|
|
3587
|
+
const proposalMatches =
|
|
3588
|
+
approvalReviewEvidence == null ||
|
|
3589
|
+
toolApprovalPayloadMatches(payload, approvalReviewEvidence.payload);
|
|
3590
|
+
if (!proposalMatches) {
|
|
3591
|
+
for (const { entry } of askEntries) {
|
|
3592
|
+
blockEntry(
|
|
3593
|
+
entry,
|
|
3594
|
+
'Reviewed tool proposal changed before resume; retry approval'
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
const decisionEntries = proposalMatches ? askEntries : [];
|
|
3317
3599
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
3318
|
-
|
|
3600
|
+
decisionEntries.map(({ entry }) => entry.call.id!),
|
|
3319
3601
|
resumeValue
|
|
3320
3602
|
);
|
|
3321
3603
|
|
|
@@ -3323,7 +3605,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3323
3605
|
entry,
|
|
3324
3606
|
reason: askReason,
|
|
3325
3607
|
allowedDecisions,
|
|
3326
|
-
} of
|
|
3608
|
+
} of decisionEntries) {
|
|
3327
3609
|
const decision = decisionByCallId.get(entry.call.id!) ?? {
|
|
3328
3610
|
type: 'reject' as const,
|
|
3329
3611
|
reason: 'No decision provided for tool approval',
|
|
@@ -3688,8 +3970,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3688
3970
|
// match it at the top level too.
|
|
3689
3971
|
agentId: this.executingAgentId,
|
|
3690
3972
|
callerCapabilityProjection:
|
|
3691
|
-
|
|
3692
|
-
configurable:
|
|
3973
|
+
this.getCallerCapabilityProjectionSnapshot(),
|
|
3974
|
+
configurable: stripHostExecutionConfig(
|
|
3693
3975
|
config.configurable as Record<string, unknown> | undefined
|
|
3694
3976
|
),
|
|
3695
3977
|
metadata: config.metadata as
|
|
@@ -4348,12 +4630,44 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4348
4630
|
const settledBatchResults =
|
|
4349
4631
|
baseContext.replayBatchKey == null
|
|
4350
4632
|
? undefined
|
|
4351
|
-
: this.
|
|
4633
|
+
: (this.settledDirectResultsByBatch.get(baseContext.replayBatchKey) ??
|
|
4634
|
+
new Map<string, SettledDirectToolResult>());
|
|
4635
|
+
const cacheResult = (
|
|
4636
|
+
call: ToolCall,
|
|
4637
|
+
position: number,
|
|
4638
|
+
result: SettledDirectToolResult
|
|
4639
|
+
): void => {
|
|
4640
|
+
if (
|
|
4641
|
+
baseContext.replayBatchKey == null ||
|
|
4642
|
+
settledBatchResults == null
|
|
4643
|
+
) {
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
settledBatchResults.set(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]), result);
|
|
4647
|
+
this.settledDirectResultsByBatch.set(
|
|
4648
|
+
baseContext.replayBatchKey,
|
|
4649
|
+
settledBatchResults
|
|
4650
|
+
);
|
|
4651
|
+
};
|
|
4352
4652
|
const restoreResult = (
|
|
4353
4653
|
call: ToolCall,
|
|
4354
4654
|
result: SettledDirectToolResult
|
|
4355
4655
|
): SettledDirectToolResult => {
|
|
4356
4656
|
baseContext.additionalContextsSink?.push(...result.additionalContexts);
|
|
4657
|
+
const turn = result.turn;
|
|
4658
|
+
if (turn != null) {
|
|
4659
|
+
this.recordToolUsageTurn(call.name, turn, call.id);
|
|
4660
|
+
}
|
|
4661
|
+
const refMeta = isBaseMessage(result.output) ? result.output.additional_kwargs as t.ToolMessageRefMetadata : undefined;
|
|
4662
|
+
if (refMeta?._refKey != null && result.referenceContent != null) {
|
|
4663
|
+
this.toolOutputRegistry?.set(baseContext.batchScopeId, refMeta._refKey, result.referenceContent);
|
|
4664
|
+
if (isBaseMessage(result.output)) {
|
|
4665
|
+
result = { ...result, output: new ToolMessage({
|
|
4666
|
+
...(result.output as ToolMessage),
|
|
4667
|
+
additional_kwargs: { ...result.output.additional_kwargs, _refScope: baseContext.batchScopeId },
|
|
4668
|
+
}) };
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4357
4671
|
if (
|
|
4358
4672
|
call.id != null &&
|
|
4359
4673
|
result.resolvedArgs != null &&
|
|
@@ -4367,11 +4681,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4367
4681
|
call: ToolCall,
|
|
4368
4682
|
position: number
|
|
4369
4683
|
): Promise<SettledDirectToolResult> => {
|
|
4370
|
-
const cachedResult =
|
|
4371
|
-
typeof call.id === 'string'
|
|
4372
|
-
? settledBatchResults?.get(call.id)
|
|
4373
|
-
: undefined;
|
|
4684
|
+
const cachedResult = settledBatchResults?.get(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]));
|
|
4374
4685
|
if (cachedResult != null) {
|
|
4686
|
+
if (cachedResult.proposal.name !== call.name ||
|
|
4687
|
+
!recordArgsEqual(cachedResult.proposal.args, call.args as Record<string, unknown>)) {
|
|
4688
|
+
throw new Error('Cannot change an already completed tool call during approval replay');
|
|
4689
|
+
}
|
|
4375
4690
|
return restoreResult(call, cachedResult);
|
|
4376
4691
|
}
|
|
4377
4692
|
const additionalContexts: string[] = [];
|
|
@@ -4384,11 +4699,19 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4384
4699
|
call.id == null
|
|
4385
4700
|
? undefined
|
|
4386
4701
|
: baseContext.resolvedArgsByCallId?.get(call.id);
|
|
4387
|
-
|
|
4702
|
+
const result: SettledDirectToolResult = {
|
|
4703
|
+
proposal: structuredClone({ name: call.name, args: call.args as Record<string, unknown> }),
|
|
4704
|
+
turn: call.id == null ? undefined : this.toolCallTurns.get(call.id) ?? this.directPathTurns.get(call.id),
|
|
4388
4705
|
output,
|
|
4389
4706
|
additionalContexts,
|
|
4390
4707
|
...(resolvedArgs == null ? {} : { resolvedArgs }),
|
|
4391
|
-
}
|
|
4708
|
+
};
|
|
4709
|
+
const refMeta = isBaseMessage(output) ? output.additional_kwargs as t.ToolMessageRefMetadata : undefined;
|
|
4710
|
+
if (refMeta?._refKey != null) {
|
|
4711
|
+
result.referenceContent = this.toolOutputRegistry?.get(refMeta._refScope, refMeta._refKey);
|
|
4712
|
+
}
|
|
4713
|
+
cacheResult(call, position, result);
|
|
4714
|
+
return restoreResult(call, result);
|
|
4392
4715
|
};
|
|
4393
4716
|
|
|
4394
4717
|
const interrupting = this.interruptingToolNames;
|
|
@@ -4397,10 +4720,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4397
4720
|
directCalls.some((call) => interrupting.has(call.name));
|
|
4398
4721
|
|
|
4399
4722
|
if (!hasInterrupting) {
|
|
4400
|
-
const results = await Promise.
|
|
4723
|
+
const results = await Promise.allSettled(
|
|
4401
4724
|
directCalls.map((call, i) => runOne(call, i))
|
|
4402
4725
|
);
|
|
4403
|
-
|
|
4726
|
+
this.throwIfBreakerTripped(config);
|
|
4727
|
+
const failed = results.find((result) => result.status === 'rejected' && !isGraphInterrupt(result.reason)) ??
|
|
4728
|
+
results.find((result) => result.status === 'rejected');
|
|
4729
|
+
if (failed?.status === 'rejected') {
|
|
4730
|
+
throw failed.reason;
|
|
4731
|
+
}
|
|
4732
|
+
return results.map((result) => {
|
|
4733
|
+
if (result.status === 'rejected') {
|
|
4734
|
+
throw result.reason;
|
|
4735
|
+
}
|
|
4736
|
+
return result.value.output;
|
|
4737
|
+
});
|
|
4404
4738
|
}
|
|
4405
4739
|
|
|
4406
4740
|
const outputs: (BaseMessage | Command)[] = new Array(directCalls.length);
|
|
@@ -4437,21 +4771,6 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4437
4771
|
}
|
|
4438
4772
|
const position = interruptingPositions[i];
|
|
4439
4773
|
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
4774
|
}
|
|
4456
4775
|
if (hasInterruptingError) {
|
|
4457
4776
|
if (isGraphInterrupt(interruptingError)) {
|
|
@@ -4549,7 +4868,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4549
4868
|
}
|
|
4550
4869
|
|
|
4551
4870
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4552
|
-
protected async run(input: any, config: RunnableConfig): Promise<T> {
|
|
4871
|
+
protected async run(input: any, config: RunnableConfig, referenceReplay: { state?: ToolOutputReferenceState } = {}): Promise<T> {
|
|
4553
4872
|
/**
|
|
4554
4873
|
* Breaker read once at batch entry: every downstream path (direct
|
|
4555
4874
|
* `tool.invoke` runtimes and the ON_TOOL_EXECUTE dispatch) inherits this
|
|
@@ -4581,7 +4900,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4581
4900
|
* otherwise perform side effects on a failed run. */
|
|
4582
4901
|
if (
|
|
4583
4902
|
composedSignal?.aborted === true &&
|
|
4584
|
-
(composedSignal.reason instanceof StreamLimitExceededError ||
|
|
4903
|
+
(composedSignal.reason instanceof StreamLimitExceededError ||
|
|
4904
|
+
composedSignal.reason instanceof PreparedSubagentError)
|
|
4585
4905
|
) {
|
|
4586
4906
|
throw composedSignal.reason;
|
|
4587
4907
|
}
|
|
@@ -4606,11 +4926,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4606
4926
|
* every subsequent registry call on this batch.
|
|
4607
4927
|
*/
|
|
4608
4928
|
const incomingRunId = config.configurable?.run_id as string | undefined;
|
|
4609
|
-
const incomingThreadId = config.configurable?.thread_id as
|
|
4610
|
-
| string
|
|
4611
|
-
| undefined;
|
|
4612
4929
|
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
4613
|
-
const
|
|
4930
|
+
const resumedReferenceState = referenceReplay.state;
|
|
4931
|
+
const replayInputSnapshot = resumedReferenceState == null ? undefined :
|
|
4932
|
+
this.toolOutputRegistry?.resumeBatch(batchScopeId, resumedReferenceState);
|
|
4933
|
+
referenceReplay.state = resumedReferenceState ?? this.toolOutputRegistry?.snapshotState(batchScopeId);
|
|
4934
|
+
const turn = resumedReferenceState?.turnCounter ?? this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
4614
4935
|
let outputs: (BaseMessage | Command)[];
|
|
4615
4936
|
let replayBatchKey: string | undefined;
|
|
4616
4937
|
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
@@ -4628,6 +4949,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4628
4949
|
batchIndices: [0],
|
|
4629
4950
|
turn,
|
|
4630
4951
|
batchScopeId,
|
|
4952
|
+
preBatchSnapshot: replayInputSnapshot,
|
|
4631
4953
|
});
|
|
4632
4954
|
}
|
|
4633
4955
|
// Same per-batch sink the message-state branches use so
|
|
@@ -4645,12 +4967,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4645
4967
|
? findAssistantBatch(sendMessages as BaseMessage[])
|
|
4646
4968
|
: undefined;
|
|
4647
4969
|
const sendReplayBatchKey =
|
|
4648
|
-
sendAssistantBatch == null
|
|
4970
|
+
sendAssistantBatch == null || getToolBatchReplayScope(config) == null
|
|
4649
4971
|
? undefined
|
|
4650
4972
|
: getAssistantBatchReplayKey(
|
|
4651
4973
|
sendAssistantBatch,
|
|
4652
|
-
|
|
4653
|
-
incomingThreadId
|
|
4974
|
+
getToolBatchReplayScope(config)
|
|
4654
4975
|
);
|
|
4655
4976
|
const sendOutput = await this.runDirectToolWithLifecycleHooks(
|
|
4656
4977
|
input.lg_tool_call,
|
|
@@ -4659,6 +4980,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4659
4980
|
batchIndex: 0,
|
|
4660
4981
|
turn,
|
|
4661
4982
|
batchScopeId,
|
|
4983
|
+
preBatchSnapshot: replayInputSnapshot,
|
|
4662
4984
|
resolvedArgsByCallId,
|
|
4663
4985
|
errorOwnership,
|
|
4664
4986
|
additionalContextsSink: directAdditionalContexts,
|
|
@@ -4713,10 +5035,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4713
5035
|
throw new Error('ToolNode only accepts AIMessages as input.');
|
|
4714
5036
|
}
|
|
4715
5037
|
const aiMessage = assistantBatch.message;
|
|
4716
|
-
replayBatchKey = getAssistantBatchReplayKey(
|
|
5038
|
+
replayBatchKey = getToolBatchReplayScope(config) == null ? undefined : getAssistantBatchReplayKey(
|
|
4717
5039
|
assistantBatch,
|
|
4718
|
-
|
|
4719
|
-
incomingThreadId
|
|
5040
|
+
getToolBatchReplayScope(config)
|
|
4720
5041
|
);
|
|
4721
5042
|
|
|
4722
5043
|
if (this.loadRuntimeTools) {
|
|
@@ -4728,6 +5049,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4728
5049
|
this.applyToolExecutionOverrides();
|
|
4729
5050
|
}
|
|
4730
5051
|
|
|
5052
|
+
const replayCallPositions = new Map((aiMessage.tool_calls ?? []).map((call, i) => [call, i]));
|
|
4731
5053
|
const filteredCalls =
|
|
4732
5054
|
aiMessage.tool_calls?.filter((call) => {
|
|
4733
5055
|
/**
|
|
@@ -4737,7 +5059,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4737
5059
|
* which are executed by the provider's API and don't require invocation
|
|
4738
5060
|
*/
|
|
4739
5061
|
return (
|
|
4740
|
-
(call.id == null || !toolMessageIds.has(call.id)) &&
|
|
5062
|
+
(call.id == null || call.id === '' || !toolMessageIds.has(call.id)) &&
|
|
4741
5063
|
!(
|
|
4742
5064
|
call.id?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) ??
|
|
4743
5065
|
false
|
|
@@ -4865,6 +5187,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4865
5187
|
batchIndices: eventEntries.map((entry) => entry.batchIndex),
|
|
4866
5188
|
turn,
|
|
4867
5189
|
batchScopeId,
|
|
5190
|
+
preBatchSnapshot: replayInputSnapshot,
|
|
4868
5191
|
});
|
|
4869
5192
|
}
|
|
4870
5193
|
|
|
@@ -4872,6 +5195,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4872
5195
|
const directIndices = directEntries.map((e) => e.batchIndex);
|
|
4873
5196
|
const eventCalls = eventEntries.map((e) => e.call);
|
|
4874
5197
|
const eventIndices = eventEntries.map((e) => e.batchIndex);
|
|
5198
|
+
const approvalScopeCallIds = new Set(
|
|
5199
|
+
filteredCalls.flatMap((call) => (call.id == null ? [] : [call.id]))
|
|
5200
|
+
);
|
|
4875
5201
|
|
|
4876
5202
|
/**
|
|
4877
5203
|
* Snapshot the event calls' args against the *pre-batch*
|
|
@@ -4898,7 +5224,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4898
5224
|
* same-turn direct outputs.
|
|
4899
5225
|
*/
|
|
4900
5226
|
const preBatchSnapshot =
|
|
4901
|
-
this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
5227
|
+
replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
4902
5228
|
if (preBatchSnapshot != null) {
|
|
4903
5229
|
for (const entry of eventEntries) {
|
|
4904
5230
|
if (entry.call.id != null) {
|
|
@@ -4937,19 +5263,40 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4937
5263
|
preBatchSnapshot,
|
|
4938
5264
|
additionalContextsSink: directAdditionalContexts,
|
|
4939
5265
|
replayBatchKey,
|
|
5266
|
+
replayCallPositions,
|
|
5267
|
+
approvalScopeCallIds,
|
|
4940
5268
|
runInput: input as T,
|
|
4941
5269
|
}
|
|
4942
5270
|
)
|
|
4943
5271
|
: [];
|
|
4944
5272
|
|
|
4945
|
-
|
|
5273
|
+
const settledDirectResults =
|
|
5274
|
+
replayBatchKey == null ? undefined : this.settledDirectResultsByBatch.get(replayBatchKey);
|
|
5275
|
+
const unhandledDirectCalls: ToolCall[] = [];
|
|
5276
|
+
const unhandledDirectOutputs: (BaseMessage | Command)[] = [];
|
|
5277
|
+
for (let i = 0; i < directCalls.length; i++) {
|
|
5278
|
+
const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])!));
|
|
5279
|
+
if (settled?.completionHandled === true) {
|
|
5280
|
+
continue;
|
|
5281
|
+
}
|
|
5282
|
+
unhandledDirectCalls.push(directCalls[i]);
|
|
5283
|
+
unhandledDirectOutputs.push(directOutputs[i]);
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
if (unhandledDirectCalls.length > 0) {
|
|
4946
5287
|
await this.handleRunToolCompletions(
|
|
4947
|
-
|
|
4948
|
-
|
|
5288
|
+
unhandledDirectCalls,
|
|
5289
|
+
unhandledDirectOutputs,
|
|
4949
5290
|
config,
|
|
4950
5291
|
resolvedArgsByCallId,
|
|
4951
5292
|
errorOwnership
|
|
4952
5293
|
);
|
|
5294
|
+
for (let i = 0; i < directCalls.length; i++) {
|
|
5295
|
+
const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])!));
|
|
5296
|
+
if (settled != null) {
|
|
5297
|
+
settled.completionHandled = true;
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
4953
5300
|
}
|
|
4954
5301
|
|
|
4955
5302
|
const eventResult =
|
|
@@ -4998,8 +5345,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4998
5345
|
// leak a sibling's just-registered output into a sister
|
|
4999
5346
|
// call's args mid-await (Codex P1 #18).
|
|
5000
5347
|
const preBatchSnapshot =
|
|
5001
|
-
this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
5348
|
+
replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
5002
5349
|
const directAdditionalContexts: string[] = [];
|
|
5350
|
+
const approvalScopeCallIds = new Set(
|
|
5351
|
+
filteredCalls.flatMap((call) => (call.id == null ? [] : [call.id]))
|
|
5352
|
+
);
|
|
5003
5353
|
const toolOutputs = await this.runDirectBatchInterruptSafe(
|
|
5004
5354
|
filteredCalls,
|
|
5005
5355
|
filteredCalls.map((_call, i) => i),
|
|
@@ -5012,6 +5362,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
5012
5362
|
preBatchSnapshot,
|
|
5013
5363
|
additionalContextsSink: directAdditionalContexts,
|
|
5014
5364
|
replayBatchKey,
|
|
5365
|
+
replayCallPositions,
|
|
5366
|
+
approvalScopeCallIds,
|
|
5015
5367
|
runInput: input as T,
|
|
5016
5368
|
}
|
|
5017
5369
|
);
|
|
@@ -5071,7 +5423,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
5071
5423
|
|
|
5072
5424
|
if (!outputs.some(isCommand)) {
|
|
5073
5425
|
if (replayBatchKey != null) {
|
|
5074
|
-
this.
|
|
5426
|
+
this.settledDirectResultsByBatch.delete(replayBatchKey);
|
|
5075
5427
|
}
|
|
5076
5428
|
return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
|
|
5077
5429
|
}
|
|
@@ -5214,7 +5566,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
5214
5566
|
}
|
|
5215
5567
|
|
|
5216
5568
|
if (replayBatchKey != null) {
|
|
5217
|
-
this.
|
|
5569
|
+
this.settledDirectResultsByBatch.delete(replayBatchKey);
|
|
5218
5570
|
}
|
|
5219
5571
|
return combinedOutputs as T;
|
|
5220
5572
|
}
|