@ssweens/pi-handoff 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ssweens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # pi-handoff
2
+
3
+ ![pi-handoff command](screenshot.png)
4
+
5
+ ```bash
6
+ pi install @ssweens/pi-handoff
7
+ ```
8
+
9
+ Context handoff extension for [pi](https://github.com/badlogic/pi-mono). Transfer context to a new session with a structured summary — the agent can trigger handoffs, or they happen automatically on compaction.
10
+
11
+ ## Features
12
+
13
+ - **User preview/editing** — Review and edit the handoff draft before submission
14
+ - **Agent-callable handoff tool** — The model can initiate handoffs when explicitly asked
15
+ - **Auto-handoff on compaction** — Uses Pi's preparation data so summaries won't overflow
16
+ - **Structured format** — Bullet list with code pointers (path:line or path#Symbol)
17
+ - **Parent session query** — `session_query` tool for looking up details from parent sessions
18
+ - **Auto-inject skill** — Detects `Parent session:` references and enables query instructions automatically
19
+ - **System prompt hints** — The model knows about handoffs and suggests them proactively
20
+ - **Session naming** — New sessions named based on handoff goal
21
+
22
+ ## Installation
23
+
24
+ ### From npm
25
+
26
+ ```bash
27
+ pi install @ssweens/pi-handoff
28
+ ```
29
+
30
+ ### From git (global)
31
+
32
+ ```bash
33
+ pi install git:github.com/ssweens/pi-handoff
34
+ ```
35
+
36
+ ### From git (project-local)
37
+
38
+ ```bash
39
+ pi install -l git:github.com/ssweens/pi-handoff
40
+ ```
41
+
42
+ ### Try without installing
43
+
44
+ ```bash
45
+ pi -e git:github.com/ssweens/pi-handoff
46
+ ```
47
+
48
+ ### From local path (development)
49
+
50
+ Add to your settings (`~/.pi/agent/settings.json` or `.pi/settings.json`):
51
+
52
+ ```json
53
+ {
54
+ "packages": [
55
+ "/path/to/pi-handoff"
56
+ ]
57
+ }
58
+ ```
59
+
60
+ ## Features
61
+
62
+ ### `/handoff <goal>` — Context Transfer
63
+
64
+ When your conversation gets long or you want to branch off to a focused task:
65
+
66
+ ```
67
+ /handoff now implement this for teams as well
68
+ /handoff execute phase one of the plan
69
+ /handoff check other places that need this fix
70
+ ```
71
+
72
+ This:
73
+ 1. Analyzes your current conversation
74
+ 2. Generates a structured handoff summary with:
75
+ - Key decisions and approaches (as bullet points)
76
+ - Relevant files with code pointers
77
+ - Clear task description based on your goal
78
+ 3. Opens an editor for you to review/modify the draft
79
+ 4. Creates a new session with parent tracking
80
+ 5. Sets up the prompt ready to submit
81
+
82
+ ### Agent-Initiated Handoff
83
+
84
+ The model can also create handoffs when you explicitly ask:
85
+
86
+ ```
87
+ "Please hand this off to a new session to implement the fix"
88
+ "Create a handoff to execute phase one"
89
+ ```
90
+
91
+ The agent uses the `handoff` tool, which defers the handoff until after the current turn completes (so the tool result is properly recorded in the old session). In tool/hook contexts, it creates a new session file and rebases the active agent context to start at the handoff prompt, so the next turn runs with the handed-off context instead of the old oversized history.
92
+
93
+ ### System Prompt Awareness
94
+
95
+ The extension injects handoff awareness into the system prompt. The model knows:
96
+ - `/handoff` exists and when to suggest it
97
+ - Handoffs after planning sessions are especially effective — clear context and start fresh with the plan
98
+ - At high context usage, it should suggest a handoff rather than losing context
99
+
100
+ ### Auto-Handoff on Compaction
101
+
102
+ When auto-compaction triggers (context exceeds the compaction threshold), the extension intercepts and offers a choice: **handoff to a new session** or **compact in place**.
103
+
104
+ If you choose handoff:
105
+ 1. A summary is generated (same structured format as `/handoff`)
106
+ 2. You review/edit the handoff prompt
107
+ 3. A new session is created with the summary, old session preserved
108
+ 4. The agent continues in the new session
109
+
110
+ If you decline, normal compaction proceeds as usual.
111
+
112
+ **Requires `compaction.enabled = true`** (the default). When auto-compaction is disabled, this hook never fires — use `/handoff` manually instead.
113
+
114
+ ### `session_query` Tool — Cross-Session Context
115
+
116
+ The model can query parent sessions for details not in the handoff summary:
117
+
118
+ ```typescript
119
+ session_query("/path/to/parent/session.jsonl", "What files were modified?")
120
+ session_query("/path/to/parent/session.jsonl", "What approach was chosen for authentication?")
121
+ ```
122
+
123
+ **Auto-injection:** When a user message contains a `**Parent session:**` reference, the extension prepends `/skill:pi-session-query` inline with the prompt body (single-submit flow, no extra Enter round-trip). No manual directive needed in handoff prompts.
124
+
125
+ **Size guard:** Large parent sessions are truncated (keeping the most recent context) to prevent exceeding context limits during the query.
126
+
127
+ ## Handoff Format
128
+
129
+ Generated handoffs follow a structured format adapted from Pi's compaction system, filtered through the user's stated goal:
130
+
131
+ ```markdown
132
+ # <goal>
133
+
134
+ **Parent session:** `/path/to/session.jsonl`
135
+
136
+ ## Goal
137
+ What the user wants to accomplish in the new thread.
138
+
139
+ ## Key Decisions
140
+ - **Decision 1**: Rationale (path/to/file.ts:42)
141
+ - **Decision 2**: Rationale
142
+
143
+ ## Constraints & Preferences
144
+ - Requirements or preferences the user stated
145
+
146
+ ## Progress
147
+ ### Done
148
+ - [x] Completed work relevant to the goal
149
+
150
+ ### In Progress
151
+ - [ ] Partially completed work
152
+
153
+ ### Blocked
154
+ - Open issues or blockers
155
+
156
+ ## Files
157
+ - path/to/file1.ts (modified)
158
+ - path/to/file2.ts (read)
159
+
160
+ ## Task
161
+ Clear, actionable next steps based on the goal.
162
+ ```
163
+
164
+ The `/skill:pi-session-query` directive is auto-injected when this prompt is submitted (detected via the `**Parent session:**` marker).
165
+
166
+ ## Architecture Comparison
167
+
168
+ | Feature | pi-amplike | mina | pi-handoff |
169
+ |---------|-----------|------|------------|
170
+ | `/handoff` command | ✅ | ✅ | ✅ |
171
+ | Agent-callable tool | ✅ | ❌ | ✅ |
172
+ | User preview/edit | ❌ | ✅ | ✅ |
173
+ | Auto-handoff on compact | ❌ | ❌ | ✅ |
174
+ | Parent query tool | ✅ | ✅ | ✅ |
175
+ | Structured bullets | ❌ | ✅ | ✅ |
176
+ | Code pointers | ❌ | ✅ | ✅ |
177
+ | Auto-detect parent ref | ❌ | ✅ | ✅ |
178
+ | System prompt hints | ❌ | ✅ | ✅ |
179
+ | Session naming | ❌ | ❌ | ✅ |
180
+ | Query size guard | ❌ | ✅ | ✅ |
181
+ | Deferred tool switch | ✅ | N/A | ✅ |
182
+
183
+ ## How It Differs from Compaction
184
+
185
+ | | Compaction (`/compact`) | Handoff (`/handoff`) | Auto-Handoff |
186
+ |---|---|---|---|
187
+ | **Purpose** | Reduce context size | Transfer to focused task | Context full → new session |
188
+ | **Trigger** | Automatic or `/compact` | User types `/handoff` | Intercepts auto-compaction |
189
+ | **Continues** | Same session | New session | New session |
190
+ | **Context** | Lossy summary | Goal-directed summary | Goal-directed summary |
191
+ | **Parent access** | Lost | Queryable via `session_query` | Queryable via `session_query` |
192
+ | **Use case** | General context overflow | Task branching | Preserve old session on overflow |
193
+
194
+ ## Session Navigation
195
+
196
+ Use pi's built-in `/resume` command to switch between sessions. Handoff creates sessions with descriptive names based on your goal.
197
+
198
+ ## Components
199
+
200
+ | Component | Type | Description |
201
+ |-----------|------|-------------|
202
+ | [handoff.ts](extensions/handoff.ts) | Extension | `/handoff` command, `handoff` tool, auto-handoff on compaction, system prompt hints |
203
+ | [session-query.ts](extensions/session-query.ts) | Extension | `session_query` tool for the model (with size guard) |
204
+ | [pi-session-query/SKILL.md](skills/pi-session-query/SKILL.md) | Skill | Instructions for using `session_query` |
205
+
206
+ ## Configuration
207
+
208
+ No configuration required. The extension uses your current model for both handoff generation and session queries.
209
+
210
+ ### Optional: Dedicated Query Model
211
+
212
+ To use a smaller/faster model for session queries (reducing cost), you can modify `session-query.ts` to use a different model:
213
+
214
+ ```typescript
215
+ // In session-query.ts execute function, replace:
216
+ const model = ctx.model;
217
+
218
+ // With a specific model lookup:
219
+ const model = ctx.modelRegistry.find("anthropic", "claude-3-haiku") ?? ctx.model;
220
+ ```
221
+
222
+ ## License
223
+
224
+ MIT
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Handoff Extension
3
+ *
4
+ * Transfers conversation context to a new focused session via:
5
+ * - /handoff <goal> command
6
+ * - Agent-callable handoff tool
7
+ * - Auto-handoff option when Pi triggers compaction
8
+ *
9
+ * The compaction hook uses Pi's preparation data (messagesToSummarize,
10
+ * previousSummary) instead of the full conversation, so the summary
11
+ * generation won't overflow the context window.
12
+ *
13
+ * Usage:
14
+ * /handoff now implement this for teams as well
15
+ * /handoff execute phase one of the plan
16
+ * /handoff check other places that need this fix
17
+ *
18
+ * The generated prompt appears as a draft in the editor for review/editing.
19
+ * The agent can also invoke the handoff tool when the user explicitly requests it.
20
+ */
21
+
22
+ import { complete, type Message } from "@mariozechner/pi-ai";
23
+ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@mariozechner/pi-coding-agent";
24
+ import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
25
+ import { Type } from "@sinclair/typebox";
26
+
27
+ // Store pending handoff text to be set in new session after switch
28
+ // Key: parent session file path, Value: handoff text to set in editor
29
+ const pendingHandoffText = new Map<string, string>();
30
+
31
+ // Handoff generation system prompt.
32
+ //
33
+ // Combines Pi's structured compaction format (Goal, Progress, Decisions,
34
+ // Constraints) with handoff-specific goal filtering, code pointers from
35
+ // mina, and an explicit Task section.
36
+ //
37
+ // Key differences from Pi compaction:
38
+ // - Goal-directed: everything is filtered through the user's stated goal
39
+ // - Code pointers: path:line and path#Symbol references in context
40
+ // - Task section: actionable next steps framed by the goal
41
+ // - Anti-continuation guard: prevent the summarizer from responding to the history
42
+ const SYSTEM_PROMPT = `You are a context transfer assistant. Read the conversation and produce a structured handoff summary for the stated goal. The new thread must be able to proceed without the old conversation.
43
+
44
+ Do NOT continue the conversation. Do NOT respond to any questions in the history. ONLY output the structured summary.
45
+
46
+ Use this EXACT format:
47
+
48
+ ## Goal
49
+ [The user's goal for the new thread — what they want to accomplish.]
50
+
51
+ ## Key Decisions
52
+ - **[Decision]**: [Brief rationale]
53
+ - Use code pointers (path/to/file.ts:42 or path/to/file.ts#functionName) where relevant
54
+
55
+ ## Constraints & Preferences
56
+ - [Any requirements, constraints, or preferences the user stated]
57
+ - [Or "(none)" if none were mentioned]
58
+
59
+ ## Progress
60
+ ### Done
61
+ - [x] [Completed work relevant to the goal]
62
+
63
+ ### In Progress
64
+ - [ ] [Partially completed work]
65
+
66
+ ### Blocked
67
+ - [Open issues or blockers, if any]
68
+
69
+ ## Files
70
+ - path/to/file1.ts (modified)
71
+ - path/to/file2.ts (read)
72
+
73
+ ## Task
74
+ [Clear, actionable description of what to do next based on the goal. Ordered steps if appropriate.]
75
+
76
+ Rules:
77
+ - Be concise. Every bullet earns its place.
78
+ - Preserve exact file paths, function names, and error messages.
79
+ - Only include information relevant to the stated goal — discard unrelated context.
80
+ - Output the formatted content only. No preamble, no filler.`;
81
+
82
+ // System prompt fragment injected via before_agent_start.
83
+ // Teaches the model about handoffs so it can suggest them proactively.
84
+ const HANDOFF_SYSTEM_HINT = `
85
+ ## Handoff
86
+
87
+ Use \`/handoff <goal>\` to transfer context to a new focused session.
88
+ Handoffs are especially effective after planning — clear the context and start a new session with the plan you just created.
89
+ At high context usage, suggest a handoff rather than losing important context.`;
90
+
91
+ /**
92
+ * Generate a session name from the goal (slug format)
93
+ */
94
+ function goalToSessionName(goal: string): string {
95
+ return goal
96
+ .toLowerCase()
97
+ .replace(/[^a-z0-9\s-]/g, "")
98
+ .trim()
99
+ .replace(/\s+/g, "-")
100
+ .slice(0, 50);
101
+ }
102
+
103
+ /**
104
+ * Handoff modes:
105
+ * - "command": User-initiated via /handoff
106
+ * - "tool": Agent-initiated via handoff tool
107
+ * - "compactHook": Triggered from session_before_compact
108
+ *
109
+ * All modes follow the same flow: generate summary → editor review → new session → input box → user sends
110
+ */
111
+ type HandoffMode = "command" | "tool" | "compactHook";
112
+
113
+ /**
114
+ * Core handoff logic shared by the /handoff command, the handoff tool,
115
+ * and the auto-handoff compaction hook.
116
+ *
117
+ * Returns an error string on failure, or undefined on success.
118
+ */
119
+ async function performHandoff(
120
+ pi: ExtensionAPI,
121
+ ctx: ExtensionContext,
122
+ goal: string,
123
+ mode: HandoffMode = "command",
124
+ preBuiltContext?: string,
125
+ ): Promise<string | undefined> {
126
+ if (!ctx.hasUI) {
127
+ return "Handoff requires interactive mode.";
128
+ }
129
+
130
+ if (!ctx.model) {
131
+ return "No model selected.";
132
+ }
133
+
134
+ let conversationText: string;
135
+
136
+ if (preBuiltContext) {
137
+ // compactHook: context already built from preparation data
138
+ conversationText = preBuiltContext;
139
+ } else {
140
+ // command/tool: gather full conversation (context isn't full yet)
141
+ const branch = ctx.sessionManager.getBranch();
142
+ const messages = branch
143
+ .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
144
+ .map((entry) => entry.message);
145
+
146
+ if (messages.length === 0) {
147
+ return "No conversation to hand off.";
148
+ }
149
+
150
+ conversationText = serializeConversation(convertToLlm(messages));
151
+ }
152
+
153
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
154
+
155
+ // Generate the handoff prompt with loader UI
156
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
157
+ const loader = new BorderedLoader(tui, theme, `Generating handoff summary...`);
158
+ loader.onAbort = () => done(null);
159
+
160
+ const doGenerate = async () => {
161
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
162
+
163
+ const userMessage: Message = {
164
+ role: "user",
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: `## Conversation History\n\n${conversationText}\n\n## Goal for New Thread\n\n${goal}`,
169
+ },
170
+ ],
171
+ timestamp: Date.now(),
172
+ };
173
+
174
+ const response = await complete(
175
+ ctx.model!,
176
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
177
+ { apiKey, signal: loader.signal },
178
+ );
179
+
180
+ if (response.stopReason === "aborted") {
181
+ return null;
182
+ }
183
+
184
+ return response.content
185
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
186
+ .map((c) => c.text)
187
+ .join("\n");
188
+ };
189
+
190
+ doGenerate()
191
+ .then(done)
192
+ .catch((err) => {
193
+ console.error("Handoff generation failed:", err);
194
+ done(null);
195
+ });
196
+
197
+ return loader;
198
+ });
199
+
200
+ if (result === null) {
201
+ return "Handoff cancelled.";
202
+ }
203
+
204
+ // Build the full prompt with parent reference
205
+ let fullPrompt = `# ${goal}\n\n`;
206
+
207
+ if (currentSessionFile) {
208
+ fullPrompt += `**Parent session:** \`${currentSessionFile}\`\n\n`;
209
+ }
210
+
211
+ fullPrompt += result;
212
+
213
+ // Prepend session-query skill if parent session present
214
+ const messageToSend = /\*\*Parent session:\*\*/.test(fullPrompt)
215
+ ? `/skill:pi-session-query ${fullPrompt}`
216
+ : fullPrompt;
217
+
218
+ // Store the handoff text for the session_switch event to pick up
219
+ // We use the parent session file as key since that's what we pass to newSession
220
+ if (currentSessionFile) {
221
+ pendingHandoffText.set(currentSessionFile, messageToSend);
222
+ }
223
+
224
+ // Create new session immediately
225
+ // Use ctx.newSession if available (command mode), otherwise use sessionManager directly
226
+ if ("newSession" in ctx && typeof ctx.newSession === "function") {
227
+ const newSessionResult = await ctx.newSession({
228
+ parentSession: currentSessionFile,
229
+ });
230
+
231
+ if (newSessionResult.cancelled) {
232
+ // Clean up pending text if cancelled
233
+ if (currentSessionFile) {
234
+ pendingHandoffText.delete(currentSessionFile);
235
+ }
236
+ return "New session cancelled.";
237
+ }
238
+ } else {
239
+ // Tool/hook contexts: create session directly via session manager
240
+ const sessionManager = ctx.sessionManager as any;
241
+ sessionManager.newSession({ parentSession: currentSessionFile });
242
+ }
243
+
244
+ pi.setSessionName(goalToSessionName(goal));
245
+
246
+ return undefined;
247
+ }
248
+
249
+ export default function (pi: ExtensionAPI) {
250
+ // --- Session switch handler ---
251
+ // When switching to a new session (e.g., after handoff), check if there's
252
+ // pending handoff text to set in the editor.
253
+ pi.on("session_switch", async (event, ctx) => {
254
+ if (event.reason !== "new" || !ctx.hasUI) return;
255
+
256
+ // Get the parent session from the session header
257
+ const header = ctx.sessionManager.getHeader();
258
+ const parentSession = header?.parentSession;
259
+ if (!parentSession) return;
260
+
261
+ // Check if there's pending handoff text for this parent session
262
+ const text = pendingHandoffText.get(parentSession);
263
+ if (text) {
264
+ ctx.ui.setEditorText(text);
265
+ ctx.ui.notify("Handoff ready - edit if needed and press Enter to send", "info");
266
+ pendingHandoffText.delete(parentSession);
267
+ }
268
+ });
269
+
270
+ // --- System prompt hint ---
271
+ // Inject handoff awareness into the system prompt so the model
272
+ // can proactively suggest handoffs at high context usage.
273
+ pi.on("before_agent_start", async (event, _ctx) => {
274
+ return {
275
+ systemPrompt: event.systemPrompt + HANDOFF_SYSTEM_HINT,
276
+ };
277
+ });
278
+
279
+ // --- Auto-handoff on compaction ---
280
+ // When auto-compaction triggers, offer handoff as an alternative.
281
+ // Uses event.preparation (messagesToSummarize, previousSummary) — the
282
+ // manageable subset Pi already prepared — instead of re-gathering the
283
+ // full conversation that caused the compaction in the first place.
284
+ pi.on("session_before_compact", async (event, ctx) => {
285
+ if (!ctx.hasUI || !ctx.model) return;
286
+
287
+ // Skip if a handoff was just initiated - the new session is already being created
288
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
289
+ if (currentSessionFile && pendingHandoffText.has(currentSessionFile)) {
290
+ return;
291
+ }
292
+
293
+ const usage = ctx.getContextUsage();
294
+ const pctStr = usage ? `${Math.round(usage.percent)}%` : "high";
295
+
296
+ const choice = await ctx.ui.select(
297
+ `Context is ${pctStr} full. What would you like to do?`,
298
+ ["Handoff to new session", "Compact context", "Continue without either"],
299
+ );
300
+
301
+ if (choice === "Compact context" || choice === undefined) return;
302
+ if (choice === "Continue without either") return { cancel: true };
303
+
304
+ // Build context from preparation data — already the right subset
305
+ const { preparation } = event;
306
+ const conversationText = serializeConversation(
307
+ convertToLlm(preparation.messagesToSummarize),
308
+ );
309
+
310
+ let contextForHandoff = "";
311
+ if (preparation.previousSummary) {
312
+ contextForHandoff += `## Previous Context\n\n${preparation.previousSummary}\n\n`;
313
+ }
314
+ contextForHandoff += `## Recent Conversation\n\n${conversationText}`;
315
+
316
+ const error = await performHandoff(pi, ctx, "Continue current work", "compactHook", contextForHandoff);
317
+ if (error) {
318
+ ctx.ui.notify(`Handoff failed: ${error}. Compacting instead.`, "warning");
319
+ return;
320
+ }
321
+
322
+ return { cancel: true };
323
+ });
324
+
325
+ // --- /handoff command ---
326
+ pi.registerCommand("handoff", {
327
+ description: "Transfer context to a new focused session",
328
+ handler: async (args, ctx) => {
329
+ const goal = args.trim();
330
+ if (!goal) {
331
+ ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
332
+ return;
333
+ }
334
+
335
+ const error = await performHandoff(pi, ctx, goal);
336
+ if (error) {
337
+ ctx.ui.notify(error, "error");
338
+ }
339
+ },
340
+ });
341
+
342
+ // --- handoff tool (agent-callable) ---
343
+ pi.registerTool({
344
+ name: "handoff",
345
+ label: "Handoff",
346
+ description:
347
+ "Transfer context to a new focused session. ONLY use this when the user explicitly asks for a handoff. Provide a goal describing what the new session should focus on.",
348
+ parameters: Type.Object({
349
+ goal: Type.String({ description: "The goal/task for the new session" }),
350
+ }),
351
+
352
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
353
+ const error = await performHandoff(pi, ctx, params.goal, "tool");
354
+ return {
355
+ content: [
356
+ {
357
+ type: "text" as const,
358
+ text: error ?? "Handoff queued. Switching to a new session with the generated prompt.",
359
+ },
360
+ ],
361
+ };
362
+ },
363
+ });
364
+
365
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Session Query Extension - Query previous pi sessions
3
+ *
4
+ * Provides a tool the model can use to query past sessions for context,
5
+ * decisions, code changes, or other information.
6
+ *
7
+ * Works with handoff: when a handoff prompt includes "Parent session: <path>",
8
+ * the model can use this tool to look up details from that session.
9
+ *
10
+ * Based on pi-amplike's session-query, enhanced with:
11
+ * - Better error handling
12
+ * - Rendered results with markdown support
13
+ * - Session metadata in response
14
+ */
15
+
16
+ import { complete, type Message } from "@mariozechner/pi-ai";
17
+ import type { ExtensionAPI, SessionEntry } from "@mariozechner/pi-coding-agent";
18
+ import {
19
+ SessionManager,
20
+ convertToLlm,
21
+ serializeConversation,
22
+ getMarkdownTheme,
23
+ } from "@mariozechner/pi-coding-agent";
24
+ import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
25
+ import { Type } from "@sinclair/typebox";
26
+ import * as fs from "node:fs";
27
+
28
+ // Maximum characters of serialized conversation to send to the query LLM.
29
+ // Prevents blowing context when the parent session is very large.
30
+ // ~100k chars ≈ ~25-30k tokens for most models — leaves room for the
31
+ // question, system prompt, and answer within a 128k context window.
32
+ const MAX_SESSION_CHARS = 100_000;
33
+
34
+ const QUERY_SYSTEM_PROMPT = `Extract information relevant to the question from the session history.
35
+ Return a concise answer using bullet points where appropriate.
36
+ Use code pointers (path/to/file.ts:42 or path/to/file.ts#functionName) when referencing specific code.
37
+ If the information is not in the session, say so clearly.`;
38
+
39
+ export default function (pi: ExtensionAPI) {
40
+ pi.registerTool({
41
+ name: "session_query",
42
+ label: (params) => `Query Session: ${params.question}`,
43
+ description:
44
+ "Query a previous pi session file for context, decisions, or information. Use when you need to look up what happened in a parent session or any other session. The sessionPath should be the full path to a .jsonl session file.",
45
+
46
+ parameters: Type.Object({
47
+ sessionPath: Type.String({
48
+ description:
49
+ "Full path to the session file (e.g., /home/user/.pi/agent/sessions/.../session.jsonl)",
50
+ }),
51
+ question: Type.String({
52
+ description:
53
+ "What you want to know about that session (e.g., 'What files were modified?' or 'What approach was chosen?')",
54
+ }),
55
+ }),
56
+
57
+ renderResult(result, _options, theme) {
58
+ const container = new Container();
59
+
60
+ if (result.content && result.content[0]?.text) {
61
+ const text = result.content[0].text;
62
+
63
+ // Check for error format
64
+ if (result.details?.error) {
65
+ container.addChild(new Text(theme.fg("error", text), 0, 0));
66
+ return container;
67
+ }
68
+
69
+ // Parse structured response: **Query:** question\n\n---\n\nanswer
70
+ const match = text.match(/\*\*Query:\*\* (.+?)\n\n---\n\n([\s\S]+)/);
71
+
72
+ if (match) {
73
+ const [, query, answer] = match;
74
+ container.addChild(new Text(theme.bold("Query: ") + theme.fg("accent", query), 0, 0));
75
+ container.addChild(new Spacer(1));
76
+ // Render the answer as markdown
77
+ container.addChild(
78
+ new Markdown(answer.trim(), 0, 0, getMarkdownTheme(), {
79
+ color: (text: string) => theme.fg("toolOutput", text),
80
+ }),
81
+ );
82
+ } else {
83
+ // Fallback for other formats
84
+ container.addChild(new Text(theme.fg("toolOutput", text), 0, 0));
85
+ }
86
+
87
+ // Show metadata if available
88
+ if (result.details?.messageCount) {
89
+ const truncNote = result.details.truncated ? ", truncated" : "";
90
+ container.addChild(new Spacer(1));
91
+ container.addChild(
92
+ new Text(
93
+ theme.fg("dim", `(${result.details.messageCount} messages in session${truncNote})`),
94
+ 0,
95
+ 0,
96
+ ),
97
+ );
98
+ }
99
+ }
100
+
101
+ return container;
102
+ },
103
+
104
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
105
+ const { sessionPath, question } = params;
106
+
107
+ // Helper for error returns
108
+ const errorResult = (text: string) => ({
109
+ content: [{ type: "text" as const, text }],
110
+ details: { error: true },
111
+ });
112
+
113
+ // Validate session path
114
+ if (!sessionPath.endsWith(".jsonl")) {
115
+ return errorResult(
116
+ `Error: Invalid session path. Expected a .jsonl file, got: ${sessionPath}`,
117
+ );
118
+ }
119
+
120
+ // Check if file exists
121
+ if (!fs.existsSync(sessionPath)) {
122
+ return errorResult(`Error: Session file not found: ${sessionPath}`);
123
+ }
124
+
125
+ onUpdate?.({
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `Querying session: ${question}`,
130
+ },
131
+ ],
132
+ details: { status: "loading", question },
133
+ });
134
+
135
+ // Load the session
136
+ let sessionManager: SessionManager;
137
+ try {
138
+ sessionManager = SessionManager.open(sessionPath);
139
+ } catch (err) {
140
+ return errorResult(`Error loading session: ${err}`);
141
+ }
142
+
143
+ // Get conversation from the session
144
+ const branch = sessionManager.getBranch();
145
+ const messages = branch
146
+ .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
147
+ .map((entry) => entry.message);
148
+
149
+ if (messages.length === 0) {
150
+ return {
151
+ content: [{ type: "text" as const, text: "Session is empty - no messages found." }],
152
+ details: { empty: true },
153
+ };
154
+ }
155
+
156
+ // Serialize the conversation, truncating if too large
157
+ const llmMessages = convertToLlm(messages);
158
+ let conversationText = serializeConversation(llmMessages);
159
+ let truncated = false;
160
+
161
+ if (conversationText.length > MAX_SESSION_CHARS) {
162
+ // Keep the tail (most recent context) — more likely to be relevant
163
+ conversationText = "…[earlier messages truncated]…\n\n"
164
+ + conversationText.slice(-MAX_SESSION_CHARS);
165
+ truncated = true;
166
+ }
167
+
168
+ // Use LLM to answer the question
169
+ if (!ctx.model) {
170
+ return errorResult("Error: No model available to analyze the session.");
171
+ }
172
+
173
+ try {
174
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
175
+
176
+ const userMessage: Message = {
177
+ role: "user",
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: `## Session Conversation\n\n${conversationText}\n\n## Question\n\n${question}`,
182
+ },
183
+ ],
184
+ timestamp: Date.now(),
185
+ };
186
+
187
+ const response = await complete(
188
+ ctx.model,
189
+ { systemPrompt: QUERY_SYSTEM_PROMPT, messages: [userMessage] },
190
+ { apiKey, signal },
191
+ );
192
+
193
+ if (response.stopReason === "aborted") {
194
+ return {
195
+ content: [{ type: "text" as const, text: "Query was cancelled." }],
196
+ details: { cancelled: true },
197
+ };
198
+ }
199
+
200
+ const answer = response.content
201
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
202
+ .map((c) => c.text)
203
+ .join("\n");
204
+
205
+ return {
206
+ content: [{ type: "text" as const, text: `**Query:** ${question}\n\n---\n\n${answer}` }],
207
+ details: {
208
+ sessionPath,
209
+ question,
210
+ messageCount: messages.length,
211
+ truncated,
212
+ },
213
+ };
214
+ } catch (err) {
215
+ return errorResult(`Error querying session: ${err}`);
216
+ }
217
+ },
218
+ });
219
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ssweens/pi-handoff",
3
+ "version": "1.0.0",
4
+ "description": "Enhanced handoff extension for pi - context management for agentic coding workflows",
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "author": "ssweens",
11
+ "files": [
12
+ "extensions/",
13
+ "skills/",
14
+ "README.md",
15
+ "LICENSE",
16
+ "screenshot.png"
17
+ ],
18
+ "pi": {
19
+ "extensions": [
20
+ "extensions"
21
+ ],
22
+ "skills": [
23
+ "skills"
24
+ ]
25
+ },
26
+ "peerDependencies": {
27
+ "@mariozechner/pi-ai": "*",
28
+ "@mariozechner/pi-coding-agent": "*",
29
+ "@mariozechner/pi-tui": "*",
30
+ "@sinclair/typebox": "*"
31
+ }
32
+ }
package/screenshot.png ADDED
Binary file
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: pi-session-query
3
+ description: Query previous pi sessions to retrieve context, decisions, code changes, or other information. Use when you need to look up what happened in a parent session or any other session file.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Pi Session Query
8
+
9
+ Query pi session files to retrieve context from past conversations.
10
+
11
+ This skill is automatically invoked in handed-off sessions when you need to look up details from the parent session.
12
+
13
+ ## When to Use
14
+
15
+ - When the handoff summary references a "Parent session" path
16
+ - When you need specific details not included in the handoff summary
17
+ - When you need to verify a decision or approach from the parent session
18
+ - When you need file paths or code snippets from earlier work
19
+
20
+ ## Usage
21
+
22
+ Use the `session_query` tool:
23
+
24
+ ```
25
+ session_query(sessionPath, question)
26
+ ```
27
+
28
+ **Parameters:**
29
+ - `sessionPath`: Full path to the session file (provided in the "Parent session:" line)
30
+ - `question`: Specific question about that session
31
+
32
+ ## Examples
33
+
34
+ ```typescript
35
+ // Find what files were changed
36
+ session_query("/path/to/session.jsonl", "What files were modified?")
37
+
38
+ // Get approach details
39
+ session_query("/path/to/session.jsonl", "What approach was chosen for authentication?")
40
+
41
+ // Get specific code decisions
42
+ session_query("/path/to/session.jsonl", "What error handling pattern was used?")
43
+
44
+ // Summarize key decisions
45
+ session_query("/path/to/session.jsonl", "Summarize the key decisions made")
46
+ ```
47
+
48
+ ## Best Practices
49
+
50
+ 1. **Be specific** - Ask targeted questions for better results
51
+ 2. **Reference code** - Ask about specific files or functions when relevant
52
+ 3. **Verify before assuming** - If the handoff summary seems incomplete, query for details
53
+ 4. **Don't over-query** - The handoff summary should have most context; query only when needed
54
+
55
+ ## How It Works
56
+
57
+ The tool loads the referenced session file, extracts the conversation history, and uses the LLM to answer your question based on its contents. This allows context retrieval without loading the full parent session into your context window.