pi-smart-compact 7.5.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,191 @@
1
+ /**
2
+ * Phase 4: Verification + Quality Score.
3
+ */
4
+
5
+ import type { Model, Api } from "@earendil-works/pi-ai";
6
+ import type { StructuredExtraction, VerificationResult, CacheAwareOptions } from "../types.ts";
7
+ import { COMPACT_SYSTEM_PREFIX } from "../constants.ts";
8
+ import { trackedComplete, cacheOpts } from "../utils/cache.ts";
9
+
10
+ export function verifySummary(summary: string, extraction: StructuredExtraction): VerificationResult {
11
+ const gaps: string[] = [];
12
+ const lower = summary.toLowerCase();
13
+ let score = 100;
14
+
15
+ for (const f of extraction.modifiedFiles) {
16
+ const pathLower = f.path.toLowerCase();
17
+ // Build path suffix array: "src/index.ts", "index.ts", "index"
18
+ const parts = pathLower.split("/");
19
+ const suffixes: string[] = [];
20
+ for (let j = 0; j < parts.length; j++) {
21
+ suffixes.push(parts.slice(j).join("/"));
22
+ }
23
+ const pathMatch = suffixes.some(s => s.length > 2 && lower.includes(s));
24
+ if (!pathMatch) {
25
+ gaps.push("Missing modified file: " + f.path);
26
+ score -= 5;
27
+ }
28
+ }
29
+
30
+ for (const e of extraction.errors.filter(e => !e.resolved)) {
31
+ const snippet = e.message.slice(0, 30).toLowerCase();
32
+ if (snippet.length > 5 && !lower.includes(snippet)) {
33
+ gaps.push("Missing error: " + e.message.slice(0, 80));
34
+ score -= 5;
35
+ }
36
+ }
37
+
38
+ for (const c of extraction.constraints.filter(c => c.confidence >= 0.8)) {
39
+ const keywords = c.text.split(/\s+/).filter(w => w.length > 4).slice(0, 3);
40
+ const found = keywords.some(k => lower.includes(k.toLowerCase()));
41
+ if (!found && keywords.length > 0) {
42
+ gaps.push("Missing constraint: " + c.text.slice(0, 100));
43
+ score -= 3;
44
+ }
45
+ }
46
+
47
+ if (extraction.mainGoal) {
48
+ const goalWords = extraction.mainGoal.split(/\s+/).filter(w => w.length > 3).slice(0, 4);
49
+ const goalFound = goalWords.some(w => lower.includes(w.toLowerCase()));
50
+ if (!goalFound) { gaps.push("Main goal may be missing from summary"); score -= 10; }
51
+ }
52
+
53
+ if (!lower.includes("## goal")) score -= 5;
54
+ if (!lower.includes("## progress")) score -= 5;
55
+ if (!lower.includes("## critical context")) score -= 3;
56
+
57
+ const summaryFileRefs = (summary.match(/[\w.\/-]+\.[\w]+/g) ?? []).filter(
58
+ p => p.includes("/") || p.match(/\.(ts|tsx|js|jsx|rs|py|go|java|rb|css|html|json|yaml|yml|toml|md|sh|sql)$/i)
59
+ );
60
+ const knownFiles = new Set([
61
+ ...extraction.modifiedFiles.map(f => f.path.toLowerCase()),
62
+ ...extraction.readFiles.map(f => f.toLowerCase()),
63
+ ]);
64
+ for (const ref of summaryFileRefs) {
65
+ const refLower = ref.toLowerCase();
66
+ const isKnown = [...knownFiles].some(kf => kf.endsWith("/" + refLower) || kf === refLower || kf.endsWith(refLower) && refLower.length > 3);
67
+ if (!isKnown) {
68
+ gaps.push("Potentially fabricated file: " + ref);
69
+ score -= 4;
70
+ }
71
+ }
72
+
73
+ const errorFiles = new Set(extraction.errors.map(e => e.message));
74
+ if (errorFiles.size > 0) {
75
+ const doneSection = (summary.match(/### Done[\s\S]*?(?=###|$)/i) ?? [""])[0];
76
+ if (doneSection) {
77
+ for (const f of extraction.modifiedFiles) {
78
+ const bn = f.path.split("/").pop() ?? "";
79
+ const hasError = [...errorFiles].some(e => e.toLowerCase().includes(bn.toLowerCase()));
80
+ const markedDone = doneSection.toLowerCase().includes(bn.toLowerCase());
81
+ if (hasError && markedDone) {
82
+ const unresolved = extraction.errors.find(e => e.message.toLowerCase().includes(bn.toLowerCase()) && !e.resolved);
83
+ if (unresolved) {
84
+ gaps.push("Inconsistency: " + bn + " marked Done but has unresolved error");
85
+ score -= 5;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ const highConfDecisions = extraction.decisions.filter(d => d.type === "explicit");
93
+ if (highConfDecisions.length > 0) {
94
+ const decisionSection = (summary.match(/## Key Decisions[\s\S]*?(?=##|$)/i) ?? [""])[0];
95
+ for (const d of highConfDecisions) {
96
+ const keywords = d.summary.split(/\s+/).filter(w => w.length > 4).slice(0, 3);
97
+ if (keywords.length > 0 && !keywords.some(k => decisionSection.toLowerCase().includes(k.toLowerCase()))) {
98
+ gaps.push("Missing decision: " + d.summary.slice(0, 100));
99
+ score -= 3;
100
+ }
101
+ }
102
+ }
103
+
104
+ return { ok: gaps.length === 0, gaps, score: Math.max(0, score) };
105
+ }
106
+
107
+ /**
108
+ * Deterministic patch — injects missing items directly into the summary
109
+ * without an LLM call. Appends gaps to the relevant sections.
110
+ */
111
+ export function patchDeterministic(summary: string, gaps: string[], extraction: StructuredExtraction): string {
112
+ let patched = summary;
113
+ const fileGaps = gaps.filter(g => g.startsWith("Missing modified file:"));
114
+ const errorGaps = gaps.filter(g => g.startsWith("Missing error:"));
115
+ const constraintGaps = gaps.filter(g => g.startsWith("Missing constraint:"));
116
+ const decisionGaps = gaps.filter(g => g.startsWith("Missing decision:"));
117
+ const otherGaps = gaps.filter(g =>
118
+ !g.startsWith("Missing modified file:") &&
119
+ !g.startsWith("Missing error:") &&
120
+ !g.startsWith("Missing constraint:") &&
121
+ !g.startsWith("Missing decision:") &&
122
+ !g.startsWith("Potentially fabricated") &&
123
+ !g.startsWith("Inconsistency")
124
+ );
125
+
126
+ // Inject missing files into Files Modified section
127
+ if (fileGaps.length > 0) {
128
+ const filesSection = patched.match(/## Files Modified\n/);
129
+ if (filesSection) {
130
+ const insertPos = filesSection.index! + filesSection[0].length;
131
+ const entries = fileGaps.map(g => "- " + g.replace("Missing modified file: ", "")).join("\n") + "\n";
132
+ patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
133
+ }
134
+ }
135
+
136
+ // Inject missing errors into Critical Context section
137
+ if (errorGaps.length > 0) {
138
+ const ctxSection = patched.match(/## Critical Context\n/);
139
+ if (ctxSection) {
140
+ const insertPos = ctxSection.index! + ctxSection[0].length;
141
+ const entries = errorGaps.map(g => "- " + g).join("\n") + "\n";
142
+ patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
143
+ }
144
+ }
145
+
146
+ // Inject missing constraints into Constraints section
147
+ if (constraintGaps.length > 0) {
148
+ const constrSection = patched.match(/## Constraints & Preferences\n/);
149
+ if (constrSection) {
150
+ const insertPos = constrSection.index! + constrSection[0].length;
151
+ const entries = constraintGaps.map(g => "- " + g).join("\n") + "\n";
152
+ patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
153
+ }
154
+ }
155
+
156
+ // Inject missing decisions into Key Decisions section
157
+ if (decisionGaps.length > 0) {
158
+ const decSection = patched.match(/## Key Decisions\n/);
159
+ if (decSection) {
160
+ const insertPos = decSection.index! + decSection[0].length;
161
+ const entries = decisionGaps.map(g => "- **" + g.replace("Missing decision: ", "") + "**").join("\n") + "\n";
162
+ patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
163
+ }
164
+ }
165
+
166
+ // Append any remaining gaps as a verification note
167
+ if (otherGaps.length > 0) {
168
+ patched += "\n## Verification Note\n" + otherGaps.map(g => "- " + g).join("\n");
169
+ }
170
+
171
+ return patched;
172
+ }
173
+
174
+ export async function patchSummary(
175
+ summary: string, gaps: string[],
176
+ model: Model<Api>, auth: { apiKey: string; headers?: Record<string, string> }, signal?: AbortSignal,
177
+ ): Promise<string> {
178
+ const patchPrompt = "The summary below is missing some critical information. Add the missing items WITHOUT restructuring the summary.\n\nMissing items:\n" +
179
+ gaps.map((g, i) => (i + 1) + ". " + g).join("\n") +
180
+ "\n\nCurrent summary:\n" + summary +
181
+ "\n\nReturn the COMPLETE updated summary with missing items integrated. Keep the same format.";
182
+
183
+ try {
184
+ const resp = await trackedComplete("patch", model, {
185
+ systemPrompt: COMPACT_SYSTEM_PREFIX,
186
+ messages: [{ role: "user" as const, content: [{ type: "text" as const, text: patchPrompt }] }],
187
+ }, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
188
+ const patched = (resp.content as any[]).filter((c: any): c is { type: "text"; text: string } => c?.type === "text").map(c => c.text).join("\n").trim();
189
+ return patched.startsWith("##") ? patched : summary;
190
+ } catch { return summary; }
191
+ }
package/src/types.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Core type definitions for the Smart Compact extension.
3
+ */
4
+
5
+ import type { Model, Api } from "@earendil-works/pi-ai";
6
+
7
+ export type CompressionProfile = "light" | "balanced" | "aggressive";
8
+
9
+ export interface ProfileConfig {
10
+ summaryBudgetTokens: number;
11
+ keepRecentTokens: number;
12
+ minChunkTokens: number;
13
+ maxChunkTokens: number;
14
+ singlePassMaxTokens: number;
15
+ batchMaxTokens: number;
16
+ }
17
+
18
+ export interface CompactConfig {
19
+ profile: CompressionProfile;
20
+ profiles: Record<CompressionProfile, ProfileConfig>;
21
+ summaryModel: string | null;
22
+ segmentationModel: string | null;
23
+ autoTrigger: boolean;
24
+ backupEnabled: boolean;
25
+ backupDir: string;
26
+ }
27
+
28
+ export interface ProviderCapabilities {
29
+ maxOutputTokens: number;
30
+ supportsTools: boolean | "probe";
31
+ jsonReliability: "high" | "medium" | "low";
32
+ instructionFollowing: "high" | "medium" | "low";
33
+ tokenRatioEstimate: number;
34
+ concurrencyLimit: number;
35
+ cacheStrategy: "anthropic" | "openai" | "none";
36
+ }
37
+
38
+ export interface LLMCallMetric {
39
+ phase: "probe" | "explore" | "explore-loop" | "explore-retry" | "explore-direct" | "single-pass" | "batch" | "assemble" | "patch";
40
+ model: string;
41
+ inputTokens: number;
42
+ outputTokens: number;
43
+ cacheHitTokens: number;
44
+ latencyMs: number;
45
+ success: boolean;
46
+ }
47
+
48
+ export interface TopicBoundary {
49
+ afterIndex: number;
50
+ topic: string;
51
+ priority: "critical" | "high" | "normal" | "low";
52
+ confidence: number;
53
+ }
54
+
55
+ export interface ChunkSummary {
56
+ topic: string;
57
+ startIndex: number;
58
+ endIndex: number;
59
+ summary: string;
60
+ keyDecisions: string[];
61
+ filesModified: string[];
62
+ filesRead: string[];
63
+ priority: "critical" | "high" | "normal" | "low";
64
+ }
65
+
66
+ export interface SmartCompactDetails {
67
+ method: "eesv" | "single-pass" | "heuristic";
68
+ chunkCount: number;
69
+ topics: string[];
70
+ readFiles: string[];
71
+ modifiedFiles: string[];
72
+ totalMessages: number;
73
+ totalTokensSummarized: number;
74
+ llmCalls: number;
75
+ profile: CompressionProfile;
76
+ backupPath: string | null;
77
+ tokensSaved: number;
78
+ verified: boolean;
79
+ gaps: string[];
80
+ explorationRounds: number;
81
+ explorationBoundaries: number;
82
+ model: string;
83
+ qualityScore: number;
84
+ tokensBefore: number;
85
+ }
86
+
87
+ export interface PendingCompaction {
88
+ summary: string;
89
+ firstKeptEntryId: string;
90
+ tokensBefore: number;
91
+ details: SmartCompactDetails;
92
+ }
93
+
94
+ export interface ModelOption {
95
+ value: string;
96
+ label: string;
97
+ model: Model<Api>;
98
+ supportsTools: boolean;
99
+ }
100
+
101
+ export interface VerificationResult {
102
+ ok: boolean;
103
+ gaps: string[];
104
+ score: number;
105
+ }
106
+
107
+ export interface ExplorationReport {
108
+ boundaries: TopicBoundary[];
109
+ mainGoal: string;
110
+ sessionType: "implementation" | "review" | "debugging" | "discussion";
111
+ enrichedConstraints: string[];
112
+ crossReferences: string[];
113
+ statusAssessment: { done: string[]; inProgress: string[]; blocked: string[] };
114
+ criticalContext: string[];
115
+ keyDecisions: string[];
116
+ }
117
+
118
+ export interface StructuredExtraction {
119
+ modifiedFiles: Array<{ path: string; toolCalls: number; lastModifiedIndex: number }>;
120
+ readFiles: string[];
121
+ deletedFiles: string[];
122
+ errors: Array<{ index: number; tool: string; message: string; retryAttempted: boolean; resolved: boolean }>;
123
+ decisions: Array<{ index: number; type: "explicit" | "implicit"; summary: string; userResponse?: string }>;
124
+ constraints: Array<{ index: number; text: string; category: "requirement" | "preference" | "prohibition"; confidence: number }>;
125
+ topics: Array<{ startIndex: number; endIndex: number; primaryFile: string | null; type: "implementation" | "debugging" | "exploration" | "review"; errorDensity: number }>;
126
+ timeline: Array<{ index: number; event: string; summary: string }>;
127
+ mainGoal: string | null;
128
+ lastUserMessages: string[];
129
+ lastErrors: string[];
130
+ messageCount: number;
131
+ }
132
+
133
+ export interface LlmChunk {
134
+ startIndex: number;
135
+ endIndex: number;
136
+ tokenEstimate: number;
137
+ topic: string;
138
+ priority: "critical" | "high" | "normal" | "low";
139
+ messages: LlmMessage[];
140
+ }
141
+
142
+ export interface LlmMessage {
143
+ role: "user" | "assistant" | "toolResult";
144
+ content?: unknown;
145
+ isError?: boolean;
146
+ toolCallId?: string;
147
+ timestamp?: number;
148
+ }
149
+
150
+ export interface ToolCallBlock {
151
+ type: "toolCall";
152
+ id?: string;
153
+ name: string;
154
+ arguments: Record<string, unknown>;
155
+ }
156
+
157
+ export interface TextBlock {
158
+ type: "text";
159
+ text: string;
160
+ }
161
+
162
+ export interface LlmTextBlock { type: "text"; text: string; }
163
+ export interface LlmToolCallBlock { type: "toolCall"; id?: string; name: string; arguments: Record<string, unknown>; }
164
+ export type LlmContentBlock = LlmTextBlock | LlmToolCallBlock | string;
165
+
166
+ export interface ProgressState {
167
+ phase: number;
168
+ phaseName: string;
169
+ detail: string;
170
+ extraction?: StructuredExtraction;
171
+ explorationRounds?: number;
172
+ totalBatches?: number;
173
+ currentBatch?: number;
174
+ model?: string;
175
+ profile?: string;
176
+ }