mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 06:04:40 +00:00
Update CHANGELOG.md and docs for session tree release
CHANGELOG.md: - Add /tree command, context event, before_agent_start event - Add ui.custom(), branch summarization, selectedBg theme color - Add snake game example hook - Add external contributions: CRLF fix, bash on Unix, clickable OAuth, error messages - Update theme requirements (50 total colors) session.md: - Complete rewrite for v2 tree structure - Document all entry types with examples - Add SessionManager API reference hooks.md: - Replace pi.send() with pi.sendMessage() - Add pi.appendEntry(), pi.registerCommand(), pi.registerMessageRenderer() - Move exec() from ctx to pi.exec() - Add ui.custom() for custom TUI components - Add context and before_agent_start events - Update before_compact event fields - Add ctx.sessionManager and ctx.modelRegistry
This commit is contained in:
parent
a4b36e3d63
commit
a9479458ee
4 changed files with 504 additions and 57 deletions
|
|
@ -42,11 +42,18 @@
|
|||
- Handler types renamed: `SendHandler` → `SendMessageHandler`, new `AppendEntryHandler`
|
||||
- **SessionManager**:
|
||||
- `getSessionFile()` now returns `string | undefined` (undefined for in-memory sessions)
|
||||
- **Themes**: Custom themes must add `customMessageBg`, `customMessageText`, `customMessageLabel` color tokens
|
||||
- **Themes**: Custom themes must add `selectedBg`, `customMessageBg`, `customMessageText`, `customMessageLabel` color tokens (50 total)
|
||||
|
||||
### Added
|
||||
|
||||
- **`/tree` command**: Navigate the session tree in-place. Shows full tree structure with labels, supports search (type to filter), page navigation (←/→), and filter modes (Ctrl+O cycles: default → no-tools → user-only → labeled-only → all, Shift+Ctrl+O cycles backwards). Selecting a branch generates a summary and switches context. Press `l` to label entries.
|
||||
- **`context` hook event**: Fires before each LLM call, allowing hooks to non-destructively modify messages. Returns `{ messages }` to override. Useful for dynamic context pruning without modifying session history.
|
||||
- **`before_agent_start` hook event**: Fires once when user submits a prompt, before `agent_start`. Hooks can return `{ message }` to inject a `CustomMessageEntry` that gets persisted and sent to the LLM.
|
||||
- **`ui.custom()` for hooks**: Show arbitrary TUI components with keyboard focus. Call `done()` when finished: `ctx.ui.custom(component, done)`.
|
||||
- **Branch summarization**: When switching branches via `/tree`, generates a summary of the abandoned branch including file operations (read/modified files). Summaries are stored as `BranchSummaryEntry` with cumulative file tracking in `details`.
|
||||
- **`selectedBg` theme color**: Background color for selected/active lines in tree selector and other components.
|
||||
- **`enabledModels` setting**: Configure whitelisted models in `settings.json` (same format as `--models` CLI flag). CLI `--models` takes precedence over the setting.
|
||||
- **Snake game example hook**: Added `examples/hooks/snake.ts` demonstrating `ui.custom()`, `registerCommand()`, and session persistence.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -61,7 +68,10 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- **Edit tool fails on Windows due to CRLF line endings**: Files with CRLF line endings now match correctly when LLMs send LF-only text. Line endings are normalized before matching and restored to original style on write. ([#355](https://github.com/badlogic/pi-mono/issues/355))
|
||||
- **Edit tool fails on Windows due to CRLF line endings**: Files with CRLF line endings now match correctly when LLMs send LF-only text. Line endings are normalized before matching and restored to original style on write. ([#355](https://github.com/badlogic/pi-mono/issues/355) by [@Pratham-Dubey](https://github.com/Pratham-Dubey))
|
||||
- **Use bash instead of sh on Unix**: Fixed shell commands using `/bin/sh` instead of `/bin/bash` on Unix systems. ([#328](https://github.com/badlogic/pi-mono/pull/328) by [@dnouri](https://github.com/dnouri))
|
||||
- **OAuth login URL clickable**: Made OAuth login URLs clickable in terminal. ([#349](https://github.com/badlogic/pi-mono/pull/349) by [@Cursivez](https://github.com/Cursivez))
|
||||
- **Improved error messages**: Better error messages when `apiKey` or `model` are missing. ([#346](https://github.com/badlogic/pi-mono/pull/346) by [@ronyrus](https://github.com/ronyrus))
|
||||
- **Session file validation**: `findMostRecentSession()` now validates session headers before returning, preventing non-session JSONL files from being loaded
|
||||
- **Compaction error handling**: `generateSummary()` and `generateTurnPrefixSummary()` now throw on LLM errors instead of returning empty strings
|
||||
|
||||
|
|
|
|||
64
packages/coding-agent/UNRELEASED_OLD.md
Normal file
64
packages/coding-agent/UNRELEASED_OLD.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
|
||||
### Breaking Changes
|
||||
|
||||
- **Session tree structure (v2)**: Sessions now store entries as a tree with `id`/`parentId` fields, enabling in-place branching without creating new files. Existing v1 sessions are auto-migrated on load.
|
||||
- **SessionManager API**:
|
||||
- `saveXXX()` renamed to `appendXXX()` (e.g., `appendMessage`, `appendCompaction`)
|
||||
- `branchInPlace()` renamed to `branch()`
|
||||
- `reset()` renamed to `newSession()`
|
||||
- `createBranchedSessionFromEntries(entries, index)` replaced with `createBranchedSession(leafId)`
|
||||
- `saveCompaction(entry)` replaced with `appendCompaction(summary, firstKeptEntryId, tokensBefore)`
|
||||
- `getEntries()` now excludes the session header (use `getHeader()` separately)
|
||||
- New methods: `getTree()`, `getPath()`, `getLeafUuid()`, `getLeafEntry()`, `getEntry()`, `branchWithSummary()`
|
||||
- New `appendCustomEntry(customType, data)` for hooks to store custom data (not in LLM context)
|
||||
- New `appendCustomMessageEntry(customType, content, display, details?)` for hooks to inject messages into LLM context
|
||||
- **Compaction API**:
|
||||
- `CompactionEntry<T>` and `CompactionResult<T>` are now generic with optional `details?: T` for hook-specific data
|
||||
- `compact()` now returns `CompactionResult` (`{ summary, firstKeptEntryId, tokensBefore, details? }`) instead of `CompactionEntry`
|
||||
- `appendCompaction()` now accepts optional `details` parameter
|
||||
- `CompactionEntry.firstKeptEntryIndex` replaced with `firstKeptEntryId`
|
||||
- `prepareCompaction()` now returns `firstKeptEntryId` in its result
|
||||
- **Hook types**:
|
||||
- `SessionEventBase` no longer has `sessionManager`/`modelRegistry` - access them via `HookEventContext` instead
|
||||
- `HookEventContext` now has `sessionManager` and `modelRegistry` (moved from events)
|
||||
- `HookEventContext` no longer has `exec()` - use `pi.exec()` instead
|
||||
- `HookCommandContext` no longer has `exec()` - use `pi.exec()` instead
|
||||
- `before_compact` event passes `preparation: CompactionPreparation` and `previousCompactions: CompactionEntry[]` (newest first)
|
||||
- `before_switch` event now has `targetSessionFile`, `switch` event has `previousSessionFile`
|
||||
- Removed `resolveApiKey` (use `modelRegistry.getApiKey(model)`)
|
||||
- Hooks can return `compaction.details` to store custom data (e.g., ArtifactIndex for structured compaction)
|
||||
- **Hook API**:
|
||||
- `pi.send(text, attachments?)` replaced with `pi.sendMessage(message, triggerTurn?)` which creates `CustomMessageEntry` instead of user messages
|
||||
- New `pi.appendEntry(customType, data?)` to persist hook state (does NOT participate in LLM context)
|
||||
- New `pi.registerCommand(name, options)` to register custom slash commands
|
||||
- New `pi.registerMessageRenderer(customType, renderer)` to register custom renderers for hook messages
|
||||
- New `pi.exec(command, args, options?)` to execute shell commands (moved from `HookEventContext`/`HookCommandContext`)
|
||||
- `HookMessageRenderer` type: `(message: HookMessage, options, theme) => Component | null`
|
||||
- Renderers return inner content; the TUI wraps it in a styled Box
|
||||
- New types: `HookMessage<T>`, `RegisteredCommand`, `HookCommandContext`
|
||||
- Handler types renamed: `SendHandler` → `SendMessageHandler`, new `AppendEntryHandler`
|
||||
- **SessionManager**:
|
||||
- `getSessionFile()` now returns `string | undefined` (undefined for in-memory sessions)
|
||||
- **Themes**: Custom themes must add `customMessageBg`, `customMessageText`, `customMessageLabel` color tokens
|
||||
|
||||
### Added
|
||||
|
||||
- **`enabledModels` setting**: Configure whitelisted models in `settings.json` (same format as `--models` CLI flag). CLI `--models` takes precedence over the setting.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Entry IDs**: Session entries now use short 8-character hex IDs instead of full UUIDs
|
||||
- **API key priority**: `ANTHROPIC_OAUTH_TOKEN` now takes precedence over `ANTHROPIC_API_KEY`
|
||||
- **New entry types**: `BranchSummaryEntry` for branch context, `CustomEntry<T>` for hook state persistence, `CustomMessageEntry<T>` for hook-injected context messages, `LabelEntry` for user-defined bookmarks
|
||||
- **Entry labels**: New `getLabel(id)` and `appendLabelChange(targetId, label)` methods for labeling entries. Labels are included in `SessionTreeNode` for UI/export.
|
||||
- **TUI**: `CustomMessageEntry` renders with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors). Entries with `display: false` are hidden.
|
||||
- **AgentSession**: New `sendHookMessage(message, triggerTurn?)` method for hooks to inject messages. Handles queuing during streaming, direct append when idle, and optional turn triggering.
|
||||
- **HookMessage**: New message type with `role: "hookMessage"` for hook-injected messages in agent events. Use `isHookMessage(msg)` type guard to identify them. These are converted to user messages for LLM context via `messageTransformer`.
|
||||
- **Agent.prompt()**: Now accepts `AppMessage` directly (in addition to `string, attachments?`) for custom message types like `HookMessage`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Edit tool fails on Windows due to CRLF line endings**: Files with CRLF line endings now match correctly when LLMs send LF-only text. Line endings are normalized before matching and restored to original style on write. ([#355](https://github.com/badlogic/pi-mono/issues/355))
|
||||
- **Session file validation**: `findMostRecentSession()` now validates session headers before returning, preventing non-session JSONL files from being loaded
|
||||
- **Compaction error handling**: `generateSummary()` and `generateTurnPrefixSummary()` now throw on LLM errors instead of returning empty strings
|
||||
|
||||
|
|
@ -260,19 +260,31 @@ See [src/core/compaction.ts](../src/core/compaction.ts) for the full implementat
|
|||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `entries` | All session entries (header, messages, model changes, previous compactions). Use this for custom schemes that need full session history. |
|
||||
| `cutPoint` | Where default compaction would cut. `firstKeptEntryIndex` is the entry index where kept messages start. `isSplitTurn` indicates if cutting mid-turn. |
|
||||
| `previousSummary` | Summary from the last compaction, if any. Include this in your summary to preserve accumulated context. |
|
||||
| `messagesToSummarize` | Messages that will be summarized and discarded (from after last compaction to cut point). |
|
||||
| `messagesToKeep` | Messages that will be kept verbatim after the summary (from cut point to end). |
|
||||
| `tokensBefore` | Current context token count (why compaction triggered). |
|
||||
| `preparation` | Compaction preparation with `firstKeptEntryId`, `messagesToSummarize`, `messagesToKeep`, `tokensBefore`, `isSplitTurn`. |
|
||||
| `previousCompactions` | Array of previous `CompactionEntry` objects (newest first). Access summaries for accumulated context. |
|
||||
| `model` | Model to use for summarization. |
|
||||
| `resolveApiKey` | Function to resolve API key for any model: `await resolveApiKey(model)` |
|
||||
| `customInstructions` | Optional focus for summary (from `/compact <instructions>`). |
|
||||
| `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)`.
|
||||
|
||||
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`).
|
||||
|
||||
**Returning custom compaction:**
|
||||
|
||||
```typescript
|
||||
return {
|
||||
compaction: {
|
||||
summary: "Your summary...",
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
details: { /* optional hook-specific data */ },
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The `details` field persists hook-specific metadata (e.g., artifact index, version markers) in the compaction entry.
|
||||
|
||||
See [examples/hooks/custom-compaction.ts](../examples/hooks/custom-compaction.ts) for a complete example.
|
||||
|
||||
**After compaction (`compact` event):**
|
||||
|
|
@ -437,6 +449,61 @@ export default function (pi: HookAPI) {
|
|||
|
||||
**Note:** If you modify `content`, you should also update `details` accordingly. The TUI uses `details` (e.g., truncation info) for rendering, so inconsistent values will cause display issues.
|
||||
|
||||
### context
|
||||
|
||||
Fired before each LLM call, allowing non-destructive message modification. The original session is not modified.
|
||||
|
||||
```typescript
|
||||
pi.on("context", async (event, ctx) => {
|
||||
// event.messages: AgentMessage[] (deep copy, safe to modify)
|
||||
|
||||
// Return modified messages, or undefined to keep original
|
||||
return { messages: modifiedMessages };
|
||||
});
|
||||
```
|
||||
|
||||
Use case: Dynamic context pruning without modifying session history.
|
||||
|
||||
```typescript
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("context", async (event, ctx) => {
|
||||
// Find all pruning decisions stored as custom entries
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const pruningRules = entries
|
||||
.filter(e => e.type === "custom" && e.customType === "prune-rules")
|
||||
.flatMap(e => e.data as PruneRule[]);
|
||||
|
||||
// Apply pruning to messages (e.g., truncate old tool results)
|
||||
const prunedMessages = applyPruning(event.messages, pruningRules);
|
||||
return { messages: prunedMessages };
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### before_agent_start
|
||||
|
||||
Fired once when user submits a prompt, before `agent_start`. Allows injecting a message that gets persisted.
|
||||
|
||||
```typescript
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
// event.userMessage: the user's message
|
||||
|
||||
// Return a message to inject, or undefined to skip
|
||||
return {
|
||||
message: {
|
||||
customType: "context-injection",
|
||||
content: "Additional context...",
|
||||
display: true, // Show in TUI
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
The injected message is:
|
||||
- Persisted to session as a `CustomMessageEntry`
|
||||
- Sent to the LLM as a user message
|
||||
- Visible in TUI (if `display: true`)
|
||||
|
||||
## Context API
|
||||
|
||||
Every event handler receives a context object with these methods:
|
||||
|
|
@ -480,23 +547,42 @@ ctx.ui.notify("Operation complete", "info");
|
|||
ctx.ui.notify("Something went wrong", "error");
|
||||
```
|
||||
|
||||
### ctx.exec(command, args, options?)
|
||||
### ctx.ui.custom(component, done)
|
||||
|
||||
Execute a command and get the result. Supports cancellation via `AbortSignal` and timeout.
|
||||
Show a custom TUI component with keyboard focus. Call `done()` when finished.
|
||||
|
||||
```typescript
|
||||
const result = await ctx.exec("git", ["status"]);
|
||||
// result.stdout: string
|
||||
// result.stderr: string
|
||||
// result.code: number
|
||||
// result.killed?: boolean // True if killed by signal/timeout
|
||||
import { Container, Text } from "@mariozechner/pi-tui";
|
||||
|
||||
// With timeout (5 seconds)
|
||||
const result = await ctx.exec("slow-command", [], { timeout: 5000 });
|
||||
const myComponent = new Container(0, 0, [
|
||||
new Text("Custom UI - press ESC to close", 0, 0),
|
||||
]);
|
||||
|
||||
// With abort signal
|
||||
const controller = new AbortController();
|
||||
const result = await ctx.exec("long-command", [], { signal: controller.signal });
|
||||
ctx.ui.custom(myComponent, () => {
|
||||
// Cleanup when component is dismissed
|
||||
});
|
||||
```
|
||||
|
||||
See `examples/hooks/snake.ts` for a complete example with keyboard handling.
|
||||
|
||||
### ctx.sessionManager
|
||||
|
||||
Access to the session manager for reading session state.
|
||||
|
||||
```typescript
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const path = ctx.sessionManager.getPath();
|
||||
const tree = ctx.sessionManager.getTree();
|
||||
const label = ctx.sessionManager.getLabel(entryId);
|
||||
```
|
||||
|
||||
### ctx.modelRegistry
|
||||
|
||||
Access to model registry for model discovery and API keys.
|
||||
|
||||
```typescript
|
||||
const apiKey = ctx.modelRegistry.getApiKey(model);
|
||||
const models = ctx.modelRegistry.getAvailableModels();
|
||||
```
|
||||
|
||||
### ctx.cwd
|
||||
|
|
@ -529,19 +615,152 @@ if (ctx.hasUI) {
|
|||
}
|
||||
```
|
||||
|
||||
## Sending Messages
|
||||
## Hook API Methods
|
||||
|
||||
Hooks can inject messages into the agent session using `pi.send()`. This is useful for:
|
||||
The `pi` object provides methods for interacting with the agent:
|
||||
|
||||
- Waking up the agent when an external event occurs (file change, CI result, etc.)
|
||||
- Async debugging (inject debug output from other processes)
|
||||
- Triggering agent actions from external systems
|
||||
### pi.sendMessage(message, triggerTurn?)
|
||||
|
||||
Inject a message into the session. Creates a `CustomMessageEntry` (not a user message).
|
||||
|
||||
```typescript
|
||||
pi.send(text: string, attachments?: Attachment[]): void
|
||||
pi.sendMessage(message: HookMessage, triggerTurn?: boolean): void
|
||||
|
||||
// HookMessage structure:
|
||||
interface HookMessage {
|
||||
customType: string; // Your hook's identifier
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
display: boolean; // true = show in TUI, false = hidden
|
||||
details?: unknown; // Hook-specific metadata (not sent to LLM)
|
||||
}
|
||||
```
|
||||
|
||||
If the agent is currently streaming, the message is queued. Otherwise, a new agent loop starts immediately.
|
||||
- If `triggerTurn` is true (default), starts an agent turn after injecting
|
||||
- If streaming, message is queued until current turn ends
|
||||
- Messages are persisted to session and sent to LLM as user messages
|
||||
|
||||
```typescript
|
||||
pi.sendMessage({
|
||||
customType: "my-hook",
|
||||
content: "External trigger: build failed",
|
||||
display: true,
|
||||
}, true); // Trigger agent response
|
||||
```
|
||||
|
||||
### pi.appendEntry(customType, data?)
|
||||
|
||||
Persist hook state to session. Does NOT participate in LLM context.
|
||||
|
||||
```typescript
|
||||
pi.appendEntry(customType: string, data?: unknown): void
|
||||
```
|
||||
|
||||
Use for storing state that survives session reload. Scan entries on reload to reconstruct state:
|
||||
|
||||
```typescript
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason === "start" || event.reason === "switch") {
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "custom" && entry.customType === "my-hook") {
|
||||
// Reconstruct state from entry.data
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Later, save state
|
||||
pi.appendEntry("my-hook", { count: 42 });
|
||||
```
|
||||
|
||||
### pi.registerCommand(name, options)
|
||||
|
||||
Register a custom slash command.
|
||||
|
||||
```typescript
|
||||
pi.registerCommand(name: string, options: {
|
||||
description?: string;
|
||||
handler: (ctx: HookCommandContext) => Promise<void>;
|
||||
}): void
|
||||
```
|
||||
|
||||
The handler receives:
|
||||
- `ctx.args`: Everything after `/commandname`
|
||||
- `ctx.ui`: UI methods (select, confirm, input, notify, custom)
|
||||
- `ctx.hasUI`: Whether interactive UI is available
|
||||
- `ctx.cwd`: Current working directory
|
||||
- `ctx.sessionManager`: Session access
|
||||
- `ctx.modelRegistry`: Model access
|
||||
|
||||
```typescript
|
||||
pi.registerCommand("stats", {
|
||||
description: "Show session statistics",
|
||||
handler: async (ctx) => {
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const messages = entries.filter(e => e.type === "message").length;
|
||||
ctx.ui.notify(`${messages} messages in session`, "info");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To prompt the LLM after a command, use `pi.sendMessage()` with `triggerTurn: true`.
|
||||
|
||||
### pi.registerMessageRenderer(customType, renderer)
|
||||
|
||||
Register a custom TUI renderer for `CustomMessageEntry` messages.
|
||||
|
||||
```typescript
|
||||
pi.registerMessageRenderer(customType: string, renderer: HookMessageRenderer): void
|
||||
|
||||
type HookMessageRenderer = (
|
||||
message: HookMessage,
|
||||
options: { expanded: boolean; width: number },
|
||||
theme: Theme
|
||||
) => Component | null;
|
||||
```
|
||||
|
||||
Return a TUI Component for the inner content. Pi wraps it in a styled box.
|
||||
|
||||
```typescript
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
|
||||
pi.registerMessageRenderer("my-hook", (message, options, theme) => {
|
||||
return new Text(theme.fg("accent", `[MY-HOOK] ${message.content}`), 0, 0);
|
||||
});
|
||||
```
|
||||
|
||||
### pi.exec(command, args, options?)
|
||||
|
||||
Execute a shell command.
|
||||
|
||||
```typescript
|
||||
const result = await pi.exec(command: string, args: string[], options?: {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
}): Promise<ExecResult>;
|
||||
|
||||
interface ExecResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
killed?: boolean; // True if killed by signal/timeout
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
const result = await pi.exec("git", ["status"]);
|
||||
if (result.code === 0) {
|
||||
console.log(result.stdout);
|
||||
}
|
||||
|
||||
// With timeout
|
||||
const result = await pi.exec("slow-command", [], { timeout: 5000 });
|
||||
if (result.killed) {
|
||||
console.log("Command timed out");
|
||||
}
|
||||
```
|
||||
|
||||
## Sending Messages (Examples)
|
||||
|
||||
### Example: File Watcher
|
||||
|
||||
|
|
@ -553,15 +772,18 @@ export default function (pi: HookAPI) {
|
|||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "start") return;
|
||||
|
||||
// Watch a trigger file
|
||||
const triggerFile = "/tmp/agent-trigger.txt";
|
||||
|
||||
fs.watch(triggerFile, () => {
|
||||
try {
|
||||
const content = fs.readFileSync(triggerFile, "utf-8").trim();
|
||||
if (content) {
|
||||
pi.send(`External trigger: ${content}`);
|
||||
fs.writeFileSync(triggerFile, ""); // Clear after reading
|
||||
pi.sendMessage({
|
||||
customType: "file-trigger",
|
||||
content: `External trigger: ${content}`,
|
||||
display: true,
|
||||
}, true);
|
||||
fs.writeFileSync(triggerFile, "");
|
||||
}
|
||||
} catch {
|
||||
// File might not exist yet
|
||||
|
|
@ -573,8 +795,6 @@ export default function (pi: HookAPI) {
|
|||
}
|
||||
```
|
||||
|
||||
To trigger: `echo "Run the tests" > /tmp/agent-trigger.txt`
|
||||
|
||||
### Example: HTTP Webhook
|
||||
|
||||
```typescript
|
||||
|
|
@ -589,7 +809,11 @@ export default function (pi: HookAPI) {
|
|||
let body = "";
|
||||
req.on("data", chunk => body += chunk);
|
||||
req.on("end", () => {
|
||||
pi.send(body || "Webhook triggered");
|
||||
pi.sendMessage({
|
||||
customType: "webhook",
|
||||
content: body || "Webhook triggered",
|
||||
display: true,
|
||||
}, true);
|
||||
res.writeHead(200);
|
||||
res.end("OK");
|
||||
});
|
||||
|
|
@ -602,9 +826,7 @@ export default function (pi: HookAPI) {
|
|||
}
|
||||
```
|
||||
|
||||
To trigger: `curl -X POST http://localhost:3333 -d "CI build failed"`
|
||||
|
||||
**Note:** `pi.send()` is not supported in print mode (single-shot execution).
|
||||
**Note:** `pi.sendMessage()` is not supported in print mode (single-shot execution).
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Session File Format
|
||||
|
||||
Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field.
|
||||
Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files.
|
||||
|
||||
## File Location
|
||||
|
||||
|
|
@ -10,47 +10,66 @@ Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with
|
|||
|
||||
Where `<path>` is the working directory with `/` replaced by `-`.
|
||||
|
||||
## Session Version
|
||||
|
||||
Sessions have a version field in the header:
|
||||
|
||||
- **Version 1**: Linear entry sequence (legacy, auto-migrated on load)
|
||||
- **Version 2**: Tree structure with `id`/`parentId` linking
|
||||
|
||||
Existing v1 sessions are automatically migrated to v2 when loaded.
|
||||
|
||||
## Type Definitions
|
||||
|
||||
- [`src/session-manager.ts`](../src/session-manager.ts) - Session entry types (`SessionHeader`, `SessionMessageEntry`, etc.)
|
||||
- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AppMessage`, `Attachment`, `ThinkingLevel`
|
||||
- [`src/core/session-manager.ts`](../src/core/session-manager.ts) - Session entry types
|
||||
- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `Attachment`, `ThinkingLevel`
|
||||
- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`
|
||||
|
||||
## Entry Base
|
||||
|
||||
All entries (except `SessionHeader`) extend `SessionEntryBase`:
|
||||
|
||||
```typescript
|
||||
interface SessionEntryBase {
|
||||
type: string;
|
||||
id: string; // 8-char hex ID
|
||||
parentId: string | null; // Parent entry ID (null for first entry)
|
||||
timestamp: string; // ISO timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Entry Types
|
||||
|
||||
### SessionHeader
|
||||
|
||||
First line of the file. Defines session metadata.
|
||||
First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
|
||||
|
||||
```json
|
||||
{"type":"session","id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","provider":"anthropic","modelId":"claude-sonnet-4-5","thinkingLevel":"off"}
|
||||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
|
||||
```
|
||||
|
||||
For branched sessions, includes the source session path:
|
||||
For branched sessions (created via `/branch` command):
|
||||
|
||||
```json
|
||||
{"type":"session","id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","provider":"anthropic","modelId":"claude-sonnet-4-5","thinkingLevel":"off","branchedFrom":"/path/to/original/session.jsonl"}
|
||||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","branchedFrom":"/path/to/original/session.jsonl"}
|
||||
```
|
||||
|
||||
### SessionMessageEntry
|
||||
|
||||
A message in the conversation. The `message` field contains an `AppMessage` (see [rpc.md](./rpc.md#message-types)).
|
||||
A message in the conversation. The `message` field contains an `AgentMessage`.
|
||||
|
||||
```json
|
||||
{"type":"message","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello","timestamp":1733234567890}}
|
||||
{"type":"message","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop","timestamp":1733234567891}}
|
||||
{"type":"message","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false,"timestamp":1733234567900}}
|
||||
{"type":"message","timestamp":"2024-12-03T14:00:04.000Z","message":{"role":"bashExecution","command":"ls -la","output":"total 48\n...","exitCode":0,"cancelled":false,"truncated":false,"timestamp":1733234567950}}
|
||||
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
|
||||
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
|
||||
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}
|
||||
```
|
||||
|
||||
The `bashExecution` role is a custom message type for user-executed bash commands (via `!` in TUI or `bash` RPC command). See [rpc.md](./rpc.md#bashexecutionmessage) for the full schema.
|
||||
|
||||
### ModelChangeEntry
|
||||
|
||||
Emitted when the user switches models mid-session.
|
||||
|
||||
```json
|
||||
{"type":"model_change","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}
|
||||
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}
|
||||
```
|
||||
|
||||
### ThinkingLevelChangeEntry
|
||||
|
|
@ -58,9 +77,92 @@ Emitted when the user switches models mid-session.
|
|||
Emitted when the user changes the thinking/reasoning level.
|
||||
|
||||
```json
|
||||
{"type":"thinking_level_change","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}
|
||||
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}
|
||||
```
|
||||
|
||||
### CompactionEntry
|
||||
|
||||
Created when context is compacted. Stores a summary of earlier messages.
|
||||
|
||||
```json
|
||||
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}
|
||||
```
|
||||
|
||||
Optional fields:
|
||||
- `details`: Hook-specific data (e.g., ArtifactIndex for structured compaction)
|
||||
- `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated
|
||||
|
||||
### BranchSummaryEntry
|
||||
|
||||
Created when switching branches via `/tree`. Captures context from the abandoned path.
|
||||
|
||||
```json
|
||||
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}
|
||||
```
|
||||
|
||||
Optional fields:
|
||||
- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`)
|
||||
- `fromHook`: `true` if generated by a hook
|
||||
|
||||
### CustomEntry
|
||||
|
||||
Hook state persistence. Does NOT participate in LLM context.
|
||||
|
||||
```json
|
||||
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-hook","data":{"count":42}}
|
||||
```
|
||||
|
||||
Use `customType` to identify your hook's entries on reload.
|
||||
|
||||
### CustomMessageEntry
|
||||
|
||||
Hook-injected messages that DO participate in LLM context.
|
||||
|
||||
```json
|
||||
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-hook","content":"Injected context...","display":true}
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
|
||||
- `display`: `true` = show in TUI with purple styling, `false` = hidden
|
||||
- `details`: Optional hook-specific metadata (not sent to LLM)
|
||||
|
||||
### LabelEntry
|
||||
|
||||
User-defined bookmark/marker on an entry.
|
||||
|
||||
```json
|
||||
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}
|
||||
```
|
||||
|
||||
Set `label` to `undefined` to clear a label.
|
||||
|
||||
## Tree Structure
|
||||
|
||||
Entries form a tree:
|
||||
- First entry has `parentId: null`
|
||||
- Each subsequent entry points to its parent via `parentId`
|
||||
- Branching creates new children from an earlier entry
|
||||
- The "leaf" is the current position in the tree
|
||||
|
||||
```
|
||||
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
|
||||
│
|
||||
└─ [branch_summary] ─── [user msg] ← alternate branch
|
||||
```
|
||||
|
||||
## Context Building
|
||||
|
||||
`buildSessionContext()` walks from the current leaf to the root, producing the message list for the LLM:
|
||||
|
||||
1. Collects all entries on the path
|
||||
2. Extracts current model and thinking level settings
|
||||
3. If a `CompactionEntry` is on the path:
|
||||
- Emits the summary first
|
||||
- Then messages from `firstKeptEntryId` to compaction
|
||||
- Then messages after compaction
|
||||
4. Converts `BranchSummaryEntry` and `CustomMessageEntry` to appropriate message formats
|
||||
|
||||
## Parsing Example
|
||||
|
||||
```typescript
|
||||
|
|
@ -73,17 +175,66 @@ for (const line of lines) {
|
|||
|
||||
switch (entry.type) {
|
||||
case "session":
|
||||
console.log(`Session: ${entry.id}, Model: ${entry.provider}/${entry.modelId}`);
|
||||
console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
|
||||
break;
|
||||
case "message":
|
||||
console.log(`${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
|
||||
console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
|
||||
break;
|
||||
case "compaction":
|
||||
console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
|
||||
break;
|
||||
case "branch_summary":
|
||||
console.log(`[${entry.id}] Branch from ${entry.fromId}`);
|
||||
break;
|
||||
case "custom":
|
||||
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
|
||||
break;
|
||||
case "custom_message":
|
||||
console.log(`[${entry.id}] Hook message (${entry.customType}): ${entry.content}`);
|
||||
break;
|
||||
case "label":
|
||||
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
|
||||
break;
|
||||
case "model_change":
|
||||
console.log(`Switched to: ${entry.provider}/${entry.modelId}`);
|
||||
console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
|
||||
break;
|
||||
case "thinking_level_change":
|
||||
console.log(`Thinking: ${entry.thinkingLevel}`);
|
||||
console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SessionManager API
|
||||
|
||||
Key methods for working with sessions programmatically:
|
||||
|
||||
### Creation
|
||||
- `SessionManager.create(cwd, sessionDir?)` - New session
|
||||
- `SessionManager.open(path, sessionDir?)` - Open existing
|
||||
- `SessionManager.continueRecent(cwd, sessionDir?)` - Continue most recent or create new
|
||||
- `SessionManager.inMemory(cwd?)` - No file persistence
|
||||
|
||||
### Appending (all return entry ID)
|
||||
- `appendMessage(message)` - Add message
|
||||
- `appendThinkingLevelChange(level)` - Record thinking change
|
||||
- `appendModelChange(provider, modelId)` - Record model change
|
||||
- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
|
||||
- `appendCustomEntry(customType, data?)` - Hook state (not in context)
|
||||
- `appendCustomMessageEntry(customType, content, display, details?)` - Hook message (in context)
|
||||
- `appendLabelChange(targetId, label)` - Set/clear label
|
||||
|
||||
### Tree Navigation
|
||||
- `getLeafId()` - Current position
|
||||
- `getEntry(id)` - Get entry by ID
|
||||
- `getPath(fromId?)` - Walk from entry to root
|
||||
- `getTree()` - Get full tree structure
|
||||
- `getChildren(parentId)` - Get direct children
|
||||
- `getLabel(id)` - Get label for entry
|
||||
- `branch(entryId)` - Move leaf to earlier entry
|
||||
- `branchWithSummary(entryId, summary, details?, fromHook?)` - Branch with context summary
|
||||
|
||||
### Context
|
||||
- `buildSessionContext()` - Get messages for LLM
|
||||
- `getEntries()` - All entries (excluding header)
|
||||
- `getHeader()` - Session metadata
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue