Download packages/coding-agent/examples/extensions/handoff.ts from SaylorTwift/pi: direct link, hf CLI and curl.
- Browser
- Download file 5.97 kB
-
https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/examples/extensions/handoff.ts
- Command line
-
hf download hf://SaylorTwift/pi/packages/coding-agent/examples/extensions/handoff.ts
-
curl -L -o handoff.ts https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/examples/extensions/handoff.ts
5.97 kB
| /** | |
| * Handoff extension - transfer context to a new focused session | |
| * | |
| * Instead of compacting (which is lossy), handoff extracts what matters | |
| * for your next task and creates a new session with a generated prompt. | |
| * | |
| * Usage: | |
| * /handoff now implement this for teams as well | |
| * /handoff execute phase one of the plan | |
| * /handoff check other places that need this fix | |
| * | |
| * The generated prompt appears as a draft in the editor for review/editing. | |
| */ | |
| import type { AgentMessage } from "@earendil-works/pi-agent-core"; | |
| import { type Message, uuidv7 } from "@earendil-works/pi-ai"; | |
| import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent"; | |
| import { BorderedLoader, convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent"; | |
| const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that: | |
| 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings) | |
| 2. Lists any relevant files that were discussed or modified | |
| 3. Clearly states the next task based on the user's goal | |
| 4. Is self-contained - the new thread should be able to proceed without the old conversation | |
| Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself. | |
| Example output format: | |
| ## Context | |
| We've been working on X. Key decisions: | |
| - Decision 1 | |
| - Decision 2 | |
| Files involved: | |
| - path/to/file1.ts | |
| - path/to/file2.ts | |
| ## Task | |
| [Clear description of what to do next based on user's goal]`; | |
| function entryToMessage(entry: SessionEntry): AgentMessage | undefined { | |
| if (entry.type === "message") { | |
| return entry.message; | |
| } | |
| if (entry.type === "compaction") { | |
| return { | |
| role: "compactionSummary", | |
| summary: entry.summary, | |
| tokensBefore: entry.tokensBefore, | |
| timestamp: new Date(entry.timestamp).getTime(), | |
| }; | |
| } | |
| return undefined; | |
| } | |
| function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] { | |
| let compactionIndex = -1; | |
| for (let i = branch.length - 1; i >= 0; i--) { | |
| if (branch[i].type === "compaction") { | |
| compactionIndex = i; | |
| break; | |
| } | |
| } | |
| if (compactionIndex < 0) { | |
| return branch.map(entryToMessage).filter((message) => message !== undefined); | |
| } | |
| const compaction = branch[compactionIndex]; | |
| const firstKeptIndex = | |
| compaction.type === "compaction" ? branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId) : -1; | |
| const compactedBranch = [ | |
| compaction, | |
| ...(firstKeptIndex >= 0 ? branch.slice(firstKeptIndex, compactionIndex) : []), | |
| ...branch.slice(compactionIndex + 1), | |
| ]; | |
| return compactedBranch.map(entryToMessage).filter((message) => message !== undefined); | |
| } | |
| export default function (pi: ExtensionAPI) { | |
| pi.registerCommand("handoff", { | |
| description: "Transfer context to a new focused session", | |
| handler: async (args, ctx) => { | |
| if (ctx.mode !== "tui") { | |
| ctx.ui.notify("handoff requires interactive mode", "error"); | |
| return; | |
| } | |
| if (!ctx.model) { | |
| ctx.ui.notify("No model selected", "error"); | |
| return; | |
| } | |
| const goal = args.trim(); | |
| if (!goal) { | |
| ctx.ui.notify("Usage: /handoff <goal for new thread>", "error"); | |
| return; | |
| } | |
| // Gather conversation context from current branch. If the branch was compacted, | |
| // include the compaction summary plus entries from firstKeptEntryId onward. | |
| const messages = getHandoffMessages(ctx.sessionManager.getBranch()); | |
| if (messages.length === 0) { | |
| ctx.ui.notify("No conversation to hand off", "error"); | |
| return; | |
| } | |
| // Convert to LLM format and serialize | |
| const llmMessages = convertToLlm(messages); | |
| const conversationText = serializeConversation(llmMessages); | |
| const currentSessionFile = ctx.sessionManager.getSessionFile(); | |
| // Generate the handoff prompt with loader UI | |
| const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => { | |
| const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`); | |
| loader.onAbort = () => done(null); | |
| const doGenerate = async () => { | |
| const userMessage: Message = { | |
| role: "user", | |
| content: [ | |
| { | |
| type: "text", | |
| text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`, | |
| }, | |
| ], | |
| timestamp: Date.now(), | |
| }; | |
| const response = await ctx.modelRegistry.complete( | |
| ctx.model!, | |
| { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, | |
| { | |
| signal: loader.signal, | |
| cacheRetention: "none", | |
| sessionId: uuidv7(), | |
| }, | |
| ); | |
| if (response.stopReason === "aborted") { | |
| return null; | |
| } | |
| return response.content | |
| .filter((c): c is { type: "text"; text: string } => c.type === "text") | |
| .map((c) => c.text) | |
| .join("\n"); | |
| }; | |
| doGenerate() | |
| .then(done) | |
| .catch((err) => { | |
| console.error("Handoff generation failed:", err); | |
| done(null); | |
| }); | |
| return loader; | |
| }); | |
| if (result === null) { | |
| ctx.ui.notify("Cancelled", "info"); | |
| return; | |
| } | |
| // Let user edit the generated prompt | |
| const editedPrompt = await ctx.ui.editor("Edit handoff prompt", result); | |
| if (editedPrompt === undefined) { | |
| ctx.ui.notify("Cancelled", "info"); | |
| return; | |
| } | |
| // Create new session with parent tracking. Use the replacement-session | |
| // context for post-switch UI work; the original ctx is stale after a | |
| // successful session replacement. | |
| const newSessionResult = await ctx.newSession({ | |
| parentSession: currentSessionFile, | |
| withSession: async (replacementCtx) => { | |
| replacementCtx.ui.setEditorText(editedPrompt); | |
| replacementCtx.ui.notify("Handoff ready. Submit when ready.", "info"); | |
| }, | |
| }); | |
| if (newSessionResult.cancelled) { | |
| ctx.ui.notify("New session cancelled", "info"); | |
| } | |
| }, | |
| }); | |
| } | |