@librechat/agents 3.2.59 → 3.2.61
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/graphs/Graph.cjs +31 -7
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/run.cjs +4 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +95 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +31 -7
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/run.mjs +4 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +95 -10
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +21 -3
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +57 -1
- package/dist/types/types/hitl.d.ts +49 -3
- package/dist/types/types/run.d.ts +21 -0
- package/dist/types/types/tools.d.ts +37 -1
- package/package.json +4 -4
- package/src/graphs/Graph.ts +74 -28
- package/src/run.ts +4 -0
- package/src/specs/ask-user-question-batch.test.ts +289 -0
- package/src/specs/tool-error-resume.test.ts +194 -0
- package/src/tools/ToolNode.ts +172 -27
- package/src/tools/__tests__/hitl.test.ts +48 -0
- package/src/types/hitl.ts +49 -3
- package/src/types/run.ts +21 -0
- package/src/types/tools.ts +37 -1
|
@@ -82,6 +82,14 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
|
|
|
82
82
|
*/
|
|
83
83
|
eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
|
|
84
84
|
codeSessionToolNames: string[] | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Run-scoped names of tools whose in-process body may raise a LangGraph
|
|
87
|
+
* `interrupt()` (e.g. `ask_user_question`). Threaded from
|
|
88
|
+
* `RunConfig.interruptingToolNames` into every ToolNode this graph
|
|
89
|
+
* compiles so a mid-batch interrupt cannot double-execute non-idempotent
|
|
90
|
+
* siblings on resume. See {@link t.ToolNodeOptions.interruptingToolNames}.
|
|
91
|
+
*/
|
|
92
|
+
interruptingToolNames: string[] | undefined;
|
|
85
93
|
eagerEventToolExecutions: Map<string, t.EagerEventToolExecution>;
|
|
86
94
|
eagerEventToolUsageCount: Map<string, number>;
|
|
87
95
|
private eagerEventToolUsageCountsByAgentId;
|
|
@@ -253,14 +261,24 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
|
|
|
253
261
|
dispatchRunStep(stepKey: string, stepDetails: t.StepDetails, metadata?: Record<string, unknown>): Promise<string>;
|
|
254
262
|
/**
|
|
255
263
|
* Static version of handleToolCallError to avoid creating strong references
|
|
256
|
-
* that prevent garbage collection
|
|
264
|
+
* that prevent garbage collection.
|
|
265
|
+
*
|
|
266
|
+
* Returns whether the error completion event was actually dispatched. A
|
|
267
|
+
* tool can error before this graph instance has a run step for the call —
|
|
268
|
+
* on a resume pass the interrupted batch re-executes IMMEDIATELY on graph
|
|
269
|
+
* re-entry, before any step replay has registered `toolCallStepIds` (a
|
|
270
|
+
* fast-failing tool, e.g. a schema-validation reject, loses that race).
|
|
271
|
+
* That is a caller-recoverable condition, not an invariant violation: the
|
|
272
|
+
* ToolNode falls back to its normal completion dispatch for the error
|
|
273
|
+
* ToolMessage when this returns `false`, so throwing here would only
|
|
274
|
+
* replace a recoverable miss with a lost completion event and a scary log.
|
|
257
275
|
*/
|
|
258
|
-
static handleToolCallErrorStatic(graph: StandardGraph, data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<
|
|
276
|
+
static handleToolCallErrorStatic(graph: StandardGraph, data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<boolean>;
|
|
259
277
|
/**
|
|
260
278
|
* Instance method that delegates to the static method
|
|
261
279
|
* Kept for backward compatibility
|
|
262
280
|
*/
|
|
263
|
-
handleToolCallError(data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<
|
|
281
|
+
handleToolCallError(data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<boolean>;
|
|
264
282
|
dispatchRunStepDelta(id: string, delta: t.ToolCallDelta, metadata?: Record<string, unknown>): Promise<void>;
|
|
265
283
|
dispatchMessageDelta(id: string, delta: t.MessageDelta, metadata?: Record<string, unknown>): Promise<void>;
|
|
266
284
|
dispatchReasoningDelta: (stepId: string, delta: t.ReasoningDelta, metadata?: Record<string, unknown>) => Promise<void>;
|
package/dist/types/run.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
14
14
|
private toolOutputReferences?;
|
|
15
15
|
private eagerEventToolExecution?;
|
|
16
16
|
private codeSessionToolNames?;
|
|
17
|
+
private interruptingToolNames?;
|
|
17
18
|
private toolExecution?;
|
|
18
19
|
private subagentUsageSink?;
|
|
19
20
|
private indexTokenCountMap?;
|
|
@@ -67,6 +67,13 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
67
67
|
private agentLangfuse?;
|
|
68
68
|
toolCallStepIds?: Map<string, string>;
|
|
69
69
|
errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
|
|
70
|
+
/**
|
|
71
|
+
* Tool call ids whose `errorHandler` did NOT dispatch the error completion
|
|
72
|
+
* event (it returned `false` or threw). The output loop must dispatch the
|
|
73
|
+
* completion for these itself — skipping them there would strand the
|
|
74
|
+
* client's tool-call part without a terminal event.
|
|
75
|
+
*/
|
|
76
|
+
private undispatchedToolErrors;
|
|
70
77
|
private toolUsageCount;
|
|
71
78
|
/** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
|
|
72
79
|
private toolCallTurns;
|
|
@@ -108,6 +115,21 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
108
115
|
private executingAgentId?;
|
|
109
116
|
/** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
|
|
110
117
|
private directToolNames?;
|
|
118
|
+
/**
|
|
119
|
+
* Tool names whose in-process body may raise a LangGraph `interrupt()`
|
|
120
|
+
* mid-execution (e.g. `ask_user_question`). Used only to REORDER within
|
|
121
|
+
* the direct group: a tool named here that is *already* direct (a real
|
|
122
|
+
* in-process graphTool — the only kind whose body can reach
|
|
123
|
+
* `interrupt()`) is scheduled ahead of its non-interrupting direct
|
|
124
|
+
* siblings, so a mid-body interrupt unwinds the ToolNode before a
|
|
125
|
+
* non-idempotent sibling executes and LangGraph's resume-time batch
|
|
126
|
+
* re-execution can't double it. This set is deliberately NOT folded
|
|
127
|
+
* into direct classification: a name that resolves to a schema-only
|
|
128
|
+
* event stub (an inherited `toolDefinition` with no executable
|
|
129
|
+
* instance) stays event-dispatched — forcing it direct would invoke
|
|
130
|
+
* the stub, which throws. See {@link t.ToolNodeOptions.interruptingToolNames}.
|
|
131
|
+
*/
|
|
132
|
+
private interruptingToolNames?;
|
|
111
133
|
/**
|
|
112
134
|
* File checkpointer extracted from the local coding tool bundle when
|
|
113
135
|
* `toolExecution.local.fileCheckpointing === true`. Exposed via
|
|
@@ -147,7 +169,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
147
169
|
* other's in-flight state.
|
|
148
170
|
*/
|
|
149
171
|
private anonBatchCounter;
|
|
150
|
-
constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
|
|
172
|
+
constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
|
|
151
173
|
invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
|
|
152
174
|
/**
|
|
153
175
|
* Returns the run-scoped tool output registry, or `undefined` when
|
|
@@ -385,6 +407,40 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
385
407
|
* The original role is preserved in additional_kwargs for downstream consumers.
|
|
386
408
|
*/
|
|
387
409
|
private convertInjectedMessages;
|
|
410
|
+
/**
|
|
411
|
+
* Execute a group of direct (in-process) tool calls with interrupt-safe
|
|
412
|
+
* ordering, returning outputs aligned 1:1 with `directCalls`.
|
|
413
|
+
*
|
|
414
|
+
* Fast path (the common case): when no call in the group is in
|
|
415
|
+
* `interruptingToolNames`, this is a single `Promise.all` — byte-for-byte
|
|
416
|
+
* the prior behavior, so ordinary batches are unaffected.
|
|
417
|
+
*
|
|
418
|
+
* Interrupt-safe path: when the group contains an interrupting tool (e.g.
|
|
419
|
+
* `ask_user_question`, whose body raises a LangGraph `interrupt()` to
|
|
420
|
+
* collect a human answer), the interrupting calls run as their own awaited
|
|
421
|
+
* group **first**; only after they all settle without interrupting do the
|
|
422
|
+
* remaining (potentially non-idempotent) siblings run. If an interrupting
|
|
423
|
+
* call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
|
|
424
|
+
* whole ToolNode *before* any non-interrupting sibling has started — so a
|
|
425
|
+
* sibling with real side effects (send_email, billing) never executes on
|
|
426
|
+
* the first pass. On the resume pass LangGraph re-runs the batch from the
|
|
427
|
+
* top; the interrupting tool resolves with the host's answer instead of
|
|
428
|
+
* throwing, and the siblings execute for the FIRST time, exactly once.
|
|
429
|
+
*
|
|
430
|
+
* Without this ordering, a flat `Promise.all` starts every sibling
|
|
431
|
+
* concurrently, so a non-idempotent sibling can complete its side effect
|
|
432
|
+
* before the interrupt unwinds and then run a SECOND time on resume — the
|
|
433
|
+
* duplicate side effect this method exists to prevent. Interrupting tools
|
|
434
|
+
* are expected to be side-effect-free (they only suspend), so running them
|
|
435
|
+
* as a group and re-running them on resume is harmless.
|
|
436
|
+
*
|
|
437
|
+
* `batchIndices[i]` is `directCalls[i]`'s position within the parent
|
|
438
|
+
* ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
|
|
439
|
+
* preserved regardless of execution order. `baseContext` carries the
|
|
440
|
+
* batch-scoped fields every call shares; `batchIndex` is filled in
|
|
441
|
+
* per-call here.
|
|
442
|
+
*/
|
|
443
|
+
private runDirectBatchInterruptSafe;
|
|
388
444
|
/**
|
|
389
445
|
* Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
|
|
390
446
|
* Injected messages are placed AFTER ToolMessages to respect provider
|
|
@@ -134,6 +134,13 @@ export interface AskUserQuestionRequest {
|
|
|
134
134
|
* UI allows it. Omit to require a free-form answer.
|
|
135
135
|
*/
|
|
136
136
|
options?: AskUserQuestionOption[];
|
|
137
|
+
/**
|
|
138
|
+
* When `true`, the host UI may let the user pick several options; the
|
|
139
|
+
* resulting `AskUserQuestionResolution.answer` is the selected option
|
|
140
|
+
* values joined by `", "`. When omitted or `false`, hosts render a
|
|
141
|
+
* single-select picker. Only meaningful alongside `options`.
|
|
142
|
+
*/
|
|
143
|
+
multiSelect?: boolean;
|
|
137
144
|
}
|
|
138
145
|
/**
|
|
139
146
|
* Structured payload the SDK passes to `interrupt()` when an agent (or
|
|
@@ -157,9 +164,10 @@ export type HumanInterruptPayload = ToolApprovalInterruptPayload | AskUserQuesti
|
|
|
157
164
|
export interface AskUserQuestionResolution {
|
|
158
165
|
/**
|
|
159
166
|
* The human's answer. Free-form text, or — when `options` were
|
|
160
|
-
* provided — one of the option `value`s
|
|
161
|
-
*
|
|
162
|
-
*
|
|
167
|
+
* provided — one of the option `value`s (or, when the request set
|
|
168
|
+
* `multiSelect`, several option `value`s joined by `", "`). Hosts may
|
|
169
|
+
* also send any structured object their custom UI defines; see the
|
|
170
|
+
* host docs for what your downstream consumer expects.
|
|
163
171
|
*/
|
|
164
172
|
answer: string;
|
|
165
173
|
}
|
|
@@ -257,6 +265,44 @@ export declare function isAskUserQuestionInterrupt(payload: unknown): payload is
|
|
|
257
265
|
* an interrupt. This applies equally to direct tools (handoffs,
|
|
258
266
|
* subagents) and to event tools.
|
|
259
267
|
*
|
|
268
|
+
* ### Guarding non-idempotent siblings via `interruptingToolNames`
|
|
269
|
+
*
|
|
270
|
+
* The "must be idempotent" rule above is unavoidable in the general
|
|
271
|
+
* case, but the SDK can protect siblings against the one interrupt
|
|
272
|
+
* shape it can predict: a tool whose *body* raises `interrupt()`
|
|
273
|
+
* mid-execution — the `ask_user_question` shape, where the tool
|
|
274
|
+
* suspends the run to collect a human answer. Declare such tools in
|
|
275
|
+
* `RunConfig.interruptingToolNames`
|
|
276
|
+
* ({@link ToolNodeOptions.interruptingToolNames}) and the ToolNode
|
|
277
|
+
* schedules them, within each batch, **ahead of** their
|
|
278
|
+
* non-interrupting direct siblings. When one interrupts, the batch
|
|
279
|
+
* unwinds before any declared-safe sibling has run, so the sibling
|
|
280
|
+
* executes exactly once (on resume) instead of twice. Empirically:
|
|
281
|
+
*
|
|
282
|
+
* - A **direct** sibling sharing the interrupter's in-process
|
|
283
|
+
* `Promise.all` is the only shape that double-executes; declaring
|
|
284
|
+
* the interrupter closes it.
|
|
285
|
+
* - An **event-dispatched** sibling is already safe without any
|
|
286
|
+
* config: the ToolNode awaits the whole direct group (where the
|
|
287
|
+
* body interrupt unwinds) before it dispatches event tools, so a
|
|
288
|
+
* dispatched sibling never runs on the first pass.
|
|
289
|
+
*
|
|
290
|
+
* This is a *scheduling* guard, not full resume idempotency: it only
|
|
291
|
+
* covers tools that interrupt from their own body and only protects
|
|
292
|
+
* siblings scheduled after them. It does not retroactively make a
|
|
293
|
+
* `PreToolUse` `'ask'` gate on tool B stop tool A (already executed)
|
|
294
|
+
* from re-running — unless B is itself declared interrupting, so it
|
|
295
|
+
* runs first. Tools with side effects should still be written
|
|
296
|
+
* idempotent as defense in depth.
|
|
297
|
+
*
|
|
298
|
+
* The guard only REORDERS the direct group — declaring a name does not
|
|
299
|
+
* force it onto the direct path. The interrupting tool must already be a
|
|
300
|
+
* real in-process graphTool (the only kind whose body can reach
|
|
301
|
+
* `interrupt()`). A name that resolves to a schema-only event stub (an
|
|
302
|
+
* inherited `toolDefinition` with no executable instance, e.g. in a
|
|
303
|
+
* self-spawned child that scrubs `graphTools`) stays event-dispatched
|
|
304
|
+
* and the ordering is a no-op for it.
|
|
305
|
+
*
|
|
260
306
|
* ## Note on idempotency
|
|
261
307
|
*
|
|
262
308
|
* Same root cause as the resume re-execution above: LangGraph
|
|
@@ -179,6 +179,27 @@ export type RunConfig = {
|
|
|
179
179
|
* the SDK stays name-agnostic.
|
|
180
180
|
*/
|
|
181
181
|
codeSessionToolNames?: string[];
|
|
182
|
+
/**
|
|
183
|
+
* Names of host tools whose in-process body may raise a LangGraph
|
|
184
|
+
* `interrupt()` mid-execution — the canonical example is an
|
|
185
|
+
* `ask_user_question` tool that suspends the run to collect a human
|
|
186
|
+
* answer. Within a single tool-call batch, a named tool that is a real
|
|
187
|
+
* in-process graphTool (the only kind whose body can reach `interrupt()`;
|
|
188
|
+
* graphTools are auto-marked direct) is scheduled **ahead of** its
|
|
189
|
+
* non-interrupting direct siblings. That ordering guarantees a mid-body
|
|
190
|
+
* interrupt unwinds the tool batch before a non-idempotent sibling
|
|
191
|
+
* (send_email, billing) executes, so the sibling cannot run once on the
|
|
192
|
+
* first pass and AGAIN when LangGraph re-runs the interrupted batch on
|
|
193
|
+
* resume.
|
|
194
|
+
*
|
|
195
|
+
* This only reorders the direct group — it does NOT force a name onto
|
|
196
|
+
* the direct path. A name that is only an inherited event `toolDefinition`
|
|
197
|
+
* (schema-only stub, e.g. in a self-spawned child) stays event-dispatched;
|
|
198
|
+
* the guard applies only to tools that are independently direct.
|
|
199
|
+
* Host-declared so the SDK stays name-agnostic; omit to keep the prior
|
|
200
|
+
* (unguarded) behavior.
|
|
201
|
+
*/
|
|
202
|
+
interruptingToolNames?: string[];
|
|
182
203
|
/**
|
|
183
204
|
* Selects the execution backend for built-in code tools. Omit this to keep
|
|
184
205
|
* the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
|
|
@@ -82,7 +82,15 @@ export type ToolNodeOptions = {
|
|
|
82
82
|
handleToolErrors?: boolean;
|
|
83
83
|
loadRuntimeTools?: ToolRefGenerator;
|
|
84
84
|
toolCallStepIds?: Map<string, string>;
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Dispatches the error completion event for a failed tool call. Returns
|
|
87
|
+
* whether the event was actually dispatched — `false` (e.g. no run step is
|
|
88
|
+
* registered for the call yet, which happens when a tool fails fast on a
|
|
89
|
+
* resume pass) tells the ToolNode to fall back to its own completion
|
|
90
|
+
* dispatch for the error ToolMessage. A `void` resolution is treated as
|
|
91
|
+
* dispatched for backward compatibility.
|
|
92
|
+
*/
|
|
93
|
+
errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => Promise<boolean | void>;
|
|
86
94
|
/** Tool registry for lazy computation of programmatic tools and tool search */
|
|
87
95
|
toolRegistry?: LCToolRegistry;
|
|
88
96
|
/** Reference to Graph's sessions map for automatic session injection */
|
|
@@ -98,6 +106,34 @@ export type ToolNodeOptions = {
|
|
|
98
106
|
executingAgentId?: string;
|
|
99
107
|
/** Tool names that must be executed directly (via runTool) even in event-driven mode (e.g., graph-managed handoff tools) */
|
|
100
108
|
directToolNames?: Set<string>;
|
|
109
|
+
/**
|
|
110
|
+
* Tool names whose in-process body may raise a LangGraph `interrupt()`
|
|
111
|
+
* mid-execution — e.g. an `ask_user_question` tool that suspends the
|
|
112
|
+
* run to collect a human answer.
|
|
113
|
+
*
|
|
114
|
+
* Within a single tool-call batch, a named tool that is *already*
|
|
115
|
+
* direct (a real in-process graphTool — the only kind whose body can
|
|
116
|
+
* reach `interrupt()`; graphTools are auto-marked direct by the graph)
|
|
117
|
+
* is executed as its own awaited group **before** its non-interrupting
|
|
118
|
+
* direct siblings. If it interrupts, the ToolNode unwinds before any
|
|
119
|
+
* sibling runs, so a non-idempotent sibling (send_email, billing)
|
|
120
|
+
* cannot execute once on the first pass and AGAIN when LangGraph re-runs
|
|
121
|
+
* the interrupted batch on resume.
|
|
122
|
+
*
|
|
123
|
+
* This set only REORDERS the direct group; it does NOT promote a name
|
|
124
|
+
* into it. A name that resolves to a schema-only event stub (an
|
|
125
|
+
* inherited `toolDefinition` with no executable instance — e.g. in a
|
|
126
|
+
* self-spawned child that scrubs `graphTools`) stays event-dispatched.
|
|
127
|
+
* Forcing such a name direct would invoke the stub, which throws
|
|
128
|
+
* "should not be invoked directly in event-driven mode". For the guard
|
|
129
|
+
* to apply, the interrupting tool must independently be direct.
|
|
130
|
+
*
|
|
131
|
+
* Opt-in and empty by default: when unset (or when no direct batch call
|
|
132
|
+
* matches), direct-batch execution is byte-for-byte unchanged. See the
|
|
133
|
+
* "Resume re-execution" section of {@link HumanInTheLoopConfig} for the
|
|
134
|
+
* batch re-execution contract this guards against.
|
|
135
|
+
*/
|
|
136
|
+
interruptingToolNames?: Set<string>;
|
|
101
137
|
/** Opt-in eager execution for event-driven tool calls. */
|
|
102
138
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
103
139
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.61",
|
|
4
4
|
"main": "./dist/cjs/main.cjs",
|
|
5
5
|
"module": "./dist/esm/main.mjs",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
"fast-xml-parser": "5.7.2",
|
|
209
209
|
"ajv": "6.14.0",
|
|
210
210
|
"minimatch": "3.1.4",
|
|
211
|
-
"@opentelemetry/core": "^2.
|
|
211
|
+
"@opentelemetry/core": "^2.9.0",
|
|
212
212
|
"js-yaml": "^4.2.0"
|
|
213
213
|
},
|
|
214
214
|
"dependencies": {
|
|
@@ -230,8 +230,8 @@
|
|
|
230
230
|
"@langfuse/langchain": "^5.4.1",
|
|
231
231
|
"@langfuse/otel": "^5.4.1",
|
|
232
232
|
"@langfuse/tracing": "^5.4.1",
|
|
233
|
-
"@opentelemetry/context-async-hooks": "2.
|
|
234
|
-
"@opentelemetry/sdk-node": "^0.
|
|
233
|
+
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
234
|
+
"@opentelemetry/sdk-node": "^0.220.0",
|
|
235
235
|
"@scarf/scarf": "^1.4.0",
|
|
236
236
|
"@types/diff": "^7.0.2",
|
|
237
237
|
"ai-tokenizer": "^1.0.6",
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -609,6 +609,14 @@ export abstract class Graph<
|
|
|
609
609
|
*/
|
|
610
610
|
eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
|
|
611
611
|
codeSessionToolNames: string[] | undefined;
|
|
612
|
+
/**
|
|
613
|
+
* Run-scoped names of tools whose in-process body may raise a LangGraph
|
|
614
|
+
* `interrupt()` (e.g. `ask_user_question`). Threaded from
|
|
615
|
+
* `RunConfig.interruptingToolNames` into every ToolNode this graph
|
|
616
|
+
* compiles so a mid-batch interrupt cannot double-execute non-idempotent
|
|
617
|
+
* siblings on resume. See {@link t.ToolNodeOptions.interruptingToolNames}.
|
|
618
|
+
*/
|
|
619
|
+
interruptingToolNames: string[] | undefined;
|
|
612
620
|
eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
|
|
613
621
|
eagerEventToolUsageCount: Map<string, number> = new Map();
|
|
614
622
|
private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
|
|
@@ -658,6 +666,7 @@ export abstract class Graph<
|
|
|
658
666
|
this.toolOutputReferences = undefined;
|
|
659
667
|
this.eagerEventToolExecution = undefined;
|
|
660
668
|
this.codeSessionToolNames = undefined;
|
|
669
|
+
this.interruptingToolNames = undefined;
|
|
661
670
|
this.eagerEventToolExecutions.clear();
|
|
662
671
|
this.clearEagerEventToolUsageCounts();
|
|
663
672
|
this.eagerEventToolCallChunks.clear();
|
|
@@ -1277,11 +1286,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1277
1286
|
),
|
|
1278
1287
|
toolExecution: this.toolExecution,
|
|
1279
1288
|
directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
|
|
1289
|
+
interruptingToolNames:
|
|
1290
|
+
this.interruptingToolNames != null &&
|
|
1291
|
+
this.interruptingToolNames.length > 0
|
|
1292
|
+
? new Set(this.interruptingToolNames)
|
|
1293
|
+
: undefined,
|
|
1280
1294
|
maxContextTokens: agentContext?.maxContextTokens,
|
|
1281
1295
|
maxToolResultChars: agentContext?.maxToolResultChars,
|
|
1282
1296
|
toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
|
|
1283
1297
|
fileCheckpointer: this.getOrCreateFileCheckpointer(),
|
|
1284
|
-
errorHandler: (data, metadata): Promise<
|
|
1298
|
+
errorHandler: (data, metadata): Promise<boolean> =>
|
|
1285
1299
|
StandardGraph.handleToolCallErrorStatic(this, data, metadata),
|
|
1286
1300
|
});
|
|
1287
1301
|
this.registerCompiledToolNode(node);
|
|
@@ -1330,12 +1344,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1330
1344
|
// agent so hooks can attribute the batch even at the top level.
|
|
1331
1345
|
executingAgentId: agentContext?.agentId,
|
|
1332
1346
|
toolCallStepIds: this.toolCallStepIds,
|
|
1333
|
-
errorHandler: (data, metadata): Promise<
|
|
1347
|
+
errorHandler: (data, metadata): Promise<boolean> =>
|
|
1334
1348
|
StandardGraph.handleToolCallErrorStatic(this, data, metadata),
|
|
1335
1349
|
toolRegistry: agentContext?.toolRegistry,
|
|
1336
1350
|
sessions: this.sessions,
|
|
1337
1351
|
toolExecution: this.toolExecution,
|
|
1338
1352
|
codeSessionToolNames: this.codeSessionToolNames,
|
|
1353
|
+
interruptingToolNames:
|
|
1354
|
+
this.interruptingToolNames != null &&
|
|
1355
|
+
this.interruptingToolNames.length > 0
|
|
1356
|
+
? new Set(this.interruptingToolNames)
|
|
1357
|
+
: undefined,
|
|
1339
1358
|
hookRegistry: this.hookRegistry,
|
|
1340
1359
|
humanInTheLoop: this.humanInTheLoop,
|
|
1341
1360
|
maxContextTokens: agentContext?.maxContextTokens,
|
|
@@ -2361,6 +2380,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2361
2380
|
childGraph.toolOutputReferences = this.toolOutputReferences;
|
|
2362
2381
|
childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
2363
2382
|
childGraph.codeSessionToolNames = this.codeSessionToolNames;
|
|
2383
|
+
// Pure execution-ordering hint (unlike `humanInTheLoop` above).
|
|
2384
|
+
// It ONLY reorders tools already in the child's direct group;
|
|
2385
|
+
// it does not force a name onto the direct path (that fold-in
|
|
2386
|
+
// was removed — Codex review of #294). So for a self-spawned
|
|
2387
|
+
// child that scrubs inherited `graphTools` (keeping only the
|
|
2388
|
+
// event `toolDefinition` / schema-only stub for a name like
|
|
2389
|
+
// `ask_user_question`), the name isn't in the child's direct
|
|
2390
|
+
// group and this is a no-op — the stub is still dispatched via
|
|
2391
|
+
// ON_TOOL_EXECUTE, never invoked directly. Where the child DOES
|
|
2392
|
+
// have the executable graphTool, the guard correctly applies.
|
|
2393
|
+
childGraph.interruptingToolNames = this.interruptingToolNames;
|
|
2364
2394
|
childGraph.toolExecution = this.toolExecution;
|
|
2365
2395
|
childGraph.eventToolExecutionAvailable =
|
|
2366
2396
|
this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
|
|
@@ -2715,32 +2745,38 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2715
2745
|
|
|
2716
2746
|
/**
|
|
2717
2747
|
* Static version of handleToolCallError to avoid creating strong references
|
|
2718
|
-
* that prevent garbage collection
|
|
2748
|
+
* that prevent garbage collection.
|
|
2749
|
+
*
|
|
2750
|
+
* Returns whether the error completion event was actually dispatched. A
|
|
2751
|
+
* tool can error before this graph instance has a run step for the call —
|
|
2752
|
+
* on a resume pass the interrupted batch re-executes IMMEDIATELY on graph
|
|
2753
|
+
* re-entry, before any step replay has registered `toolCallStepIds` (a
|
|
2754
|
+
* fast-failing tool, e.g. a schema-validation reject, loses that race).
|
|
2755
|
+
* That is a caller-recoverable condition, not an invariant violation: the
|
|
2756
|
+
* ToolNode falls back to its normal completion dispatch for the error
|
|
2757
|
+
* ToolMessage when this returns `false`, so throwing here would only
|
|
2758
|
+
* replace a recoverable miss with a lost completion event and a scary log.
|
|
2719
2759
|
*/
|
|
2720
2760
|
static async handleToolCallErrorStatic(
|
|
2721
2761
|
graph: StandardGraph,
|
|
2722
2762
|
data: t.ToolErrorData,
|
|
2723
2763
|
metadata?: Record<string, unknown>
|
|
2724
|
-
): Promise<
|
|
2725
|
-
if (!graph.config) {
|
|
2726
|
-
throw new Error('No config provided');
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2764
|
+
): Promise<boolean> {
|
|
2729
2765
|
if (!data.id) {
|
|
2730
2766
|
console.warn('No Tool ID provided for Tool Error');
|
|
2731
|
-
return;
|
|
2767
|
+
return false;
|
|
2732
2768
|
}
|
|
2733
2769
|
|
|
2734
2770
|
const stepId = graph.toolCallStepIds.get(data.id) ?? '';
|
|
2735
2771
|
if (!stepId) {
|
|
2736
|
-
|
|
2772
|
+
return false;
|
|
2737
2773
|
}
|
|
2738
2774
|
|
|
2739
2775
|
const { name, input: args, error } = data;
|
|
2740
2776
|
|
|
2741
2777
|
const runStep = graph.getRunStep(stepId);
|
|
2742
2778
|
if (!runStep) {
|
|
2743
|
-
|
|
2779
|
+
return false;
|
|
2744
2780
|
}
|
|
2745
2781
|
|
|
2746
2782
|
const tool_call: t.ProcessedToolCall = {
|
|
@@ -2751,21 +2787,31 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2751
2787
|
progress: 1,
|
|
2752
2788
|
};
|
|
2753
2789
|
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2790
|
+
// No registered ON_RUN_STEP_COMPLETED handler ⇒ nothing was dispatched.
|
|
2791
|
+
// Report `false` so the ToolNode runs its own fallback dispatch; returning
|
|
2792
|
+
// `true` here would silently drop the error completion for hosts that wire
|
|
2793
|
+
// completions through callback-based custom events instead of a handler.
|
|
2794
|
+
const handler = graph.handlerRegistry?.getHandler(
|
|
2795
|
+
GraphEvents.ON_RUN_STEP_COMPLETED
|
|
2796
|
+
);
|
|
2797
|
+
if (!handler) {
|
|
2798
|
+
return false;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
await handler.handle(
|
|
2802
|
+
GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
2803
|
+
{
|
|
2804
|
+
result: {
|
|
2805
|
+
id: stepId,
|
|
2806
|
+
index: runStep.index,
|
|
2807
|
+
type: 'tool_call',
|
|
2808
|
+
tool_call,
|
|
2809
|
+
} as t.ToolCompleteEvent,
|
|
2810
|
+
},
|
|
2811
|
+
metadata,
|
|
2812
|
+
graph
|
|
2813
|
+
);
|
|
2814
|
+
return true;
|
|
2769
2815
|
}
|
|
2770
2816
|
|
|
2771
2817
|
/**
|
|
@@ -2775,8 +2821,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2775
2821
|
async handleToolCallError(
|
|
2776
2822
|
data: t.ToolErrorData,
|
|
2777
2823
|
metadata?: Record<string, unknown>
|
|
2778
|
-
): Promise<
|
|
2779
|
-
|
|
2824
|
+
): Promise<boolean> {
|
|
2825
|
+
return StandardGraph.handleToolCallErrorStatic(this, data, metadata);
|
|
2780
2826
|
}
|
|
2781
2827
|
|
|
2782
2828
|
async dispatchRunStepDelta(
|
package/src/run.ts
CHANGED
|
@@ -128,6 +128,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
128
128
|
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
129
129
|
private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
|
|
130
130
|
private codeSessionToolNames?: string[];
|
|
131
|
+
private interruptingToolNames?: string[];
|
|
131
132
|
private toolExecution?: t.ToolExecutionConfig;
|
|
132
133
|
private subagentUsageSink?: t.SubagentUsageSink;
|
|
133
134
|
private indexTokenCountMap?: Record<string, number>;
|
|
@@ -184,6 +185,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
184
185
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
185
186
|
this.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
186
187
|
this.codeSessionToolNames = config.codeSessionToolNames;
|
|
188
|
+
this.interruptingToolNames = config.interruptingToolNames;
|
|
187
189
|
this.toolExecution = config.toolExecution;
|
|
188
190
|
this.subagentUsageSink = config.subagentUsageSink;
|
|
189
191
|
|
|
@@ -271,6 +273,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
271
273
|
standardGraph.toolOutputReferences = this.toolOutputReferences;
|
|
272
274
|
standardGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
273
275
|
standardGraph.codeSessionToolNames = this.codeSessionToolNames;
|
|
276
|
+
standardGraph.interruptingToolNames = this.interruptingToolNames;
|
|
274
277
|
standardGraph.toolExecution = this.toolExecution;
|
|
275
278
|
this.Graph = standardGraph;
|
|
276
279
|
return standardGraph.createWorkflow();
|
|
@@ -301,6 +304,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
301
304
|
multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
|
|
302
305
|
multiAgentGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
303
306
|
multiAgentGraph.codeSessionToolNames = this.codeSessionToolNames;
|
|
307
|
+
multiAgentGraph.interruptingToolNames = this.interruptingToolNames;
|
|
304
308
|
multiAgentGraph.toolExecution = this.toolExecution;
|
|
305
309
|
this.Graph = multiAgentGraph;
|
|
306
310
|
return multiAgentGraph.createWorkflow();
|