@librechat/agents 3.3.13 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +121 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +35 -206
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +121 -241
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +19 -12
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/llm/mistral/index.cjs +26 -0
- package/dist/cjs/llm/mistral/index.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +82 -80
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs +19 -19
- package/dist/cjs/llm/providers.cjs.map +1 -1
- package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
- package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
- package/dist/cjs/llm/stream/smoother.cjs +369 -0
- package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
- package/dist/cjs/llm/vertexai/index.cjs +13 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +25 -16
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +82 -15
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs +316 -94
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +664 -80
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/cjs/utils/tokens.cjs +76 -29
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +121 -27
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +34 -205
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +120 -240
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +19 -12
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/llm/mistral/index.mjs +26 -0
- package/dist/esm/llm/mistral/index.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +82 -80
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs +3 -3
- package/dist/esm/llm/providers.mjs.map +1 -1
- package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
- package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
- package/dist/esm/llm/stream/smoother.mjs +365 -0
- package/dist/esm/llm/stream/smoother.mjs.map +1 -0
- package/dist/esm/llm/vertexai/index.mjs +13 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +13 -11
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +83 -16
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs +317 -95
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +665 -81
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/esm/utils/tokens.mjs +76 -30
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +19 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +2 -0
- package/dist/types/llm/mistral/index.d.ts +11 -0
- package/dist/types/llm/openai/index.d.ts +20 -4
- package/dist/types/llm/openrouter/index.d.ts +4 -1
- package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
- package/dist/types/llm/stream/smoother.d.ts +95 -0
- package/dist/types/llm/vertexai/index.d.ts +2 -0
- package/dist/types/run.d.ts +4 -1
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +49 -7
- package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +5 -5
- package/dist/types/types/hitl.d.ts +15 -0
- package/dist/types/types/llm.d.ts +21 -14
- package/dist/types/utils/tokens.d.ts +10 -0
- package/package.json +6 -2
- package/src/graphs/Graph.ts +257 -52
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +8 -0
- package/src/llm/anthropic/index.ts +85 -354
- package/src/llm/bedrock/index.ts +174 -390
- package/src/llm/bedrock/llm.spec.ts +2 -0
- package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
- package/src/llm/custom-chat-models.smoke.test.ts +16 -1
- package/src/llm/google/index.ts +17 -18
- package/src/llm/google/streamSmoothing.test.ts +121 -0
- package/src/llm/mistral/index.ts +33 -0
- package/src/llm/mistral/streamSmoothing.test.ts +97 -0
- package/src/llm/openai/deepseek.test.ts +56 -0
- package/src/llm/openai/index.ts +119 -126
- package/src/llm/openrouter/index.ts +4 -1
- package/src/llm/providers.ts +3 -3
- package/src/llm/stream/chunkAdapters.test.ts +202 -0
- package/src/llm/stream/chunkAdapters.ts +317 -0
- package/src/llm/stream/reassembly.test.ts +241 -0
- package/src/llm/stream/smoother.bench.test.ts +155 -0
- package/src/llm/stream/smoother.test.ts +519 -0
- package/src/llm/stream/smoother.ts +574 -0
- package/src/llm/vertexai/index.ts +17 -1
- package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +164 -18
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/specs/subagent.test.ts +44 -0
- package/src/specs/tokens.test.ts +159 -14
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/ToolNode.ts +631 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +590 -7
- package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/subagent/SubagentExecutor.ts +1438 -136
- package/src/tools/subagent/SubagentReplay.ts +575 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +5 -5
- package/src/types/hitl.ts +16 -0
- package/src/types/llm.ts +53 -36
- package/src/utils/tokens.ts +115 -30
package/src/tools/ToolNode.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
END,
|
|
13
13
|
Send,
|
|
14
14
|
Command,
|
|
15
|
+
GraphInterrupt,
|
|
15
16
|
isCommand,
|
|
16
17
|
interrupt,
|
|
17
18
|
isGraphInterrupt,
|
|
@@ -25,8 +26,8 @@ import type {
|
|
|
25
26
|
ToolRuntime,
|
|
26
27
|
StructuredToolInterface,
|
|
27
28
|
} from '@langchain/core/tools';
|
|
28
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
29
29
|
import type { LangGraphRunnableConfig } from '@langchain/langgraph';
|
|
30
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
30
31
|
import type {
|
|
31
32
|
ToolOutputResolveView,
|
|
32
33
|
PreResolvedArgsMap,
|
|
@@ -34,11 +35,18 @@ import type {
|
|
|
34
35
|
ResolveResult,
|
|
35
36
|
ResolveOptions,
|
|
36
37
|
} from '@/tools/toolOutputReferences';
|
|
38
|
+
import type {
|
|
39
|
+
ReplayableSubagentTool,
|
|
40
|
+
SubagentResumeManifest,
|
|
41
|
+
SubagentToolNodeResumeState,
|
|
42
|
+
} from '@/tools/subagent/SubagentReplay';
|
|
37
43
|
import type {
|
|
38
44
|
HookRegistry,
|
|
39
45
|
AggregatedHookResult,
|
|
40
46
|
PostToolBatchEntry,
|
|
47
|
+
ToolApprovalReplayKey,
|
|
41
48
|
} from '@/hooks';
|
|
49
|
+
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
42
50
|
import type * as t from '@/types';
|
|
43
51
|
import {
|
|
44
52
|
cloneToolMessageWithContent,
|
|
@@ -49,6 +57,11 @@ import {
|
|
|
49
57
|
serializeStructuredValueBounded,
|
|
50
58
|
serializeToolContentBounded,
|
|
51
59
|
} from '@/utils/toolContent';
|
|
60
|
+
import {
|
|
61
|
+
attachSubagentResumeManifest,
|
|
62
|
+
SUBAGENT_PARENT_BATCH_CONFIG_KEY,
|
|
63
|
+
SUBAGENT_REPLAY_CONTROLLER,
|
|
64
|
+
} from '@/tools/subagent/SubagentReplay';
|
|
52
65
|
import {
|
|
53
66
|
INTENT_ARG,
|
|
54
67
|
readOutcomeFields,
|
|
@@ -73,17 +86,16 @@ import {
|
|
|
73
86
|
calculateMaxToolResultChars,
|
|
74
87
|
truncateToolResultContent,
|
|
75
88
|
} from '@/utils/truncation';
|
|
89
|
+
import {
|
|
90
|
+
StreamLimitExceededError,
|
|
91
|
+
RUN_BREAKER_SCOPE_CONFIG_KEY,
|
|
92
|
+
} from '@/llm/streamLimits';
|
|
76
93
|
import {
|
|
77
94
|
resolveLocalToolRegistry,
|
|
78
95
|
resolveLocalExecutionTools,
|
|
79
96
|
} from '@/tools/local';
|
|
80
97
|
import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
81
98
|
import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
|
|
82
|
-
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
83
|
-
import {
|
|
84
|
-
StreamLimitExceededError,
|
|
85
|
-
RUN_BREAKER_SCOPE_CONFIG_KEY,
|
|
86
|
-
} from '@/llm/streamLimits';
|
|
87
99
|
|
|
88
100
|
/** Host-facing batch requests must not carry the batch's breaker scope —
|
|
89
101
|
* hosts spread `configurable` into their own run configs. */
|
|
@@ -99,7 +111,42 @@ function stripRunBreakerScope(
|
|
|
99
111
|
import { convertInjectedMessages } from '@/messages/injected';
|
|
100
112
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
101
113
|
import { RunnableCallable, composeAbortSignals } from '@/utils';
|
|
102
|
-
import {
|
|
114
|
+
import {
|
|
115
|
+
executeHooks,
|
|
116
|
+
TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
|
|
117
|
+
} from '@/hooks';
|
|
118
|
+
|
|
119
|
+
function createToolApprovalReplayKey(
|
|
120
|
+
config: RunnableConfig,
|
|
121
|
+
agentId: string,
|
|
122
|
+
toolUseId: string
|
|
123
|
+
): ToolApprovalReplayKey {
|
|
124
|
+
const configuredScope =
|
|
125
|
+
config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
|
|
126
|
+
const threadId = config.configurable?.thread_id;
|
|
127
|
+
let executionScope = '';
|
|
128
|
+
if (typeof configuredScope === 'string') {
|
|
129
|
+
executionScope = configuredScope;
|
|
130
|
+
} else if (typeof threadId === 'string') {
|
|
131
|
+
executionScope = threadId;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
executionScope,
|
|
135
|
+
agentId,
|
|
136
|
+
toolUseId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getToolApprovalReplaySessionId(
|
|
141
|
+
config: RunnableConfig,
|
|
142
|
+
hookSessionId: string
|
|
143
|
+
): string {
|
|
144
|
+
const configuredScope =
|
|
145
|
+
config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
|
|
146
|
+
return typeof configuredScope === 'string' && configuredScope.length > 0
|
|
147
|
+
? configuredScope
|
|
148
|
+
: hookSessionId;
|
|
149
|
+
}
|
|
103
150
|
|
|
104
151
|
/**
|
|
105
152
|
* Per-call batch context for `runTool`. Bundles every optional
|
|
@@ -145,6 +192,8 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
145
192
|
* contract for hosts relying on it for policy / recovery guidance.
|
|
146
193
|
*/
|
|
147
194
|
additionalContextsSink?: string[];
|
|
195
|
+
/** Stable identity of the assistant tool-call batch across HITL replay. */
|
|
196
|
+
replayBatchKey?: string;
|
|
148
197
|
/**
|
|
149
198
|
* Graph state the ToolNode was invoked with, threaded from `run()`
|
|
150
199
|
* so `tool.invoke` can forward it as langgraph 1.4's `runtime.state`
|
|
@@ -156,6 +205,12 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
156
205
|
errorOwnership?: ToolErrorOwnership;
|
|
157
206
|
};
|
|
158
207
|
|
|
208
|
+
type SettledDirectToolResult = {
|
|
209
|
+
output: BaseMessage | Command;
|
|
210
|
+
additionalContexts: string[];
|
|
211
|
+
resolvedArgs?: Record<string, unknown>;
|
|
212
|
+
};
|
|
213
|
+
|
|
159
214
|
/**
|
|
160
215
|
* Batch-local record of who owns each failed call's completion event.
|
|
161
216
|
*
|
|
@@ -315,6 +370,38 @@ function describeOfferedShape(value: unknown): string {
|
|
|
315
370
|
return typeof value;
|
|
316
371
|
}
|
|
317
372
|
|
|
373
|
+
type AssistantBatch = {
|
|
374
|
+
message: AIMessage;
|
|
375
|
+
index: number;
|
|
376
|
+
messageCount: number;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
function findAssistantBatch(
|
|
380
|
+
messages: BaseMessage[]
|
|
381
|
+
): AssistantBatch | undefined {
|
|
382
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
383
|
+
const message = messages[i];
|
|
384
|
+
if (isAIMessage(message)) {
|
|
385
|
+
return { message, index: i, messageCount: messages.length };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getAssistantBatchReplayKey(
|
|
392
|
+
batch: AssistantBatch,
|
|
393
|
+
runId: string | undefined,
|
|
394
|
+
threadId: string | undefined
|
|
395
|
+
): string {
|
|
396
|
+
return JSON.stringify([
|
|
397
|
+
runId ?? null,
|
|
398
|
+
threadId ?? null,
|
|
399
|
+
batch.message.id ?? null,
|
|
400
|
+
batch.index,
|
|
401
|
+
batch.messageCount,
|
|
402
|
+
]);
|
|
403
|
+
}
|
|
404
|
+
|
|
318
405
|
/**
|
|
319
406
|
* Per-entry record collected during PreToolUse hook handling for tool
|
|
320
407
|
* calls that need human approval. Carries everything
|
|
@@ -342,10 +429,14 @@ type AskEntry = {
|
|
|
342
429
|
* needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
|
|
343
430
|
*/
|
|
344
431
|
function buildToolApprovalInterruptPayload(
|
|
345
|
-
askEntries: ReadonlyArray<AskEntry
|
|
432
|
+
askEntries: ReadonlyArray<AskEntry>,
|
|
433
|
+
hookSessionId?: string
|
|
346
434
|
): t.ToolApprovalInterruptPayload {
|
|
347
435
|
return {
|
|
348
436
|
type: 'tool_approval',
|
|
437
|
+
...(hookSessionId == null || hookSessionId === ''
|
|
438
|
+
? {}
|
|
439
|
+
: { hook_session_id: hookSessionId }),
|
|
349
440
|
action_requests: askEntries.map(({ entry, reason }) => {
|
|
350
441
|
const request: t.ToolApprovalRequest = {
|
|
351
442
|
tool_call_id: entry.call.id!,
|
|
@@ -574,6 +665,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
574
665
|
* the Run ends.
|
|
575
666
|
*/
|
|
576
667
|
private directPathTurns: Map<string, number> = new Map();
|
|
668
|
+
/** Terminal results from interrupting siblings that must survive a
|
|
669
|
+
* LangGraph replay of the containing ToolNode. Includes the sidecar data
|
|
670
|
+
* the fresh batch needs for hook-context injection and completion events. */
|
|
671
|
+
private settledInterruptingResults = new Map<
|
|
672
|
+
string,
|
|
673
|
+
Map<string, SettledDirectToolResult>
|
|
674
|
+
>();
|
|
577
675
|
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
578
676
|
private toolRegistry?: t.LCToolRegistry;
|
|
579
677
|
/** Cached programmatic tools (computed once on first PTC call) */
|
|
@@ -931,16 +1029,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
931
1029
|
}
|
|
932
1030
|
|
|
933
1031
|
/**
|
|
934
|
-
* Flush
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
* call IDs (Codex P2 #33). Hosts can also call this directly if
|
|
1032
|
+
* Flush per-Run direct replay state. Called by the Graph at end-of-Run via
|
|
1033
|
+
* `clearHeavyState`. The state intentionally survives `run()` re-entry so
|
|
1034
|
+
* interrupt + resume keeps both original turn slots and terminal sibling
|
|
1035
|
+
* outputs, but it would otherwise grow linearly and could collide across
|
|
1036
|
+
* Runs if a provider reused call IDs. Hosts can also call this directly if
|
|
940
1037
|
* they reuse a ToolNode across batches outside of a Graph.
|
|
941
1038
|
*/
|
|
942
1039
|
clearDirectPathTurns(): void {
|
|
943
1040
|
this.directPathTurns.clear();
|
|
1041
|
+
this.settledInterruptingResults.clear();
|
|
944
1042
|
}
|
|
945
1043
|
|
|
946
1044
|
/**
|
|
@@ -977,6 +1075,35 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
977
1075
|
return new Map(this.toolUsageCount); // Return a copy
|
|
978
1076
|
}
|
|
979
1077
|
|
|
1078
|
+
createSubagentResumeState(): SubagentToolNodeResumeState {
|
|
1079
|
+
return {
|
|
1080
|
+
stateKey: JSON.stringify([
|
|
1081
|
+
this.executingAgentId ?? '',
|
|
1082
|
+
this.agentId ?? '',
|
|
1083
|
+
this.name,
|
|
1084
|
+
]),
|
|
1085
|
+
toolUsageCounts: [...this.toolUsageCount].map(([toolName, count]) => ({
|
|
1086
|
+
toolName,
|
|
1087
|
+
count,
|
|
1088
|
+
})),
|
|
1089
|
+
directPathTurns: [...this.directPathTurns].map(([toolCallId, turn]) => ({
|
|
1090
|
+
toolCallId,
|
|
1091
|
+
turn,
|
|
1092
|
+
})),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
restoreSubagentResumeState(state: SubagentToolNodeResumeState): void {
|
|
1097
|
+
this.toolUsageCount.clear();
|
|
1098
|
+
for (const { toolName, count } of state.toolUsageCounts) {
|
|
1099
|
+
this.toolUsageCount.set(toolName, count);
|
|
1100
|
+
}
|
|
1101
|
+
this.directPathTurns.clear();
|
|
1102
|
+
for (const { toolCallId, turn } of state.directPathTurns) {
|
|
1103
|
+
this.directPathTurns.set(toolCallId, turn);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
980
1107
|
private recordToolUsageTurn(
|
|
981
1108
|
toolName: string,
|
|
982
1109
|
turn: number,
|
|
@@ -1475,9 +1602,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1475
1602
|
* `blockDirectCall`, `respond` returns the host-supplied
|
|
1476
1603
|
* `responseText` as a synthetic success ToolMessage,
|
|
1477
1604
|
* `edit` re-runs with edited args. LangGraph re-enters
|
|
1478
|
-
* ToolNode.run from the start on resume
|
|
1479
|
-
* again
|
|
1480
|
-
*
|
|
1605
|
+
* ToolNode.run from the start on resume. Reusable hooks fire
|
|
1606
|
+
* again; a consumed one-shot hook replays its pending approval
|
|
1607
|
+
* result. In both cases `interrupt()` consumes the resume value.
|
|
1481
1608
|
* • When HITL is off: collapses to a fail-closed deny (matches
|
|
1482
1609
|
* the rest of the SDK's HITL-disabled default). One-time
|
|
1483
1610
|
* warning logged so hosts notice the gap.
|
|
@@ -1501,21 +1628,128 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1501
1628
|
config: RunnableConfig,
|
|
1502
1629
|
batchContext: RunToolBatchContext<T> = {}
|
|
1503
1630
|
): Promise<BaseMessage | Command> {
|
|
1631
|
+
const replayController = (
|
|
1632
|
+
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
1633
|
+
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
1634
|
+
const replayConfig =
|
|
1635
|
+
replayController == null || batchContext.replayBatchKey == null
|
|
1636
|
+
? config
|
|
1637
|
+
: {
|
|
1638
|
+
...config,
|
|
1639
|
+
configurable: {
|
|
1640
|
+
...config.configurable,
|
|
1641
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: batchContext.replayBatchKey,
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1644
|
+
const settledOutput = await replayController?.getSettledOutput(
|
|
1645
|
+
call,
|
|
1646
|
+
replayConfig
|
|
1647
|
+
);
|
|
1648
|
+
if (settledOutput != null) {
|
|
1649
|
+
if (
|
|
1650
|
+
batchContext.additionalContextsSink != null &&
|
|
1651
|
+
settledOutput.additionalContexts.length > 0
|
|
1652
|
+
) {
|
|
1653
|
+
batchContext.additionalContextsSink.push(
|
|
1654
|
+
...settledOutput.additionalContexts
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
if (
|
|
1658
|
+
batchContext.resolvedArgsByCallId != null &&
|
|
1659
|
+
call.id != null &&
|
|
1660
|
+
settledOutput.resolvedArgs != null
|
|
1661
|
+
) {
|
|
1662
|
+
batchContext.resolvedArgsByCallId.set(
|
|
1663
|
+
call.id,
|
|
1664
|
+
settledOutput.resolvedArgs
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const refMeta = settledOutput.output.additional_kwargs as
|
|
1668
|
+
| t.ToolMessageRefMetadata
|
|
1669
|
+
| undefined;
|
|
1670
|
+
if (
|
|
1671
|
+
this.toolOutputRegistry != null &&
|
|
1672
|
+
refMeta?._refKey != null &&
|
|
1673
|
+
settledOutput.referenceContent != null
|
|
1674
|
+
) {
|
|
1675
|
+
this.toolOutputRegistry.set(
|
|
1676
|
+
refMeta._refScope,
|
|
1677
|
+
refMeta._refKey,
|
|
1678
|
+
settledOutput.referenceContent
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
return settledOutput.output;
|
|
1682
|
+
}
|
|
1683
|
+
const replayAdditionalContexts: string[] = [];
|
|
1684
|
+
const persistOutput = async (
|
|
1685
|
+
output: ToolMessage,
|
|
1686
|
+
terminalArgs?: Record<string, unknown>
|
|
1687
|
+
): Promise<ToolMessage> => {
|
|
1688
|
+
const refMeta = output.additional_kwargs as
|
|
1689
|
+
| t.ToolMessageRefMetadata
|
|
1690
|
+
| undefined;
|
|
1691
|
+
if (
|
|
1692
|
+
terminalArgs != null &&
|
|
1693
|
+
call.id != null &&
|
|
1694
|
+
batchContext.resolvedArgsByCallId != null
|
|
1695
|
+
) {
|
|
1696
|
+
batchContext.resolvedArgsByCallId.set(call.id, terminalArgs);
|
|
1697
|
+
}
|
|
1698
|
+
const resolvedArgs =
|
|
1699
|
+
terminalArgs ??
|
|
1700
|
+
(call.id == null
|
|
1701
|
+
? undefined
|
|
1702
|
+
: batchContext.resolvedArgsByCallId?.get(call.id));
|
|
1703
|
+
const referenceContent =
|
|
1704
|
+
this.toolOutputRegistry == null || refMeta?._refKey == null
|
|
1705
|
+
? undefined
|
|
1706
|
+
: this.toolOutputRegistry.get(refMeta._refScope, refMeta._refKey);
|
|
1707
|
+
await replayController?.persistSettledOutput(call, replayConfig, {
|
|
1708
|
+
output,
|
|
1709
|
+
additionalContexts: replayAdditionalContexts,
|
|
1710
|
+
...(resolvedArgs == null ? {} : { resolvedArgs }),
|
|
1711
|
+
...(referenceContent == null ? {} : { referenceContent }),
|
|
1712
|
+
});
|
|
1713
|
+
return output;
|
|
1714
|
+
};
|
|
1504
1715
|
const runId = (config.configurable?.run_id as string | undefined) ?? '';
|
|
1716
|
+
const threadId = config.configurable?.thread_id as string | undefined;
|
|
1505
1717
|
const hookRegistry = this.hookRegistry;
|
|
1506
1718
|
const hasPreHook = hookRegistry?.hasHookFor('PreToolUse', runId) === true;
|
|
1719
|
+
const approvalReplayKey =
|
|
1720
|
+
call.id == null || call.id === ''
|
|
1721
|
+
? undefined
|
|
1722
|
+
: createToolApprovalReplayKey(
|
|
1723
|
+
config,
|
|
1724
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
1725
|
+
call.id
|
|
1726
|
+
);
|
|
1727
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(
|
|
1728
|
+
config,
|
|
1729
|
+
runId
|
|
1730
|
+
);
|
|
1731
|
+
const pendingApproval =
|
|
1732
|
+
approvalReplayKey == null
|
|
1733
|
+
? undefined
|
|
1734
|
+
: hookRegistry?.getPendingToolApproval(
|
|
1735
|
+
approvalReplaySessionId,
|
|
1736
|
+
approvalReplayKey
|
|
1737
|
+
);
|
|
1507
1738
|
const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
1508
1739
|
const hasFailureHook =
|
|
1509
1740
|
hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
1510
1741
|
|
|
1511
1742
|
if (
|
|
1512
1743
|
hookRegistry == null ||
|
|
1513
|
-
(!hasPreHook &&
|
|
1744
|
+
(!hasPreHook &&
|
|
1745
|
+
pendingApproval == null &&
|
|
1746
|
+
!hasPostHook &&
|
|
1747
|
+
!hasFailureHook)
|
|
1514
1748
|
) {
|
|
1515
|
-
|
|
1749
|
+
const output = await this.runTool(call, replayConfig, batchContext);
|
|
1750
|
+
return output instanceof ToolMessage ? persistOutput(output) : output;
|
|
1516
1751
|
}
|
|
1517
1752
|
|
|
1518
|
-
const threadId = config.configurable?.thread_id as string | undefined;
|
|
1519
1753
|
const registryRunId =
|
|
1520
1754
|
batchContext.batchScopeId ??
|
|
1521
1755
|
(config.configurable?.run_id as string | undefined);
|
|
@@ -1583,7 +1817,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1583
1817
|
}
|
|
1584
1818
|
|
|
1585
1819
|
let effectiveCall = call;
|
|
1586
|
-
if (hasPreHook) {
|
|
1820
|
+
if (hasPreHook || pendingApproval != null) {
|
|
1587
1821
|
const preResult = await executeHooks({
|
|
1588
1822
|
registry: hookRegistry,
|
|
1589
1823
|
input: {
|
|
@@ -1600,6 +1834,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1600
1834
|
},
|
|
1601
1835
|
sessionId: runId,
|
|
1602
1836
|
matchQuery: call.name,
|
|
1837
|
+
onceReplayKey: approvalReplayKey,
|
|
1838
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
1603
1839
|
}).catch(() => undefined);
|
|
1604
1840
|
|
|
1605
1841
|
if (preResult != null) {
|
|
@@ -1607,11 +1843,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1607
1843
|
// the per-batch sink so the caller materializes them as a
|
|
1608
1844
|
// HumanMessage for the next model turn — same shape as the
|
|
1609
1845
|
// event-driven path's `injected[]`. Codex P2 #39.
|
|
1610
|
-
if (
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
) {
|
|
1614
|
-
batchContext.additionalContextsSink.push(
|
|
1846
|
+
if (preResult.additionalContexts.length > 0) {
|
|
1847
|
+
replayAdditionalContexts.push(...preResult.additionalContexts);
|
|
1848
|
+
batchContext.additionalContextsSink?.push(
|
|
1615
1849
|
...preResult.additionalContexts
|
|
1616
1850
|
);
|
|
1617
1851
|
}
|
|
@@ -1626,14 +1860,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1626
1860
|
}
|
|
1627
1861
|
|
|
1628
1862
|
if (preResult.decision === 'deny') {
|
|
1629
|
-
return
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1863
|
+
return persistOutput(
|
|
1864
|
+
this.blockDirectCall({
|
|
1865
|
+
call,
|
|
1866
|
+
resolvedArgs,
|
|
1867
|
+
reason: preResult.reason ?? 'Blocked by hook',
|
|
1868
|
+
hookRegistry,
|
|
1869
|
+
runId,
|
|
1870
|
+
threadId,
|
|
1871
|
+
}),
|
|
1872
|
+
effectiveCall.args as Record<string, unknown>
|
|
1873
|
+
);
|
|
1637
1874
|
}
|
|
1638
1875
|
|
|
1639
1876
|
if (preResult.decision === 'ask') {
|
|
@@ -1644,27 +1881,49 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1644
1881
|
preResult.reason,
|
|
1645
1882
|
call.name
|
|
1646
1883
|
);
|
|
1647
|
-
return
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1884
|
+
return persistOutput(
|
|
1885
|
+
this.blockDirectCall({
|
|
1886
|
+
call,
|
|
1887
|
+
resolvedArgs,
|
|
1888
|
+
reason,
|
|
1889
|
+
hookRegistry,
|
|
1890
|
+
runId,
|
|
1891
|
+
threadId,
|
|
1892
|
+
}),
|
|
1893
|
+
effectiveCall.args as Record<string, unknown>
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
const toolCallId = call.id;
|
|
1897
|
+
if (
|
|
1898
|
+
toolCallId == null ||
|
|
1899
|
+
toolCallId === '' ||
|
|
1900
|
+
approvalReplayKey == null
|
|
1901
|
+
) {
|
|
1902
|
+
return persistOutput(
|
|
1903
|
+
this.blockDirectCall({
|
|
1904
|
+
call,
|
|
1905
|
+
resolvedArgs,
|
|
1906
|
+
reason:
|
|
1907
|
+
'Tool approval requires a non-empty tool call ID — failing closed',
|
|
1908
|
+
hookRegistry,
|
|
1909
|
+
runId,
|
|
1910
|
+
threadId,
|
|
1911
|
+
}),
|
|
1912
|
+
effectiveCall.args as Record<string, unknown>
|
|
1913
|
+
);
|
|
1655
1914
|
}
|
|
1656
1915
|
|
|
1657
1916
|
// Raise a single-tool tool_approval interrupt. LangGraph
|
|
1658
1917
|
// throws on the first execution (host gets the interrupt)
|
|
1659
1918
|
// and returns the resume value on re-entry. Because direct
|
|
1660
|
-
// tools re-enter the entire ToolNode.run on resume
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1663
|
-
// distinguishes "first call asking" from "second call after
|
|
1664
|
-
// approve/reject". We anchor `interrupt()` against the
|
|
1919
|
+
// tools re-enter the entire ToolNode.run on resume. Reusable
|
|
1920
|
+
// hooks fire again; a consumed one-shot hook instead replays
|
|
1921
|
+
// the pending approval. We anchor `interrupt()` against the
|
|
1665
1922
|
// node's RunnableConfig the same way `dispatchToolEvents`
|
|
1666
|
-
// does
|
|
1667
|
-
//
|
|
1923
|
+
// does. A one-shot hook's pending contribution reconstructs this
|
|
1924
|
+
// same ask entry without dispatching the consumed hook again.
|
|
1925
|
+
// ToolNode disables LangSmith tracing, so the AsyncLocalStorage
|
|
1926
|
+
// frame must be re-established here.
|
|
1668
1927
|
const askEntry: AskEntry = {
|
|
1669
1928
|
entry: {
|
|
1670
1929
|
call: effectiveCall,
|
|
@@ -1674,7 +1933,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1674
1933
|
reason: preResult.reason,
|
|
1675
1934
|
allowedDecisions: preResult.allowedDecisions,
|
|
1676
1935
|
};
|
|
1677
|
-
const payload = buildToolApprovalInterruptPayload([askEntry]);
|
|
1936
|
+
const payload = buildToolApprovalInterruptPayload([askEntry], runId);
|
|
1678
1937
|
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
1679
1938
|
config,
|
|
1680
1939
|
() =>
|
|
@@ -1683,11 +1942,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1683
1942
|
t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap
|
|
1684
1943
|
>(payload)
|
|
1685
1944
|
);
|
|
1945
|
+
hookRegistry.clearPendingToolApproval(
|
|
1946
|
+
approvalReplaySessionId,
|
|
1947
|
+
approvalReplayKey
|
|
1948
|
+
);
|
|
1686
1949
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
1687
|
-
[
|
|
1950
|
+
[toolCallId],
|
|
1688
1951
|
resumeValue
|
|
1689
1952
|
);
|
|
1690
|
-
const decision = decisionByCallId.get(
|
|
1953
|
+
const decision = decisionByCallId.get(toolCallId) ?? {
|
|
1691
1954
|
type: 'reject' as const,
|
|
1692
1955
|
reason: 'No decision provided for tool approval',
|
|
1693
1956
|
};
|
|
@@ -1700,47 +1963,63 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1700
1963
|
declaredType as t.ToolApprovalDecisionType
|
|
1701
1964
|
))
|
|
1702
1965
|
) {
|
|
1703
|
-
return
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1966
|
+
return persistOutput(
|
|
1967
|
+
this.blockDirectCall({
|
|
1968
|
+
call,
|
|
1969
|
+
resolvedArgs,
|
|
1970
|
+
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${preResult.allowedDecisions.join(', ')}] — failing closed`,
|
|
1971
|
+
hookRegistry,
|
|
1972
|
+
runId,
|
|
1973
|
+
threadId,
|
|
1974
|
+
}),
|
|
1975
|
+
effectiveCall.args as Record<string, unknown>
|
|
1976
|
+
);
|
|
1711
1977
|
}
|
|
1712
1978
|
|
|
1713
1979
|
if (decision.type === 'reject') {
|
|
1714
|
-
return
|
|
1715
|
-
|
|
1716
|
-
resolvedArgs,
|
|
1717
|
-
reason: decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
1718
|
-
hookRegistry,
|
|
1719
|
-
runId,
|
|
1720
|
-
threadId,
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
if (decision.type === 'respond') {
|
|
1725
|
-
const responseText = (decision as { responseText?: unknown })
|
|
1726
|
-
.responseText;
|
|
1727
|
-
if (typeof responseText !== 'string') {
|
|
1728
|
-
return this.blockDirectCall({
|
|
1980
|
+
return persistOutput(
|
|
1981
|
+
this.blockDirectCall({
|
|
1729
1982
|
call,
|
|
1730
1983
|
resolvedArgs,
|
|
1731
1984
|
reason:
|
|
1732
|
-
|
|
1985
|
+
decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
1733
1986
|
hookRegistry,
|
|
1734
1987
|
runId,
|
|
1735
1988
|
threadId,
|
|
1736
|
-
})
|
|
1989
|
+
}),
|
|
1990
|
+
effectiveCall.args as Record<string, unknown>
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (decision.type === 'respond') {
|
|
1995
|
+
const responseText = (decision as { responseText?: unknown })
|
|
1996
|
+
.responseText;
|
|
1997
|
+
if (typeof responseText !== 'string') {
|
|
1998
|
+
return persistOutput(
|
|
1999
|
+
this.blockDirectCall({
|
|
2000
|
+
call,
|
|
2001
|
+
resolvedArgs,
|
|
2002
|
+
reason:
|
|
2003
|
+
'Approval payload `respond` was missing a string `responseText`',
|
|
2004
|
+
hookRegistry,
|
|
2005
|
+
runId,
|
|
2006
|
+
threadId,
|
|
2007
|
+
}),
|
|
2008
|
+
effectiveCall.args as Record<string, unknown>
|
|
2009
|
+
);
|
|
1737
2010
|
}
|
|
1738
|
-
return
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2011
|
+
return persistOutput(
|
|
2012
|
+
new ToolMessage({
|
|
2013
|
+
status: 'success',
|
|
2014
|
+
content: truncateToolResultContent(
|
|
2015
|
+
responseText,
|
|
2016
|
+
this.maxToolResultChars
|
|
2017
|
+
),
|
|
2018
|
+
name: call.name,
|
|
2019
|
+
tool_call_id: call.id ?? '',
|
|
2020
|
+
}),
|
|
2021
|
+
effectiveCall.args as Record<string, unknown>
|
|
2022
|
+
);
|
|
1744
2023
|
}
|
|
1745
2024
|
|
|
1746
2025
|
if (decision.type === 'edit') {
|
|
@@ -1759,13 +2038,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1759
2038
|
typeof updatedInput !== 'object' ||
|
|
1760
2039
|
Array.isArray(updatedInput)
|
|
1761
2040
|
) {
|
|
1762
|
-
return
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2041
|
+
return persistOutput(
|
|
2042
|
+
new ToolMessage({
|
|
2043
|
+
status: 'error',
|
|
2044
|
+
content:
|
|
2045
|
+
'Decision "edit" missing object updatedInput — failing closed.',
|
|
2046
|
+
name: call.name,
|
|
2047
|
+
tool_call_id: call.id ?? '',
|
|
2048
|
+
}),
|
|
2049
|
+
effectiveCall.args as Record<string, unknown>
|
|
2050
|
+
);
|
|
1769
2051
|
}
|
|
1770
2052
|
effectiveCall = {
|
|
1771
2053
|
...call,
|
|
@@ -1773,6 +2055,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1773
2055
|
};
|
|
1774
2056
|
// fall through to executing the edited call
|
|
1775
2057
|
}
|
|
2058
|
+
if (declaredType !== 'approve' && declaredType !== 'edit') {
|
|
2059
|
+
const unknownType =
|
|
2060
|
+
typeof declaredType === 'string' ? declaredType : '<missing>';
|
|
2061
|
+
return persistOutput(
|
|
2062
|
+
this.blockDirectCall({
|
|
2063
|
+
call,
|
|
2064
|
+
resolvedArgs,
|
|
2065
|
+
reason: `Unknown approval decision type "${unknownType}" — failing closed`,
|
|
2066
|
+
hookRegistry,
|
|
2067
|
+
runId,
|
|
2068
|
+
threadId,
|
|
2069
|
+
}),
|
|
2070
|
+
effectiveCall.args as Record<string, unknown>
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
1776
2073
|
// 'approve' (or 'edit' after applying edits) → fall through
|
|
1777
2074
|
}
|
|
1778
2075
|
}
|
|
@@ -1799,7 +2096,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1799
2096
|
);
|
|
1800
2097
|
}
|
|
1801
2098
|
|
|
1802
|
-
const output = await this.runTool(effectiveCall,
|
|
2099
|
+
const output = await this.runTool(effectiveCall, replayConfig, {
|
|
1803
2100
|
...batchContext,
|
|
1804
2101
|
usageCount,
|
|
1805
2102
|
});
|
|
@@ -1839,14 +2136,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1839
2136
|
}).catch(() => undefined);
|
|
1840
2137
|
if (
|
|
1841
2138
|
failureResult != null &&
|
|
1842
|
-
batchContext.additionalContextsSink != null &&
|
|
1843
2139
|
failureResult.additionalContexts.length > 0
|
|
1844
2140
|
) {
|
|
1845
|
-
|
|
2141
|
+
replayAdditionalContexts.push(...failureResult.additionalContexts);
|
|
2142
|
+
batchContext.additionalContextsSink?.push(
|
|
1846
2143
|
...failureResult.additionalContexts
|
|
1847
2144
|
);
|
|
1848
2145
|
}
|
|
1849
|
-
return output;
|
|
2146
|
+
return persistOutput(output);
|
|
1850
2147
|
}
|
|
1851
2148
|
|
|
1852
2149
|
if (output.status !== 'error' && hasPostHook) {
|
|
@@ -1871,12 +2168,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1871
2168
|
|
|
1872
2169
|
// Forward additionalContexts from the PostToolUse hook into
|
|
1873
2170
|
// the per-batch sink (Codex P2 #39).
|
|
1874
|
-
if (
|
|
1875
|
-
postResult
|
|
1876
|
-
batchContext.additionalContextsSink
|
|
1877
|
-
postResult.additionalContexts.length > 0
|
|
1878
|
-
) {
|
|
1879
|
-
batchContext.additionalContextsSink.push(
|
|
2171
|
+
if (postResult != null && postResult.additionalContexts.length > 0) {
|
|
2172
|
+
replayAdditionalContexts.push(...postResult.additionalContexts);
|
|
2173
|
+
batchContext.additionalContextsSink?.push(
|
|
1880
2174
|
...postResult.additionalContexts
|
|
1881
2175
|
);
|
|
1882
2176
|
}
|
|
@@ -1888,7 +2182,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1888
2182
|
'PostToolUse updatedOutput for a computer call must be a valid screenshot URL or screenshot content block.'
|
|
1889
2183
|
);
|
|
1890
2184
|
}
|
|
1891
|
-
return
|
|
2185
|
+
return persistOutput(
|
|
2186
|
+
cloneToolMessageWithContent(output, postResult.updatedOutput)
|
|
2187
|
+
);
|
|
1892
2188
|
}
|
|
1893
2189
|
// Keep the tool-output registry in sync with what the model
|
|
1894
2190
|
// actually sees. Without this, `runTool` already registered
|
|
@@ -1918,11 +2214,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1918
2214
|
replaced.registryContent
|
|
1919
2215
|
);
|
|
1920
2216
|
}
|
|
1921
|
-
return
|
|
2217
|
+
return persistOutput(
|
|
2218
|
+
cloneToolMessageWithContent(output, replaced.content)
|
|
2219
|
+
);
|
|
1922
2220
|
}
|
|
1923
2221
|
}
|
|
1924
2222
|
|
|
1925
|
-
return output;
|
|
2223
|
+
return persistOutput(output);
|
|
1926
2224
|
}
|
|
1927
2225
|
|
|
1928
2226
|
/**
|
|
@@ -2381,13 +2679,27 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2381
2679
|
errors: [] as string[],
|
|
2382
2680
|
});
|
|
2383
2681
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2682
|
+
const hookRegistry = this.hookRegistry;
|
|
2683
|
+
const approvalReplaySessionId = getToolApprovalReplaySessionId(
|
|
2684
|
+
config,
|
|
2685
|
+
runId
|
|
2686
|
+
);
|
|
2687
|
+
const hasPendingApproval = preToolCalls.some(
|
|
2688
|
+
(entry) =>
|
|
2689
|
+
entry.call.id != null &&
|
|
2690
|
+
hookRegistry?.getPendingToolApproval(
|
|
2691
|
+
approvalReplaySessionId,
|
|
2692
|
+
createToolApprovalReplayKey(
|
|
2693
|
+
config,
|
|
2694
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
2695
|
+
entry.call.id
|
|
2696
|
+
)
|
|
2697
|
+
) != null
|
|
2698
|
+
);
|
|
2699
|
+
if (
|
|
2700
|
+
hookRegistry != null &&
|
|
2701
|
+
(hookRegistry.hasHookFor('PreToolUse', runId) || hasPendingApproval)
|
|
2702
|
+
) {
|
|
2391
2703
|
/**
|
|
2392
2704
|
* Pull each call's prestarted eager record BEFORE awaiting the hooks:
|
|
2393
2705
|
* an async deny must never race the eager host promise into emitting a
|
|
@@ -2415,8 +2727,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2415
2727
|
}
|
|
2416
2728
|
}
|
|
2417
2729
|
const preResults = await Promise.all(
|
|
2418
|
-
preToolCalls.map((entry) =>
|
|
2419
|
-
|
|
2730
|
+
preToolCalls.map((entry) => {
|
|
2731
|
+
const toolUseId = entry.call.id;
|
|
2732
|
+
const approvalReplayKey =
|
|
2733
|
+
toolUseId == null
|
|
2734
|
+
? undefined
|
|
2735
|
+
: createToolApprovalReplayKey(
|
|
2736
|
+
config,
|
|
2737
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
2738
|
+
toolUseId
|
|
2739
|
+
);
|
|
2740
|
+
return executeHooks({
|
|
2420
2741
|
registry: hookRegistry,
|
|
2421
2742
|
input: {
|
|
2422
2743
|
hook_event_name: 'PreToolUse',
|
|
@@ -2432,8 +2753,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2432
2753
|
},
|
|
2433
2754
|
sessionId: runId,
|
|
2434
2755
|
matchQuery: entry.call.name,
|
|
2435
|
-
|
|
2436
|
-
|
|
2756
|
+
onceReplayKey: approvalReplayKey,
|
|
2757
|
+
onceReplaySessionId: approvalReplaySessionId,
|
|
2758
|
+
}).catch((): AggregatedHookResult => HOOK_FALLBACK);
|
|
2759
|
+
})
|
|
2437
2760
|
);
|
|
2438
2761
|
|
|
2439
2762
|
type PendingEntry = (typeof preToolCalls)[number];
|
|
@@ -2653,12 +2976,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2653
2976
|
* pauses, gathers human input, and resumes the run with one
|
|
2654
2977
|
* decision per request. On resume LangGraph re-executes this node
|
|
2655
2978
|
* from the start; `interrupt()` then returns the resume value
|
|
2656
|
-
* instead of throwing
|
|
2657
|
-
*
|
|
2658
|
-
* are pure — see `humanInTheLoop` docs).
|
|
2979
|
+
* instead of throwing. Reusable hooks rebuild their entries, while
|
|
2980
|
+
* consumed one-shot hooks replay their pending approval aggregate.
|
|
2659
2981
|
*/
|
|
2660
2982
|
if (askEntries.length > 0) {
|
|
2661
|
-
const payload = buildToolApprovalInterruptPayload(askEntries);
|
|
2983
|
+
const payload = buildToolApprovalInterruptPayload(askEntries, runId);
|
|
2662
2984
|
|
|
2663
2985
|
/**
|
|
2664
2986
|
* `interrupt()` reads the current `RunnableConfig` from
|
|
@@ -2678,6 +3000,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2678
3000
|
>(payload)
|
|
2679
3001
|
);
|
|
2680
3002
|
|
|
3003
|
+
for (const { entry } of askEntries) {
|
|
3004
|
+
hookRegistry.clearPendingToolApproval(
|
|
3005
|
+
approvalReplaySessionId,
|
|
3006
|
+
createToolApprovalReplayKey(
|
|
3007
|
+
config,
|
|
3008
|
+
this.executingAgentId ?? this.agentId ?? '',
|
|
3009
|
+
entry.call.id!
|
|
3010
|
+
)
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2681
3014
|
const decisionByCallId = normalizeApprovalDecisions(
|
|
2682
3015
|
askEntries.map(({ entry }) => entry.call.id!),
|
|
2683
3016
|
resumeValue
|
|
@@ -3051,7 +3384,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3051
3384
|
// match it at the top level too.
|
|
3052
3385
|
agentId: this.executingAgentId,
|
|
3053
3386
|
configurable: stripRunBreakerScope(
|
|
3054
|
-
|
|
3387
|
+
config.configurable as Record<string, unknown> | undefined
|
|
3055
3388
|
),
|
|
3056
3389
|
metadata: config.metadata as
|
|
3057
3390
|
| Record<string, unknown>
|
|
@@ -3680,16 +4013,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3680
4013
|
* call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
|
|
3681
4014
|
* whole ToolNode *before* any non-interrupting sibling has started — so a
|
|
3682
4015
|
* sibling with real side effects (send_email, billing) never executes on
|
|
3683
|
-
* the first pass.
|
|
3684
|
-
*
|
|
3685
|
-
*
|
|
4016
|
+
* the first pass. Terminal interrupting siblings are cached by call id, so
|
|
4017
|
+
* LangGraph replay reuses their complete lifecycle output instead of
|
|
4018
|
+
* repeating model calls, hooks, or side effects.
|
|
3686
4019
|
*
|
|
3687
4020
|
* Without this ordering, a flat `Promise.all` starts every sibling
|
|
3688
4021
|
* concurrently, so a non-idempotent sibling can complete its side effect
|
|
3689
4022
|
* before the interrupt unwinds and then run a SECOND time on resume — the
|
|
3690
|
-
* duplicate side effect this method exists to prevent.
|
|
3691
|
-
*
|
|
3692
|
-
*
|
|
4023
|
+
* duplicate side effect this method exists to prevent. A tool that actually
|
|
4024
|
+
* suspends re-enters until it reaches a terminal result; siblings that
|
|
4025
|
+
* already settled do not re-enter.
|
|
3693
4026
|
*
|
|
3694
4027
|
* `batchIndices[i]` is `directCalls[i]`'s position within the parent
|
|
3695
4028
|
* ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
|
|
@@ -3703,14 +4036,51 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3703
4036
|
config: RunnableConfig,
|
|
3704
4037
|
baseContext: Omit<RunToolBatchContext<T>, 'batchIndex'>
|
|
3705
4038
|
): Promise<(BaseMessage | Command)[]> {
|
|
3706
|
-
const
|
|
4039
|
+
const settledBatchResults =
|
|
4040
|
+
baseContext.replayBatchKey == null
|
|
4041
|
+
? undefined
|
|
4042
|
+
: this.settledInterruptingResults.get(baseContext.replayBatchKey);
|
|
4043
|
+
const restoreResult = (
|
|
4044
|
+
call: ToolCall,
|
|
4045
|
+
result: SettledDirectToolResult
|
|
4046
|
+
): SettledDirectToolResult => {
|
|
4047
|
+
baseContext.additionalContextsSink?.push(...result.additionalContexts);
|
|
4048
|
+
if (
|
|
4049
|
+
call.id != null &&
|
|
4050
|
+
result.resolvedArgs != null &&
|
|
4051
|
+
baseContext.resolvedArgsByCallId != null
|
|
4052
|
+
) {
|
|
4053
|
+
baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
|
|
4054
|
+
}
|
|
4055
|
+
return result;
|
|
4056
|
+
};
|
|
4057
|
+
const runOne = async (
|
|
3707
4058
|
call: ToolCall,
|
|
3708
4059
|
position: number
|
|
3709
|
-
): Promise<
|
|
3710
|
-
|
|
4060
|
+
): Promise<SettledDirectToolResult> => {
|
|
4061
|
+
const cachedResult =
|
|
4062
|
+
typeof call.id === 'string'
|
|
4063
|
+
? settledBatchResults?.get(call.id)
|
|
4064
|
+
: undefined;
|
|
4065
|
+
if (cachedResult != null) {
|
|
4066
|
+
return restoreResult(call, cachedResult);
|
|
4067
|
+
}
|
|
4068
|
+
const additionalContexts: string[] = [];
|
|
4069
|
+
const output = await this.runDirectToolWithLifecycleHooks(call, config, {
|
|
3711
4070
|
...baseContext,
|
|
3712
4071
|
batchIndex: batchIndices[position],
|
|
4072
|
+
additionalContextsSink: additionalContexts,
|
|
4073
|
+
});
|
|
4074
|
+
const resolvedArgs =
|
|
4075
|
+
call.id == null
|
|
4076
|
+
? undefined
|
|
4077
|
+
: baseContext.resolvedArgsByCallId?.get(call.id);
|
|
4078
|
+
return restoreResult(call, {
|
|
4079
|
+
output,
|
|
4080
|
+
additionalContexts,
|
|
4081
|
+
...(resolvedArgs == null ? {} : { resolvedArgs }),
|
|
3713
4082
|
});
|
|
4083
|
+
};
|
|
3714
4084
|
|
|
3715
4085
|
const interrupting = this.interruptingToolNames;
|
|
3716
4086
|
const hasInterrupting =
|
|
@@ -3718,7 +4088,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3718
4088
|
directCalls.some((call) => interrupting.has(call.name));
|
|
3719
4089
|
|
|
3720
4090
|
if (!hasInterrupting) {
|
|
3721
|
-
|
|
4091
|
+
const results = await Promise.all(
|
|
4092
|
+
directCalls.map((call, i) => runOne(call, i))
|
|
4093
|
+
);
|
|
4094
|
+
return results.map((result) => result.output);
|
|
3722
4095
|
}
|
|
3723
4096
|
|
|
3724
4097
|
const outputs: (BaseMessage | Command)[] = new Array(directCalls.length);
|
|
@@ -3732,14 +4105,94 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3732
4105
|
}
|
|
3733
4106
|
}
|
|
3734
4107
|
|
|
3735
|
-
// Interrupting group first.
|
|
3736
|
-
//
|
|
3737
|
-
|
|
4108
|
+
// Interrupting group first. Wait for every sibling to settle before
|
|
4109
|
+
// propagating an interrupt so approved subagents cannot keep running in
|
|
4110
|
+
// the background while the parent exposes the next pending approval.
|
|
4111
|
+
const interruptingResults = await Promise.allSettled(
|
|
3738
4112
|
interruptingPositions.map((i) => runOne(directCalls[i], i))
|
|
3739
4113
|
);
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
4114
|
+
/** A sibling may have tripped the run-wide breaker while another raised
|
|
4115
|
+
* a GraphInterrupt. Safety failures take precedence over approval pauses
|
|
4116
|
+
* regardless of input order. */
|
|
4117
|
+
this.throwIfBreakerTripped(config);
|
|
4118
|
+
let hasInterruptingError = false;
|
|
4119
|
+
let interruptingError: unknown;
|
|
4120
|
+
for (let i = 0; i < interruptingResults.length; i++) {
|
|
4121
|
+
const result = interruptingResults[i];
|
|
4122
|
+
if (result.status === 'rejected') {
|
|
4123
|
+
if (!hasInterruptingError) {
|
|
4124
|
+
hasInterruptingError = true;
|
|
4125
|
+
interruptingError = result.reason;
|
|
4126
|
+
}
|
|
4127
|
+
continue;
|
|
4128
|
+
}
|
|
4129
|
+
const position = interruptingPositions[i];
|
|
4130
|
+
outputs[position] = result.value.output;
|
|
4131
|
+
const callId = directCalls[position].id;
|
|
4132
|
+
if (
|
|
4133
|
+
baseContext.replayBatchKey != null &&
|
|
4134
|
+
typeof callId === 'string' &&
|
|
4135
|
+
callId !== ''
|
|
4136
|
+
) {
|
|
4137
|
+
const batchResults =
|
|
4138
|
+
this.settledInterruptingResults.get(baseContext.replayBatchKey) ??
|
|
4139
|
+
new Map<string, SettledDirectToolResult>();
|
|
4140
|
+
batchResults.set(callId, result.value);
|
|
4141
|
+
this.settledInterruptingResults.set(
|
|
4142
|
+
baseContext.replayBatchKey,
|
|
4143
|
+
batchResults
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
if (hasInterruptingError) {
|
|
4148
|
+
if (isGraphInterrupt(interruptingError)) {
|
|
4149
|
+
const controllers = new Set<
|
|
4150
|
+
NonNullable<ReplayableSubagentTool[typeof SUBAGENT_REPLAY_CONTROLLER]>
|
|
4151
|
+
>();
|
|
4152
|
+
const parentToolCallIds = new Set<string>();
|
|
4153
|
+
for (const call of directCalls) {
|
|
4154
|
+
const controller = (
|
|
4155
|
+
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
4156
|
+
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
4157
|
+
if (controller?.getResumeManifest != null) {
|
|
4158
|
+
controllers.add(controller);
|
|
4159
|
+
}
|
|
4160
|
+
if (call.id != null && call.id !== '') {
|
|
4161
|
+
parentToolCallIds.add(call.id);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
const executionsByParentCall = new Map<
|
|
4165
|
+
string,
|
|
4166
|
+
SubagentResumeManifest['executions'][number]
|
|
4167
|
+
>();
|
|
4168
|
+
for (const controller of controllers) {
|
|
4169
|
+
const manifest =
|
|
4170
|
+
await controller.getResumeManifest?.(parentToolCallIds);
|
|
4171
|
+
if (manifest != null) {
|
|
4172
|
+
for (const execution of manifest.executions) {
|
|
4173
|
+
executionsByParentCall.set(execution.parentToolCallId, execution);
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
const executions = [...executionsByParentCall.values()];
|
|
4178
|
+
if (executions.length > 0) {
|
|
4179
|
+
const manifest: SubagentResumeManifest = {
|
|
4180
|
+
version: 1,
|
|
4181
|
+
executions,
|
|
4182
|
+
};
|
|
4183
|
+
throw new GraphInterrupt(
|
|
4184
|
+
interruptingError.interrupts.map((pendingInterrupt) => ({
|
|
4185
|
+
...pendingInterrupt,
|
|
4186
|
+
value: attachSubagentResumeManifest(
|
|
4187
|
+
pendingInterrupt.value,
|
|
4188
|
+
manifest
|
|
4189
|
+
),
|
|
4190
|
+
}))
|
|
4191
|
+
);
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
throw interruptingError;
|
|
4195
|
+
}
|
|
3743
4196
|
|
|
3744
4197
|
/** The breaker can trip while the interrupting group is awaited — e.g.
|
|
3745
4198
|
* a tool that ignores cancellation and completes normally. Recheck
|
|
@@ -3751,7 +4204,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3751
4204
|
regularPositions.map((i) => runOne(directCalls[i], i))
|
|
3752
4205
|
);
|
|
3753
4206
|
regularPositions.forEach((i, k) => {
|
|
3754
|
-
outputs[i] = regularOutputs[k];
|
|
4207
|
+
outputs[i] = regularOutputs[k].output;
|
|
3755
4208
|
});
|
|
3756
4209
|
|
|
3757
4210
|
return outputs;
|
|
@@ -3842,9 +4295,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3842
4295
|
* every subsequent registry call on this batch.
|
|
3843
4296
|
*/
|
|
3844
4297
|
const incomingRunId = config.configurable?.run_id as string | undefined;
|
|
4298
|
+
const incomingThreadId = config.configurable?.thread_id as
|
|
4299
|
+
| string
|
|
4300
|
+
| undefined;
|
|
3845
4301
|
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
3846
4302
|
const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
3847
4303
|
let outputs: (BaseMessage | Command)[];
|
|
4304
|
+
let replayBatchKey: string | undefined;
|
|
3848
4305
|
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
3849
4306
|
* the promotion into handoff updates (same-id state copies there would
|
|
3850
4307
|
* otherwise overwrite the replacement message). */
|
|
@@ -3872,6 +4329,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3872
4329
|
// Mirror langgraph's prebuilt ToolNode: the Send-input state is
|
|
3873
4330
|
// the input minus the `lg_tool_call` envelope key.
|
|
3874
4331
|
const { lg_tool_call: _sendToolCall, ...sendState } = input;
|
|
4332
|
+
const sendMessages = (sendState as { messages?: unknown }).messages;
|
|
4333
|
+
const sendAssistantBatch = Array.isArray(sendMessages)
|
|
4334
|
+
? findAssistantBatch(sendMessages as BaseMessage[])
|
|
4335
|
+
: undefined;
|
|
4336
|
+
const sendReplayBatchKey =
|
|
4337
|
+
sendAssistantBatch == null
|
|
4338
|
+
? undefined
|
|
4339
|
+
: getAssistantBatchReplayKey(
|
|
4340
|
+
sendAssistantBatch,
|
|
4341
|
+
incomingRunId,
|
|
4342
|
+
incomingThreadId
|
|
4343
|
+
);
|
|
3875
4344
|
const sendOutput = await this.runDirectToolWithLifecycleHooks(
|
|
3876
4345
|
input.lg_tool_call,
|
|
3877
4346
|
config,
|
|
@@ -3882,6 +4351,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3882
4351
|
resolvedArgsByCallId,
|
|
3883
4352
|
errorOwnership,
|
|
3884
4353
|
additionalContextsSink: directAdditionalContexts,
|
|
4354
|
+
replayBatchKey: sendReplayBatchKey,
|
|
3885
4355
|
runInput: sendState as T,
|
|
3886
4356
|
}
|
|
3887
4357
|
);
|
|
@@ -3925,18 +4395,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3925
4395
|
.map((msg) => (msg as ToolMessage).tool_call_id)
|
|
3926
4396
|
);
|
|
3927
4397
|
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
const message = messages[i];
|
|
3931
|
-
if (isAIMessage(message)) {
|
|
3932
|
-
aiMessage = message;
|
|
3933
|
-
break;
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3936
|
-
|
|
3937
|
-
if (aiMessage == null || !isAIMessage(aiMessage)) {
|
|
4398
|
+
const assistantBatch = findAssistantBatch(messages);
|
|
4399
|
+
if (assistantBatch == null) {
|
|
3938
4400
|
throw new Error('ToolNode only accepts AIMessages as input.');
|
|
3939
4401
|
}
|
|
4402
|
+
const aiMessage = assistantBatch.message;
|
|
4403
|
+
replayBatchKey = getAssistantBatchReplayKey(
|
|
4404
|
+
assistantBatch,
|
|
4405
|
+
incomingRunId,
|
|
4406
|
+
incomingThreadId
|
|
4407
|
+
);
|
|
3940
4408
|
|
|
3941
4409
|
if (this.loadRuntimeTools) {
|
|
3942
4410
|
const { tools, toolMap } = this.loadRuntimeTools(
|
|
@@ -3999,9 +4467,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
3999
4467
|
: (aiMessage.invalid_tool_calls ?? []).filter(
|
|
4000
4468
|
(call) =>
|
|
4001
4469
|
call.id != null &&
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4470
|
+
call.id !== '' &&
|
|
4471
|
+
!toolMessageIds.has(call.id) &&
|
|
4472
|
+
!call.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
|
|
4005
4473
|
);
|
|
4006
4474
|
invalidCallResults = attributableInvalidCalls.map(
|
|
4007
4475
|
(call) =>
|
|
@@ -4156,6 +4624,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4156
4624
|
errorOwnership,
|
|
4157
4625
|
preBatchSnapshot,
|
|
4158
4626
|
additionalContextsSink: directAdditionalContexts,
|
|
4627
|
+
replayBatchKey,
|
|
4159
4628
|
runInput: input as T,
|
|
4160
4629
|
}
|
|
4161
4630
|
)
|
|
@@ -4228,6 +4697,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4228
4697
|
errorOwnership,
|
|
4229
4698
|
preBatchSnapshot,
|
|
4230
4699
|
additionalContextsSink: directAdditionalContexts,
|
|
4700
|
+
replayBatchKey,
|
|
4231
4701
|
runInput: input as T,
|
|
4232
4702
|
}
|
|
4233
4703
|
);
|
|
@@ -4240,7 +4710,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4240
4710
|
);
|
|
4241
4711
|
// Append accumulated additionalContexts as a single
|
|
4242
4712
|
// HumanMessage so the next model turn sees them. Codex P2 #39.
|
|
4243
|
-
const promotedPrefix =
|
|
4713
|
+
const promotedPrefix =
|
|
4714
|
+
promotedAiMessage != null ? [promotedAiMessage] : [];
|
|
4244
4715
|
outputs =
|
|
4245
4716
|
directAdditionalContexts.length > 0
|
|
4246
4717
|
? [
|
|
@@ -4283,6 +4754,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4283
4754
|
}
|
|
4284
4755
|
|
|
4285
4756
|
if (!outputs.some(isCommand)) {
|
|
4757
|
+
if (replayBatchKey != null) {
|
|
4758
|
+
this.settledInterruptingResults.delete(replayBatchKey);
|
|
4759
|
+
}
|
|
4286
4760
|
return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
|
|
4287
4761
|
}
|
|
4288
4762
|
|
|
@@ -4301,8 +4775,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4301
4775
|
isCommand(output)
|
|
4302
4776
|
? patchCommandUpdateForPromotedInvalidCalls(
|
|
4303
4777
|
output,
|
|
4304
|
-
|
|
4305
|
-
|
|
4778
|
+
promotedAiMessage!,
|
|
4779
|
+
invalidCallResults
|
|
4306
4780
|
)
|
|
4307
4781
|
: output
|
|
4308
4782
|
);
|
|
@@ -4423,6 +4897,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4423
4897
|
combinedOutputs.push(parentCommand);
|
|
4424
4898
|
}
|
|
4425
4899
|
|
|
4900
|
+
if (replayBatchKey != null) {
|
|
4901
|
+
this.settledInterruptingResults.delete(replayBatchKey);
|
|
4902
|
+
}
|
|
4426
4903
|
return combinedOutputs as T;
|
|
4427
4904
|
}
|
|
4428
4905
|
|
|
@@ -4498,8 +4975,7 @@ function sanitizeInvalidToolUseBlocks(
|
|
|
4498
4975
|
/** `name` normalizes with the SAME fallback the promoted `tool_calls`
|
|
4499
4976
|
* entry uses — a nameless block would fail provider validation on its
|
|
4500
4977
|
* own even with a valid input. */
|
|
4501
|
-
const nameIsValid =
|
|
4502
|
-
typeof toolUse.name === 'string' && toolUse.name !== '';
|
|
4978
|
+
const nameIsValid = typeof toolUse.name === 'string' && toolUse.name !== '';
|
|
4503
4979
|
if (inputIsObject && nameIsValid) {
|
|
4504
4980
|
return block;
|
|
4505
4981
|
}
|
|
@@ -4571,9 +5047,7 @@ function patchCommandUpdateForPromotedInvalidCalls(
|
|
|
4571
5047
|
if (!isAIMessage(msg) || msg.id !== promoted.id) {
|
|
4572
5048
|
return msg;
|
|
4573
5049
|
}
|
|
4574
|
-
const existingIds = new Set(
|
|
4575
|
-
(msg.tool_calls ?? []).map((call) => call.id)
|
|
4576
|
-
);
|
|
5050
|
+
const existingIds = new Set((msg.tool_calls ?? []).map((call) => call.id));
|
|
4577
5051
|
const promotedEntries = invalidResults
|
|
4578
5052
|
.filter((result) => !existingIds.has(result.tool_call_id))
|
|
4579
5053
|
.map((result) => ({
|