Update hooks.md and session.md for consolidated HookContext

- HookEventContext renamed to HookContext (used for events and commands)
- RegisteredCommand.handler: (ctx) -> (args, ctx)
- before_compact: previousCompactions -> branchEntries, model moved to ctx.model
- ctx.exec -> pi.exec in examples
- ctx.sessionFile -> ctx.sessionManager.getSessionFile()
- CompactionPreparation: document turnPrefixMessages, isSplitTurn, previousSummary
- session.md: clarify details field for compaction/branch summary
This commit is contained in:
Mario Zechner 2025-12-31 02:40:31 +01:00
parent ddda8b124c
commit 450d77fb79
2 changed files with 39 additions and 27 deletions

View file

@ -67,7 +67,7 @@ import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
export default function (pi: HookAPI) { export default function (pi: HookAPI) {
pi.on("session", async (event, ctx) => { pi.on("session", async (event, ctx) => {
ctx.ui.notify(`Session ${event.reason}: ${ctx.sessionFile ?? "ephemeral"}`, "info"); ctx.ui.notify(`Session ${event.reason}: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
}); });
} }
``` ```
@ -148,9 +148,8 @@ Fired on session lifecycle events. The `before_*` variants fire before the actio
```typescript ```typescript
pi.on("session", async (event, ctx) => { pi.on("session", async (event, ctx) => {
// event.entries: SessionEntry[] - all session entries // Access session file: ctx.sessionManager.getSessionFile() (undefined with --no-session)
// event.sessionFile: string | null - current session file (null with --no-session) // event.previousSessionFile: string | undefined - previous session file (for switch events)
// event.previousSessionFile: string | null - previous session file
// event.reason: "start" | "before_switch" | "switch" | "before_new" | "new" | // event.reason: "start" | "before_switch" | "switch" | "before_new" | "new" |
// "before_branch" | "branch" | "before_compact" | "compact" | "shutdown" // "before_branch" | "branch" | "before_compact" | "compact" | "shutdown"
// event.targetTurnIndex: number - only for "before_branch" and "branch" // event.targetTurnIndex: number - only for "before_branch" and "branch"
@ -200,9 +199,9 @@ Session entries (before compaction):
│ hdr │ cmp │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │ │ hdr │ cmp │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │
└─────┴─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┘ └─────┴─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┘
↑ └───────┬───────┘ └────────────┬────────────┘ ↑ └───────┬───────┘ └────────────┬────────────┘
previousSummary messagesToSummarize messagesToKeep previousSummary messagesToSummarize kept (firstKeptEntryId = "...")
cutPoint.firstKeptEntryIndex = 5 firstKeptEntryIndex = 5
After compaction (new entry appended): After compaction (new entry appended):
@ -213,7 +212,7 @@ After compaction (new entry appended):
└──────────┬───────────┘ └────────────────────────┬─────────────────┘ └──────────┬───────────┘ └────────────────────────┬─────────────────┘
not sent to LLM sent to LLM not sent to LLM sent to LLM
firstKeptEntryIndex = 5 firstKeptEntryId = "..."
(stored in new cmp) (stored in new cmp)
``` ```
@ -243,15 +242,16 @@ Split turn example (one huge turn that exceeds keepRecentTokens):
↑ ↑ ↑ ↑
turnStartIndex = 1 firstKeptEntryIndex = 7 turnStartIndex = 1 firstKeptEntryIndex = 7
│ │ (must be usr/ass/bash, not tool) │ │ (must be usr/ass/bash, not tool)
└─────────── turn prefix ───────────────┘ (idx 1-6, summarized separately) └──────── turnPrefixMessages ───────────┘ (idx 1-6, summarized separately)
└── kept messages (idx 7-9) └── kept messages (idx 7-9)
isSplitTurn = true
messagesToSummarize = [] (no complete turns before this one) messagesToSummarize = [] (no complete turns before this one)
messagesToKeep = [ass idx 7, tool idx 8, ass idx 9] turnPrefixMessages = [usr idx 1, ass idx 2, tool idx 3, ass idx 4, tool idx 5, tool idx 6]
The default compaction generates TWO summaries that get merged: The default compaction generates TWO summaries that get merged:
1. History summary (previousSummary + messagesToSummarize) 1. History summary (previousSummary + messagesToSummarize)
2. Turn prefix summary (messages from turnStartIndex to firstKeptEntryIndex) 2. Turn prefix summary (turnPrefixMessages)
``` ```
See [src/core/compaction.ts](../src/core/compaction.ts) for the full implementation. See [src/core/compaction.ts](../src/core/compaction.ts) for the full implementation.
@ -260,13 +260,12 @@ See [src/core/compaction.ts](../src/core/compaction.ts) for the full implementat
| Field | Description | | Field | Description |
|-------|-------------| |-------|-------------|
| `preparation` | Compaction preparation with `firstKeptEntryId`, `messagesToSummarize`, `messagesToKeep`, `tokensBefore`, `isSplitTurn`. | | `preparation` | Compaction preparation with `firstKeptEntryId`, `messagesToSummarize`, `turnPrefixMessages`, `isSplitTurn`, `previousSummary`, `fileOps`, `tokensBefore`, `settings`. |
| `previousCompactions` | Array of previous `CompactionEntry` objects (newest first). Access summaries for accumulated context. | | `branchEntries` | All entries on current branch (root to leaf). Use to find previous compactions or hook state. |
| `model` | Model to use for summarization. |
| `customInstructions` | Optional focus for summary (from `/compact <instructions>`). | | `customInstructions` | Optional focus for summary (from `/compact <instructions>`). |
| `signal` | AbortSignal for cancellation. Pass to LLM calls and check periodically. | | `signal` | AbortSignal for cancellation. Pass to LLM calls and check periodically. |
Access session entries via `ctx.sessionManager.getEntries()` and API keys via `ctx.modelRegistry.getApiKey(model)`. Access session entries via `ctx.sessionManager.getEntries()`, API keys via `ctx.modelRegistry.getApiKey(model)`, and the current model via `ctx.model`.
Custom compaction hooks should honor the abort signal by passing it to `complete()` calls. This allows users to cancel compaction (e.g., via Ctrl+C during `/compact`). Custom compaction hooks should honor the abort signal by passing it to `complete()` calls. This allows users to cancel compaction (e.g., via Ctrl+C during `/compact`).
@ -593,13 +592,25 @@ The current working directory.
console.log(`Working in: ${ctx.cwd}`); console.log(`Working in: ${ctx.cwd}`);
``` ```
### ctx.sessionFile ### ctx.model
Path to the current session file, or `null` when running with `--no-session` (ephemeral mode). The current model, or `undefined` if no model is selected yet.
```typescript ```typescript
if (ctx.sessionFile) { if (ctx.model) {
console.log(`Session: ${ctx.sessionFile}`); const apiKey = ctx.modelRegistry.getApiKey(ctx.model);
// Use for LLM calls
}
```
### ctx.sessionManager.getSessionFile()
Path to the current session file, or `undefined` when running with `--no-session` (ephemeral mode).
```typescript
const sessionFile = ctx.sessionManager.getSessionFile();
if (sessionFile) {
console.log(`Session: ${sessionFile}`);
} }
``` ```
@ -680,22 +691,23 @@ Register a custom slash command.
```typescript ```typescript
pi.registerCommand(name: string, options: { pi.registerCommand(name: string, options: {
description?: string; description?: string;
handler: (ctx: HookCommandContext) => Promise<void>; handler: (args: string, ctx: HookContext) => Promise<void>;
}): void }): void
``` ```
The handler receives: The handler receives:
- `ctx.args`: Everything after `/commandname` - `args`: Everything after `/commandname` (e.g., `/stats foo``"foo"`)
- `ctx.ui`: UI methods (select, confirm, input, notify, custom) - `ctx.ui`: UI methods (select, confirm, input, notify, custom)
- `ctx.hasUI`: Whether interactive UI is available - `ctx.hasUI`: Whether interactive UI is available
- `ctx.cwd`: Current working directory - `ctx.cwd`: Current working directory
- `ctx.model`: Current model (may be undefined)
- `ctx.sessionManager`: Session access - `ctx.sessionManager`: Session access
- `ctx.modelRegistry`: Model access - `ctx.modelRegistry`: Model access
```typescript ```typescript
pi.registerCommand("stats", { pi.registerCommand("stats", {
description: "Show session statistics", description: "Show session statistics",
handler: async (ctx) => { handler: async (args, ctx) => {
const entries = ctx.sessionManager.getEntries(); const entries = ctx.sessionManager.getEntries();
const messages = entries.filter(e => e.type === "message").length; const messages = entries.filter(e => e.type === "message").length;
ctx.ui.notify(`${messages} messages in session`, "info"); ctx.ui.notify(`${messages} messages in session`, "info");
@ -876,7 +888,7 @@ export default function (pi: HookAPI) {
pi.on("turn_start", async (event, ctx) => { pi.on("turn_start", async (event, ctx) => {
// Create a git stash entry before LLM makes changes // Create a git stash entry before LLM makes changes
const { stdout } = await ctx.exec("git", ["stash", "create"]); const { stdout } = await pi.exec("git", ["stash", "create"]);
const ref = stdout.trim(); const ref = stdout.trim();
if (ref) { if (ref) {
checkpoints.set(event.turnIndex, ref); checkpoints.set(event.turnIndex, ref);
@ -896,7 +908,7 @@ export default function (pi: HookAPI) {
]); ]);
if (choice?.startsWith("Yes")) { if (choice?.startsWith("Yes")) {
await ctx.exec("git", ["stash", "apply", ref]); await pi.exec("git", ["stash", "apply", ref]);
ctx.ui.notify("Code restored to checkpoint", "info"); ctx.ui.notify("Code restored to checkpoint", "info");
} }
}); });

View file

@ -89,19 +89,19 @@ Created when context is compacted. Stores a summary of earlier messages.
``` ```
Optional fields: Optional fields:
- `details`: Hook-specific data (e.g., ArtifactIndex for structured compaction) - `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for custom hook implementations)
- `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated - `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated
### BranchSummaryEntry ### BranchSummaryEntry
Created when switching branches via `/tree`. Captures context from the abandoned path. Created when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.
```json ```json
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."} {"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}
``` ```
Optional fields: Optional fields:
- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) - `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default implementation, arbitrary for custom implementation
- `fromHook`: `true` if generated by a hook - `fromHook`: `true` if generated by a hook
### CustomEntry ### CustomEntry
@ -172,7 +172,7 @@ const lines = readFileSync("session.jsonl", "utf8").trim().split("\n");
for (const line of lines) { for (const line of lines) {
const entry = JSON.parse(line); const entry = JSON.parse(line);
switch (entry.type) { switch (entry.type) {
case "session": case "session":
console.log(`Session v${entry.version ?? 1}: ${entry.id}`); console.log(`Session v${entry.version ?? 1}: ${entry.id}`);