pi-context 1.1.4 → 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 +70 -69
- 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,14 +82,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
|
|
84
|
-
// Helper: Check if a
|
|
85
|
+
// Helper: Check if a checkpoint name already exists in the tree
|
|
85
86
|
// Iterative DFS to avoid call stack overflows on deep histories.
|
|
86
87
|
// Push children in reverse order to preserve left-to-right pre-order semantics.
|
|
87
|
-
const
|
|
88
|
+
const findCheckpointInTree = (sm: SessionManager, nodes: SessionTreeNode[], checkpointName: string): string | null => {
|
|
88
89
|
const stack: SessionTreeNode[] = [...nodes].reverse();
|
|
89
90
|
while (stack.length > 0) {
|
|
90
91
|
const n = stack.pop()!;
|
|
91
|
-
if (sm.getLabel(n.entry.id) ===
|
|
92
|
+
if (sm.getLabel(n.entry.id) === checkpointName) return n.entry.id;
|
|
92
93
|
if (n.children?.length) {
|
|
93
94
|
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
94
95
|
stack.push(n.children[i]);
|
|
@@ -99,20 +100,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
100
|
};
|
|
100
101
|
|
|
101
102
|
pi.registerTool({
|
|
102
|
-
name: "
|
|
103
|
-
label: "Context
|
|
104
|
-
description: "
|
|
105
|
-
parameters:
|
|
106
|
-
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) {
|
|
107
108
|
const sm = ctx.sessionManager as SessionManager;
|
|
108
109
|
|
|
109
|
-
// Deduplication check: ensure
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
110
|
+
// Deduplication check: ensure checkpoint name is unique
|
|
111
|
+
const existingCheckpointId = findCheckpointInTree(sm, sm.getTree(), params.name);
|
|
112
|
+
if (existingCheckpointId) {
|
|
112
113
|
return {
|
|
113
114
|
content: [{
|
|
114
115
|
type: "text",
|
|
115
|
-
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.`
|
|
116
117
|
}],
|
|
117
118
|
details: {}
|
|
118
119
|
};
|
|
@@ -121,8 +122,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
121
122
|
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
122
123
|
|
|
123
124
|
if (!id) {
|
|
124
|
-
// Auto-resolve: Find the last
|
|
125
|
-
// 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.
|
|
126
127
|
const branch = sm.getBranch();
|
|
127
128
|
for (let i = branch.length - 1; i >= 0; i--) {
|
|
128
129
|
const entry = branch[i];
|
|
@@ -156,16 +157,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
pi.setLabel(id, params.name);
|
|
159
|
-
return { content: [{ type: "text", text: `Created
|
|
160
|
+
return { content: [{ type: "text", text: `Created checkpoint '${params.name}' at ${id}` }], details: {} };
|
|
160
161
|
},
|
|
161
162
|
});
|
|
162
163
|
|
|
163
164
|
pi.registerTool({
|
|
164
|
-
name: "
|
|
165
|
-
label: "Context
|
|
166
|
-
description: "Show the
|
|
167
|
-
parameters:
|
|
168
|
-
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) {
|
|
169
170
|
const sm = ctx.sessionManager as SessionManager;
|
|
170
171
|
const branch = sm.getBranch();
|
|
171
172
|
const currentLeafId = sm.getLeafId();
|
|
@@ -193,7 +194,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
194
|
return e.summary || "[No summary provided]";
|
|
194
195
|
}
|
|
195
196
|
if (entry.type === "label") {
|
|
196
|
-
return `
|
|
197
|
+
return `checkpoint: ${entry.label}`;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
if (entry.type === "message") {
|
|
@@ -257,7 +258,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
257
258
|
if (entry.id === currentLeafId) return true;
|
|
258
259
|
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
259
260
|
|
|
260
|
-
// 2. Explicit
|
|
261
|
+
// 2. Explicit checkpoints (labels) - only show the checkpointed node, not the label node itself
|
|
261
262
|
if (sm.getLabel(entry.id)) return true;
|
|
262
263
|
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
263
264
|
|
|
@@ -327,7 +328,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
327
328
|
|
|
328
329
|
const id = entry.id;
|
|
329
330
|
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
330
|
-
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(", ");
|
|
331
332
|
|
|
332
333
|
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
333
334
|
|
|
@@ -343,27 +344,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
343
344
|
// --- Context Dashboard (HUD) ---
|
|
344
345
|
const usage = await ctx.getContextUsage();
|
|
345
346
|
let usageStr = "Unknown";
|
|
346
|
-
if (usage) {
|
|
347
|
+
if (usage?.percent != null && usage.tokens != null && usage.contextWindow != null) {
|
|
347
348
|
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
// Find the distance to the nearest
|
|
351
|
-
let
|
|
352
|
-
let
|
|
351
|
+
// Find the distance to the nearest checkpoint
|
|
352
|
+
let stepsSinceCheckpoint = 0;
|
|
353
|
+
let nearestCheckpointName = "None";
|
|
353
354
|
for (let i = branch.length - 1; i >= 0; i--) {
|
|
354
355
|
const id = branch[i].id;
|
|
355
356
|
const label = sm.getLabel(id);
|
|
356
357
|
if (label) {
|
|
357
|
-
|
|
358
|
+
nearestCheckpointName = label;
|
|
358
359
|
break;
|
|
359
360
|
}
|
|
360
|
-
|
|
361
|
+
stepsSinceCheckpoint++;
|
|
361
362
|
}
|
|
362
363
|
|
|
363
364
|
const hud = [
|
|
364
365
|
`[Context Dashboard]`,
|
|
365
366
|
`• Context Usage: ${usageStr}`,
|
|
366
|
-
`• Segment Size: ${
|
|
367
|
+
`• Segment Size: ${stepsSinceCheckpoint} steps since last checkpoint '${nearestCheckpointName}'`,
|
|
367
368
|
`---------------------------------------------------`
|
|
368
369
|
].join("\n");
|
|
369
370
|
|
|
@@ -372,11 +373,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
372
373
|
});
|
|
373
374
|
|
|
374
375
|
pi.registerTool({
|
|
375
|
-
name: "
|
|
376
|
-
label: "
|
|
377
|
-
description: "
|
|
378
|
-
parameters:
|
|
379
|
-
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) {
|
|
380
381
|
if (!CommandCtx) {
|
|
381
382
|
ctx.ui.setEditorText(`/acm ${ctx.ui.getEditorText() || "continue"}`)
|
|
382
383
|
return {
|
|
@@ -395,49 +396,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
395
396
|
if (currentLeaf === tid) {
|
|
396
397
|
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
397
398
|
}
|
|
398
|
-
if (params.
|
|
399
|
-
pi.setLabel(currentLeaf, params.
|
|
399
|
+
if (params.backupCheckpoint && currentLeaf) {
|
|
400
|
+
pi.setLabel(currentLeaf, params.backupCheckpoint);
|
|
400
401
|
}
|
|
401
402
|
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
402
|
-
const origin = currentLabel ? `
|
|
403
|
+
const origin = currentLabel ? `checkpoint: ${currentLabel}` : (currentLeaf || "unknown");
|
|
403
404
|
|
|
404
405
|
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
405
406
|
|
|
406
407
|
const nid = await sm.branchWithSummary(tid, enrichedMessage);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
408
|
+
RewindParams = params;
|
|
409
|
+
RewindParams.nid = nid;
|
|
410
|
+
RewindParams.tid = tid;
|
|
411
|
+
RewindParams.enrichedMessage = enrichedMessage;
|
|
411
412
|
|
|
412
|
-
return { content: [{ type: "text", text: "
|
|
413
|
+
return { content: [{ type: "text", text: "rewind start" }], details: {} };
|
|
413
414
|
},
|
|
414
415
|
});
|
|
415
416
|
|
|
416
|
-
pi.on("turn_end", async (
|
|
417
|
-
if (!
|
|
417
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
418
|
+
if (!RewindParams) {
|
|
418
419
|
return
|
|
419
420
|
}
|
|
420
421
|
ctx.abort()
|
|
421
422
|
});
|
|
422
423
|
|
|
423
424
|
pi.on("agent_end", async (_event, ctx) => {
|
|
424
|
-
if (!
|
|
425
|
+
if (!RewindParams) {
|
|
425
426
|
return
|
|
426
427
|
}
|
|
427
428
|
if (!CommandCtx) {
|
|
428
429
|
return
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
await CommandCtx.navigateTree(
|
|
432
|
+
await CommandCtx.navigateTree(RewindParams.nid, {
|
|
432
433
|
summarize: false,
|
|
433
434
|
});
|
|
434
435
|
|
|
435
|
-
ctx.ui.notify(`
|
|
436
|
-
|
|
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;
|
|
437
438
|
|
|
438
439
|
pi.sendMessage({
|
|
439
440
|
customType: "pi-context",
|
|
440
|
-
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",
|
|
441
442
|
display: false,
|
|
442
443
|
}, {
|
|
443
444
|
triggerTurn: true,
|