ds4-context-engine 0.1.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.
@@ -0,0 +1,150 @@
1
+ import {
2
+ convertToLlm,
3
+ serializeConversation,
4
+ sessionEntryToContextMessages,
5
+ type SessionBeforeCompactEvent,
6
+ type SessionEntry,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ parseDs4CompactionDetails,
10
+ type EmbeddedSummaryNode,
11
+ } from "ds4-context-core/compaction/compaction-record";
12
+ import { computeSummarySourceHash } from "ds4-context-core/compaction/summary-contract";
13
+ import { sha256 } from "ds4-context-core/shared/hash";
14
+ import { stableStringify } from "ds4-context-core/shared/stable-json";
15
+
16
+ interface SourceCandidate {
17
+ entryId: string;
18
+ fingerprint: string;
19
+ used: boolean;
20
+ }
21
+
22
+ export interface PreparedCompactionSource {
23
+ messages: SessionBeforeCompactEvent["preparation"]["messagesToSummarize"];
24
+ conversationText: string;
25
+ sourceText: string;
26
+ sourceEntryIds: string[];
27
+ sourceHash: string;
28
+ segmentReadFiles: string[];
29
+ segmentModifiedFiles: string[];
30
+ readFiles: string[];
31
+ modifiedFiles: string[];
32
+ previousSummary?: string;
33
+ previousNode?: EmbeddedSummaryNode;
34
+ }
35
+
36
+ function fingerprint(message: unknown): string {
37
+ return sha256(stableStringify(message));
38
+ }
39
+
40
+ function candidates(entries: readonly SessionEntry[]): SourceCandidate[] {
41
+ return entries.flatMap((entry) => sessionEntryToContextMessages(entry).map((message) => ({
42
+ entryId: entry.id,
43
+ fingerprint: fingerprint(message),
44
+ used: false,
45
+ })));
46
+ }
47
+
48
+ function mapSourceEntryIds(messages: readonly unknown[], entries: readonly SessionEntry[]): string[] {
49
+ const available = candidates(entries);
50
+ const ids: string[] = [];
51
+
52
+ for (const message of messages) {
53
+ const match = available.find((candidate) => !candidate.used && candidate.fingerprint === fingerprint(message));
54
+ if (!match) throw new Error("Compaction source message has no exact canonical Pi session entry");
55
+ match.used = true;
56
+ if (!ids.includes(match.entryId)) ids.push(match.entryId);
57
+ }
58
+
59
+ return ids;
60
+ }
61
+
62
+ function previousFiles(entries: readonly SessionEntry[]): { read: string[]; modified: string[] } {
63
+ for (let index = entries.length - 1; index >= 0; index--) {
64
+ const entry = entries[index];
65
+ if (entry?.type !== "compaction") continue;
66
+ const details = parseDs4CompactionDetails(entry.details);
67
+ if (details) return { read: details.readFiles, modified: details.modifiedFiles };
68
+ }
69
+ return { read: [], modified: [] };
70
+ }
71
+
72
+ function sortedUnique(values: Iterable<string>): string[] {
73
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
74
+ }
75
+
76
+ export function findActiveBranchSummary(
77
+ entries: readonly SessionEntry[],
78
+ previousSummary?: string,
79
+ ): EmbeddedSummaryNode | undefined {
80
+ if (!previousSummary) return undefined;
81
+ for (let index = entries.length - 1; index >= 0; index--) {
82
+ const entry = entries[index];
83
+ if (entry?.type !== "compaction" || entry.summary !== previousSummary) continue;
84
+ const details = parseDs4CompactionDetails(entry.details);
85
+ if (!details) return undefined;
86
+ const metadata = details.ds4ContextEngine;
87
+ return {
88
+ id: metadata.summaryId,
89
+ kind: metadata.summaryKind,
90
+ content: entry.summary,
91
+ sourceHash: metadata.sourceHash,
92
+ sourceEntryIds: [...metadata.sourceEntryIds],
93
+ childSummaryIds: [...metadata.childSummaryIds],
94
+ graphLevel: metadata.graphLevel,
95
+ createdAt: metadata.generatedAt,
96
+ validationStatus: metadata.validationStatus,
97
+ validationIssueCodes: [...metadata.validationIssueCodes],
98
+ provider: metadata.provider,
99
+ model: metadata.model,
100
+ };
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ export function prepareCompactionSource(event: SessionBeforeCompactEvent): PreparedCompactionSource {
106
+ const messages = [
107
+ ...event.preparation.messagesToSummarize,
108
+ ...event.preparation.turnPrefixMessages,
109
+ ];
110
+ if (messages.length === 0) throw new Error("Pi compaction preparation contains no source messages");
111
+
112
+ const sourceEntryIds = mapSourceEntryIds(messages, event.branchEntries);
113
+ const priorFiles = previousFiles(event.branchEntries);
114
+ const segmentReadFiles = sortedUnique(event.preparation.fileOps.read);
115
+ const segmentModifiedFiles = sortedUnique([
116
+ ...event.preparation.fileOps.written,
117
+ ...event.preparation.fileOps.edited,
118
+ ]);
119
+ const readFiles = sortedUnique([...priorFiles.read, ...segmentReadFiles]);
120
+ const modifiedFiles = sortedUnique([...priorFiles.modified, ...segmentModifiedFiles]);
121
+ const conversationText = serializeConversation(convertToLlm(messages));
122
+ const canonicalSourceText = stableStringify(messages);
123
+ const previousSummary = event.preparation.previousSummary;
124
+ const previousNode = findActiveBranchSummary(event.branchEntries, previousSummary);
125
+ const sourceText = [
126
+ canonicalSourceText,
127
+ conversationText,
128
+ segmentReadFiles.join("\n"),
129
+ segmentModifiedFiles.join("\n"),
130
+ ].filter(Boolean).join("\n\n");
131
+
132
+ return {
133
+ messages,
134
+ conversationText,
135
+ sourceText,
136
+ sourceEntryIds,
137
+ sourceHash: computeSummarySourceHash({
138
+ conversationText: canonicalSourceText,
139
+ sourceEntryIds,
140
+ readFiles: segmentReadFiles,
141
+ modifiedFiles: segmentModifiedFiles,
142
+ }),
143
+ segmentReadFiles,
144
+ segmentModifiedFiles,
145
+ readFiles,
146
+ modifiedFiles,
147
+ ...(previousSummary ? { previousSummary } : {}),
148
+ ...(previousNode ? { previousNode } : {}),
149
+ };
150
+ }