@librechat/agents 3.2.55 → 3.2.57
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 +5 -0
- 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/stream.cjs +9 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +19 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +5 -0
- 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/stream.mjs +9 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +19 -5
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +1 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/types/run.d.ts +9 -0
- package/dist/types/types/tools.d.ts +16 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +144 -0
- package/src/graphs/Graph.ts +6 -1
- package/src/run.ts +4 -0
- package/src/stream.ts +28 -2
- package/src/tools/ToolNode.ts +28 -4
- package/src/tools/__tests__/ToolNode.session.test.ts +160 -0
- package/src/types/run.ts +9 -0
- package/src/types/tools.ts +16 -0
|
@@ -81,6 +81,7 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
|
|
|
81
81
|
* consumes the settled promises while preserving final ToolMessage order.
|
|
82
82
|
*/
|
|
83
83
|
eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
|
|
84
|
+
codeSessionToolNames: string[] | undefined;
|
|
84
85
|
eagerEventToolExecutions: Map<string, t.EagerEventToolExecution>;
|
|
85
86
|
eagerEventToolUsageCount: Map<string, number>;
|
|
86
87
|
private eagerEventToolUsageCountsByAgentId;
|
package/dist/types/run.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
13
13
|
private langfuse?;
|
|
14
14
|
private toolOutputReferences?;
|
|
15
15
|
private eagerEventToolExecution?;
|
|
16
|
+
private codeSessionToolNames?;
|
|
16
17
|
private toolExecution?;
|
|
17
18
|
private subagentUsageSink?;
|
|
18
19
|
private indexTokenCountMap?;
|
|
@@ -91,6 +91,8 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
91
91
|
private eventDrivenMode;
|
|
92
92
|
/** Opt-in stream-layer prestart config for event-driven tools. */
|
|
93
93
|
private eagerEventToolExecution?;
|
|
94
|
+
/** Host tools that write to the code sandbox and share its exec session. */
|
|
95
|
+
private codeSessionToolNames?;
|
|
94
96
|
/** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
|
|
95
97
|
private eagerEventToolExecutions?;
|
|
96
98
|
/** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
|
|
@@ -145,7 +147,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
145
147
|
* other's in-flight state.
|
|
146
148
|
*/
|
|
147
149
|
private anonBatchCounter;
|
|
148
|
-
constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
|
|
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);
|
|
149
151
|
invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
|
|
150
152
|
/**
|
|
151
153
|
* Returns the run-scoped tool output registry, or `undefined` when
|
|
@@ -304,6 +306,14 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
304
306
|
* Extracts code execution session context from tool results and stores in Graph.sessions.
|
|
305
307
|
* Mirrors the session storage logic in handleRunToolCompletions for direct execution.
|
|
306
308
|
*/
|
|
309
|
+
/**
|
|
310
|
+
* True when a tool's successful result should fold its returned exec
|
|
311
|
+
* `session_id` into the shared code session: built-in `CODE_EXECUTION_TOOLS`,
|
|
312
|
+
* plus host-declared sandbox-writing tools (`codeSessionToolNames`, e.g.
|
|
313
|
+
* create_file/edit_file). Kept name-scoped rather than a blanket artifact
|
|
314
|
+
* opt-in so only host-declared tools can influence the shared session.
|
|
315
|
+
*/
|
|
316
|
+
private participatesInCodeSession;
|
|
307
317
|
private storeCodeSessionFromResults;
|
|
308
318
|
/**
|
|
309
319
|
* Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
|
|
@@ -170,6 +170,15 @@ export type RunConfig = {
|
|
|
170
170
|
* eager dispatch unsafe.
|
|
171
171
|
*/
|
|
172
172
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
173
|
+
/**
|
|
174
|
+
* Names of host tools that write to the code-execution sandbox but are not
|
|
175
|
+
* built-in `CODE_EXECUTION_TOOLS` (e.g. LibreChat's create_file/edit_file).
|
|
176
|
+
* Their successful results fold the returned exec `session_id` into the
|
|
177
|
+
* shared code session, so a file such a tool writes is visible to later
|
|
178
|
+
* bash_tool/execute_code calls running in the same sandbox. Host-declared so
|
|
179
|
+
* the SDK stays name-agnostic.
|
|
180
|
+
*/
|
|
181
|
+
codeSessionToolNames?: string[];
|
|
173
182
|
/**
|
|
174
183
|
* Selects the execution backend for built-in code tools. Omit this to keep
|
|
175
184
|
* the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
|
|
@@ -31,6 +31,16 @@ export type EagerEventToolExecutionConfig = {
|
|
|
31
31
|
* ToolMessages so provider message ordering is preserved.
|
|
32
32
|
*/
|
|
33
33
|
enabled?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Tool names that must never be started eagerly. Eager execution
|
|
36
|
+
* speculates on tool args before the model turn commits, so
|
|
37
|
+
* side-effecting tools (e.g. file writes) should opt out: a
|
|
38
|
+
* speculative write can land even if the turn is superseded, and a
|
|
39
|
+
* later arg revision would otherwise trip the "changed after eager
|
|
40
|
+
* execution" guard. Excluded calls fall through to normal ToolNode
|
|
41
|
+
* execution with final args.
|
|
42
|
+
*/
|
|
43
|
+
excludeToolNames?: string[];
|
|
34
44
|
};
|
|
35
45
|
export type EagerEventToolExecutionOutcome = {
|
|
36
46
|
results: ToolExecuteResult[];
|
|
@@ -90,6 +100,12 @@ export type ToolNodeOptions = {
|
|
|
90
100
|
directToolNames?: Set<string>;
|
|
91
101
|
/** Opt-in eager execution for event-driven tool calls. */
|
|
92
102
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
103
|
+
/**
|
|
104
|
+
* Host tool names that write to the code-execution sandbox but are not
|
|
105
|
+
* built-in `CODE_EXECUTION_TOOLS`. Their exec `session_id` is folded into the
|
|
106
|
+
* shared code session so later bash_tool/execute_code calls see written files.
|
|
107
|
+
*/
|
|
108
|
+
codeSessionToolNames?: string[];
|
|
93
109
|
/** Shared per-run eager execution registry populated by the stream handler. */
|
|
94
110
|
eagerEventToolExecutions?: Map<string, EagerEventToolExecution>;
|
|
95
111
|
/** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
|
package/package.json
CHANGED
|
@@ -4726,4 +4726,148 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
4726
4726
|
expect(toolExecuteCalls).toHaveLength(0);
|
|
4727
4727
|
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
4728
4728
|
});
|
|
4729
|
+
|
|
4730
|
+
it('does not prestart tools listed in excludeToolNames', async () => {
|
|
4731
|
+
const graph = createGraph({
|
|
4732
|
+
eagerEventToolExecution: {
|
|
4733
|
+
enabled: true,
|
|
4734
|
+
excludeToolNames: ['create_file'],
|
|
4735
|
+
},
|
|
4736
|
+
});
|
|
4737
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4738
|
+
jest
|
|
4739
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4740
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4741
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
4745
|
+
toolExecuteCalls.push(batch);
|
|
4746
|
+
batch.resolve([
|
|
4747
|
+
{ toolCallId: 'call_file', status: 'success', content: 'ok' },
|
|
4748
|
+
]);
|
|
4749
|
+
});
|
|
4750
|
+
|
|
4751
|
+
await new ChatModelStreamHandler().handle(
|
|
4752
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4753
|
+
{
|
|
4754
|
+
chunk: {
|
|
4755
|
+
content: '',
|
|
4756
|
+
tool_calls: [
|
|
4757
|
+
{
|
|
4758
|
+
id: 'call_file',
|
|
4759
|
+
name: 'create_file',
|
|
4760
|
+
args: { path: '/mnt/data/x.py', content: 'print(1)' },
|
|
4761
|
+
},
|
|
4762
|
+
],
|
|
4763
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
4764
|
+
} as unknown as t.StreamChunk,
|
|
4765
|
+
},
|
|
4766
|
+
{ langgraph_node: 'agent' },
|
|
4767
|
+
graph
|
|
4768
|
+
);
|
|
4769
|
+
|
|
4770
|
+
// Excluded: no eager execution started; the call falls through to ToolNode.
|
|
4771
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4772
|
+
expect(graph.eagerEventToolExecutions.has('call_file')).toBe(false);
|
|
4773
|
+
});
|
|
4774
|
+
|
|
4775
|
+
it('does not prestart codeSessionToolNames tools even without excludeToolNames', async () => {
|
|
4776
|
+
// A declared session-writing host tool is side-effecting, so it must not be
|
|
4777
|
+
// eagerly prestarted even when the host didn't also list it in excludeToolNames.
|
|
4778
|
+
const graph = createGraph({
|
|
4779
|
+
eagerEventToolExecution: { enabled: true },
|
|
4780
|
+
codeSessionToolNames: ['create_file', 'edit_file'],
|
|
4781
|
+
});
|
|
4782
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4783
|
+
jest
|
|
4784
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4785
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4786
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
4790
|
+
toolExecuteCalls.push(batch);
|
|
4791
|
+
batch.resolve([
|
|
4792
|
+
{ toolCallId: 'call_cf2', status: 'success', content: 'ok' },
|
|
4793
|
+
]);
|
|
4794
|
+
});
|
|
4795
|
+
|
|
4796
|
+
await new ChatModelStreamHandler().handle(
|
|
4797
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4798
|
+
{
|
|
4799
|
+
chunk: {
|
|
4800
|
+
content: '',
|
|
4801
|
+
tool_calls: [
|
|
4802
|
+
{
|
|
4803
|
+
id: 'call_cf2',
|
|
4804
|
+
name: 'create_file',
|
|
4805
|
+
args: { path: '/mnt/data/y.py', content: 'print(2)' },
|
|
4806
|
+
},
|
|
4807
|
+
],
|
|
4808
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
4809
|
+
} as unknown as t.StreamChunk,
|
|
4810
|
+
},
|
|
4811
|
+
{ langgraph_node: 'agent' },
|
|
4812
|
+
graph
|
|
4813
|
+
);
|
|
4814
|
+
|
|
4815
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4816
|
+
expect(graph.eagerEventToolExecutions.has('call_cf2')).toBe(false);
|
|
4817
|
+
});
|
|
4818
|
+
|
|
4819
|
+
it('keeps the direct-tool batch guard when an excluded tool is also direct', async () => {
|
|
4820
|
+
// edit_file is both a direct graph tool AND excluded from eager. A mixed
|
|
4821
|
+
// batch with a direct tool must suppress eager for the whole batch, so the
|
|
4822
|
+
// sibling event tool must NOT prestart — excluding edit_file must not hide
|
|
4823
|
+
// it from the batch-level direct-tool guard.
|
|
4824
|
+
const graph = createGraph({
|
|
4825
|
+
eagerEventToolExecution: {
|
|
4826
|
+
enabled: true,
|
|
4827
|
+
excludeToolNames: ['edit_file'],
|
|
4828
|
+
},
|
|
4829
|
+
getAgentContext: jest.fn(() => ({
|
|
4830
|
+
provider: Providers.ANTHROPIC,
|
|
4831
|
+
reasoningKey: 'reasoning',
|
|
4832
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'edit_file' }],
|
|
4833
|
+
graphTools: [{ name: 'edit_file' }],
|
|
4834
|
+
agentId: 'agent_1',
|
|
4835
|
+
})) as unknown as StandardGraph['getAgentContext'],
|
|
4836
|
+
});
|
|
4837
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
4838
|
+
jest
|
|
4839
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
4840
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
4841
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
|
|
4845
|
+
(data as t.ToolExecuteBatchRequest).resolve([]);
|
|
4846
|
+
});
|
|
4847
|
+
|
|
4848
|
+
await new ChatModelStreamHandler().handle(
|
|
4849
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
4850
|
+
{
|
|
4851
|
+
chunk: {
|
|
4852
|
+
content: '',
|
|
4853
|
+
tool_calls: [
|
|
4854
|
+
{ id: 'call_weather', name: 'weather', args: { city: 'NYC' } },
|
|
4855
|
+
{
|
|
4856
|
+
id: 'call_edit',
|
|
4857
|
+
name: 'edit_file',
|
|
4858
|
+
args: { path: '/mnt/data/x.py' },
|
|
4859
|
+
},
|
|
4860
|
+
],
|
|
4861
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
4862
|
+
} as unknown as t.StreamChunk,
|
|
4863
|
+
},
|
|
4864
|
+
{ langgraph_node: 'agent' },
|
|
4865
|
+
graph
|
|
4866
|
+
);
|
|
4867
|
+
|
|
4868
|
+
// Direct tool in batch suppresses eager for the whole batch.
|
|
4869
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
4870
|
+
expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(false);
|
|
4871
|
+
expect(graph.eagerEventToolExecutions.has('call_edit')).toBe(false);
|
|
4872
|
+
});
|
|
4729
4873
|
});
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -66,8 +66,8 @@ import {
|
|
|
66
66
|
type CallbackEntry,
|
|
67
67
|
} from '@/utils/callbacks';
|
|
68
68
|
import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
|
|
69
|
-
import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
|
|
70
69
|
import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
|
|
70
|
+
import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
|
|
71
71
|
import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
|
|
72
72
|
import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
|
|
73
73
|
import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
@@ -608,6 +608,7 @@ export abstract class Graph<
|
|
|
608
608
|
* consumes the settled promises while preserving final ToolMessage order.
|
|
609
609
|
*/
|
|
610
610
|
eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
|
|
611
|
+
codeSessionToolNames: string[] | undefined;
|
|
611
612
|
eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
|
|
612
613
|
eagerEventToolUsageCount: Map<string, number> = new Map();
|
|
613
614
|
private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
|
|
@@ -656,6 +657,7 @@ export abstract class Graph<
|
|
|
656
657
|
this.humanInTheLoop = undefined;
|
|
657
658
|
this.toolOutputReferences = undefined;
|
|
658
659
|
this.eagerEventToolExecution = undefined;
|
|
660
|
+
this.codeSessionToolNames = undefined;
|
|
659
661
|
this.eagerEventToolExecutions.clear();
|
|
660
662
|
this.clearEagerEventToolUsageCounts();
|
|
661
663
|
this.eagerEventToolCallChunks.clear();
|
|
@@ -1260,6 +1262,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1260
1262
|
hookRegistry: this.hookRegistry,
|
|
1261
1263
|
humanInTheLoop: this.humanInTheLoop,
|
|
1262
1264
|
eagerEventToolExecution: this.eagerEventToolExecution,
|
|
1265
|
+
codeSessionToolNames: this.codeSessionToolNames,
|
|
1263
1266
|
eagerEventToolExecutions: this.eagerEventToolExecutions,
|
|
1264
1267
|
eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
|
|
1265
1268
|
agentContext?.agentId
|
|
@@ -1309,6 +1312,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1309
1312
|
toolRegistry: agentContext?.toolRegistry,
|
|
1310
1313
|
sessions: this.sessions,
|
|
1311
1314
|
toolExecution: this.toolExecution,
|
|
1315
|
+
codeSessionToolNames: this.codeSessionToolNames,
|
|
1312
1316
|
hookRegistry: this.hookRegistry,
|
|
1313
1317
|
humanInTheLoop: this.humanInTheLoop,
|
|
1314
1318
|
maxContextTokens: agentContext?.maxContextTokens,
|
|
@@ -2333,6 +2337,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2333
2337
|
*/
|
|
2334
2338
|
childGraph.toolOutputReferences = this.toolOutputReferences;
|
|
2335
2339
|
childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
2340
|
+
childGraph.codeSessionToolNames = this.codeSessionToolNames;
|
|
2336
2341
|
childGraph.toolExecution = this.toolExecution;
|
|
2337
2342
|
childGraph.eventToolExecutionAvailable =
|
|
2338
2343
|
this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
|
package/src/run.ts
CHANGED
|
@@ -127,6 +127,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
127
127
|
private langfuse?: t.LangfuseConfig;
|
|
128
128
|
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
129
129
|
private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
|
|
130
|
+
private codeSessionToolNames?: string[];
|
|
130
131
|
private toolExecution?: t.ToolExecutionConfig;
|
|
131
132
|
private subagentUsageSink?: t.SubagentUsageSink;
|
|
132
133
|
private indexTokenCountMap?: Record<string, number>;
|
|
@@ -182,6 +183,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
182
183
|
this.langfuse = config.langfuse;
|
|
183
184
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
184
185
|
this.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
186
|
+
this.codeSessionToolNames = config.codeSessionToolNames;
|
|
185
187
|
this.toolExecution = config.toolExecution;
|
|
186
188
|
this.subagentUsageSink = config.subagentUsageSink;
|
|
187
189
|
|
|
@@ -268,6 +270,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
268
270
|
standardGraph.humanInTheLoop = this.humanInTheLoop;
|
|
269
271
|
standardGraph.toolOutputReferences = this.toolOutputReferences;
|
|
270
272
|
standardGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
273
|
+
standardGraph.codeSessionToolNames = this.codeSessionToolNames;
|
|
271
274
|
standardGraph.toolExecution = this.toolExecution;
|
|
272
275
|
this.Graph = standardGraph;
|
|
273
276
|
return standardGraph.createWorkflow();
|
|
@@ -297,6 +300,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
297
300
|
multiAgentGraph.humanInTheLoop = this.humanInTheLoop;
|
|
298
301
|
multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
|
|
299
302
|
multiAgentGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
303
|
+
multiAgentGraph.codeSessionToolNames = this.codeSessionToolNames;
|
|
300
304
|
multiAgentGraph.toolExecution = this.toolExecution;
|
|
301
305
|
this.Graph = multiAgentGraph;
|
|
302
306
|
return multiAgentGraph.createWorkflow();
|
package/src/stream.ts
CHANGED
|
@@ -127,6 +127,24 @@ function hasToolOutputReference(value: unknown): boolean {
|
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function isEagerExecutionExcludedTool(
|
|
131
|
+
name: string,
|
|
132
|
+
graph: StandardGraph
|
|
133
|
+
): boolean {
|
|
134
|
+
if (name === '') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const excluded = graph.eagerEventToolExecution?.excludeToolNames;
|
|
138
|
+
if (excluded != null && excluded.includes(name)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
// A code-session participant writes to the shared sandbox, so it is
|
|
142
|
+
// side-effecting: never prestart it speculatively (a revised/superseded turn
|
|
143
|
+
// would leave the write applied). Implies exclusion without the host having
|
|
144
|
+
// to also list the name in excludeToolNames.
|
|
145
|
+
return graph.codeSessionToolNames?.includes(name) === true;
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
function isDirectGraphTool(
|
|
131
149
|
name: string,
|
|
132
150
|
agentContext: AgentContext | undefined
|
|
@@ -184,7 +202,8 @@ function getCodeSessionContext(
|
|
|
184
202
|
if (
|
|
185
203
|
!CODE_EXECUTION_TOOLS.has(name) &&
|
|
186
204
|
name !== Constants.SKILL_TOOL &&
|
|
187
|
-
name !== Constants.READ_FILE
|
|
205
|
+
name !== Constants.READ_FILE &&
|
|
206
|
+
graph.codeSessionToolNames?.includes(name) !== true
|
|
188
207
|
) {
|
|
189
208
|
return undefined;
|
|
190
209
|
}
|
|
@@ -615,7 +634,7 @@ function createEagerToolExecutionPlan(args: {
|
|
|
615
634
|
return undefined;
|
|
616
635
|
}
|
|
617
636
|
|
|
618
|
-
const
|
|
637
|
+
const unstartedToolCalls = skipExisting
|
|
619
638
|
? toolCalls.filter((toolCall) => {
|
|
620
639
|
if (toolCall.id == null || toolCall.id === '') {
|
|
621
640
|
return true;
|
|
@@ -623,6 +642,13 @@ function createEagerToolExecutionPlan(args: {
|
|
|
623
642
|
return !graph.eagerEventToolExecutions.has(toolCall.id);
|
|
624
643
|
})
|
|
625
644
|
: toolCalls;
|
|
645
|
+
// Drop host-excluded tools only AFTER the batch-level guards above have run
|
|
646
|
+
// against the full batch, so excluding a call never hides a sibling direct
|
|
647
|
+
// tool from `hasDirectToolCallInBatch`. Excluded calls fall through to normal
|
|
648
|
+
// ToolNode execution; siblings may still eager-execute.
|
|
649
|
+
const candidateToolCalls = unstartedToolCalls.filter(
|
|
650
|
+
(toolCall) => !isEagerExecutionExcludedTool(toolCall.name, graph)
|
|
651
|
+
);
|
|
626
652
|
if (candidateToolCalls.length === 0) {
|
|
627
653
|
return [];
|
|
628
654
|
}
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -439,6 +439,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
439
439
|
private eventDrivenMode: boolean = false;
|
|
440
440
|
/** Opt-in stream-layer prestart config for event-driven tools. */
|
|
441
441
|
private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
|
|
442
|
+
/** Host tools that write to the code sandbox and share its exec session. */
|
|
443
|
+
private codeSessionToolNames?: ReadonlySet<string>;
|
|
442
444
|
/** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
|
|
443
445
|
private eagerEventToolExecutions?: Map<string, t.EagerEventToolExecution>;
|
|
444
446
|
/** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
|
|
@@ -515,6 +517,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
515
517
|
agentId,
|
|
516
518
|
executingAgentId,
|
|
517
519
|
directToolNames,
|
|
520
|
+
codeSessionToolNames,
|
|
518
521
|
maxContextTokens,
|
|
519
522
|
maxToolResultChars,
|
|
520
523
|
hookRegistry,
|
|
@@ -552,6 +555,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
552
555
|
// existing agentId option) still get attribution without knowing the new option.
|
|
553
556
|
this.executingAgentId = executingAgentId ?? agentId;
|
|
554
557
|
this.directToolNames = directToolNames;
|
|
558
|
+
this.codeSessionToolNames =
|
|
559
|
+
codeSessionToolNames != null && codeSessionToolNames.length > 0
|
|
560
|
+
? new Set(codeSessionToolNames)
|
|
561
|
+
: undefined;
|
|
555
562
|
this.maxToolResultChars =
|
|
556
563
|
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
|
|
557
564
|
this.hookRegistry = hookRegistry;
|
|
@@ -973,7 +980,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
973
980
|
* still need to travel through `_injected_files`; the legacy
|
|
974
981
|
* `/files/<session_id>` fallback was removed from the executors.
|
|
975
982
|
*/
|
|
976
|
-
if (
|
|
983
|
+
if (this.participatesInCodeSession(call.name)) {
|
|
977
984
|
const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
|
|
978
985
|
| t.CodeSessionContext
|
|
979
986
|
| undefined;
|
|
@@ -1759,6 +1766,23 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1759
1766
|
* Extracts code execution session context from tool results and stores in Graph.sessions.
|
|
1760
1767
|
* Mirrors the session storage logic in handleRunToolCompletions for direct execution.
|
|
1761
1768
|
*/
|
|
1769
|
+
/**
|
|
1770
|
+
* True when a tool's successful result should fold its returned exec
|
|
1771
|
+
* `session_id` into the shared code session: built-in `CODE_EXECUTION_TOOLS`,
|
|
1772
|
+
* plus host-declared sandbox-writing tools (`codeSessionToolNames`, e.g.
|
|
1773
|
+
* create_file/edit_file). Kept name-scoped rather than a blanket artifact
|
|
1774
|
+
* opt-in so only host-declared tools can influence the shared session.
|
|
1775
|
+
*/
|
|
1776
|
+
private participatesInCodeSession(name: string): boolean {
|
|
1777
|
+
if (name === '') {
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
return (
|
|
1781
|
+
CODE_EXECUTION_TOOLS.has(name) ||
|
|
1782
|
+
this.codeSessionToolNames?.has(name) === true
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1762
1786
|
private storeCodeSessionFromResults(
|
|
1763
1787
|
results: t.ToolExecuteResult[],
|
|
1764
1788
|
requestMap: Map<string, t.ToolCallRequest>
|
|
@@ -1777,7 +1801,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1777
1801
|
if (
|
|
1778
1802
|
request?.name == null ||
|
|
1779
1803
|
request.name === '' ||
|
|
1780
|
-
(!
|
|
1804
|
+
(!this.participatesInCodeSession(request.name) &&
|
|
1781
1805
|
request.name !== Constants.SKILL_TOOL)
|
|
1782
1806
|
) {
|
|
1783
1807
|
continue;
|
|
@@ -1830,7 +1854,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1830
1854
|
continue;
|
|
1831
1855
|
}
|
|
1832
1856
|
|
|
1833
|
-
if (this.sessions &&
|
|
1857
|
+
if (this.sessions && this.participatesInCodeSession(call.name)) {
|
|
1834
1858
|
const artifact = toolMessage.artifact as
|
|
1835
1859
|
| t.CodeExecutionArtifact
|
|
1836
1860
|
| undefined;
|
|
@@ -2463,7 +2487,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2463
2487
|
const plan = buildToolExecutionRequestPlan({
|
|
2464
2488
|
toolCalls: approvedEntries.map((entry) => {
|
|
2465
2489
|
const codeSessionContext =
|
|
2466
|
-
|
|
2490
|
+
this.participatesInCodeSession(entry.call.name) ||
|
|
2467
2491
|
entry.call.name === Constants.SKILL_TOOL ||
|
|
2468
2492
|
entry.call.name === Constants.READ_FILE
|
|
2469
2493
|
? this.getCodeSessionContext()
|
|
@@ -140,6 +140,52 @@ describe('ToolNode code execution session management', () => {
|
|
|
140
140
|
expect(capturedConfigs[0]._injected_files).toBeUndefined();
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
it('injects the existing session into declared host tools on the direct path', async () => {
|
|
144
|
+
const capturedConfigs: Record<string, unknown>[] = [];
|
|
145
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
146
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
147
|
+
session_id: 'host-session',
|
|
148
|
+
files: [
|
|
149
|
+
{ id: 'g1', name: 'gen.png', storage_session_id: 'host-session' },
|
|
150
|
+
],
|
|
151
|
+
lastUpdated: Date.now(),
|
|
152
|
+
} satisfies t.CodeSessionContext);
|
|
153
|
+
|
|
154
|
+
const cfTool = tool(
|
|
155
|
+
async (_input, config) => {
|
|
156
|
+
capturedConfigs.push({ ...(config.toolCall ?? {}) });
|
|
157
|
+
return 'Created /mnt/data/x.py';
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'create_file',
|
|
161
|
+
description: 'write a file',
|
|
162
|
+
schema: z.object({ path: z.string(), content: z.string() }),
|
|
163
|
+
}
|
|
164
|
+
) as unknown as StructuredToolInterface;
|
|
165
|
+
|
|
166
|
+
const toolNode = new ToolNode({
|
|
167
|
+
tools: [cfTool],
|
|
168
|
+
sessions,
|
|
169
|
+
codeSessionToolNames: ['create_file', 'edit_file'],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const aiMsg = new AIMessage({
|
|
173
|
+
content: '',
|
|
174
|
+
tool_calls: [
|
|
175
|
+
{
|
|
176
|
+
id: 'call_cf',
|
|
177
|
+
name: 'create_file',
|
|
178
|
+
args: { path: '/mnt/data/x.py', content: 'print(1)' },
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
await toolNode.invoke({ messages: [aiMsg] });
|
|
183
|
+
|
|
184
|
+
// Declared host tool writes into the current sandbox, not a fresh one.
|
|
185
|
+
expect(capturedConfigs).toHaveLength(1);
|
|
186
|
+
expect(capturedConfigs[0].session_id).toBe('host-session');
|
|
187
|
+
});
|
|
188
|
+
|
|
143
189
|
it('preserves per-file storage_session_id for multi-session files', async () => {
|
|
144
190
|
const capturedConfigs: Record<string, unknown>[] = [];
|
|
145
191
|
const sessions: t.ToolSessionMap = new Map();
|
|
@@ -774,6 +820,78 @@ describe('ToolNode code execution session management', () => {
|
|
|
774
820
|
expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
|
|
775
821
|
});
|
|
776
822
|
|
|
823
|
+
it('stores the exec session for host tools declared in codeSessionToolNames', () => {
|
|
824
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
825
|
+
const toolNode = new ToolNode({
|
|
826
|
+
tools: [createMockCodeTool({ capturedConfigs: [] })],
|
|
827
|
+
sessions,
|
|
828
|
+
eventDrivenMode: true,
|
|
829
|
+
codeSessionToolNames: ['create_file', 'edit_file'],
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const storeMethod = (
|
|
833
|
+
toolNode as unknown as {
|
|
834
|
+
storeCodeSessionFromResults: (
|
|
835
|
+
results: t.ToolExecuteResult[],
|
|
836
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
837
|
+
) => void;
|
|
838
|
+
}
|
|
839
|
+
).storeCodeSessionFromResults.bind(toolNode);
|
|
840
|
+
|
|
841
|
+
storeMethod(
|
|
842
|
+
[
|
|
843
|
+
{
|
|
844
|
+
toolCallId: 'tc-cf',
|
|
845
|
+
content: 'Created /mnt/data/x.py (10 chars).',
|
|
846
|
+
artifact: { session_id: 'authoring-session' },
|
|
847
|
+
status: 'success',
|
|
848
|
+
},
|
|
849
|
+
],
|
|
850
|
+
new Map([['tc-cf', { id: 'tc-cf', name: 'create_file', args: {} }]])
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
// create_file's exec session is folded into the shared code session, so a
|
|
854
|
+
// later bash_tool/execute_code call reuses the same sandbox.
|
|
855
|
+
const stored = sessions.get(Constants.EXECUTE_CODE) as
|
|
856
|
+
| t.CodeSessionContext
|
|
857
|
+
| undefined;
|
|
858
|
+
expect(stored?.session_id).toBe('authoring-session');
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('does not store a host authoring tool session when not declared', () => {
|
|
862
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
863
|
+
const toolNode = new ToolNode({
|
|
864
|
+
tools: [createMockCodeTool({ capturedConfigs: [] })],
|
|
865
|
+
sessions,
|
|
866
|
+
eventDrivenMode: true,
|
|
867
|
+
// codeSessionToolNames omitted — create_file must NOT be treated as a
|
|
868
|
+
// code-session participant.
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const storeMethod = (
|
|
872
|
+
toolNode as unknown as {
|
|
873
|
+
storeCodeSessionFromResults: (
|
|
874
|
+
results: t.ToolExecuteResult[],
|
|
875
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
876
|
+
) => void;
|
|
877
|
+
}
|
|
878
|
+
).storeCodeSessionFromResults.bind(toolNode);
|
|
879
|
+
|
|
880
|
+
storeMethod(
|
|
881
|
+
[
|
|
882
|
+
{
|
|
883
|
+
toolCallId: 'tc-cf2',
|
|
884
|
+
content: 'Created /mnt/data/x.py (10 chars).',
|
|
885
|
+
artifact: { session_id: 'authoring-session' },
|
|
886
|
+
status: 'success',
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
new Map([['tc-cf2', { id: 'tc-cf2', name: 'create_file', args: {} }]])
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
|
|
893
|
+
});
|
|
894
|
+
|
|
777
895
|
it('ignores error results', () => {
|
|
778
896
|
const sessions: t.ToolSessionMap = new Map();
|
|
779
897
|
|
|
@@ -1102,5 +1220,47 @@ describe('ToolNode code execution session management', () => {
|
|
|
1102
1220
|
expect(capturedRequests[0].name).toBe('web_search');
|
|
1103
1221
|
expect(capturedRequests[0].codeSessionContext).toBeUndefined();
|
|
1104
1222
|
});
|
|
1223
|
+
|
|
1224
|
+
it('attaches codeSessionContext to declared host tools so they write into the current sandbox', async () => {
|
|
1225
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
1226
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
1227
|
+
session_id: 'cf-session',
|
|
1228
|
+
files: [
|
|
1229
|
+
{ id: 'g1', name: 'gen.png', storage_session_id: 'cf-session' },
|
|
1230
|
+
],
|
|
1231
|
+
lastUpdated: Date.now(),
|
|
1232
|
+
} satisfies t.CodeSessionContext);
|
|
1233
|
+
|
|
1234
|
+
const { capturedRequests } = captureBatchRequests();
|
|
1235
|
+
|
|
1236
|
+
const toolNode = new ToolNode({
|
|
1237
|
+
tools: [createDummyTool('create_file')],
|
|
1238
|
+
sessions,
|
|
1239
|
+
eventDrivenMode: true,
|
|
1240
|
+
codeSessionToolNames: ['create_file', 'edit_file'],
|
|
1241
|
+
toolCallStepIds: new Map([['call_cf', 'step_cf']]),
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const aiMsg = new AIMessage({
|
|
1245
|
+
content: '',
|
|
1246
|
+
tool_calls: [
|
|
1247
|
+
{
|
|
1248
|
+
id: 'call_cf',
|
|
1249
|
+
name: 'create_file',
|
|
1250
|
+
args: { path: '/mnt/data/x.py', content: 'print(1)' },
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
await toolNode.invoke({ messages: [aiMsg] });
|
|
1256
|
+
|
|
1257
|
+
expect(capturedRequests).toHaveLength(1);
|
|
1258
|
+
expect(capturedRequests[0].name).toBe('create_file');
|
|
1259
|
+
// Existing session is injected so create_file writes into the current
|
|
1260
|
+
// sandbox instead of spawning a separate one.
|
|
1261
|
+
expect(capturedRequests[0].codeSessionContext?.session_id).toBe(
|
|
1262
|
+
'cf-session'
|
|
1263
|
+
);
|
|
1264
|
+
});
|
|
1105
1265
|
});
|
|
1106
1266
|
});
|
package/src/types/run.ts
CHANGED
|
@@ -184,6 +184,15 @@ export type RunConfig = {
|
|
|
184
184
|
* eager dispatch unsafe.
|
|
185
185
|
*/
|
|
186
186
|
eagerEventToolExecution?: EagerEventToolExecutionConfig;
|
|
187
|
+
/**
|
|
188
|
+
* Names of host tools that write to the code-execution sandbox but are not
|
|
189
|
+
* built-in `CODE_EXECUTION_TOOLS` (e.g. LibreChat's create_file/edit_file).
|
|
190
|
+
* Their successful results fold the returned exec `session_id` into the
|
|
191
|
+
* shared code session, so a file such a tool writes is visible to later
|
|
192
|
+
* bash_tool/execute_code calls running in the same sandbox. Host-declared so
|
|
193
|
+
* the SDK stays name-agnostic.
|
|
194
|
+
*/
|
|
195
|
+
codeSessionToolNames?: string[];
|
|
187
196
|
/**
|
|
188
197
|
* Selects the execution backend for built-in code tools. Omit this to keep
|
|
189
198
|
* the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
|