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