ds4-context-engine 0.3.0-alpha.4 → 0.3.0-beta.1

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.
@@ -19,14 +19,22 @@ interface SourceCandidate {
19
19
  used: boolean;
20
20
  }
21
21
 
22
- export interface PreparedCompactionSource {
23
- messages: SessionBeforeCompactEvent["preparation"]["messagesToSummarize"];
22
+ type CompactionMessage = SessionBeforeCompactEvent["preparation"]["messagesToSummarize"][number];
23
+
24
+ export interface PreparedCompactionSourceSlice {
25
+ messages: CompactionMessage[];
24
26
  conversationText: string;
25
27
  sourceText: string;
26
28
  sourceEntryIds: string[];
27
29
  sourceHash: string;
28
30
  segmentReadFiles: string[];
29
31
  segmentModifiedFiles: string[];
32
+ isSplitTurn: boolean;
33
+ }
34
+
35
+ export interface PreparedCompactionSource extends PreparedCompactionSourceSlice {
36
+ messageEntryIds: string[];
37
+ turnPrefixStartIndex: number;
30
38
  readFiles: string[];
31
39
  modifiedFiles: string[];
32
40
  previousSummary?: string;
@@ -53,7 +61,7 @@ function mapSourceEntryIds(messages: readonly unknown[], entries: readonly Sessi
53
61
  const match = available.find((candidate) => !candidate.used && candidate.fingerprint === fingerprint(message));
54
62
  if (!match) throw new Error("Compaction source message has no exact canonical Pi session entry");
55
63
  match.used = true;
56
- if (!ids.includes(match.entryId)) ids.push(match.entryId);
64
+ ids.push(match.entryId);
57
65
  }
58
66
 
59
67
  return ids;
@@ -69,8 +77,93 @@ function previousFiles(entries: readonly SessionEntry[]): { read: string[]; modi
69
77
  return { read: [], modified: [] };
70
78
  }
71
79
 
80
+ function unique(values: Iterable<string>): string[] {
81
+ return [...new Set(values)];
82
+ }
83
+
72
84
  function sortedUnique(values: Iterable<string>): string[] {
73
- return [...new Set(values)].sort((left, right) => left.localeCompare(right));
85
+ return unique(values).sort((left, right) => left.localeCompare(right));
86
+ }
87
+
88
+ function segmentFiles(messages: readonly CompactionMessage[]): { readFiles: string[]; modifiedFiles: string[] } {
89
+ const read = new Set<string>();
90
+ const modified = new Set<string>();
91
+ for (const message of messages) {
92
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
93
+ for (const block of message.content) {
94
+ if (block.type !== "toolCall" || !block.arguments || typeof block.arguments !== "object") continue;
95
+ const path = "path" in block.arguments && typeof block.arguments.path === "string"
96
+ ? block.arguments.path
97
+ : undefined;
98
+ if (!path) continue;
99
+ if (block.name === "read") read.add(path);
100
+ if (block.name === "write" || block.name === "edit") modified.add(path);
101
+ }
102
+ }
103
+ return {
104
+ readFiles: sortedUnique([...read].filter((path) => !modified.has(path))),
105
+ modifiedFiles: sortedUnique(modified),
106
+ };
107
+ }
108
+
109
+ function createSourceSlice(input: {
110
+ messages: CompactionMessage[];
111
+ sourceEntryIds: string[];
112
+ segmentReadFiles: string[];
113
+ segmentModifiedFiles: string[];
114
+ isSplitTurn: boolean;
115
+ }): PreparedCompactionSourceSlice {
116
+ const conversationText = serializeConversation(convertToLlm(input.messages));
117
+ const canonicalSourceText = stableStringify(input.messages);
118
+ const sourceEntryIds = unique(input.sourceEntryIds);
119
+ const sourceText = [
120
+ canonicalSourceText,
121
+ conversationText,
122
+ input.segmentReadFiles.join("\n"),
123
+ input.segmentModifiedFiles.join("\n"),
124
+ ].filter(Boolean).join("\n\n");
125
+ return {
126
+ messages: input.messages,
127
+ conversationText,
128
+ sourceText,
129
+ sourceEntryIds,
130
+ sourceHash: computeSummarySourceHash({
131
+ conversationText: canonicalSourceText,
132
+ sourceEntryIds,
133
+ readFiles: input.segmentReadFiles,
134
+ modifiedFiles: input.segmentModifiedFiles,
135
+ }),
136
+ segmentReadFiles: input.segmentReadFiles,
137
+ segmentModifiedFiles: input.segmentModifiedFiles,
138
+ isSplitTurn: input.isSplitTurn,
139
+ };
140
+ }
141
+
142
+ export function sliceCompactionSource(
143
+ source: PreparedCompactionSource,
144
+ messageIndices: readonly number[],
145
+ ): PreparedCompactionSourceSlice {
146
+ const indices = [...new Set(messageIndices)].sort((left, right) => left - right);
147
+ if (indices.length === 0) throw new Error("Compaction segment contains no source messages");
148
+ for (let offset = 0; offset < indices.length; offset++) {
149
+ const index = indices[offset];
150
+ if (index === undefined || !source.messages[index] || !source.messageEntryIds[index]) {
151
+ throw new Error("Compaction segment references an unavailable source message");
152
+ }
153
+ if (offset > 0 && index !== (indices[offset - 1] ?? -1) + 1) {
154
+ throw new Error("Compaction segment source messages must be contiguous");
155
+ }
156
+ }
157
+ const messages = indices.map((index) => source.messages[index] as CompactionMessage);
158
+ const files = segmentFiles(messages);
159
+ return createSourceSlice({
160
+ messages,
161
+ sourceEntryIds: indices.map((index) => source.messageEntryIds[index] as string),
162
+ segmentReadFiles: files.readFiles,
163
+ segmentModifiedFiles: files.modifiedFiles,
164
+ isSplitTurn: source.isSplitTurn
165
+ && indices.some((index) => index >= source.turnPrefixStartIndex),
166
+ });
74
167
  }
75
168
 
76
169
  export function findActiveBranchSummary(
@@ -109,7 +202,8 @@ export function prepareCompactionSource(event: SessionBeforeCompactEvent): Prepa
109
202
  ];
110
203
  if (messages.length === 0) throw new Error("Pi compaction preparation contains no source messages");
111
204
 
112
- const sourceEntryIds = mapSourceEntryIds(messages, event.branchEntries);
205
+ const messageEntryIds = mapSourceEntryIds(messages, event.branchEntries);
206
+ const sourceEntryIds = unique(messageEntryIds);
113
207
  const priorFiles = previousFiles(event.branchEntries);
114
208
  const segmentReadFiles = sortedUnique(event.preparation.fileOps.read);
115
209
  const segmentModifiedFiles = sortedUnique([
@@ -118,30 +212,20 @@ export function prepareCompactionSource(event: SessionBeforeCompactEvent): Prepa
118
212
  ]);
119
213
  const readFiles = sortedUnique([...priorFiles.read, ...segmentReadFiles]);
120
214
  const modifiedFiles = sortedUnique([...priorFiles.modified, ...segmentModifiedFiles]);
121
- const conversationText = serializeConversation(convertToLlm(messages));
122
- const canonicalSourceText = stableStringify(messages);
123
215
  const previousSummary = event.preparation.previousSummary;
124
216
  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 {
217
+ const prepared = createSourceSlice({
133
218
  messages,
134
- conversationText,
135
- sourceText,
136
219
  sourceEntryIds,
137
- sourceHash: computeSummarySourceHash({
138
- conversationText: canonicalSourceText,
139
- sourceEntryIds,
140
- readFiles: segmentReadFiles,
141
- modifiedFiles: segmentModifiedFiles,
142
- }),
143
220
  segmentReadFiles,
144
221
  segmentModifiedFiles,
222
+ isSplitTurn: event.preparation.isSplitTurn,
223
+ });
224
+
225
+ return {
226
+ ...prepared,
227
+ messageEntryIds,
228
+ turnPrefixStartIndex: event.preparation.messagesToSummarize.length,
145
229
  readFiles,
146
230
  modifiedFiles,
147
231
  ...(previousSummary ? { previousSummary } : {}),