pi-context 2.0.0-beta.0 → 2.1.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 +10 -10
- package/package.json +7 -6
- package/skills/context-management/SKILL.md +169 -238
- package/skills/context-management/references/development-and-troubleshooting.md +8 -8
- package/skills/context-management/references/interleaved-async-work.md +143 -0
- package/skills/context-management/references/planning-and-execution.md +11 -9
- package/skills/context-management/references/repeated-items-and-batch-work.md +11 -10
- package/skills/context-management/references/retry-branch-and-pivot.md +20 -11
- package/skills/context-management/references/search-research-and-reading.md +32 -11
- package/skills/context-management/references/task-switching-and-cleanup.md +22 -12
- package/src/index.ts +102 -54
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type SessionManager,
|
|
4
4
|
type SessionEntry,
|
|
5
5
|
type ExtensionCommandContext,
|
|
6
|
+
type ContextUsage,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import {
|
|
8
9
|
Type,
|
|
@@ -20,12 +21,23 @@ interface SessionTreeNode {
|
|
|
20
21
|
label?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const InternalTools = ["context_checkpoint", "context_timeline", "
|
|
24
|
+
const InternalTools = ["context_checkpoint", "context_timeline", "context_compact"];
|
|
25
|
+
const PiContextCustomMessageType = "pi-context";
|
|
26
|
+
const AcmEnableFollowUp = "Agentic context management is now enabled";
|
|
24
27
|
let CommandCtx: ExtensionCommandContext | null = null;
|
|
25
|
-
let
|
|
28
|
+
let CompactParams: any = null;
|
|
26
29
|
|
|
27
30
|
const isInternal = (name: string) => InternalTools.includes(name);
|
|
28
31
|
|
|
32
|
+
const formatContextUsage = (usage: ContextUsage | undefined, includeTokens = false): string => {
|
|
33
|
+
if (usage?.percent == null) return "Unknown";
|
|
34
|
+
|
|
35
|
+
const percent = `${usage.percent.toFixed(1)}%`;
|
|
36
|
+
if (!includeTokens || usage.tokens == null) return percent;
|
|
37
|
+
|
|
38
|
+
return `${percent} (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
39
|
+
};
|
|
40
|
+
|
|
29
41
|
const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
30
42
|
if (target.toLowerCase() === "root") {
|
|
31
43
|
const tree = sm.getTree();
|
|
@@ -47,20 +59,23 @@ const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
|
47
59
|
return target;
|
|
48
60
|
};
|
|
49
61
|
|
|
62
|
+
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.";
|
|
50
63
|
const ContextTimelineParams = Type.Object({
|
|
51
|
-
limit: Type.Optional(Type.Number({ description: "
|
|
52
|
-
verbose: Type.Optional(Type.Boolean({ description: "If true, show all messages. If false (default), collapse
|
|
64
|
+
limit: Type.Optional(Type.Number({ description: "Maximum visible timeline entries (default: 50)." })),
|
|
65
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show all messages including internal context-tool traffic. If false (default), collapse to structural milestones." })),
|
|
53
66
|
});
|
|
54
67
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
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.";
|
|
69
|
+
const ContextCompactParams = Type.Object({
|
|
70
|
+
target: Type.String({ description: "Checkpoint name, history node ID, or root to use as the branch point for the summarized continuation." }),
|
|
71
|
+
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." }),
|
|
72
|
+
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." })),
|
|
59
73
|
});
|
|
60
74
|
|
|
75
|
+
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.";
|
|
61
76
|
const ContextCheckpointParams = Type.Object({
|
|
62
|
-
name: Type.String({ description: "
|
|
63
|
-
target: Type.Optional(Type.String({ description: "Optional history node ID or checkpoint name to label. Defaults to the current conversation
|
|
77
|
+
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." }),
|
|
78
|
+
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." })),
|
|
64
79
|
});
|
|
65
80
|
|
|
66
81
|
export default function (pi: ExtensionAPI) {
|
|
@@ -69,15 +84,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
84
|
handler: async (args, ctx) => {
|
|
70
85
|
CommandCtx = ctx;
|
|
71
86
|
ctx.ui.notify("Agentic Context Management enabled.", "info");
|
|
72
|
-
pi.sendMessage({
|
|
73
|
-
customType: "pi-context",
|
|
74
|
-
content: "use context-management skill",
|
|
75
|
-
display: false,
|
|
76
|
-
}, {
|
|
77
|
-
deliverAs: "followUp"
|
|
78
|
-
});
|
|
79
87
|
if (args) {
|
|
80
|
-
pi.sendUserMessage(args)
|
|
88
|
+
pi.sendUserMessage(args, { deliverAs: "followUp" });
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
});
|
|
@@ -102,7 +110,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
110
|
pi.registerTool({
|
|
103
111
|
name: "context_checkpoint",
|
|
104
112
|
label: "Context Checkpoint",
|
|
105
|
-
description:
|
|
113
|
+
description: ContextCheckpointDescription,
|
|
106
114
|
parameters: ContextCheckpointParams,
|
|
107
115
|
async execute(_id, params: Static<typeof ContextCheckpointParams>, _signal, _onUpdate, ctx) {
|
|
108
116
|
const sm = ctx.sessionManager as SessionManager;
|
|
@@ -157,14 +165,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
pi.setLabel(id, params.name);
|
|
160
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: `Created checkpoint '${params.name}' at ${id}.`
|
|
172
|
+
}],
|
|
173
|
+
details: {}
|
|
174
|
+
};
|
|
161
175
|
},
|
|
162
176
|
});
|
|
163
177
|
|
|
164
178
|
pi.registerTool({
|
|
165
179
|
name: "context_timeline",
|
|
166
180
|
label: "Context Timeline",
|
|
167
|
-
description:
|
|
181
|
+
description: ContextTimelineDescription,
|
|
168
182
|
parameters: ContextTimelineParams,
|
|
169
183
|
async execute(_id, params: Static<typeof ContextTimelineParams>, _signal, _onUpdate, ctx) {
|
|
170
184
|
const sm = ctx.sessionManager as SessionManager;
|
|
@@ -342,11 +356,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
342
356
|
}
|
|
343
357
|
|
|
344
358
|
// --- Context Dashboard (HUD) ---
|
|
345
|
-
const
|
|
346
|
-
let usageStr = "Unknown";
|
|
347
|
-
if (usage?.percent != null && usage.tokens != null && usage.contextWindow != null) {
|
|
348
|
-
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
349
|
-
}
|
|
359
|
+
const usageStr = formatContextUsage(ctx.getContextUsage(), true);
|
|
350
360
|
|
|
351
361
|
// Find the distance to the nearest checkpoint
|
|
352
362
|
let stepsSinceCheckpoint = 0;
|
|
@@ -361,10 +371,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
361
371
|
stepsSinceCheckpoint++;
|
|
362
372
|
}
|
|
363
373
|
|
|
374
|
+
const compactCue = nearestCheckpointName === "None"
|
|
375
|
+
? "create a checkpoint before the next noisy phase"
|
|
376
|
+
: `if this segment has produced a stable result and another phase remains, compact to '${nearestCheckpointName}' with a handoff summary before continuing`;
|
|
377
|
+
|
|
364
378
|
const hud = [
|
|
365
379
|
`[Context Dashboard]`,
|
|
366
380
|
`• Context Usage: ${usageStr}`,
|
|
367
381
|
`• Segment Size: ${stepsSinceCheckpoint} steps since last checkpoint '${nearestCheckpointName}'`,
|
|
382
|
+
`• Compact Cue: ${compactCue}`,
|
|
368
383
|
`---------------------------------------------------`
|
|
369
384
|
].join("\n");
|
|
370
385
|
|
|
@@ -373,13 +388,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
373
388
|
});
|
|
374
389
|
|
|
375
390
|
pi.registerTool({
|
|
376
|
-
name: "
|
|
377
|
-
label: "
|
|
378
|
-
description:
|
|
379
|
-
parameters:
|
|
380
|
-
async execute(_id, params: Static<typeof
|
|
391
|
+
name: "context_compact",
|
|
392
|
+
label: "Context Compact",
|
|
393
|
+
description: ContextCompactDescription,
|
|
394
|
+
parameters: ContextCompactParams,
|
|
395
|
+
async execute(_id, params: Static<typeof ContextCompactParams>, _signal, _onUpdate, ctx) {
|
|
381
396
|
if (!CommandCtx) {
|
|
382
|
-
|
|
397
|
+
const editorText = ctx.ui.getEditorText();
|
|
398
|
+
const followUp = editorText
|
|
399
|
+
? `${AcmEnableFollowUp}\n${editorText}`
|
|
400
|
+
: AcmEnableFollowUp;
|
|
401
|
+
ctx.ui.setEditorText(`/acm ${followUp}`)
|
|
383
402
|
return {
|
|
384
403
|
content: [{
|
|
385
404
|
type: "text",
|
|
@@ -389,6 +408,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
389
408
|
};
|
|
390
409
|
}
|
|
391
410
|
const sm = ctx.sessionManager as SessionManager;
|
|
411
|
+
const usageBeforeText = formatContextUsage(ctx.getContextUsage());
|
|
392
412
|
|
|
393
413
|
const tid = resolveTargetId(sm, params.target);
|
|
394
414
|
|
|
@@ -402,46 +422,74 @@ export default function (pi: ExtensionAPI) {
|
|
|
402
422
|
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
403
423
|
const origin = currentLabel ? `checkpoint: ${currentLabel}` : (currentLeaf || "unknown");
|
|
404
424
|
|
|
405
|
-
const enrichedMessage = `(summary from ${origin})\n${params.
|
|
425
|
+
const enrichedMessage = `(handoff summary from ${origin})\n${params.summary}`;
|
|
406
426
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
RewindParams.enrichedMessage = enrichedMessage;
|
|
427
|
+
CompactParams = params;
|
|
428
|
+
CompactParams.tid = tid;
|
|
429
|
+
CompactParams.enrichedMessage = enrichedMessage;
|
|
430
|
+
CompactParams.usageBeforeText = usageBeforeText;
|
|
412
431
|
|
|
413
|
-
return { content: [{ type: "text", text: "
|
|
432
|
+
return { content: [{ type: "text", text: "compact start" }], details: {} };
|
|
414
433
|
},
|
|
415
434
|
});
|
|
416
435
|
|
|
417
436
|
pi.on("turn_end", async (_event, ctx) => {
|
|
418
|
-
if (!
|
|
437
|
+
if (!CompactParams) {
|
|
419
438
|
return
|
|
420
439
|
}
|
|
421
440
|
ctx.abort()
|
|
422
441
|
});
|
|
423
442
|
|
|
424
443
|
pi.on("agent_end", async (_event, ctx) => {
|
|
425
|
-
if (!
|
|
444
|
+
if (!CompactParams) {
|
|
426
445
|
return
|
|
427
446
|
}
|
|
428
447
|
if (!CommandCtx) {
|
|
429
448
|
return
|
|
430
449
|
}
|
|
431
450
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
451
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
452
|
+
const compactParams = CompactParams;
|
|
453
|
+
const commandCtx = CommandCtx;
|
|
454
|
+
CompactParams = null;
|
|
455
|
+
|
|
456
|
+
// `agent_end` is emitted before the core Agent is actually idle. If we
|
|
457
|
+
// call pi.sendMessage({ triggerTurn: true }) inside this handler, pi still
|
|
458
|
+
// sees an active stream and queues the message as steering; after
|
|
459
|
+
// `agent_end` the loop has already stopped, so that queued message is not
|
|
460
|
+
// drained. Defer navigation + continuation until the current run settles.
|
|
461
|
+
setTimeout(async () => {
|
|
462
|
+
try {
|
|
463
|
+
await commandCtx.waitForIdle();
|
|
464
|
+
const nid = sm.branchWithSummary(compactParams.tid, compactParams.enrichedMessage);
|
|
465
|
+
compactParams.nid = nid;
|
|
466
|
+
// branchWithSummary advances the leaf to the summary entry. Reset
|
|
467
|
+
// it so navigateTree(nid) can rebuild agent state instead of
|
|
468
|
+
// returning early as a no-op.
|
|
469
|
+
sm.branch(compactParams.tid);
|
|
470
|
+
await commandCtx.navigateTree(compactParams.nid, {
|
|
471
|
+
summarize: false,
|
|
472
|
+
});
|
|
438
473
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
474
|
+
const usageAfter = commandCtx.getContextUsage();
|
|
475
|
+
commandCtx.ui.notify([
|
|
476
|
+
`Compacted to ${compactParams.target}${compactParams.target === compactParams.tid ? "" : `(${compactParams.tid})`}`,
|
|
477
|
+
`Context Usage: ${compactParams.usageBeforeText} -> ${formatContextUsage(usageAfter)}`,
|
|
478
|
+
`Backup checkpoint created: ${compactParams.backupCheckpoint || "none"}`,
|
|
479
|
+
`Summary: ${compactParams.enrichedMessage}`,
|
|
480
|
+
].join("\n"), "info");
|
|
481
|
+
|
|
482
|
+
pi.sendMessage({
|
|
483
|
+
customType: PiContextCustomMessageType,
|
|
484
|
+
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",
|
|
485
|
+
display: false,
|
|
486
|
+
}, {
|
|
487
|
+
triggerTurn: true,
|
|
488
|
+
deliverAs: "followUp",
|
|
489
|
+
});
|
|
490
|
+
} catch (err) {
|
|
491
|
+
commandCtx.ui.notify(`context_compact failed to continue: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
492
|
+
}
|
|
493
|
+
}, 0);
|
|
446
494
|
});
|
|
447
495
|
}
|