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