@illuma-ai/agents 1.1.3 → 1.1.5
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 +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +10 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +86 -12
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/fileManifest.cjs +49 -0
- package/dist/cjs/utils/fileManifest.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +10 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +87 -13
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/fileManifest.mjs +46 -0
- package/dist/esm/utils/fileManifest.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/common/constants.d.ts +9 -0
- package/dist/types/types/graph.d.ts +35 -0
- package/dist/types/utils/fileManifest.d.ts +17 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +7 -0
- package/src/common/constants.ts +10 -0
- package/src/graphs/Graph.ts +92 -13
- package/src/graphs/gapFeatures.test.ts +246 -8
- package/src/types/graph.ts +36 -0
- package/src/utils/fileManifest.ts +49 -0
- package/src/utils/index.ts +1 -0
|
@@ -636,11 +636,16 @@ describe('Proactive Summarization — Context Pressure', () => {
|
|
|
636
636
|
// ===========================================================================
|
|
637
637
|
|
|
638
638
|
import { applyCalibration as _applyCalibration } from '@/utils/pruneCalibration';
|
|
639
|
+
import { COMPACTION_RECENT_ROUNDS } from '@/common/constants';
|
|
640
|
+
import { buildFileManifestBlock, FILE_MANIFEST_PREFIX } from '@/utils/fileManifest';
|
|
641
|
+
import type { FileManifestEntry } from '@/types/graph';
|
|
639
642
|
|
|
640
643
|
describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
641
644
|
/**
|
|
642
645
|
* Simulates the compaction logic from Graph.ts without the full Graph instance.
|
|
643
|
-
*
|
|
646
|
+
* Mirrors the two modes:
|
|
647
|
+
* A) No summary → fill budget with as many recent messages as fit
|
|
648
|
+
* B) Summary exists → keep last COMPACTION_RECENT_ROUNDS rounds only
|
|
644
649
|
*/
|
|
645
650
|
function buildWindowedView(opts: {
|
|
646
651
|
messages: BaseMessage[];
|
|
@@ -648,8 +653,9 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
648
653
|
maxTokens: number;
|
|
649
654
|
summary?: string;
|
|
650
655
|
tokenCounter: TokenCounter;
|
|
656
|
+
fileManifest?: FileManifestEntry[];
|
|
651
657
|
}) {
|
|
652
|
-
const { messages, indexTokenCountMap, maxTokens, summary, tokenCounter } = opts;
|
|
658
|
+
const { messages, indexTokenCountMap, maxTokens, summary, tokenCounter, fileManifest } = opts;
|
|
653
659
|
|
|
654
660
|
const systemMsg = messages[0]?.getType() === 'system' ? messages[0] : null;
|
|
655
661
|
const systemTokens = systemMsg != null ? (indexTokenCountMap[0] ?? 0) : 0;
|
|
@@ -663,11 +669,28 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
663
669
|
let usedTokens = 0;
|
|
664
670
|
let windowStart = messages.length;
|
|
665
671
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
672
|
+
if (!summary) {
|
|
673
|
+
// Mode A: No summary — fill budget
|
|
674
|
+
for (let i = messages.length - 1; i >= contentStart; i--) {
|
|
675
|
+
const msgTokens = indexTokenCountMap[i] ?? 0;
|
|
676
|
+
if (usedTokens + msgTokens > recentBudget) break;
|
|
677
|
+
usedTokens += msgTokens;
|
|
678
|
+
windowStart = i;
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
// Mode B: Summary exists — keep last N rounds
|
|
682
|
+
let roundsSeen = 0;
|
|
683
|
+
for (let i = messages.length - 1; i >= contentStart; i--) {
|
|
684
|
+
const msgType = messages[i]?.getType();
|
|
685
|
+
const msgTokens = indexTokenCountMap[i] ?? 0;
|
|
686
|
+
if (usedTokens + msgTokens > recentBudget) break;
|
|
687
|
+
usedTokens += msgTokens;
|
|
688
|
+
windowStart = i;
|
|
689
|
+
if (msgType === 'human') {
|
|
690
|
+
roundsSeen++;
|
|
691
|
+
if (roundsSeen >= COMPACTION_RECENT_ROUNDS) break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
671
694
|
}
|
|
672
695
|
|
|
673
696
|
// Don't split tool-call / tool-result pairs
|
|
@@ -685,9 +708,20 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
685
708
|
const view: BaseMessage[] = [];
|
|
686
709
|
if (systemMsg) view.push(systemMsg);
|
|
687
710
|
if (summaryMsg) view.push(summaryMsg);
|
|
711
|
+
|
|
712
|
+
// Inject file manifest when files exist and messages are being compacted
|
|
713
|
+
let fileManifestMsg: SystemMessage | null = null;
|
|
714
|
+
if (fileManifest && fileManifest.length > 0 && compactedMessages.length > 0) {
|
|
715
|
+
const manifestBlock = buildFileManifestBlock(fileManifest);
|
|
716
|
+
if (manifestBlock) {
|
|
717
|
+
fileManifestMsg = new SystemMessage(manifestBlock);
|
|
718
|
+
view.push(fileManifestMsg);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
688
722
|
view.push(...recentMessages);
|
|
689
723
|
|
|
690
|
-
return { view, compactedMessages, recentMessages, usedTokens };
|
|
724
|
+
return { view, compactedMessages, recentMessages, usedTokens, fileManifestMsg };
|
|
691
725
|
}
|
|
692
726
|
|
|
693
727
|
it('builds a windowed view without deleting any messages', () => {
|
|
@@ -836,6 +870,73 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
836
870
|
expect(recentWithSummary.length).toBeLessThan(recentWithout.length);
|
|
837
871
|
});
|
|
838
872
|
|
|
873
|
+
it('with summary, limits window to last 2 rounds (not budget-filling)', () => {
|
|
874
|
+
// 20 messages = 10 rounds. With summary, should only keep last 2 rounds (4 msgs).
|
|
875
|
+
const messages: BaseMessage[] = [
|
|
876
|
+
new SystemMessage('System prompt'),
|
|
877
|
+
];
|
|
878
|
+
for (let i = 0; i < 20; i++) {
|
|
879
|
+
messages.push(
|
|
880
|
+
i % 2 === 0
|
|
881
|
+
? new HumanMessage(`User question ${i / 2}`)
|
|
882
|
+
: new AIMessage(`AI answer ${(i - 1) / 2}`)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
886
|
+
for (let i = 0; i < messages.length; i++) {
|
|
887
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const { view, recentMessages, compactedMessages } = buildWindowedView({
|
|
891
|
+
messages,
|
|
892
|
+
indexTokenCountMap,
|
|
893
|
+
maxTokens: 100_000, // huge budget — would fit everything
|
|
894
|
+
summary: 'Summary of earlier conversation',
|
|
895
|
+
tokenCounter: simpleTokenCounter,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// Despite huge budget, only last 2 rounds kept (4 content msgs: H+A+H+A)
|
|
899
|
+
// Plus possible trailing messages in the last round
|
|
900
|
+
expect(recentMessages.length).toBeLessThanOrEqual(5); // 2 rounds + maybe 1 trailing
|
|
901
|
+
expect(recentMessages.length).toBeGreaterThanOrEqual(4); // at least 2 full rounds
|
|
902
|
+
|
|
903
|
+
// Most messages are compacted behind the summary
|
|
904
|
+
expect(compactedMessages.length).toBeGreaterThan(10);
|
|
905
|
+
|
|
906
|
+
// View = system + summary + recent window
|
|
907
|
+
expect(view[0].getType()).toBe('system');
|
|
908
|
+
expect(view[1].content).toContain('[Conversation Summary]');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('without summary, fills budget (no round limit)', () => {
|
|
912
|
+
const messages = buildConversation(20, 100); // small messages
|
|
913
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
914
|
+
for (let i = 0; i < messages.length; i++) {
|
|
915
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const { recentMessages: withoutSummary } = buildWindowedView({
|
|
919
|
+
messages,
|
|
920
|
+
indexTokenCountMap,
|
|
921
|
+
maxTokens: 100_000, // huge budget
|
|
922
|
+
tokenCounter: simpleTokenCounter,
|
|
923
|
+
// no summary → mode A
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
const { recentMessages: withSummary } = buildWindowedView({
|
|
927
|
+
messages,
|
|
928
|
+
indexTokenCountMap,
|
|
929
|
+
maxTokens: 100_000,
|
|
930
|
+
summary: 'Summary exists',
|
|
931
|
+
tokenCounter: simpleTokenCounter,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Without summary: all messages included (budget-filling mode)
|
|
935
|
+
expect(withoutSummary.length).toBe(20); // all content messages
|
|
936
|
+
// With summary: only last 2 rounds
|
|
937
|
+
expect(withSummary.length).toBeLessThan(withoutSummary.length);
|
|
938
|
+
});
|
|
939
|
+
|
|
839
940
|
it('original messages array is never mutated', () => {
|
|
840
941
|
const messages = buildConversation(15, 400);
|
|
841
942
|
const originalLength = messages.length;
|
|
@@ -862,4 +963,141 @@ describe('Context Compaction — Windowed View (no message deletion)', () => {
|
|
|
862
963
|
expect(messages[0].content).toBe(originalFirstContent);
|
|
863
964
|
expect(messages[messages.length - 1].content).toBe(originalLastContent);
|
|
864
965
|
});
|
|
966
|
+
|
|
967
|
+
// ── File Manifest in Windowed View ─────────────────────────────────────
|
|
968
|
+
|
|
969
|
+
it('injects file manifest block when files exist and messages are compacted', () => {
|
|
970
|
+
const messages = buildConversation(20, 400);
|
|
971
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
972
|
+
for (let i = 0; i < messages.length; i++) {
|
|
973
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const manifest: FileManifestEntry[] = [
|
|
977
|
+
{ fileId: 'f1', filename: 'report.pdf', contentId: 'abc123' },
|
|
978
|
+
{ fileId: 'f2', filename: 'data.csv', contentId: 'def456', source: 'local' },
|
|
979
|
+
];
|
|
980
|
+
|
|
981
|
+
const { view, compactedMessages, fileManifestMsg } = buildWindowedView({
|
|
982
|
+
messages,
|
|
983
|
+
indexTokenCountMap,
|
|
984
|
+
maxTokens: 600,
|
|
985
|
+
summary: 'Summary of earlier turns',
|
|
986
|
+
tokenCounter: simpleTokenCounter,
|
|
987
|
+
fileManifest: manifest,
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// File manifest is injected
|
|
991
|
+
expect(fileManifestMsg).not.toBeNull();
|
|
992
|
+
expect(compactedMessages.length).toBeGreaterThan(0);
|
|
993
|
+
|
|
994
|
+
// Manifest message contains file names and content IDs
|
|
995
|
+
const manifestContent = fileManifestMsg!.content as string;
|
|
996
|
+
expect(manifestContent).toContain(FILE_MANIFEST_PREFIX);
|
|
997
|
+
expect(manifestContent).toContain('report.pdf');
|
|
998
|
+
expect(manifestContent).toContain('abc123');
|
|
999
|
+
expect(manifestContent).toContain('data.csv');
|
|
1000
|
+
expect(manifestContent).toContain('def456');
|
|
1001
|
+
|
|
1002
|
+
// View order: [system] + [summary] + [file manifest] + [recent messages]
|
|
1003
|
+
expect(view[0].getType()).toBe('system');
|
|
1004
|
+
expect((view[1].content as string)).toContain('[Conversation Summary]');
|
|
1005
|
+
expect((view[2].content as string)).toContain(FILE_MANIFEST_PREFIX);
|
|
1006
|
+
// Recent messages follow
|
|
1007
|
+
expect(view.length).toBeGreaterThan(3);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('does NOT inject file manifest when no messages are compacted', () => {
|
|
1011
|
+
const messages = buildConversation(4, 100); // small conversation
|
|
1012
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
1013
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1014
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const manifest: FileManifestEntry[] = [
|
|
1018
|
+
{ fileId: 'f1', filename: 'file.txt', contentId: 'aaa' },
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
const { compactedMessages, fileManifestMsg } = buildWindowedView({
|
|
1022
|
+
messages,
|
|
1023
|
+
indexTokenCountMap,
|
|
1024
|
+
maxTokens: 100_000, // everything fits
|
|
1025
|
+
tokenCounter: simpleTokenCounter,
|
|
1026
|
+
fileManifest: manifest,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// No compaction happened, so no manifest injected
|
|
1030
|
+
expect(compactedMessages.length).toBe(0);
|
|
1031
|
+
expect(fileManifestMsg).toBeNull();
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('does NOT inject file manifest when manifest is empty', () => {
|
|
1035
|
+
const messages = buildConversation(20, 400);
|
|
1036
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
1037
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1038
|
+
indexTokenCountMap[i] = simpleTokenCounter(messages[i]);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const { fileManifestMsg } = buildWindowedView({
|
|
1042
|
+
messages,
|
|
1043
|
+
indexTokenCountMap,
|
|
1044
|
+
maxTokens: 600,
|
|
1045
|
+
summary: 'Summary',
|
|
1046
|
+
tokenCounter: simpleTokenCounter,
|
|
1047
|
+
fileManifest: [],
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
expect(fileManifestMsg).toBeNull();
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// ===========================================================================
|
|
1055
|
+
// File Manifest Utility — Unit Tests
|
|
1056
|
+
// ===========================================================================
|
|
1057
|
+
|
|
1058
|
+
describe('buildFileManifestBlock', () => {
|
|
1059
|
+
it('returns empty string for undefined manifest', () => {
|
|
1060
|
+
expect(buildFileManifestBlock(undefined)).toBe('');
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('returns empty string for empty manifest', () => {
|
|
1064
|
+
expect(buildFileManifestBlock([])).toBe('');
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('builds block with file names and content IDs', () => {
|
|
1068
|
+
const manifest: FileManifestEntry[] = [
|
|
1069
|
+
{ fileId: 'f1', filename: 'report.pdf', contentId: 'abc123' },
|
|
1070
|
+
{ fileId: 'f2', filename: 'notes.md', contentId: 'def456' },
|
|
1071
|
+
];
|
|
1072
|
+
|
|
1073
|
+
const block = buildFileManifestBlock(manifest);
|
|
1074
|
+
|
|
1075
|
+
expect(block).toContain(FILE_MANIFEST_PREFIX);
|
|
1076
|
+
expect(block).toContain('report.pdf');
|
|
1077
|
+
expect(block).toContain('(content_id: abc123)');
|
|
1078
|
+
expect(block).toContain('notes.md');
|
|
1079
|
+
expect(block).toContain('(content_id: def456)');
|
|
1080
|
+
expect(block).toContain('file_search or content_tool read');
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it('includes source when provided', () => {
|
|
1084
|
+
const manifest: FileManifestEntry[] = [
|
|
1085
|
+
{ fileId: 'f1', filename: 'doc.pdf', source: 'sharepoint' },
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
const block = buildFileManifestBlock(manifest);
|
|
1089
|
+
expect(block).toContain('[sharepoint]');
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('handles entries without contentId or source', () => {
|
|
1093
|
+
const manifest: FileManifestEntry[] = [
|
|
1094
|
+
{ fileId: 'f1', filename: 'image.png' },
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
const block = buildFileManifestBlock(manifest);
|
|
1098
|
+
expect(block).toContain('- image.png');
|
|
1099
|
+
// No per-file content_id or source brackets in the file line
|
|
1100
|
+
expect(block).not.toContain('(content_id:');
|
|
1101
|
+
expect(block).not.toContain('[local]');
|
|
1102
|
+
});
|
|
865
1103
|
});
|
package/src/types/graph.ts
CHANGED
|
@@ -555,6 +555,26 @@ export interface PruneCalibrationState {
|
|
|
555
555
|
iterations: number;
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Lightweight file metadata entry for conversation-level file awareness.
|
|
560
|
+
* Contains only IDs and names — NOT full content — so the agent always knows
|
|
561
|
+
* what files exist in the conversation even after compaction pushes old messages
|
|
562
|
+
* behind the summary window. The agent can retrieve full content on-demand
|
|
563
|
+
* via file_search (RAG) or content_tool read (by contentId).
|
|
564
|
+
*/
|
|
565
|
+
export interface FileManifestEntry {
|
|
566
|
+
/** Unique file identifier (e.g., MongoDB ObjectId or UUID) */
|
|
567
|
+
fileId: string;
|
|
568
|
+
/** Original filename (e.g., "quarterly-report.pdf") */
|
|
569
|
+
filename: string;
|
|
570
|
+
/** Content identifier for on-demand retrieval via content_tool read */
|
|
571
|
+
contentId?: string;
|
|
572
|
+
/** File source (e.g., "local", "sharepoint", "onedrive") */
|
|
573
|
+
source?: string;
|
|
574
|
+
/** Index of the message that introduced this file (0-based in the original message array) */
|
|
575
|
+
messageIndex?: number;
|
|
576
|
+
}
|
|
577
|
+
|
|
558
578
|
export interface AgentInputs {
|
|
559
579
|
agentId: string;
|
|
560
580
|
/** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
|
|
@@ -632,4 +652,20 @@ export interface AgentInputs {
|
|
|
632
652
|
* @see SummarizationConfig
|
|
633
653
|
*/
|
|
634
654
|
summarizationConfig?: SummarizationConfig;
|
|
655
|
+
/**
|
|
656
|
+
* Lightweight file manifest for the conversation.
|
|
657
|
+
* Contains file IDs, names, and metadata — NOT full content.
|
|
658
|
+
*
|
|
659
|
+
* Used by the compaction engine to inject a [Conversation Files] block
|
|
660
|
+
* into the windowed view, ensuring the LLM always knows what files exist
|
|
661
|
+
* even when old messages (with full file content) are behind the summary.
|
|
662
|
+
*
|
|
663
|
+
* The agent can retrieve full content on-demand via:
|
|
664
|
+
* - file_search (RAG semantic search over embedded files)
|
|
665
|
+
* - content_tool read (by contentId for exact file retrieval)
|
|
666
|
+
*
|
|
667
|
+
* Built by the orchestrator (e.g., Ranger) from message_file_map
|
|
668
|
+
* and metadata.context_files across all conversation messages.
|
|
669
|
+
*/
|
|
670
|
+
fileManifest?: FileManifestEntry[];
|
|
635
671
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/utils/fileManifest.ts
|
|
2
|
+
//
|
|
3
|
+
// Utility for building a lightweight [Conversation Files] context block
|
|
4
|
+
// from a file manifest. Injected into the compaction windowed view so the
|
|
5
|
+
// LLM retains awareness of ALL conversation files, even when old messages
|
|
6
|
+
// (with full file content) are behind the summary.
|
|
7
|
+
|
|
8
|
+
import type { FileManifestEntry } from '@/types/graph';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Prefix marker for the file manifest block.
|
|
12
|
+
* Used to detect and deduplicate manifest messages across turns.
|
|
13
|
+
*/
|
|
14
|
+
export const FILE_MANIFEST_PREFIX = '[Conversation Files]';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds a compact text block listing all files in the conversation.
|
|
18
|
+
* Each entry costs ~10-15 tokens, so 10 files ≈ 100-150 tokens total.
|
|
19
|
+
*
|
|
20
|
+
* The block includes retrieval hints so the LLM knows how to fetch
|
|
21
|
+
* full content on demand (via file_search or content_tool).
|
|
22
|
+
*
|
|
23
|
+
* @param manifest - Array of file metadata entries
|
|
24
|
+
* @returns Formatted text block, or empty string if manifest is empty/undefined
|
|
25
|
+
*/
|
|
26
|
+
export function buildFileManifestBlock(manifest: FileManifestEntry[] | undefined): string {
|
|
27
|
+
if (!manifest || manifest.length === 0) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lines = manifest.map((entry) => {
|
|
32
|
+
const parts: string[] = [`- ${entry.filename}`];
|
|
33
|
+
if (entry.contentId) {
|
|
34
|
+
parts.push(`(content_id: ${entry.contentId})`);
|
|
35
|
+
}
|
|
36
|
+
if (entry.source) {
|
|
37
|
+
parts.push(`[${entry.source}]`);
|
|
38
|
+
}
|
|
39
|
+
return parts.join(' ');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
FILE_MANIFEST_PREFIX,
|
|
44
|
+
'The following files have been shared in this conversation.',
|
|
45
|
+
'Use file_search or content_tool read (with content_id) to retrieve full content when needed.',
|
|
46
|
+
'',
|
|
47
|
+
...lines,
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
package/src/utils/index.ts
CHANGED