@librechat/agents 3.2.58 → 3.2.60
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/main.cjs +7 -0
- package/dist/cjs/run.cjs +4 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +2 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +58 -9
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +4 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +57 -7
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -3
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +114 -11
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +18 -1
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +31 -7
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/run.mjs +4 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +2 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +56 -10
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +4 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +54 -8
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -3
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +115 -12
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +18 -2
- package/dist/esm/tools/eagerEventExecution.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/BashExecutor.d.ts +13 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +60 -1
- package/dist/types/tools/eagerEventExecution.d.ts +8 -0
- 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 +95 -1
- package/package.json +1 -1
- 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/stream.ts +17 -1
- package/src/tools/BashExecutor.ts +107 -14
- package/src/tools/BashProgrammaticToolCalling.ts +20 -1
- package/src/tools/CodeExecutor.ts +113 -9
- package/src/tools/ProgrammaticToolCalling.ts +27 -1
- package/src/tools/ToolNode.ts +208 -31
- package/src/tools/__tests__/BashExecutor.test.ts +39 -0
- package/src/tools/__tests__/CodeExecutor.stateful.test.ts +113 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +86 -0
- package/src/tools/__tests__/eagerEventExecution.session.test.ts +92 -0
- package/src/tools/__tests__/hitl.test.ts +48 -0
- package/src/tools/eagerEventExecution.ts +32 -5
- package/src/types/hitl.ts +49 -3
- package/src/types/run.ts +21 -0
- package/src/types/tools.ts +102 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import type * as t from '@/types';
|
|
3
|
+
import {
|
|
4
|
+
buildToolExecutionRequestPlan,
|
|
5
|
+
resolveRuntimeSessionHint,
|
|
6
|
+
} from '../eagerEventExecution';
|
|
7
|
+
|
|
8
|
+
describe('buildToolExecutionRequestPlan — runtimeSessionHint', () => {
|
|
9
|
+
const usageCount = () => new Map<string, number>();
|
|
10
|
+
|
|
11
|
+
it('carries runtimeSessionHint onto the built ToolCallRequest', () => {
|
|
12
|
+
const plan = buildToolExecutionRequestPlan({
|
|
13
|
+
toolCalls: [
|
|
14
|
+
{
|
|
15
|
+
id: 'call_1',
|
|
16
|
+
name: 'execute_code',
|
|
17
|
+
args: { lang: 'py', code: 'print(1)' },
|
|
18
|
+
runtimeSessionHint: 'conv-42',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
usageCount: usageCount(),
|
|
22
|
+
});
|
|
23
|
+
expect(plan?.requests[0].runtimeSessionHint).toBe('conv-42');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('omits the field entirely when the hint is absent or empty', () => {
|
|
27
|
+
const plan = buildToolExecutionRequestPlan({
|
|
28
|
+
toolCalls: [{ id: 'c1', name: 'execute_code', args: {} }],
|
|
29
|
+
usageCount: usageCount(),
|
|
30
|
+
});
|
|
31
|
+
expect('runtimeSessionHint' in (plan?.requests[0] as object)).toBe(false);
|
|
32
|
+
|
|
33
|
+
const empty = buildToolExecutionRequestPlan({
|
|
34
|
+
toolCalls: [
|
|
35
|
+
{ id: 'c2', name: 'execute_code', args: {}, runtimeSessionHint: '' },
|
|
36
|
+
],
|
|
37
|
+
usageCount: usageCount(),
|
|
38
|
+
});
|
|
39
|
+
expect('runtimeSessionHint' in (empty?.requests[0] as object)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('carries the hint onto invalid-arg (rejected) requests too', () => {
|
|
43
|
+
const plan = buildToolExecutionRequestPlan({
|
|
44
|
+
toolCalls: [
|
|
45
|
+
{
|
|
46
|
+
id: 'c1',
|
|
47
|
+
name: 'execute_code',
|
|
48
|
+
args: 'not-an-object',
|
|
49
|
+
runtimeSessionHint: 'conv-9',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
usageCount: usageCount(),
|
|
53
|
+
invalidArgsBehavior: 'error-result',
|
|
54
|
+
});
|
|
55
|
+
expect(plan?.allRequests[0].runtimeSessionHint).toBe('conv-9');
|
|
56
|
+
expect(plan?.rejectedResults).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('resolveRuntimeSessionHint', () => {
|
|
61
|
+
const sandbox = (
|
|
62
|
+
o: Partial<t.SandboxExecutionConfig>
|
|
63
|
+
): t.ToolExecutionConfig => ({
|
|
64
|
+
sandbox: o,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns undefined unless statefulSessions is on', () => {
|
|
68
|
+
expect(resolveRuntimeSessionHint(undefined, 'thread-1')).toBeUndefined();
|
|
69
|
+
expect(resolveRuntimeSessionHint(sandbox({}), 'thread-1')).toBeUndefined();
|
|
70
|
+
expect(
|
|
71
|
+
resolveRuntimeSessionHint(
|
|
72
|
+
sandbox({ statefulSessions: false }),
|
|
73
|
+
'thread-1'
|
|
74
|
+
)
|
|
75
|
+
).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('prefers an explicit hint, else falls back to thread_id', () => {
|
|
79
|
+
expect(
|
|
80
|
+
resolveRuntimeSessionHint(
|
|
81
|
+
sandbox({ statefulSessions: true, runtimeSessionHint: 'explicit' }),
|
|
82
|
+
'thread-1'
|
|
83
|
+
)
|
|
84
|
+
).toBe('explicit');
|
|
85
|
+
expect(
|
|
86
|
+
resolveRuntimeSessionHint(sandbox({ statefulSessions: true }), 'thread-1')
|
|
87
|
+
).toBe('thread-1');
|
|
88
|
+
expect(
|
|
89
|
+
resolveRuntimeSessionHint(sandbox({ statefulSessions: true }), '')
|
|
90
|
+
).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -3996,6 +3996,54 @@ describe('AskUserQuestion — interrupt + resume', () => {
|
|
|
3996
3996
|
expect(resumedAnswer).toBe('production');
|
|
3997
3997
|
});
|
|
3998
3998
|
|
|
3999
|
+
it('carries multiSelect through the interrupt payload and resumes with the joined option values', async () => {
|
|
4000
|
+
const { askUserQuestion } = await import('@/hitl');
|
|
4001
|
+
|
|
4002
|
+
let resumedAnswer: string | undefined;
|
|
4003
|
+
|
|
4004
|
+
const builder = new StateGraph(MessagesAnnotation)
|
|
4005
|
+
.addNode('clarifier', () => {
|
|
4006
|
+
const resolution = askUserQuestion({
|
|
4007
|
+
question: 'Which environments?',
|
|
4008
|
+
options: [
|
|
4009
|
+
{ label: 'Staging', value: 'staging' },
|
|
4010
|
+
{ label: 'Production', value: 'production' },
|
|
4011
|
+
],
|
|
4012
|
+
multiSelect: true,
|
|
4013
|
+
});
|
|
4014
|
+
resumedAnswer = resolution.answer;
|
|
4015
|
+
return { messages: [] };
|
|
4016
|
+
})
|
|
4017
|
+
.addEdge(START, 'clarifier')
|
|
4018
|
+
.addEdge('clarifier', END);
|
|
4019
|
+
const graph = builder.compile({ checkpointer: new MemorySaver() });
|
|
4020
|
+
|
|
4021
|
+
const config = { configurable: { thread_id: 'ask-q-multi-thread' } };
|
|
4022
|
+
|
|
4023
|
+
const interrupted = (await graph.invoke({ messages: [] }, config)) as {
|
|
4024
|
+
__interrupt__?: Array<{ id?: string; value?: t.HumanInterruptPayload }>;
|
|
4025
|
+
};
|
|
4026
|
+
const payload = interrupted.__interrupt__![0].value!;
|
|
4027
|
+
if (payload.type !== 'ask_user_question') {
|
|
4028
|
+
throw new Error('expected ask_user_question');
|
|
4029
|
+
}
|
|
4030
|
+
expect(payload.question.multiSelect).toBe(true);
|
|
4031
|
+
expect(payload.question.options).toHaveLength(2);
|
|
4032
|
+
|
|
4033
|
+
// Host joins the selected option values with ", ".
|
|
4034
|
+
const resolution: t.AskUserQuestionResolution = {
|
|
4035
|
+
answer: 'staging, production',
|
|
4036
|
+
};
|
|
4037
|
+
await resumeGraph(
|
|
4038
|
+
graph as unknown as CompiledMessagesGraph,
|
|
4039
|
+
interrupted,
|
|
4040
|
+
resolution,
|
|
4041
|
+
config
|
|
4042
|
+
);
|
|
4043
|
+
|
|
4044
|
+
expect(resumedAnswer).toBe('staging, production');
|
|
4045
|
+
});
|
|
4046
|
+
|
|
3999
4047
|
it('a DIRECT tool in event-driven mode can raise ask_user_question from its body and resume with the answer as its ToolMessage', async () => {
|
|
4000
4048
|
/**
|
|
4001
4049
|
* The production host shape (e.g. LibreChat's `AgentInputs.graphTools`
|
|
@@ -52,8 +52,30 @@ export type ToolExecutionPlanCall = {
|
|
|
52
52
|
args: unknown;
|
|
53
53
|
stepId?: string;
|
|
54
54
|
codeSessionContext?: t.ToolCallRequest['codeSessionContext'];
|
|
55
|
+
runtimeSessionHint?: string;
|
|
55
56
|
};
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Stateful runtime session hint for the remote sandbox: only when
|
|
60
|
+
* `toolExecution.sandbox.statefulSessions` is on; explicit host hint else the
|
|
61
|
+
* conversation `thread_id`. Undefined disables the wire field. Shared by the
|
|
62
|
+
* direct ToolNode path and both event-driven planners so they stay in lockstep.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveRuntimeSessionHint(
|
|
65
|
+
toolExecution: t.ToolExecutionConfig | undefined,
|
|
66
|
+
threadId: string | undefined
|
|
67
|
+
): string | undefined {
|
|
68
|
+
const sandbox = toolExecution?.sandbox;
|
|
69
|
+
if (sandbox?.statefulSessions !== true) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const explicit = sandbox.runtimeSessionHint;
|
|
73
|
+
if (explicit != null && explicit !== '') {
|
|
74
|
+
return explicit;
|
|
75
|
+
}
|
|
76
|
+
return threadId != null && threadId !== '' ? threadId : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
export type ToolExecutionRequestPlan = {
|
|
58
80
|
allRequests: t.ToolCallRequest[];
|
|
59
81
|
requests: t.ToolCallRequest[];
|
|
@@ -73,15 +95,12 @@ export function buildToolExecutionRequestPlan(args: {
|
|
|
73
95
|
args: Record<string, unknown>;
|
|
74
96
|
stepId?: string;
|
|
75
97
|
codeSessionContext?: t.ToolCallRequest['codeSessionContext'];
|
|
98
|
+
runtimeSessionHint?: string;
|
|
76
99
|
rejectedErrorMessage?: string;
|
|
77
100
|
}> = [];
|
|
78
101
|
|
|
79
102
|
for (const toolCall of args.toolCalls) {
|
|
80
|
-
if (
|
|
81
|
-
toolCall.id == null ||
|
|
82
|
-
toolCall.id === '' ||
|
|
83
|
-
toolCall.name === ''
|
|
84
|
-
) {
|
|
103
|
+
if (toolCall.id == null || toolCall.id === '' || toolCall.name === '') {
|
|
85
104
|
return undefined;
|
|
86
105
|
}
|
|
87
106
|
const coercedArgs = coerceRecordArgs(toolCall.args);
|
|
@@ -95,6 +114,7 @@ export function buildToolExecutionRequestPlan(args: {
|
|
|
95
114
|
args: {},
|
|
96
115
|
stepId: toolCall.stepId,
|
|
97
116
|
codeSessionContext: toolCall.codeSessionContext,
|
|
117
|
+
runtimeSessionHint: toolCall.runtimeSessionHint,
|
|
98
118
|
rejectedErrorMessage:
|
|
99
119
|
'Invalid tool call arguments: expected a JSON object.',
|
|
100
120
|
});
|
|
@@ -106,6 +126,7 @@ export function buildToolExecutionRequestPlan(args: {
|
|
|
106
126
|
args: coercedArgs,
|
|
107
127
|
stepId: toolCall.stepId,
|
|
108
128
|
codeSessionContext: toolCall.codeSessionContext,
|
|
129
|
+
runtimeSessionHint: toolCall.runtimeSessionHint,
|
|
109
130
|
});
|
|
110
131
|
}
|
|
111
132
|
|
|
@@ -123,6 +144,12 @@ export function buildToolExecutionRequestPlan(args: {
|
|
|
123
144
|
if (toolCall.codeSessionContext != null) {
|
|
124
145
|
request.codeSessionContext = toolCall.codeSessionContext;
|
|
125
146
|
}
|
|
147
|
+
if (
|
|
148
|
+
toolCall.runtimeSessionHint != null &&
|
|
149
|
+
toolCall.runtimeSessionHint !== ''
|
|
150
|
+
) {
|
|
151
|
+
request.runtimeSessionHint = toolCall.runtimeSessionHint;
|
|
152
|
+
}
|
|
126
153
|
return request;
|
|
127
154
|
});
|
|
128
155
|
const requests = allRequests.filter(
|
package/src/types/hitl.ts
CHANGED
|
@@ -140,6 +140,13 @@ export interface AskUserQuestionRequest {
|
|
|
140
140
|
* UI allows it. Omit to require a free-form answer.
|
|
141
141
|
*/
|
|
142
142
|
options?: AskUserQuestionOption[];
|
|
143
|
+
/**
|
|
144
|
+
* When `true`, the host UI may let the user pick several options; the
|
|
145
|
+
* resulting `AskUserQuestionResolution.answer` is the selected option
|
|
146
|
+
* values joined by `", "`. When omitted or `false`, hosts render a
|
|
147
|
+
* single-select picker. Only meaningful alongside `options`.
|
|
148
|
+
*/
|
|
149
|
+
multiSelect?: boolean;
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/**
|
|
@@ -168,9 +175,10 @@ export type HumanInterruptPayload =
|
|
|
168
175
|
export interface AskUserQuestionResolution {
|
|
169
176
|
/**
|
|
170
177
|
* The human's answer. Free-form text, or — when `options` were
|
|
171
|
-
* provided — one of the option `value`s
|
|
172
|
-
*
|
|
173
|
-
*
|
|
178
|
+
* provided — one of the option `value`s (or, when the request set
|
|
179
|
+
* `multiSelect`, several option `value`s joined by `", "`). Hosts may
|
|
180
|
+
* also send any structured object their custom UI defines; see the
|
|
181
|
+
* host docs for what your downstream consumer expects.
|
|
174
182
|
*/
|
|
175
183
|
answer: string;
|
|
176
184
|
}
|
|
@@ -287,6 +295,44 @@ export function isAskUserQuestionInterrupt(
|
|
|
287
295
|
* an interrupt. This applies equally to direct tools (handoffs,
|
|
288
296
|
* subagents) and to event tools.
|
|
289
297
|
*
|
|
298
|
+
* ### Guarding non-idempotent siblings via `interruptingToolNames`
|
|
299
|
+
*
|
|
300
|
+
* The "must be idempotent" rule above is unavoidable in the general
|
|
301
|
+
* case, but the SDK can protect siblings against the one interrupt
|
|
302
|
+
* shape it can predict: a tool whose *body* raises `interrupt()`
|
|
303
|
+
* mid-execution — the `ask_user_question` shape, where the tool
|
|
304
|
+
* suspends the run to collect a human answer. Declare such tools in
|
|
305
|
+
* `RunConfig.interruptingToolNames`
|
|
306
|
+
* ({@link ToolNodeOptions.interruptingToolNames}) and the ToolNode
|
|
307
|
+
* schedules them, within each batch, **ahead of** their
|
|
308
|
+
* non-interrupting direct siblings. When one interrupts, the batch
|
|
309
|
+
* unwinds before any declared-safe sibling has run, so the sibling
|
|
310
|
+
* executes exactly once (on resume) instead of twice. Empirically:
|
|
311
|
+
*
|
|
312
|
+
* - A **direct** sibling sharing the interrupter's in-process
|
|
313
|
+
* `Promise.all` is the only shape that double-executes; declaring
|
|
314
|
+
* the interrupter closes it.
|
|
315
|
+
* - An **event-dispatched** sibling is already safe without any
|
|
316
|
+
* config: the ToolNode awaits the whole direct group (where the
|
|
317
|
+
* body interrupt unwinds) before it dispatches event tools, so a
|
|
318
|
+
* dispatched sibling never runs on the first pass.
|
|
319
|
+
*
|
|
320
|
+
* This is a *scheduling* guard, not full resume idempotency: it only
|
|
321
|
+
* covers tools that interrupt from their own body and only protects
|
|
322
|
+
* siblings scheduled after them. It does not retroactively make a
|
|
323
|
+
* `PreToolUse` `'ask'` gate on tool B stop tool A (already executed)
|
|
324
|
+
* from re-running — unless B is itself declared interrupting, so it
|
|
325
|
+
* runs first. Tools with side effects should still be written
|
|
326
|
+
* idempotent as defense in depth.
|
|
327
|
+
*
|
|
328
|
+
* The guard only REORDERS the direct group — declaring a name does not
|
|
329
|
+
* force it onto the direct path. The interrupting tool must already be a
|
|
330
|
+
* real in-process graphTool (the only kind whose body can reach
|
|
331
|
+
* `interrupt()`). A name that resolves to a schema-only event stub (an
|
|
332
|
+
* inherited `toolDefinition` with no executable instance, e.g. in a
|
|
333
|
+
* self-spawned child that scrubs `graphTools`) stays event-dispatched
|
|
334
|
+
* and the ordering is a no-op for it.
|
|
335
|
+
*
|
|
290
336
|
* ## Note on idempotency
|
|
291
337
|
*
|
|
292
338
|
* Same root cause as the resume re-execution above: LangGraph
|
package/src/types/run.ts
CHANGED
|
@@ -193,6 +193,27 @@ export type RunConfig = {
|
|
|
193
193
|
* the SDK stays name-agnostic.
|
|
194
194
|
*/
|
|
195
195
|
codeSessionToolNames?: string[];
|
|
196
|
+
/**
|
|
197
|
+
* Names of host tools whose in-process body may raise a LangGraph
|
|
198
|
+
* `interrupt()` mid-execution — the canonical example is an
|
|
199
|
+
* `ask_user_question` tool that suspends the run to collect a human
|
|
200
|
+
* answer. Within a single tool-call batch, a named tool that is a real
|
|
201
|
+
* in-process graphTool (the only kind whose body can reach `interrupt()`;
|
|
202
|
+
* graphTools are auto-marked direct) is scheduled **ahead of** its
|
|
203
|
+
* non-interrupting direct siblings. That ordering guarantees a mid-body
|
|
204
|
+
* interrupt unwinds the tool batch before a non-idempotent sibling
|
|
205
|
+
* (send_email, billing) executes, so the sibling cannot run once on the
|
|
206
|
+
* first pass and AGAIN when LangGraph re-runs the interrupted batch on
|
|
207
|
+
* resume.
|
|
208
|
+
*
|
|
209
|
+
* This only reorders the direct group — it does NOT force a name onto
|
|
210
|
+
* the direct path. A name that is only an inherited event `toolDefinition`
|
|
211
|
+
* (schema-only stub, e.g. in a self-spawned child) stays event-dispatched;
|
|
212
|
+
* the guard applies only to tools that are independently direct.
|
|
213
|
+
* Host-declared so the SDK stays name-agnostic; omit to keep the prior
|
|
214
|
+
* (unguarded) behavior.
|
|
215
|
+
*/
|
|
216
|
+
interruptingToolNames?: string[];
|
|
196
217
|
/**
|
|
197
218
|
* Selects the execution backend for built-in code tools. Omit this to keep
|
|
198
219
|
* the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
|
package/src/types/tools.ts
CHANGED
|
@@ -89,10 +89,18 @@ export type ToolNodeOptions = {
|
|
|
89
89
|
handleToolErrors?: boolean;
|
|
90
90
|
loadRuntimeTools?: ToolRefGenerator;
|
|
91
91
|
toolCallStepIds?: Map<string, string>;
|
|
92
|
+
/**
|
|
93
|
+
* Dispatches the error completion event for a failed tool call. Returns
|
|
94
|
+
* whether the event was actually dispatched — `false` (e.g. no run step is
|
|
95
|
+
* registered for the call yet, which happens when a tool fails fast on a
|
|
96
|
+
* resume pass) tells the ToolNode to fall back to its own completion
|
|
97
|
+
* dispatch for the error ToolMessage. A `void` resolution is treated as
|
|
98
|
+
* dispatched for backward compatibility.
|
|
99
|
+
*/
|
|
92
100
|
errorHandler?: (
|
|
93
101
|
data: ToolErrorData,
|
|
94
102
|
metadata?: Record<string, unknown>
|
|
95
|
-
) => Promise<void>;
|
|
103
|
+
) => Promise<boolean | void>;
|
|
96
104
|
/** Tool registry for lazy computation of programmatic tools and tool search */
|
|
97
105
|
toolRegistry?: LCToolRegistry;
|
|
98
106
|
/** Reference to Graph's sessions map for automatic session injection */
|
|
@@ -108,6 +116,34 @@ export type ToolNodeOptions = {
|
|
|
108
116
|
executingAgentId?: string;
|
|
109
117
|
/** Tool names that must be executed directly (via runTool) even in event-driven mode (e.g., graph-managed handoff tools) */
|
|
110
118
|
directToolNames?: Set<string>;
|
|
119
|
+
/**
|
|
120
|
+
* Tool names whose in-process body may raise a LangGraph `interrupt()`
|
|
121
|
+
* mid-execution — e.g. an `ask_user_question` tool that suspends the
|
|
122
|
+
* run to collect a human answer.
|
|
123
|
+
*
|
|
124
|
+
* Within a single tool-call batch, a named tool that is *already*
|
|
125
|
+
* direct (a real in-process graphTool — the only kind whose body can
|
|
126
|
+
* reach `interrupt()`; graphTools are auto-marked direct by the graph)
|
|
127
|
+
* is executed as its own awaited group **before** its non-interrupting
|
|
128
|
+
* direct siblings. If it interrupts, the ToolNode unwinds before any
|
|
129
|
+
* sibling runs, so a non-idempotent sibling (send_email, billing)
|
|
130
|
+
* cannot execute once on the first pass and AGAIN when LangGraph re-runs
|
|
131
|
+
* the interrupted batch on resume.
|
|
132
|
+
*
|
|
133
|
+
* This set only REORDERS the direct group; it does NOT promote a name
|
|
134
|
+
* into it. A name that resolves to a schema-only event stub (an
|
|
135
|
+
* inherited `toolDefinition` with no executable instance — e.g. in a
|
|
136
|
+
* self-spawned child that scrubs `graphTools`) stays event-dispatched.
|
|
137
|
+
* Forcing such a name direct would invoke the stub, which throws
|
|
138
|
+
* "should not be invoked directly in event-driven mode". For the guard
|
|
139
|
+
* to apply, the interrupting tool must independently be direct.
|
|
140
|
+
*
|
|
141
|
+
* Opt-in and empty by default: when unset (or when no direct batch call
|
|
142
|
+
* matches), direct-batch execution is byte-for-byte unchanged. See the
|
|
143
|
+
* "Resume re-execution" section of {@link HumanInTheLoopConfig} for the
|
|
144
|
+
* batch re-execution contract this guards against.
|
|
145
|
+
*/
|
|
146
|
+
interruptingToolNames?: Set<string>;
|
|
111
147
|
/** Opt-in eager execution for event-driven tool calls. */
|
|
112
148
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
113
149
|
/**
|
|
@@ -292,6 +328,15 @@ export type CodeExecutionToolParams =
|
|
|
292
328
|
files?: CodeEnvFile[];
|
|
293
329
|
/** Optional host-supplied Code API auth headers. */
|
|
294
330
|
authHeaders?: CodeApiAuthHeaders;
|
|
331
|
+
/**
|
|
332
|
+
* Advertise best-effort stateful sessions in the tool description
|
|
333
|
+
* (variables/files may persist between calls, may reset). Prompt text
|
|
334
|
+
* only, and it must be set here because the description is bound to the
|
|
335
|
+
* LLM at construction time. Pair it with the run-scoped
|
|
336
|
+
* `toolExecution.sandbox.statefulSessions` gate, which drives the wire
|
|
337
|
+
* hint — set both from one flag so the prompt and the backend agree.
|
|
338
|
+
*/
|
|
339
|
+
statefulSessions?: boolean;
|
|
295
340
|
};
|
|
296
341
|
|
|
297
342
|
export type CodeApiAuthHeaderMap = Record<string, string>;
|
|
@@ -347,6 +392,13 @@ export type ExecuteResult = {
|
|
|
347
392
|
stdout: string;
|
|
348
393
|
stderr: string;
|
|
349
394
|
files?: FileRefs;
|
|
395
|
+
/**
|
|
396
|
+
* Durable runtime session id echoed by a stateful Code API backend
|
|
397
|
+
* (hash of tenant+user+hint). Additive; absent on stateless servers.
|
|
398
|
+
*/
|
|
399
|
+
runtime_session_id?: string;
|
|
400
|
+
/** Whether this execution reused a warm runtime session or started fresh. */
|
|
401
|
+
runtime_status?: 'new' | 'reused';
|
|
350
402
|
};
|
|
351
403
|
|
|
352
404
|
/** JSON Schema type definition for tool parameters */
|
|
@@ -413,6 +465,12 @@ export type ToolCallRequest = {
|
|
|
413
465
|
session_id: string;
|
|
414
466
|
files?: CodeEnvFile[];
|
|
415
467
|
};
|
|
468
|
+
/**
|
|
469
|
+
* Stable runtime session hint for stateful sandbox sessions. Orthogonal to
|
|
470
|
+
* `codeSessionContext` (which threads the transient exec-session for file
|
|
471
|
+
* continuity): the hint identifies the durable server-side runtime session.
|
|
472
|
+
*/
|
|
473
|
+
runtimeSessionHint?: string;
|
|
416
474
|
};
|
|
417
475
|
|
|
418
476
|
/** Batch request containing ALL tool calls for a graph step */
|
|
@@ -969,6 +1027,32 @@ export type CloudflareSandboxExecutionConfig = {
|
|
|
969
1027
|
postEditSyntaxCheck?: LocalExecutionConfig['postEditSyntaxCheck'];
|
|
970
1028
|
};
|
|
971
1029
|
|
|
1030
|
+
export type SandboxExecutionConfig = {
|
|
1031
|
+
/**
|
|
1032
|
+
* Opt into best-effort stateful runtime sessions on the remote Code API
|
|
1033
|
+
* (its warm per-session MicroVM backend). This gate is run-scoped: it only
|
|
1034
|
+
* controls the wire behavior (ToolNode injecting the session hint on
|
|
1035
|
+
* execute_code/bash calls). The transport is otherwise unchanged.
|
|
1036
|
+
*
|
|
1037
|
+
* It does NOT change the model-facing tool description. Tool descriptions are
|
|
1038
|
+
* bound to the LLM at construction time (`createCodeExecutionTool` /
|
|
1039
|
+
* `createBashExecutionTool`), before this run config is applied inside the
|
|
1040
|
+
* graph, so they can only be adjusted via the tools' own `statefulSessions`
|
|
1041
|
+
* factory param. Set BOTH from one flag (as LibreChat does): with this on but
|
|
1042
|
+
* the factory param off, the backend runs statefully while the model is still
|
|
1043
|
+
* told the environment is stateless (non-corrupting — the model just won't
|
|
1044
|
+
* exploit persistence).
|
|
1045
|
+
*/
|
|
1046
|
+
statefulSessions?: boolean;
|
|
1047
|
+
/**
|
|
1048
|
+
* Stable identity for the runtime session (e.g. the conversation id). The
|
|
1049
|
+
* server derives the real session id as hash(tenant, user, hint), so this
|
|
1050
|
+
* is never a security boundary. Falls back to `configurable.thread_id` when
|
|
1051
|
+
* omitted.
|
|
1052
|
+
*/
|
|
1053
|
+
runtimeSessionHint?: string;
|
|
1054
|
+
};
|
|
1055
|
+
|
|
972
1056
|
export type ToolExecutionConfig = {
|
|
973
1057
|
/** `sandbox` preserves the remote Code API behavior and is the default. */
|
|
974
1058
|
engine?: ToolExecutionEngine;
|
|
@@ -976,6 +1060,8 @@ export type ToolExecutionConfig = {
|
|
|
976
1060
|
local?: LocalExecutionConfig;
|
|
977
1061
|
/** Cloudflare Sandbox execution settings used when `engine` is `cloudflare-sandbox`. */
|
|
978
1062
|
cloudflare?: CloudflareSandboxExecutionConfig;
|
|
1063
|
+
/** Remote sandbox settings; applies when `engine` is `sandbox` or omitted. */
|
|
1064
|
+
sandbox?: SandboxExecutionConfig;
|
|
979
1065
|
};
|
|
980
1066
|
|
|
981
1067
|
export type ProgrammaticCache = {
|
|
@@ -1099,6 +1185,10 @@ export type ProgrammaticExecutionResponse = {
|
|
|
1099
1185
|
stderr?: string;
|
|
1100
1186
|
files?: FileRefs;
|
|
1101
1187
|
|
|
1188
|
+
/** Durable runtime session echo from a stateful backend (additive). */
|
|
1189
|
+
runtime_session_id?: string;
|
|
1190
|
+
runtime_status?: 'new' | 'reused';
|
|
1191
|
+
|
|
1102
1192
|
/** Present when status='error' */
|
|
1103
1193
|
error?: string;
|
|
1104
1194
|
};
|
|
@@ -1110,6 +1200,9 @@ export type ProgrammaticExecutionArtifact = {
|
|
|
1110
1200
|
/** Execution session — see `CodeSessionContext.session_id`. */
|
|
1111
1201
|
session_id?: string;
|
|
1112
1202
|
files?: FileRefs;
|
|
1203
|
+
/** Durable runtime session echo from a stateful backend (additive). */
|
|
1204
|
+
runtime_session_id?: string;
|
|
1205
|
+
runtime_status?: 'new' | 'reused';
|
|
1113
1206
|
};
|
|
1114
1207
|
|
|
1115
1208
|
/** Parameters for creating a bash execution tool (same API as CodeExecutor, bash-only) */
|
|
@@ -1134,6 +1227,11 @@ export type ProgrammaticToolCallingParams = {
|
|
|
1134
1227
|
debug?: boolean;
|
|
1135
1228
|
/** Optional host-supplied Code API auth headers. */
|
|
1136
1229
|
authHeaders?: CodeApiAuthHeaders;
|
|
1230
|
+
/* No `statefulSessions` here: PTC is stateless in v1. The initial
|
|
1231
|
+
* /exec/programmatic request still forwards a ToolNode-injected
|
|
1232
|
+
* `_runtime_session_hint` when present, but there is no factory-level opt-in
|
|
1233
|
+
* to advertise (it would be a no-op). Re-add with real behavior when PTC
|
|
1234
|
+
* stateful prompting lands. */
|
|
1137
1235
|
};
|
|
1138
1236
|
|
|
1139
1237
|
// ============================================================================
|
|
@@ -1166,6 +1264,9 @@ export type CodeExecutionArtifact = {
|
|
|
1166
1264
|
/** Execution session — see `CodeSessionContext.session_id`. */
|
|
1167
1265
|
session_id?: string;
|
|
1168
1266
|
files?: FileRefs;
|
|
1267
|
+
/** Durable runtime session echo from a stateful backend (additive). */
|
|
1268
|
+
runtime_session_id?: string;
|
|
1269
|
+
runtime_status?: 'new' | 'reused';
|
|
1169
1270
|
};
|
|
1170
1271
|
|
|
1171
1272
|
/**
|