@librechat/agents 3.2.58 → 3.2.59
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/main.cjs +7 -0
- 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 +19 -1
- 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/main.mjs +3 -3
- 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 +20 -2
- 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/tools/BashExecutor.d.ts +13 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +3 -0
- package/dist/types/tools/eagerEventExecution.d.ts +8 -0
- package/dist/types/types/tools.d.ts +58 -0
- package/package.json +1 -1
- 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 +36 -4
- 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/eagerEventExecution.ts +32 -5
- package/src/types/tools.ts +65 -0
|
@@ -149,6 +149,64 @@ Usage:
|
|
|
149
149
|
- NEVER use this tool to execute malicious code.
|
|
150
150
|
`.trim();
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Best-effort statefulness note. Deliberately hedged: warm reuse is an
|
|
154
|
+
* optimization, not a guarantee (the runtime may be reset on idle timeout,
|
|
155
|
+
* eviction, or the 8h VM lifetime), so the model must never depend on carried
|
|
156
|
+
* state for correctness and must persist anything durable to /mnt/data.
|
|
157
|
+
*/
|
|
158
|
+
export const STATEFUL_ENV_NOTE =
|
|
159
|
+
'Session state (best-effort): consecutive executions in this conversation usually share one runtime, so variables, imports, and in-memory data from earlier successful calls are typically still available. The runtime may be reset at any time, so treat carried-over state as an optimization, never a guarantee. Anything that must survive MUST be written to /mnt/data. If a NameError/ImportError signals lost state, re-run the needed setup and continue.';
|
|
160
|
+
|
|
161
|
+
export const StatefulCodeExecutionToolDescription = `
|
|
162
|
+
Runs code and returns stdout/stderr output from a session-based execution environment, similar to a long-running command-line session.
|
|
163
|
+
|
|
164
|
+
${STATEFUL_ENV_NOTE}
|
|
165
|
+
|
|
166
|
+
Usage:
|
|
167
|
+
- No network access available.
|
|
168
|
+
- Generated files are automatically delivered; **DO NOT** provide download links.
|
|
169
|
+
- ${CODE_ARTIFACT_PATH_GUIDANCE}
|
|
170
|
+
- NEVER use this tool to execute malicious code.
|
|
171
|
+
`.trim();
|
|
172
|
+
|
|
173
|
+
export function buildCodeExecutionToolDescription(opts?: {
|
|
174
|
+
statefulSessions?: boolean;
|
|
175
|
+
}): string {
|
|
176
|
+
return opts?.statefulSessions === true
|
|
177
|
+
? StatefulCodeExecutionToolDescription
|
|
178
|
+
: CodeExecutionToolDescription;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const STATELESS_CODE_PARAM_NOTE =
|
|
182
|
+
'The environment is stateless; variables and imports don\'t persist between executions.';
|
|
183
|
+
const STATEFUL_CODE_PARAM_NOTE =
|
|
184
|
+
'Executions in this conversation usually share one runtime: variables and imports from prior successful calls are typically still defined, but the runtime may reset between calls. Rebuild state on NameError/ImportError; persist anything important to /mnt/data.';
|
|
185
|
+
|
|
186
|
+
export function buildCodeExecutionToolSchema(opts?: {
|
|
187
|
+
statefulSessions?: boolean;
|
|
188
|
+
}): typeof CodeExecutionToolSchema {
|
|
189
|
+
const note =
|
|
190
|
+
opts?.statefulSessions === true
|
|
191
|
+
? STATEFUL_CODE_PARAM_NOTE
|
|
192
|
+
: STATELESS_CODE_PARAM_NOTE;
|
|
193
|
+
const codeDescription =
|
|
194
|
+
CodeExecutionToolSchema.properties.code.description.replace(
|
|
195
|
+
STATELESS_CODE_PARAM_NOTE,
|
|
196
|
+
note
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
...CodeExecutionToolSchema,
|
|
200
|
+
properties: {
|
|
201
|
+
...CodeExecutionToolSchema.properties,
|
|
202
|
+
code: {
|
|
203
|
+
...CodeExecutionToolSchema.properties.code,
|
|
204
|
+
description: codeDescription,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
} as typeof CodeExecutionToolSchema;
|
|
208
|
+
}
|
|
209
|
+
|
|
152
210
|
export const CodeExecutionToolName = Constants.EXECUTE_CODE;
|
|
153
211
|
|
|
154
212
|
export const CodeExecutionToolDefinition = {
|
|
@@ -162,21 +220,42 @@ function createCodeExecutionTool(
|
|
|
162
220
|
): DynamicStructuredTool {
|
|
163
221
|
return tool(
|
|
164
222
|
async (rawInput, config) => {
|
|
165
|
-
|
|
166
|
-
|
|
223
|
+
/* `statefulSessions` is a prompt-only flag (drives the description);
|
|
224
|
+
* keep it out of the wire body. */
|
|
225
|
+
const {
|
|
226
|
+
authHeaders,
|
|
227
|
+
statefulSessions: _statefulSessions,
|
|
228
|
+
...executionParams
|
|
229
|
+
} = params ?? {};
|
|
230
|
+
void _statefulSessions;
|
|
231
|
+
/* Drop any model-supplied `runtime_session_hint` from the raw args: the
|
|
232
|
+
* hint is host-controlled and must only ever come from ToolNode's
|
|
233
|
+
* injected `_runtime_session_hint` (below). Spreading `...rest` into
|
|
234
|
+
* postData would otherwise let a tool call opt itself into / pick a
|
|
235
|
+
* stateful runtime even when statefulSessions is off. */
|
|
236
|
+
const {
|
|
237
|
+
lang,
|
|
238
|
+
code,
|
|
239
|
+
runtime_session_hint: _ignoredModelHint,
|
|
240
|
+
...rest
|
|
241
|
+
} = rawInput as {
|
|
167
242
|
lang: SupportedLanguage;
|
|
168
243
|
code: string;
|
|
244
|
+
runtime_session_hint?: unknown;
|
|
169
245
|
args?: string[];
|
|
170
246
|
};
|
|
247
|
+
void _ignoredModelHint;
|
|
171
248
|
/**
|
|
172
249
|
* Extract session context from config.toolCall (injected by ToolNode).
|
|
173
250
|
* - session_id: associates with the previous run.
|
|
174
251
|
* - _injected_files: File refs to pass directly (avoids /files endpoint race condition).
|
|
175
252
|
*/
|
|
176
|
-
const { session_id, _injected_files } =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
253
|
+
const { session_id, _injected_files, _runtime_session_hint } =
|
|
254
|
+
(config.toolCall ?? {}) as {
|
|
255
|
+
session_id?: string;
|
|
256
|
+
_injected_files?: t.CodeEnvFile[];
|
|
257
|
+
_runtime_session_hint?: string;
|
|
258
|
+
};
|
|
180
259
|
|
|
181
260
|
const postData: Record<string, unknown> = {
|
|
182
261
|
lang,
|
|
@@ -185,6 +264,16 @@ function createCodeExecutionTool(
|
|
|
185
264
|
...executionParams,
|
|
186
265
|
};
|
|
187
266
|
|
|
267
|
+
/* Stateful sessions: forward the hint so the Code API can route this
|
|
268
|
+
* execution to a warm per-session runtime. Additive — stateless
|
|
269
|
+
* servers ignore the unknown field. */
|
|
270
|
+
if (
|
|
271
|
+
typeof _runtime_session_hint === 'string' &&
|
|
272
|
+
_runtime_session_hint !== ''
|
|
273
|
+
) {
|
|
274
|
+
postData.runtime_session_hint = _runtime_session_hint;
|
|
275
|
+
}
|
|
276
|
+
|
|
188
277
|
/* File injection: `_injected_files` from ToolNode (set when host
|
|
189
278
|
* primes a CodeSessionContext) or `params.files` from tool
|
|
190
279
|
* factory (set by hosts that pre-resolve at construction time).
|
|
@@ -242,12 +331,27 @@ function createCodeExecutionTool(
|
|
|
242
331
|
code
|
|
243
332
|
);
|
|
244
333
|
const hasFiles = result.files != null && result.files.length > 0;
|
|
334
|
+
/* Echo the durable runtime session (stateful backends only) so hosts
|
|
335
|
+
* can surface a "session active / was reset" signal later. Additive:
|
|
336
|
+
* absent on stateless servers. */
|
|
337
|
+
const runtimeEcho =
|
|
338
|
+
result.runtime_session_id != null
|
|
339
|
+
? {
|
|
340
|
+
runtime_session_id: result.runtime_session_id,
|
|
341
|
+
runtime_status: result.runtime_status,
|
|
342
|
+
}
|
|
343
|
+
: {};
|
|
245
344
|
return [
|
|
246
345
|
appendCodeSessionFileSummary(outputWithReminder, result.files),
|
|
247
346
|
(hasFiles
|
|
248
|
-
? {
|
|
347
|
+
? {
|
|
348
|
+
session_id: result.session_id,
|
|
349
|
+
files: result.files,
|
|
350
|
+
...runtimeEcho,
|
|
351
|
+
}
|
|
249
352
|
: {
|
|
250
353
|
session_id: result.session_id,
|
|
354
|
+
...runtimeEcho,
|
|
251
355
|
}) satisfies t.CodeExecutionArtifact,
|
|
252
356
|
];
|
|
253
357
|
} catch (error) {
|
|
@@ -260,8 +364,8 @@ function createCodeExecutionTool(
|
|
|
260
364
|
},
|
|
261
365
|
{
|
|
262
366
|
name: CodeExecutionToolName,
|
|
263
|
-
description:
|
|
264
|
-
schema:
|
|
367
|
+
description: buildCodeExecutionToolDescription(params ?? undefined),
|
|
368
|
+
schema: buildCodeExecutionToolSchema(params ?? undefined),
|
|
265
369
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
266
370
|
}
|
|
267
371
|
);
|
|
@@ -830,6 +830,12 @@ export function formatCompletedResponse(
|
|
|
830
830
|
{
|
|
831
831
|
session_id: response.session_id,
|
|
832
832
|
files: response.files,
|
|
833
|
+
...(response.runtime_session_id != null
|
|
834
|
+
? {
|
|
835
|
+
runtime_session_id: response.runtime_session_id,
|
|
836
|
+
runtime_status: response.runtime_status,
|
|
837
|
+
}
|
|
838
|
+
: {}),
|
|
833
839
|
} satisfies t.ProgrammaticExecutionArtifact,
|
|
834
840
|
];
|
|
835
841
|
}
|
|
@@ -878,8 +884,15 @@ export function createProgrammaticToolCallingTool(
|
|
|
878
884
|
Partial<t.ProgrammaticCache> & {
|
|
879
885
|
session_id?: string;
|
|
880
886
|
_injected_files?: t.CodeEnvFile[];
|
|
887
|
+
_runtime_session_hint?: string;
|
|
881
888
|
};
|
|
882
|
-
const {
|
|
889
|
+
const {
|
|
890
|
+
toolMap,
|
|
891
|
+
toolDefs,
|
|
892
|
+
session_id,
|
|
893
|
+
_injected_files,
|
|
894
|
+
_runtime_session_hint,
|
|
895
|
+
} = toolCall;
|
|
883
896
|
|
|
884
897
|
if (toolMap == null || toolMap.size === 0) {
|
|
885
898
|
throw new Error(
|
|
@@ -928,6 +941,16 @@ export function createProgrammaticToolCallingTool(
|
|
|
928
941
|
);
|
|
929
942
|
}
|
|
930
943
|
|
|
944
|
+
/* Stateful sessions: hint rides the INITIAL request only; the server
|
|
945
|
+
* binds continuation round-trips to the same runtime via the
|
|
946
|
+
* continuation_token. Additive — ignored by stateless servers. PTC
|
|
947
|
+
* keeps its stateless prompt in v1; only the wire hint plumbs here. */
|
|
948
|
+
const runtimeSessionHint =
|
|
949
|
+
typeof _runtime_session_hint === 'string' &&
|
|
950
|
+
_runtime_session_hint !== ''
|
|
951
|
+
? _runtime_session_hint
|
|
952
|
+
: undefined;
|
|
953
|
+
|
|
931
954
|
let response = await makeRequest(
|
|
932
955
|
EXEC_ENDPOINT,
|
|
933
956
|
{
|
|
@@ -936,6 +959,9 @@ export function createProgrammaticToolCallingTool(
|
|
|
936
959
|
session_id,
|
|
937
960
|
timeout,
|
|
938
961
|
...(files && files.length > 0 ? { files } : {}),
|
|
962
|
+
...(runtimeSessionHint != null
|
|
963
|
+
? { runtime_session_hint: runtimeSessionHint }
|
|
964
|
+
: {}),
|
|
939
965
|
},
|
|
940
966
|
proxy,
|
|
941
967
|
initParams.authHeaders
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -37,6 +37,11 @@ import type {
|
|
|
37
37
|
PostToolBatchEntry,
|
|
38
38
|
} from '@/hooks';
|
|
39
39
|
import type * as t from '@/types';
|
|
40
|
+
import {
|
|
41
|
+
buildToolExecutionRequestPlan,
|
|
42
|
+
resolveRuntimeSessionHint,
|
|
43
|
+
recordArgsEqual,
|
|
44
|
+
} from '@/tools/eagerEventExecution';
|
|
40
45
|
import {
|
|
41
46
|
resolveLangfuseRuntimeScope,
|
|
42
47
|
withLangfuseRuntimeScope,
|
|
@@ -45,10 +50,6 @@ import {
|
|
|
45
50
|
buildReferenceKey,
|
|
46
51
|
ToolOutputReferenceRegistry,
|
|
47
52
|
} from '@/tools/toolOutputReferences';
|
|
48
|
-
import {
|
|
49
|
-
buildToolExecutionRequestPlan,
|
|
50
|
-
recordArgsEqual,
|
|
51
|
-
} from '@/tools/eagerEventExecution';
|
|
52
53
|
import {
|
|
53
54
|
calculateMaxToolResultChars,
|
|
54
55
|
truncateToolResultContent,
|
|
@@ -997,6 +998,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
997
998
|
);
|
|
998
999
|
}
|
|
999
1000
|
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Stateful runtime session hint — orthogonal to the transient
|
|
1004
|
+
* exec-session above, and injected independently (a first call has a
|
|
1005
|
+
* hint but no exec session yet). Explicit host hint wins; otherwise
|
|
1006
|
+
* fall back to the conversation's thread_id.
|
|
1007
|
+
*/
|
|
1008
|
+
const runtimeSessionHint = this.resolveRuntimeSessionHint(config);
|
|
1009
|
+
if (runtimeSessionHint != null) {
|
|
1010
|
+
invokeParams = {
|
|
1011
|
+
...invokeParams,
|
|
1012
|
+
_runtime_session_hint: runtimeSessionHint,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1000
1015
|
}
|
|
1001
1016
|
|
|
1002
1017
|
/**
|
|
@@ -1783,6 +1798,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1783
1798
|
);
|
|
1784
1799
|
}
|
|
1785
1800
|
|
|
1801
|
+
/** Delegates to the shared resolver so the direct and event-driven planning
|
|
1802
|
+
* paths derive the runtime session hint identically. */
|
|
1803
|
+
private resolveRuntimeSessionHint(
|
|
1804
|
+
config: RunnableConfig
|
|
1805
|
+
): string | undefined {
|
|
1806
|
+
return resolveRuntimeSessionHint(
|
|
1807
|
+
this.toolExecution,
|
|
1808
|
+
config.configurable?.thread_id as string | undefined
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1786
1812
|
private storeCodeSessionFromResults(
|
|
1787
1813
|
results: t.ToolExecuteResult[],
|
|
1788
1814
|
requestMap: Map<string, t.ToolCallRequest>
|
|
@@ -2492,12 +2518,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2492
2518
|
entry.call.name === Constants.READ_FILE
|
|
2493
2519
|
? this.getCodeSessionContext()
|
|
2494
2520
|
: undefined;
|
|
2521
|
+
const runtimeSessionHint = this.participatesInCodeSession(
|
|
2522
|
+
entry.call.name
|
|
2523
|
+
)
|
|
2524
|
+
? this.resolveRuntimeSessionHint(config)
|
|
2525
|
+
: undefined;
|
|
2495
2526
|
return {
|
|
2496
2527
|
id: entry.call.id,
|
|
2497
2528
|
name: entry.call.name,
|
|
2498
2529
|
args: entry.args,
|
|
2499
2530
|
stepId: entry.stepId,
|
|
2500
2531
|
codeSessionContext,
|
|
2532
|
+
runtimeSessionHint,
|
|
2501
2533
|
};
|
|
2502
2534
|
}),
|
|
2503
2535
|
usageCount: this.toolUsageCount,
|
|
@@ -2,8 +2,10 @@ import { describe, it, expect } from '@jest/globals';
|
|
|
2
2
|
import {
|
|
3
3
|
BashExecutionToolDescription,
|
|
4
4
|
BashToolOutputReferencesGuide,
|
|
5
|
+
StatefulBashExecutionToolDescription,
|
|
5
6
|
buildBashExecutionToolDescription,
|
|
6
7
|
} from '../BashExecutor';
|
|
8
|
+
import { CODE_ARTIFACT_PATH_GUIDANCE } from '../CodeExecutor';
|
|
7
9
|
|
|
8
10
|
describe('buildBashExecutionToolDescription', () => {
|
|
9
11
|
it('returns the base description by default', () => {
|
|
@@ -55,4 +57,41 @@ describe('buildBashExecutionToolDescription', () => {
|
|
|
55
57
|
});
|
|
56
58
|
expect(composed.includes(`${BashExecutionToolDescription}\n\n`)).toBe(true);
|
|
57
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('stateful variant', () => {
|
|
62
|
+
it('selects the stateful description when statefulSessions is on', () => {
|
|
63
|
+
expect(
|
|
64
|
+
buildBashExecutionToolDescription({ statefulSessions: true })
|
|
65
|
+
).toBe(StatefulBashExecutionToolDescription);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('hedges: usually-persists but may-reset, and only /mnt/data is durable', () => {
|
|
69
|
+
const d = StatefulBashExecutionToolDescription;
|
|
70
|
+
expect(d).toContain('usually');
|
|
71
|
+
expect(d).toContain('may be reset');
|
|
72
|
+
expect(d).toContain('Only /mnt/data is durable');
|
|
73
|
+
/* filesystem-tier, not shell-variable-tier */
|
|
74
|
+
expect(d).toContain('do not rely on shell variables');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('keeps the artifact-path guidance in both variants', () => {
|
|
78
|
+
expect(BashExecutionToolDescription).toContain(
|
|
79
|
+
CODE_ARTIFACT_PATH_GUIDANCE
|
|
80
|
+
);
|
|
81
|
+
expect(StatefulBashExecutionToolDescription).toContain(
|
|
82
|
+
CODE_ARTIFACT_PATH_GUIDANCE
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('still composes with the output-references guide', () => {
|
|
87
|
+
const composed = buildBashExecutionToolDescription({
|
|
88
|
+
statefulSessions: true,
|
|
89
|
+
enableToolOutputReferences: true,
|
|
90
|
+
});
|
|
91
|
+
expect(composed.startsWith(StatefulBashExecutionToolDescription)).toBe(
|
|
92
|
+
true
|
|
93
|
+
);
|
|
94
|
+
expect(composed).toContain(BashToolOutputReferencesGuide);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
58
97
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, jest, afterEach } from '@jest/globals';
|
|
2
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
3
|
+
|
|
4
|
+
/* CodeExecutor imports the default export of node-fetch; capture POST bodies. */
|
|
5
|
+
const fetchCalls: Array<Record<string, unknown>> = [];
|
|
6
|
+
jest.mock('node-fetch', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
default: async (_url: string, init?: { body?: string }) => {
|
|
9
|
+
fetchCalls.push(JSON.parse(init?.body ?? '{}'));
|
|
10
|
+
return {
|
|
11
|
+
ok: true,
|
|
12
|
+
json: async () => ({
|
|
13
|
+
session_id: 's1',
|
|
14
|
+
stdout: 'ok',
|
|
15
|
+
stderr: '',
|
|
16
|
+
files: [],
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
CODE_ARTIFACT_PATH_GUIDANCE,
|
|
24
|
+
CodeExecutionToolDescription,
|
|
25
|
+
StatefulCodeExecutionToolDescription,
|
|
26
|
+
buildCodeExecutionToolDescription,
|
|
27
|
+
buildCodeExecutionToolSchema,
|
|
28
|
+
createCodeExecutionTool,
|
|
29
|
+
} from '../CodeExecutor';
|
|
30
|
+
|
|
31
|
+
describe('CodeExecutor stateful description', () => {
|
|
32
|
+
it('selects the stateful description only when statefulSessions is on', () => {
|
|
33
|
+
expect(buildCodeExecutionToolDescription()).toBe(
|
|
34
|
+
CodeExecutionToolDescription
|
|
35
|
+
);
|
|
36
|
+
expect(buildCodeExecutionToolDescription({ statefulSessions: false })).toBe(
|
|
37
|
+
CodeExecutionToolDescription
|
|
38
|
+
);
|
|
39
|
+
expect(buildCodeExecutionToolDescription({ statefulSessions: true })).toBe(
|
|
40
|
+
StatefulCodeExecutionToolDescription
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('hedges the stateful wording and keeps /mnt/data as the durable store', () => {
|
|
45
|
+
const d = StatefulCodeExecutionToolDescription;
|
|
46
|
+
expect(d).toContain('usually share one runtime');
|
|
47
|
+
expect(d).toContain('may be reset at any time');
|
|
48
|
+
expect(d).toContain('MUST be written to /mnt/data');
|
|
49
|
+
expect(d).toContain(CODE_ARTIFACT_PATH_GUIDANCE);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('adjusts the code-param note per mode', () => {
|
|
53
|
+
const stateless =
|
|
54
|
+
buildCodeExecutionToolSchema().properties.code.description;
|
|
55
|
+
const stateful = buildCodeExecutionToolSchema({ statefulSessions: true })
|
|
56
|
+
.properties.code.description;
|
|
57
|
+
expect(stateless).toContain('variables and imports don\'t persist');
|
|
58
|
+
expect(stateful).toContain('typically still defined');
|
|
59
|
+
expect(stateful).toContain('may reset between calls');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('CodeExecutor runtime_session_hint wire field', () => {
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fetchCalls.length = 0;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function run(
|
|
69
|
+
toolCall: Record<string, unknown>,
|
|
70
|
+
params: Record<string, unknown> = {}
|
|
71
|
+
): Promise<Record<string, unknown>[]> {
|
|
72
|
+
const codeTool = createCodeExecutionTool(params);
|
|
73
|
+
await codeTool.invoke({ lang: 'py', code: 'print(1)' }, {
|
|
74
|
+
toolCall,
|
|
75
|
+
} as unknown as RunnableConfig);
|
|
76
|
+
return fetchCalls;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it('sends runtime_session_hint when ToolNode injected it', async () => {
|
|
80
|
+
const calls = await run({ _runtime_session_hint: 'conv-42' });
|
|
81
|
+
expect(calls[0].runtime_session_hint).toBe('conv-42');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('omits runtime_session_hint when not injected', async () => {
|
|
85
|
+
const calls = await run({});
|
|
86
|
+
expect('runtime_session_hint' in calls[0]).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not leak the statefulSessions factory flag onto the wire', async () => {
|
|
90
|
+
const calls = await run({}, { statefulSessions: true });
|
|
91
|
+
expect('statefulSessions' in calls[0]).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('strips a model-supplied runtime_session_hint from the raw args', async () => {
|
|
95
|
+
const codeTool = createCodeExecutionTool({});
|
|
96
|
+
await codeTool.invoke(
|
|
97
|
+
{ lang: 'py', code: 'print(1)', runtime_session_hint: 'model-picked' },
|
|
98
|
+
{ toolCall: {} } as unknown as RunnableConfig
|
|
99
|
+
);
|
|
100
|
+
expect('runtime_session_hint' in fetchCalls[0]).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('lets only the ToolNode-injected hint reach the wire, ignoring model args', async () => {
|
|
104
|
+
const codeTool = createCodeExecutionTool({});
|
|
105
|
+
await codeTool.invoke(
|
|
106
|
+
{ lang: 'py', code: 'print(1)', runtime_session_hint: 'model-picked' },
|
|
107
|
+
{
|
|
108
|
+
toolCall: { _runtime_session_hint: 'host-conv' },
|
|
109
|
+
} as unknown as RunnableConfig
|
|
110
|
+
);
|
|
111
|
+
expect(fetchCalls[0].runtime_session_hint).toBe('host-conv');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -1263,4 +1263,90 @@ describe('ToolNode code execution session management', () => {
|
|
|
1263
1263
|
);
|
|
1264
1264
|
});
|
|
1265
1265
|
});
|
|
1266
|
+
|
|
1267
|
+
describe('stateful runtime session hint injection', () => {
|
|
1268
|
+
it('injects the explicit runtimeSessionHint when statefulSessions is on', async () => {
|
|
1269
|
+
const capturedConfigs: Record<string, unknown>[] = [];
|
|
1270
|
+
const mockTool = createMockCodeTool({ capturedConfigs });
|
|
1271
|
+
const toolNode = new ToolNode({
|
|
1272
|
+
tools: [mockTool],
|
|
1273
|
+
sessions: new Map(),
|
|
1274
|
+
toolExecution: {
|
|
1275
|
+
engine: 'sandbox',
|
|
1276
|
+
sandbox: {
|
|
1277
|
+
statefulSessions: true,
|
|
1278
|
+
runtimeSessionHint: 'conv-explicit',
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
await toolNode.invoke({ messages: [createAIMessageWithCodeCall('c1')] });
|
|
1284
|
+
|
|
1285
|
+
expect(capturedConfigs[0]._runtime_session_hint).toBe('conv-explicit');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('falls back to configurable.thread_id when no explicit hint is given', async () => {
|
|
1289
|
+
const capturedConfigs: Record<string, unknown>[] = [];
|
|
1290
|
+
const mockTool = createMockCodeTool({ capturedConfigs });
|
|
1291
|
+
const toolNode = new ToolNode({
|
|
1292
|
+
tools: [mockTool],
|
|
1293
|
+
sessions: new Map(),
|
|
1294
|
+
toolExecution: {
|
|
1295
|
+
engine: 'sandbox',
|
|
1296
|
+
sandbox: { statefulSessions: true },
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
await toolNode.invoke(
|
|
1301
|
+
{ messages: [createAIMessageWithCodeCall('c1')] },
|
|
1302
|
+
{ configurable: { thread_id: 'thread-abc' } }
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
expect(capturedConfigs[0]._runtime_session_hint).toBe('thread-abc');
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it('does not inject a hint when statefulSessions is off', async () => {
|
|
1309
|
+
const capturedConfigs: Record<string, unknown>[] = [];
|
|
1310
|
+
const mockTool = createMockCodeTool({ capturedConfigs });
|
|
1311
|
+
const toolNode = new ToolNode({
|
|
1312
|
+
tools: [mockTool],
|
|
1313
|
+
sessions: new Map(),
|
|
1314
|
+
toolExecution: {
|
|
1315
|
+
engine: 'sandbox',
|
|
1316
|
+
sandbox: { statefulSessions: false },
|
|
1317
|
+
},
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
await toolNode.invoke(
|
|
1321
|
+
{ messages: [createAIMessageWithCodeCall('c1')] },
|
|
1322
|
+
{ configurable: { thread_id: 'thread-abc' } }
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1325
|
+
expect(capturedConfigs[0]._runtime_session_hint).toBeUndefined();
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it('injects the hint independently of an existing exec session', async () => {
|
|
1329
|
+
const capturedConfigs: Record<string, unknown>[] = [];
|
|
1330
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
1331
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
1332
|
+
session_id: 'exec-1',
|
|
1333
|
+
files: [],
|
|
1334
|
+
lastUpdated: 0,
|
|
1335
|
+
});
|
|
1336
|
+
const mockTool = createMockCodeTool({ capturedConfigs });
|
|
1337
|
+
const toolNode = new ToolNode({
|
|
1338
|
+
tools: [mockTool],
|
|
1339
|
+
sessions,
|
|
1340
|
+
toolExecution: {
|
|
1341
|
+
engine: 'sandbox',
|
|
1342
|
+
sandbox: { statefulSessions: true, runtimeSessionHint: 'conv-1' },
|
|
1343
|
+
},
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
await toolNode.invoke({ messages: [createAIMessageWithCodeCall('c1')] });
|
|
1347
|
+
|
|
1348
|
+
expect(capturedConfigs[0].session_id).toBe('exec-1');
|
|
1349
|
+
expect(capturedConfigs[0]._runtime_session_hint).toBe('conv-1');
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1266
1352
|
});
|
|
@@ -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
|
+
});
|