@librechat/agents 3.1.72 → 3.1.74
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/agents/AgentContext.cjs +62 -20
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +11 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +27 -1
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +21 -11
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +37 -10
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -11
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +21 -2
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +62 -20
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +11 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/format.mjs +27 -1
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +22 -12
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +37 -11
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -12
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +21 -2
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +15 -0
- package/dist/types/messages/format.d.ts +11 -1
- package/dist/types/tools/CodeExecutor.d.ts +6 -0
- package/dist/types/types/tools.d.ts +9 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +66 -27
- package/src/agents/__tests__/AgentContext.test.ts +178 -0
- package/src/graphs/Graph.ts +12 -1
- package/src/messages/ensureThinkingBlock.test.ts +167 -0
- package/src/messages/format.ts +29 -1
- package/src/tools/BashExecutor.ts +37 -13
- package/src/tools/CodeExecutor.ts +55 -11
- package/src/tools/ProgrammaticToolCalling.ts +29 -14
- package/src/tools/ToolNode.ts +21 -2
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +60 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +124 -0
- package/src/types/tools.ts +9 -0
|
@@ -15,10 +15,41 @@ export const getCodeBaseURL = (): string =>
|
|
|
15
15
|
|
|
16
16
|
const imageMessage = 'Image is already displayed to the user';
|
|
17
17
|
const otherMessage = 'File is already downloaded by the user';
|
|
18
|
+
const inheritedFileMessage =
|
|
19
|
+
'Available as an input — already known to the user';
|
|
18
20
|
const accessMessage =
|
|
19
21
|
'Note: Files from previous executions are automatically available and can be modified.';
|
|
20
22
|
const emptyOutputMessage =
|
|
21
23
|
'stdout: Empty. Ensure you\'re writing output explicitly.\n';
|
|
24
|
+
const inheritedFilesHeader =
|
|
25
|
+
'Available files (inputs, not generated by this execution):';
|
|
26
|
+
const generatedFilesHeader = 'Generated files:';
|
|
27
|
+
const inheritedNote =
|
|
28
|
+
'Note: Files in "Available files" are inputs the user (or a skill) already provided to the sandbox. They were not produced by this execution and you should not present them as new outputs in your response.';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Renders one section of the post-execution file listing. Used by the
|
|
32
|
+
* code/bash tool formatters to keep generated outputs and inherited
|
|
33
|
+
* inputs visually separated. See BashExecutor for full docs.
|
|
34
|
+
*/
|
|
35
|
+
export function renderFileSection(
|
|
36
|
+
header: string,
|
|
37
|
+
files: t.FileRefs,
|
|
38
|
+
defaultMessage: string
|
|
39
|
+
): string {
|
|
40
|
+
if (files.length === 0) return '';
|
|
41
|
+
let out = `${header}\n`;
|
|
42
|
+
for (let i = 0; i < files.length; i++) {
|
|
43
|
+
const file = files[i];
|
|
44
|
+
const isImage = imageExtRegex.test(file.name);
|
|
45
|
+
out += `- /mnt/data/${file.name} | ${isImage ? imageMessage : defaultMessage}`;
|
|
46
|
+
if (i < files.length - 1) {
|
|
47
|
+
out += files.length <= 3 ? ', ' : ',\n';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
out += '\n';
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
22
53
|
|
|
23
54
|
const SUPPORTED_LANGUAGES = [
|
|
24
55
|
'py',
|
|
@@ -196,20 +227,33 @@ function createCodeExecutionTool(
|
|
|
196
227
|
}
|
|
197
228
|
if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
|
|
198
229
|
if (result.files && result.files.length > 0) {
|
|
199
|
-
|
|
230
|
+
/* See BashExecutor for the rationale: split inherited (read-only
|
|
231
|
+
* passthrough) inputs from real generated outputs so the LLM
|
|
232
|
+
* doesn't conflate skill files with newly-produced artifacts. */
|
|
233
|
+
const inheritedFiles = result.files.filter(
|
|
234
|
+
(f) => f.inherited === true
|
|
235
|
+
);
|
|
236
|
+
const generatedFiles = result.files.filter(
|
|
237
|
+
(f) => f.inherited !== true
|
|
238
|
+
);
|
|
200
239
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
240
|
+
formattedOutput += renderFileSection(
|
|
241
|
+
generatedFilesHeader,
|
|
242
|
+
generatedFiles,
|
|
243
|
+
otherMessage
|
|
244
|
+
);
|
|
245
|
+
formattedOutput += renderFileSection(
|
|
246
|
+
inheritedFilesHeader,
|
|
247
|
+
inheritedFiles,
|
|
248
|
+
inheritedFileMessage
|
|
249
|
+
);
|
|
206
250
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
if (generatedFiles.length > 0) {
|
|
252
|
+
formattedOutput += `\n\n${accessMessage}`;
|
|
253
|
+
}
|
|
254
|
+
if (inheritedFiles.length > 0) {
|
|
255
|
+
formattedOutput += `\n\n${inheritedNote}`;
|
|
210
256
|
}
|
|
211
|
-
|
|
212
|
-
formattedOutput += `\n\n${accessMessage}`;
|
|
213
257
|
return [
|
|
214
258
|
formattedOutput.trim(),
|
|
215
259
|
{
|
|
@@ -5,7 +5,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
|
5
5
|
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
6
6
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
7
7
|
import type * as t from '@/types';
|
|
8
|
-
import {
|
|
8
|
+
import { getCodeBaseURL, renderFileSection } from './CodeExecutor';
|
|
9
9
|
import { Constants } from '@/common';
|
|
10
10
|
|
|
11
11
|
config();
|
|
@@ -14,8 +14,14 @@ config();
|
|
|
14
14
|
// Constants
|
|
15
15
|
// ============================================================================
|
|
16
16
|
|
|
17
|
-
const imageMessage = 'Image is already displayed to the user';
|
|
18
17
|
const otherMessage = 'File is already downloaded by the user';
|
|
18
|
+
const inheritedFileMessage =
|
|
19
|
+
'Available as an input — already known to the user';
|
|
20
|
+
const inheritedFilesHeader =
|
|
21
|
+
'Available files (inputs, not generated by this execution):';
|
|
22
|
+
const generatedFilesHeader = 'Generated files:';
|
|
23
|
+
const inheritedNote =
|
|
24
|
+
'Note: Files in "Available files" are inputs the user (or a skill) already provided to the sandbox. They were not produced by this execution and you should not present them as new outputs in your response.';
|
|
19
25
|
const accessMessage =
|
|
20
26
|
'Note: Files from previous executions are automatically available and can be modified.';
|
|
21
27
|
const emptyOutputMessage =
|
|
@@ -552,20 +558,29 @@ export function formatCompletedResponse(
|
|
|
552
558
|
}
|
|
553
559
|
|
|
554
560
|
if (response.files && response.files.length > 0) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
561
|
+
/* See BashExecutor for the rationale: split inherited (read-only
|
|
562
|
+
* passthrough) inputs from real generated outputs so the LLM doesn't
|
|
563
|
+
* conflate skill files with newly-produced artifacts. */
|
|
564
|
+
const inheritedFiles = response.files.filter((f) => f.inherited === true);
|
|
565
|
+
const generatedFiles = response.files.filter((f) => f.inherited !== true);
|
|
566
|
+
|
|
567
|
+
formatted += renderFileSection(
|
|
568
|
+
generatedFilesHeader,
|
|
569
|
+
generatedFiles,
|
|
570
|
+
otherMessage
|
|
571
|
+
);
|
|
572
|
+
formatted += renderFileSection(
|
|
573
|
+
inheritedFilesHeader,
|
|
574
|
+
inheritedFiles,
|
|
575
|
+
inheritedFileMessage
|
|
576
|
+
);
|
|
562
577
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
578
|
+
if (generatedFiles.length > 0) {
|
|
579
|
+
formatted += `\n\n${accessMessage}`;
|
|
580
|
+
}
|
|
581
|
+
if (inheritedFiles.length > 0) {
|
|
582
|
+
formatted += `\n\n${inheritedNote}`;
|
|
566
583
|
}
|
|
567
|
-
|
|
568
|
-
formatted += `\n\n${accessMessage}`;
|
|
569
584
|
}
|
|
570
585
|
|
|
571
586
|
return [
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -89,7 +89,26 @@ function isSend(value: unknown): value is Send {
|
|
|
89
89
|
return value instanceof Send;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Merges code execution session context into the sessions map.
|
|
94
|
+
*
|
|
95
|
+
* The codeapi worker reports two distinct ids on a code-execution result:
|
|
96
|
+
* - `artifact.session_id` (the `sessionId` arg here) is the EXEC session
|
|
97
|
+
* — the sandbox VM that ran the code. It's transient and torn down
|
|
98
|
+
* post-execution; subsequent calls cannot reuse it as a sandbox.
|
|
99
|
+
* - `file.session_id` on each `artifact.files[i]` is the STORAGE
|
|
100
|
+
* session — the file-server bucket prefix where the artifact actually
|
|
101
|
+
* lives and is served from.
|
|
102
|
+
*
|
|
103
|
+
* Per-file `session_id` is preserved (not overwritten with the exec id)
|
|
104
|
+
* because `_injected_files` are looked up against the file-server's
|
|
105
|
+
* storage path on subsequent tool calls. Stomping the storage id with
|
|
106
|
+
* the exec id silently 404s every follow-up tool call within the same
|
|
107
|
+
* run — `cat /mnt/data/foo.txt` reports "No such file or directory"
|
|
108
|
+
* because the worker can't mount a file at a path the storage doesn't
|
|
109
|
+
* know about. Fall back to `sessionId` only when the per-file id is
|
|
110
|
+
* absent (older worker payloads).
|
|
111
|
+
*/
|
|
93
112
|
function updateCodeSession(
|
|
94
113
|
sessions: t.ToolSessionMap,
|
|
95
114
|
sessionId: string,
|
|
@@ -104,7 +123,7 @@ function updateCodeSession(
|
|
|
104
123
|
if (newFiles.length > 0) {
|
|
105
124
|
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
106
125
|
...file,
|
|
107
|
-
session_id: sessionId,
|
|
126
|
+
session_id: file.session_id ?? sessionId,
|
|
108
127
|
}));
|
|
109
128
|
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
110
129
|
const filteredExisting = existingFiles.filter(
|
|
@@ -664,6 +664,66 @@ for member in team:
|
|
|
664
664
|
expect(output).toContain('chart.png');
|
|
665
665
|
expect(output).toContain('Image is already displayed to the user');
|
|
666
666
|
});
|
|
667
|
+
|
|
668
|
+
it('splits inherited inputs from generated outputs into distinct sections', () => {
|
|
669
|
+
const response: t.ProgrammaticExecutionResponse = {
|
|
670
|
+
status: 'completed',
|
|
671
|
+
stdout: 'analysis done\n',
|
|
672
|
+
stderr: '',
|
|
673
|
+
files: [
|
|
674
|
+
{ id: 'g1', name: 'report.pdf' },
|
|
675
|
+
{ id: 'i1', name: 'pptx/SKILL.md', inherited: true },
|
|
676
|
+
{ id: 'i2', name: 'pptx/scripts/clean.py', inherited: true },
|
|
677
|
+
{ id: 'g2', name: 'chart.png' },
|
|
678
|
+
],
|
|
679
|
+
session_id: 'sess_abc123',
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const [output, artifact] = formatCompletedResponse(response);
|
|
683
|
+
|
|
684
|
+
/* Generated section lists only outputs the run produced. */
|
|
685
|
+
const generatedIdx = output.indexOf('Generated files:');
|
|
686
|
+
const inheritedIdx = output.indexOf('Available files (inputs');
|
|
687
|
+
expect(generatedIdx).toBeGreaterThan(-1);
|
|
688
|
+
expect(inheritedIdx).toBeGreaterThan(generatedIdx);
|
|
689
|
+
|
|
690
|
+
/* Slice each section so we can assert membership without
|
|
691
|
+
* cross-talk between the two listings. */
|
|
692
|
+
const generatedSection = output.slice(generatedIdx, inheritedIdx);
|
|
693
|
+
const inheritedSection = output.slice(inheritedIdx);
|
|
694
|
+
|
|
695
|
+
expect(generatedSection).toContain('report.pdf');
|
|
696
|
+
expect(generatedSection).toContain('chart.png');
|
|
697
|
+
expect(generatedSection).not.toContain('SKILL.md');
|
|
698
|
+
|
|
699
|
+
expect(inheritedSection).toContain('pptx/SKILL.md');
|
|
700
|
+
expect(inheritedSection).toContain('pptx/scripts/clean.py');
|
|
701
|
+
expect(inheritedSection).toContain('Available as an input');
|
|
702
|
+
|
|
703
|
+
/* The artifact still carries every file so the host can still
|
|
704
|
+
* thread per-file ids through to subsequent calls. */
|
|
705
|
+
expect(artifact.files).toHaveLength(4);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('omits the Generated files header when every entry is inherited', () => {
|
|
709
|
+
const response: t.ProgrammaticExecutionResponse = {
|
|
710
|
+
status: 'completed',
|
|
711
|
+
stdout: 'cat: ok\n',
|
|
712
|
+
stderr: '',
|
|
713
|
+
files: [
|
|
714
|
+
{ id: 'i1', name: 'pptx/SKILL.md', inherited: true },
|
|
715
|
+
{ id: 'i2', name: 'pptx/editing.md', inherited: true },
|
|
716
|
+
],
|
|
717
|
+
session_id: 'sess_abc123',
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const [output] = formatCompletedResponse(response);
|
|
721
|
+
|
|
722
|
+
expect(output).not.toContain('Generated files:');
|
|
723
|
+
expect(output).toContain('Available files (inputs');
|
|
724
|
+
expect(output).toContain('pptx/SKILL.md');
|
|
725
|
+
expect(output).toContain('pptx/editing.md');
|
|
726
|
+
});
|
|
667
727
|
});
|
|
668
728
|
|
|
669
729
|
describe('createProgrammaticToolCallingTool - Manual Invocation', () => {
|
|
@@ -472,6 +472,130 @@ describe('ToolNode code execution session management', () => {
|
|
|
472
472
|
|
|
473
473
|
expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
|
|
474
474
|
});
|
|
475
|
+
|
|
476
|
+
it('preserves per-file storage session_id (not overwritten with the exec session_id)', () => {
|
|
477
|
+
/**
|
|
478
|
+
* Regression: the codeapi worker reports `artifact.session_id` (EXEC
|
|
479
|
+
* session — torn down post-run) and per-file `session_id` (STORAGE
|
|
480
|
+
* session where the file lives). Stomping the storage id with the
|
|
481
|
+
* exec id silently 404s every follow-up tool call within the same
|
|
482
|
+
* run because `_injected_files` carry the wrong path on the next
|
|
483
|
+
* `/exec`. The worker tries to mount `<exec_session>/<id>` against
|
|
484
|
+
* file-server, gets 404, mounts nothing — `cat /mnt/data/foo.txt`
|
|
485
|
+
* → "No such file or directory".
|
|
486
|
+
*/
|
|
487
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
488
|
+
const mockTool = createMockCodeTool({ capturedConfigs: [] });
|
|
489
|
+
const toolNode = new ToolNode({
|
|
490
|
+
tools: [mockTool],
|
|
491
|
+
sessions,
|
|
492
|
+
eventDrivenMode: true,
|
|
493
|
+
});
|
|
494
|
+
const storeMethod = (
|
|
495
|
+
toolNode as unknown as {
|
|
496
|
+
storeCodeSessionFromResults: (
|
|
497
|
+
results: t.ToolExecuteResult[],
|
|
498
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
499
|
+
) => void;
|
|
500
|
+
}
|
|
501
|
+
).storeCodeSessionFromResults.bind(toolNode);
|
|
502
|
+
|
|
503
|
+
storeMethod(
|
|
504
|
+
[
|
|
505
|
+
{
|
|
506
|
+
toolCallId: 'tc-storage',
|
|
507
|
+
content: 'output',
|
|
508
|
+
artifact: {
|
|
509
|
+
/* EXEC session — transient, torn down after this run */
|
|
510
|
+
session_id: 'exec-session-123',
|
|
511
|
+
files: [
|
|
512
|
+
/* STORAGE session — persistent file-server bucket prefix */
|
|
513
|
+
{
|
|
514
|
+
id: 'f1',
|
|
515
|
+
name: 'sentinel.txt',
|
|
516
|
+
session_id: 'storage-session-A',
|
|
517
|
+
},
|
|
518
|
+
{ id: 'f2', name: 'data.csv', session_id: 'storage-session-B' },
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
status: 'success',
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
new Map([
|
|
525
|
+
[
|
|
526
|
+
'tc-storage',
|
|
527
|
+
{ id: 'tc-storage', name: Constants.EXECUTE_CODE, args: {} },
|
|
528
|
+
],
|
|
529
|
+
])
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const stored = sessions.get(
|
|
533
|
+
Constants.EXECUTE_CODE
|
|
534
|
+
) as t.CodeSessionContext;
|
|
535
|
+
/* The session-level id is the (latest) exec id — fine for tracking
|
|
536
|
+
"what session ran last" — but per-file storage ids must survive. */
|
|
537
|
+
expect(stored.session_id).toBe('exec-session-123');
|
|
538
|
+
expect(stored.files).toHaveLength(2);
|
|
539
|
+
expect(stored.files![0]).toEqual({
|
|
540
|
+
id: 'f1',
|
|
541
|
+
name: 'sentinel.txt',
|
|
542
|
+
session_id: 'storage-session-A',
|
|
543
|
+
});
|
|
544
|
+
expect(stored.files![1]).toEqual({
|
|
545
|
+
id: 'f2',
|
|
546
|
+
name: 'data.csv',
|
|
547
|
+
session_id: 'storage-session-B',
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('falls back to exec session_id only when per-file session_id is absent (older worker payloads)', () => {
|
|
552
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
553
|
+
const mockTool = createMockCodeTool({ capturedConfigs: [] });
|
|
554
|
+
const toolNode = new ToolNode({
|
|
555
|
+
tools: [mockTool],
|
|
556
|
+
sessions,
|
|
557
|
+
eventDrivenMode: true,
|
|
558
|
+
});
|
|
559
|
+
const storeMethod = (
|
|
560
|
+
toolNode as unknown as {
|
|
561
|
+
storeCodeSessionFromResults: (
|
|
562
|
+
results: t.ToolExecuteResult[],
|
|
563
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
564
|
+
) => void;
|
|
565
|
+
}
|
|
566
|
+
).storeCodeSessionFromResults.bind(toolNode);
|
|
567
|
+
|
|
568
|
+
storeMethod(
|
|
569
|
+
[
|
|
570
|
+
{
|
|
571
|
+
toolCallId: 'tc-mixed',
|
|
572
|
+
content: 'output',
|
|
573
|
+
artifact: {
|
|
574
|
+
session_id: 'exec-mixed',
|
|
575
|
+
files: [
|
|
576
|
+
/* Mix: one file with storage id, one without (older payload). */
|
|
577
|
+
{ id: 'f1', name: 'fresh.csv', session_id: 'storage-fresh' },
|
|
578
|
+
{ id: 'f2', name: 'legacy.csv' },
|
|
579
|
+
],
|
|
580
|
+
},
|
|
581
|
+
status: 'success',
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
new Map([
|
|
585
|
+
[
|
|
586
|
+
'tc-mixed',
|
|
587
|
+
{ id: 'tc-mixed', name: Constants.EXECUTE_CODE, args: {} },
|
|
588
|
+
],
|
|
589
|
+
])
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const stored = sessions.get(
|
|
593
|
+
Constants.EXECUTE_CODE
|
|
594
|
+
) as t.CodeSessionContext;
|
|
595
|
+
expect(stored.files![0].session_id).toBe('storage-fresh');
|
|
596
|
+
/* Fallback only when the per-file id is missing. */
|
|
597
|
+
expect(stored.files![1].session_id).toBe('exec-mixed');
|
|
598
|
+
});
|
|
475
599
|
});
|
|
476
600
|
|
|
477
601
|
describe('codeSessionContext emission gate (event-driven request building)', () => {
|
package/src/types/tools.ts
CHANGED
|
@@ -113,6 +113,15 @@ export type FileRef = {
|
|
|
113
113
|
path?: string;
|
|
114
114
|
/** Session ID this file belongs to (for multi-session file tracking) */
|
|
115
115
|
session_id?: string;
|
|
116
|
+
/**
|
|
117
|
+
* `true` when the codeapi sandbox echoed this entry as an unchanged
|
|
118
|
+
* passthrough of an input the caller already owns (skill files,
|
|
119
|
+
* downloaded inputs whose hash matched the baseline, inherited
|
|
120
|
+
* `.dirkeep` markers). The tool-result formatter renders these as
|
|
121
|
+
* "Available files" rather than "Generated files" so the LLM doesn't
|
|
122
|
+
* conflate infrastructure inputs with newly-produced outputs.
|
|
123
|
+
*/
|
|
124
|
+
inherited?: true;
|
|
116
125
|
};
|
|
117
126
|
|
|
118
127
|
export type FileRefs = FileRef[];
|