pi-context 1.1.3 → 2.0.0-beta.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 +24 -14
- package/package.json +15 -9
- package/skills/context-management/SKILL.md +242 -226
- package/skills/context-management/references/development-and-troubleshooting.md +75 -0
- package/skills/context-management/references/planning-and-execution.md +79 -0
- package/skills/context-management/references/repeated-items-and-batch-work.md +68 -0
- package/skills/context-management/references/retry-branch-and-pivot.md +71 -0
- package/skills/context-management/references/search-research-and-reading.md +82 -0
- package/skills/context-management/references/task-switching-and-cleanup.md +63 -0
- package/src/context.ts +5 -4
- package/src/index.ts +80 -72
- package/dist/index.js +0 -370
package/src/index.ts
CHANGED
|
@@ -2,14 +2,15 @@ import {
|
|
|
2
2
|
type ExtensionAPI,
|
|
3
3
|
type SessionManager,
|
|
4
4
|
type SessionEntry,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
type ExtensionCommandContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
Type,
|
|
9
|
+
type Static,
|
|
10
|
+
type TextContent,
|
|
11
|
+
type ImageContent,
|
|
12
|
+
type ToolCall,
|
|
13
|
+
} from "@earendil-works/pi-ai";
|
|
13
14
|
import { formatTokens } from "./utils.js";
|
|
14
15
|
|
|
15
16
|
// Define missing types locally as they are not exported from the main entry point
|
|
@@ -19,9 +20,9 @@ interface SessionTreeNode {
|
|
|
19
20
|
label?: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const InternalTools = ["
|
|
23
|
+
const InternalTools = ["context_checkpoint", "context_timeline", "context_rewind"];
|
|
23
24
|
let CommandCtx: ExtensionCommandContext | null = null;
|
|
24
|
-
let
|
|
25
|
+
let RewindParams: any = null;
|
|
25
26
|
|
|
26
27
|
const isInternal = (name: string) => InternalTools.includes(name);
|
|
27
28
|
|
|
@@ -46,20 +47,20 @@ const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
|
46
47
|
return target;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
const
|
|
50
|
+
const ContextTimelineParams = Type.Object({
|
|
50
51
|
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
51
|
-
verbose: Type.Optional(Type.Boolean({ description: "If true, show
|
|
52
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show all messages. If false (default), collapse intermediate AI steps and only show milestones, user messages, checkpoints, branch points, and summaries." })),
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
-
target: Type.String({ description: "Where to
|
|
56
|
-
message: Type.String({ description: "The
|
|
57
|
-
|
|
55
|
+
const ContextRewindParams = Type.Object({
|
|
56
|
+
target: Type.String({ description: "Where to rewind or compact to. Can be a checkpoint name, a history node ID, or root. This becomes the starting point for a fresh continuation of the conversation." }),
|
|
57
|
+
message: Type.String({ description: "The carryover summary for the fresh continuation. Summarize current progress, lessons, important file changes, and the next step so the agent can continue from a clean context. A good summary message includes status, reason, important changes, and next step." }),
|
|
58
|
+
backupCheckpoint: Type.Optional(Type.String({ description: "Optional checkpoint name to apply to the current conversation state before rewinding. Use this to preserve a named backup of the noisy path you are about to compact away." })),
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
const
|
|
61
|
-
name: Type.String({ description: "The
|
|
62
|
-
target: Type.Optional(Type.String({ description: "
|
|
61
|
+
const ContextCheckpointParams = Type.Object({
|
|
62
|
+
name: Type.String({ description: "The checkpoint or milestone name. Use meaningful names." }),
|
|
63
|
+
target: Type.Optional(Type.String({ description: "Optional history node ID or checkpoint name to label. Defaults to the current conversation position." })),
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
export default function (pi: ExtensionAPI) {
|
|
@@ -81,31 +82,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
|
|
84
|
-
// Helper: Check if a
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// Helper: Check if a checkpoint name already exists in the tree
|
|
86
|
+
// Iterative DFS to avoid call stack overflows on deep histories.
|
|
87
|
+
// Push children in reverse order to preserve left-to-right pre-order semantics.
|
|
88
|
+
const findCheckpointInTree = (sm: SessionManager, nodes: SessionTreeNode[], checkpointName: string): string | null => {
|
|
89
|
+
const stack: SessionTreeNode[] = [...nodes].reverse();
|
|
90
|
+
while (stack.length > 0) {
|
|
91
|
+
const n = stack.pop()!;
|
|
92
|
+
if (sm.getLabel(n.entry.id) === checkpointName) return n.entry.id;
|
|
93
|
+
if (n.children?.length) {
|
|
94
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
95
|
+
stack.push(n.children[i]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
90
98
|
}
|
|
91
99
|
return null;
|
|
92
100
|
};
|
|
93
101
|
|
|
94
102
|
pi.registerTool({
|
|
95
|
-
name: "
|
|
96
|
-
label: "Context
|
|
97
|
-
description: "
|
|
98
|
-
parameters:
|
|
99
|
-
async execute(_id, params: Static<typeof
|
|
103
|
+
name: "context_checkpoint",
|
|
104
|
+
label: "Context Checkpoint",
|
|
105
|
+
description: "Create a named checkpoint in the conversation history. Use this before risky work, before a long research pass, or whenever you reach a stable milestone.",
|
|
106
|
+
parameters: ContextCheckpointParams,
|
|
107
|
+
async execute(_id, params: Static<typeof ContextCheckpointParams>, _signal, _onUpdate, ctx) {
|
|
100
108
|
const sm = ctx.sessionManager as SessionManager;
|
|
101
109
|
|
|
102
|
-
// Deduplication check: ensure
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
110
|
+
// Deduplication check: ensure checkpoint name is unique
|
|
111
|
+
const existingCheckpointId = findCheckpointInTree(sm, sm.getTree(), params.name);
|
|
112
|
+
if (existingCheckpointId) {
|
|
105
113
|
return {
|
|
106
114
|
content: [{
|
|
107
115
|
type: "text",
|
|
108
|
-
text: `Error:
|
|
116
|
+
text: `Error: Checkpoint '${params.name}' already exists at ${existingCheckpointId}. Checkpoint names must be unique. Use a different name or remove the existing one first.`
|
|
109
117
|
}],
|
|
110
118
|
details: {}
|
|
111
119
|
};
|
|
@@ -114,8 +122,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
114
122
|
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
115
123
|
|
|
116
124
|
if (!id) {
|
|
117
|
-
// Auto-resolve: Find the last
|
|
118
|
-
// We skip ToolResults
|
|
125
|
+
// Auto-resolve: Find the last interesting node to checkpoint.
|
|
126
|
+
// We skip ToolResults that look awkward when checkpointed and internal-only assistant messages that look empty.
|
|
119
127
|
const branch = sm.getBranch();
|
|
120
128
|
for (let i = branch.length - 1; i >= 0; i--) {
|
|
121
129
|
const entry = branch[i];
|
|
@@ -149,16 +157,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
pi.setLabel(id, params.name);
|
|
152
|
-
return { content: [{ type: "text", text: `Created
|
|
160
|
+
return { content: [{ type: "text", text: `Created checkpoint '${params.name}' at ${id}` }], details: {} };
|
|
153
161
|
},
|
|
154
162
|
});
|
|
155
163
|
|
|
156
164
|
pi.registerTool({
|
|
157
|
-
name: "
|
|
158
|
-
label: "Context
|
|
159
|
-
description: "Show the
|
|
160
|
-
parameters:
|
|
161
|
-
async execute(_id, params: Static<typeof
|
|
165
|
+
name: "context_timeline",
|
|
166
|
+
label: "Context Timeline",
|
|
167
|
+
description: "Show the conversation timeline, checkpoints, summaries, and current position. Use this to understand session structure, monitor drift, and choose a checkpoint to return to.",
|
|
168
|
+
parameters: ContextTimelineParams,
|
|
169
|
+
async execute(_id, params: Static<typeof ContextTimelineParams>, _signal, _onUpdate, ctx) {
|
|
162
170
|
const sm = ctx.sessionManager as SessionManager;
|
|
163
171
|
const branch = sm.getBranch();
|
|
164
172
|
const currentLeafId = sm.getLeafId();
|
|
@@ -186,7 +194,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
194
|
return e.summary || "[No summary provided]";
|
|
187
195
|
}
|
|
188
196
|
if (entry.type === "label") {
|
|
189
|
-
return `
|
|
197
|
+
return `checkpoint: ${entry.label}`;
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
if (entry.type === "message") {
|
|
@@ -250,7 +258,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
250
258
|
if (entry.id === currentLeafId) return true;
|
|
251
259
|
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
252
260
|
|
|
253
|
-
// 2. Explicit
|
|
261
|
+
// 2. Explicit checkpoints (labels) - only show the checkpointed node, not the label node itself
|
|
254
262
|
if (sm.getLabel(entry.id)) return true;
|
|
255
263
|
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
256
264
|
|
|
@@ -320,7 +328,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
320
328
|
|
|
321
329
|
const id = entry.id;
|
|
322
330
|
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
323
|
-
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `
|
|
331
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `checkpoint: ${label}` : null].filter(Boolean).join(", ");
|
|
324
332
|
|
|
325
333
|
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
326
334
|
|
|
@@ -336,27 +344,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
336
344
|
// --- Context Dashboard (HUD) ---
|
|
337
345
|
const usage = await ctx.getContextUsage();
|
|
338
346
|
let usageStr = "Unknown";
|
|
339
|
-
if (usage) {
|
|
347
|
+
if (usage?.percent != null && usage.tokens != null && usage.contextWindow != null) {
|
|
340
348
|
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
341
349
|
}
|
|
342
350
|
|
|
343
|
-
// Find the distance to the nearest
|
|
344
|
-
let
|
|
345
|
-
let
|
|
351
|
+
// Find the distance to the nearest checkpoint
|
|
352
|
+
let stepsSinceCheckpoint = 0;
|
|
353
|
+
let nearestCheckpointName = "None";
|
|
346
354
|
for (let i = branch.length - 1; i >= 0; i--) {
|
|
347
355
|
const id = branch[i].id;
|
|
348
356
|
const label = sm.getLabel(id);
|
|
349
357
|
if (label) {
|
|
350
|
-
|
|
358
|
+
nearestCheckpointName = label;
|
|
351
359
|
break;
|
|
352
360
|
}
|
|
353
|
-
|
|
361
|
+
stepsSinceCheckpoint++;
|
|
354
362
|
}
|
|
355
363
|
|
|
356
364
|
const hud = [
|
|
357
365
|
`[Context Dashboard]`,
|
|
358
366
|
`• Context Usage: ${usageStr}`,
|
|
359
|
-
`• Segment Size: ${
|
|
367
|
+
`• Segment Size: ${stepsSinceCheckpoint} steps since last checkpoint '${nearestCheckpointName}'`,
|
|
360
368
|
`---------------------------------------------------`
|
|
361
369
|
].join("\n");
|
|
362
370
|
|
|
@@ -365,11 +373,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
365
373
|
});
|
|
366
374
|
|
|
367
375
|
pi.registerTool({
|
|
368
|
-
name: "
|
|
369
|
-
label: "
|
|
370
|
-
description: "
|
|
371
|
-
parameters:
|
|
372
|
-
async execute(_id, params: Static<typeof
|
|
376
|
+
name: "context_rewind",
|
|
377
|
+
label: "Conversation Rewind",
|
|
378
|
+
description: "Return to an earlier conversation checkpoint and start a fresh continuation with a carryover summary. This changes session history only and does not modify disk files. Always provide a detailed message.",
|
|
379
|
+
parameters: ContextRewindParams,
|
|
380
|
+
async execute(_id, params: Static<typeof ContextRewindParams>, _signal, _onUpdate, ctx) {
|
|
373
381
|
if (!CommandCtx) {
|
|
374
382
|
ctx.ui.setEditorText(`/acm ${ctx.ui.getEditorText() || "continue"}`)
|
|
375
383
|
return {
|
|
@@ -388,49 +396,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
396
|
if (currentLeaf === tid) {
|
|
389
397
|
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
390
398
|
}
|
|
391
|
-
if (params.
|
|
392
|
-
pi.setLabel(currentLeaf, params.
|
|
399
|
+
if (params.backupCheckpoint && currentLeaf) {
|
|
400
|
+
pi.setLabel(currentLeaf, params.backupCheckpoint);
|
|
393
401
|
}
|
|
394
402
|
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
395
|
-
const origin = currentLabel ? `
|
|
403
|
+
const origin = currentLabel ? `checkpoint: ${currentLabel}` : (currentLeaf || "unknown");
|
|
396
404
|
|
|
397
405
|
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
398
406
|
|
|
399
407
|
const nid = await sm.branchWithSummary(tid, enrichedMessage);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
408
|
+
RewindParams = params;
|
|
409
|
+
RewindParams.nid = nid;
|
|
410
|
+
RewindParams.tid = tid;
|
|
411
|
+
RewindParams.enrichedMessage = enrichedMessage;
|
|
404
412
|
|
|
405
|
-
return { content: [{ type: "text", text: "
|
|
413
|
+
return { content: [{ type: "text", text: "rewind start" }], details: {} };
|
|
406
414
|
},
|
|
407
415
|
});
|
|
408
416
|
|
|
409
|
-
pi.on("turn_end", async (
|
|
410
|
-
if (!
|
|
417
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
418
|
+
if (!RewindParams) {
|
|
411
419
|
return
|
|
412
420
|
}
|
|
413
421
|
ctx.abort()
|
|
414
422
|
});
|
|
415
423
|
|
|
416
424
|
pi.on("agent_end", async (_event, ctx) => {
|
|
417
|
-
if (!
|
|
425
|
+
if (!RewindParams) {
|
|
418
426
|
return
|
|
419
427
|
}
|
|
420
428
|
if (!CommandCtx) {
|
|
421
429
|
return
|
|
422
430
|
}
|
|
423
431
|
|
|
424
|
-
await CommandCtx.navigateTree(
|
|
432
|
+
await CommandCtx.navigateTree(RewindParams.nid, {
|
|
425
433
|
summarize: false,
|
|
426
434
|
});
|
|
427
435
|
|
|
428
|
-
ctx.ui.notify(`
|
|
429
|
-
|
|
436
|
+
ctx.ui.notify(`Rewound to ${RewindParams.target}${RewindParams.target === RewindParams.tid ? "" : `(${RewindParams.tid})`}\nBackup checkpoint created: ${RewindParams.backupCheckpoint || "none"}\nmessage: ${RewindParams.enrichedMessage}`, "info");
|
|
437
|
+
RewindParams = null;
|
|
430
438
|
|
|
431
439
|
pi.sendMessage({
|
|
432
440
|
customType: "pi-context",
|
|
433
|
-
content: "
|
|
441
|
+
content: "context_rewind complete. A summary of your previous conversation path was injected above. Read it to understand your new state. Execute the 'Next Step' from the summary",
|
|
434
442
|
display: false,
|
|
435
443
|
}, {
|
|
436
444
|
triggerTurn: true,
|