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/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", "context_rewind"];
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 RewindParams: any = null;
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: "History limit for visible entries (default: 50)." })),
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." })),
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 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." })),
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: "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." })),
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: "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.",
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 { content: [{ type: "text", text: `Created checkpoint '${params.name}' at ${id}` }], details: {} };
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: "Show the conversation timeline, checkpoints, summaries, and current position. Use this to understand session structure, monitor drift, and choose a checkpoint to return to.",
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 usage = await ctx.getContextUsage();
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: "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) {
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
- ctx.ui.setEditorText(`/acm ${ctx.ui.getEditorText() || "continue"}`)
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.message}`;
425
+ const enrichedMessage = `(handoff summary from ${origin})\n${params.summary}`;
406
426
 
407
- const nid = await sm.branchWithSummary(tid, enrichedMessage);
408
- RewindParams = params;
409
- RewindParams.nid = nid;
410
- RewindParams.tid = tid;
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: "rewind start" }], details: {} };
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 (!RewindParams) {
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 (!RewindParams) {
444
+ if (!CompactParams) {
426
445
  return
427
446
  }
428
447
  if (!CommandCtx) {
429
448
  return
430
449
  }
431
450
 
432
- await CommandCtx.navigateTree(RewindParams.nid, {
433
- summarize: false,
434
- });
435
-
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;
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
- pi.sendMessage({
440
- customType: "pi-context",
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",
442
- display: false,
443
- }, {
444
- triggerTurn: true,
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
  }