@librechat/agents 3.1.68 → 3.1.71-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs +23 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +16 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +136 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +162 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +276 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
- package/dist/cjs/hooks/matchers.cjs +256 -0
- package/dist/cjs/hooks/matchers.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs +27 -0
- package/dist/cjs/hooks/types.cjs.map +1 -0
- package/dist/cjs/main.cjs +57 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +74 -12
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +9 -2
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +115 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +44 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +208 -0
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/CodeExecutor.cjs +0 -9
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +43 -0
- package/dist/cjs/tools/ReadFile.cjs.map +1 -0
- package/dist/cjs/tools/SkillTool.cjs +50 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -0
- package/dist/cjs/tools/SubagentTool.cjs +92 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +746 -174
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +2 -13
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +84 -0
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +475 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -0
- package/dist/cjs/utils/truncation.cjs +28 -0
- package/dist/cjs/utils/truncation.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +23 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +15 -2
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +136 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +160 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +273 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -0
- package/dist/esm/hooks/matchers.mjs +251 -0
- package/dist/esm/hooks/matchers.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs +25 -0
- package/dist/esm/hooks/types.mjs.map +1 -0
- package/dist/esm/main.mjs +13 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +66 -4
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +9 -2
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +115 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +44 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +200 -0
- package/dist/esm/tools/BashExecutor.mjs.map +1 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +278 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/CodeExecutor.mjs +0 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -24
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +38 -0
- package/dist/esm/tools/ReadFile.mjs.map +1 -0
- package/dist/esm/tools/SkillTool.mjs +45 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -0
- package/dist/esm/tools/SubagentTool.mjs +85 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +748 -176
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +3 -14
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +82 -0
- package/dist/esm/tools/skillCatalog.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +468 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -0
- package/dist/esm/utils/truncation.mjs +27 -1
- package/dist/esm/utils/truncation.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +6 -0
- package/dist/types/common/enum.d.ts +10 -2
- package/dist/types/graphs/Graph.d.ts +23 -0
- package/dist/types/hooks/HookRegistry.d.ts +56 -0
- package/dist/types/hooks/executeHooks.d.ts +79 -0
- package/dist/types/hooks/index.d.ts +6 -0
- package/dist/types/hooks/matchers.d.ts +95 -0
- package/dist/types/hooks/types.d.ts +320 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/run.d.ts +2 -0
- package/dist/types/summarization/node.d.ts +2 -0
- package/dist/types/tools/BashExecutor.d.ts +76 -0
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
- package/dist/types/tools/ReadFile.d.ts +28 -0
- package/dist/types/tools/SkillTool.d.ts +40 -0
- package/dist/types/tools/SubagentTool.d.ts +36 -0
- package/dist/types/tools/ToolNode.d.ts +109 -4
- package/dist/types/tools/ToolSearch.d.ts +2 -2
- package/dist/types/tools/skillCatalog.d.ts +19 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/tools/toolOutputReferences.d.ts +205 -0
- package/dist/types/types/graph.d.ts +61 -2
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/run.d.ts +28 -0
- package/dist/types/types/skill.d.ts +9 -0
- package/dist/types/types/tools.d.ts +108 -10
- package/dist/types/utils/truncation.d.ts +21 -0
- package/package.json +5 -1
- package/src/agents/AgentContext.ts +26 -2
- package/src/common/enum.ts +15 -1
- package/src/graphs/Graph.ts +161 -0
- package/src/hooks/HookRegistry.ts +208 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
- package/src/hooks/__tests__/compactHooks.test.ts +214 -0
- package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
- package/src/hooks/__tests__/integration.test.ts +337 -0
- package/src/hooks/__tests__/matchers.test.ts +238 -0
- package/src/hooks/__tests__/toolHooks.test.ts +669 -0
- package/src/hooks/executeHooks.ts +375 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/matchers.ts +280 -0
- package/src/hooks/types.ts +404 -0
- package/src/index.ts +10 -0
- package/src/messages/format.ts +74 -4
- package/src/messages/formatAgentMessages.skills.test.ts +334 -0
- package/src/messages/prune.ts +9 -2
- package/src/run.ts +130 -0
- package/src/scripts/multi-agent-subagent.ts +246 -0
- package/src/scripts/programmatic_exec.ts +1 -10
- package/src/scripts/subagent-event-driven-debug.ts +190 -0
- package/src/scripts/subagent-tools-debug.ts +160 -0
- package/src/scripts/test_code_api.ts +0 -7
- package/src/scripts/tool_search.ts +1 -10
- package/src/specs/prune.test.ts +413 -0
- package/src/specs/subagent.test.ts +305 -0
- package/src/summarization/node.ts +53 -0
- package/src/tools/BashExecutor.ts +238 -0
- package/src/tools/BashProgrammaticToolCalling.ts +381 -0
- package/src/tools/CodeExecutor.ts +0 -11
- package/src/tools/ProgrammaticToolCalling.ts +4 -29
- package/src/tools/ReadFile.ts +39 -0
- package/src/tools/SkillTool.ts +46 -0
- package/src/tools/SubagentTool.ts +100 -0
- package/src/tools/ToolNode.ts +999 -214
- package/src/tools/ToolSearch.ts +3 -19
- package/src/tools/__tests__/BashExecutor.test.ts +36 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
- package/src/tools/__tests__/ReadFile.test.ts +44 -0
- package/src/tools/__tests__/SkillTool.test.ts +442 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
- package/src/tools/__tests__/SubagentTool.test.ts +149 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +1395 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
- package/src/tools/__tests__/skillCatalog.test.ts +161 -0
- package/src/tools/__tests__/subagentHooks.test.ts +215 -0
- package/src/tools/__tests__/toolOutputReferences.test.ts +415 -0
- package/src/tools/skillCatalog.ts +126 -0
- package/src/tools/subagent/SubagentExecutor.ts +676 -0
- package/src/tools/subagent/index.ts +13 -0
- package/src/tools/toolOutputReferences.ts +590 -0
- package/src/types/graph.ts +80 -1
- package/src/types/index.ts +1 -0
- package/src/types/run.ts +28 -0
- package/src/types/skill.ts +11 -0
- package/src/types/tools.ts +112 -10
- package/src/utils/__tests__/truncation.test.ts +66 -0
- package/src/utils/truncation.ts +30 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { isBaseMessage, ToolMessage, isAIMessage } from '@langchain/core/messages';
|
|
1
|
+
import { isBaseMessage, ToolMessage, HumanMessage, isAIMessage } from '@langchain/core/messages';
|
|
2
2
|
import { isCommand, isGraphInterrupt, Command, Send, END } from '@langchain/langgraph';
|
|
3
|
-
import { Constants, GraphEvents } from '../common/enum.mjs';
|
|
3
|
+
import { Constants, CODE_EXECUTION_TOOLS, GraphEvents } from '../common/enum.mjs';
|
|
4
4
|
import 'nanoid';
|
|
5
5
|
import '../messages/core.mjs';
|
|
6
6
|
import { calculateMaxToolResultChars, truncateToolResultContent } from '../utils/truncation.mjs';
|
|
@@ -9,6 +9,8 @@ import 'uuid';
|
|
|
9
9
|
import { RunnableCallable } from '../utils/run.mjs';
|
|
10
10
|
import 'ai-tokenizer';
|
|
11
11
|
import 'zod-to-json-schema';
|
|
12
|
+
import { executeHooks } from '../hooks/executeHooks.mjs';
|
|
13
|
+
import { ToolOutputReferenceRegistry, annotateToolOutputWithReference, buildReferenceKey } from './toolOutputReferences.mjs';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Helper to check if a value is a Send object
|
|
@@ -16,6 +18,32 @@ import 'zod-to-json-schema';
|
|
|
16
18
|
function isSend(value) {
|
|
17
19
|
return value instanceof Send;
|
|
18
20
|
}
|
|
21
|
+
/** Merges code execution session context into the sessions map. */
|
|
22
|
+
function updateCodeSession(sessions, sessionId, files) {
|
|
23
|
+
const newFiles = files ?? [];
|
|
24
|
+
const existingSession = sessions.get(Constants.EXECUTE_CODE);
|
|
25
|
+
const existingFiles = existingSession?.files ?? [];
|
|
26
|
+
if (newFiles.length > 0) {
|
|
27
|
+
const filesWithSession = newFiles.map((file) => ({
|
|
28
|
+
...file,
|
|
29
|
+
session_id: sessionId,
|
|
30
|
+
}));
|
|
31
|
+
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
32
|
+
const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
|
|
33
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
34
|
+
session_id: sessionId,
|
|
35
|
+
files: [...filteredExisting, ...filesWithSession],
|
|
36
|
+
lastUpdated: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
41
|
+
session_id: sessionId,
|
|
42
|
+
files: existingFiles,
|
|
43
|
+
lastUpdated: Date.now(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
19
47
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
48
|
class ToolNode extends RunnableCallable {
|
|
21
49
|
toolMap;
|
|
@@ -41,7 +69,29 @@ class ToolNode extends RunnableCallable {
|
|
|
41
69
|
directToolNames;
|
|
42
70
|
/** Maximum characters allowed in a single tool result before truncation. */
|
|
43
71
|
maxToolResultChars;
|
|
44
|
-
|
|
72
|
+
/** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
|
|
73
|
+
hookRegistry;
|
|
74
|
+
/**
|
|
75
|
+
* Registry of tool outputs keyed by `tool<idx>turn<turn>`.
|
|
76
|
+
*
|
|
77
|
+
* Populated only when `toolOutputReferences.enabled` is true. The
|
|
78
|
+
* registry owns the run-scoped state (turn counter, last-seen runId,
|
|
79
|
+
* warn-once memo, stored outputs), so sharing a single instance
|
|
80
|
+
* across multiple ToolNodes in a run lets cross-agent `{{…}}`
|
|
81
|
+
* references resolve — which is why multi-agent graphs pass the
|
|
82
|
+
* *same* instance to every ToolNode they compile rather than each
|
|
83
|
+
* ToolNode building its own.
|
|
84
|
+
*/
|
|
85
|
+
toolOutputRegistry;
|
|
86
|
+
/**
|
|
87
|
+
* Monotonic counter used to mint a unique scope id for anonymous
|
|
88
|
+
* batches (ones invoked without a `run_id` in
|
|
89
|
+
* `config.configurable`). Each such batch gets its own registry
|
|
90
|
+
* partition so concurrent anonymous invocations can't delete each
|
|
91
|
+
* other's in-flight state.
|
|
92
|
+
*/
|
|
93
|
+
anonBatchCounter = 0;
|
|
94
|
+
constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, toolOutputReferences, toolOutputRegistry, }) {
|
|
45
95
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
46
96
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
47
97
|
this.toolCallStepIds = toolCallStepIds;
|
|
@@ -56,6 +106,38 @@ class ToolNode extends RunnableCallable {
|
|
|
56
106
|
this.directToolNames = directToolNames;
|
|
57
107
|
this.maxToolResultChars =
|
|
58
108
|
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
|
|
109
|
+
this.hookRegistry = hookRegistry;
|
|
110
|
+
/**
|
|
111
|
+
* Precedence: an explicitly passed `toolOutputRegistry` instance
|
|
112
|
+
* wins over a config object so a host (`Graph`) can share one
|
|
113
|
+
* registry across many ToolNodes. When only the config is
|
|
114
|
+
* provided (direct ToolNode usage), build a local registry so
|
|
115
|
+
* the feature still works without graph-level plumbing. Registry
|
|
116
|
+
* caps are intentionally decoupled from `maxToolResultChars`:
|
|
117
|
+
* the registry stores the raw untruncated output so a later
|
|
118
|
+
* `{{…}}` substitution pipes the full payload into the next
|
|
119
|
+
* tool, even when the LLM saw a truncated preview.
|
|
120
|
+
*/
|
|
121
|
+
if (toolOutputRegistry != null) {
|
|
122
|
+
this.toolOutputRegistry = toolOutputRegistry;
|
|
123
|
+
}
|
|
124
|
+
else if (toolOutputReferences?.enabled === true) {
|
|
125
|
+
this.toolOutputRegistry = new ToolOutputReferenceRegistry({
|
|
126
|
+
maxOutputSize: toolOutputReferences.maxOutputSize,
|
|
127
|
+
maxTotalSize: toolOutputReferences.maxTotalSize,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Returns the run-scoped tool output registry, or `undefined` when
|
|
133
|
+
* the feature is disabled.
|
|
134
|
+
*
|
|
135
|
+
* @internal Exposed for test observation only. Host code should rely
|
|
136
|
+
* on `{{tool<i>turn<n>}}` substitution at tool-invocation time and
|
|
137
|
+
* not mutate the registry directly.
|
|
138
|
+
*/
|
|
139
|
+
_unsafeGetToolOutputRegistry() {
|
|
140
|
+
return this.toolOutputRegistry;
|
|
59
141
|
}
|
|
60
142
|
/**
|
|
61
143
|
* Returns cached programmatic tools, computing once on first access.
|
|
@@ -87,31 +169,89 @@ class ToolNode extends RunnableCallable {
|
|
|
87
169
|
return new Map(this.toolUsageCount); // Return a copy
|
|
88
170
|
}
|
|
89
171
|
/**
|
|
90
|
-
* Runs a single tool call with error handling
|
|
172
|
+
* Runs a single tool call with error handling.
|
|
173
|
+
*
|
|
174
|
+
* `batchIndex` is the tool's position within the current ToolNode
|
|
175
|
+
* batch and, together with `this.currentTurn`, forms the key used to
|
|
176
|
+
* register the output for future `{{tool<idx>turn<turn>}}`
|
|
177
|
+
* substitutions. Omit when no registration should occur.
|
|
91
178
|
*/
|
|
92
|
-
async runTool(call, config) {
|
|
179
|
+
async runTool(call, config, batchContext = {}) {
|
|
180
|
+
const { batchIndex, turn, batchScopeId, resolvedArgsByCallId } = batchContext;
|
|
93
181
|
const tool = this.toolMap.get(call.name);
|
|
182
|
+
const registry = this.toolOutputRegistry;
|
|
183
|
+
/**
|
|
184
|
+
* Precompute the reference key once per call — captured locally
|
|
185
|
+
* so concurrent `invoke()` calls on the same ToolNode cannot race
|
|
186
|
+
* on a shared turn field.
|
|
187
|
+
*/
|
|
188
|
+
const refKey = registry != null && batchIndex != null && turn != null
|
|
189
|
+
? buildReferenceKey(batchIndex, turn)
|
|
190
|
+
: undefined;
|
|
191
|
+
/**
|
|
192
|
+
* Hoisted outside the try so the catch branch can append
|
|
193
|
+
* `[unresolved refs: …]` to error messages — otherwise the LLM
|
|
194
|
+
* only sees a generic error when it references a bad key, losing
|
|
195
|
+
* the self-correction signal this feature is meant to provide.
|
|
196
|
+
*/
|
|
197
|
+
let unresolvedRefs = [];
|
|
198
|
+
/**
|
|
199
|
+
* Use the caller-provided `batchScopeId` when threaded from
|
|
200
|
+
* `run()` (so anonymous batches get their own unique scope).
|
|
201
|
+
* Fall back to the config's `run_id` when runTool is invoked
|
|
202
|
+
* from a context that doesn't thread it — that still preserves
|
|
203
|
+
* the runId-based partitioning for named runs.
|
|
204
|
+
*/
|
|
205
|
+
const runId = batchScopeId ?? config.configurable?.run_id;
|
|
94
206
|
try {
|
|
95
207
|
if (tool === undefined) {
|
|
96
208
|
throw new Error(`Tool "${call.name}" not found.`);
|
|
97
209
|
}
|
|
98
|
-
|
|
99
|
-
|
|
210
|
+
/**
|
|
211
|
+
* `usageCount` is the per-tool-name invocation index that
|
|
212
|
+
* web-search and other tools observe via `invokeParams.turn`.
|
|
213
|
+
* It is intentionally distinct from the outer `turn` parameter
|
|
214
|
+
* (the batch turn used for ref keys); the latter is captured
|
|
215
|
+
* before the try block when constructing `refKey`.
|
|
216
|
+
*/
|
|
217
|
+
const usageCount = this.toolUsageCount.get(call.name) ?? 0;
|
|
218
|
+
this.toolUsageCount.set(call.name, usageCount + 1);
|
|
100
219
|
if (call.id != null && call.id !== '') {
|
|
101
|
-
this.toolCallTurns.set(call.id,
|
|
220
|
+
this.toolCallTurns.set(call.id, usageCount);
|
|
221
|
+
}
|
|
222
|
+
let args = call.args;
|
|
223
|
+
if (registry != null) {
|
|
224
|
+
const { resolved, unresolved } = registry.resolve(runId, args);
|
|
225
|
+
args = resolved;
|
|
226
|
+
unresolvedRefs = unresolved;
|
|
227
|
+
/**
|
|
228
|
+
* Expose the post-substitution args to downstream completion
|
|
229
|
+
* events so audit logs / host-side `ON_RUN_STEP_COMPLETED`
|
|
230
|
+
* handlers observe what actually ran, not the `{{…}}`
|
|
231
|
+
* template. Only string/object args are worth recording.
|
|
232
|
+
*/
|
|
233
|
+
if (resolvedArgsByCallId != null &&
|
|
234
|
+
call.id != null &&
|
|
235
|
+
call.id !== '' &&
|
|
236
|
+
resolved !== call.args &&
|
|
237
|
+
typeof resolved === 'object') {
|
|
238
|
+
resolvedArgsByCallId.set(call.id, resolved);
|
|
239
|
+
}
|
|
102
240
|
}
|
|
103
|
-
const args = call.args;
|
|
104
241
|
const stepId = this.toolCallStepIds?.get(call.id);
|
|
105
242
|
// Build invoke params - LangChain extracts non-schema fields to config.toolCall
|
|
243
|
+
// `turn` here is the per-tool usage count (matches what tools have
|
|
244
|
+
// observed historically via config.toolCall.turn — e.g. web search).
|
|
106
245
|
let invokeParams = {
|
|
107
246
|
...call,
|
|
108
247
|
args,
|
|
109
248
|
type: 'tool_call',
|
|
110
249
|
stepId,
|
|
111
|
-
turn,
|
|
250
|
+
turn: usageCount,
|
|
112
251
|
};
|
|
113
252
|
// Inject runtime data for special tools (becomes available at config.toolCall)
|
|
114
|
-
if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING
|
|
253
|
+
if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING ||
|
|
254
|
+
call.name === Constants.BASH_PROGRAMMATIC_TOOL_CALLING) {
|
|
115
255
|
const { toolMap, toolDefs } = this.getProgrammaticTools();
|
|
116
256
|
invokeParams = {
|
|
117
257
|
...invokeParams,
|
|
@@ -134,8 +274,7 @@ class ToolNode extends RunnableCallable {
|
|
|
134
274
|
* session_id is always injected when available (even without tracked files)
|
|
135
275
|
* so the CodeExecutor can fall back to the /files endpoint for session continuity.
|
|
136
276
|
*/
|
|
137
|
-
if (call.name
|
|
138
|
-
call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
|
|
277
|
+
if (CODE_EXECUTION_TOOLS.has(call.name)) {
|
|
139
278
|
const codeSession = this.sessions?.get(Constants.EXECUTE_CODE);
|
|
140
279
|
if (codeSession?.session_id != null && codeSession.session_id !== '') {
|
|
141
280
|
invokeParams = {
|
|
@@ -153,19 +292,72 @@ class ToolNode extends RunnableCallable {
|
|
|
153
292
|
}
|
|
154
293
|
}
|
|
155
294
|
const output = await tool.invoke(invokeParams, config);
|
|
156
|
-
if ((
|
|
157
|
-
isCommand(output)) {
|
|
295
|
+
if (isCommand(output)) {
|
|
158
296
|
return output;
|
|
159
297
|
}
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
298
|
+
if (isBaseMessage(output) && output._getType() === 'tool') {
|
|
299
|
+
const toolMsg = output;
|
|
300
|
+
const isError = toolMsg.status === 'error';
|
|
301
|
+
if (isError) {
|
|
302
|
+
/**
|
|
303
|
+
* Error ToolMessages bypass registration/annotation but must
|
|
304
|
+
* still carry the unresolved-refs hint so the LLM can
|
|
305
|
+
* self-correct when its reference key caused the failure.
|
|
306
|
+
*/
|
|
307
|
+
if (unresolvedRefs.length > 0 &&
|
|
308
|
+
typeof toolMsg.content === 'string') {
|
|
309
|
+
toolMsg.content = this.applyOutputReference(runId, toolMsg.content, toolMsg.content, undefined, unresolvedRefs);
|
|
310
|
+
}
|
|
311
|
+
return toolMsg;
|
|
312
|
+
}
|
|
313
|
+
if (this.toolOutputRegistry != null || unresolvedRefs.length > 0) {
|
|
314
|
+
if (typeof toolMsg.content === 'string') {
|
|
315
|
+
const rawContent = toolMsg.content;
|
|
316
|
+
const llmContent = truncateToolResultContent(rawContent, this.maxToolResultChars);
|
|
317
|
+
toolMsg.content = this.applyOutputReference(runId, llmContent, rawContent, refKey, unresolvedRefs);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
/**
|
|
321
|
+
* Non-string content (multi-part content blocks — text +
|
|
322
|
+
* image). Known limitation: we cannot register under a
|
|
323
|
+
* reference key because there's no canonical serialized
|
|
324
|
+
* form. Warn once per tool per run when the caller
|
|
325
|
+
* intended to register.
|
|
326
|
+
*
|
|
327
|
+
* Still surface unresolved-ref warnings so the LLM gets
|
|
328
|
+
* the self-correction signal that the string and error
|
|
329
|
+
* paths already emit. Prepended as a leading text block
|
|
330
|
+
* to keep the original content ordering intact.
|
|
331
|
+
*/
|
|
332
|
+
if (unresolvedRefs.length > 0 && Array.isArray(toolMsg.content)) {
|
|
333
|
+
const warningBlock = {
|
|
334
|
+
type: 'text',
|
|
335
|
+
text: `[unresolved refs: ${unresolvedRefs.join(', ')}]`,
|
|
336
|
+
};
|
|
337
|
+
toolMsg.content = [
|
|
338
|
+
warningBlock,
|
|
339
|
+
...toolMsg.content,
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
if (refKey != null &&
|
|
343
|
+
this.toolOutputRegistry.claimWarnOnce(runId, call.name)) {
|
|
344
|
+
// eslint-disable-next-line no-console
|
|
345
|
+
console.warn(`[ToolNode] Skipping tool output reference for "${call.name}": ` +
|
|
346
|
+
'ToolMessage content is not a string (further occurrences for this tool in the same run are suppressed).');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return toolMsg;
|
|
168
351
|
}
|
|
352
|
+
const rawContent = typeof output === 'string' ? output : JSON.stringify(output);
|
|
353
|
+
const truncated = truncateToolResultContent(rawContent, this.maxToolResultChars);
|
|
354
|
+
const content = this.applyOutputReference(runId, truncated, rawContent, refKey, unresolvedRefs);
|
|
355
|
+
return new ToolMessage({
|
|
356
|
+
status: 'success',
|
|
357
|
+
name: tool.name,
|
|
358
|
+
content,
|
|
359
|
+
tool_call_id: call.id,
|
|
360
|
+
});
|
|
169
361
|
}
|
|
170
362
|
catch (_e) {
|
|
171
363
|
const e = _e;
|
|
@@ -208,14 +400,52 @@ class ToolNode extends RunnableCallable {
|
|
|
208
400
|
});
|
|
209
401
|
}
|
|
210
402
|
}
|
|
403
|
+
let errorContent = `Error: ${e.message}\n Please fix your mistakes.`;
|
|
404
|
+
if (unresolvedRefs.length > 0) {
|
|
405
|
+
errorContent = this.applyOutputReference(runId, errorContent, errorContent, undefined, unresolvedRefs);
|
|
406
|
+
}
|
|
211
407
|
return new ToolMessage({
|
|
212
408
|
status: 'error',
|
|
213
|
-
content:
|
|
409
|
+
content: errorContent,
|
|
214
410
|
name: call.name,
|
|
215
411
|
tool_call_id: call.id ?? '',
|
|
216
412
|
});
|
|
217
413
|
}
|
|
218
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Finalizes the LLM-visible content for a tool call and (when a
|
|
417
|
+
* `refKey` is provided) registers the full, raw output under that
|
|
418
|
+
* key.
|
|
419
|
+
*
|
|
420
|
+
* @param llmContent The content string the LLM will see. This is
|
|
421
|
+
* the already-truncated, post-hook view; the annotation is
|
|
422
|
+
* applied on top of it.
|
|
423
|
+
* @param registryContent The full, untruncated output to store in
|
|
424
|
+
* the registry so `{{tool<i>turn<n>}}` substitutions deliver the
|
|
425
|
+
* complete payload. Ignored when `refKey` is undefined.
|
|
426
|
+
* @param refKey Precomputed `tool<i>turn<n>` key, or undefined when
|
|
427
|
+
* the output is not to be registered (errors, disabled feature,
|
|
428
|
+
* unavailable batch/turn).
|
|
429
|
+
* @param unresolved Placeholder keys that did not resolve; appended
|
|
430
|
+
* as `[unresolved refs: …]` so the LLM can self-correct.
|
|
431
|
+
*
|
|
432
|
+
* `refKey` is passed in (rather than built from `this.currentTurn`)
|
|
433
|
+
* so parallel `invoke()` calls on the same ToolNode cannot race on
|
|
434
|
+
* the shared turn field.
|
|
435
|
+
*/
|
|
436
|
+
applyOutputReference(runId, llmContent, registryContent, refKey, unresolved) {
|
|
437
|
+
if (this.toolOutputRegistry != null && refKey != null) {
|
|
438
|
+
this.toolOutputRegistry.set(runId, refKey, registryContent);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* `annotateToolOutputWithReference` handles both the ref-key and
|
|
442
|
+
* unresolved-refs cases together so JSON-object outputs stay
|
|
443
|
+
* parseable: unresolved refs land in an `_unresolved_refs` field
|
|
444
|
+
* instead of as a trailing text line that would break
|
|
445
|
+
* `JSON.parse` for downstream consumers.
|
|
446
|
+
*/
|
|
447
|
+
return annotateToolOutputWithReference(llmContent, refKey, unresolved);
|
|
448
|
+
}
|
|
219
449
|
/**
|
|
220
450
|
* Builds code session context for injection into event-driven tool calls.
|
|
221
451
|
* Mirrors the session injection logic in runTool() for direct execution.
|
|
@@ -244,7 +474,7 @@ class ToolNode extends RunnableCallable {
|
|
|
244
474
|
* Extracts code execution session context from tool results and stores in Graph.sessions.
|
|
245
475
|
* Mirrors the session storage logic in handleRunToolCompletions for direct execution.
|
|
246
476
|
*/
|
|
247
|
-
storeCodeSessionFromResults(results,
|
|
477
|
+
storeCodeSessionFromResults(results, requestMap) {
|
|
248
478
|
if (!this.sessions) {
|
|
249
479
|
return;
|
|
250
480
|
}
|
|
@@ -253,38 +483,17 @@ class ToolNode extends RunnableCallable {
|
|
|
253
483
|
if (result.status !== 'success' || result.artifact == null) {
|
|
254
484
|
continue;
|
|
255
485
|
}
|
|
256
|
-
const request =
|
|
257
|
-
if (request?.name
|
|
258
|
-
request
|
|
486
|
+
const request = requestMap.get(result.toolCallId);
|
|
487
|
+
if (!request?.name ||
|
|
488
|
+
(!CODE_EXECUTION_TOOLS.has(request.name) &&
|
|
489
|
+
request.name !== Constants.SKILL_TOOL)) {
|
|
259
490
|
continue;
|
|
260
491
|
}
|
|
261
492
|
const artifact = result.artifact;
|
|
262
493
|
if (artifact?.session_id == null || artifact.session_id === '') {
|
|
263
494
|
continue;
|
|
264
495
|
}
|
|
265
|
-
|
|
266
|
-
const existingSession = this.sessions.get(Constants.EXECUTE_CODE);
|
|
267
|
-
const existingFiles = existingSession?.files ?? [];
|
|
268
|
-
if (newFiles.length > 0) {
|
|
269
|
-
const filesWithSession = newFiles.map((file) => ({
|
|
270
|
-
...file,
|
|
271
|
-
session_id: artifact.session_id,
|
|
272
|
-
}));
|
|
273
|
-
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
274
|
-
const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
|
|
275
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
276
|
-
session_id: artifact.session_id,
|
|
277
|
-
files: [...filteredExisting, ...filesWithSession],
|
|
278
|
-
lastUpdated: Date.now(),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
283
|
-
session_id: artifact.session_id,
|
|
284
|
-
files: existingFiles,
|
|
285
|
-
lastUpdated: Date.now(),
|
|
286
|
-
});
|
|
287
|
-
}
|
|
496
|
+
updateCodeSession(this.sessions, artifact.session_id, artifact.files);
|
|
288
497
|
}
|
|
289
498
|
}
|
|
290
499
|
/**
|
|
@@ -295,8 +504,12 @@ class ToolNode extends RunnableCallable {
|
|
|
295
504
|
* By handling completions here in graph context (rather than in the
|
|
296
505
|
* stream consumer via ToolEndHandler), the race between the stream
|
|
297
506
|
* consumer and graph execution is eliminated.
|
|
507
|
+
*
|
|
508
|
+
* @param resolvedArgsByCallId Per-batch resolved-args sink populated
|
|
509
|
+
* by `runTool`. Threaded as a local map (instead of instance state)
|
|
510
|
+
* so concurrent batches cannot read each other's entries.
|
|
298
511
|
*/
|
|
299
|
-
handleRunToolCompletions(calls, outputs, config) {
|
|
512
|
+
handleRunToolCompletions(calls, outputs, config, resolvedArgsByCallId) {
|
|
300
513
|
for (let i = 0; i < calls.length; i++) {
|
|
301
514
|
const call = calls[i];
|
|
302
515
|
const output = outputs[i];
|
|
@@ -311,35 +524,10 @@ class ToolNode extends RunnableCallable {
|
|
|
311
524
|
if (toolMessage.status === 'error' && this.errorHandler != null) {
|
|
312
525
|
continue;
|
|
313
526
|
}
|
|
314
|
-
|
|
315
|
-
if (this.sessions &&
|
|
316
|
-
(call.name === Constants.EXECUTE_CODE ||
|
|
317
|
-
call.name === Constants.PROGRAMMATIC_TOOL_CALLING)) {
|
|
527
|
+
if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name)) {
|
|
318
528
|
const artifact = toolMessage.artifact;
|
|
319
529
|
if (artifact?.session_id != null && artifact.session_id !== '') {
|
|
320
|
-
|
|
321
|
-
const existingSession = this.sessions.get(Constants.EXECUTE_CODE);
|
|
322
|
-
const existingFiles = existingSession?.files ?? [];
|
|
323
|
-
if (newFiles.length > 0) {
|
|
324
|
-
const filesWithSession = newFiles.map((file) => ({
|
|
325
|
-
...file,
|
|
326
|
-
session_id: artifact.session_id,
|
|
327
|
-
}));
|
|
328
|
-
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
329
|
-
const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
|
|
330
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
331
|
-
session_id: artifact.session_id,
|
|
332
|
-
files: [...filteredExisting, ...filesWithSession],
|
|
333
|
-
lastUpdated: Date.now(),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
338
|
-
session_id: artifact.session_id,
|
|
339
|
-
files: existingFiles,
|
|
340
|
-
lastUpdated: Date.now(),
|
|
341
|
-
});
|
|
342
|
-
}
|
|
530
|
+
updateCodeSession(this.sessions, artifact.session_id, artifact.files);
|
|
343
531
|
}
|
|
344
532
|
}
|
|
345
533
|
// Dispatch ON_RUN_STEP_COMPLETED via custom event (same path as dispatchToolEvents)
|
|
@@ -350,10 +538,17 @@ class ToolNode extends RunnableCallable {
|
|
|
350
538
|
const contentString = typeof toolMessage.content === 'string'
|
|
351
539
|
? toolMessage.content
|
|
352
540
|
: JSON.stringify(toolMessage.content);
|
|
541
|
+
/**
|
|
542
|
+
* Prefer the post-substitution args when a `{{…}}` placeholder
|
|
543
|
+
* was resolved in `runTool`. This keeps
|
|
544
|
+
* `ON_RUN_STEP_COMPLETED.tool_call.args` consistent with what
|
|
545
|
+
* the tool actually received rather than leaking the template.
|
|
546
|
+
*/
|
|
547
|
+
const effectiveArgs = resolvedArgsByCallId?.get(toolCallId) ?? call.args;
|
|
353
548
|
const tool_call = {
|
|
354
|
-
args: typeof
|
|
355
|
-
?
|
|
356
|
-
: JSON.stringify(
|
|
549
|
+
args: typeof effectiveArgs === 'string'
|
|
550
|
+
? effectiveArgs
|
|
551
|
+
: JSON.stringify(effectiveArgs ?? {}),
|
|
357
552
|
name: call.name,
|
|
358
553
|
id: toolCallId,
|
|
359
554
|
output: contentString,
|
|
@@ -372,113 +567,415 @@ class ToolNode extends RunnableCallable {
|
|
|
372
567
|
/**
|
|
373
568
|
* Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
|
|
374
569
|
* Core logic for event-driven execution, separated from output shaping.
|
|
570
|
+
*
|
|
571
|
+
* Hook lifecycle (when `hookRegistry` is set):
|
|
572
|
+
* 1. **PreToolUse** fires per call in parallel before dispatch. Denied
|
|
573
|
+
* calls produce error ToolMessages and fire **PermissionDenied**;
|
|
574
|
+
* surviving calls proceed with optional `updatedInput`.
|
|
575
|
+
* 2. Surviving calls are dispatched to the host via `ON_TOOL_EXECUTE`.
|
|
576
|
+
* 3. **PostToolUse** / **PostToolUseFailure** fire per result. Post hooks
|
|
577
|
+
* can replace tool output via `updatedOutput`.
|
|
578
|
+
* 4. Injected messages from results are collected and returned alongside
|
|
579
|
+
* ToolMessages (appended AFTER to respect provider ordering).
|
|
375
580
|
*/
|
|
376
|
-
async dispatchToolEvents(toolCalls, config) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
581
|
+
async dispatchToolEvents(toolCalls, config, batchContext = {}) {
|
|
582
|
+
const { batchIndices, turn, batchScopeId, preResolvedArgs, preBatchSnapshot, } = batchContext;
|
|
583
|
+
const runId = config.configurable?.run_id ?? '';
|
|
584
|
+
/**
|
|
585
|
+
* Registry-facing scope id — prefers the caller-threaded
|
|
586
|
+
* `batchScopeId` so anonymous batches target their own unique
|
|
587
|
+
* bucket and don't step on concurrent anonymous invocations.
|
|
588
|
+
* Hooks and event payloads keep using the empty-string coerced
|
|
589
|
+
* `runId` for backward compat.
|
|
590
|
+
*/
|
|
591
|
+
const registryRunId = batchScopeId ?? config.configurable?.run_id;
|
|
592
|
+
const threadId = config.configurable?.thread_id;
|
|
593
|
+
const registry = this.toolOutputRegistry;
|
|
594
|
+
const unresolvedByCallId = new Map();
|
|
595
|
+
const preToolCalls = toolCalls.map((call, i) => {
|
|
596
|
+
const originalArgs = call.args;
|
|
597
|
+
let resolvedArgs = originalArgs;
|
|
598
|
+
/**
|
|
599
|
+
* When the caller provided a pre-resolved map (the mixed
|
|
600
|
+
* direct+event path snapshots event args synchronously before
|
|
601
|
+
* awaiting directs so they can't accidentally resolve
|
|
602
|
+
* same-turn direct outputs), use those entries verbatim instead
|
|
603
|
+
* of re-resolving against a registry that may have changed
|
|
604
|
+
* since the batch started.
|
|
605
|
+
*/
|
|
606
|
+
const pre = call.id != null ? preResolvedArgs?.get(call.id) : undefined;
|
|
607
|
+
if (pre != null) {
|
|
608
|
+
resolvedArgs = pre.resolved;
|
|
609
|
+
if (pre.unresolved.length > 0 && call.id != null) {
|
|
610
|
+
unresolvedByCallId.set(call.id, pre.unresolved);
|
|
611
|
+
}
|
|
390
612
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
613
|
+
else if (registry != null) {
|
|
614
|
+
const { resolved, unresolved } = registry.resolve(registryRunId, originalArgs);
|
|
615
|
+
resolvedArgs = resolved;
|
|
616
|
+
if (unresolved.length > 0 && call.id != null) {
|
|
617
|
+
unresolvedByCallId.set(call.id, unresolved);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
call,
|
|
622
|
+
stepId: this.toolCallStepIds?.get(call.id) ?? '',
|
|
623
|
+
args: resolvedArgs,
|
|
624
|
+
batchIndex: batchIndices?.[i],
|
|
402
625
|
};
|
|
403
|
-
safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, request, config);
|
|
404
626
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
627
|
+
const messageByCallId = new Map();
|
|
628
|
+
const approvedEntries = [];
|
|
629
|
+
const HOOK_FALLBACK = Object.freeze({
|
|
630
|
+
additionalContexts: [],
|
|
631
|
+
errors: [],
|
|
632
|
+
});
|
|
633
|
+
if (this.hookRegistry?.hasHookFor('PreToolUse', runId) === true) {
|
|
634
|
+
const preResults = await Promise.all(preToolCalls.map((entry) => executeHooks({
|
|
635
|
+
registry: this.hookRegistry,
|
|
636
|
+
input: {
|
|
637
|
+
hook_event_name: 'PreToolUse',
|
|
638
|
+
runId,
|
|
639
|
+
threadId,
|
|
640
|
+
agentId: this.agentId,
|
|
641
|
+
toolName: entry.call.name,
|
|
642
|
+
toolInput: entry.args,
|
|
643
|
+
toolUseId: entry.call.id,
|
|
644
|
+
stepId: entry.stepId,
|
|
645
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0,
|
|
646
|
+
},
|
|
647
|
+
sessionId: runId,
|
|
648
|
+
matchQuery: entry.call.name,
|
|
649
|
+
}).catch(() => HOOK_FALLBACK)));
|
|
650
|
+
for (let i = 0; i < preToolCalls.length; i++) {
|
|
651
|
+
const hookResult = preResults[i];
|
|
652
|
+
const entry = preToolCalls[i];
|
|
653
|
+
const isDenied = hookResult.decision === 'deny' || hookResult.decision === 'ask';
|
|
654
|
+
if (isDenied) {
|
|
655
|
+
const reason = hookResult.reason ?? 'Blocked by hook';
|
|
656
|
+
const contentString = `Blocked: ${reason}`;
|
|
657
|
+
messageByCallId.set(entry.call.id, new ToolMessage({
|
|
658
|
+
status: 'error',
|
|
659
|
+
content: contentString,
|
|
660
|
+
name: entry.call.name,
|
|
661
|
+
tool_call_id: entry.call.id,
|
|
662
|
+
}));
|
|
663
|
+
this.dispatchStepCompleted(entry.call.id, entry.call.name, entry.args, contentString, config);
|
|
664
|
+
if (this.hookRegistry.hasHookFor('PermissionDenied', runId)) {
|
|
665
|
+
executeHooks({
|
|
666
|
+
registry: this.hookRegistry,
|
|
667
|
+
input: {
|
|
668
|
+
hook_event_name: 'PermissionDenied',
|
|
669
|
+
runId,
|
|
670
|
+
threadId,
|
|
671
|
+
agentId: this.agentId,
|
|
672
|
+
toolName: entry.call.name,
|
|
673
|
+
toolInput: entry.args,
|
|
674
|
+
toolUseId: entry.call.id,
|
|
675
|
+
reason,
|
|
676
|
+
},
|
|
677
|
+
sessionId: runId,
|
|
678
|
+
matchQuery: entry.call.name,
|
|
679
|
+
}).catch(() => {
|
|
680
|
+
/* PermissionDenied is observational — swallow errors */
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (hookResult.updatedInput != null) {
|
|
686
|
+
/**
|
|
687
|
+
* Re-resolve after PreToolUse replaces the input: a hook may
|
|
688
|
+
* introduce new `{{tool<i>turn<n>}}` placeholders (e.g., by
|
|
689
|
+
* copying user-supplied text) that the pre-hook pass never
|
|
690
|
+
* saw. Re-running the resolver on the hook-rewritten args
|
|
691
|
+
* keeps substitution and the unresolved-refs record in sync
|
|
692
|
+
* with what the tool will actually receive.
|
|
693
|
+
*/
|
|
694
|
+
if (registry != null) {
|
|
695
|
+
/**
|
|
696
|
+
* Mixed direct+event batches must use the pre-batch
|
|
697
|
+
* snapshot so a hook-introduced placeholder cannot
|
|
698
|
+
* accidentally resolve to a same-turn direct output that
|
|
699
|
+
* has just registered. Pure event batches don't have a
|
|
700
|
+
* snapshot and resolve against the live registry — safe
|
|
701
|
+
* because no event-side registrations have happened yet.
|
|
702
|
+
*/
|
|
703
|
+
const view = preBatchSnapshot ?? {
|
|
704
|
+
resolve: (args) => registry.resolve(registryRunId, args),
|
|
705
|
+
};
|
|
706
|
+
const { resolved, unresolved } = view.resolve(hookResult.updatedInput);
|
|
707
|
+
entry.args = resolved;
|
|
708
|
+
if (entry.call.id != null) {
|
|
709
|
+
if (unresolved.length > 0) {
|
|
710
|
+
unresolvedByCallId.set(entry.call.id, unresolved);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
unresolvedByCallId.delete(entry.call.id);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
entry.args = hookResult.updatedInput;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
approvedEntries.push(entry);
|
|
426
722
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
approvedEntries.push(...preToolCalls);
|
|
726
|
+
}
|
|
727
|
+
const injected = [];
|
|
728
|
+
const batchIndexByCallId = new Map();
|
|
729
|
+
if (approvedEntries.length > 0) {
|
|
730
|
+
const requests = approvedEntries.map((entry) => {
|
|
731
|
+
const turn = this.toolUsageCount.get(entry.call.name) ?? 0;
|
|
732
|
+
this.toolUsageCount.set(entry.call.name, turn + 1);
|
|
733
|
+
if (entry.batchIndex != null && entry.call.id != null) {
|
|
734
|
+
batchIndexByCallId.set(entry.call.id, entry.batchIndex);
|
|
735
|
+
}
|
|
736
|
+
const request = {
|
|
737
|
+
id: entry.call.id,
|
|
738
|
+
name: entry.call.name,
|
|
739
|
+
args: entry.args,
|
|
740
|
+
stepId: entry.stepId,
|
|
741
|
+
turn,
|
|
742
|
+
};
|
|
743
|
+
if (CODE_EXECUTION_TOOLS.has(entry.call.name) ||
|
|
744
|
+
entry.call.name === Constants.SKILL_TOOL) {
|
|
745
|
+
request.codeSessionContext = this.getCodeSessionContext();
|
|
746
|
+
}
|
|
747
|
+
return request;
|
|
748
|
+
});
|
|
749
|
+
const requestMap = new Map(requests.map((r) => [r.id, r]));
|
|
750
|
+
const results = await new Promise((resolve, reject) => {
|
|
751
|
+
const batchRequest = {
|
|
752
|
+
toolCalls: requests,
|
|
753
|
+
userId: config.configurable?.user_id,
|
|
754
|
+
agentId: this.agentId,
|
|
755
|
+
configurable: config.configurable,
|
|
756
|
+
metadata: config.metadata,
|
|
757
|
+
resolve,
|
|
758
|
+
reject,
|
|
759
|
+
};
|
|
760
|
+
safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, batchRequest, config);
|
|
761
|
+
});
|
|
762
|
+
this.storeCodeSessionFromResults(results, requestMap);
|
|
763
|
+
const hasPostHook = this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
764
|
+
const hasFailureHook = this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
765
|
+
for (const result of results) {
|
|
766
|
+
if (result.injectedMessages && result.injectedMessages.length > 0) {
|
|
767
|
+
try {
|
|
768
|
+
injected.push(...this.convertInjectedMessages(result.injectedMessages));
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
// eslint-disable-next-line no-console
|
|
772
|
+
console.warn(`[ToolNode] Failed to convert injectedMessages for toolCallId=${result.toolCallId}:`, e instanceof Error ? e.message : e);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const request = requestMap.get(result.toolCallId);
|
|
776
|
+
const toolName = request?.name ?? 'unknown';
|
|
777
|
+
let contentString;
|
|
778
|
+
let toolMessage;
|
|
779
|
+
if (result.status === 'error') {
|
|
780
|
+
contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
|
|
781
|
+
/**
|
|
782
|
+
* Error results bypass registration/annotation but must still
|
|
783
|
+
* carry the unresolved-refs hint so the LLM can self-correct
|
|
784
|
+
* when its reference key caused the failure.
|
|
785
|
+
*/
|
|
786
|
+
const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
|
|
787
|
+
if (unresolved.length > 0) {
|
|
788
|
+
contentString = this.applyOutputReference(registryRunId, contentString, contentString, undefined, unresolved);
|
|
789
|
+
}
|
|
790
|
+
toolMessage = new ToolMessage({
|
|
791
|
+
status: 'error',
|
|
792
|
+
content: contentString,
|
|
793
|
+
name: toolName,
|
|
794
|
+
tool_call_id: result.toolCallId,
|
|
795
|
+
});
|
|
796
|
+
if (hasFailureHook) {
|
|
797
|
+
await executeHooks({
|
|
798
|
+
registry: this.hookRegistry,
|
|
799
|
+
input: {
|
|
800
|
+
hook_event_name: 'PostToolUseFailure',
|
|
801
|
+
runId,
|
|
802
|
+
threadId,
|
|
803
|
+
agentId: this.agentId,
|
|
804
|
+
toolName,
|
|
805
|
+
toolInput: request?.args ?? {},
|
|
806
|
+
toolUseId: result.toolCallId,
|
|
807
|
+
error: result.errorMessage ?? 'Unknown error',
|
|
808
|
+
stepId: request?.stepId,
|
|
809
|
+
turn: request?.turn,
|
|
810
|
+
},
|
|
811
|
+
sessionId: runId,
|
|
812
|
+
matchQuery: toolName,
|
|
813
|
+
}).catch(() => {
|
|
814
|
+
/* PostToolUseFailure is observational — swallow errors */
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
let registryRaw = typeof result.content === 'string'
|
|
820
|
+
? result.content
|
|
821
|
+
: JSON.stringify(result.content);
|
|
822
|
+
contentString = truncateToolResultContent(registryRaw, this.maxToolResultChars);
|
|
823
|
+
if (hasPostHook) {
|
|
824
|
+
const hookResult = await executeHooks({
|
|
825
|
+
registry: this.hookRegistry,
|
|
826
|
+
input: {
|
|
827
|
+
hook_event_name: 'PostToolUse',
|
|
828
|
+
runId,
|
|
829
|
+
threadId,
|
|
830
|
+
agentId: this.agentId,
|
|
831
|
+
toolName,
|
|
832
|
+
toolInput: request?.args ?? {},
|
|
833
|
+
toolOutput: result.content,
|
|
834
|
+
toolUseId: result.toolCallId,
|
|
835
|
+
stepId: request?.stepId,
|
|
836
|
+
turn: request?.turn,
|
|
837
|
+
},
|
|
838
|
+
sessionId: runId,
|
|
839
|
+
matchQuery: toolName,
|
|
840
|
+
}).catch(() => undefined);
|
|
841
|
+
if (hookResult?.updatedOutput != null) {
|
|
842
|
+
const replaced = typeof hookResult.updatedOutput === 'string'
|
|
843
|
+
? hookResult.updatedOutput
|
|
844
|
+
: JSON.stringify(hookResult.updatedOutput);
|
|
845
|
+
registryRaw = replaced;
|
|
846
|
+
contentString = truncateToolResultContent(replaced, this.maxToolResultChars);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const batchIndex = batchIndexByCallId.get(result.toolCallId);
|
|
850
|
+
const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
|
|
851
|
+
const refKey = this.toolOutputRegistry != null &&
|
|
852
|
+
batchIndex != null &&
|
|
853
|
+
turn != null
|
|
854
|
+
? buildReferenceKey(batchIndex, turn)
|
|
855
|
+
: undefined;
|
|
856
|
+
contentString = this.applyOutputReference(registryRunId, contentString, registryRaw, refKey, unresolved);
|
|
857
|
+
toolMessage = new ToolMessage({
|
|
858
|
+
status: 'success',
|
|
859
|
+
name: toolName,
|
|
860
|
+
content: contentString,
|
|
861
|
+
artifact: result.artifact,
|
|
862
|
+
tool_call_id: result.toolCallId,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
this.dispatchStepCompleted(result.toolCallId, toolName, request?.args ?? {}, contentString, config, request?.turn);
|
|
866
|
+
messageByCallId.set(result.toolCallId, toolMessage);
|
|
439
867
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
868
|
+
}
|
|
869
|
+
const toolMessages = toolCalls
|
|
870
|
+
.map((call) => messageByCallId.get(call.id))
|
|
871
|
+
.filter((m) => m != null);
|
|
872
|
+
return { toolMessages, injected };
|
|
873
|
+
}
|
|
874
|
+
dispatchStepCompleted(toolCallId, toolName, args, output, config, turn) {
|
|
875
|
+
const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
|
|
876
|
+
if (!stepId) {
|
|
877
|
+
// eslint-disable-next-line no-console
|
|
878
|
+
console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${toolCallId} (tool=${toolName}). ` +
|
|
879
|
+
'This indicates a race between the stream consumer and graph execution. ' +
|
|
880
|
+
`Map size: ${this.toolCallStepIds?.size ?? 0}`);
|
|
881
|
+
}
|
|
882
|
+
safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, {
|
|
883
|
+
result: {
|
|
884
|
+
id: stepId,
|
|
885
|
+
index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
|
|
886
|
+
type: 'tool_call',
|
|
887
|
+
tool_call: {
|
|
888
|
+
args: JSON.stringify(args),
|
|
889
|
+
name: toolName,
|
|
890
|
+
id: toolCallId,
|
|
891
|
+
output,
|
|
892
|
+
progress: 1,
|
|
455
893
|
},
|
|
894
|
+
},
|
|
895
|
+
}, config);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Converts InjectedMessage instances to LangChain HumanMessage objects.
|
|
899
|
+
* Both 'user' and 'system' roles become HumanMessage to avoid provider
|
|
900
|
+
* rejections (Anthropic/Google reject non-leading SystemMessages).
|
|
901
|
+
* The original role is preserved in additional_kwargs for downstream consumers.
|
|
902
|
+
*/
|
|
903
|
+
convertInjectedMessages(messages) {
|
|
904
|
+
const converted = [];
|
|
905
|
+
for (const msg of messages) {
|
|
906
|
+
const additional_kwargs = {
|
|
907
|
+
role: msg.role,
|
|
456
908
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
909
|
+
if (msg.isMeta != null)
|
|
910
|
+
additional_kwargs.isMeta = msg.isMeta;
|
|
911
|
+
if (msg.source != null)
|
|
912
|
+
additional_kwargs.source = msg.source;
|
|
913
|
+
if (msg.skillName != null)
|
|
914
|
+
additional_kwargs.skillName = msg.skillName;
|
|
915
|
+
converted.push(new HumanMessage({ content: msg.content, additional_kwargs }));
|
|
916
|
+
}
|
|
917
|
+
return converted;
|
|
460
918
|
}
|
|
461
919
|
/**
|
|
462
920
|
* Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
|
|
463
|
-
*
|
|
921
|
+
* Injected messages are placed AFTER ToolMessages to respect provider
|
|
922
|
+
* message ordering (AIMessage tool_calls must be immediately followed
|
|
923
|
+
* by their ToolMessage results).
|
|
924
|
+
*
|
|
925
|
+
* `batchIndices` mirrors `toolCalls` and carries each call's position
|
|
926
|
+
* within the parent batch. `turn` is the per-`run()` batch index
|
|
927
|
+
* captured locally by the caller. Both are threaded so concurrent
|
|
928
|
+
* invocations cannot race on shared mutable state.
|
|
464
929
|
*/
|
|
465
930
|
async executeViaEvent(toolCalls, config,
|
|
466
931
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
467
|
-
input) {
|
|
468
|
-
const
|
|
932
|
+
input, batchContext = {}) {
|
|
933
|
+
const { toolMessages, injected } = await this.dispatchToolEvents(toolCalls, config, batchContext);
|
|
934
|
+
const outputs = [...toolMessages, ...injected];
|
|
469
935
|
return (Array.isArray(input) ? outputs : { messages: outputs });
|
|
470
936
|
}
|
|
471
937
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
472
938
|
async run(input, config) {
|
|
473
939
|
this.toolCallTurns.clear();
|
|
940
|
+
/**
|
|
941
|
+
* Per-batch local map for resolved (post-substitution) args.
|
|
942
|
+
* Lives on the stack so concurrent `run()` calls on the same
|
|
943
|
+
* ToolNode cannot read or wipe each other's entries.
|
|
944
|
+
*/
|
|
945
|
+
const resolvedArgsByCallId = new Map();
|
|
946
|
+
/**
|
|
947
|
+
* Claim this batch's turn synchronously from the registry (or
|
|
948
|
+
* fall back to 0 when the feature is disabled). The registry is
|
|
949
|
+
* partitioned by scope id so overlapping batches cannot
|
|
950
|
+
* overwrite each other's state even under a shared registry.
|
|
951
|
+
*
|
|
952
|
+
* For anonymous callers (no `run_id` in config), mint a unique
|
|
953
|
+
* per-batch scope id so two concurrent anonymous invocations
|
|
954
|
+
* don't target the same bucket. The scope is threaded down to
|
|
955
|
+
* every subsequent registry call on this batch.
|
|
956
|
+
*/
|
|
957
|
+
const incomingRunId = config.configurable?.run_id;
|
|
958
|
+
const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
|
|
959
|
+
const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
474
960
|
let outputs;
|
|
475
961
|
if (this.isSendInput(input)) {
|
|
476
962
|
const isDirectTool = this.directToolNames?.has(input.lg_tool_call.name);
|
|
477
963
|
if (this.eventDrivenMode && isDirectTool !== true) {
|
|
478
|
-
return this.executeViaEvent([input.lg_tool_call], config, input
|
|
964
|
+
return this.executeViaEvent([input.lg_tool_call], config, input, {
|
|
965
|
+
batchIndices: [0],
|
|
966
|
+
turn,
|
|
967
|
+
batchScopeId,
|
|
968
|
+
});
|
|
479
969
|
}
|
|
480
|
-
outputs = [
|
|
481
|
-
|
|
970
|
+
outputs = [
|
|
971
|
+
await this.runTool(input.lg_tool_call, config, {
|
|
972
|
+
batchIndex: 0,
|
|
973
|
+
turn,
|
|
974
|
+
batchScopeId,
|
|
975
|
+
resolvedArgsByCallId,
|
|
976
|
+
}),
|
|
977
|
+
];
|
|
978
|
+
this.handleRunToolCompletions([input.lg_tool_call], outputs, config, resolvedArgsByCallId);
|
|
482
979
|
}
|
|
483
980
|
else {
|
|
484
981
|
let messages;
|
|
@@ -523,25 +1020,100 @@ class ToolNode extends RunnableCallable {
|
|
|
523
1020
|
false));
|
|
524
1021
|
}) ?? [];
|
|
525
1022
|
if (this.eventDrivenMode && filteredCalls.length > 0) {
|
|
1023
|
+
const filteredIndices = filteredCalls.map((_, idx) => idx);
|
|
526
1024
|
if (!this.directToolNames || this.directToolNames.size === 0) {
|
|
527
|
-
return this.executeViaEvent(filteredCalls, config, input
|
|
1025
|
+
return this.executeViaEvent(filteredCalls, config, input, {
|
|
1026
|
+
batchIndices: filteredIndices,
|
|
1027
|
+
turn,
|
|
1028
|
+
batchScopeId,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const directEntries = [];
|
|
1032
|
+
const eventEntries = [];
|
|
1033
|
+
for (let i = 0; i < filteredCalls.length; i++) {
|
|
1034
|
+
const call = filteredCalls[i];
|
|
1035
|
+
const entry = { call, batchIndex: i };
|
|
1036
|
+
if (this.directToolNames.has(call.name)) {
|
|
1037
|
+
directEntries.push(entry);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
eventEntries.push(entry);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const directCalls = directEntries.map((e) => e.call);
|
|
1044
|
+
const directIndices = directEntries.map((e) => e.batchIndex);
|
|
1045
|
+
const eventCalls = eventEntries.map((e) => e.call);
|
|
1046
|
+
const eventIndices = eventEntries.map((e) => e.batchIndex);
|
|
1047
|
+
/**
|
|
1048
|
+
* Snapshot the event calls' args against the *pre-batch*
|
|
1049
|
+
* registry state synchronously, before any await runs. The
|
|
1050
|
+
* directs are then awaited first (preserving fail-fast
|
|
1051
|
+
* semantics — a thrown error in a direct tool, e.g. with
|
|
1052
|
+
* `handleToolErrors=false` or a `GraphInterrupt`, aborts
|
|
1053
|
+
* before we dispatch any event-driven tools to the host).
|
|
1054
|
+
* Because the event args were captured pre-await, they stay
|
|
1055
|
+
* isolated from same-turn direct outputs that register
|
|
1056
|
+
* during the await.
|
|
1057
|
+
*/
|
|
1058
|
+
const preResolvedEventArgs = new Map();
|
|
1059
|
+
/**
|
|
1060
|
+
* Take a frozen snapshot of the registry state before any
|
|
1061
|
+
* direct registrations land. The snapshot resolves
|
|
1062
|
+
* placeholders against this point-in-time view, so a
|
|
1063
|
+
* `PreToolUse` hook later rewriting event args via
|
|
1064
|
+
* `updatedInput` can introduce placeholders that resolve
|
|
1065
|
+
* cross-batch (against prior runs) without ever picking up
|
|
1066
|
+
* same-turn direct outputs.
|
|
1067
|
+
*/
|
|
1068
|
+
const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
1069
|
+
if (preBatchSnapshot != null) {
|
|
1070
|
+
for (const entry of eventEntries) {
|
|
1071
|
+
if (entry.call.id != null) {
|
|
1072
|
+
const { resolved, unresolved } = preBatchSnapshot.resolve(entry.call.args);
|
|
1073
|
+
preResolvedEventArgs.set(entry.call.id, {
|
|
1074
|
+
resolved: resolved,
|
|
1075
|
+
unresolved,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
528
1079
|
}
|
|
529
|
-
const directCalls = filteredCalls.filter((c) => this.directToolNames.has(c.name));
|
|
530
|
-
const eventCalls = filteredCalls.filter((c) => !this.directToolNames.has(c.name));
|
|
531
1080
|
const directOutputs = directCalls.length > 0
|
|
532
|
-
? await Promise.all(directCalls.map((call) => this.runTool(call, config
|
|
1081
|
+
? await Promise.all(directCalls.map((call, i) => this.runTool(call, config, {
|
|
1082
|
+
batchIndex: directIndices[i],
|
|
1083
|
+
turn,
|
|
1084
|
+
batchScopeId,
|
|
1085
|
+
resolvedArgsByCallId,
|
|
1086
|
+
})))
|
|
533
1087
|
: [];
|
|
534
1088
|
if (directCalls.length > 0 && directOutputs.length > 0) {
|
|
535
|
-
this.handleRunToolCompletions(directCalls, directOutputs, config);
|
|
1089
|
+
this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId);
|
|
536
1090
|
}
|
|
537
|
-
const
|
|
538
|
-
? await this.dispatchToolEvents(eventCalls, config
|
|
539
|
-
|
|
540
|
-
|
|
1091
|
+
const eventResult = eventCalls.length > 0
|
|
1092
|
+
? await this.dispatchToolEvents(eventCalls, config, {
|
|
1093
|
+
batchIndices: eventIndices,
|
|
1094
|
+
turn,
|
|
1095
|
+
batchScopeId,
|
|
1096
|
+
preResolvedArgs: preResolvedEventArgs,
|
|
1097
|
+
preBatchSnapshot,
|
|
1098
|
+
})
|
|
1099
|
+
: {
|
|
1100
|
+
toolMessages: [],
|
|
1101
|
+
injected: [],
|
|
1102
|
+
};
|
|
1103
|
+
outputs = [
|
|
1104
|
+
...directOutputs,
|
|
1105
|
+
...eventResult.toolMessages,
|
|
1106
|
+
...eventResult.injected,
|
|
1107
|
+
];
|
|
541
1108
|
}
|
|
542
1109
|
else {
|
|
543
|
-
outputs = await Promise.all(filteredCalls.map((call) => this.runTool(call, config
|
|
544
|
-
|
|
1110
|
+
outputs = await Promise.all(filteredCalls.map((call, i) => this.runTool(call, config, {
|
|
1111
|
+
batchIndex: i,
|
|
1112
|
+
turn,
|
|
1113
|
+
batchScopeId,
|
|
1114
|
+
resolvedArgsByCallId,
|
|
1115
|
+
})));
|
|
1116
|
+
this.handleRunToolCompletions(filteredCalls, outputs, config, resolvedArgsByCallId);
|
|
545
1117
|
}
|
|
546
1118
|
}
|
|
547
1119
|
if (!outputs.some(isCommand)) {
|