@librechat/agents 3.1.77-dev.1 → 3.1.78-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/common/enum.cjs +54 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +148 -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/llm/openai/index.cjs +317 -1
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- 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 +1 -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 +149 -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/llm/openai/index.mjs +318 -2
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- 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 +1 -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/llm/openai/index.d.ts +17 -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/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 +164 -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/llm/openai/deepseek.test.ts +479 -0
- package/src/llm/openai/index.ts +484 -1
- 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/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__/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/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
package/src/tools/ToolNode.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
ToolOutputResolveView,
|
|
26
26
|
PreResolvedArgsMap,
|
|
27
27
|
ResolvedArgsByCallId,
|
|
28
|
+
ResolveResult,
|
|
28
29
|
} from '@/tools/toolOutputReferences';
|
|
29
30
|
import type {
|
|
30
31
|
HookRegistry,
|
|
@@ -45,6 +46,10 @@ import {
|
|
|
45
46
|
buildReferenceKey,
|
|
46
47
|
ToolOutputReferenceRegistry,
|
|
47
48
|
} from '@/tools/toolOutputReferences';
|
|
49
|
+
import {
|
|
50
|
+
resolveLocalToolRegistry,
|
|
51
|
+
resolveLocalExecutionTools,
|
|
52
|
+
} from '@/tools/local';
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
55
|
* Per-call batch context for `runTool`. Bundles every optional
|
|
@@ -60,6 +65,36 @@ type RunToolBatchContext = {
|
|
|
60
65
|
batchScopeId?: string;
|
|
61
66
|
/** Batch-local sink for post-substitution args. */
|
|
62
67
|
resolvedArgsByCallId?: ResolvedArgsByCallId;
|
|
68
|
+
/**
|
|
69
|
+
* Frozen pre-batch view of the tool-output registry. When supplied,
|
|
70
|
+
* `runTool` resolves `{{tool…turn…}}` placeholders against this
|
|
71
|
+
* snapshot instead of the live registry, so a slow `PreToolUse`
|
|
72
|
+
* hook on one direct call cannot cause a sibling's just-registered
|
|
73
|
+
* output to leak into this call's args mid-batch (Codex P1 #18 —
|
|
74
|
+
* `Promise.all`-induced ordering would otherwise be observable).
|
|
75
|
+
*/
|
|
76
|
+
preBatchSnapshot?: ToolOutputResolveView;
|
|
77
|
+
/**
|
|
78
|
+
* Pre-incremented per-tool usage counter. Set by
|
|
79
|
+
* `runDirectToolWithLifecycleHooks` so PreToolUse hooks observe
|
|
80
|
+
* the same `turn` the tool will actually execute under (Codex P2
|
|
81
|
+
* #27 — without this, parallel direct calls of the same tool in
|
|
82
|
+
* one Promise.all batch all read `turn=N` for the hook but
|
|
83
|
+
* actually executed as `turn=N`, `N+1`, `N+2`). When supplied,
|
|
84
|
+
* `runTool` skips its own counter increment.
|
|
85
|
+
*/
|
|
86
|
+
usageCount?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Per-batch sink for `additionalContext` strings returned by
|
|
89
|
+
* direct-path PreToolUse / PostToolUse / PostToolUseFailure hooks.
|
|
90
|
+
* The caller in `run()` materializes the accumulated strings as a
|
|
91
|
+
* `HumanMessage` appended to outputs so the next model turn sees
|
|
92
|
+
* them — same shape as the event-driven path's `injected[]`.
|
|
93
|
+
* Codex P2 #39: pre-fix the direct path called `executeHooks` and
|
|
94
|
+
* discarded `additionalContexts`, silently breaking the hook API
|
|
95
|
+
* contract for hosts relying on it for policy / recovery guidance.
|
|
96
|
+
*/
|
|
97
|
+
additionalContextsSink?: string[];
|
|
63
98
|
};
|
|
64
99
|
|
|
65
100
|
/**
|
|
@@ -272,6 +307,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
272
307
|
private toolUsageCount: Map<string, number>;
|
|
273
308
|
/** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
|
|
274
309
|
private toolCallTurns: Map<string, number> = new Map();
|
|
310
|
+
/**
|
|
311
|
+
* `call.id → turn` map dedicated to the direct-path lifecycle so the
|
|
312
|
+
* turn assigned on first entry is REUSED on LangGraph resume.
|
|
313
|
+
* Distinct from `toolCallTurns` (which is cleared at the start of
|
|
314
|
+
* every `run()` to keep per-batch event-dispatch metadata fresh) —
|
|
315
|
+
* the direct path needs stability across re-entries triggered by
|
|
316
|
+
* `interrupt()` resumes (Codex P2 #30). Cleared with the rest of
|
|
317
|
+
* the per-Run state in `clearHeavyState`-equivalent flushes when
|
|
318
|
+
* the Run ends.
|
|
319
|
+
*/
|
|
320
|
+
private directPathTurns: Map<string, number> = new Map();
|
|
275
321
|
/** Tool registry for filtering (lazy computation of programmatic maps) */
|
|
276
322
|
private toolRegistry?: t.LCToolRegistry;
|
|
277
323
|
/** Cached programmatic tools (computed once on first PTC call) */
|
|
@@ -284,6 +330,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
284
330
|
private agentId?: string;
|
|
285
331
|
/** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
|
|
286
332
|
private directToolNames?: Set<string>;
|
|
333
|
+
/**
|
|
334
|
+
* File checkpointer extracted from the local coding tool bundle when
|
|
335
|
+
* `toolExecution.local.fileCheckpointing === true`. Exposed via
|
|
336
|
+
* {@link getFileCheckpointer}. Undefined when checkpointing is off
|
|
337
|
+
* or the local coding suite isn't bound to this node.
|
|
338
|
+
*/
|
|
339
|
+
private fileCheckpointer?: t.LocalFileCheckpointer;
|
|
287
340
|
/** Maximum characters allowed in a single tool result before truncation. */
|
|
288
341
|
private maxToolResultChars: number;
|
|
289
342
|
/** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
|
|
@@ -306,6 +359,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
306
359
|
* ToolNode building its own.
|
|
307
360
|
*/
|
|
308
361
|
private toolOutputRegistry?: ToolOutputReferenceRegistry;
|
|
362
|
+
/** Run-scoped selection for swapping remote code tools to local executors. */
|
|
363
|
+
private toolExecution?: t.ToolExecutionConfig;
|
|
309
364
|
/**
|
|
310
365
|
* Monotonic counter used to mint a unique scope id for anonymous
|
|
311
366
|
* batches (ones invoked without a `run_id` in
|
|
@@ -335,6 +390,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
335
390
|
humanInTheLoop,
|
|
336
391
|
toolOutputReferences,
|
|
337
392
|
toolOutputRegistry,
|
|
393
|
+
toolExecution,
|
|
394
|
+
fileCheckpointer,
|
|
338
395
|
}: t.ToolNodeConstructorParams) {
|
|
339
396
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
340
397
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
@@ -343,7 +400,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
343
400
|
this.loadRuntimeTools = loadRuntimeTools;
|
|
344
401
|
this.errorHandler = errorHandler;
|
|
345
402
|
this.toolUsageCount = new Map<string, number>();
|
|
346
|
-
this.toolRegistry = toolRegistry;
|
|
403
|
+
this.toolRegistry = resolveLocalToolRegistry({ toolRegistry, toolExecution });
|
|
347
404
|
this.sessions = sessions;
|
|
348
405
|
this.eventDrivenMode = eventDrivenMode ?? false;
|
|
349
406
|
this.agentId = agentId;
|
|
@@ -352,6 +409,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
352
409
|
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
|
|
353
410
|
this.hookRegistry = hookRegistry;
|
|
354
411
|
this.humanInTheLoop = humanInTheLoop;
|
|
412
|
+
this.toolExecution = toolExecution;
|
|
413
|
+
// Caller-provided checkpointer wins. Graphs use this to share a
|
|
414
|
+
// single per-Run instance across every ToolNode they compile so
|
|
415
|
+
// `Run.rewindFiles()` reaches the same snapshot store regardless
|
|
416
|
+
// of which agent's tool batch ran. Falls through to the bundle's
|
|
417
|
+
// auto-created one when undefined (direct ToolNode construction).
|
|
418
|
+
this.fileCheckpointer = fileCheckpointer;
|
|
419
|
+
this.applyToolExecutionOverrides();
|
|
355
420
|
/**
|
|
356
421
|
* Precedence: an explicitly passed `toolOutputRegistry` instance
|
|
357
422
|
* wins over a config object so a host (`Graph`) can share one
|
|
@@ -387,6 +452,62 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
387
452
|
return this.toolOutputRegistry;
|
|
388
453
|
}
|
|
389
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Replaces known remote Code API tools with local-process tools when
|
|
457
|
+
* `RunConfig.toolExecution.engine === 'local'`. In event-driven mode those
|
|
458
|
+
* names are also marked direct so the SDK executes them locally instead of
|
|
459
|
+
* dispatching the batch to a host-side remote sandbox handler. When the
|
|
460
|
+
* local coding suite is enabled, this also injects file/search/edit tools.
|
|
461
|
+
*/
|
|
462
|
+
private applyToolExecutionOverrides(): void {
|
|
463
|
+
const resolved = resolveLocalExecutionTools({
|
|
464
|
+
toolMap: this.toolMap,
|
|
465
|
+
toolExecution: this.toolExecution,
|
|
466
|
+
fileCheckpointer: this.fileCheckpointer,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
this.toolMap = resolved.toolMap;
|
|
470
|
+
if (resolved.fileCheckpointer != null) {
|
|
471
|
+
this.fileCheckpointer = resolved.fileCheckpointer;
|
|
472
|
+
}
|
|
473
|
+
if (resolved.directToolNames.size === 0) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this.directToolNames = new Set([
|
|
478
|
+
...(this.directToolNames ?? new Set<string>()),
|
|
479
|
+
...resolved.directToolNames,
|
|
480
|
+
]);
|
|
481
|
+
this.programmaticCache = undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Returns the per-Run file checkpointer when
|
|
486
|
+
* `toolExecution.local.fileCheckpointing === true`. Hosts call
|
|
487
|
+
* `rewind()` on the returned object to restore captured pre-write
|
|
488
|
+
* file contents — the standard "undo a tool batch" pattern. Returns
|
|
489
|
+
* undefined when checkpointing is disabled or the local coding suite
|
|
490
|
+
* isn't bound. Manual review (finding E): without this getter, the
|
|
491
|
+
* config flag was a silent no-op outside of direct
|
|
492
|
+
* `createLocalCodingToolBundle()` use.
|
|
493
|
+
*/
|
|
494
|
+
getFileCheckpointer(): t.LocalFileCheckpointer | undefined {
|
|
495
|
+
return this.fileCheckpointer;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Flush the per-Run direct-path turn cache. Called by the Graph at
|
|
500
|
+
* end-of-Run via `clearHeavyState`. The map intentionally survives
|
|
501
|
+
* `run()` re-entry so an interrupt + resume reuses the original
|
|
502
|
+
* slot (Codex P2 #30), but it would otherwise grow linearly with
|
|
503
|
+
* tool calls and could collide across Runs if a provider reused
|
|
504
|
+
* call IDs (Codex P2 #33). Hosts can also call this directly if
|
|
505
|
+
* they reuse a ToolNode across batches outside of a Graph.
|
|
506
|
+
*/
|
|
507
|
+
clearDirectPathTurns(): void {
|
|
508
|
+
this.directPathTurns.clear();
|
|
509
|
+
}
|
|
510
|
+
|
|
390
511
|
/**
|
|
391
512
|
* Returns cached programmatic tools, computing once on first access.
|
|
392
513
|
* Single iteration builds both toolMap and toolDefs simultaneously.
|
|
@@ -434,10 +555,35 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
434
555
|
config: RunnableConfig,
|
|
435
556
|
batchContext: RunToolBatchContext = {}
|
|
436
557
|
): Promise<BaseMessage | Command> {
|
|
437
|
-
const {
|
|
438
|
-
|
|
558
|
+
const {
|
|
559
|
+
batchIndex,
|
|
560
|
+
turn,
|
|
561
|
+
batchScopeId,
|
|
562
|
+
resolvedArgsByCallId,
|
|
563
|
+
preBatchSnapshot,
|
|
564
|
+
} = batchContext;
|
|
439
565
|
const tool = this.toolMap.get(call.name);
|
|
440
566
|
const registry = this.toolOutputRegistry;
|
|
567
|
+
/**
|
|
568
|
+
* Prefer the caller-provided snapshot when present — `run()`
|
|
569
|
+
* captures one synchronously per batch so direct-path placeholder
|
|
570
|
+
* resolution stays isolated from same-turn sibling outputs even
|
|
571
|
+
* when a slow `PreToolUse` hook lets siblings finish first.
|
|
572
|
+
* Falls back to the live registry for callers that didn't thread
|
|
573
|
+
* a snapshot (anonymous direct invokes, legacy paths).
|
|
574
|
+
*/
|
|
575
|
+
type ResolveFn = <T>(
|
|
576
|
+
runIdArg: string | undefined,
|
|
577
|
+
args: T
|
|
578
|
+
) => ResolveResult<T>;
|
|
579
|
+
let resolveFn: ResolveFn | undefined;
|
|
580
|
+
if (preBatchSnapshot != null) {
|
|
581
|
+
resolveFn = <T>(_runId: string | undefined, args: T): ResolveResult<T> =>
|
|
582
|
+
preBatchSnapshot.resolve(args);
|
|
583
|
+
} else if (registry != null) {
|
|
584
|
+
resolveFn = <T>(runIdArg: string | undefined, args: T): ResolveResult<T> =>
|
|
585
|
+
registry.resolve(runIdArg, args);
|
|
586
|
+
}
|
|
441
587
|
/**
|
|
442
588
|
* Precompute the reference key once per call — captured locally
|
|
443
589
|
* so concurrent `invoke()` calls on the same ToolNode cannot race
|
|
@@ -473,15 +619,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
473
619
|
* It is intentionally distinct from the outer `turn` parameter
|
|
474
620
|
* (the batch turn used for ref keys); the latter is captured
|
|
475
621
|
* before the try block when constructing `refKey`.
|
|
622
|
+
*
|
|
623
|
+
* Prefer the value `runDirectToolWithLifecycleHooks` already
|
|
624
|
+
* incremented (Codex P2 #27) — its hook wants the SAME turn
|
|
625
|
+
* the tool will execute under. When called from a path that
|
|
626
|
+
* doesn't pre-increment (event dispatch, the no-hooks
|
|
627
|
+
* shortcut), do the read+increment here.
|
|
476
628
|
*/
|
|
477
|
-
const usageCount =
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
629
|
+
const usageCount =
|
|
630
|
+
batchContext.usageCount ??
|
|
631
|
+
((): number => {
|
|
632
|
+
const next = this.toolUsageCount.get(call.name) ?? 0;
|
|
633
|
+
this.toolUsageCount.set(call.name, next + 1);
|
|
634
|
+
if (call.id != null && call.id !== '') {
|
|
635
|
+
this.toolCallTurns.set(call.id, next);
|
|
636
|
+
}
|
|
637
|
+
return next;
|
|
638
|
+
})();
|
|
482
639
|
let args = call.args;
|
|
483
|
-
if (
|
|
484
|
-
const { resolved, unresolved } =
|
|
640
|
+
if (resolveFn != null) {
|
|
641
|
+
const { resolved, unresolved } = resolveFn(runId, args);
|
|
485
642
|
args = resolved;
|
|
486
643
|
unresolvedRefs = unresolved;
|
|
487
644
|
/**
|
|
@@ -526,6 +683,19 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
526
683
|
...invokeParams,
|
|
527
684
|
toolMap,
|
|
528
685
|
toolDefs,
|
|
686
|
+
// Plumb the hook context into the programmatic-tool path so
|
|
687
|
+
// inner tool calls made via the in-process bridge can run
|
|
688
|
+
// through `PreToolUse` (deny / updatedInput) before reaching
|
|
689
|
+
// the underlying tool. Without this, `run_tools_with_code`
|
|
690
|
+
// bypassed every PreToolUse hook the host registered for
|
|
691
|
+
// the tools it dispatches — including HITL gates on
|
|
692
|
+
// `write_file` / `edit_file` (manual review finding A).
|
|
693
|
+
hookContext: {
|
|
694
|
+
registry: this.hookRegistry,
|
|
695
|
+
runId: (config.configurable?.run_id as string | undefined) ?? '',
|
|
696
|
+
threadId: config.configurable?.thread_id as string | undefined,
|
|
697
|
+
agentId: this.agentId,
|
|
698
|
+
},
|
|
529
699
|
};
|
|
530
700
|
} else if (call.name === Constants.TOOL_SEARCH) {
|
|
531
701
|
invokeParams = {
|
|
@@ -558,6 +728,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
558
728
|
session_id: file.session_id ?? codeSession.session_id,
|
|
559
729
|
id: file.id,
|
|
560
730
|
name: file.name,
|
|
731
|
+
...(file.entity_id != null ? { entity_id: file.entity_id } : {}),
|
|
561
732
|
}));
|
|
562
733
|
invokeParams._injected_files = fileRefs;
|
|
563
734
|
}
|
|
@@ -725,6 +896,511 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
725
896
|
}
|
|
726
897
|
}
|
|
727
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Runs a single in-process tool call with the same lifecycle hooks
|
|
901
|
+
* the event-dispatch path fires (`PreToolUse`, `PermissionDenied`,
|
|
902
|
+
* `PostToolUse`, `PostToolUseFailure`). Used for any tool whose
|
|
903
|
+
* implementation lives in the SDK process — i.e. every entry in
|
|
904
|
+
* `directToolNames` — so host-supplied policy hooks gate
|
|
905
|
+
* direct-invoked tools the same way they gate dispatched ones.
|
|
906
|
+
*
|
|
907
|
+
* Fast path: when the registry has none of the relevant events
|
|
908
|
+
* registered for this run, falls through to `runTool` with zero
|
|
909
|
+
* extra work. The hook list is also checked via
|
|
910
|
+
* `hasHookFor(event, runId)`, which performs the registry's own
|
|
911
|
+
* O(1) shortcut.
|
|
912
|
+
*
|
|
913
|
+
* Hook semantics intentionally mirror `dispatchToolEvents` for the
|
|
914
|
+
* single-call case:
|
|
915
|
+
* - `PreToolUse` returning `decision: 'deny'` synthesizes an error
|
|
916
|
+
* `ToolMessage` and fires `PermissionDenied` (observational).
|
|
917
|
+
* - `PreToolUse` returning `decision: 'ask'`:
|
|
918
|
+
* • When `humanInTheLoop.enabled === true`: raises a real
|
|
919
|
+
* `tool_approval` interrupt for this single tool call (the
|
|
920
|
+
* same payload shape the event path produces). On resume:
|
|
921
|
+
* `approve` runs the tool, `reject` blocks via
|
|
922
|
+
* `blockDirectCall`, `respond` returns the host-supplied
|
|
923
|
+
* `responseText` as a synthetic success ToolMessage,
|
|
924
|
+
* `edit` re-runs with edited args. LangGraph re-enters
|
|
925
|
+
* ToolNode.run from the start on resume; the hook fires
|
|
926
|
+
* again and the resume value distinguishes "first ask" from
|
|
927
|
+
* "second pass with decision".
|
|
928
|
+
* • When HITL is off: collapses to a fail-closed deny (matches
|
|
929
|
+
* the rest of the SDK's HITL-disabled default). One-time
|
|
930
|
+
* warning logged so hosts notice the gap.
|
|
931
|
+
* - `PreToolUse.updatedInput` is applied to the call before
|
|
932
|
+
* `runTool` runs; placeholder resolution inside `runTool` is
|
|
933
|
+
* idempotent on already-resolved args.
|
|
934
|
+
* - `PostToolUse.updatedOutput` replaces the returned
|
|
935
|
+
* `ToolMessage` content (preserving id/name/status).
|
|
936
|
+
* - `PostToolUseFailure` fires when `runTool` returns a
|
|
937
|
+
* `ToolMessage` whose `status === 'error'`. Observational only;
|
|
938
|
+
* the error message stays the source of truth.
|
|
939
|
+
*
|
|
940
|
+
* `PostToolBatch` aggregation across direct + dispatched outcomes is
|
|
941
|
+
* a separate concern: `dispatchToolEvents` accumulates batch entries
|
|
942
|
+
* locally and fires `PostToolBatch` at the end of its scope. Wiring
|
|
943
|
+
* direct-call entries into that aggregation crosses the two paths'
|
|
944
|
+
* scopes and is left to a follow-up.
|
|
945
|
+
*/
|
|
946
|
+
private async runDirectToolWithLifecycleHooks(
|
|
947
|
+
call: ToolCall,
|
|
948
|
+
config: RunnableConfig,
|
|
949
|
+
batchContext: RunToolBatchContext = {}
|
|
950
|
+
): Promise<BaseMessage | Command> {
|
|
951
|
+
const runId = (config.configurable?.run_id as string | undefined) ?? '';
|
|
952
|
+
const hookRegistry = this.hookRegistry;
|
|
953
|
+
const hasPreHook =
|
|
954
|
+
hookRegistry?.hasHookFor('PreToolUse', runId) === true;
|
|
955
|
+
const hasPostHook =
|
|
956
|
+
hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
957
|
+
const hasFailureHook =
|
|
958
|
+
hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
959
|
+
|
|
960
|
+
if (
|
|
961
|
+
hookRegistry == null ||
|
|
962
|
+
(!hasPreHook && !hasPostHook && !hasFailureHook)
|
|
963
|
+
) {
|
|
964
|
+
return this.runTool(call, config, batchContext);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const threadId = config.configurable?.thread_id as string | undefined;
|
|
968
|
+
const registryRunId =
|
|
969
|
+
batchContext.batchScopeId ??
|
|
970
|
+
(config.configurable?.run_id as string | undefined);
|
|
971
|
+
// Slot reservation, synchronous, before any await:
|
|
972
|
+
// 1. If this call.id already has a recorded turn (from a prior
|
|
973
|
+
// entry that asked / interrupted), REUSE it. LangGraph
|
|
974
|
+
// re-runs the entire ToolNode on resume, so the same call
|
|
975
|
+
// can hit this code multiple times — incrementing on each
|
|
976
|
+
// pass would push the eventual approved execution to
|
|
977
|
+
// `turn=N` instead of `turn=0` (Codex P2 #30: the fix from
|
|
978
|
+
// P2 #27 over-incremented across re-entries).
|
|
979
|
+
// 2. Otherwise reserve the next slot from the counter. Done
|
|
980
|
+
// synchronously so concurrent same-tool calls in a single
|
|
981
|
+
// Promise.all batch get distinct turns (the original P2 #27
|
|
982
|
+
// requirement still holds).
|
|
983
|
+
// Net: turns are stable per call.id across interrupt/resume,
|
|
984
|
+
// unique per call within a batch.
|
|
985
|
+
let usageCount: number;
|
|
986
|
+
// Look in the resume-stable map first; fall back to the
|
|
987
|
+
// per-batch one. (`directPathTurns` is set on first entry and
|
|
988
|
+
// survives `run()`'s clear, so a resume sees the original
|
|
989
|
+
// assignment.)
|
|
990
|
+
const cachedTurn =
|
|
991
|
+
call.id != null && call.id !== ''
|
|
992
|
+
? this.directPathTurns.get(call.id) ??
|
|
993
|
+
this.toolCallTurns.get(call.id)
|
|
994
|
+
: undefined;
|
|
995
|
+
if (cachedTurn != null) {
|
|
996
|
+
usageCount = cachedTurn;
|
|
997
|
+
} else {
|
|
998
|
+
usageCount = this.toolUsageCount.get(call.name) ?? 0;
|
|
999
|
+
this.toolUsageCount.set(call.name, usageCount + 1);
|
|
1000
|
+
if (call.id != null && call.id !== '') {
|
|
1001
|
+
this.toolCallTurns.set(call.id, usageCount);
|
|
1002
|
+
// Dedicated direct-path map that SURVIVES `run()`'s
|
|
1003
|
+
// toolCallTurns.clear() — so a re-entry triggered by
|
|
1004
|
+
// LangGraph interrupt resume reuses this slot instead of
|
|
1005
|
+
// re-incrementing. Codex P2 #30.
|
|
1006
|
+
this.directPathTurns.set(call.id, usageCount);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const turn = usageCount;
|
|
1010
|
+
const stepId = this.toolCallStepIds?.get(call.id ?? '') ?? '';
|
|
1011
|
+
|
|
1012
|
+
// Use the caller-threaded snapshot when available (P1 #18) so the
|
|
1013
|
+
// value the PreToolUse hook observes matches the value the
|
|
1014
|
+
// (later-awaited) `runTool` will actually run with — both are
|
|
1015
|
+
// anchored to the pre-batch registry state.
|
|
1016
|
+
let resolvedArgs = call.args as Record<string, unknown>;
|
|
1017
|
+
if (batchContext.preBatchSnapshot != null) {
|
|
1018
|
+
const { resolved } = batchContext.preBatchSnapshot.resolve(call.args);
|
|
1019
|
+
resolvedArgs = resolved as Record<string, unknown>;
|
|
1020
|
+
} else if (this.toolOutputRegistry != null) {
|
|
1021
|
+
const { resolved } = this.toolOutputRegistry.resolve(
|
|
1022
|
+
registryRunId,
|
|
1023
|
+
call.args
|
|
1024
|
+
);
|
|
1025
|
+
resolvedArgs = resolved as Record<string, unknown>;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
let effectiveCall = call;
|
|
1029
|
+
if (hasPreHook) {
|
|
1030
|
+
const preResult = await executeHooks({
|
|
1031
|
+
registry: hookRegistry,
|
|
1032
|
+
input: {
|
|
1033
|
+
hook_event_name: 'PreToolUse',
|
|
1034
|
+
runId,
|
|
1035
|
+
threadId,
|
|
1036
|
+
agentId: this.agentId,
|
|
1037
|
+
toolName: call.name,
|
|
1038
|
+
toolInput: resolvedArgs,
|
|
1039
|
+
toolUseId: call.id ?? '',
|
|
1040
|
+
stepId,
|
|
1041
|
+
turn,
|
|
1042
|
+
},
|
|
1043
|
+
sessionId: runId,
|
|
1044
|
+
matchQuery: call.name,
|
|
1045
|
+
}).catch(() => undefined);
|
|
1046
|
+
|
|
1047
|
+
if (preResult != null) {
|
|
1048
|
+
// Forward any additionalContext strings hooks returned into
|
|
1049
|
+
// the per-batch sink so the caller materializes them as a
|
|
1050
|
+
// HumanMessage for the next model turn — same shape as the
|
|
1051
|
+
// event-driven path's `injected[]`. Codex P2 #39.
|
|
1052
|
+
if (
|
|
1053
|
+
batchContext.additionalContextsSink != null &&
|
|
1054
|
+
preResult.additionalContexts.length > 0
|
|
1055
|
+
) {
|
|
1056
|
+
batchContext.additionalContextsSink.push(
|
|
1057
|
+
...preResult.additionalContexts
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
// Apply any input rewrite first — `ask`-with-`updatedInput` is
|
|
1061
|
+
// a valid combination (one matcher sanitises args, another asks
|
|
1062
|
+
// for approval); the reviewer should see the sanitised args.
|
|
1063
|
+
if (preResult.updatedInput != null) {
|
|
1064
|
+
effectiveCall = {
|
|
1065
|
+
...call,
|
|
1066
|
+
args: preResult.updatedInput as Record<string, unknown>,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (preResult.decision === 'deny') {
|
|
1071
|
+
return this.blockDirectCall({
|
|
1072
|
+
call,
|
|
1073
|
+
resolvedArgs,
|
|
1074
|
+
reason: preResult.reason ?? 'Blocked by hook',
|
|
1075
|
+
hookRegistry,
|
|
1076
|
+
runId,
|
|
1077
|
+
threadId,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (preResult.decision === 'ask') {
|
|
1082
|
+
if (this.humanInTheLoop?.enabled !== true) {
|
|
1083
|
+
// Fail-closed: no HITL UI configured, so we can't actually
|
|
1084
|
+
// ask. Logged once via the existing helper.
|
|
1085
|
+
const reason = this.resolveAskDecisionForDirectTool(
|
|
1086
|
+
preResult.reason,
|
|
1087
|
+
call.name
|
|
1088
|
+
);
|
|
1089
|
+
return this.blockDirectCall({
|
|
1090
|
+
call,
|
|
1091
|
+
resolvedArgs,
|
|
1092
|
+
reason,
|
|
1093
|
+
hookRegistry,
|
|
1094
|
+
runId,
|
|
1095
|
+
threadId,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Raise a single-tool tool_approval interrupt. LangGraph
|
|
1100
|
+
// throws on the first execution (host gets the interrupt)
|
|
1101
|
+
// and returns the resume value on re-entry. Because direct
|
|
1102
|
+
// tools re-enter the entire ToolNode.run on resume, the
|
|
1103
|
+
// PreToolUse hook fires AGAIN — which is fine: the hook is
|
|
1104
|
+
// expected to be deterministic, and the resume value is what
|
|
1105
|
+
// distinguishes "first call asking" from "second call after
|
|
1106
|
+
// approve/reject". We anchor `interrupt()` against the
|
|
1107
|
+
// node's RunnableConfig the same way `dispatchToolEvents`
|
|
1108
|
+
// does (ToolNode disables LangSmith tracing, so the
|
|
1109
|
+
// AsyncLocalStorage frame must be re-established here).
|
|
1110
|
+
const askEntry: AskEntry = {
|
|
1111
|
+
entry: {
|
|
1112
|
+
call: effectiveCall,
|
|
1113
|
+
args: effectiveCall.args as Record<string, unknown>,
|
|
1114
|
+
stepId,
|
|
1115
|
+
},
|
|
1116
|
+
reason: preResult.reason,
|
|
1117
|
+
allowedDecisions: preResult.allowedDecisions,
|
|
1118
|
+
};
|
|
1119
|
+
const payload = buildToolApprovalInterruptPayload([askEntry]);
|
|
1120
|
+
const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
1121
|
+
config,
|
|
1122
|
+
() =>
|
|
1123
|
+
interrupt<
|
|
1124
|
+
t.ToolApprovalInterruptPayload,
|
|
1125
|
+
t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap
|
|
1126
|
+
>(payload)
|
|
1127
|
+
);
|
|
1128
|
+
const decisionByCallId = normalizeApprovalDecisions(
|
|
1129
|
+
[call.id!],
|
|
1130
|
+
resumeValue
|
|
1131
|
+
);
|
|
1132
|
+
const decision = decisionByCallId.get(call.id!) ?? {
|
|
1133
|
+
type: 'reject' as const,
|
|
1134
|
+
reason: 'No decision provided for tool approval',
|
|
1135
|
+
};
|
|
1136
|
+
const declaredType = (decision as { type?: unknown }).type;
|
|
1137
|
+
|
|
1138
|
+
if (
|
|
1139
|
+
preResult.allowedDecisions != null &&
|
|
1140
|
+
(typeof declaredType !== 'string' ||
|
|
1141
|
+
!preResult.allowedDecisions.includes(
|
|
1142
|
+
declaredType as t.ToolApprovalDecisionType
|
|
1143
|
+
))
|
|
1144
|
+
) {
|
|
1145
|
+
return this.blockDirectCall({
|
|
1146
|
+
call,
|
|
1147
|
+
resolvedArgs,
|
|
1148
|
+
reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${preResult.allowedDecisions.join(', ')}] — failing closed`,
|
|
1149
|
+
hookRegistry,
|
|
1150
|
+
runId,
|
|
1151
|
+
threadId,
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (decision.type === 'reject') {
|
|
1156
|
+
return this.blockDirectCall({
|
|
1157
|
+
call,
|
|
1158
|
+
resolvedArgs,
|
|
1159
|
+
reason:
|
|
1160
|
+
decision.reason ??
|
|
1161
|
+
preResult.reason ??
|
|
1162
|
+
'Rejected by user',
|
|
1163
|
+
hookRegistry,
|
|
1164
|
+
runId,
|
|
1165
|
+
threadId,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (decision.type === 'respond') {
|
|
1170
|
+
const responseText = (decision as { responseText?: unknown })
|
|
1171
|
+
.responseText;
|
|
1172
|
+
if (typeof responseText !== 'string') {
|
|
1173
|
+
return this.blockDirectCall({
|
|
1174
|
+
call,
|
|
1175
|
+
resolvedArgs,
|
|
1176
|
+
reason: 'Approval payload `respond` was missing a string `responseText`',
|
|
1177
|
+
hookRegistry,
|
|
1178
|
+
runId,
|
|
1179
|
+
threadId,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return new ToolMessage({
|
|
1183
|
+
status: 'success',
|
|
1184
|
+
content: responseText,
|
|
1185
|
+
name: call.name,
|
|
1186
|
+
tool_call_id: call.id ?? '',
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (decision.type === 'edit') {
|
|
1191
|
+
// Mirror the event-driven path's validation
|
|
1192
|
+
// (see `dispatchToolEvents`'s edit branch). The wire
|
|
1193
|
+
// field is `updatedInput`, NOT `args` — hosts following
|
|
1194
|
+
// the documented `ToolApprovalDecision` shape were
|
|
1195
|
+
// silently ignored before, so the tool ran with the
|
|
1196
|
+
// original (un-edited) arguments. Fail closed on
|
|
1197
|
+
// malformed payloads instead of falling through with
|
|
1198
|
+
// undefined args.
|
|
1199
|
+
const updatedInput = (decision as { updatedInput?: unknown })
|
|
1200
|
+
.updatedInput;
|
|
1201
|
+
if (
|
|
1202
|
+
updatedInput === null ||
|
|
1203
|
+
typeof updatedInput !== 'object' ||
|
|
1204
|
+
Array.isArray(updatedInput)
|
|
1205
|
+
) {
|
|
1206
|
+
return new ToolMessage({
|
|
1207
|
+
status: 'error',
|
|
1208
|
+
content:
|
|
1209
|
+
'Decision "edit" missing object updatedInput — failing closed.',
|
|
1210
|
+
name: call.name,
|
|
1211
|
+
tool_call_id: call.id ?? '',
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
effectiveCall = {
|
|
1215
|
+
...call,
|
|
1216
|
+
args: updatedInput as Record<string, unknown>,
|
|
1217
|
+
};
|
|
1218
|
+
// fall through to executing the edited call
|
|
1219
|
+
}
|
|
1220
|
+
// 'approve' (or 'edit' after applying edits) → fall through
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const output = await this.runTool(effectiveCall, config, {
|
|
1226
|
+
...batchContext,
|
|
1227
|
+
usageCount,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
if (!(output instanceof ToolMessage)) {
|
|
1231
|
+
return output;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (output.status === 'error' && hasFailureHook) {
|
|
1235
|
+
// Await the failure hook (instead of fire-and-forget) so we
|
|
1236
|
+
// can capture additionalContexts before returning. The hook is
|
|
1237
|
+
// still observational w.r.t. the tool result itself — we don't
|
|
1238
|
+
// mutate `output`, just plumb the contexts. Codex P2 #39.
|
|
1239
|
+
const failureResult = await executeHooks({
|
|
1240
|
+
registry: hookRegistry,
|
|
1241
|
+
input: {
|
|
1242
|
+
hook_event_name: 'PostToolUseFailure',
|
|
1243
|
+
runId,
|
|
1244
|
+
threadId,
|
|
1245
|
+
agentId: this.agentId,
|
|
1246
|
+
toolName: call.name,
|
|
1247
|
+
toolInput: effectiveCall.args as Record<string, unknown>,
|
|
1248
|
+
toolUseId: call.id ?? '',
|
|
1249
|
+
error:
|
|
1250
|
+
typeof output.content === 'string'
|
|
1251
|
+
? output.content
|
|
1252
|
+
: JSON.stringify(output.content),
|
|
1253
|
+
stepId,
|
|
1254
|
+
turn,
|
|
1255
|
+
},
|
|
1256
|
+
sessionId: runId,
|
|
1257
|
+
matchQuery: call.name,
|
|
1258
|
+
}).catch(() => undefined);
|
|
1259
|
+
if (
|
|
1260
|
+
failureResult != null &&
|
|
1261
|
+
batchContext.additionalContextsSink != null &&
|
|
1262
|
+
failureResult.additionalContexts.length > 0
|
|
1263
|
+
) {
|
|
1264
|
+
batchContext.additionalContextsSink.push(
|
|
1265
|
+
...failureResult.additionalContexts
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
return output;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (output.status !== 'error' && hasPostHook) {
|
|
1272
|
+
const postResult = await executeHooks({
|
|
1273
|
+
registry: hookRegistry,
|
|
1274
|
+
input: {
|
|
1275
|
+
hook_event_name: 'PostToolUse',
|
|
1276
|
+
runId,
|
|
1277
|
+
threadId,
|
|
1278
|
+
agentId: this.agentId,
|
|
1279
|
+
toolName: call.name,
|
|
1280
|
+
toolInput: effectiveCall.args as Record<string, unknown>,
|
|
1281
|
+
toolOutput: output.content,
|
|
1282
|
+
toolUseId: call.id ?? '',
|
|
1283
|
+
stepId,
|
|
1284
|
+
turn,
|
|
1285
|
+
},
|
|
1286
|
+
sessionId: runId,
|
|
1287
|
+
matchQuery: call.name,
|
|
1288
|
+
}).catch(() => undefined);
|
|
1289
|
+
|
|
1290
|
+
// Forward additionalContexts from the PostToolUse hook into
|
|
1291
|
+
// the per-batch sink (Codex P2 #39).
|
|
1292
|
+
if (
|
|
1293
|
+
postResult != null &&
|
|
1294
|
+
batchContext.additionalContextsSink != null &&
|
|
1295
|
+
postResult.additionalContexts.length > 0
|
|
1296
|
+
) {
|
|
1297
|
+
batchContext.additionalContextsSink.push(
|
|
1298
|
+
...postResult.additionalContexts
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (postResult?.updatedOutput != null) {
|
|
1303
|
+
const replaced =
|
|
1304
|
+
typeof postResult.updatedOutput === 'string'
|
|
1305
|
+
? postResult.updatedOutput
|
|
1306
|
+
: JSON.stringify(postResult.updatedOutput);
|
|
1307
|
+
// Keep the tool-output registry in sync with what the model
|
|
1308
|
+
// actually sees. Without this, `runTool` already registered
|
|
1309
|
+
// the PRE-hook content under `_refKey`, and a later
|
|
1310
|
+
// `{{tool<i>turn<n>}}` substitution would deliver the stale
|
|
1311
|
+
// pre-hook bytes while the model (and downstream tools)
|
|
1312
|
+
// observed the post-hook replacement. Read `_refKey` /
|
|
1313
|
+
// `_refScope` straight off the message metadata that
|
|
1314
|
+
// `recordOutputReference` stamped — no need to re-derive
|
|
1315
|
+
// (and we couldn't, for anonymous-batch synthetic scopes).
|
|
1316
|
+
const refMeta = output.additional_kwargs as
|
|
1317
|
+
| t.ToolMessageRefMetadata
|
|
1318
|
+
| undefined;
|
|
1319
|
+
const refKey = refMeta?._refKey;
|
|
1320
|
+
const refScope = refMeta?._refScope;
|
|
1321
|
+
if (this.toolOutputRegistry != null && refKey != null) {
|
|
1322
|
+
this.toolOutputRegistry.set(refScope, refKey, replaced);
|
|
1323
|
+
}
|
|
1324
|
+
return new ToolMessage({
|
|
1325
|
+
status: output.status,
|
|
1326
|
+
name: output.name,
|
|
1327
|
+
content: replaced,
|
|
1328
|
+
artifact: output.artifact,
|
|
1329
|
+
tool_call_id: output.tool_call_id,
|
|
1330
|
+
additional_kwargs: output.additional_kwargs,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return output;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* `ask` decisions on direct-path tools collapse to fail-closed deny
|
|
1340
|
+
* only when `humanInTheLoop.enabled !== true` (i.e. there's no host
|
|
1341
|
+
* UI configured to actually prompt the user). Logged once per process
|
|
1342
|
+
* so the gap is visible. When HITL IS enabled, `ask` raises a real
|
|
1343
|
+
* LangGraph `interrupt()` instead — see `runDirectToolWithLifecycleHooks`.
|
|
1344
|
+
*/
|
|
1345
|
+
private askDirectWarningEmitted = false;
|
|
1346
|
+
private resolveAskDecisionForDirectTool(
|
|
1347
|
+
reason: string | undefined,
|
|
1348
|
+
toolName: string
|
|
1349
|
+
): string {
|
|
1350
|
+
if (!this.askDirectWarningEmitted) {
|
|
1351
|
+
this.askDirectWarningEmitted = true;
|
|
1352
|
+
// eslint-disable-next-line no-console
|
|
1353
|
+
console.warn(
|
|
1354
|
+
`[ToolNode] PreToolUse returned 'ask' for direct-path tool "${toolName}" but ` +
|
|
1355
|
+
'humanInTheLoop is not enabled — failing closed. Set humanInTheLoop.enabled=true ' +
|
|
1356
|
+
'to raise a tool_approval interrupt the host can resolve.'
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return reason ?? 'Blocked by hook';
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Synthesize a Blocked ToolMessage AND fire `PermissionDenied`
|
|
1364
|
+
* (observational) for a direct-path tool call. Centralised so the
|
|
1365
|
+
* deny path looks identical whether the block came from `'deny'` or
|
|
1366
|
+
* from a fail-closed/`'reject'`/policy-violation path.
|
|
1367
|
+
*/
|
|
1368
|
+
private blockDirectCall(args: {
|
|
1369
|
+
call: ToolCall;
|
|
1370
|
+
resolvedArgs: Record<string, unknown>;
|
|
1371
|
+
reason: string;
|
|
1372
|
+
hookRegistry: HookRegistry;
|
|
1373
|
+
runId: string;
|
|
1374
|
+
threadId: string | undefined;
|
|
1375
|
+
}): ToolMessage {
|
|
1376
|
+
const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
|
|
1377
|
+
if (hookRegistry.hasHookFor('PermissionDenied', runId) === true) {
|
|
1378
|
+
executeHooks({
|
|
1379
|
+
registry: hookRegistry,
|
|
1380
|
+
input: {
|
|
1381
|
+
hook_event_name: 'PermissionDenied',
|
|
1382
|
+
runId,
|
|
1383
|
+
threadId,
|
|
1384
|
+
agentId: this.agentId,
|
|
1385
|
+
toolName: call.name,
|
|
1386
|
+
toolInput: resolvedArgs,
|
|
1387
|
+
toolUseId: call.id ?? '',
|
|
1388
|
+
reason,
|
|
1389
|
+
},
|
|
1390
|
+
sessionId: runId,
|
|
1391
|
+
matchQuery: call.name,
|
|
1392
|
+
}).catch(() => {
|
|
1393
|
+
/* observational */
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
return new ToolMessage({
|
|
1397
|
+
status: 'error',
|
|
1398
|
+
content: `Blocked: ${reason}`,
|
|
1399
|
+
name: call.name,
|
|
1400
|
+
tool_call_id: call.id ?? '',
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
728
1404
|
/**
|
|
729
1405
|
* Registers the full, raw output under `refKey` (when provided) and
|
|
730
1406
|
* builds the per-message ref metadata stamped onto the resulting
|
|
@@ -797,6 +1473,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
797
1473
|
session_id: file.session_id ?? codeSession.session_id,
|
|
798
1474
|
id: file.id,
|
|
799
1475
|
name: file.name,
|
|
1476
|
+
...(file.entity_id != null ? { entity_id: file.entity_id } : {}),
|
|
800
1477
|
}));
|
|
801
1478
|
}
|
|
802
1479
|
|
|
@@ -823,7 +1500,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
823
1500
|
|
|
824
1501
|
const request = requestMap.get(result.toolCallId);
|
|
825
1502
|
if (
|
|
826
|
-
|
|
1503
|
+
request?.name == null ||
|
|
1504
|
+
request.name === '' ||
|
|
827
1505
|
(!CODE_EXECUTION_TOOLS.has(request.name) &&
|
|
828
1506
|
request.name !== Constants.SKILL_TOOL)
|
|
829
1507
|
) {
|
|
@@ -1992,17 +2670,42 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1992
2670
|
batchScopeId,
|
|
1993
2671
|
});
|
|
1994
2672
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
2673
|
+
// Same per-batch sink the message-state branches use so
|
|
2674
|
+
// direct-path PreToolUse/PostToolUse/Failure additionalContexts
|
|
2675
|
+
// surface here too. Codex P2 [44] — round 14 added the sink to
|
|
2676
|
+
// both message-state branches but missed this Send-input
|
|
2677
|
+
// branch, so direct tools dispatched via Send (a supported
|
|
2678
|
+
// input shape) still silently dropped hook context.
|
|
2679
|
+
const directAdditionalContexts: string[] = [];
|
|
2680
|
+
const sendOutput = await this.runDirectToolWithLifecycleHooks(
|
|
2681
|
+
input.lg_tool_call,
|
|
2682
|
+
config,
|
|
2683
|
+
{
|
|
1997
2684
|
batchIndex: 0,
|
|
1998
2685
|
turn,
|
|
1999
2686
|
batchScopeId,
|
|
2000
2687
|
resolvedArgsByCallId,
|
|
2001
|
-
|
|
2002
|
-
|
|
2688
|
+
additionalContextsSink: directAdditionalContexts,
|
|
2689
|
+
}
|
|
2690
|
+
);
|
|
2691
|
+
outputs =
|
|
2692
|
+
directAdditionalContexts.length > 0
|
|
2693
|
+
? [
|
|
2694
|
+
sendOutput,
|
|
2695
|
+
new HumanMessage({
|
|
2696
|
+
content: directAdditionalContexts.join('\n\n'),
|
|
2697
|
+
// Match the event-driven path's marker so hosts /
|
|
2698
|
+
// model-side annotators treat this as system intent
|
|
2699
|
+
// rather than ordinary user text. Codex P2 [46].
|
|
2700
|
+
additional_kwargs: { role: 'system', source: 'hook' },
|
|
2701
|
+
}),
|
|
2702
|
+
]
|
|
2703
|
+
: [sendOutput];
|
|
2003
2704
|
this.handleRunToolCompletions(
|
|
2004
2705
|
[input.lg_tool_call],
|
|
2005
|
-
|
|
2706
|
+
// Pass only the tool output to completion handling; the
|
|
2707
|
+
// HumanMessage isn't a tool result.
|
|
2708
|
+
[sendOutput],
|
|
2006
2709
|
config,
|
|
2007
2710
|
resolvedArgsByCallId
|
|
2008
2711
|
);
|
|
@@ -2043,6 +2746,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2043
2746
|
);
|
|
2044
2747
|
this.toolMap =
|
|
2045
2748
|
toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
2749
|
+
this.applyToolExecutionOverrides();
|
|
2046
2750
|
this.programmaticCache = undefined; // Invalidate cache on toolMap change
|
|
2047
2751
|
}
|
|
2048
2752
|
|
|
@@ -2131,15 +2835,22 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2131
2835
|
}
|
|
2132
2836
|
}
|
|
2133
2837
|
|
|
2838
|
+
// Per-batch sink for direct-path hook additionalContexts
|
|
2839
|
+
// (Codex P2 #39). Materialized as a HumanMessage at end-of-
|
|
2840
|
+
// batch so the next model turn sees the injected context,
|
|
2841
|
+
// matching the event path's `injected[]` shape.
|
|
2842
|
+
const directAdditionalContexts: string[] = [];
|
|
2134
2843
|
const directOutputs: (BaseMessage | Command)[] =
|
|
2135
2844
|
directCalls.length > 0
|
|
2136
2845
|
? await Promise.all(
|
|
2137
2846
|
directCalls.map((call, i) =>
|
|
2138
|
-
this.
|
|
2847
|
+
this.runDirectToolWithLifecycleHooks(call, config, {
|
|
2139
2848
|
batchIndex: directIndices[i],
|
|
2140
2849
|
turn,
|
|
2141
2850
|
batchScopeId,
|
|
2142
2851
|
resolvedArgsByCallId,
|
|
2852
|
+
preBatchSnapshot,
|
|
2853
|
+
additionalContextsSink: directAdditionalContexts,
|
|
2143
2854
|
})
|
|
2144
2855
|
)
|
|
2145
2856
|
)
|
|
@@ -2168,28 +2879,66 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2168
2879
|
injected: [] as BaseMessage[],
|
|
2169
2880
|
};
|
|
2170
2881
|
|
|
2882
|
+
const directInjected: BaseMessage[] =
|
|
2883
|
+
directAdditionalContexts.length > 0
|
|
2884
|
+
? [
|
|
2885
|
+
new HumanMessage({
|
|
2886
|
+
content: directAdditionalContexts.join('\n\n'),
|
|
2887
|
+
// System-role metadata to match the event-driven
|
|
2888
|
+
// path so policy/recovery guidance is treated
|
|
2889
|
+
// consistently regardless of whether the tool ran
|
|
2890
|
+
// direct or dispatched. Codex P2 [46].
|
|
2891
|
+
additional_kwargs: { role: 'system', source: 'hook' },
|
|
2892
|
+
}),
|
|
2893
|
+
]
|
|
2894
|
+
: [];
|
|
2171
2895
|
outputs = [
|
|
2172
2896
|
...directOutputs,
|
|
2173
2897
|
...eventResult.toolMessages,
|
|
2898
|
+
...directInjected,
|
|
2174
2899
|
...eventResult.injected,
|
|
2175
2900
|
];
|
|
2176
2901
|
} else {
|
|
2177
|
-
|
|
2902
|
+
// Same per-batch pre-snapshot as the mixed path, applied to
|
|
2903
|
+
// the all-direct case so `Promise.all`-induced ordering can't
|
|
2904
|
+
// leak a sibling's just-registered output into a sister
|
|
2905
|
+
// call's args mid-await (Codex P1 #18).
|
|
2906
|
+
const preBatchSnapshot =
|
|
2907
|
+
this.toolOutputRegistry?.snapshot(batchScopeId);
|
|
2908
|
+
const directAdditionalContexts: string[] = [];
|
|
2909
|
+
const toolOutputs = await Promise.all(
|
|
2178
2910
|
filteredCalls.map((call, i) =>
|
|
2179
|
-
this.
|
|
2911
|
+
this.runDirectToolWithLifecycleHooks(call, config, {
|
|
2180
2912
|
batchIndex: i,
|
|
2181
2913
|
turn,
|
|
2182
2914
|
batchScopeId,
|
|
2183
2915
|
resolvedArgsByCallId,
|
|
2916
|
+
preBatchSnapshot,
|
|
2917
|
+
additionalContextsSink: directAdditionalContexts,
|
|
2184
2918
|
})
|
|
2185
2919
|
)
|
|
2186
2920
|
);
|
|
2187
2921
|
this.handleRunToolCompletions(
|
|
2188
2922
|
filteredCalls,
|
|
2189
|
-
|
|
2923
|
+
toolOutputs,
|
|
2190
2924
|
config,
|
|
2191
2925
|
resolvedArgsByCallId
|
|
2192
2926
|
);
|
|
2927
|
+
// Append accumulated additionalContexts as a single
|
|
2928
|
+
// HumanMessage so the next model turn sees them. Codex P2 #39.
|
|
2929
|
+
outputs =
|
|
2930
|
+
directAdditionalContexts.length > 0
|
|
2931
|
+
? [
|
|
2932
|
+
...toolOutputs,
|
|
2933
|
+
new HumanMessage({
|
|
2934
|
+
content: directAdditionalContexts.join('\n\n'),
|
|
2935
|
+
// Same system-role marker the event-driven path
|
|
2936
|
+
// uses so direct vs dispatched is invisible to
|
|
2937
|
+
// downstream consumers. Codex P2 [46].
|
|
2938
|
+
additional_kwargs: { role: 'system', source: 'hook' },
|
|
2939
|
+
}),
|
|
2940
|
+
]
|
|
2941
|
+
: toolOutputs;
|
|
2193
2942
|
}
|
|
2194
2943
|
}
|
|
2195
2944
|
|