pi-context 1.1.4 → 2.0.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 +180 -223
- package/skills/context-management/references/development-and-troubleshooting.md +75 -0
- package/skills/context-management/references/planning-and-execution.md +81 -0
- package/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
- package/skills/context-management/references/retry-branch-and-pivot.md +71 -0
- package/skills/context-management/references/search-research-and-reading.md +103 -0
- package/skills/context-management/references/task-switching-and-cleanup.md +72 -0
- package/src/context.ts +5 -4
- package/src/index.ts +131 -93
- package/dist/index.js +0 -370
package/src/index.ts
CHANGED
|
@@ -2,14 +2,16 @@ 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
|
+
type ContextUsage,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
Type,
|
|
10
|
+
type Static,
|
|
11
|
+
type TextContent,
|
|
12
|
+
type ImageContent,
|
|
13
|
+
type ToolCall,
|
|
14
|
+
} from "@earendil-works/pi-ai";
|
|
13
15
|
import { formatTokens } from "./utils.js";
|
|
14
16
|
|
|
15
17
|
// Define missing types locally as they are not exported from the main entry point
|
|
@@ -19,12 +21,21 @@ interface SessionTreeNode {
|
|
|
19
21
|
label?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const InternalTools = ["
|
|
24
|
+
const InternalTools = ["context_checkpoint", "context_timeline", "context_compact"];
|
|
23
25
|
let CommandCtx: ExtensionCommandContext | null = null;
|
|
24
|
-
let
|
|
26
|
+
let CompactParams: any = null;
|
|
25
27
|
|
|
26
28
|
const isInternal = (name: string) => InternalTools.includes(name);
|
|
27
29
|
|
|
30
|
+
const formatContextUsage = (usage: ContextUsage | undefined, includeTokens = false): string => {
|
|
31
|
+
if (usage?.percent == null) return "Unknown";
|
|
32
|
+
|
|
33
|
+
const percent = `${usage.percent.toFixed(1)}%`;
|
|
34
|
+
if (!includeTokens || usage.tokens == null) return percent;
|
|
35
|
+
|
|
36
|
+
return `${percent} (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
37
|
+
};
|
|
38
|
+
|
|
28
39
|
const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
29
40
|
if (target.toLowerCase() === "root") {
|
|
30
41
|
const tree = sm.getTree();
|
|
@@ -46,20 +57,23 @@ const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
|
46
57
|
return target;
|
|
47
58
|
};
|
|
48
59
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
const ContextTimelineDescription = "Inspect the active conversation path as a structural map: checkpoints, summaries/compactions, branch points, user turns, and current position. Use when orientation or compact target selection depends on the shape of history.";
|
|
61
|
+
const ContextTimelineParams = Type.Object({
|
|
62
|
+
limit: Type.Optional(Type.Number({ description: "Maximum visible timeline entries (default: 50)." })),
|
|
63
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show all messages including internal context-tool traffic. If false (default), collapse to structural milestones." })),
|
|
52
64
|
});
|
|
53
65
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
const ContextCompactDescription = "Create a summarized continuation branch from an earlier checkpoint or history node. The selected target is the branch point; the summary must restore the useful state from the compacted path after that target. This changes conversation history only; it does not modify or roll back disk files or external systems.";
|
|
67
|
+
const ContextCompactParams = Type.Object({
|
|
68
|
+
target: Type.String({ description: "Checkpoint name, history node ID, or root to use as the branch point for the summarized continuation." }),
|
|
69
|
+
summary: Type.String({ description: "Handoff summary injected into the new continuation branch. Restore current task/state, decisions/constraints, important external side effects (changed files, processes, browser/tickets/remote state), validation status, source anchors/evidence/open questions likely needed soon, and explicit next step. Do not rely on backupCheckpoint for details needed in the next phase." }),
|
|
70
|
+
backupCheckpoint: Type.Optional(Type.String({ description: "Optional checkpoint name to label the current conversation state before branching. This is only a recovery pointer; the summary must still contain the state needed to continue." })),
|
|
58
71
|
});
|
|
59
72
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const ContextCheckpointDescription = "Create a named anchor by labeling a conversation history node. This does not branch, summarize, or affect external state; it only makes the point easy to find later in timeline or compact target selection.";
|
|
74
|
+
const ContextCheckpointParams = Type.Object({
|
|
75
|
+
name: Type.String({ description: "Unique semantic anchor name that encodes the task and phase/purpose, e.g. parser-fix-start or timeout-investigation-search. Avoid generic names like start, checkpoint-1, or retry." }),
|
|
76
|
+
target: Type.Optional(Type.String({ description: "Optional history node ID or checkpoint name to label. Defaults to the current meaningful position near the conversation head." })),
|
|
63
77
|
});
|
|
64
78
|
|
|
65
79
|
export default function (pi: ExtensionAPI) {
|
|
@@ -68,27 +82,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
68
82
|
handler: async (args, ctx) => {
|
|
69
83
|
CommandCtx = ctx;
|
|
70
84
|
ctx.ui.notify("Agentic Context Management enabled.", "info");
|
|
71
|
-
pi.sendMessage({
|
|
72
|
-
customType: "pi-context",
|
|
73
|
-
content: "use context-management skill",
|
|
74
|
-
display: false,
|
|
75
|
-
}, {
|
|
76
|
-
deliverAs: "followUp"
|
|
77
|
-
});
|
|
78
85
|
if (args) {
|
|
79
|
-
pi.sendUserMessage(args)
|
|
86
|
+
pi.sendUserMessage(args, { deliverAs: "followUp" });
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
});
|
|
83
90
|
|
|
84
|
-
// Helper: Check if a
|
|
91
|
+
// Helper: Check if a checkpoint name already exists in the tree
|
|
85
92
|
// Iterative DFS to avoid call stack overflows on deep histories.
|
|
86
93
|
// Push children in reverse order to preserve left-to-right pre-order semantics.
|
|
87
|
-
const
|
|
94
|
+
const findCheckpointInTree = (sm: SessionManager, nodes: SessionTreeNode[], checkpointName: string): string | null => {
|
|
88
95
|
const stack: SessionTreeNode[] = [...nodes].reverse();
|
|
89
96
|
while (stack.length > 0) {
|
|
90
97
|
const n = stack.pop()!;
|
|
91
|
-
if (sm.getLabel(n.entry.id) ===
|
|
98
|
+
if (sm.getLabel(n.entry.id) === checkpointName) return n.entry.id;
|
|
92
99
|
if (n.children?.length) {
|
|
93
100
|
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
94
101
|
stack.push(n.children[i]);
|
|
@@ -99,20 +106,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
106
|
};
|
|
100
107
|
|
|
101
108
|
pi.registerTool({
|
|
102
|
-
name: "
|
|
103
|
-
label: "Context
|
|
104
|
-
description:
|
|
105
|
-
parameters:
|
|
106
|
-
async execute(_id, params: Static<typeof
|
|
109
|
+
name: "context_checkpoint",
|
|
110
|
+
label: "Context Checkpoint",
|
|
111
|
+
description: ContextCheckpointDescription,
|
|
112
|
+
parameters: ContextCheckpointParams,
|
|
113
|
+
async execute(_id, params: Static<typeof ContextCheckpointParams>, _signal, _onUpdate, ctx) {
|
|
107
114
|
const sm = ctx.sessionManager as SessionManager;
|
|
108
115
|
|
|
109
|
-
// Deduplication check: ensure
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
116
|
+
// Deduplication check: ensure checkpoint name is unique
|
|
117
|
+
const existingCheckpointId = findCheckpointInTree(sm, sm.getTree(), params.name);
|
|
118
|
+
if (existingCheckpointId) {
|
|
112
119
|
return {
|
|
113
120
|
content: [{
|
|
114
121
|
type: "text",
|
|
115
|
-
text: `Error:
|
|
122
|
+
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
123
|
}],
|
|
117
124
|
details: {}
|
|
118
125
|
};
|
|
@@ -121,8 +128,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
121
128
|
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
122
129
|
|
|
123
130
|
if (!id) {
|
|
124
|
-
// Auto-resolve: Find the last
|
|
125
|
-
// We skip ToolResults
|
|
131
|
+
// Auto-resolve: Find the last interesting node to checkpoint.
|
|
132
|
+
// We skip ToolResults that look awkward when checkpointed and internal-only assistant messages that look empty.
|
|
126
133
|
const branch = sm.getBranch();
|
|
127
134
|
for (let i = branch.length - 1; i >= 0; i--) {
|
|
128
135
|
const entry = branch[i];
|
|
@@ -156,16 +163,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
pi.setLabel(id, params.name);
|
|
159
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `Created checkpoint '${params.name}' at ${id}.`
|
|
170
|
+
}],
|
|
171
|
+
details: {}
|
|
172
|
+
};
|
|
160
173
|
},
|
|
161
174
|
});
|
|
162
175
|
|
|
163
176
|
pi.registerTool({
|
|
164
|
-
name: "
|
|
165
|
-
label: "Context
|
|
166
|
-
description:
|
|
167
|
-
parameters:
|
|
168
|
-
async execute(_id, params: Static<typeof
|
|
177
|
+
name: "context_timeline",
|
|
178
|
+
label: "Context Timeline",
|
|
179
|
+
description: ContextTimelineDescription,
|
|
180
|
+
parameters: ContextTimelineParams,
|
|
181
|
+
async execute(_id, params: Static<typeof ContextTimelineParams>, _signal, _onUpdate, ctx) {
|
|
169
182
|
const sm = ctx.sessionManager as SessionManager;
|
|
170
183
|
const branch = sm.getBranch();
|
|
171
184
|
const currentLeafId = sm.getLeafId();
|
|
@@ -193,7 +206,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
206
|
return e.summary || "[No summary provided]";
|
|
194
207
|
}
|
|
195
208
|
if (entry.type === "label") {
|
|
196
|
-
return `
|
|
209
|
+
return `checkpoint: ${entry.label}`;
|
|
197
210
|
}
|
|
198
211
|
|
|
199
212
|
if (entry.type === "message") {
|
|
@@ -257,7 +270,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
257
270
|
if (entry.id === currentLeafId) return true;
|
|
258
271
|
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
259
272
|
|
|
260
|
-
// 2. Explicit
|
|
273
|
+
// 2. Explicit checkpoints (labels) - only show the checkpointed node, not the label node itself
|
|
261
274
|
if (sm.getLabel(entry.id)) return true;
|
|
262
275
|
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
263
276
|
|
|
@@ -327,7 +340,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
327
340
|
|
|
328
341
|
const id = entry.id;
|
|
329
342
|
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
330
|
-
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `
|
|
343
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `checkpoint: ${label}` : null].filter(Boolean).join(", ");
|
|
331
344
|
|
|
332
345
|
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
333
346
|
|
|
@@ -341,29 +354,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
// --- Context Dashboard (HUD) ---
|
|
344
|
-
const
|
|
345
|
-
let usageStr = "Unknown";
|
|
346
|
-
if (usage) {
|
|
347
|
-
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
348
|
-
}
|
|
357
|
+
const usageStr = formatContextUsage(ctx.getContextUsage(), true);
|
|
349
358
|
|
|
350
|
-
// Find the distance to the nearest
|
|
351
|
-
let
|
|
352
|
-
let
|
|
359
|
+
// Find the distance to the nearest checkpoint
|
|
360
|
+
let stepsSinceCheckpoint = 0;
|
|
361
|
+
let nearestCheckpointName = "None";
|
|
353
362
|
for (let i = branch.length - 1; i >= 0; i--) {
|
|
354
363
|
const id = branch[i].id;
|
|
355
364
|
const label = sm.getLabel(id);
|
|
356
365
|
if (label) {
|
|
357
|
-
|
|
366
|
+
nearestCheckpointName = label;
|
|
358
367
|
break;
|
|
359
368
|
}
|
|
360
|
-
|
|
369
|
+
stepsSinceCheckpoint++;
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
const compactCue = nearestCheckpointName === "None"
|
|
373
|
+
? "create a checkpoint before the next noisy phase"
|
|
374
|
+
: `if this segment has produced a stable result and another phase remains, compact to '${nearestCheckpointName}' with a handoff summary before continuing`;
|
|
375
|
+
|
|
363
376
|
const hud = [
|
|
364
377
|
`[Context Dashboard]`,
|
|
365
378
|
`• Context Usage: ${usageStr}`,
|
|
366
|
-
`• Segment Size: ${
|
|
379
|
+
`• Segment Size: ${stepsSinceCheckpoint} steps since last checkpoint '${nearestCheckpointName}'`,
|
|
380
|
+
`• Compact Cue: ${compactCue}`,
|
|
367
381
|
`---------------------------------------------------`
|
|
368
382
|
].join("\n");
|
|
369
383
|
|
|
@@ -372,11 +386,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
372
386
|
});
|
|
373
387
|
|
|
374
388
|
pi.registerTool({
|
|
375
|
-
name: "
|
|
376
|
-
label: "Context
|
|
377
|
-
description:
|
|
378
|
-
parameters:
|
|
379
|
-
async execute(_id, params: Static<typeof
|
|
389
|
+
name: "context_compact",
|
|
390
|
+
label: "Context Compact",
|
|
391
|
+
description: ContextCompactDescription,
|
|
392
|
+
parameters: ContextCompactParams,
|
|
393
|
+
async execute(_id, params: Static<typeof ContextCompactParams>, _signal, _onUpdate, ctx) {
|
|
380
394
|
if (!CommandCtx) {
|
|
381
395
|
ctx.ui.setEditorText(`/acm ${ctx.ui.getEditorText() || "continue"}`)
|
|
382
396
|
return {
|
|
@@ -388,6 +402,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
402
|
};
|
|
389
403
|
}
|
|
390
404
|
const sm = ctx.sessionManager as SessionManager;
|
|
405
|
+
const usageBeforeText = formatContextUsage(ctx.getContextUsage());
|
|
391
406
|
|
|
392
407
|
const tid = resolveTargetId(sm, params.target);
|
|
393
408
|
|
|
@@ -395,52 +410,75 @@ export default function (pi: ExtensionAPI) {
|
|
|
395
410
|
if (currentLeaf === tid) {
|
|
396
411
|
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
397
412
|
}
|
|
398
|
-
if (params.
|
|
399
|
-
pi.setLabel(currentLeaf, params.
|
|
413
|
+
if (params.backupCheckpoint && currentLeaf) {
|
|
414
|
+
pi.setLabel(currentLeaf, params.backupCheckpoint);
|
|
400
415
|
}
|
|
401
416
|
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
402
|
-
const origin = currentLabel ? `
|
|
417
|
+
const origin = currentLabel ? `checkpoint: ${currentLabel}` : (currentLeaf || "unknown");
|
|
403
418
|
|
|
404
|
-
const enrichedMessage = `(summary from ${origin})\n${params.
|
|
419
|
+
const enrichedMessage = `(handoff summary from ${origin})\n${params.summary}`;
|
|
405
420
|
|
|
406
421
|
const nid = await sm.branchWithSummary(tid, enrichedMessage);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
422
|
+
CompactParams = params;
|
|
423
|
+
CompactParams.nid = nid;
|
|
424
|
+
CompactParams.tid = tid;
|
|
425
|
+
CompactParams.enrichedMessage = enrichedMessage;
|
|
426
|
+
CompactParams.usageBeforeText = usageBeforeText;
|
|
411
427
|
|
|
412
|
-
return { content: [{ type: "text", text: "
|
|
428
|
+
return { content: [{ type: "text", text: "compact start" }], details: {} };
|
|
413
429
|
},
|
|
414
430
|
});
|
|
415
431
|
|
|
416
|
-
pi.on("turn_end", async (
|
|
417
|
-
if (!
|
|
432
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
433
|
+
if (!CompactParams) {
|
|
418
434
|
return
|
|
419
435
|
}
|
|
420
436
|
ctx.abort()
|
|
421
437
|
});
|
|
422
438
|
|
|
423
|
-
pi.on("agent_end", async (
|
|
424
|
-
if (!
|
|
439
|
+
pi.on("agent_end", async () => {
|
|
440
|
+
if (!CompactParams) {
|
|
425
441
|
return
|
|
426
442
|
}
|
|
427
443
|
if (!CommandCtx) {
|
|
428
444
|
return
|
|
429
445
|
}
|
|
430
446
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
447
|
+
const compactParams = CompactParams;
|
|
448
|
+
const commandCtx = CommandCtx;
|
|
449
|
+
CompactParams = null;
|
|
450
|
+
|
|
451
|
+
// `agent_end` is emitted before the core Agent is actually idle. If we
|
|
452
|
+
// call pi.sendMessage({ triggerTurn: true }) inside this handler, pi still
|
|
453
|
+
// sees an active stream and queues the message as steering; after
|
|
454
|
+
// `agent_end` the loop has already stopped, so that queued message is not
|
|
455
|
+
// drained. Defer navigation + continuation until the current run settles.
|
|
456
|
+
setTimeout(async () => {
|
|
457
|
+
try {
|
|
458
|
+
await commandCtx.waitForIdle();
|
|
459
|
+
await commandCtx.navigateTree(compactParams.nid, {
|
|
460
|
+
summarize: false,
|
|
461
|
+
});
|
|
437
462
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
463
|
+
const usageAfter = commandCtx.getContextUsage();
|
|
464
|
+
commandCtx.ui.notify([
|
|
465
|
+
`Compacted to ${compactParams.target}${compactParams.target === compactParams.tid ? "" : `(${compactParams.tid})`}`,
|
|
466
|
+
`Context Usage: ${compactParams.usageBeforeText} -> ${formatContextUsage(usageAfter)}`,
|
|
467
|
+
`Backup checkpoint created: ${compactParams.backupCheckpoint || "none"}`,
|
|
468
|
+
`Summary: ${compactParams.enrichedMessage}`,
|
|
469
|
+
].join("\n"), "info");
|
|
470
|
+
|
|
471
|
+
pi.sendMessage({
|
|
472
|
+
customType: "pi-context",
|
|
473
|
+
content: "context_compact complete. A handoff summary of your previous conversation path was injected above. Read it to understand your new state. Execute the Next Step from the summary",
|
|
474
|
+
display: false,
|
|
475
|
+
}, {
|
|
476
|
+
triggerTurn: true,
|
|
477
|
+
deliverAs: "followUp",
|
|
478
|
+
});
|
|
479
|
+
} catch (err) {
|
|
480
|
+
commandCtx.ui.notify(`context_compact failed to continue: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
481
|
+
}
|
|
482
|
+
}, 0);
|
|
445
483
|
});
|
|
446
484
|
}
|