pi-smart-compact 7.5.2 → 7.7.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.
- package/README.md +492 -182
- package/dist/index.js +2979 -0
- package/package.json +5 -3
- package/src/constants.ts +0 -140
- package/src/core.ts +0 -360
- package/src/index.ts +0 -175
- package/src/phases/explore.ts +0 -371
- package/src/phases/synthesize.ts +0 -184
- package/src/phases/verify.ts +0 -191
- package/src/types.ts +0 -176
- package/src/ui/overlays.ts +0 -329
- package/src/utils/cache.ts +0 -145
- package/src/utils/damage.ts +0 -153
- package/src/utils/extraction.ts +0 -259
- package/src/utils/fingerprint.ts +0 -190
- package/src/utils/helpers.ts +0 -161
- package/src/utils/message-blocks.ts +0 -21
- package/src/utils/pruning.ts +0 -147
- package/src/utils/tokens.ts +0 -63
package/src/phases/synthesize.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 3: Hierarchical Synthesis.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { Model, Api } from "@earendil-works/pi-ai";
|
|
6
|
-
import type {
|
|
7
|
-
LlmMessage, LlmChunk, ChunkSummary, StructuredExtraction,
|
|
8
|
-
ExplorationReport, ProfileConfig, CacheAwareOptions,
|
|
9
|
-
} from "../types.ts";
|
|
10
|
-
import { COMPACT_SYSTEM_PREFIX, SINGLE_PASS_PREFIX, SINGLE_PASS_SUFFIX, BATCH_PROMPT_PREFIX, BATCH_PROMPT_SUFFIX, ASSEMBLY_PROMPT_PREFIX, ASSEMBLY_PROMPT_SUFFIX, SESSION_TYPE_INSTRUCTIONS } from "../constants.ts";
|
|
11
|
-
import { estimateTokens } from "../utils/tokens.ts";
|
|
12
|
-
import { trackedComplete, cacheOpts } from "../utils/cache.ts";
|
|
13
|
-
import { extractText } from "../utils/extraction.ts";
|
|
14
|
-
import { buildExtractionContext, buildExplorationContext, createBatches, preProcessSummaries } from "../utils/helpers.ts";
|
|
15
|
-
|
|
16
|
-
export function chunkLlmMessages(msgs: LlmMessage[], boundaries: import("../types.ts").TopicBoundary[], pc: ProfileConfig): LlmChunk[] {
|
|
17
|
-
if (!msgs.length) return [];
|
|
18
|
-
if (!boundaries.length) {
|
|
19
|
-
return [{
|
|
20
|
-
startIndex: 0, endIndex: msgs.length - 1,
|
|
21
|
-
tokenEstimate: estimateTokens(JSON.stringify(msgs)),
|
|
22
|
-
topic: "Full conversation", priority: "normal", messages: msgs,
|
|
23
|
-
}];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const sorted = [...boundaries].sort((a, b) => a.afterIndex - b.afterIndex);
|
|
27
|
-
const chunks: LlmChunk[] = [];
|
|
28
|
-
let start = 0;
|
|
29
|
-
|
|
30
|
-
for (const bp of sorted) {
|
|
31
|
-
const end = bp.afterIndex + 1;
|
|
32
|
-
if (end > start && end <= msgs.length) {
|
|
33
|
-
const slice = msgs.slice(start, end);
|
|
34
|
-
chunks.push({
|
|
35
|
-
startIndex: start, endIndex: end - 1,
|
|
36
|
-
tokenEstimate: estimateTokens(JSON.stringify(slice)),
|
|
37
|
-
topic: bp.topic || "Segment " + (chunks.length + 1),
|
|
38
|
-
priority: bp.priority,
|
|
39
|
-
messages: slice,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
start = end;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (start < msgs.length) {
|
|
46
|
-
const slice = msgs.slice(start);
|
|
47
|
-
const lastTopic = sorted.length ? "After: " + sorted[sorted.length - 1].topic : "Full conversation";
|
|
48
|
-
chunks.push({
|
|
49
|
-
startIndex: start, endIndex: msgs.length - 1,
|
|
50
|
-
tokenEstimate: estimateTokens(JSON.stringify(slice)),
|
|
51
|
-
topic: lastTopic, priority: "normal", messages: slice,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const merged: LlmChunk[] = [];
|
|
56
|
-
for (const ch of chunks) {
|
|
57
|
-
if (merged.length && ch.tokenEstimate < pc.minChunkTokens) {
|
|
58
|
-
const prev = merged[merged.length - 1];
|
|
59
|
-
prev.endIndex = ch.endIndex;
|
|
60
|
-
prev.tokenEstimate += ch.tokenEstimate;
|
|
61
|
-
prev.messages = msgs.slice(prev.startIndex, prev.endIndex + 1);
|
|
62
|
-
prev.topic = prev.topic + " + " + ch.topic;
|
|
63
|
-
} else {
|
|
64
|
-
merged.push(ch);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return merged;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function singlePassCompact(
|
|
71
|
-
convText: string, extraction: StructuredExtraction, report: ExplorationReport | null,
|
|
72
|
-
prevContext: string,
|
|
73
|
-
model: Model<Api>, auth: { apiKey: string; headers?: Record<string, string> }, signal?: AbortSignal,
|
|
74
|
-
): Promise<{ summary: string; llmCalls: 1 }> {
|
|
75
|
-
const extractionCtx = buildExtractionContext(extraction);
|
|
76
|
-
const explorationCtx = report ? buildExplorationContext(report) : "";
|
|
77
|
-
// Session-aware prompt adaptation
|
|
78
|
-
const sessionType = report?.sessionType ?? "implementation";
|
|
79
|
-
const sessionInstruction = SESSION_TYPE_INSTRUCTIONS[sessionType] ?? SESSION_TYPE_INSTRUCTIONS.implementation;
|
|
80
|
-
const adaptedPrefix = SINGLE_PASS_PREFIX + "\nSession-specific instructions:\n" + sessionInstruction;
|
|
81
|
-
const dynamicSuffix = SINGLE_PASS_SUFFIX
|
|
82
|
-
.replace("{PREV_CONTEXT}", prevContext)
|
|
83
|
-
.replace("{EXTRACTION_CONTEXT}", extractionCtx)
|
|
84
|
-
.replace("{EXPLORATION_CONTEXT}", explorationCtx)
|
|
85
|
-
.replace("{CONVERSATION}", convText);
|
|
86
|
-
|
|
87
|
-
const resp = await trackedComplete("single-pass", model, {
|
|
88
|
-
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
89
|
-
messages: [
|
|
90
|
-
{ role: "user" as const, content: [{ type: "text" as const, text: adaptedPrefix }] },
|
|
91
|
-
{ role: "user" as const, content: [{ type: "text" as const, text: dynamicSuffix }] },
|
|
92
|
-
],
|
|
93
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
|
|
94
|
-
const summary = (resp.content as any[]).filter((c: any): c is { type: "text"; text: string } => c?.type === "text").map(c => c.text).join("\n").trim();
|
|
95
|
-
if (!summary.startsWith("##")) throw new Error("Single-pass malformed output");
|
|
96
|
-
return { summary, llmCalls: 1 };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function summarizeBatch(
|
|
100
|
-
batch: LlmChunk[], extraction: StructuredExtraction,
|
|
101
|
-
model: Model<Api>, auth: { apiKey: string; headers?: Record<string, string> }, signal?: AbortSignal,
|
|
102
|
-
): Promise<ChunkSummary[]> {
|
|
103
|
-
const range = { start: batch[0].startIndex, end: batch[batch.length - 1].endIndex };
|
|
104
|
-
const extractionCtx = buildExtractionContext(extraction, range);
|
|
105
|
-
// Decision propagation: inject decisions from before this batch's range
|
|
106
|
-
const activeDecisions = extraction.decisions
|
|
107
|
-
.filter(d => d.index < range.start)
|
|
108
|
-
.map(d => "- " + d.summary.slice(0, 120) + (d.userResponse ? " → " + d.userResponse.slice(0, 60) : ""));
|
|
109
|
-
const decisionCtx = activeDecisions.length
|
|
110
|
-
? "\n## Active Decisions from previous segments (honour these):\n" + activeDecisions.join("\n")
|
|
111
|
-
: "";
|
|
112
|
-
const text = batch.map(ch => "--- Topic: " + ch.topic + " (" + ch.priority + ") ---\n" + ch.messages.map((m) => {
|
|
113
|
-
const role = m?.role ?? "unknown";
|
|
114
|
-
const content = extractText(m?.content).slice(0, 500);
|
|
115
|
-
return "[" + role + "] " + content;
|
|
116
|
-
}).join("\n")).join("\n\n");
|
|
117
|
-
const dynamicSuffix = BATCH_PROMPT_SUFFIX.replace("{EXTRACTION_CONTEXT}", extractionCtx + decisionCtx).replace("{TEXT}", text);
|
|
118
|
-
|
|
119
|
-
const resp = await trackedComplete("batch", model, {
|
|
120
|
-
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
121
|
-
messages: [
|
|
122
|
-
{ role: "user" as const, content: [{ type: "text" as const, text: BATCH_PROMPT_PREFIX }] },
|
|
123
|
-
{ role: "user" as const, content: [{ type: "text" as const, text: dynamicSuffix }] },
|
|
124
|
-
],
|
|
125
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
126
|
-
const output = (resp.content as any[]).filter((c: any): c is { type: "text"; text: string } => c?.type === "text").map(c => c.text).join("\n");
|
|
127
|
-
const sections = output.split(/^### /m).filter(s => s.trim());
|
|
128
|
-
return batch.map((ch, i) => {
|
|
129
|
-
const sec = sections[i] ?? "";
|
|
130
|
-
const f = (n: string) => { const m = sec.match(new RegExp("\\*\\*" + n + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i")); return m ? m[1].trim() : ""; };
|
|
131
|
-
const l = (n: string) => { const v = f(n); return !v || v === "None" ? [] : v.split(",").map(s => s.trim()).filter(Boolean); };
|
|
132
|
-
const prio = f("Priority").toLowerCase();
|
|
133
|
-
return {
|
|
134
|
-
topic: ch.topic,
|
|
135
|
-
startIndex: ch.startIndex, endIndex: ch.endIndex,
|
|
136
|
-
summary: f("Summary") || sec.split("\n").slice(1).join("\n").trim().slice(0, 500),
|
|
137
|
-
keyDecisions: l("Decisions"), filesModified: l("Modified"), filesRead: l("Read"),
|
|
138
|
-
priority: ["critical", "high", "normal", "low"].includes(prio) ? prio as ChunkSummary["priority"] : ch.priority,
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function assembleLLM(
|
|
144
|
-
summaries: ChunkSummary[], extraction: StructuredExtraction, report: ExplorationReport | null,
|
|
145
|
-
model: Model<Api>, auth: { apiKey: string; headers?: Record<string, string> }, budget: number,
|
|
146
|
-
prevContext: string, signal?: AbortSignal,
|
|
147
|
-
): Promise<string> {
|
|
148
|
-
const pp = preProcessSummaries(summaries, budget);
|
|
149
|
-
const detModified = extraction.modifiedFiles.map(f => f.path);
|
|
150
|
-
const detRead = extraction.readFiles;
|
|
151
|
-
const explorationCtx = report ? buildExplorationContext(report) : "";
|
|
152
|
-
const dynamicSuffix = ASSEMBLY_PROMPT_SUFFIX
|
|
153
|
-
.replace("{DECISIONS}", pp.decisions.join("; ") || "None")
|
|
154
|
-
.replace("{MODIFIED}", detModified.join(", ") || "None")
|
|
155
|
-
.replace("{READ}", detRead.join(", ") || "None")
|
|
156
|
-
.replace("{EXPLORATION_CONTEXT}", explorationCtx)
|
|
157
|
-
.replace("{PREV_CONTEXT}", prevContext)
|
|
158
|
-
.replace("{SUMMARIES}", pp.text);
|
|
159
|
-
|
|
160
|
-
const resp = await trackedComplete("assemble", model, {
|
|
161
|
-
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
162
|
-
messages: [
|
|
163
|
-
{ role: "user" as const, content: [{ type: "text" as const, text: ASSEMBLY_PROMPT_PREFIX }] },
|
|
164
|
-
{ role: "user" as const, content: [{ type: "text" as const, text: dynamicSuffix }] },
|
|
165
|
-
],
|
|
166
|
-
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, 8192), signal }));
|
|
167
|
-
return (resp.content as any[]).filter((c: any): c is { type: "text"; text: string } => c?.type === "text").map(c => c.text).join("\n").trim();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function assembleFallback(summaries: ChunkSummary[], extraction: StructuredExtraction): string {
|
|
171
|
-
const detModified = extraction.modifiedFiles.map(f => f.path);
|
|
172
|
-
const detRead = extraction.readFiles;
|
|
173
|
-
return [
|
|
174
|
-
"## Goal", extraction.mainGoal ?? "See topics below.", "",
|
|
175
|
-
"## Constraints & Preferences", ...extraction.constraints.map(c => "- [" + c.category + "] " + c.text.slice(0, 200)), "",
|
|
176
|
-
"## Progress", "### Done", "- See topics below", "### In Progress", ...summaries.filter(s => s.priority === "high").map(s => "- [ ] " + s.summary.slice(0, 150)), "### Blocked", "- None", "",
|
|
177
|
-
"## Key Decisions", ...extraction.decisions.map(d => "- **" + d.summary.slice(0, 100) + "**" + (d.userResponse ? " → " + d.userResponse : "")), "",
|
|
178
|
-
"## Files Modified", ...detModified.map(f => "- " + f), "",
|
|
179
|
-
"## Files Read", ...detRead.map(f => "- " + f), "",
|
|
180
|
-
"## Next Steps", "1. See topics below", "",
|
|
181
|
-
"## Critical Context", ...extraction.errors.filter(e => !e.resolved).map(e => "- Unresolved error: " + e.message.slice(0, 100)), "",
|
|
182
|
-
"## Topics Covered", ...summaries.map(s => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0, 200)),
|
|
183
|
-
].join("\n");
|
|
184
|
-
}
|
package/src/phases/verify.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
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
|
-
}
|