@librechat/agents 3.1.77 → 3.1.78
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/common/enum.cjs +54 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +155 -4
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
- package/dist/cjs/main.cjs +90 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +27 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +99 -0
- package/dist/cjs/messages/recency.cjs.map +1 -0
- package/dist/cjs/run.cjs +30 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +100 -6
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +635 -23
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/local/attachments.cjs +183 -0
- package/dist/cjs/tools/local/attachments.cjs.map +1 -0
- package/dist/cjs/tools/local/bashAst.cjs +129 -0
- package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
- package/dist/cjs/tools/local/editStrategies.cjs +188 -0
- package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
- package/dist/cjs/tools/local/textEncoding.cjs +30 -0
- package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
- package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +31 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +53 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +156 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
- package/dist/esm/main.mjs +17 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +99 -0
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +26 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +97 -0
- package/dist/esm/messages/recency.mjs.map +1 -0
- package/dist/esm/run.mjs +30 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +100 -6
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +635 -23
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/local/attachments.mjs +180 -0
- package/dist/esm/tools/local/attachments.mjs.map +1 -0
- package/dist/esm/tools/local/bashAst.mjs +126 -0
- package/dist/esm/tools/local/bashAst.mjs.map +1 -0
- package/dist/esm/tools/local/editStrategies.mjs +185 -0
- package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
- package/dist/esm/tools/local/textEncoding.mjs +27 -0
- package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
- package/dist/esm/tools/local/workspaceFS.mjs +49 -0
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +31 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +39 -1
- package/dist/types/graphs/Graph.d.ts +34 -0
- package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messages/anthropicToolCache.d.ts +51 -0
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/messages/prune.d.ts +11 -0
- package/dist/types/messages/recency.d.ts +64 -0
- package/dist/types/run.d.ts +21 -0
- package/dist/types/tools/ToolNode.d.ts +145 -2
- package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
- package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
- package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
- package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
- package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
- package/dist/types/tools/local/attachments.d.ts +84 -0
- package/dist/types/tools/local/bashAst.d.ts +11 -0
- package/dist/types/tools/local/editStrategies.d.ts +28 -0
- package/dist/types/tools/local/index.d.ts +12 -0
- package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
- package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
- package/dist/types/tools/local/textEncoding.d.ts +21 -0
- package/dist/types/tools/local/workspaceFS.d.ts +49 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +29 -0
- package/dist/types/types/hitl.d.ts +56 -27
- package/dist/types/types/run.d.ts +8 -1
- package/dist/types/types/summarize.d.ts +30 -0
- package/dist/types/types/tools.d.ts +341 -6
- package/package.json +21 -2
- package/src/common/enum.ts +54 -0
- package/src/graphs/Graph.ts +173 -6
- package/src/hooks/__tests__/compactHooks.test.ts +38 -2
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
- package/src/hooks/createWorkspacePolicyHook.ts +355 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +1 -0
- package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
- package/src/messages/__tests__/recency.test.ts +267 -0
- package/src/messages/anthropicToolCache.ts +116 -0
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +27 -1
- package/src/messages/recency.ts +155 -0
- package/src/run.ts +31 -0
- package/src/scripts/compare_pi_vs_ours.ts +840 -0
- package/src/scripts/local_engine.ts +166 -0
- package/src/scripts/local_engine_checkpointer.ts +205 -0
- package/src/scripts/local_engine_compile.ts +263 -0
- package/src/scripts/local_engine_hooks.ts +226 -0
- package/src/scripts/local_engine_image.ts +201 -0
- package/src/scripts/local_engine_ptc.ts +151 -0
- package/src/scripts/local_engine_workspace.ts +258 -0
- package/src/scripts/subagent-configurable-inheritance.ts +252 -0
- package/src/scripts/summarization-recency.ts +462 -0
- package/src/specs/prune.test.ts +39 -0
- package/src/summarization/__tests__/node.test.ts +499 -3
- package/src/summarization/node.ts +124 -7
- package/src/tools/ToolNode.ts +769 -20
- package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
- package/src/tools/__tests__/directToolHooks.test.ts +411 -0
- package/src/tools/__tests__/localToolNames.test.ts +73 -0
- package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
- package/src/tools/local/CompileCheckTool.ts +278 -0
- package/src/tools/local/FileCheckpointer.ts +93 -0
- package/src/tools/local/LocalCodingTools.ts +1342 -0
- package/src/tools/local/LocalExecutionEngine.ts +1329 -0
- package/src/tools/local/LocalExecutionTools.ts +167 -0
- package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
- package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
- package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
- package/src/tools/local/attachments.ts +251 -0
- package/src/tools/local/bashAst.ts +151 -0
- package/src/tools/local/editStrategies.ts +188 -0
- package/src/tools/local/index.ts +12 -0
- package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
- package/src/tools/local/syntaxCheck.ts +243 -0
- package/src/tools/local/textEncoding.ts +37 -0
- package/src/tools/local/workspaceFS.ts +89 -0
- package/src/tools/subagent/SubagentExecutor.ts +60 -0
- package/src/types/hitl.ts +56 -27
- package/src/types/run.ts +12 -1
- package/src/types/summarize.ts +31 -0
- package/src/types/tools.ts +359 -7
|
@@ -12,7 +12,19 @@ import { RunnableCallable } from '../utils/run.mjs';
|
|
|
12
12
|
import 'ai-tokenizer';
|
|
13
13
|
import 'zod-to-json-schema';
|
|
14
14
|
import { executeHooks } from '../hooks/executeHooks.mjs';
|
|
15
|
+
import '../hooks/createWorkspacePolicyHook.mjs';
|
|
15
16
|
import { ToolOutputReferenceRegistry, buildReferenceKey } from './toolOutputReferences.mjs';
|
|
17
|
+
import './local/CompileCheckTool.mjs';
|
|
18
|
+
import 'path';
|
|
19
|
+
import 'fs/promises';
|
|
20
|
+
import './local/LocalCodingTools.mjs';
|
|
21
|
+
import './local/LocalExecutionEngine.mjs';
|
|
22
|
+
import '@langchain/core/tools';
|
|
23
|
+
import './CodeExecutor.mjs';
|
|
24
|
+
import './BashExecutor.mjs';
|
|
25
|
+
import './local/LocalProgrammaticToolCalling.mjs';
|
|
26
|
+
import { resolveLocalToolRegistry, resolveLocalExecutionTools } from './local/resolveLocalExecutionTools.mjs';
|
|
27
|
+
import './local/attachments.mjs';
|
|
16
28
|
|
|
17
29
|
/**
|
|
18
30
|
* Helper to check if a value is a Send object
|
|
@@ -158,6 +170,17 @@ class ToolNode extends RunnableCallable {
|
|
|
158
170
|
toolUsageCount;
|
|
159
171
|
/** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
|
|
160
172
|
toolCallTurns = new Map();
|
|
173
|
+
/**
|
|
174
|
+
* `call.id → turn` map dedicated to the direct-path lifecycle so the
|
|
175
|
+
* turn assigned on first entry is REUSED on LangGraph resume.
|
|
176
|
+
* Distinct from `toolCallTurns` (which is cleared at the start of
|
|
177
|
+
* every `run()` to keep per-batch event-dispatch metadata fresh) —
|
|
178
|
+
* the direct path needs stability across re-entries triggered by
|
|
179
|
+
* `interrupt()` resumes (Codex P2 #30). Cleared with the rest of
|
|
180
|
+
* the per-Run state in `clearHeavyState`-equivalent flushes when
|
|
181
|
+
* the Run ends.
|
|
182
|
+
*/
|
|
183
|
+
directPathTurns = new Map();
|
|
161
184
|
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
162
185
|
toolRegistry;
|
|
163
186
|
/** Cached programmatic tools (computed once on first PTC call) */
|
|
@@ -170,6 +193,13 @@ class ToolNode extends RunnableCallable {
|
|
|
170
193
|
agentId;
|
|
171
194
|
/** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
|
|
172
195
|
directToolNames;
|
|
196
|
+
/**
|
|
197
|
+
* File checkpointer extracted from the local coding tool bundle when
|
|
198
|
+
* `toolExecution.local.fileCheckpointing === true`. Exposed via
|
|
199
|
+
* {@link getFileCheckpointer}. Undefined when checkpointing is off
|
|
200
|
+
* or the local coding suite isn't bound to this node.
|
|
201
|
+
*/
|
|
202
|
+
fileCheckpointer;
|
|
173
203
|
/** Maximum characters allowed in a single tool result before truncation. */
|
|
174
204
|
maxToolResultChars;
|
|
175
205
|
/** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
|
|
@@ -192,6 +222,8 @@ class ToolNode extends RunnableCallable {
|
|
|
192
222
|
* ToolNode building its own.
|
|
193
223
|
*/
|
|
194
224
|
toolOutputRegistry;
|
|
225
|
+
/** Run-scoped selection for swapping remote code tools to local executors. */
|
|
226
|
+
toolExecution;
|
|
195
227
|
/**
|
|
196
228
|
* Monotonic counter used to mint a unique scope id for anonymous
|
|
197
229
|
* batches (ones invoked without a `run_id` in
|
|
@@ -200,7 +232,7 @@ class ToolNode extends RunnableCallable {
|
|
|
200
232
|
* other's in-flight state.
|
|
201
233
|
*/
|
|
202
234
|
anonBatchCounter = 0;
|
|
203
|
-
constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, }) {
|
|
235
|
+
constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }) {
|
|
204
236
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
205
237
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
206
238
|
this.toolCallStepIds = toolCallStepIds;
|
|
@@ -208,7 +240,7 @@ class ToolNode extends RunnableCallable {
|
|
|
208
240
|
this.loadRuntimeTools = loadRuntimeTools;
|
|
209
241
|
this.errorHandler = errorHandler;
|
|
210
242
|
this.toolUsageCount = new Map();
|
|
211
|
-
this.toolRegistry = toolRegistry;
|
|
243
|
+
this.toolRegistry = resolveLocalToolRegistry({ toolRegistry, toolExecution });
|
|
212
244
|
this.sessions = sessions;
|
|
213
245
|
this.eventDrivenMode = eventDrivenMode ?? false;
|
|
214
246
|
this.agentId = agentId;
|
|
@@ -217,6 +249,14 @@ class ToolNode extends RunnableCallable {
|
|
|
217
249
|
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
|
|
218
250
|
this.hookRegistry = hookRegistry;
|
|
219
251
|
this.humanInTheLoop = humanInTheLoop;
|
|
252
|
+
this.toolExecution = toolExecution;
|
|
253
|
+
// Caller-provided checkpointer wins. Graphs use this to share a
|
|
254
|
+
// single per-Run instance across every ToolNode they compile so
|
|
255
|
+
// `Run.rewindFiles()` reaches the same snapshot store regardless
|
|
256
|
+
// of which agent's tool batch ran. Falls through to the bundle's
|
|
257
|
+
// auto-created one when undefined (direct ToolNode construction).
|
|
258
|
+
this.fileCheckpointer = fileCheckpointer;
|
|
259
|
+
this.applyToolExecutionOverrides();
|
|
220
260
|
/**
|
|
221
261
|
* Precedence: an explicitly passed `toolOutputRegistry` instance
|
|
222
262
|
* wins over a config object so a host (`Graph`) can share one
|
|
@@ -249,6 +289,57 @@ class ToolNode extends RunnableCallable {
|
|
|
249
289
|
_unsafeGetToolOutputRegistry() {
|
|
250
290
|
return this.toolOutputRegistry;
|
|
251
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Replaces known remote Code API tools with local-process tools when
|
|
294
|
+
* `RunConfig.toolExecution.engine === 'local'`. In event-driven mode those
|
|
295
|
+
* names are also marked direct so the SDK executes them locally instead of
|
|
296
|
+
* dispatching the batch to a host-side remote sandbox handler. When the
|
|
297
|
+
* local coding suite is enabled, this also injects file/search/edit tools.
|
|
298
|
+
*/
|
|
299
|
+
applyToolExecutionOverrides() {
|
|
300
|
+
const resolved = resolveLocalExecutionTools({
|
|
301
|
+
toolMap: this.toolMap,
|
|
302
|
+
toolExecution: this.toolExecution,
|
|
303
|
+
fileCheckpointer: this.fileCheckpointer,
|
|
304
|
+
});
|
|
305
|
+
this.toolMap = resolved.toolMap;
|
|
306
|
+
if (resolved.fileCheckpointer != null) {
|
|
307
|
+
this.fileCheckpointer = resolved.fileCheckpointer;
|
|
308
|
+
}
|
|
309
|
+
if (resolved.directToolNames.size === 0) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.directToolNames = new Set([
|
|
313
|
+
...(this.directToolNames ?? new Set()),
|
|
314
|
+
...resolved.directToolNames,
|
|
315
|
+
]);
|
|
316
|
+
this.programmaticCache = undefined;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Returns the per-Run file checkpointer when
|
|
320
|
+
* `toolExecution.local.fileCheckpointing === true`. Hosts call
|
|
321
|
+
* `rewind()` on the returned object to restore captured pre-write
|
|
322
|
+
* file contents — the standard "undo a tool batch" pattern. Returns
|
|
323
|
+
* undefined when checkpointing is disabled or the local coding suite
|
|
324
|
+
* isn't bound. Manual review (finding E): without this getter, the
|
|
325
|
+
* config flag was a silent no-op outside of direct
|
|
326
|
+
* `createLocalCodingToolBundle()` use.
|
|
327
|
+
*/
|
|
328
|
+
getFileCheckpointer() {
|
|
329
|
+
return this.fileCheckpointer;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Flush the per-Run direct-path turn cache. Called by the Graph at
|
|
333
|
+
* end-of-Run via `clearHeavyState`. The map intentionally survives
|
|
334
|
+
* `run()` re-entry so an interrupt + resume reuses the original
|
|
335
|
+
* slot (Codex P2 #30), but it would otherwise grow linearly with
|
|
336
|
+
* tool calls and could collide across Runs if a provider reused
|
|
337
|
+
* call IDs (Codex P2 #33). Hosts can also call this directly if
|
|
338
|
+
* they reuse a ToolNode across batches outside of a Graph.
|
|
339
|
+
*/
|
|
340
|
+
clearDirectPathTurns() {
|
|
341
|
+
this.directPathTurns.clear();
|
|
342
|
+
}
|
|
252
343
|
/**
|
|
253
344
|
* Returns cached programmatic tools, computing once on first access.
|
|
254
345
|
* Single iteration builds both toolMap and toolDefs simultaneously.
|
|
@@ -287,9 +378,16 @@ class ToolNode extends RunnableCallable {
|
|
|
287
378
|
* substitutions. Omit when no registration should occur.
|
|
288
379
|
*/
|
|
289
380
|
async runTool(call, config, batchContext = {}) {
|
|
290
|
-
const { batchIndex, turn, batchScopeId, resolvedArgsByCallId } = batchContext;
|
|
381
|
+
const { batchIndex, turn, batchScopeId, resolvedArgsByCallId, preBatchSnapshot, } = batchContext;
|
|
291
382
|
const tool = this.toolMap.get(call.name);
|
|
292
383
|
const registry = this.toolOutputRegistry;
|
|
384
|
+
let resolveFn;
|
|
385
|
+
if (preBatchSnapshot != null) {
|
|
386
|
+
resolveFn = (_runId, args) => preBatchSnapshot.resolve(args);
|
|
387
|
+
}
|
|
388
|
+
else if (registry != null) {
|
|
389
|
+
resolveFn = (runIdArg, args) => registry.resolve(runIdArg, args);
|
|
390
|
+
}
|
|
293
391
|
/**
|
|
294
392
|
* Precompute the reference key once per call — captured locally
|
|
295
393
|
* so concurrent `invoke()` calls on the same ToolNode cannot race
|
|
@@ -323,15 +421,25 @@ class ToolNode extends RunnableCallable {
|
|
|
323
421
|
* It is intentionally distinct from the outer `turn` parameter
|
|
324
422
|
* (the batch turn used for ref keys); the latter is captured
|
|
325
423
|
* before the try block when constructing `refKey`.
|
|
424
|
+
*
|
|
425
|
+
* Prefer the value `runDirectToolWithLifecycleHooks` already
|
|
426
|
+
* incremented (Codex P2 #27) — its hook wants the SAME turn
|
|
427
|
+
* the tool will execute under. When called from a path that
|
|
428
|
+
* doesn't pre-increment (event dispatch, the no-hooks
|
|
429
|
+
* shortcut), do the read+increment here.
|
|
326
430
|
*/
|
|
327
|
-
const usageCount =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
431
|
+
const usageCount = batchContext.usageCount ??
|
|
432
|
+
(() => {
|
|
433
|
+
const next = this.toolUsageCount.get(call.name) ?? 0;
|
|
434
|
+
this.toolUsageCount.set(call.name, next + 1);
|
|
435
|
+
if (call.id != null && call.id !== '') {
|
|
436
|
+
this.toolCallTurns.set(call.id, next);
|
|
437
|
+
}
|
|
438
|
+
return next;
|
|
439
|
+
})();
|
|
332
440
|
let args = call.args;
|
|
333
|
-
if (
|
|
334
|
-
const { resolved, unresolved } =
|
|
441
|
+
if (resolveFn != null) {
|
|
442
|
+
const { resolved, unresolved } = resolveFn(runId, args);
|
|
335
443
|
args = resolved;
|
|
336
444
|
unresolvedRefs = unresolved;
|
|
337
445
|
/**
|
|
@@ -367,6 +475,19 @@ class ToolNode extends RunnableCallable {
|
|
|
367
475
|
...invokeParams,
|
|
368
476
|
toolMap,
|
|
369
477
|
toolDefs,
|
|
478
|
+
// Plumb the hook context into the programmatic-tool path so
|
|
479
|
+
// inner tool calls made via the in-process bridge can run
|
|
480
|
+
// through `PreToolUse` (deny / updatedInput) before reaching
|
|
481
|
+
// the underlying tool. Without this, `run_tools_with_code`
|
|
482
|
+
// bypassed every PreToolUse hook the host registered for
|
|
483
|
+
// the tools it dispatches — including HITL gates on
|
|
484
|
+
// `write_file` / `edit_file` (manual review finding A).
|
|
485
|
+
hookContext: {
|
|
486
|
+
registry: this.hookRegistry,
|
|
487
|
+
runId: config.configurable?.run_id ?? '',
|
|
488
|
+
threadId: config.configurable?.thread_id,
|
|
489
|
+
agentId: this.agentId,
|
|
490
|
+
},
|
|
370
491
|
};
|
|
371
492
|
}
|
|
372
493
|
else if (call.name === Constants.TOOL_SEARCH) {
|
|
@@ -396,6 +517,7 @@ class ToolNode extends RunnableCallable {
|
|
|
396
517
|
session_id: file.session_id ?? codeSession.session_id,
|
|
397
518
|
id: file.id,
|
|
398
519
|
name: file.name,
|
|
520
|
+
...(file.entity_id != null ? { entity_id: file.entity_id } : {}),
|
|
399
521
|
}));
|
|
400
522
|
invokeParams._injected_files = fileRefs;
|
|
401
523
|
}
|
|
@@ -533,6 +655,428 @@ class ToolNode extends RunnableCallable {
|
|
|
533
655
|
});
|
|
534
656
|
}
|
|
535
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Runs a single in-process tool call with the same lifecycle hooks
|
|
660
|
+
* the event-dispatch path fires (`PreToolUse`, `PermissionDenied`,
|
|
661
|
+
* `PostToolUse`, `PostToolUseFailure`). Used for any tool whose
|
|
662
|
+
* implementation lives in the SDK process — i.e. every entry in
|
|
663
|
+
* `directToolNames` — so host-supplied policy hooks gate
|
|
664
|
+
* direct-invoked tools the same way they gate dispatched ones.
|
|
665
|
+
*
|
|
666
|
+
* Fast path: when the registry has none of the relevant events
|
|
667
|
+
* registered for this run, falls through to `runTool` with zero
|
|
668
|
+
* extra work. The hook list is also checked via
|
|
669
|
+
* `hasHookFor(event, runId)`, which performs the registry's own
|
|
670
|
+
* O(1) shortcut.
|
|
671
|
+
*
|
|
672
|
+
* Hook semantics intentionally mirror `dispatchToolEvents` for the
|
|
673
|
+
* single-call case:
|
|
674
|
+
* - `PreToolUse` returning `decision: 'deny'` synthesizes an error
|
|
675
|
+
* `ToolMessage` and fires `PermissionDenied` (observational).
|
|
676
|
+
* - `PreToolUse` returning `decision: 'ask'`:
|
|
677
|
+
* • When `humanInTheLoop.enabled === true`: raises a real
|
|
678
|
+
* `tool_approval` interrupt for this single tool call (the
|
|
679
|
+
* same payload shape the event path produces). On resume:
|
|
680
|
+
* `approve` runs the tool, `reject` blocks via
|
|
681
|
+
* `blockDirectCall`, `respond` returns the host-supplied
|
|
682
|
+
* `responseText` as a synthetic success ToolMessage,
|
|
683
|
+
* `edit` re-runs with edited args. LangGraph re-enters
|
|
684
|
+
* ToolNode.run from the start on resume; the hook fires
|
|
685
|
+
* again and the resume value distinguishes "first ask" from
|
|
686
|
+
* "second pass with decision".
|
|
687
|
+
* • When HITL is off: collapses to a fail-closed deny (matches
|
|
688
|
+
* the rest of the SDK's HITL-disabled default). One-time
|
|
689
|
+
* warning logged so hosts notice the gap.
|
|
690
|
+
* - `PreToolUse.updatedInput` is applied to the call before
|
|
691
|
+
* `runTool` runs; placeholder resolution inside `runTool` is
|
|
692
|
+
* idempotent on already-resolved args.
|
|
693
|
+
* - `PostToolUse.updatedOutput` replaces the returned
|
|
694
|
+
* `ToolMessage` content (preserving id/name/status).
|
|
695
|
+
* - `PostToolUseFailure` fires when `runTool` returns a
|
|
696
|
+
* `ToolMessage` whose `status === 'error'`. Observational only;
|
|
697
|
+
* the error message stays the source of truth.
|
|
698
|
+
*
|
|
699
|
+
* `PostToolBatch` aggregation across direct + dispatched outcomes is
|
|
700
|
+
* a separate concern: `dispatchToolEvents` accumulates batch entries
|
|
701
|
+
* locally and fires `PostToolBatch` at the end of its scope. Wiring
|
|
702
|
+
* direct-call entries into that aggregation crosses the two paths'
|
|
703
|
+
* scopes and is left to a follow-up.
|
|
704
|
+
*/
|
|
705
|
+
async runDirectToolWithLifecycleHooks(call, config, batchContext = {}) {
|
|
706
|
+
const runId = config.configurable?.run_id ?? '';
|
|
707
|
+
const hookRegistry = this.hookRegistry;
|
|
708
|
+
const hasPreHook = hookRegistry?.hasHookFor('PreToolUse', runId) === true;
|
|
709
|
+
const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
710
|
+
const hasFailureHook = hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
711
|
+
if (hookRegistry == null ||
|
|
712
|
+
(!hasPreHook && !hasPostHook && !hasFailureHook)) {
|
|
713
|
+
return this.runTool(call, config, batchContext);
|
|
714
|
+
}
|
|
715
|
+
const threadId = config.configurable?.thread_id;
|
|
716
|
+
const registryRunId = batchContext.batchScopeId ??
|
|
717
|
+
config.configurable?.run_id;
|
|
718
|
+
// Slot reservation, synchronous, before any await:
|
|
719
|
+
// 1. If this call.id already has a recorded turn (from a prior
|
|
720
|
+
// entry that asked / interrupted), REUSE it. LangGraph
|
|
721
|
+
// re-runs the entire ToolNode on resume, so the same call
|
|
722
|
+
// can hit this code multiple times — incrementing on each
|
|
723
|
+
// pass would push the eventual approved execution to
|
|
724
|
+
// `turn=N` instead of `turn=0` (Codex P2 #30: the fix from
|
|
725
|
+
// P2 #27 over-incremented across re-entries).
|
|
726
|
+
// 2. Otherwise reserve the next slot from the counter. Done
|
|
727
|
+
// synchronously so concurrent same-tool calls in a single
|
|
728
|
+
// Promise.all batch get distinct turns (the original P2 #27
|
|
729
|
+
// requirement still holds).
|
|
730
|
+
// Net: turns are stable per call.id across interrupt/resume,
|
|
731
|
+
// unique per call within a batch.
|
|
732
|
+
let usageCount;
|
|
733
|
+
// Look in the resume-stable map first; fall back to the
|
|
734
|
+
// per-batch one. (`directPathTurns` is set on first entry and
|
|
735
|
+
// survives `run()`'s clear, so a resume sees the original
|
|
736
|
+
// assignment.)
|
|
737
|
+
const cachedTurn = call.id != null && call.id !== ''
|
|
738
|
+
? this.directPathTurns.get(call.id) ??
|
|
739
|
+
this.toolCallTurns.get(call.id)
|
|
740
|
+
: undefined;
|
|
741
|
+
if (cachedTurn != null) {
|
|
742
|
+
usageCount = cachedTurn;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
usageCount = this.toolUsageCount.get(call.name) ?? 0;
|
|
746
|
+
this.toolUsageCount.set(call.name, usageCount + 1);
|
|
747
|
+
if (call.id != null && call.id !== '') {
|
|
748
|
+
this.toolCallTurns.set(call.id, usageCount);
|
|
749
|
+
// Dedicated direct-path map that SURVIVES `run()`'s
|
|
750
|
+
// toolCallTurns.clear() — so a re-entry triggered by
|
|
751
|
+
// LangGraph interrupt resume reuses this slot instead of
|
|
752
|
+
// re-incrementing. Codex P2 #30.
|
|
753
|
+
this.directPathTurns.set(call.id, usageCount);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const turn = usageCount;
|
|
757
|
+
const stepId = this.toolCallStepIds?.get(call.id ?? '') ?? '';
|
|
758
|
+
// Use the caller-threaded snapshot when available (P1 #18) so the
|
|
759
|
+
// value the PreToolUse hook observes matches the value the
|
|
760
|
+
// (later-awaited) `runTool` will actually run with — both are
|
|
761
|
+
// anchored to the pre-batch registry state.
|
|
762
|
+
let resolvedArgs = call.args;
|
|
763
|
+
if (batchContext.preBatchSnapshot != null) {
|
|
764
|
+
const { resolved } = batchContext.preBatchSnapshot.resolve(call.args);
|
|
765
|
+
resolvedArgs = resolved;
|
|
766
|
+
}
|
|
767
|
+
else if (this.toolOutputRegistry != null) {
|
|
768
|
+
const { resolved } = this.toolOutputRegistry.resolve(registryRunId, call.args);
|
|
769
|
+
resolvedArgs = resolved;
|
|
770
|
+
}
|
|
771
|
+
let effectiveCall = call;
|
|
772
|
+
if (hasPreHook) {
|
|
773
|
+
const preResult = await executeHooks({
|
|
774
|
+
registry: hookRegistry,
|
|
775
|
+
input: {
|
|
776
|
+
hook_event_name: 'PreToolUse',
|
|
777
|
+
runId,
|
|
778
|
+
threadId,
|
|
779
|
+
agentId: this.agentId,
|
|
780
|
+
toolName: call.name,
|
|
781
|
+
toolInput: resolvedArgs,
|
|
782
|
+
toolUseId: call.id ?? '',
|
|
783
|
+
stepId,
|
|
784
|
+
turn,
|
|
785
|
+
},
|
|
786
|
+
sessionId: runId,
|
|
787
|
+
matchQuery: call.name,
|
|
788
|
+
}).catch(() => undefined);
|
|
789
|
+
if (preResult != null) {
|
|
790
|
+
// Forward any additionalContext strings hooks returned into
|
|
791
|
+
// the per-batch sink so the caller materializes them as a
|
|
792
|
+
// HumanMessage for the next model turn — same shape as the
|
|
793
|
+
// event-driven path's `injected[]`. Codex P2 #39.
|
|
794
|
+
if (batchContext.additionalContextsSink != null &&
|
|
795
|
+
preResult.additionalContexts.length > 0) {
|
|
796
|
+
batchContext.additionalContextsSink.push(...preResult.additionalContexts);
|
|
797
|
+
}
|
|
798
|
+
// Apply any input rewrite first — `ask`-with-`updatedInput` is
|
|
799
|
+
// a valid combination (one matcher sanitises args, another asks
|
|
800
|
+
// for approval); the reviewer should see the sanitised args.
|
|
801
|
+
if (preResult.updatedInput != null) {
|
|
802
|
+
effectiveCall = {
|
|
803
|
+
...call,
|
|
804
|
+
args: preResult.updatedInput,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
if (preResult.decision === 'deny') {
|
|
808
|
+
return this.blockDirectCall({
|
|
809
|
+
call,
|
|
810
|
+
resolvedArgs,
|
|
811
|
+
reason: preResult.reason ?? 'Blocked by hook',
|
|
812
|
+
hookRegistry,
|
|
813
|
+
runId,
|
|
814
|
+
threadId,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (preResult.decision === 'ask') {
|
|
818
|
+
if (this.humanInTheLoop?.enabled !== true) {
|
|
819
|
+
// Fail-closed: no HITL UI configured, so we can't actually
|
|
820
|
+
// ask. Logged once via the existing helper.
|
|
821
|
+
const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
|
|
822
|
+
return this.blockDirectCall({
|
|
823
|
+
call,
|
|
824
|
+
resolvedArgs,
|
|
825
|
+
reason,
|
|
826
|
+
hookRegistry,
|
|
827
|
+
runId,
|
|
828
|
+
threadId,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
// Raise a single-tool tool_approval interrupt. LangGraph
|
|
832
|
+
// throws on the first execution (host gets the interrupt)
|
|
833
|
+
// and returns the resume value on re-entry. Because direct
|
|
834
|
+
// tools re-enter the entire ToolNode.run on resume, the
|
|
835
|
+
// PreToolUse hook fires AGAIN — which is fine: the hook is
|
|
836
|
+
// expected to be deterministic, and the resume value is what
|
|
837
|
+
// distinguishes "first call asking" from "second call after
|
|
838
|
+
// approve/reject". We anchor `interrupt()` against the
|
|
839
|
+
// node's RunnableConfig the same way `dispatchToolEvents`
|
|
840
|
+
// does (ToolNode disables LangSmith tracing, so the
|
|
841
|
+
// AsyncLocalStorage frame must be re-established here).
|
|
842
|
+
const askEntry = {
|
|
843
|
+
entry: {
|
|
844
|
+
call: effectiveCall,
|
|
845
|
+
args: effectiveCall.args,
|
|
846
|
+
stepId,
|
|
847
|
+
},
|
|
848
|
+
reason: preResult.reason,
|
|
849
|
+
allowedDecisions: preResult.allowedDecisions,
|
|
850
|
+
};
|
|
851
|
+
const payload = buildToolApprovalInterruptPayload([askEntry]);
|
|
852
|
+
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
|
|
853
|
+
const decisionByCallId = normalizeApprovalDecisions([call.id], resumeValue);
|
|
854
|
+
const decision = decisionByCallId.get(call.id) ?? {
|
|
855
|
+
type: 'reject',
|
|
856
|
+
reason: 'No decision provided for tool approval',
|
|
857
|
+
};
|
|
858
|
+
const declaredType = decision.type;
|
|
859
|
+
if (preResult.allowedDecisions != null &&
|
|
860
|
+
(typeof declaredType !== 'string' ||
|
|
861
|
+
!preResult.allowedDecisions.includes(declaredType))) {
|
|
862
|
+
return this.blockDirectCall({
|
|
863
|
+
call,
|
|
864
|
+
resolvedArgs,
|
|
865
|
+
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${preResult.allowedDecisions.join(', ')}] — failing closed`,
|
|
866
|
+
hookRegistry,
|
|
867
|
+
runId,
|
|
868
|
+
threadId,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (decision.type === 'reject') {
|
|
872
|
+
return this.blockDirectCall({
|
|
873
|
+
call,
|
|
874
|
+
resolvedArgs,
|
|
875
|
+
reason: decision.reason ??
|
|
876
|
+
preResult.reason ??
|
|
877
|
+
'Rejected by user',
|
|
878
|
+
hookRegistry,
|
|
879
|
+
runId,
|
|
880
|
+
threadId,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (decision.type === 'respond') {
|
|
884
|
+
const responseText = decision
|
|
885
|
+
.responseText;
|
|
886
|
+
if (typeof responseText !== 'string') {
|
|
887
|
+
return this.blockDirectCall({
|
|
888
|
+
call,
|
|
889
|
+
resolvedArgs,
|
|
890
|
+
reason: 'Approval payload `respond` was missing a string `responseText`',
|
|
891
|
+
hookRegistry,
|
|
892
|
+
runId,
|
|
893
|
+
threadId,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
return new ToolMessage({
|
|
897
|
+
status: 'success',
|
|
898
|
+
content: responseText,
|
|
899
|
+
name: call.name,
|
|
900
|
+
tool_call_id: call.id ?? '',
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
if (decision.type === 'edit') {
|
|
904
|
+
// Mirror the event-driven path's validation
|
|
905
|
+
// (see `dispatchToolEvents`'s edit branch). The wire
|
|
906
|
+
// field is `updatedInput`, NOT `args` — hosts following
|
|
907
|
+
// the documented `ToolApprovalDecision` shape were
|
|
908
|
+
// silently ignored before, so the tool ran with the
|
|
909
|
+
// original (un-edited) arguments. Fail closed on
|
|
910
|
+
// malformed payloads instead of falling through with
|
|
911
|
+
// undefined args.
|
|
912
|
+
const updatedInput = decision
|
|
913
|
+
.updatedInput;
|
|
914
|
+
if (updatedInput === null ||
|
|
915
|
+
typeof updatedInput !== 'object' ||
|
|
916
|
+
Array.isArray(updatedInput)) {
|
|
917
|
+
return new ToolMessage({
|
|
918
|
+
status: 'error',
|
|
919
|
+
content: 'Decision "edit" missing object updatedInput — failing closed.',
|
|
920
|
+
name: call.name,
|
|
921
|
+
tool_call_id: call.id ?? '',
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
effectiveCall = {
|
|
925
|
+
...call,
|
|
926
|
+
args: updatedInput,
|
|
927
|
+
};
|
|
928
|
+
// fall through to executing the edited call
|
|
929
|
+
}
|
|
930
|
+
// 'approve' (or 'edit' after applying edits) → fall through
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const output = await this.runTool(effectiveCall, config, {
|
|
935
|
+
...batchContext,
|
|
936
|
+
usageCount,
|
|
937
|
+
});
|
|
938
|
+
if (!(output instanceof ToolMessage)) {
|
|
939
|
+
return output;
|
|
940
|
+
}
|
|
941
|
+
if (output.status === 'error' && hasFailureHook) {
|
|
942
|
+
// Await the failure hook (instead of fire-and-forget) so we
|
|
943
|
+
// can capture additionalContexts before returning. The hook is
|
|
944
|
+
// still observational w.r.t. the tool result itself — we don't
|
|
945
|
+
// mutate `output`, just plumb the contexts. Codex P2 #39.
|
|
946
|
+
const failureResult = await executeHooks({
|
|
947
|
+
registry: hookRegistry,
|
|
948
|
+
input: {
|
|
949
|
+
hook_event_name: 'PostToolUseFailure',
|
|
950
|
+
runId,
|
|
951
|
+
threadId,
|
|
952
|
+
agentId: this.agentId,
|
|
953
|
+
toolName: call.name,
|
|
954
|
+
toolInput: effectiveCall.args,
|
|
955
|
+
toolUseId: call.id ?? '',
|
|
956
|
+
error: typeof output.content === 'string'
|
|
957
|
+
? output.content
|
|
958
|
+
: JSON.stringify(output.content),
|
|
959
|
+
stepId,
|
|
960
|
+
turn,
|
|
961
|
+
},
|
|
962
|
+
sessionId: runId,
|
|
963
|
+
matchQuery: call.name,
|
|
964
|
+
}).catch(() => undefined);
|
|
965
|
+
if (failureResult != null &&
|
|
966
|
+
batchContext.additionalContextsSink != null &&
|
|
967
|
+
failureResult.additionalContexts.length > 0) {
|
|
968
|
+
batchContext.additionalContextsSink.push(...failureResult.additionalContexts);
|
|
969
|
+
}
|
|
970
|
+
return output;
|
|
971
|
+
}
|
|
972
|
+
if (output.status !== 'error' && hasPostHook) {
|
|
973
|
+
const postResult = await executeHooks({
|
|
974
|
+
registry: hookRegistry,
|
|
975
|
+
input: {
|
|
976
|
+
hook_event_name: 'PostToolUse',
|
|
977
|
+
runId,
|
|
978
|
+
threadId,
|
|
979
|
+
agentId: this.agentId,
|
|
980
|
+
toolName: call.name,
|
|
981
|
+
toolInput: effectiveCall.args,
|
|
982
|
+
toolOutput: output.content,
|
|
983
|
+
toolUseId: call.id ?? '',
|
|
984
|
+
stepId,
|
|
985
|
+
turn,
|
|
986
|
+
},
|
|
987
|
+
sessionId: runId,
|
|
988
|
+
matchQuery: call.name,
|
|
989
|
+
}).catch(() => undefined);
|
|
990
|
+
// Forward additionalContexts from the PostToolUse hook into
|
|
991
|
+
// the per-batch sink (Codex P2 #39).
|
|
992
|
+
if (postResult != null &&
|
|
993
|
+
batchContext.additionalContextsSink != null &&
|
|
994
|
+
postResult.additionalContexts.length > 0) {
|
|
995
|
+
batchContext.additionalContextsSink.push(...postResult.additionalContexts);
|
|
996
|
+
}
|
|
997
|
+
if (postResult?.updatedOutput != null) {
|
|
998
|
+
const replaced = typeof postResult.updatedOutput === 'string'
|
|
999
|
+
? postResult.updatedOutput
|
|
1000
|
+
: JSON.stringify(postResult.updatedOutput);
|
|
1001
|
+
// Keep the tool-output registry in sync with what the model
|
|
1002
|
+
// actually sees. Without this, `runTool` already registered
|
|
1003
|
+
// the PRE-hook content under `_refKey`, and a later
|
|
1004
|
+
// `{{tool<i>turn<n>}}` substitution would deliver the stale
|
|
1005
|
+
// pre-hook bytes while the model (and downstream tools)
|
|
1006
|
+
// observed the post-hook replacement. Read `_refKey` /
|
|
1007
|
+
// `_refScope` straight off the message metadata that
|
|
1008
|
+
// `recordOutputReference` stamped — no need to re-derive
|
|
1009
|
+
// (and we couldn't, for anonymous-batch synthetic scopes).
|
|
1010
|
+
const refMeta = output.additional_kwargs;
|
|
1011
|
+
const refKey = refMeta?._refKey;
|
|
1012
|
+
const refScope = refMeta?._refScope;
|
|
1013
|
+
if (this.toolOutputRegistry != null && refKey != null) {
|
|
1014
|
+
this.toolOutputRegistry.set(refScope, refKey, replaced);
|
|
1015
|
+
}
|
|
1016
|
+
return new ToolMessage({
|
|
1017
|
+
status: output.status,
|
|
1018
|
+
name: output.name,
|
|
1019
|
+
content: replaced,
|
|
1020
|
+
artifact: output.artifact,
|
|
1021
|
+
tool_call_id: output.tool_call_id,
|
|
1022
|
+
additional_kwargs: output.additional_kwargs,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return output;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* `ask` decisions on direct-path tools collapse to fail-closed deny
|
|
1030
|
+
* only when `humanInTheLoop.enabled !== true` (i.e. there's no host
|
|
1031
|
+
* UI configured to actually prompt the user). Logged once per process
|
|
1032
|
+
* so the gap is visible. When HITL IS enabled, `ask` raises a real
|
|
1033
|
+
* LangGraph `interrupt()` instead — see `runDirectToolWithLifecycleHooks`.
|
|
1034
|
+
*/
|
|
1035
|
+
askDirectWarningEmitted = false;
|
|
1036
|
+
resolveAskDecisionForDirectTool(reason, toolName) {
|
|
1037
|
+
if (!this.askDirectWarningEmitted) {
|
|
1038
|
+
this.askDirectWarningEmitted = true;
|
|
1039
|
+
// eslint-disable-next-line no-console
|
|
1040
|
+
console.warn(`[ToolNode] PreToolUse returned 'ask' for direct-path tool "${toolName}" but ` +
|
|
1041
|
+
'humanInTheLoop is not enabled — failing closed. Set humanInTheLoop.enabled=true ' +
|
|
1042
|
+
'to raise a tool_approval interrupt the host can resolve.');
|
|
1043
|
+
}
|
|
1044
|
+
return reason ?? 'Blocked by hook';
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Synthesize a Blocked ToolMessage AND fire `PermissionDenied`
|
|
1048
|
+
* (observational) for a direct-path tool call. Centralised so the
|
|
1049
|
+
* deny path looks identical whether the block came from `'deny'` or
|
|
1050
|
+
* from a fail-closed/`'reject'`/policy-violation path.
|
|
1051
|
+
*/
|
|
1052
|
+
blockDirectCall(args) {
|
|
1053
|
+
const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
|
|
1054
|
+
if (hookRegistry.hasHookFor('PermissionDenied', runId) === true) {
|
|
1055
|
+
executeHooks({
|
|
1056
|
+
registry: hookRegistry,
|
|
1057
|
+
input: {
|
|
1058
|
+
hook_event_name: 'PermissionDenied',
|
|
1059
|
+
runId,
|
|
1060
|
+
threadId,
|
|
1061
|
+
agentId: this.agentId,
|
|
1062
|
+
toolName: call.name,
|
|
1063
|
+
toolInput: resolvedArgs,
|
|
1064
|
+
toolUseId: call.id ?? '',
|
|
1065
|
+
reason,
|
|
1066
|
+
},
|
|
1067
|
+
sessionId: runId,
|
|
1068
|
+
matchQuery: call.name,
|
|
1069
|
+
}).catch(() => {
|
|
1070
|
+
/* observational */
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
return new ToolMessage({
|
|
1074
|
+
status: 'error',
|
|
1075
|
+
content: `Blocked: ${reason}`,
|
|
1076
|
+
name: call.name,
|
|
1077
|
+
tool_call_id: call.id ?? '',
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
536
1080
|
/**
|
|
537
1081
|
* Registers the full, raw output under `refKey` (when provided) and
|
|
538
1082
|
* builds the per-message ref metadata stamped onto the resulting
|
|
@@ -597,6 +1141,7 @@ class ToolNode extends RunnableCallable {
|
|
|
597
1141
|
session_id: file.session_id ?? codeSession.session_id,
|
|
598
1142
|
id: file.id,
|
|
599
1143
|
name: file.name,
|
|
1144
|
+
...(file.entity_id != null ? { entity_id: file.entity_id } : {}),
|
|
600
1145
|
}));
|
|
601
1146
|
}
|
|
602
1147
|
return context;
|
|
@@ -615,7 +1160,8 @@ class ToolNode extends RunnableCallable {
|
|
|
615
1160
|
continue;
|
|
616
1161
|
}
|
|
617
1162
|
const request = requestMap.get(result.toolCallId);
|
|
618
|
-
if (
|
|
1163
|
+
if (request?.name == null ||
|
|
1164
|
+
request.name === '' ||
|
|
619
1165
|
(!CODE_EXECUTION_TOOLS.has(request.name) &&
|
|
620
1166
|
request.name !== Constants.SKILL_TOOL)) {
|
|
621
1167
|
continue;
|
|
@@ -1527,15 +2073,37 @@ class ToolNode extends RunnableCallable {
|
|
|
1527
2073
|
batchScopeId,
|
|
1528
2074
|
});
|
|
1529
2075
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2076
|
+
// Same per-batch sink the message-state branches use so
|
|
2077
|
+
// direct-path PreToolUse/PostToolUse/Failure additionalContexts
|
|
2078
|
+
// surface here too. Codex P2 [44] — round 14 added the sink to
|
|
2079
|
+
// both message-state branches but missed this Send-input
|
|
2080
|
+
// branch, so direct tools dispatched via Send (a supported
|
|
2081
|
+
// input shape) still silently dropped hook context.
|
|
2082
|
+
const directAdditionalContexts = [];
|
|
2083
|
+
const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
|
|
2084
|
+
batchIndex: 0,
|
|
2085
|
+
turn,
|
|
2086
|
+
batchScopeId,
|
|
2087
|
+
resolvedArgsByCallId,
|
|
2088
|
+
additionalContextsSink: directAdditionalContexts,
|
|
2089
|
+
});
|
|
2090
|
+
outputs =
|
|
2091
|
+
directAdditionalContexts.length > 0
|
|
2092
|
+
? [
|
|
2093
|
+
sendOutput,
|
|
2094
|
+
new HumanMessage({
|
|
2095
|
+
content: directAdditionalContexts.join('\n\n'),
|
|
2096
|
+
// Match the event-driven path's marker so hosts /
|
|
2097
|
+
// model-side annotators treat this as system intent
|
|
2098
|
+
// rather than ordinary user text. Codex P2 [46].
|
|
2099
|
+
additional_kwargs: { role: 'system', source: 'hook' },
|
|
2100
|
+
}),
|
|
2101
|
+
]
|
|
2102
|
+
: [sendOutput];
|
|
2103
|
+
this.handleRunToolCompletions([input.lg_tool_call],
|
|
2104
|
+
// Pass only the tool output to completion handling; the
|
|
2105
|
+
// HumanMessage isn't a tool result.
|
|
2106
|
+
[sendOutput], config, resolvedArgsByCallId);
|
|
1539
2107
|
}
|
|
1540
2108
|
else {
|
|
1541
2109
|
let messages;
|
|
@@ -1566,6 +2134,7 @@ class ToolNode extends RunnableCallable {
|
|
|
1566
2134
|
const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
|
|
1567
2135
|
this.toolMap =
|
|
1568
2136
|
toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
2137
|
+
this.applyToolExecutionOverrides();
|
|
1569
2138
|
this.programmaticCache = undefined; // Invalidate cache on toolMap change
|
|
1570
2139
|
}
|
|
1571
2140
|
const filteredCalls = aiMessage.tool_calls?.filter((call) => {
|
|
@@ -1637,12 +2206,19 @@ class ToolNode extends RunnableCallable {
|
|
|
1637
2206
|
}
|
|
1638
2207
|
}
|
|
1639
2208
|
}
|
|
2209
|
+
// Per-batch sink for direct-path hook additionalContexts
|
|
2210
|
+
// (Codex P2 #39). Materialized as a HumanMessage at end-of-
|
|
2211
|
+
// batch so the next model turn sees the injected context,
|
|
2212
|
+
// matching the event path's `injected[]` shape.
|
|
2213
|
+
const directAdditionalContexts = [];
|
|
1640
2214
|
const directOutputs = directCalls.length > 0
|
|
1641
|
-
? await Promise.all(directCalls.map((call, i) => this.
|
|
2215
|
+
? await Promise.all(directCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
|
|
1642
2216
|
batchIndex: directIndices[i],
|
|
1643
2217
|
turn,
|
|
1644
2218
|
batchScopeId,
|
|
1645
2219
|
resolvedArgsByCallId,
|
|
2220
|
+
preBatchSnapshot,
|
|
2221
|
+
additionalContextsSink: directAdditionalContexts,
|
|
1646
2222
|
})))
|
|
1647
2223
|
: [];
|
|
1648
2224
|
if (directCalls.length > 0 && directOutputs.length > 0) {
|
|
@@ -1660,20 +2236,56 @@ class ToolNode extends RunnableCallable {
|
|
|
1660
2236
|
toolMessages: [],
|
|
1661
2237
|
injected: [],
|
|
1662
2238
|
};
|
|
2239
|
+
const directInjected = directAdditionalContexts.length > 0
|
|
2240
|
+
? [
|
|
2241
|
+
new HumanMessage({
|
|
2242
|
+
content: directAdditionalContexts.join('\n\n'),
|
|
2243
|
+
// System-role metadata to match the event-driven
|
|
2244
|
+
// path so policy/recovery guidance is treated
|
|
2245
|
+
// consistently regardless of whether the tool ran
|
|
2246
|
+
// direct or dispatched. Codex P2 [46].
|
|
2247
|
+
additional_kwargs: { role: 'system', source: 'hook' },
|
|
2248
|
+
}),
|
|
2249
|
+
]
|
|
2250
|
+
: [];
|
|
1663
2251
|
outputs = [
|
|
1664
2252
|
...directOutputs,
|
|
1665
2253
|
...eventResult.toolMessages,
|
|
2254
|
+
...directInjected,
|
|
1666
2255
|
...eventResult.injected,
|
|
1667
2256
|
];
|
|
1668
2257
|
}
|
|
1669
2258
|
else {
|
|
1670
|
-
|
|
2259
|
+
// Same per-batch pre-snapshot as the mixed path, applied to
|
|
2260
|
+
// the all-direct case so `Promise.all`-induced ordering can't
|
|
2261
|
+
// leak a sibling's just-registered output into a sister
|
|
2262
|
+
// call's args mid-await (Codex P1 #18).
|
|
2263
|
+
const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
2264
|
+
const directAdditionalContexts = [];
|
|
2265
|
+
const toolOutputs = await Promise.all(filteredCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
|
|
1671
2266
|
batchIndex: i,
|
|
1672
2267
|
turn,
|
|
1673
2268
|
batchScopeId,
|
|
1674
2269
|
resolvedArgsByCallId,
|
|
2270
|
+
preBatchSnapshot,
|
|
2271
|
+
additionalContextsSink: directAdditionalContexts,
|
|
1675
2272
|
})));
|
|
1676
|
-
this.handleRunToolCompletions(filteredCalls,
|
|
2273
|
+
this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId);
|
|
2274
|
+
// Append accumulated additionalContexts as a single
|
|
2275
|
+
// HumanMessage so the next model turn sees them. Codex P2 #39.
|
|
2276
|
+
outputs =
|
|
2277
|
+
directAdditionalContexts.length > 0
|
|
2278
|
+
? [
|
|
2279
|
+
...toolOutputs,
|
|
2280
|
+
new HumanMessage({
|
|
2281
|
+
content: directAdditionalContexts.join('\n\n'),
|
|
2282
|
+
// Same system-role marker the event-driven path
|
|
2283
|
+
// uses so direct vs dispatched is invisible to
|
|
2284
|
+
// downstream consumers. Codex P2 [46].
|
|
2285
|
+
additional_kwargs: { role: 'system', source: 'hook' },
|
|
2286
|
+
}),
|
|
2287
|
+
]
|
|
2288
|
+
: toolOutputs;
|
|
1677
2289
|
}
|
|
1678
2290
|
}
|
|
1679
2291
|
if (!outputs.some(isCommand)) {
|