@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,194 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
4
|
+
import { MemorySaver, Command } from '@langchain/langgraph';
|
|
5
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
6
|
+
import type * as t from '@/types';
|
|
7
|
+
import { GraphEvents, Providers } from '@/common';
|
|
8
|
+
import { FakeChatModel } from '@/llm/fake';
|
|
9
|
+
import { askUserQuestion } from '@/hitl';
|
|
10
|
+
import { StandardGraph } from '@/graphs';
|
|
11
|
+
import { Run } from '@/run';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Regression: a DIRECT (graphTools) tool that fails schema validation during
|
|
15
|
+
* the RESUME pass of an interrupted batch.
|
|
16
|
+
*
|
|
17
|
+
* LangGraph re-executes the whole interrupted batch on resume, and a
|
|
18
|
+
* fast-failing sibling (e.g. a zod reject) errors BEFORE the rebuilt graph
|
|
19
|
+
* has registered run steps for the replayed calls. `handleToolCallErrorStatic`
|
|
20
|
+
* used to throw (`No config provided`) in that state, which surfaced as a
|
|
21
|
+
* scary `Error in errorHandler` log on every such resume — the error itself
|
|
22
|
+
* was already handled (the model receives the error ToolMessage either way).
|
|
23
|
+
* It now reports `false` (nothing dispatched) so the ToolNode can decide, and
|
|
24
|
+
* the client-facing contract stays: the error completion event dispatches
|
|
25
|
+
* exactly ONCE, on the pass where the call's run step is live (the first
|
|
26
|
+
* pass), never zero times and never twice.
|
|
27
|
+
*/
|
|
28
|
+
const askTool = tool(
|
|
29
|
+
async (input) => {
|
|
30
|
+
const { answer } = askUserQuestion(input as { question: string });
|
|
31
|
+
return answer;
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'ask_user_question',
|
|
35
|
+
description:
|
|
36
|
+
'Ask the user a clarifying question and wait for their answer.',
|
|
37
|
+
schema: z.object({ question: z.string() }),
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const strictTool = tool(
|
|
42
|
+
async ({ items }: { items: string[] }) => `got ${items.length}`,
|
|
43
|
+
{
|
|
44
|
+
name: 'strict_tool',
|
|
45
|
+
// Array cap mirrors the field repro: an ask_user_question sibling whose
|
|
46
|
+
// `options` array exceeded the 12-option schema cap.
|
|
47
|
+
description: 'A tool with a strict array-cap schema.',
|
|
48
|
+
schema: z.object({ items: z.array(z.string()).max(1) }),
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
describe('Direct tool schema error across interrupt/resume', () => {
|
|
53
|
+
jest.setTimeout(30000);
|
|
54
|
+
|
|
55
|
+
const threadConfig: Partial<RunnableConfig> & {
|
|
56
|
+
version: 'v1' | 'v2';
|
|
57
|
+
streamMode: string;
|
|
58
|
+
} = {
|
|
59
|
+
configurable: { thread_id: 'tool-error-resume-thread' },
|
|
60
|
+
streamMode: 'values',
|
|
61
|
+
version: 'v2' as const,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const completedToolEvents: Array<{ name?: string; output?: string }> = [];
|
|
65
|
+
const customHandlers = {
|
|
66
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
67
|
+
handle: (_event: GraphEvents, data: t.StreamEventData): void => {
|
|
68
|
+
const result = (data as { result?: t.ToolCompleteEvent }).result;
|
|
69
|
+
if (result?.type === 'tool_call') {
|
|
70
|
+
completedToolEvents.push({
|
|
71
|
+
name: result.tool_call.name,
|
|
72
|
+
output: result.tool_call.output,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
async function buildRun(
|
|
80
|
+
saver: MemorySaver,
|
|
81
|
+
runId: string
|
|
82
|
+
): Promise<Run<t.IState>> {
|
|
83
|
+
const run = await Run.create<t.IState>({
|
|
84
|
+
runId,
|
|
85
|
+
graphConfig: {
|
|
86
|
+
type: 'standard',
|
|
87
|
+
agents: [
|
|
88
|
+
{
|
|
89
|
+
agentId: 'resume-error-agent',
|
|
90
|
+
provider: Providers.OPENAI,
|
|
91
|
+
clientOptions: {
|
|
92
|
+
model: 'gpt-4o-mini',
|
|
93
|
+
streaming: true,
|
|
94
|
+
streamUsage: false,
|
|
95
|
+
},
|
|
96
|
+
instructions: 'You are a helpful assistant.',
|
|
97
|
+
maxContextTokens: 8000,
|
|
98
|
+
/** Non-empty toolDefinitions flips the ToolNode to event dispatch —
|
|
99
|
+
* the production shape — while graphTools ride the direct path. */
|
|
100
|
+
toolDefinitions: [
|
|
101
|
+
{ name: 'dummy_event_tool', description: 'host tool' },
|
|
102
|
+
],
|
|
103
|
+
graphTools: [askTool, strictTool],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
compileOptions: { checkpointer: saver },
|
|
107
|
+
},
|
|
108
|
+
returnContent: true,
|
|
109
|
+
skipCleanup: true,
|
|
110
|
+
customHandlers,
|
|
111
|
+
eagerEventToolExecution: {
|
|
112
|
+
enabled: true,
|
|
113
|
+
excludeToolNames: ['ask_user_question', 'strict_tool'],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
run.Graph!.overrideModel = new FakeChatModel({
|
|
117
|
+
responses: ['calling tools', 'done after resume'],
|
|
118
|
+
toolCalls: [
|
|
119
|
+
{
|
|
120
|
+
name: 'ask_user_question',
|
|
121
|
+
args: { question: 'pick one' },
|
|
122
|
+
id: 'call_ask_1',
|
|
123
|
+
type: 'tool_call',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'strict_tool',
|
|
127
|
+
args: { items: ['a', 'b', 'c'] },
|
|
128
|
+
id: 'call_strict_1',
|
|
129
|
+
type: 'tool_call',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
return run;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
test('error completion dispatches exactly once; resume-pass handler reports false instead of throwing', async () => {
|
|
137
|
+
const saver = new MemorySaver();
|
|
138
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
139
|
+
const staticSpy = jest.spyOn(StandardGraph, 'handleToolCallErrorStatic');
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
/** Pass 1: the strict tool rejects its args while the ask tool
|
|
143
|
+
* interrupts — the run pauses AND the schema error completes. */
|
|
144
|
+
const run1 = await buildRun(saver, 'run-pass-1');
|
|
145
|
+
await run1.processStream(
|
|
146
|
+
{ messages: [new HumanMessage('go')] },
|
|
147
|
+
threadConfig
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const strictCompletions = completedToolEvents.filter(
|
|
151
|
+
(e) =>
|
|
152
|
+
e.name === 'strict_tool' &&
|
|
153
|
+
e.output?.includes('Error processing tool') === true
|
|
154
|
+
);
|
|
155
|
+
expect(strictCompletions).toHaveLength(1);
|
|
156
|
+
// Stable LangChain wrapper text (version-independent), proving the
|
|
157
|
+
// completion carried the schema-validation failure.
|
|
158
|
+
expect(strictCompletions[0].output).toContain(
|
|
159
|
+
'did not match expected schema'
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
/** Pass 2: fresh instance (host restart shape), resume with the
|
|
163
|
+
* answer. The batch re-executes; the strict tool fails again before
|
|
164
|
+
* any run step is registered on the rebuilt graph. */
|
|
165
|
+
const run2 = await buildRun(saver, 'run-pass-2');
|
|
166
|
+
await run2.processStream(
|
|
167
|
+
new Command({ resume: { answer: 'blue' } }) as unknown as t.IState,
|
|
168
|
+
threadConfig
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
/** The handler reported "not dispatched" (no run step yet) rather than
|
|
172
|
+
* throwing — so nothing was logged and no duplicate completion event
|
|
173
|
+
* reached the client. */
|
|
174
|
+
const resumeResults = await Promise.all(
|
|
175
|
+
staticSpy.mock.results.map((r) => r.value as Promise<boolean>)
|
|
176
|
+
);
|
|
177
|
+
expect(resumeResults).toContain(false);
|
|
178
|
+
const handlerFailures = errorSpy.mock.calls.filter((args) =>
|
|
179
|
+
String(args[0]).includes('Error in errorHandler')
|
|
180
|
+
);
|
|
181
|
+
expect(handlerFailures).toHaveLength(0);
|
|
182
|
+
expect(
|
|
183
|
+
completedToolEvents.filter(
|
|
184
|
+
(e) =>
|
|
185
|
+
e.name === 'strict_tool' &&
|
|
186
|
+
e.output?.includes('Error processing tool') === true
|
|
187
|
+
)
|
|
188
|
+
).toHaveLength(1);
|
|
189
|
+
} finally {
|
|
190
|
+
errorSpy.mockRestore();
|
|
191
|
+
staticSpy.mockRestore();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
package/src/stream.ts
CHANGED
|
@@ -142,7 +142,18 @@ function isEagerExecutionExcludedTool(
|
|
|
142
142
|
// side-effecting: never prestart it speculatively (a revised/superseded turn
|
|
143
143
|
// would leave the write applied). Implies exclusion without the host having
|
|
144
144
|
// to also list the name in excludeToolNames.
|
|
145
|
-
|
|
145
|
+
if (graph.codeSessionToolNames?.includes(name) === true) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
// With stateful sessions on, execute_code/bash run against a DURABLE warm
|
|
149
|
+
// runtime. Speculative prestart there is unsafe: if the model revises the
|
|
150
|
+
// args, ToolNode discards the eager result but the mutation has already
|
|
151
|
+
// landed in the session workspace, corrupting later runs. Stateless mode
|
|
152
|
+
// uses a throwaway VM per call, so eager prestart stays safe there.
|
|
153
|
+
return (
|
|
154
|
+
graph.toolExecution?.sandbox?.statefulSessions === true &&
|
|
155
|
+
CODE_EXECUTION_TOOLS.has(name)
|
|
156
|
+
);
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
function isDirectGraphTool(
|
|
@@ -668,6 +679,11 @@ function createEagerToolExecutionPlan(args: {
|
|
|
668
679
|
return undefined;
|
|
669
680
|
}
|
|
670
681
|
|
|
682
|
+
/* No runtimeSessionHint here on purpose: the eager path is speculative, and
|
|
683
|
+
* code-session tools (the only ones that carry a hint) are excluded from
|
|
684
|
+
* eager prestart entirely when stateful sessions are on — see
|
|
685
|
+
* isEagerExecutionExcludedTool. The durable runtime is only ever touched by
|
|
686
|
+
* the committed ToolNode path. */
|
|
671
687
|
const plan = buildToolExecutionRequestPlan({
|
|
672
688
|
toolCalls: candidateToolCalls.map((toolCall) => ({
|
|
673
689
|
id: toolCall.id,
|
|
@@ -57,6 +57,29 @@ Usage:
|
|
|
57
57
|
- NEVER use this tool to execute malicious commands.
|
|
58
58
|
`.trim();
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Bash statefulness is filesystem-tier: on a warm session the machine (files
|
|
62
|
+
* including /tmp, installed packages, background processes) persists between
|
|
63
|
+
* calls, but each call may start a fresh shell — so shell variables and cwd
|
|
64
|
+
* are NOT reliable, and the machine can be reset at any time. Only /mnt/data
|
|
65
|
+
* is durable.
|
|
66
|
+
*/
|
|
67
|
+
export const STATEFUL_BASH_NOTE =
|
|
68
|
+
'Session state (best-effort): commands in this conversation usually run on the same machine, so files (including /tmp), installed packages, and running background processes from earlier calls typically persist. Each call may still start a fresh shell — do not rely on shell variables or the working directory carrying over — and the machine may be reset at any time. Only /mnt/data is durable.';
|
|
69
|
+
|
|
70
|
+
export const StatefulBashExecutionToolDescription = `
|
|
71
|
+
Runs bash commands and returns stdout/stderr output from a session-based execution environment, similar to a long-running machine.
|
|
72
|
+
|
|
73
|
+
${STATEFUL_BASH_NOTE}
|
|
74
|
+
|
|
75
|
+
Usage:
|
|
76
|
+
- No network access available.
|
|
77
|
+
- Generated files are automatically delivered; **DO NOT** provide download links.
|
|
78
|
+
- ${CODE_ARTIFACT_PATH_GUIDANCE}
|
|
79
|
+
- ${BASH_SHELL_GUIDANCE}
|
|
80
|
+
- NEVER use this tool to execute malicious commands.
|
|
81
|
+
`.trim();
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* Supplemental prompt documenting the tool-output reference feature.
|
|
62
85
|
*
|
|
@@ -87,11 +110,45 @@ Referencing previous tool outputs:
|
|
|
87
110
|
*/
|
|
88
111
|
export function buildBashExecutionToolDescription(options?: {
|
|
89
112
|
enableToolOutputReferences?: boolean;
|
|
113
|
+
statefulSessions?: boolean;
|
|
90
114
|
}): string {
|
|
115
|
+
const base =
|
|
116
|
+
options?.statefulSessions === true
|
|
117
|
+
? StatefulBashExecutionToolDescription
|
|
118
|
+
: BashExecutionToolDescription;
|
|
91
119
|
if (options?.enableToolOutputReferences === true) {
|
|
92
|
-
return `${
|
|
120
|
+
return `${base}\n\n${BashToolOutputReferencesGuide}`;
|
|
93
121
|
}
|
|
94
|
-
return
|
|
122
|
+
return base;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const STATELESS_BASH_PARAM_NOTE =
|
|
126
|
+
'The environment is stateless; variables and state don\'t persist between executions.';
|
|
127
|
+
const STATEFUL_BASH_PARAM_NOTE =
|
|
128
|
+
'Files, installed packages, and background processes usually persist between calls, but each call may start a fresh shell (do not rely on shell variables or cwd) and the machine may reset. Only /mnt/data is durable.';
|
|
129
|
+
|
|
130
|
+
export function buildBashExecutionToolSchema(opts?: {
|
|
131
|
+
statefulSessions?: boolean;
|
|
132
|
+
}): typeof BashExecutionToolSchema {
|
|
133
|
+
const note =
|
|
134
|
+
opts?.statefulSessions === true
|
|
135
|
+
? STATEFUL_BASH_PARAM_NOTE
|
|
136
|
+
: STATELESS_BASH_PARAM_NOTE;
|
|
137
|
+
const commandDescription =
|
|
138
|
+
BashExecutionToolSchema.properties.command.description.replace(
|
|
139
|
+
STATELESS_BASH_PARAM_NOTE,
|
|
140
|
+
note
|
|
141
|
+
);
|
|
142
|
+
return {
|
|
143
|
+
...BashExecutionToolSchema,
|
|
144
|
+
properties: {
|
|
145
|
+
...BashExecutionToolSchema.properties,
|
|
146
|
+
command: {
|
|
147
|
+
...BashExecutionToolSchema.properties.command,
|
|
148
|
+
description: commandDescription,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
} as typeof BashExecutionToolSchema;
|
|
95
152
|
}
|
|
96
153
|
|
|
97
154
|
export const BashExecutionToolName = Constants.BASH_TOOL;
|
|
@@ -117,15 +174,32 @@ function createBashExecutionTool(
|
|
|
117
174
|
): DynamicStructuredTool {
|
|
118
175
|
return tool(
|
|
119
176
|
async (rawInput, config) => {
|
|
120
|
-
|
|
121
|
-
const {
|
|
177
|
+
/* `statefulSessions` is prompt-only — keep it out of the wire body. */
|
|
178
|
+
const {
|
|
179
|
+
authHeaders,
|
|
180
|
+
statefulSessions: _statefulSessions,
|
|
181
|
+
...executionParams
|
|
182
|
+
} = params ?? {};
|
|
183
|
+
void _statefulSessions;
|
|
184
|
+
/* Drop any model-supplied `runtime_session_hint` from the raw args: the
|
|
185
|
+
* hint must only come from ToolNode's injected `_runtime_session_hint`
|
|
186
|
+
* (below), never from the tool call itself. */
|
|
187
|
+
const {
|
|
188
|
+
command,
|
|
189
|
+
runtime_session_hint: _ignoredModelHint,
|
|
190
|
+
...rest
|
|
191
|
+
} = rawInput as {
|
|
122
192
|
command: string;
|
|
193
|
+
runtime_session_hint?: unknown;
|
|
123
194
|
args?: string[];
|
|
124
195
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
196
|
+
void _ignoredModelHint;
|
|
197
|
+
const { session_id, _injected_files, _runtime_session_hint } =
|
|
198
|
+
(config.toolCall ?? {}) as {
|
|
199
|
+
session_id?: string;
|
|
200
|
+
_injected_files?: t.CodeEnvFile[];
|
|
201
|
+
_runtime_session_hint?: string;
|
|
202
|
+
};
|
|
129
203
|
|
|
130
204
|
const postData: Record<string, unknown> = {
|
|
131
205
|
lang: 'bash',
|
|
@@ -134,6 +208,13 @@ function createBashExecutionTool(
|
|
|
134
208
|
...executionParams,
|
|
135
209
|
};
|
|
136
210
|
|
|
211
|
+
if (
|
|
212
|
+
typeof _runtime_session_hint === 'string' &&
|
|
213
|
+
_runtime_session_hint !== ''
|
|
214
|
+
) {
|
|
215
|
+
postData.runtime_session_hint = _runtime_session_hint;
|
|
216
|
+
}
|
|
217
|
+
|
|
137
218
|
/* See `CodeExecutor.ts` for the rationale — `/files/<session_id>`
|
|
138
219
|
* HTTP fallback was removed because codeapi's sessionAuth requires
|
|
139
220
|
* kind/id query params unavailable at this point. */
|
|
@@ -187,12 +268,24 @@ function createBashExecutionTool(
|
|
|
187
268
|
command
|
|
188
269
|
);
|
|
189
270
|
const hasFiles = result.files != null && result.files.length > 0;
|
|
271
|
+
const runtimeEcho =
|
|
272
|
+
result.runtime_session_id != null
|
|
273
|
+
? {
|
|
274
|
+
runtime_session_id: result.runtime_session_id,
|
|
275
|
+
runtime_status: result.runtime_status,
|
|
276
|
+
}
|
|
277
|
+
: {};
|
|
190
278
|
return [
|
|
191
279
|
appendCodeSessionFileSummary(outputWithReminder, result.files),
|
|
192
280
|
(hasFiles
|
|
193
|
-
? {
|
|
281
|
+
? {
|
|
282
|
+
session_id: result.session_id,
|
|
283
|
+
files: result.files,
|
|
284
|
+
...runtimeEcho,
|
|
285
|
+
}
|
|
194
286
|
: {
|
|
195
287
|
session_id: result.session_id,
|
|
288
|
+
...runtimeEcho,
|
|
196
289
|
}) satisfies t.CodeExecutionArtifact,
|
|
197
290
|
];
|
|
198
291
|
} catch (error) {
|
|
@@ -200,15 +293,15 @@ function createBashExecutionTool(
|
|
|
200
293
|
(error as Error | undefined)?.message ?? '',
|
|
201
294
|
command
|
|
202
295
|
);
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Execution error:\n\n${messageWithReminder}`
|
|
205
|
-
);
|
|
296
|
+
throw new Error(`Execution error:\n\n${messageWithReminder}`);
|
|
206
297
|
}
|
|
207
298
|
},
|
|
208
299
|
{
|
|
209
300
|
name: BashExecutionToolName,
|
|
210
|
-
description:
|
|
211
|
-
|
|
301
|
+
description: buildBashExecutionToolDescription({
|
|
302
|
+
statefulSessions: params?.statefulSessions,
|
|
303
|
+
}),
|
|
304
|
+
schema: buildBashExecutionToolSchema(params ?? undefined),
|
|
212
305
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
213
306
|
}
|
|
214
307
|
);
|
|
@@ -304,8 +304,15 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
304
304
|
Partial<t.ProgrammaticCache> & {
|
|
305
305
|
session_id?: string;
|
|
306
306
|
_injected_files?: t.CodeEnvFile[];
|
|
307
|
+
_runtime_session_hint?: string;
|
|
307
308
|
};
|
|
308
|
-
const {
|
|
309
|
+
const {
|
|
310
|
+
toolMap,
|
|
311
|
+
toolDefs,
|
|
312
|
+
session_id,
|
|
313
|
+
_injected_files,
|
|
314
|
+
_runtime_session_hint,
|
|
315
|
+
} = toolCall;
|
|
309
316
|
|
|
310
317
|
if (toolMap == null || toolMap.size === 0) {
|
|
311
318
|
throw new Error(
|
|
@@ -351,6 +358,15 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
351
358
|
);
|
|
352
359
|
}
|
|
353
360
|
|
|
361
|
+
/* Stateful sessions: hint on the INITIAL request only (continuations
|
|
362
|
+
* bind via continuation_token). Wire-only in v1 — BashPTC keeps its
|
|
363
|
+
* stateless prompt. */
|
|
364
|
+
const runtimeSessionHint =
|
|
365
|
+
typeof _runtime_session_hint === 'string' &&
|
|
366
|
+
_runtime_session_hint !== ''
|
|
367
|
+
? _runtime_session_hint
|
|
368
|
+
: undefined;
|
|
369
|
+
|
|
354
370
|
let response = await makeRequest(
|
|
355
371
|
EXEC_ENDPOINT,
|
|
356
372
|
{
|
|
@@ -360,6 +376,9 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
360
376
|
session_id,
|
|
361
377
|
timeout,
|
|
362
378
|
...(files && files.length > 0 ? { files } : {}),
|
|
379
|
+
...(runtimeSessionHint != null
|
|
380
|
+
? { runtime_session_hint: runtimeSessionHint }
|
|
381
|
+
: {}),
|
|
363
382
|
},
|
|
364
383
|
proxy,
|
|
365
384
|
initParams.authHeaders
|
|
@@ -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
|