@librechat/agents 3.1.78 → 3.1.80-dev.0
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/llm/anthropic/index.cjs +44 -55
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +97 -14
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +10 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +16 -5
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -4
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +63 -40
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +43 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +48 -15
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +97 -14
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +10 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +16 -5
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -4
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +63 -40
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/llm/anthropic/index.d.ts +1 -9
- package/dist/types/messages/anthropicToolCache.d.ts +5 -5
- package/dist/types/types/tools.d.ts +82 -17
- package/package.json +1 -1
- package/src/llm/anthropic/index.ts +55 -64
- package/src/llm/anthropic/llm.spec.ts +585 -0
- package/src/llm/anthropic/utils/message_inputs.ts +36 -21
- package/src/llm/anthropic/utils/message_outputs.ts +0 -4
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
- package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
- package/src/messages/anthropicToolCache.ts +70 -25
- package/src/messages/format.ts +117 -18
- package/src/messages/formatAgentMessages.test.ts +202 -1
- package/src/scripts/code_exec_multi_session.ts +4 -4
- package/src/specs/summarization.test.ts +3 -3
- package/src/tools/BashExecutor.ts +11 -3
- package/src/tools/BashProgrammaticToolCalling.ts +6 -6
- package/src/tools/CodeExecutor.ts +17 -6
- package/src/tools/ProgrammaticToolCalling.ts +14 -10
- package/src/tools/ToolNode.ts +85 -48
- package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +9 -2
- package/src/tools/__tests__/ToolNode.session.test.ts +131 -50
- package/src/tools/local/LocalExecutionEngine.ts +55 -54
- package/src/tools/local/LocalExecutionTools.ts +2 -2
- package/src/tools/local/LocalProgrammaticToolCalling.ts +23 -6
- package/src/types/diff.d.ts +15 -0
- package/src/types/tools.ts +79 -17
package/src/tools/ToolNode.ts
CHANGED
|
@@ -249,22 +249,62 @@ function normalizeApprovalDecisions(
|
|
|
249
249
|
* - `artifact.session_id` (the `sessionId` arg here) is the EXEC session
|
|
250
250
|
* — the sandbox VM that ran the code. It's transient and torn down
|
|
251
251
|
* post-execution; subsequent calls cannot reuse it as a sandbox.
|
|
252
|
-
* - `file.
|
|
252
|
+
* - `file.storage_session_id` on each `artifact.files[i]` is the STORAGE
|
|
253
253
|
* session — the file-server bucket prefix where the artifact actually
|
|
254
254
|
* lives and is served from.
|
|
255
255
|
*
|
|
256
|
-
* Per-file `
|
|
257
|
-
* because `_injected_files` are looked up against the
|
|
258
|
-
* storage path on subsequent tool calls. Stomping the
|
|
259
|
-
* the exec id silently 404s every follow-up tool call
|
|
260
|
-
* run — `cat /mnt/data/foo.txt` reports "No such file
|
|
261
|
-
* because the worker can't mount a file at a path the
|
|
262
|
-
* know about. Fall back to
|
|
263
|
-
* absent (
|
|
256
|
+
* Per-file `storage_session_id` is preserved (not overwritten with the
|
|
257
|
+
* exec id) because `_injected_files` are looked up against the
|
|
258
|
+
* file-server's storage path on subsequent tool calls. Stomping the
|
|
259
|
+
* storage id with the exec id silently 404s every follow-up tool call
|
|
260
|
+
* within the same run — `cat /mnt/data/foo.txt` reports "No such file
|
|
261
|
+
* or directory" because the worker can't mount a file at a path the
|
|
262
|
+
* storage doesn't know about. Fall back to the exec id only when the
|
|
263
|
+
* per-file id is absent (e.g. inline `content` files have no persistent
|
|
264
|
+
* storage location).
|
|
264
265
|
*/
|
|
266
|
+
/**
|
|
267
|
+
* Builds a `CodeEnvFile` ref from an arbitrary `FileRef`-like input,
|
|
268
|
+
* narrowing onto the discriminated union: `kind: 'skill'` requires
|
|
269
|
+
* `version`, other kinds forbid it.
|
|
270
|
+
*
|
|
271
|
+
* Defaults `kind` to `'user'` when unset — most ad-hoc files are
|
|
272
|
+
* user-private; shared resources (skills/agents) populate their kind
|
|
273
|
+
* upstream. A skill ref missing `version` falls back to `'user'` so
|
|
274
|
+
* the upstream contract bug surfaces as a degraded sessionKey rather
|
|
275
|
+
* than a runtime crash; primeSkillFiles is the only writer, and it
|
|
276
|
+
* always sets `version` — see LC packages/api/src/agents/skillFiles.ts.
|
|
277
|
+
*/
|
|
278
|
+
function toInjectedFileRef(
|
|
279
|
+
file: {
|
|
280
|
+
id: string;
|
|
281
|
+
name: string;
|
|
282
|
+
storage_session_id?: string;
|
|
283
|
+
kind?: t.CodeEnvKind;
|
|
284
|
+
version?: number;
|
|
285
|
+
},
|
|
286
|
+
execSessionId: string
|
|
287
|
+
): t.CodeEnvFile {
|
|
288
|
+
const base = {
|
|
289
|
+
id: file.id,
|
|
290
|
+
name: file.name,
|
|
291
|
+
/* Inline `content` files have no persistent storage location;
|
|
292
|
+
* fall back to the execution session id for those entries. */
|
|
293
|
+
storage_session_id: file.storage_session_id ?? execSessionId,
|
|
294
|
+
};
|
|
295
|
+
const kind = file.kind ?? 'user';
|
|
296
|
+
if (kind === 'skill' && file.version != null) {
|
|
297
|
+
return { ...base, kind: 'skill', version: file.version };
|
|
298
|
+
}
|
|
299
|
+
if (kind === 'agent') {
|
|
300
|
+
return { ...base, kind: 'agent' };
|
|
301
|
+
}
|
|
302
|
+
return { ...base, kind: 'user' };
|
|
303
|
+
}
|
|
304
|
+
|
|
265
305
|
function updateCodeSession(
|
|
266
306
|
sessions: t.ToolSessionMap,
|
|
267
|
-
|
|
307
|
+
execSessionId: string,
|
|
268
308
|
files: t.FileRefs | undefined
|
|
269
309
|
): void {
|
|
270
310
|
const newFiles = files ?? [];
|
|
@@ -276,20 +316,20 @@ function updateCodeSession(
|
|
|
276
316
|
if (newFiles.length > 0) {
|
|
277
317
|
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
278
318
|
...file,
|
|
279
|
-
|
|
319
|
+
storage_session_id: file.storage_session_id ?? execSessionId,
|
|
280
320
|
}));
|
|
281
321
|
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
282
322
|
const filteredExisting = existingFiles.filter(
|
|
283
323
|
(f) => !newFileNames.has(f.name)
|
|
284
324
|
);
|
|
285
325
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
286
|
-
session_id:
|
|
326
|
+
session_id: execSessionId,
|
|
287
327
|
files: [...filteredExisting, ...filesWithSession],
|
|
288
328
|
lastUpdated: Date.now(),
|
|
289
329
|
});
|
|
290
330
|
} else {
|
|
291
331
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
292
|
-
session_id:
|
|
332
|
+
session_id: execSessionId,
|
|
293
333
|
files: existingFiles,
|
|
294
334
|
lastUpdated: Date.now(),
|
|
295
335
|
});
|
|
@@ -400,7 +440,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
400
440
|
this.loadRuntimeTools = loadRuntimeTools;
|
|
401
441
|
this.errorHandler = errorHandler;
|
|
402
442
|
this.toolUsageCount = new Map<string, number>();
|
|
403
|
-
this.toolRegistry = resolveLocalToolRegistry({
|
|
443
|
+
this.toolRegistry = resolveLocalToolRegistry({
|
|
444
|
+
toolRegistry,
|
|
445
|
+
toolExecution,
|
|
446
|
+
});
|
|
404
447
|
this.sessions = sessions;
|
|
405
448
|
this.eventDrivenMode = eventDrivenMode ?? false;
|
|
406
449
|
this.agentId = agentId;
|
|
@@ -581,8 +624,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
581
624
|
resolveFn = <T>(_runId: string | undefined, args: T): ResolveResult<T> =>
|
|
582
625
|
preBatchSnapshot.resolve(args);
|
|
583
626
|
} else if (registry != null) {
|
|
584
|
-
resolveFn = <T>(
|
|
585
|
-
|
|
627
|
+
resolveFn = <T>(
|
|
628
|
+
runIdArg: string | undefined,
|
|
629
|
+
args: T
|
|
630
|
+
): ResolveResult<T> => registry.resolve(runIdArg, args);
|
|
586
631
|
}
|
|
587
632
|
/**
|
|
588
633
|
* Precompute the reference key once per call — captured locally
|
|
@@ -717,20 +762,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
717
762
|
const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
|
|
718
763
|
| t.CodeSessionContext
|
|
719
764
|
| undefined;
|
|
720
|
-
|
|
765
|
+
const execSessionId = codeSession?.session_id;
|
|
766
|
+
if (execSessionId != null && execSessionId !== '') {
|
|
721
767
|
invokeParams = {
|
|
722
768
|
...invokeParams,
|
|
723
|
-
session_id:
|
|
769
|
+
session_id: execSessionId,
|
|
724
770
|
};
|
|
725
771
|
|
|
726
|
-
if (codeSession
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
name: file.name,
|
|
731
|
-
...(file.entity_id != null ? { entity_id: file.entity_id } : {}),
|
|
732
|
-
}));
|
|
733
|
-
invokeParams._injected_files = fileRefs;
|
|
772
|
+
if (codeSession?.files != null && codeSession.files.length > 0) {
|
|
773
|
+
invokeParams._injected_files = codeSession.files.map((file) =>
|
|
774
|
+
toInjectedFileRef(file, execSessionId)
|
|
775
|
+
);
|
|
734
776
|
}
|
|
735
777
|
}
|
|
736
778
|
}
|
|
@@ -950,10 +992,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
950
992
|
): Promise<BaseMessage | Command> {
|
|
951
993
|
const runId = (config.configurable?.run_id as string | undefined) ?? '';
|
|
952
994
|
const hookRegistry = this.hookRegistry;
|
|
953
|
-
const hasPreHook =
|
|
954
|
-
|
|
955
|
-
const hasPostHook =
|
|
956
|
-
hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
995
|
+
const hasPreHook = hookRegistry?.hasHookFor('PreToolUse', runId) === true;
|
|
996
|
+
const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
957
997
|
const hasFailureHook =
|
|
958
998
|
hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
959
999
|
|
|
@@ -989,8 +1029,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
989
1029
|
// assignment.)
|
|
990
1030
|
const cachedTurn =
|
|
991
1031
|
call.id != null && call.id !== ''
|
|
992
|
-
? this.directPathTurns.get(call.id) ??
|
|
993
|
-
this.toolCallTurns.get(call.id)
|
|
1032
|
+
? (this.directPathTurns.get(call.id) ?? this.toolCallTurns.get(call.id))
|
|
994
1033
|
: undefined;
|
|
995
1034
|
if (cachedTurn != null) {
|
|
996
1035
|
usageCount = cachedTurn;
|
|
@@ -1156,10 +1195,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1156
1195
|
return this.blockDirectCall({
|
|
1157
1196
|
call,
|
|
1158
1197
|
resolvedArgs,
|
|
1159
|
-
reason:
|
|
1160
|
-
decision.reason ??
|
|
1161
|
-
preResult.reason ??
|
|
1162
|
-
'Rejected by user',
|
|
1198
|
+
reason: decision.reason ?? preResult.reason ?? 'Rejected by user',
|
|
1163
1199
|
hookRegistry,
|
|
1164
1200
|
runId,
|
|
1165
1201
|
threadId,
|
|
@@ -1173,7 +1209,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1173
1209
|
return this.blockDirectCall({
|
|
1174
1210
|
call,
|
|
1175
1211
|
resolvedArgs,
|
|
1176
|
-
reason:
|
|
1212
|
+
reason:
|
|
1213
|
+
'Approval payload `respond` was missing a string `responseText`',
|
|
1177
1214
|
hookRegistry,
|
|
1178
1215
|
runId,
|
|
1179
1216
|
threadId,
|
|
@@ -1464,17 +1501,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1464
1501
|
return undefined;
|
|
1465
1502
|
}
|
|
1466
1503
|
|
|
1504
|
+
const execSessionId = codeSession.session_id;
|
|
1467
1505
|
const context: NonNullable<t.ToolCallRequest['codeSessionContext']> = {
|
|
1468
|
-
session_id:
|
|
1506
|
+
session_id: execSessionId,
|
|
1469
1507
|
};
|
|
1470
1508
|
|
|
1471
1509
|
if (codeSession.files && codeSession.files.length > 0) {
|
|
1472
|
-
context.files = codeSession.files.map((file) =>
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
name: file.name,
|
|
1476
|
-
...(file.entity_id != null ? { entity_id: file.entity_id } : {}),
|
|
1477
|
-
}));
|
|
1510
|
+
context.files = codeSession.files.map((file) =>
|
|
1511
|
+
toInjectedFileRef(file, execSessionId)
|
|
1512
|
+
);
|
|
1478
1513
|
}
|
|
1479
1514
|
|
|
1480
1515
|
return context;
|
|
@@ -1509,11 +1544,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1509
1544
|
}
|
|
1510
1545
|
|
|
1511
1546
|
const artifact = result.artifact as t.CodeExecutionArtifact | undefined;
|
|
1512
|
-
|
|
1547
|
+
const execSessionId = artifact?.session_id;
|
|
1548
|
+
if (execSessionId == null || execSessionId === '') {
|
|
1513
1549
|
continue;
|
|
1514
1550
|
}
|
|
1515
1551
|
|
|
1516
|
-
updateCodeSession(this.sessions,
|
|
1552
|
+
updateCodeSession(this.sessions, execSessionId, artifact?.files);
|
|
1517
1553
|
}
|
|
1518
1554
|
}
|
|
1519
1555
|
|
|
@@ -1558,8 +1594,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1558
1594
|
const artifact = toolMessage.artifact as
|
|
1559
1595
|
| t.CodeExecutionArtifact
|
|
1560
1596
|
| undefined;
|
|
1561
|
-
|
|
1562
|
-
|
|
1597
|
+
const execSessionId = artifact?.session_id;
|
|
1598
|
+
if (execSessionId != null && execSessionId !== '') {
|
|
1599
|
+
updateCodeSession(this.sessions, execSessionId, artifact?.files);
|
|
1563
1600
|
}
|
|
1564
1601
|
}
|
|
1565
1602
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getReadRoots, getWriteRoots } from '../local/LocalExecutionEngine';
|
|
2
|
+
|
|
3
|
+
describe('local execution workspace roots', () => {
|
|
4
|
+
it('uses the current working directory boundary when config is omitted', () => {
|
|
5
|
+
expect(getWriteRoots()).toEqual([process.cwd()]);
|
|
6
|
+
expect(getReadRoots()).toEqual([process.cwd()]);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -1064,8 +1064,11 @@ for member in team:
|
|
|
1064
1064
|
});
|
|
1065
1065
|
|
|
1066
1066
|
describe('bash bridge script does not require python3 (Codex P2 #19)', () => {
|
|
1067
|
-
|
|
1068
|
-
const {
|
|
1067
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
1068
|
+
const {
|
|
1069
|
+
_createBashProgramForTests,
|
|
1070
|
+
} = require('../local/LocalProgrammaticToolCalling');
|
|
1071
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
1069
1072
|
|
|
1070
1073
|
it('uses curl as the primary HTTP helper with python3 only as fallback', () => {
|
|
1071
1074
|
const script: string = _createBashProgramForTests(
|
|
@@ -1129,6 +1132,7 @@ for member in team:
|
|
|
1129
1132
|
const registry = new HookRegistry();
|
|
1130
1133
|
registry.register('PreToolUse', {
|
|
1131
1134
|
hooks: [
|
|
1135
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
1132
1136
|
async (input) => {
|
|
1133
1137
|
if (input.toolName === 'write_file') {
|
|
1134
1138
|
return { decision: 'deny', reason: 'no writes from bridge' };
|
|
@@ -1179,6 +1183,7 @@ for member in team:
|
|
|
1179
1183
|
|
|
1180
1184
|
const registry = new HookRegistry();
|
|
1181
1185
|
registry.register('PreToolUse', {
|
|
1186
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
1182
1187
|
hooks: [async () => ({ decision: 'allow' })],
|
|
1183
1188
|
});
|
|
1184
1189
|
|
|
@@ -1200,6 +1205,7 @@ for member in team:
|
|
|
1200
1205
|
const registry = new HookRegistry();
|
|
1201
1206
|
registry.register('PreToolUse', {
|
|
1202
1207
|
hooks: [
|
|
1208
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
1203
1209
|
async () => ({
|
|
1204
1210
|
decision: 'allow',
|
|
1205
1211
|
updatedInput: { file_path: '/tmp/rewritten' },
|
|
@@ -1224,6 +1230,7 @@ for member in team:
|
|
|
1224
1230
|
|
|
1225
1231
|
const registry = new HookRegistry();
|
|
1226
1232
|
registry.register('PreToolUse', {
|
|
1233
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
1227
1234
|
hooks: [async () => ({ decision: 'ask' })],
|
|
1228
1235
|
});
|
|
1229
1236
|
|
|
@@ -51,14 +51,22 @@ function createAIMessageWithCodeCall(callId: string): AIMessage {
|
|
|
51
51
|
|
|
52
52
|
describe('ToolNode code execution session management', () => {
|
|
53
53
|
describe('session injection via runTool (direct execution)', () => {
|
|
54
|
-
it('injects
|
|
54
|
+
it('injects session ids (both names) and _injected_files when session has files', async () => {
|
|
55
55
|
const capturedConfigs: Record<string, unknown>[] = [];
|
|
56
56
|
const sessions: t.ToolSessionMap = new Map();
|
|
57
57
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
58
58
|
session_id: 'prev-session-abc',
|
|
59
59
|
files: [
|
|
60
|
-
{
|
|
61
|
-
|
|
60
|
+
{
|
|
61
|
+
id: 'file1',
|
|
62
|
+
name: 'data.csv',
|
|
63
|
+
storage_session_id: 'prev-session-abc',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'file2',
|
|
67
|
+
name: 'chart.png',
|
|
68
|
+
storage_session_id: 'prev-session-abc',
|
|
69
|
+
},
|
|
62
70
|
],
|
|
63
71
|
lastUpdated: Date.now(),
|
|
64
72
|
} satisfies t.CodeSessionContext);
|
|
@@ -70,14 +78,26 @@ describe('ToolNode code execution session management', () => {
|
|
|
70
78
|
await toolNode.invoke({ messages: [aiMsg] });
|
|
71
79
|
|
|
72
80
|
expect(capturedConfigs).toHaveLength(1);
|
|
81
|
+
/* Both names injected so pre- and post-rename consumers see the
|
|
82
|
+
* field they expect. */
|
|
73
83
|
expect(capturedConfigs[0].session_id).toBe('prev-session-abc');
|
|
74
84
|
expect(capturedConfigs[0]._injected_files).toEqual([
|
|
75
|
-
{
|
|
76
|
-
|
|
85
|
+
{
|
|
86
|
+
id: 'file1',
|
|
87
|
+
name: 'data.csv',
|
|
88
|
+
storage_session_id: 'prev-session-abc',
|
|
89
|
+
kind: 'user',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'file2',
|
|
93
|
+
name: 'chart.png',
|
|
94
|
+
storage_session_id: 'prev-session-abc',
|
|
95
|
+
kind: 'user',
|
|
96
|
+
},
|
|
77
97
|
]);
|
|
78
98
|
});
|
|
79
99
|
|
|
80
|
-
it('injects
|
|
100
|
+
it('injects session ids even when session has no tracked files', async () => {
|
|
81
101
|
const capturedConfigs: Record<string, unknown>[] = [];
|
|
82
102
|
const sessions: t.ToolSessionMap = new Map();
|
|
83
103
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
@@ -112,14 +132,22 @@ describe('ToolNode code execution session management', () => {
|
|
|
112
132
|
expect(capturedConfigs[0]._injected_files).toBeUndefined();
|
|
113
133
|
});
|
|
114
134
|
|
|
115
|
-
it('preserves per-file
|
|
135
|
+
it('preserves per-file storage_session_id for multi-session files', async () => {
|
|
116
136
|
const capturedConfigs: Record<string, unknown>[] = [];
|
|
117
137
|
const sessions: t.ToolSessionMap = new Map();
|
|
118
138
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
119
139
|
session_id: 'session-B',
|
|
120
140
|
files: [
|
|
121
|
-
{
|
|
122
|
-
|
|
141
|
+
{
|
|
142
|
+
id: 'f1',
|
|
143
|
+
name: 'old.csv',
|
|
144
|
+
storage_session_id: 'session-A',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'f2',
|
|
148
|
+
name: 'new.png',
|
|
149
|
+
storage_session_id: 'session-B',
|
|
150
|
+
},
|
|
123
151
|
],
|
|
124
152
|
lastUpdated: Date.now(),
|
|
125
153
|
} satisfies t.CodeSessionContext);
|
|
@@ -131,26 +159,27 @@ describe('ToolNode code execution session management', () => {
|
|
|
131
159
|
await toolNode.invoke({ messages: [aiMsg] });
|
|
132
160
|
|
|
133
161
|
const files = capturedConfigs[0]._injected_files as t.CodeEnvFile[];
|
|
134
|
-
expect(files[0].
|
|
135
|
-
expect(files[1].
|
|
162
|
+
expect(files[0].storage_session_id).toBe('session-A');
|
|
163
|
+
expect(files[1].storage_session_id).toBe('session-B');
|
|
136
164
|
});
|
|
137
165
|
|
|
138
|
-
it('forwards per-file
|
|
166
|
+
it('forwards per-file kind and version for mixed-kind sessions', async () => {
|
|
139
167
|
const capturedConfigs: Record<string, unknown>[] = [];
|
|
140
168
|
const sessions: t.ToolSessionMap = new Map();
|
|
141
169
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
142
170
|
session_id: 'session-A',
|
|
143
171
|
files: [
|
|
144
172
|
{
|
|
145
|
-
id: 'skill-
|
|
173
|
+
id: 'skill-123',
|
|
146
174
|
name: 'demo/SKILL.md',
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
storage_session_id: 'session-A',
|
|
176
|
+
kind: 'skill',
|
|
177
|
+
version: 7,
|
|
149
178
|
},
|
|
150
179
|
{
|
|
151
180
|
id: 'user-file',
|
|
152
181
|
name: 'attachment.csv',
|
|
153
|
-
|
|
182
|
+
storage_session_id: 'session-B',
|
|
154
183
|
},
|
|
155
184
|
],
|
|
156
185
|
lastUpdated: Date.now(),
|
|
@@ -165,15 +194,17 @@ describe('ToolNode code execution session management', () => {
|
|
|
165
194
|
const files = capturedConfigs[0]._injected_files as t.CodeEnvFile[];
|
|
166
195
|
expect(files).toEqual([
|
|
167
196
|
{
|
|
168
|
-
|
|
169
|
-
id: 'skill-file',
|
|
197
|
+
id: 'skill-123',
|
|
170
198
|
name: 'demo/SKILL.md',
|
|
171
|
-
|
|
199
|
+
storage_session_id: 'session-A',
|
|
200
|
+
kind: 'skill',
|
|
201
|
+
version: 7,
|
|
172
202
|
},
|
|
173
203
|
{
|
|
174
|
-
session_id: 'session-B',
|
|
175
204
|
id: 'user-file',
|
|
176
205
|
name: 'attachment.csv',
|
|
206
|
+
storage_session_id: 'session-B',
|
|
207
|
+
kind: 'user',
|
|
177
208
|
},
|
|
178
209
|
]);
|
|
179
210
|
});
|
|
@@ -184,7 +215,13 @@ describe('ToolNode code execution session management', () => {
|
|
|
184
215
|
const sessions: t.ToolSessionMap = new Map();
|
|
185
216
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
186
217
|
session_id: 'evt-session',
|
|
187
|
-
files: [
|
|
218
|
+
files: [
|
|
219
|
+
{
|
|
220
|
+
id: 'ef1',
|
|
221
|
+
name: 'out.parquet',
|
|
222
|
+
storage_session_id: 'evt-session',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
188
225
|
lastUpdated: Date.now(),
|
|
189
226
|
} satisfies t.CodeSessionContext);
|
|
190
227
|
|
|
@@ -201,7 +238,14 @@ describe('ToolNode code execution session management', () => {
|
|
|
201
238
|
|
|
202
239
|
expect(context).toEqual({
|
|
203
240
|
session_id: 'evt-session',
|
|
204
|
-
files: [
|
|
241
|
+
files: [
|
|
242
|
+
{
|
|
243
|
+
id: 'ef1',
|
|
244
|
+
name: 'out.parquet',
|
|
245
|
+
storage_session_id: 'evt-session',
|
|
246
|
+
kind: 'user',
|
|
247
|
+
},
|
|
248
|
+
],
|
|
205
249
|
});
|
|
206
250
|
});
|
|
207
251
|
|
|
@@ -224,7 +268,9 @@ describe('ToolNode code execution session management', () => {
|
|
|
224
268
|
toolNode as unknown as { getCodeSessionContext: () => unknown }
|
|
225
269
|
).getCodeSessionContext();
|
|
226
270
|
|
|
227
|
-
expect(context).toEqual({
|
|
271
|
+
expect(context).toEqual({
|
|
272
|
+
session_id: 'evt-session-empty',
|
|
273
|
+
});
|
|
228
274
|
});
|
|
229
275
|
|
|
230
276
|
it('returns undefined when no session exists', () => {
|
|
@@ -244,18 +290,23 @@ describe('ToolNode code execution session management', () => {
|
|
|
244
290
|
expect(context).toBeUndefined();
|
|
245
291
|
});
|
|
246
292
|
|
|
247
|
-
it('forwards per-file
|
|
293
|
+
it('forwards per-file kind and version to event-driven request context', () => {
|
|
248
294
|
const sessions: t.ToolSessionMap = new Map();
|
|
249
295
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
250
296
|
session_id: 'evt-session',
|
|
251
297
|
files: [
|
|
252
298
|
{
|
|
253
|
-
id: '
|
|
299
|
+
id: 'skill-abc',
|
|
254
300
|
name: 'demo/SKILL.md',
|
|
255
|
-
|
|
256
|
-
|
|
301
|
+
storage_session_id: 'evt-session',
|
|
302
|
+
kind: 'skill',
|
|
303
|
+
version: 3,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: 'usr1',
|
|
307
|
+
name: 'data.csv',
|
|
308
|
+
storage_session_id: 'evt-session',
|
|
257
309
|
},
|
|
258
|
-
{ id: 'usr1', name: 'data.csv', session_id: 'evt-session' },
|
|
259
310
|
],
|
|
260
311
|
lastUpdated: Date.now(),
|
|
261
312
|
} satisfies t.CodeSessionContext);
|
|
@@ -275,12 +326,18 @@ describe('ToolNode code execution session management', () => {
|
|
|
275
326
|
session_id: 'evt-session',
|
|
276
327
|
files: [
|
|
277
328
|
{
|
|
278
|
-
|
|
279
|
-
id: 'sk1',
|
|
329
|
+
id: 'skill-abc',
|
|
280
330
|
name: 'demo/SKILL.md',
|
|
281
|
-
|
|
331
|
+
storage_session_id: 'evt-session',
|
|
332
|
+
kind: 'skill',
|
|
333
|
+
version: 3,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: 'usr1',
|
|
337
|
+
name: 'data.csv',
|
|
338
|
+
storage_session_id: 'evt-session',
|
|
339
|
+
kind: 'user',
|
|
282
340
|
},
|
|
283
|
-
{ session_id: 'evt-session', id: 'usr1', name: 'data.csv' },
|
|
284
341
|
],
|
|
285
342
|
});
|
|
286
343
|
});
|
|
@@ -333,7 +390,7 @@ describe('ToolNode code execution session management', () => {
|
|
|
333
390
|
expect.objectContaining({
|
|
334
391
|
id: 'f1',
|
|
335
392
|
name: 'result.csv',
|
|
336
|
-
|
|
393
|
+
storage_session_id: 'new-sess',
|
|
337
394
|
})
|
|
338
395
|
);
|
|
339
396
|
});
|
|
@@ -384,8 +441,8 @@ describe('ToolNode code execution session management', () => {
|
|
|
384
441
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
385
442
|
session_id: 'old-sess',
|
|
386
443
|
files: [
|
|
387
|
-
{ id: 'f1', name: 'data.csv',
|
|
388
|
-
{ id: 'f2', name: 'chart.png',
|
|
444
|
+
{ id: 'f1', name: 'data.csv', storage_session_id: 'old-sess' },
|
|
445
|
+
{ id: 'f2', name: 'chart.png', storage_session_id: 'old-sess' },
|
|
389
446
|
],
|
|
390
447
|
lastUpdated: Date.now(),
|
|
391
448
|
} satisfies t.CodeSessionContext);
|
|
@@ -430,18 +487,18 @@ describe('ToolNode code execution session management', () => {
|
|
|
430
487
|
expect(stored.files).toHaveLength(2);
|
|
431
488
|
|
|
432
489
|
const csvFile = stored.files!.find((f) => f.name === 'data.csv');
|
|
433
|
-
expect(csvFile!.
|
|
490
|
+
expect(csvFile!.storage_session_id).toBe('old-sess');
|
|
434
491
|
|
|
435
492
|
const chartFile = stored.files!.find((f) => f.name === 'chart.png');
|
|
436
493
|
expect(chartFile!.id).toBe('f3');
|
|
437
|
-
expect(chartFile!.
|
|
494
|
+
expect(chartFile!.storage_session_id).toBe('new-sess');
|
|
438
495
|
});
|
|
439
496
|
|
|
440
497
|
it('preserves existing files when new execution has no files', () => {
|
|
441
498
|
const sessions: t.ToolSessionMap = new Map();
|
|
442
499
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
443
500
|
session_id: 'old-sess',
|
|
444
|
-
files: [{ id: 'f1', name: 'data.csv',
|
|
501
|
+
files: [{ id: 'f1', name: 'data.csv', storage_session_id: 'old-sess' }],
|
|
445
502
|
lastUpdated: Date.now(),
|
|
446
503
|
} satisfies t.CodeSessionContext);
|
|
447
504
|
|
|
@@ -507,7 +564,7 @@ describe('ToolNode code execution session management', () => {
|
|
|
507
564
|
{
|
|
508
565
|
toolCallId: 'tc5',
|
|
509
566
|
content: 'search results',
|
|
510
|
-
artifact: {
|
|
567
|
+
artifact: { storage_session_id: 'should-not-store' },
|
|
511
568
|
status: 'success',
|
|
512
569
|
},
|
|
513
570
|
],
|
|
@@ -597,9 +654,13 @@ describe('ToolNode code execution session management', () => {
|
|
|
597
654
|
{
|
|
598
655
|
id: 'f1',
|
|
599
656
|
name: 'sentinel.txt',
|
|
600
|
-
|
|
657
|
+
storage_session_id: 'storage-session-A',
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: 'f2',
|
|
661
|
+
name: 'data.csv',
|
|
662
|
+
storage_session_id: 'storage-session-B',
|
|
601
663
|
},
|
|
602
|
-
{ id: 'f2', name: 'data.csv', session_id: 'storage-session-B' },
|
|
603
664
|
],
|
|
604
665
|
},
|
|
605
666
|
status: 'success',
|
|
@@ -617,18 +678,20 @@ describe('ToolNode code execution session management', () => {
|
|
|
617
678
|
Constants.EXECUTE_CODE
|
|
618
679
|
) as t.CodeSessionContext;
|
|
619
680
|
/* The session-level id is the (latest) exec id — fine for tracking
|
|
620
|
-
"what session ran last" — but per-file storage ids must survive.
|
|
681
|
+
"what session ran last" — but per-file storage ids must survive.
|
|
682
|
+
After the rename, both names appear at the top level (exec) and
|
|
683
|
+
on each file (storage). */
|
|
621
684
|
expect(stored.session_id).toBe('exec-session-123');
|
|
622
685
|
expect(stored.files).toHaveLength(2);
|
|
623
686
|
expect(stored.files![0]).toEqual({
|
|
624
687
|
id: 'f1',
|
|
625
688
|
name: 'sentinel.txt',
|
|
626
|
-
|
|
689
|
+
storage_session_id: 'storage-session-A',
|
|
627
690
|
});
|
|
628
691
|
expect(stored.files![1]).toEqual({
|
|
629
692
|
id: 'f2',
|
|
630
693
|
name: 'data.csv',
|
|
631
|
-
|
|
694
|
+
storage_session_id: 'storage-session-B',
|
|
632
695
|
});
|
|
633
696
|
});
|
|
634
697
|
|
|
@@ -658,7 +721,11 @@ describe('ToolNode code execution session management', () => {
|
|
|
658
721
|
session_id: 'exec-mixed',
|
|
659
722
|
files: [
|
|
660
723
|
/* Mix: one file with storage id, one without (older payload). */
|
|
661
|
-
{
|
|
724
|
+
{
|
|
725
|
+
id: 'f1',
|
|
726
|
+
name: 'fresh.csv',
|
|
727
|
+
storage_session_id: 'storage-fresh',
|
|
728
|
+
},
|
|
662
729
|
{ id: 'f2', name: 'legacy.csv' },
|
|
663
730
|
],
|
|
664
731
|
},
|
|
@@ -676,9 +743,10 @@ describe('ToolNode code execution session management', () => {
|
|
|
676
743
|
const stored = sessions.get(
|
|
677
744
|
Constants.EXECUTE_CODE
|
|
678
745
|
) as t.CodeSessionContext;
|
|
679
|
-
expect(stored.files![0].
|
|
680
|
-
/* Fallback only when the per-file id is missing
|
|
681
|
-
|
|
746
|
+
expect(stored.files![0].storage_session_id).toBe('storage-fresh');
|
|
747
|
+
/* Fallback only when the per-file id is missing — the fallback
|
|
748
|
+
* value is the exec session id. */
|
|
749
|
+
expect(stored.files![1].storage_session_id).toBe('exec-mixed');
|
|
682
750
|
});
|
|
683
751
|
});
|
|
684
752
|
|
|
@@ -728,7 +796,13 @@ describe('ToolNode code execution session management', () => {
|
|
|
728
796
|
const sessions: t.ToolSessionMap = new Map();
|
|
729
797
|
sessions.set(Constants.EXECUTE_CODE, {
|
|
730
798
|
session_id: 'rf-session',
|
|
731
|
-
files: [
|
|
799
|
+
files: [
|
|
800
|
+
{
|
|
801
|
+
id: 'rf1',
|
|
802
|
+
name: 'data.csv',
|
|
803
|
+
storage_session_id: 'rf-session',
|
|
804
|
+
},
|
|
805
|
+
],
|
|
732
806
|
lastUpdated: Date.now(),
|
|
733
807
|
} satisfies t.CodeSessionContext);
|
|
734
808
|
|
|
@@ -758,7 +832,14 @@ describe('ToolNode code execution session management', () => {
|
|
|
758
832
|
expect(capturedRequests[0].name).toBe(Constants.READ_FILE);
|
|
759
833
|
expect(capturedRequests[0].codeSessionContext).toEqual({
|
|
760
834
|
session_id: 'rf-session',
|
|
761
|
-
files: [
|
|
835
|
+
files: [
|
|
836
|
+
{
|
|
837
|
+
id: 'rf1',
|
|
838
|
+
name: 'data.csv',
|
|
839
|
+
storage_session_id: 'rf-session',
|
|
840
|
+
kind: 'user',
|
|
841
|
+
},
|
|
842
|
+
],
|
|
762
843
|
});
|
|
763
844
|
});
|
|
764
845
|
|