mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 19:05:11 +00:00
Custom tools with session lifecycle, examples for hooks and tools
- Custom tools: TypeScript modules that extend pi with new tools - Custom TUI rendering via renderCall/renderResult - User interaction via pi.ui (select, confirm, input, notify) - Session lifecycle via onSession callback for state reconstruction - Examples: todo.ts, question.ts, hello.ts - Hook examples: permission-gate, git-checkpoint, protected-paths - Session lifecycle centralized in AgentSession - Works across all modes (interactive, print, RPC) - Unified session event for hooks (replaces session_start/session_switch) - Box component added to pi-tui - Examples bundled in npm and binary releases Fixes #190
This commit is contained in:
parent
295f51b53f
commit
e7097d911a
33 changed files with 1926 additions and 117 deletions
|
|
@ -43,8 +43,8 @@ A hook is a TypeScript file that exports a default function. The function receiv
|
|||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_start", async (event, ctx) => {
|
||||
ctx.ui.notify(`Session: ${ctx.sessionFile ?? "ephemeral"}`, "info");
|
||||
pi.on("session", async (event, ctx) => {
|
||||
ctx.ui.notify(`Session ${event.reason}: ${ctx.sessionFile ?? "ephemeral"}`, "info");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
|
@ -76,7 +76,7 @@ Hooks are loaded using [jiti](https://github.com/unjs/jiti), so TypeScript works
|
|||
```
|
||||
pi starts
|
||||
│
|
||||
├─► session_start
|
||||
├─► session (reason: "start")
|
||||
│
|
||||
▼
|
||||
user sends prompt ─────────────────────────────────────────┐
|
||||
|
|
@ -98,36 +98,54 @@ user sends prompt ────────────────────
|
|||
│
|
||||
user sends another prompt ◄────────────────────────────────┘
|
||||
|
||||
user branches or switches session
|
||||
user branches (/branch)
|
||||
│
|
||||
└─► session_switch
|
||||
├─► branch (BEFORE branch, can control)
|
||||
└─► session (reason: "switch", AFTER branch)
|
||||
|
||||
user switches session (/session)
|
||||
│
|
||||
└─► session (reason: "switch")
|
||||
|
||||
user clears session (/clear)
|
||||
│
|
||||
└─► session (reason: "clear")
|
||||
```
|
||||
|
||||
A **turn** is one LLM response plus any tool calls. Complex tasks loop through multiple turns until the LLM responds without calling tools.
|
||||
|
||||
### session_start
|
||||
### session
|
||||
|
||||
Fired once when pi starts.
|
||||
Fired on startup and when session changes.
|
||||
|
||||
```typescript
|
||||
pi.on("session_start", async (event, ctx) => {
|
||||
// ctx.sessionFile: string | null
|
||||
// ctx.hasUI: boolean
|
||||
pi.on("session", async (event, ctx) => {
|
||||
// event.entries: SessionEntry[] - all session entries
|
||||
// event.sessionFile: string | null - current session file
|
||||
// event.previousSessionFile: string | null - previous session file
|
||||
// event.reason: "start" | "switch" | "clear"
|
||||
});
|
||||
```
|
||||
|
||||
### session_switch
|
||||
**Reasons:**
|
||||
- `start`: Initial session load on startup
|
||||
- `switch`: User switched sessions (`/session`) or branched (`/branch`)
|
||||
- `clear`: User cleared the session (`/clear`)
|
||||
|
||||
Fired when session changes (`/branch` or session switch).
|
||||
### branch
|
||||
|
||||
Fired BEFORE a branch happens. Can control branch behavior.
|
||||
|
||||
```typescript
|
||||
pi.on("session_switch", async (event, ctx) => {
|
||||
// event.newSessionFile: string | null (null in --no-session mode)
|
||||
// event.previousSessionFile: string | null (null in --no-session mode)
|
||||
// event.reason: "branch" | "switch"
|
||||
pi.on("branch", async (event, ctx) => {
|
||||
// event.targetTurnIndex: number
|
||||
// event.entries: SessionEntry[]
|
||||
return { skipConversationRestore: true }; // or undefined
|
||||
});
|
||||
```
|
||||
|
||||
Note: After branch completes, a `session` event fires with `reason: "switch"`.
|
||||
|
||||
### agent_start / agent_end
|
||||
|
||||
Fired once per user prompt.
|
||||
|
|
@ -192,18 +210,6 @@ pi.on("tool_result", async (event, ctx) => {
|
|||
});
|
||||
```
|
||||
|
||||
### branch
|
||||
|
||||
Fired when user branches via `/branch`.
|
||||
|
||||
```typescript
|
||||
pi.on("branch", async (event, ctx) => {
|
||||
// event.targetTurnIndex: number
|
||||
// event.entries: SessionEntry[]
|
||||
return { skipConversationRestore: true }; // or undefined
|
||||
});
|
||||
```
|
||||
|
||||
## Context API
|
||||
|
||||
Every event handler receives a context object with these methods:
|
||||
|
|
@ -309,7 +315,9 @@ import * as fs from "node:fs";
|
|||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_start", async (event, ctx) => {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "start") return;
|
||||
|
||||
// Watch a trigger file
|
||||
const triggerFile = "/tmp/agent-trigger.txt";
|
||||
|
||||
|
|
@ -339,7 +347,9 @@ import * as http from "node:http";
|
|||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_start", async (event, ctx) => {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "start") return;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
let body = "";
|
||||
req.on("data", chunk => body += chunk);
|
||||
|
|
@ -563,7 +573,7 @@ Key behaviors:
|
|||
Mode initialization:
|
||||
-> hookRunner.setUIContext(ctx, hasUI)
|
||||
-> hookRunner.setSessionFile(path)
|
||||
-> hookRunner.emit({ type: "session_start" })
|
||||
-> hookRunner.emit({ type: "session", reason: "start", ... })
|
||||
|
||||
User sends prompt:
|
||||
-> AgentSession.prompt()
|
||||
|
|
@ -581,9 +591,19 @@ User sends prompt:
|
|||
-> [repeat if more tool calls]
|
||||
-> hookRunner.emit({ type: "agent_end", messages })
|
||||
|
||||
Branch or session switch:
|
||||
-> AgentSession.branch() or AgentSession.switchSession()
|
||||
-> hookRunner.emit({ type: "session_switch", ... })
|
||||
Branch:
|
||||
-> AgentSession.branch()
|
||||
-> hookRunner.emit({ type: "branch", ... }) # BEFORE branch
|
||||
-> [branch happens]
|
||||
-> hookRunner.emit({ type: "session", reason: "switch", ... }) # AFTER
|
||||
|
||||
Session switch:
|
||||
-> AgentSession.switchSession()
|
||||
-> hookRunner.emit({ type: "session", reason: "switch", ... })
|
||||
|
||||
Clear:
|
||||
-> AgentSession.reset()
|
||||
-> hookRunner.emit({ type: "session", reason: "clear", ... })
|
||||
```
|
||||
|
||||
## UI Context by Mode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue