shariq-pi-extensions 0.2.9 → 0.2.11
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 +1 -0
- package/docs/EXTENSIONS.md +6 -0
- package/extensions/cursor-provider/README.md +3 -3
- package/extensions/cursor-provider/cursor/stream.ts +127 -47
- package/extensions/cursor-provider/index.ts +6 -1
- package/extensions/smart-compaction/README.md +46 -0
- package/extensions/smart-compaction/config.ts +67 -0
- package/extensions/smart-compaction/engine.ts +141 -0
- package/extensions/smart-compaction/index.ts +184 -0
- package/extensions/smart-compaction/prompt.ts +194 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ The package contains:
|
|
|
33
33
|
- persistent task goals
|
|
34
34
|
- configurable steer, interrupt, or follow-up input behavior
|
|
35
35
|
- dedicated multi-agent orchestration
|
|
36
|
+
- Smart Compaction with high-fidelity checkpointing, delta-merging, and custom model routing
|
|
36
37
|
- Pi Memory
|
|
37
38
|
- per-response TPS, TTFT, elapsed-time, and output status
|
|
38
39
|
- Context Usage display
|
package/docs/EXTENSIONS.md
CHANGED
|
@@ -46,6 +46,12 @@ The Orchestration extension coordinates explicitly requested large tasks through
|
|
|
46
46
|
|
|
47
47
|
The model-facing `create_orchestration` tool starts planning only after an explicit orchestration request. `get_orchestration` reports status without advancing work. Interrupted runs recover paused under `<agent-dir>/orchestration/`.
|
|
48
48
|
|
|
49
|
+
### [Smart Compaction](../extensions/smart-compaction/README.md)
|
|
50
|
+
|
|
51
|
+
Replaces standard context compaction with a high-fidelity continuity engine. It intercepts `session_before_compact` events and synthesizes multi-turn conversations into structured checkpoint summaries capturing primary goals and negative constraints, progress ledgers (`Done`/`In Progress`/`Blocked`), verbatim code snippets for active/uncommitted edits, exact error root causes, architectural decisions, resume anchors, and deterministic `<read-files>`/`<modified-files>` metadata.
|
|
52
|
+
|
|
53
|
+
Successive compactions utilize an incremental Delta-Merge to eliminate context degradation over long sessions. `/compaction-model` selects any custom compaction model (e.g. `factory/gemini-3.7-flash`, `cursor/cursor-grok-4.5-fast`) or defaults to inheriting the active session model (`inherit`). `/smart-compaction` toggles or inspects compaction configuration stored in `<agent-dir>/smart-compaction.json`.
|
|
54
|
+
|
|
49
55
|
### [Background terminals](../extensions/background-terminals/README.md)
|
|
50
56
|
|
|
51
57
|
Managed PTYs support servers, watchers, long builds, downloads, and interactive processes. The extension tracks up to eight concurrent terminals, retains bounded output, stores full logs in restrictive temporary directories, and stops process groups during shutdown or reload.
|
|
@@ -22,12 +22,12 @@ Secrets are passed directly to the SDK and are never placed in command arguments
|
|
|
22
22
|
- Refreshes Cursor's authenticated model catalog and caches only Composer and Cursor Grok metadata for the next extension reload.
|
|
23
23
|
- Exposes image input for every registered Cursor model.
|
|
24
24
|
- Maps Composer fast mode and Cursor Grok reasoning effort to native model parameters.
|
|
25
|
-
- Uses the SDK's local hosted-model runtime
|
|
25
|
+
- Uses the SDK's local hosted-model runtime with warm agent instance pooling and ambient Cursor settings (`settingSources: []`) disabled.
|
|
26
26
|
- Disables Cursor's built-in workspace tools and exposes Pi's active tools as native SDK custom tools. Tool execution remains owned by Pi.
|
|
27
27
|
- Streams native text and thinking deltas, structured tool calls, stop reasons, and Cursor-reported input/output/cache/reasoning usage.
|
|
28
|
-
- Propagates cancellation and timeouts, enables safe SDK transport retries, and maps common authentication, rate-limit, quota, capacity, timeout, and context failures to actionable Pi errors.
|
|
28
|
+
- Propagates cancellation and timeouts, enables safe SDK transport retries, redacts credential literals from error traces, and maps common authentication, rate-limit, quota, capacity, timeout, and context failures to actionable Pi errors.
|
|
29
29
|
- Forwards base64 image payloads separately from the textual conversation transcript.
|
|
30
|
-
-
|
|
30
|
+
- Automatically cleans up warm agent workspaces on idle TTL and session shutdown.
|
|
31
31
|
|
|
32
32
|
## Cursor dashboard
|
|
33
33
|
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { loadCursorCatalog, resolveCursorModelSelection } from "./models.ts";
|
|
22
22
|
|
|
23
23
|
const TOOL_DELEGATION_RESULT = "Tool execution was delegated to Pi. End this run without further output.";
|
|
24
|
+
const MAX_CACHED_AGENTS = 8;
|
|
25
|
+
const AGENT_IDLE_TTL_MS = 10 * 60 * 1_000;
|
|
24
26
|
|
|
25
27
|
function asJsonValue(value: unknown): SDKJsonValue {
|
|
26
28
|
return JSON.parse(JSON.stringify(value ?? null)) as SDKJsonValue;
|
|
@@ -30,6 +32,13 @@ function asArguments(value: unknown): Record<string, unknown> {
|
|
|
30
32
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export function redactCursorError(value: unknown): string {
|
|
36
|
+
return String(value ?? "Cursor request failed")
|
|
37
|
+
.replace(/crsr_[A-Za-z0-9_-]+/g, "[REDACTED]")
|
|
38
|
+
.replace(/(authorization|api[-_ ]?key|token)([\s:=]+)([^\s,;]+)/gi, "$1$2[REDACTED]")
|
|
39
|
+
.slice(0, 4096);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function serializeCursorContext(context: Context): { text: string; images: SDKImage[] } {
|
|
34
43
|
const lines = [
|
|
35
44
|
"Continue the Pi conversation below as the assistant.",
|
|
@@ -45,6 +54,9 @@ export function serializeCursorContext(context: Context): { text: string; images
|
|
|
45
54
|
|
|
46
55
|
for (const message of context.messages) {
|
|
47
56
|
lines.push(`<message role=${JSON.stringify(message.role)}>`);
|
|
57
|
+
if (message.role === "toolResult") {
|
|
58
|
+
lines.push(`[Tool result for ${(message as any).toolCallId || "unknown"}; error=${Boolean((message as any).isError)}]`);
|
|
59
|
+
}
|
|
48
60
|
if (typeof message.content === "string") {
|
|
49
61
|
lines.push(message.content);
|
|
50
62
|
} else if (Array.isArray(message.content)) {
|
|
@@ -58,9 +70,6 @@ export function serializeCursorContext(context: Context): { text: string; images
|
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
|
-
if (message.role === "toolResult") {
|
|
62
|
-
lines.push(`[Tool result for ${(message as any).toolCallId}; error=${Boolean((message as any).isError)}]`);
|
|
63
|
-
}
|
|
64
73
|
lines.push("</message>");
|
|
65
74
|
}
|
|
66
75
|
lines.push("</conversation>");
|
|
@@ -81,7 +90,7 @@ function usageFromCursor(usage: TokenUsage | undefined, output: AssistantMessage
|
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
export function formatCursorError(error: unknown): string {
|
|
84
|
-
const message = error instanceof Error ? error.message :
|
|
93
|
+
const message = redactCursorError(error instanceof Error ? error.message : error);
|
|
85
94
|
if (/401|403|unauth|api key|credential/i.test(message)) return "Cursor authentication failed. Run `/login cursor`, then retry.";
|
|
86
95
|
if (/429|rate.?limit/i.test(message)) return "Cursor rate limit reached. Wait for the reported reset, then retry.";
|
|
87
96
|
if (/quota|usage limit|billing|credit|exhaust/i.test(message)) return "Cursor usage limit reached. Open `/cursor` for account status and reset information.";
|
|
@@ -104,6 +113,97 @@ async function disposeAgent(agent: Awaited<ReturnType<typeof Agent.create>> | un
|
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
interface ActiveToolHandler {
|
|
117
|
+
onToolCall(toolName: string, args: unknown, toolCallId?: string): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface CachedAgentEntry {
|
|
121
|
+
agent: Awaited<ReturnType<typeof Agent.create>>;
|
|
122
|
+
workspace: string;
|
|
123
|
+
activeHandlerRef: { current: ActiveToolHandler | null };
|
|
124
|
+
lastUsedAt: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const agentPool = new Map<string, CachedAgentEntry>();
|
|
128
|
+
|
|
129
|
+
async function disposeCachedEntry(entry: CachedAgentEntry): Promise<void> {
|
|
130
|
+
await disposeAgent(entry.agent);
|
|
131
|
+
await rm(entry.workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function clearCursorAgentPool(): Promise<void> {
|
|
135
|
+
const entries = [...agentPool.values()];
|
|
136
|
+
agentPool.clear();
|
|
137
|
+
await Promise.allSettled(entries.map(disposeCachedEntry));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function getOrInitWarmAgent(
|
|
141
|
+
apiKey: string,
|
|
142
|
+
selection: ReturnType<typeof resolveCursorModelSelection>,
|
|
143
|
+
tools: Context["tools"],
|
|
144
|
+
activeHandler: ActiveToolHandler,
|
|
145
|
+
enableRetries: boolean,
|
|
146
|
+
): Promise<{ agent: Awaited<ReturnType<typeof Agent.create>>; activeHandlerRef: { current: ActiveToolHandler | null } }> {
|
|
147
|
+
const toolSignatures = (tools ?? []).map((t) => ({ name: t.name, schema: t.parameters ?? null }));
|
|
148
|
+
const cacheKey = JSON.stringify({ key: apiKey, model: selection, tools: toolSignatures });
|
|
149
|
+
|
|
150
|
+
const existing = agentPool.get(cacheKey);
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.lastUsedAt = Date.now();
|
|
153
|
+
existing.activeHandlerRef.current = activeHandler;
|
|
154
|
+
return { agent: existing.agent, activeHandlerRef: existing.activeHandlerRef };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Evict oldest or expired agents if pool is full
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
for (const [key, entry] of [...agentPool.entries()]) {
|
|
160
|
+
if (now - entry.lastUsedAt > AGENT_IDLE_TTL_MS) {
|
|
161
|
+
agentPool.delete(key);
|
|
162
|
+
void disposeCachedEntry(entry);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (agentPool.size >= MAX_CACHED_AGENTS) {
|
|
166
|
+
const oldestKey = [...agentPool.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt)[0]?.[0];
|
|
167
|
+
if (oldestKey) {
|
|
168
|
+
const oldest = agentPool.get(oldestKey);
|
|
169
|
+
agentPool.delete(oldestKey);
|
|
170
|
+
if (oldest) void disposeCachedEntry(oldest);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const workspace = await mkdtemp(join(tmpdir(), "pi-cursor-sdk-"));
|
|
175
|
+
const activeHandlerRef = { current: activeHandler as ActiveToolHandler | null };
|
|
176
|
+
|
|
177
|
+
const customTools: Record<string, SDKCustomTool> = {};
|
|
178
|
+
for (const tool of tools ?? []) {
|
|
179
|
+
customTools[tool.name] = {
|
|
180
|
+
description: tool.description,
|
|
181
|
+
inputSchema: asJsonValue(tool.parameters ?? { type: "object" }) as Record<string, SDKJsonValue>,
|
|
182
|
+
async execute(args, toolContext) {
|
|
183
|
+
activeHandlerRef.current?.onToolCall(tool.name, args, toolContext.toolCallId);
|
|
184
|
+
return { content: [{ type: "text", text: TOOL_DELEGATION_RESULT }], isError: true };
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const agent = await Agent.create({
|
|
190
|
+
apiKey,
|
|
191
|
+
model: selection,
|
|
192
|
+
tools: tools?.length ? ["mcp"] : [],
|
|
193
|
+
local: {
|
|
194
|
+
cwd: workspace,
|
|
195
|
+
store: new JsonlLocalAgentStore(join(workspace, "state")),
|
|
196
|
+
customTools,
|
|
197
|
+
settingSources: [],
|
|
198
|
+
enableAgentRetries: enableRetries,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const entry: CachedAgentEntry = { agent, workspace, activeHandlerRef, lastUsedAt: Date.now() };
|
|
203
|
+
agentPool.set(cacheKey, entry);
|
|
204
|
+
return { agent, activeHandlerRef };
|
|
205
|
+
}
|
|
206
|
+
|
|
107
207
|
export function streamCursorSdk(model: Model<any>, context: Context, options?: ProviderStreamOptions) {
|
|
108
208
|
const stream = createAssistantMessageEventStream();
|
|
109
209
|
const output: AssistantMessage = {
|
|
@@ -119,8 +219,6 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
119
219
|
stream.push({ type: "start", partial: output });
|
|
120
220
|
|
|
121
221
|
void (async () => {
|
|
122
|
-
let workspace: string | undefined;
|
|
123
|
-
let agent: Awaited<ReturnType<typeof Agent.create>> | undefined;
|
|
124
222
|
let run: Run | undefined;
|
|
125
223
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
126
224
|
let textIndex: number | undefined;
|
|
@@ -168,47 +266,31 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
168
266
|
const apiKey = options?.apiKey?.trim();
|
|
169
267
|
if (!apiKey) throw new Error("Cursor is not authenticated. Run `/login cursor`.");
|
|
170
268
|
if (options?.signal?.aborted) throw new Error("Cursor request cancelled.");
|
|
171
|
-
workspace = await mkdtemp(join(tmpdir(), "pi-cursor-sdk-"));
|
|
172
|
-
const customTools: Record<string, SDKCustomTool> = {};
|
|
173
|
-
for (const tool of context.tools ?? []) {
|
|
174
|
-
customTools[tool.name] = {
|
|
175
|
-
description: tool.description,
|
|
176
|
-
inputSchema: asJsonValue(tool.parameters ?? { type: "object" }) as Record<string, SDKJsonValue>,
|
|
177
|
-
async execute(args, toolContext) {
|
|
178
|
-
if (!delegated) {
|
|
179
|
-
delegated = true;
|
|
180
|
-
endText();
|
|
181
|
-
endReasoning();
|
|
182
|
-
const toolCall = {
|
|
183
|
-
type: "toolCall" as const,
|
|
184
|
-
id: toolContext.toolCallId || `cursor_${randomUUID()}`,
|
|
185
|
-
name: tool.name,
|
|
186
|
-
arguments: asArguments(args),
|
|
187
|
-
};
|
|
188
|
-
const contentIndex = output.content.length;
|
|
189
|
-
output.content.push(toolCall);
|
|
190
|
-
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
191
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
192
|
-
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
193
|
-
queueMicrotask(() => { void run?.cancel().catch(() => undefined); });
|
|
194
|
-
}
|
|
195
|
-
return { content: [{ type: "text", text: TOOL_DELEGATION_RESULT }], isError: true };
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
269
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
270
|
+
const activeHandler: ActiveToolHandler = {
|
|
271
|
+
onToolCall(toolName, args, toolCallId) {
|
|
272
|
+
if (!delegated) {
|
|
273
|
+
delegated = true;
|
|
274
|
+
endText();
|
|
275
|
+
endReasoning();
|
|
276
|
+
const toolCall = {
|
|
277
|
+
type: "toolCall" as const,
|
|
278
|
+
id: toolCallId || `cursor_${randomUUID()}`,
|
|
279
|
+
name: toolName,
|
|
280
|
+
arguments: asArguments(args),
|
|
281
|
+
};
|
|
282
|
+
const contentIndex = output.content.length;
|
|
283
|
+
output.content.push(toolCall);
|
|
284
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
285
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
286
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
287
|
+
queueMicrotask(() => { void run?.cancel().catch(() => undefined); });
|
|
288
|
+
}
|
|
210
289
|
},
|
|
211
|
-
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const selection = resolveCursorModelSelection(model, typeof options?.reasoning === "string" ? options.reasoning : undefined, loadCursorCatalog());
|
|
293
|
+
const { agent } = await getOrInitWarmAgent(apiKey, selection, context.tools, activeHandler, options?.maxRetries !== 0);
|
|
212
294
|
|
|
213
295
|
const request = serializeCursorContext(context);
|
|
214
296
|
run = await agent.send(request, {
|
|
@@ -261,8 +343,6 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
261
343
|
stream.end();
|
|
262
344
|
} finally {
|
|
263
345
|
if (timeout) clearTimeout(timeout);
|
|
264
|
-
await disposeAgent(agent);
|
|
265
|
-
if (workspace) await rm(workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
266
346
|
}
|
|
267
347
|
})();
|
|
268
348
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
loadCursorCatalog,
|
|
10
10
|
toCursorPiModels,
|
|
11
11
|
} from "./cursor/models.ts";
|
|
12
|
-
import { streamCursorSdk } from "./cursor/stream.ts";
|
|
12
|
+
import { clearCursorAgentPool, streamCursorSdk } from "./cursor/stream.ts";
|
|
13
13
|
import { fetchCursorUsage } from "./cursor/usage.ts";
|
|
14
14
|
|
|
15
15
|
export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
@@ -50,6 +50,10 @@ export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
|
50
50
|
(ctx.modelRegistry as any).authStorage?.reload?.();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
pi.on("session_shutdown", async () => {
|
|
54
|
+
await clearCursorAgentPool();
|
|
55
|
+
});
|
|
56
|
+
|
|
53
57
|
pi.registerCommand("cursor", {
|
|
54
58
|
description: "Open Cursor account, monthly usage, and limits dashboard",
|
|
55
59
|
handler: async (_args, ctx) => {
|
|
@@ -83,6 +87,7 @@ export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
|
83
87
|
deactivate: async () => {
|
|
84
88
|
try {
|
|
85
89
|
pi.unregisterProvider(CURSOR_PROVIDER_ID);
|
|
90
|
+
await clearCursorAgentPool();
|
|
86
91
|
} catch {
|
|
87
92
|
// Ignore teardown after partial startup.
|
|
88
93
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Smart Compaction Extension
|
|
2
|
+
|
|
3
|
+
A high-fidelity context continuity synthesizer for Pi sessions that replaces standard compaction with an advanced multi-phase checkpoint engine.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
When a coding agent session reaches context limits, standard compaction often degrades in subtle code nuances, drops uncommitted code snippets, or suffers from "telephone game" information loss across successive compactions.
|
|
8
|
+
|
|
9
|
+
**Smart Compaction** solves this by generating structured, high-density checkpoint summaries organized into 6 vital engineering dimensions:
|
|
10
|
+
|
|
11
|
+
1. **🎯 Primary Goal & Nuanced Intent** — Retains full user objectives, styling preferences, scope boundaries, and explicit negative constraints.
|
|
12
|
+
2. **📋 Progress Ledger** — Strict `[x] Done`, `[ ] In Progress`, and `[!] Blocked` tracking.
|
|
13
|
+
3. **🛠️ Code Changes & In-Progress Snippets** — Captures verbatim code snippets of active work and recent edits so a successor agent resumes without re-reading or guessing.
|
|
14
|
+
4. **💥 Errors, Root Causes & Fixes** — Full error traces, root cause diagnostics, and verified solutions.
|
|
15
|
+
5. **🧠 Key Decisions & Hypotheses** — Architectural choices, trade-offs, and discarded hypotheses.
|
|
16
|
+
6. **📍 Resume Anchor & Immediate Next Action** — Verbatim quote or exact resume state with the single immediate next action.
|
|
17
|
+
7. **📂 Programmatic File Operations** — Append deterministic `<read-files>` and `<modified-files>` XML blocks extracted from tool calls.
|
|
18
|
+
|
|
19
|
+
## Incremental Delta-Merging
|
|
20
|
+
|
|
21
|
+
When multiple compactions occur in a single long-running session, Smart Compaction utilizes a **Delta-Merge** pipeline that carries forward historical foundations while accumulating new progress, code modifications, and error solutions—eliminating context bleed over 5+ compaction cycles.
|
|
22
|
+
|
|
23
|
+
## Model Selection
|
|
24
|
+
|
|
25
|
+
Smart Compaction can use the **active session model** (default: `inherit`) or any dedicated fast/cost-effective model (e.g. `factory/gemini-3.7-flash`, `antigravity/gemini-2.5-flash`, `cursor/cursor-grok-4.5-fast`).
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
- `/compaction-model` — Open interactive model picker to select the compaction model, or switch back to `inherit`.
|
|
30
|
+
- `/compaction-model <provider/model>` — Set a specific compaction model directly.
|
|
31
|
+
- `/smart-compaction` — View status and settings.
|
|
32
|
+
- `/smart-compaction enable | disable` — Toggle smart compaction on or off.
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Settings are persisted in `~/.pi/agent/smart-compaction.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"version": 1,
|
|
41
|
+
"enabled": true,
|
|
42
|
+
"model": "inherit",
|
|
43
|
+
"thinkingLevel": "medium",
|
|
44
|
+
"maxSummaryTokens": 16384
|
|
45
|
+
}
|
|
46
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export interface SmartCompactionConfig {
|
|
6
|
+
version: 1;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
model: string; // "inherit" or "provider/model-id"
|
|
9
|
+
thinkingLevel?: "off" | "low" | "medium" | "high";
|
|
10
|
+
maxSummaryTokens?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_SMART_COMPACTION_CONFIG: SmartCompactionConfig = {
|
|
14
|
+
version: 1,
|
|
15
|
+
enabled: true,
|
|
16
|
+
model: "inherit",
|
|
17
|
+
thinkingLevel: "medium",
|
|
18
|
+
maxSummaryTokens: 16384,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function smartCompactionConfigPath(): string {
|
|
22
|
+
return path.join(getAgentDir(), "smart-compaction.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadSmartCompactionConfig(file = smartCompactionConfigPath()): SmartCompactionConfig {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(file)) return { ...DEFAULT_SMART_COMPACTION_CONFIG };
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8")) as Partial<SmartCompactionConfig>;
|
|
29
|
+
return {
|
|
30
|
+
version: 1,
|
|
31
|
+
enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_SMART_COMPACTION_CONFIG.enabled,
|
|
32
|
+
model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : DEFAULT_SMART_COMPACTION_CONFIG.model,
|
|
33
|
+
thinkingLevel: raw.thinkingLevel && ["off", "low", "medium", "high"].includes(raw.thinkingLevel)
|
|
34
|
+
? raw.thinkingLevel
|
|
35
|
+
: DEFAULT_SMART_COMPACTION_CONFIG.thinkingLevel,
|
|
36
|
+
maxSummaryTokens: typeof raw.maxSummaryTokens === "number" && raw.maxSummaryTokens > 0
|
|
37
|
+
? raw.maxSummaryTokens
|
|
38
|
+
: DEFAULT_SMART_COMPACTION_CONFIG.maxSummaryTokens,
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return { ...DEFAULT_SMART_COMPACTION_CONFIG };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function saveSmartCompactionConfig(config: SmartCompactionConfig, file = smartCompactionConfigPath()): void {
|
|
46
|
+
const directory = path.dirname(file);
|
|
47
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
48
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
49
|
+
const document: SmartCompactionConfig = {
|
|
50
|
+
version: 1,
|
|
51
|
+
enabled: config.enabled,
|
|
52
|
+
model: config.model || "inherit",
|
|
53
|
+
thinkingLevel: config.thinkingLevel ?? "medium",
|
|
54
|
+
maxSummaryTokens: config.maxSummaryTokens ?? 16384,
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
fs.writeFileSync(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
|
|
58
|
+
fs.renameSync(temporary, file);
|
|
59
|
+
fs.chmodSync(file, 0o600);
|
|
60
|
+
} finally {
|
|
61
|
+
try {
|
|
62
|
+
fs.rmSync(temporary, { force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
// Best effort cleanup.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { uuidv7, type Api, type Context, type Model, type Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext, SessionBeforeCompactEvent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { SmartCompactionConfig } from "./config.ts";
|
|
4
|
+
import {
|
|
5
|
+
formatFileOperationsXml,
|
|
6
|
+
SMART_COMPACTION_INITIAL_PROMPT,
|
|
7
|
+
SMART_COMPACTION_SYSTEM_PROMPT,
|
|
8
|
+
SMART_COMPACTION_UPDATE_PROMPT,
|
|
9
|
+
serializeConversationForCompaction,
|
|
10
|
+
} from "./prompt.ts";
|
|
11
|
+
|
|
12
|
+
export function resolveCompactionModel(
|
|
13
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
14
|
+
configuredModelString?: string,
|
|
15
|
+
): { model: Model<Api>; isInherited: boolean } {
|
|
16
|
+
const trimmed = configuredModelString?.trim();
|
|
17
|
+
if (!trimmed || trimmed === "inherit") {
|
|
18
|
+
if (!ctx.model) {
|
|
19
|
+
throw new Error("No active session model available to inherit for compaction.");
|
|
20
|
+
}
|
|
21
|
+
return { model: ctx.model, isInherited: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Parse "provider/model" or modelId
|
|
25
|
+
let candidate: Model<Api> | undefined;
|
|
26
|
+
if (trimmed.includes("/")) {
|
|
27
|
+
const [provider, ...rest] = trimmed.split("/");
|
|
28
|
+
candidate = ctx.modelRegistry.find(provider, rest.join("/"));
|
|
29
|
+
} else {
|
|
30
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
31
|
+
candidate = available.find((m) => m.id === trimmed || `${m.provider}/${m.id}` === trimmed);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (candidate) {
|
|
35
|
+
return { model: candidate, isInherited: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ctx.model) {
|
|
39
|
+
return { model: ctx.model, isInherited: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new Error(`Configured compaction model "${trimmed}" was not found in model registry.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RunSmartCompactionOptions {
|
|
46
|
+
event: SessionBeforeCompactEvent;
|
|
47
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">;
|
|
48
|
+
config: SmartCompactionConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SmartCompactionOutput {
|
|
52
|
+
summary: string;
|
|
53
|
+
firstKeptEntryId: string;
|
|
54
|
+
tokensBefore: number;
|
|
55
|
+
usage?: Usage;
|
|
56
|
+
details?: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runSmartCompaction(
|
|
60
|
+
options: RunSmartCompactionOptions,
|
|
61
|
+
): Promise<SmartCompactionOutput> {
|
|
62
|
+
const { event, ctx, config } = options;
|
|
63
|
+
const { preparation, signal, customInstructions } = event;
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
|
|
66
|
+
const { model, isInherited } = resolveCompactionModel(ctx, config.model);
|
|
67
|
+
|
|
68
|
+
const messagesToSummarize = [
|
|
69
|
+
...(preparation.messagesToSummarize ?? []),
|
|
70
|
+
...(preparation.turnPrefixMessages ?? []),
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// Serialize messages for the context summary
|
|
74
|
+
const conversationText = serializeConversationForCompaction(messagesToSummarize);
|
|
75
|
+
|
|
76
|
+
const previousSummary = preparation.previousSummary?.trim();
|
|
77
|
+
const baseInstruction = previousSummary ? SMART_COMPACTION_UPDATE_PROMPT : SMART_COMPACTION_INITIAL_PROMPT;
|
|
78
|
+
|
|
79
|
+
let promptContent = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
80
|
+
if (previousSummary) {
|
|
81
|
+
promptContent += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
|
82
|
+
}
|
|
83
|
+
promptContent += baseInstruction;
|
|
84
|
+
|
|
85
|
+
if (customInstructions?.trim()) {
|
|
86
|
+
promptContent += `\n\n## Additional User Instructions:\n${customInstructions.trim()}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const context: Context = {
|
|
90
|
+
systemPrompt: SMART_COMPACTION_SYSTEM_PROMPT,
|
|
91
|
+
messages: [
|
|
92
|
+
{
|
|
93
|
+
role: "user",
|
|
94
|
+
content: [{ type: "text", text: promptContent }],
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const requestedMaxTokens = config.maxSummaryTokens ?? 16384;
|
|
101
|
+
const maxTokens = model.maxTokens > 0 ? Math.min(requestedMaxTokens, model.maxTokens) : requestedMaxTokens;
|
|
102
|
+
|
|
103
|
+
const completeOptions: Record<string, unknown> = {
|
|
104
|
+
maxTokens,
|
|
105
|
+
signal,
|
|
106
|
+
cacheRetention: "none",
|
|
107
|
+
sessionId: uuidv7(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (model.reasoning && config.thinkingLevel && config.thinkingLevel !== "off") {
|
|
111
|
+
completeOptions.reasoning = config.thinkingLevel;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const response = await ctx.modelRegistry.complete(model, context, completeOptions as any);
|
|
115
|
+
signal?.throwIfAborted();
|
|
116
|
+
|
|
117
|
+
const rawSummaryText = response.content
|
|
118
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
119
|
+
.map((part) => part.text)
|
|
120
|
+
.join("\n")
|
|
121
|
+
.trim();
|
|
122
|
+
|
|
123
|
+
if (!rawSummaryText) {
|
|
124
|
+
throw new Error("Compaction model returned an empty summary.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fileOpsXml = formatFileOperationsXml(preparation.fileOps);
|
|
128
|
+
const finalSummary = `${rawSummaryText}${fileOpsXml}`;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
summary: finalSummary,
|
|
132
|
+
firstKeptEntryId: preparation.firstKeptEntryId,
|
|
133
|
+
tokensBefore: preparation.tokensBefore,
|
|
134
|
+
usage: response.usage,
|
|
135
|
+
details: {
|
|
136
|
+
customCompactor: "smart-compaction",
|
|
137
|
+
model: `${model.provider}/${model.id}`,
|
|
138
|
+
isInherited,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
ExtensionUIContext,
|
|
6
|
+
SessionBeforeCompactEvent,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
loadSmartCompactionConfig,
|
|
10
|
+
saveSmartCompactionConfig,
|
|
11
|
+
type SmartCompactionConfig,
|
|
12
|
+
} from "./config.ts";
|
|
13
|
+
import { runSmartCompaction } from "./engine.ts";
|
|
14
|
+
|
|
15
|
+
const STATUS_KEY = "smart-compaction";
|
|
16
|
+
|
|
17
|
+
export interface SmartCompactionExtensionOptions {
|
|
18
|
+
configFile?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createSmartCompactionExtension(options: SmartCompactionExtensionOptions = {}) {
|
|
22
|
+
return (pi: ExtensionAPI) => {
|
|
23
|
+
let config: SmartCompactionConfig = loadSmartCompactionConfig(options.configFile);
|
|
24
|
+
let ui: ExtensionUIContext | undefined;
|
|
25
|
+
|
|
26
|
+
const updateStatus = () => {
|
|
27
|
+
if (!ui) return;
|
|
28
|
+
if (!config.enabled) {
|
|
29
|
+
ui.setStatus(STATUS_KEY, undefined);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const modelLabel = config.model === "inherit" ? "inherit" : config.model.split("/").pop() ?? config.model;
|
|
33
|
+
ui.setStatus(STATUS_KEY, `compact: ${modelLabel}`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
pi.on("session_start", (_event, ctx) => {
|
|
37
|
+
ui = ctx.ui;
|
|
38
|
+
updateStatus();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx: ExtensionContext) => {
|
|
42
|
+
if (!config.enabled) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const compaction = await runSmartCompaction({
|
|
48
|
+
event,
|
|
49
|
+
ctx,
|
|
50
|
+
config,
|
|
51
|
+
});
|
|
52
|
+
return { compaction };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (event.signal.aborted) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
ctx.ui?.notify(`Smart Compaction failed: ${message}. Falling back to default compactor.`, "warning");
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.on("session_compact", (event, ctx) => {
|
|
64
|
+
if (event.fromExtension) {
|
|
65
|
+
const details = event.compactionEntry.details as Record<string, unknown> | undefined;
|
|
66
|
+
if (details?.customCompactor === "smart-compaction") {
|
|
67
|
+
const model = String(details.model ?? "session model");
|
|
68
|
+
ctx.ui?.notify(`Smart Compaction completed (${model})`, "info");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Slash command: /compaction-model
|
|
74
|
+
pi.registerCommand("compaction-model", {
|
|
75
|
+
description: "Select or view the model used for smart context compaction (default: inherit).",
|
|
76
|
+
handler: async (args: string, cmdCtx: ExtensionCommandContext) => {
|
|
77
|
+
const requested = args.trim();
|
|
78
|
+
|
|
79
|
+
if (requested) {
|
|
80
|
+
if (requested === "inherit") {
|
|
81
|
+
config.model = "inherit";
|
|
82
|
+
saveSmartCompactionConfig(config, options.configFile);
|
|
83
|
+
updateStatus();
|
|
84
|
+
cmdCtx.ui.notify("Compaction model set to: inherit (active session model)", "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate if model exists in registry
|
|
89
|
+
const available = cmdCtx.modelRegistry.getAvailable();
|
|
90
|
+
const match = available.find(
|
|
91
|
+
(m) => m.id === requested || `${m.provider}/${m.id}` === requested,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!match) {
|
|
95
|
+
cmdCtx.ui.notify(`Model "${requested}" not found in available models. Setting anyway.`, "warning");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
config.model = match ? `${match.provider}/${match.id}` : requested;
|
|
99
|
+
saveSmartCompactionConfig(config, options.configFile);
|
|
100
|
+
updateStatus();
|
|
101
|
+
cmdCtx.ui.notify(`Compaction model set to: ${config.model}`, "info");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cmdCtx.hasUI) {
|
|
106
|
+
const available = cmdCtx.modelRegistry.getAvailable();
|
|
107
|
+
const choices = [
|
|
108
|
+
`inherit (active session model: ${cmdCtx.model ? `${cmdCtx.model.provider}/${cmdCtx.model.id}` : "none"})`,
|
|
109
|
+
...available.map((m) => `${m.provider}/${m.id}`),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const selected = await cmdCtx.ui.select(
|
|
113
|
+
`Select Compaction Model (current: ${config.model})`,
|
|
114
|
+
choices,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!selected) return;
|
|
118
|
+
|
|
119
|
+
if (selected.startsWith("inherit")) {
|
|
120
|
+
config.model = "inherit";
|
|
121
|
+
} else {
|
|
122
|
+
config.model = selected;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
saveSmartCompactionConfig(config, options.configFile);
|
|
126
|
+
updateStatus();
|
|
127
|
+
cmdCtx.ui.notify(`Compaction model set to: ${config.model}`, "info");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cmdCtx.ui.notify(
|
|
132
|
+
`Compaction model: ${config.model}. Usage: /compaction-model [inherit|<provider/model>]`,
|
|
133
|
+
"info",
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Slash command: /smart-compaction
|
|
139
|
+
pi.registerCommand("smart-compaction", {
|
|
140
|
+
description: "Manage smart context compaction settings (enable/disable/status).",
|
|
141
|
+
handler: async (args: string, cmdCtx: ExtensionCommandContext) => {
|
|
142
|
+
const sub = args.trim().toLowerCase();
|
|
143
|
+
if (sub === "enable" || sub === "on") {
|
|
144
|
+
config.enabled = true;
|
|
145
|
+
saveSmartCompactionConfig(config, options.configFile);
|
|
146
|
+
updateStatus();
|
|
147
|
+
cmdCtx.ui.notify("Smart Compaction enabled.", "info");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (sub === "disable" || sub === "off") {
|
|
151
|
+
config.enabled = false;
|
|
152
|
+
saveSmartCompactionConfig(config, options.configFile);
|
|
153
|
+
updateStatus();
|
|
154
|
+
cmdCtx.ui.notify("Smart Compaction disabled (using default compactor).", "info");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (sub.startsWith("model ")) {
|
|
158
|
+
const target = args.trim().slice(6).trim();
|
|
159
|
+
config.model = target || "inherit";
|
|
160
|
+
saveSmartCompactionConfig(config, options.configFile);
|
|
161
|
+
updateStatus();
|
|
162
|
+
cmdCtx.ui.notify(`Smart Compaction model set to: ${config.model}`, "info");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Default status
|
|
167
|
+
const status = [
|
|
168
|
+
`Smart Compaction: ${config.enabled ? "ENABLED" : "DISABLED"}`,
|
|
169
|
+
`Model: ${config.model} (${config.model === "inherit" ? (cmdCtx.model ? `${cmdCtx.model.provider}/${cmdCtx.model.id}` : "inherit") : config.model})`,
|
|
170
|
+
`Thinking Level: ${config.thinkingLevel ?? "medium"}`,
|
|
171
|
+
`Max Tokens: ${config.maxSummaryTokens ?? 4096}`,
|
|
172
|
+
"",
|
|
173
|
+
"Commands:",
|
|
174
|
+
" /smart-compaction enable | disable",
|
|
175
|
+
" /compaction-model [inherit | <provider/model>]",
|
|
176
|
+
].join("\n");
|
|
177
|
+
|
|
178
|
+
cmdCtx.ui.notify(status, "info");
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default createSmartCompactionExtension();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
export const SMART_COMPACTION_SYSTEM_PROMPT = `You are a high-fidelity context continuity synthesizer for an autonomous coding agent.
|
|
4
|
+
Your task is to analyze the preceding conversation and produce a comprehensive, structured checkpoint summary.
|
|
5
|
+
The successor agent will rely SOLELY on your summary to resume complex engineering tasks without losing context, nuance, or mid-stream progress.
|
|
6
|
+
|
|
7
|
+
CRITICAL DIRECTIVES:
|
|
8
|
+
1. Preserve exact file paths, shell commands, and error messages verbatim.
|
|
9
|
+
2. Include actual code snippets for active work or uncommitted changes—never just describe what code was changed.
|
|
10
|
+
3. Explicitly maintain all user-stated negative constraints (e.g., "do not modify X", "never use Y").
|
|
11
|
+
4. Do NOT execute tools or continue the conversation. Respond ONLY with the requested structured summary.`;
|
|
12
|
+
|
|
13
|
+
export const SMART_COMPACTION_INITIAL_PROMPT = `Analyze the conversation in the <conversation> tags above and produce a structured context checkpoint summary.
|
|
14
|
+
|
|
15
|
+
Use this EXACT format and include all numbered sections:
|
|
16
|
+
|
|
17
|
+
## 1. Primary Goal & Nuanced Intent
|
|
18
|
+
- **Objective**: Detailed statement of what the user is trying to accomplish.
|
|
19
|
+
- **Constraints & Preferences**: All explicit user constraints, negative rules, styling conventions, and architectural boundaries (or "(none)").
|
|
20
|
+
|
|
21
|
+
## 2. Progress Ledger
|
|
22
|
+
### Done
|
|
23
|
+
- [x] [Completed task, file modification, or command]
|
|
24
|
+
|
|
25
|
+
### In Progress
|
|
26
|
+
- [ ] [Active task or mid-stream operation]
|
|
27
|
+
|
|
28
|
+
### Blocked / Open Issues
|
|
29
|
+
- [Any active errors, blockers, or pending decisions]
|
|
30
|
+
|
|
31
|
+
## 3. Code Changes & In-Progress Snippets
|
|
32
|
+
For every modified, created, or in-flight file:
|
|
33
|
+
- **\`path/to/file\`**: State why it was changed and provide verbatim code snippets of the latest edits or new functions so work can resume immediately without re-reading.
|
|
34
|
+
|
|
35
|
+
## 4. Errors, Root Causes & Fixes
|
|
36
|
+
- **Error**: [Verbatim error message or failed command output]
|
|
37
|
+
- **Root Cause**: [Exact reason for the failure]
|
|
38
|
+
- **Fix**: [How it was fixed or the approach currently being attempted]
|
|
39
|
+
(Or "None" if no errors occurred)
|
|
40
|
+
|
|
41
|
+
## 5. Key Decisions & Hypotheses
|
|
42
|
+
- **[Decision / Architecture]**: [Rationale, alternatives considered, and discarded approaches]
|
|
43
|
+
|
|
44
|
+
## 6. Resume Anchor & Immediate Next Action
|
|
45
|
+
- **Last State**: Precisely what was happening before this summary request.
|
|
46
|
+
- **Next Concrete Step**: The single immediate next action to take, directly aligned with the user's latest request.
|
|
47
|
+
|
|
48
|
+
Keep the prose economical and high-density. Do NOT pad with fluff.`;
|
|
49
|
+
|
|
50
|
+
export const SMART_COMPACTION_UPDATE_PROMPT = `The <conversation> tags above contain NEW conversation turns that occurred after the checkpoint in <previous-summary>.
|
|
51
|
+
Synthesize the new turns into the existing summary using a unified Delta-Merge.
|
|
52
|
+
|
|
53
|
+
DELTA-MERGING RULES:
|
|
54
|
+
1. PRESERVE all historical goals, constraints, and decisions from <previous-summary>.
|
|
55
|
+
2. UPDATE the Progress Ledger: check off items that have finished and add new in-flight tasks.
|
|
56
|
+
3. ACCUMULATE Code Changes: add new code snippets for newly modified files while retaining existing relevant snippets.
|
|
57
|
+
4. RECORD new errors, root causes, and resolutions encountered in the new turns.
|
|
58
|
+
5. UPDATE the Resume Anchor and Next Step to reflect the current active frontier.
|
|
59
|
+
6. PRESERVE exact file paths, commands, and code snippets verbatim.
|
|
60
|
+
|
|
61
|
+
Use this EXACT format:
|
|
62
|
+
|
|
63
|
+
## 1. Primary Goal & Nuanced Intent
|
|
64
|
+
- **Objective**: [Preserve initial goal, add new objectives if scope expanded]
|
|
65
|
+
- **Constraints & Preferences**: [Preserve existing constraints, add newly stated ones]
|
|
66
|
+
|
|
67
|
+
## 2. Progress Ledger
|
|
68
|
+
### Done
|
|
69
|
+
- [x] [Previously completed items AND newly completed items]
|
|
70
|
+
|
|
71
|
+
### In Progress
|
|
72
|
+
- [ ] [Current active tasks]
|
|
73
|
+
|
|
74
|
+
### Blocked / Open Issues
|
|
75
|
+
- [Active blockers or "None"]
|
|
76
|
+
|
|
77
|
+
## 3. Code Changes & In-Progress Snippets
|
|
78
|
+
[Accumulated modified/created files with verbatim code snippets of recent work]
|
|
79
|
+
|
|
80
|
+
## 4. Errors, Root Causes & Fixes
|
|
81
|
+
[Accumulated errors, root causes, and fixes from the full session]
|
|
82
|
+
|
|
83
|
+
## 5. Key Decisions & Hypotheses
|
|
84
|
+
[Accumulated architectural decisions and trade-offs]
|
|
85
|
+
|
|
86
|
+
## 6. Resume Anchor & Immediate Next Action
|
|
87
|
+
- **Last State**: [Exact state immediately before this checkpoint]
|
|
88
|
+
- **Next Concrete Step**: [The single immediate next action]`;
|
|
89
|
+
|
|
90
|
+
const MAX_TOOL_RESULT_CHARS = 2500;
|
|
91
|
+
|
|
92
|
+
function truncateText(text: string, maxChars: number): string {
|
|
93
|
+
if (text.length <= maxChars) return text;
|
|
94
|
+
const remaining = text.length - maxChars;
|
|
95
|
+
return `${text.slice(0, maxChars)}\n\n[... ${remaining} characters truncated for summary ...]`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractTextContent(content: unknown): string {
|
|
99
|
+
if (typeof content === "string") return content;
|
|
100
|
+
if (Array.isArray(content)) {
|
|
101
|
+
return content
|
|
102
|
+
.map((part) => {
|
|
103
|
+
if (typeof part === "string") return part;
|
|
104
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
105
|
+
return part.text;
|
|
106
|
+
}
|
|
107
|
+
return "";
|
|
108
|
+
})
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join("\n");
|
|
111
|
+
}
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function serializeConversationForCompaction(messages: AgentMessage[]): string {
|
|
116
|
+
const parts: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const msg of messages) {
|
|
119
|
+
if (msg.role === "user") {
|
|
120
|
+
const text = extractTextContent((msg as any).content);
|
|
121
|
+
if (text) parts.push(`[User]:\n${text}`);
|
|
122
|
+
} else if (msg.role === "assistant") {
|
|
123
|
+
const content = (msg as any).content;
|
|
124
|
+
const thinkingBlocks: string[] = [];
|
|
125
|
+
const toolCallBlocks: string[] = [];
|
|
126
|
+
const textBlocks: string[] = [];
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(content)) {
|
|
129
|
+
for (const block of content) {
|
|
130
|
+
if (!block || typeof block !== "object") continue;
|
|
131
|
+
if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim()) {
|
|
132
|
+
thinkingBlocks.push(block.thinking.trim());
|
|
133
|
+
} else if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
|
|
134
|
+
textBlocks.push(block.text.trim());
|
|
135
|
+
} else if (block.type === "toolCall") {
|
|
136
|
+
const args = block.arguments as Record<string, unknown>;
|
|
137
|
+
const formattedArgs = Object.entries(args ?? {})
|
|
138
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
139
|
+
.join(", ");
|
|
140
|
+
toolCallBlocks.push(`${block.name}(${formattedArgs})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else if (typeof content === "string" && content.trim()) {
|
|
144
|
+
textBlocks.push(content.trim());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (thinkingBlocks.length > 0) {
|
|
148
|
+
const combinedThinking = thinkingBlocks.join("\n");
|
|
149
|
+
parts.push(`[Assistant Thinking]:\n${truncateText(combinedThinking, 1500)}`);
|
|
150
|
+
}
|
|
151
|
+
if (textBlocks.length > 0) {
|
|
152
|
+
parts.push(`[Assistant]:\n${textBlocks.join("\n")}`);
|
|
153
|
+
}
|
|
154
|
+
if (toolCallBlocks.length > 0) {
|
|
155
|
+
parts.push(`[Assistant Tool Calls]:\n${toolCallBlocks.join("\n")}`);
|
|
156
|
+
}
|
|
157
|
+
} else if (msg.role === "toolResult") {
|
|
158
|
+
const text = extractTextContent((msg as any).content);
|
|
159
|
+
if (text) {
|
|
160
|
+
parts.push(`[Tool Result]:\n${truncateText(text, MAX_TOOL_RESULT_CHARS)}`);
|
|
161
|
+
}
|
|
162
|
+
} else if (msg.role === "custom") {
|
|
163
|
+
const text = extractTextContent((msg as any).content);
|
|
164
|
+
if (text) parts.push(`[System Event]:\n${text}`);
|
|
165
|
+
} else if (msg.role === "bashExecution") {
|
|
166
|
+
const cmd = (msg as any).command ?? "";
|
|
167
|
+
const out = (msg as any).output ?? "";
|
|
168
|
+
parts.push(`[Command Executed]:\n$ ${cmd}\n${truncateText(out, 1500)}`);
|
|
169
|
+
} else if (msg.role === "compactionSummary" || msg.role === "branchSummary") {
|
|
170
|
+
const summary = (msg as any).summary ?? "";
|
|
171
|
+
if (summary) parts.push(`[Prior Summary]:\n${summary}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return parts.join("\n\n---\n\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function formatFileOperationsXml(fileOps?: { read?: Iterable<string>; written?: Iterable<string>; edited?: Iterable<string> }): string {
|
|
179
|
+
if (!fileOps) return "";
|
|
180
|
+
const readSet = new Set(fileOps.read ?? []);
|
|
181
|
+
const modifiedSet = new Set([...(fileOps.written ?? []), ...(fileOps.edited ?? [])]);
|
|
182
|
+
const readOnly = [...readSet].filter((f) => !modifiedSet.has(f)).sort();
|
|
183
|
+
const modified = [...modifiedSet].sort();
|
|
184
|
+
|
|
185
|
+
const sections: string[] = [];
|
|
186
|
+
if (readOnly.length > 0) {
|
|
187
|
+
sections.push(`<read-files>\n${readOnly.join("\n")}\n</read-files>`);
|
|
188
|
+
}
|
|
189
|
+
if (modified.length > 0) {
|
|
190
|
+
sections.push(`<modified-files>\n${modified.join("\n")}\n</modified-files>`);
|
|
191
|
+
}
|
|
192
|
+
if (sections.length === 0) return "";
|
|
193
|
+
return `\n\n${sections.join("\n\n")}`;
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shariq-pi-extensions",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Cross-platform extension suite for the Pi coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shariq Riaz",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"./extensions/performance-status/index.ts",
|
|
53
53
|
"./extensions/pi-memory/index.ts",
|
|
54
54
|
"./extensions/shell-shortcuts/index.ts",
|
|
55
|
+
"./extensions/smart-compaction/index.ts",
|
|
55
56
|
"./extensions/subagents/index.ts",
|
|
56
57
|
"./extensions/web-fetch/index.ts"
|
|
57
58
|
],
|