ds4-context-engine 0.2.0-rc.1 → 0.3.0-alpha.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.
@@ -0,0 +1,450 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
+ import type { PrivacyClassification } from "ds4-context-core/privacy/privacy-policy";
3
+ import {
4
+ CONTEXT_PERSISTENCE_ACTIONS,
5
+ CONTEXT_PERSISTENCE_RESULT_CONTRACT,
6
+ type ContextPersistenceAction,
7
+ type ContextPersistenceReadAction,
8
+ } from "./context-persistence-contract.ts";
9
+
10
+ export type ContextPersistenceOutcome =
11
+ | "ok"
12
+ | "rejected"
13
+ | "cancelled"
14
+ | "unavailable"
15
+ | "committed"
16
+ | "committed_projection_pending"
17
+ | "indeterminate";
18
+
19
+ export type ContextPersistenceClass = "read-only" | "canonical-jsonl" | "derived-local-policy";
20
+ export type ContextPersistenceKind = "pin" | "memory" | "project-memory-source";
21
+ export type PreviewStatus = "included" | "omitted-by-policy";
22
+ export type MatchKind = "exact-key" | "exact-phrase" | "all-terms" | "partial-terms" | "metadata-only";
23
+
24
+ interface ItemBase {
25
+ kind: ContextPersistenceKind;
26
+ targetRevision: string;
27
+ }
28
+
29
+ export interface PinReadItem extends ItemBase {
30
+ id: string;
31
+ kind: "pin";
32
+ scope: "session" | "branch" | "project";
33
+ status: "active" | "superseded" | "deleted";
34
+ classification: PrivacyClassification;
35
+ applicableToActiveBranch: boolean;
36
+ createdAt: number;
37
+ updatedAt: number;
38
+ matchKind?: MatchKind;
39
+ score?: number;
40
+ previewStatus?: PreviewStatus;
41
+ }
42
+
43
+ export interface MemoryReadItem extends ItemBase {
44
+ id: string;
45
+ kind: "memory";
46
+ scope: "session" | "project";
47
+ status: "active" | "superseded" | "invalid" | "expired";
48
+ classification: PrivacyClassification;
49
+ createdAt: number;
50
+ updatedAt: number;
51
+ matchKind?: MatchKind;
52
+ score?: number;
53
+ previewStatus?: PreviewStatus;
54
+ }
55
+
56
+ export interface SourceReadItem extends ItemBase {
57
+ sourceRef: string;
58
+ kind: "project-memory-source";
59
+ status: "ready" | "missing" | "corrupt" | "excluded";
60
+ indexedMutations: number;
61
+ activeProjectMemories: number;
62
+ activeProjectPins: number;
63
+ hasMalformedLines: boolean;
64
+ errorCode?: "source-missing" | "source-corrupt" | "source-malformed";
65
+ }
66
+
67
+ export type ContextPersistenceReadItem = PinReadItem | MemoryReadItem | SourceReadItem;
68
+
69
+ export interface ContextPersistenceDetails {
70
+ schema: typeof CONTEXT_PERSISTENCE_RESULT_CONTRACT;
71
+ action: ContextPersistenceAction;
72
+ outcome: ContextPersistenceOutcome;
73
+ persistenceClass: ContextPersistenceClass;
74
+ count?: number;
75
+ truncated?: boolean;
76
+ incomplete?: boolean;
77
+ items?: ContextPersistenceReadItem[];
78
+ id?: string;
79
+ sourceRef?: string;
80
+ kind?: ContextPersistenceKind;
81
+ scope?: "session" | "branch" | "project";
82
+ status?: string;
83
+ classification?: PrivacyClassification;
84
+ targetRevision?: string;
85
+ duplicate?: boolean;
86
+ errorCode?: string;
87
+ }
88
+
89
+ export interface ReadResultInput {
90
+ action: ContextPersistenceReadAction;
91
+ items: ContextPersistenceReadItem[];
92
+ truncated: boolean;
93
+ incomplete: boolean;
94
+ previews?: ReadonlyMap<string, string>;
95
+ }
96
+
97
+ const MAX_CONTENT_BYTES = 96 * 1024;
98
+ const MAX_DETAILS_BYTES = 64 * 1024;
99
+ const MAX_ITEMS = 100;
100
+ const OPAQUE_ID = /^[A-Za-z0-9._:-]{1,128}$/u;
101
+ const REVISION = /^rev_[A-Za-z0-9_-]{1,60}$/u;
102
+ const SOURCE_REF = /^source_[A-Za-z0-9_-]{22,57}$/u;
103
+ const CLASSIFICATIONS = new Set(["normal", "internal", "sensitive", "local-only"]);
104
+ const OUTCOMES = new Set<ContextPersistenceOutcome>([
105
+ "ok", "rejected", "cancelled", "unavailable", "committed",
106
+ "committed_projection_pending", "indeterminate",
107
+ ]);
108
+ const PERSISTENCE_CLASSES = new Set<ContextPersistenceClass>([
109
+ "read-only", "canonical-jsonl", "derived-local-policy",
110
+ ]);
111
+ const ACTIONS = new Set<string>(CONTEXT_PERSISTENCE_ACTIONS);
112
+
113
+ function isRecord(value: unknown): value is Record<string, unknown> {
114
+ return value !== null && typeof value === "object" && !Array.isArray(value);
115
+ }
116
+
117
+ function integer(value: unknown): value is number {
118
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
119
+ }
120
+
121
+ function metadataLine(item: ContextPersistenceReadItem): string {
122
+ if (item.kind === "pin") {
123
+ return [
124
+ `pin ${item.id}`,
125
+ `scope=${item.scope}`,
126
+ `status=${item.status}`,
127
+ `classification=${item.classification}`,
128
+ `applicable=${item.applicableToActiveBranch}`,
129
+ `createdAt=${item.createdAt}`,
130
+ `updatedAt=${item.updatedAt}`,
131
+ `revision=${item.targetRevision}`,
132
+ ...(item.matchKind ? [`match=${item.matchKind}`, `score=${item.score ?? 0}`] : []),
133
+ ].join("; ");
134
+ }
135
+ if (item.kind === "memory") {
136
+ return [
137
+ `memory ${item.id}`,
138
+ `scope=${item.scope}`,
139
+ `status=${item.status}`,
140
+ `classification=${item.classification}`,
141
+ `createdAt=${item.createdAt}`,
142
+ `updatedAt=${item.updatedAt}`,
143
+ `revision=${item.targetRevision}`,
144
+ ...(item.matchKind ? [`match=${item.matchKind}`, `score=${item.score ?? 0}`] : []),
145
+ ].join("; ");
146
+ }
147
+ return [
148
+ `project-memory-source ${item.sourceRef}`,
149
+ `status=${item.status}`,
150
+ `indexedMutations=${item.indexedMutations}`,
151
+ `activeProjectMemories=${item.activeProjectMemories}`,
152
+ `activeProjectPins=${item.activeProjectPins}`,
153
+ `hasMalformedLines=${item.hasMalformedLines}`,
154
+ `revision=${item.targetRevision}`,
155
+ ...(item.errorCode ? [`error=${item.errorCode}`] : []),
156
+ ].join("; ");
157
+ }
158
+
159
+ export function renderReadContent(input: ReadResultInput): string {
160
+ const find = input.action === "pins_find" || input.action === "memory_find";
161
+ const noun = find ? "match(es)" : "item(s)";
162
+ const lines = [
163
+ `${input.action}: ${input.items.length} ${noun}; truncated=${input.truncated}; incomplete=${input.incomplete}.`,
164
+ ];
165
+ for (const item of input.items) {
166
+ lines.push(metadataLine(item));
167
+ if (find && item.kind !== "project-memory-source") {
168
+ const preview = input.previews?.get(item.id);
169
+ lines.push(`${item.kind} ${item.id}: ${preview ?? "preview omitted by policy"}`);
170
+ }
171
+ }
172
+ if (input.incomplete) lines.push("Search incomplete; refine the query or use an exact opaque ID.");
173
+ return lines.join("\n");
174
+ }
175
+
176
+ function budgetFailure(
177
+ action: ContextPersistenceAction,
178
+ persistenceClass: ContextPersistenceClass,
179
+ ): AgentToolResult<ContextPersistenceDetails> {
180
+ const details: ContextPersistenceDetails = {
181
+ schema: CONTEXT_PERSISTENCE_RESULT_CONTRACT,
182
+ action,
183
+ outcome: "unavailable",
184
+ persistenceClass,
185
+ errorCode: "result-budget-exceeded",
186
+ };
187
+ return {
188
+ content: [{ type: "text", text: `${action} unavailable: result-budget-exceeded.` }],
189
+ details,
190
+ };
191
+ }
192
+
193
+ function withinBudget(text: string, details: ContextPersistenceDetails): boolean {
194
+ return Buffer.byteLength(text, "utf8") <= MAX_CONTENT_BYTES
195
+ && Buffer.byteLength(JSON.stringify(details), "utf8") <= MAX_DETAILS_BYTES
196
+ && (details.items?.length ?? 0) <= MAX_ITEMS;
197
+ }
198
+
199
+ export function buildReadResult(input: ReadResultInput): AgentToolResult<ContextPersistenceDetails> {
200
+ const details: ContextPersistenceDetails = {
201
+ schema: CONTEXT_PERSISTENCE_RESULT_CONTRACT,
202
+ action: input.action,
203
+ outcome: "ok",
204
+ persistenceClass: "read-only",
205
+ count: input.items.length,
206
+ truncated: input.truncated,
207
+ incomplete: input.incomplete,
208
+ items: input.items,
209
+ };
210
+ const text = renderReadContent(input);
211
+ if (!withinBudget(text, details)) return budgetFailure(input.action, "read-only");
212
+ return { content: [{ type: "text", text }], details };
213
+ }
214
+
215
+ export interface MutationResultInput {
216
+ action: ContextPersistenceAction;
217
+ outcome: "ok" | "committed" | "committed_projection_pending" | "indeterminate";
218
+ persistenceClass: Exclude<ContextPersistenceClass, "read-only">;
219
+ id?: string;
220
+ sourceRef?: string;
221
+ kind?: ContextPersistenceKind;
222
+ scope?: "session" | "branch" | "project";
223
+ status?: string;
224
+ classification?: PrivacyClassification;
225
+ targetRevision?: string;
226
+ duplicate?: boolean;
227
+ errorCode?: "committed-projection-pending" | "append-indeterminate";
228
+ }
229
+
230
+ function renderMutationContent(details: ContextPersistenceDetails): string {
231
+ if (details.outcome === "committed_projection_pending") {
232
+ return "Canonical mutation committed; projection pending. Do not retry automatically.";
233
+ }
234
+ if (details.outcome === "indeterminate") {
235
+ return "Append outcome indeterminate. Inspect state before retrying.";
236
+ }
237
+ if (details.outcome === "ok" && details.duplicate && details.kind && details.id) {
238
+ return `Existing ${details.kind} ${details.id}; no mutation appended.`;
239
+ }
240
+ if (details.outcome === "committed" && details.persistenceClass === "derived-local-policy" && details.sourceRef) {
241
+ return `Updated derived local policy for ${details.sourceRef}.`;
242
+ }
243
+ if (details.outcome === "committed" && details.kind && details.id) {
244
+ return `Committed ${details.kind} ${details.id}.`;
245
+ }
246
+ return `${details.action} ${details.outcome}${details.errorCode ? `: ${details.errorCode}` : ""}.`;
247
+ }
248
+
249
+ export function buildMutationResult(
250
+ input: MutationResultInput,
251
+ ): AgentToolResult<ContextPersistenceDetails> {
252
+ const details: ContextPersistenceDetails = {
253
+ schema: CONTEXT_PERSISTENCE_RESULT_CONTRACT,
254
+ action: input.action,
255
+ outcome: input.outcome,
256
+ persistenceClass: input.persistenceClass,
257
+ ...(input.id ? { id: input.id } : {}),
258
+ ...(input.sourceRef ? { sourceRef: input.sourceRef } : {}),
259
+ ...(input.kind ? { kind: input.kind } : {}),
260
+ ...(input.scope ? { scope: input.scope } : {}),
261
+ ...(input.status ? { status: input.status } : {}),
262
+ ...(input.classification ? { classification: input.classification } : {}),
263
+ ...(input.targetRevision ? { targetRevision: input.targetRevision } : {}),
264
+ ...(input.duplicate !== undefined ? { duplicate: input.duplicate } : {}),
265
+ ...(input.errorCode ? { errorCode: input.errorCode } : {}),
266
+ };
267
+ const text = renderMutationContent(details);
268
+ if (!withinBudget(text, details)) return budgetFailure(input.action, input.persistenceClass);
269
+ return { content: [{ type: "text", text }], details };
270
+ }
271
+
272
+ export function buildFailureResult(
273
+ action: ContextPersistenceAction,
274
+ outcome: "rejected" | "cancelled" | "unavailable",
275
+ errorCode?: string,
276
+ persistenceClass: ContextPersistenceClass = "read-only",
277
+ ): AgentToolResult<ContextPersistenceDetails> {
278
+ const details: ContextPersistenceDetails = {
279
+ schema: CONTEXT_PERSISTENCE_RESULT_CONTRACT,
280
+ action,
281
+ outcome,
282
+ persistenceClass,
283
+ ...(errorCode ? { errorCode } : {}),
284
+ };
285
+ const text = outcome === "cancelled" && !errorCode
286
+ ? `${action} cancelled.`
287
+ : `${action} ${outcome}${errorCode ? `: ${errorCode}` : ""}.`;
288
+ return { content: [{ type: "text", text }], details };
289
+ }
290
+
291
+ function sanitizePinItem(value: unknown): PinReadItem | undefined {
292
+ if (!isRecord(value)
293
+ || value.kind !== "pin"
294
+ || typeof value.id !== "string" || !OPAQUE_ID.test(value.id)
295
+ || (value.scope !== "session" && value.scope !== "branch" && value.scope !== "project")
296
+ || (value.status !== "active" && value.status !== "superseded" && value.status !== "deleted")
297
+ || typeof value.classification !== "string" || !CLASSIFICATIONS.has(value.classification)
298
+ || typeof value.applicableToActiveBranch !== "boolean"
299
+ || !integer(value.createdAt) || !integer(value.updatedAt)
300
+ || typeof value.targetRevision !== "string" || !REVISION.test(value.targetRevision)) return undefined;
301
+ const item: PinReadItem = {
302
+ id: value.id,
303
+ kind: "pin",
304
+ scope: value.scope,
305
+ status: value.status,
306
+ classification: value.classification as PrivacyClassification,
307
+ applicableToActiveBranch: value.applicableToActiveBranch,
308
+ createdAt: value.createdAt,
309
+ updatedAt: value.updatedAt,
310
+ targetRevision: value.targetRevision,
311
+ };
312
+ return copyMatchFields(value, item);
313
+ }
314
+
315
+ function sanitizeMemoryItem(value: unknown): MemoryReadItem | undefined {
316
+ if (!isRecord(value)
317
+ || value.kind !== "memory"
318
+ || typeof value.id !== "string" || !OPAQUE_ID.test(value.id)
319
+ || (value.scope !== "session" && value.scope !== "project")
320
+ || (value.status !== "active" && value.status !== "superseded" && value.status !== "invalid" && value.status !== "expired")
321
+ || typeof value.classification !== "string" || !CLASSIFICATIONS.has(value.classification)
322
+ || !integer(value.createdAt) || !integer(value.updatedAt)
323
+ || typeof value.targetRevision !== "string" || !REVISION.test(value.targetRevision)) return undefined;
324
+ const item: MemoryReadItem = {
325
+ id: value.id,
326
+ kind: "memory",
327
+ scope: value.scope,
328
+ status: value.status,
329
+ classification: value.classification as PrivacyClassification,
330
+ createdAt: value.createdAt,
331
+ updatedAt: value.updatedAt,
332
+ targetRevision: value.targetRevision,
333
+ };
334
+ return copyMatchFields(value, item);
335
+ }
336
+
337
+ function copyMatchFields<T extends PinReadItem | MemoryReadItem>(
338
+ source: Record<string, unknown>,
339
+ item: T,
340
+ ): T {
341
+ const matchKinds = new Set(["exact-key", "exact-phrase", "all-terms", "partial-terms", "metadata-only"]);
342
+ if (typeof source.matchKind === "string" && matchKinds.has(source.matchKind)
343
+ && integer(source.score) && source.score <= 130
344
+ && (source.previewStatus === "included" || source.previewStatus === "omitted-by-policy")) {
345
+ item.matchKind = source.matchKind as MatchKind;
346
+ item.score = source.score;
347
+ item.previewStatus = source.previewStatus;
348
+ }
349
+ return item;
350
+ }
351
+
352
+ function sanitizeSourceItem(value: unknown): SourceReadItem | undefined {
353
+ if (!isRecord(value)
354
+ || value.kind !== "project-memory-source"
355
+ || typeof value.sourceRef !== "string" || !SOURCE_REF.test(value.sourceRef)
356
+ || (value.status !== "ready" && value.status !== "missing" && value.status !== "corrupt" && value.status !== "excluded")
357
+ || !integer(value.indexedMutations) || !integer(value.activeProjectMemories) || !integer(value.activeProjectPins)
358
+ || typeof value.hasMalformedLines !== "boolean"
359
+ || typeof value.targetRevision !== "string" || !REVISION.test(value.targetRevision)) return undefined;
360
+ const errorCode = value.errorCode === "source-missing"
361
+ || value.errorCode === "source-corrupt"
362
+ || value.errorCode === "source-malformed"
363
+ ? value.errorCode
364
+ : undefined;
365
+ return {
366
+ sourceRef: value.sourceRef,
367
+ kind: "project-memory-source",
368
+ status: value.status,
369
+ indexedMutations: value.indexedMutations,
370
+ activeProjectMemories: value.activeProjectMemories,
371
+ activeProjectPins: value.activeProjectPins,
372
+ hasMalformedLines: value.hasMalformedLines,
373
+ targetRevision: value.targetRevision,
374
+ ...(errorCode ? { errorCode } : {}),
375
+ };
376
+ }
377
+
378
+ /** Reduces historical details to the V1 metadata allowlist. */
379
+ export function sanitizeHistoricalContextPersistenceDetails(
380
+ value: unknown,
381
+ ): ContextPersistenceDetails | undefined {
382
+ if (!isRecord(value)
383
+ || value.schema !== CONTEXT_PERSISTENCE_RESULT_CONTRACT
384
+ || typeof value.action !== "string" || !ACTIONS.has(value.action)
385
+ || typeof value.outcome !== "string" || !OUTCOMES.has(value.outcome as ContextPersistenceOutcome)
386
+ || typeof value.persistenceClass !== "string"
387
+ || !PERSISTENCE_CLASSES.has(value.persistenceClass as ContextPersistenceClass)) return undefined;
388
+ const details: ContextPersistenceDetails = {
389
+ schema: CONTEXT_PERSISTENCE_RESULT_CONTRACT,
390
+ action: value.action as ContextPersistenceAction,
391
+ outcome: value.outcome as ContextPersistenceOutcome,
392
+ persistenceClass: value.persistenceClass as ContextPersistenceClass,
393
+ };
394
+ if (Array.isArray(value.items) && value.items.length <= MAX_ITEMS) {
395
+ details.items = value.items.flatMap((candidate) => {
396
+ const record = isRecord(candidate) ? candidate : undefined;
397
+ const item = record?.kind === "pin"
398
+ ? sanitizePinItem(candidate)
399
+ : record?.kind === "memory"
400
+ ? sanitizeMemoryItem(candidate)
401
+ : sanitizeSourceItem(candidate);
402
+ return item ? [item] : [];
403
+ });
404
+ details.count = details.items.length;
405
+ details.truncated = value.truncated === true;
406
+ details.incomplete = value.incomplete === true;
407
+ }
408
+ if (typeof value.id === "string" && OPAQUE_ID.test(value.id)) details.id = value.id;
409
+ if (typeof value.sourceRef === "string" && SOURCE_REF.test(value.sourceRef)) {
410
+ details.sourceRef = value.sourceRef;
411
+ }
412
+ if (value.kind === "pin" || value.kind === "memory" || value.kind === "project-memory-source") {
413
+ details.kind = value.kind;
414
+ }
415
+ if (value.scope === "session" || value.scope === "branch" || value.scope === "project") {
416
+ details.scope = value.scope;
417
+ }
418
+ if (typeof value.status === "string"
419
+ && /^(?:active|superseded|deleted|invalid|expired|ready|missing|corrupt|excluded)$/u.test(value.status)) {
420
+ details.status = value.status;
421
+ }
422
+ if (typeof value.classification === "string" && CLASSIFICATIONS.has(value.classification)) {
423
+ details.classification = value.classification as PrivacyClassification;
424
+ }
425
+ if (typeof value.targetRevision === "string" && REVISION.test(value.targetRevision)) {
426
+ details.targetRevision = value.targetRevision;
427
+ }
428
+ if (typeof value.errorCode === "string" && /^[a-z0-9-]{1,64}$/u.test(value.errorCode)) {
429
+ details.errorCode = value.errorCode;
430
+ }
431
+ if (typeof value.duplicate === "boolean") details.duplicate = value.duplicate;
432
+ return details;
433
+ }
434
+
435
+ /** Historical provider replay never includes previews, even when the original result did. */
436
+ export function renderHistoricalContextPersistenceResult(details: ContextPersistenceDetails | undefined): string {
437
+ if (!details) return "[omitted-by-ds4-egress-policy]";
438
+ if (details.items && typeof details.count === "number") {
439
+ return renderReadContent({
440
+ action: details.action as ContextPersistenceReadAction,
441
+ items: details.items.map((item) => item.kind === "project-memory-source"
442
+ ? item
443
+ : { ...item, previewStatus: "omitted-by-policy" }),
444
+ truncated: details.truncated === true,
445
+ incomplete: details.incomplete === true,
446
+ });
447
+ }
448
+ if (details.outcome === "cancelled" && !details.errorCode) return `${details.action} cancelled.`;
449
+ return renderMutationContent(details);
450
+ }