import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, Message } from "@earendil-works/pi-ai"; import type { SessionEntry } from "@earendil-works/pi-coding-agent"; export 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 Rules: - Reference existing artifacts (specs, plans, commits, issues, pull requests, documents) by file path, commit hash, or URL instead of duplicating their content. - Redact secrets and personal data: replace API keys, tokens, passwords, credentials, email addresses, phone numbers, and similar values with [REDACTED]. Never copy them into the prompt. - End with a short "## Suggested skills" section naming the skills the next session should load, based on skills used or referenced in the conversation. Write "none" when no skill applies. 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 Artifacts: - specs/feature/index.md - commit abc1234 ## Task [Clear description of what to do next based on user's goal] ## Suggested skills - skill-name: why the next session needs it`; 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; } /** * Messages worth handing off from the current branch. * After a compaction, the summary replaces everything before `firstKeptEntryId`. */ export function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] { let compactionIndex = -1; for (let index = branch.length - 1; index >= 0; index--) { if (branch[index]?.type === "compaction") { compactionIndex = index; break; } } const compaction = branch[compactionIndex]; if (compaction === undefined || compaction.type !== "compaction") return branch.map(entryToMessage).filter((m) => m !== undefined); const firstKeptIndex = branch.findIndex( (entry) => entry.id === compaction.firstKeptEntryId, ); const compacted = [ compaction, ...(firstKeptIndex >= 0 ? branch.slice(firstKeptIndex, compactionIndex) : []), ...branch.slice(compactionIndex + 1), ]; return compacted.map(entryToMessage).filter((m) => m !== undefined); } export function buildUserMessage(conversation: string, goal: string): Message { return { role: "user", content: [ { type: "text", text: `## Conversation History\n\n${conversation}\n\n## User's Goal for New Thread\n\n${goal}`, }, ], timestamp: Date.now(), }; } export function responseText(response: AssistantMessage): string { return response.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join("\n") .trim(); } export function boundedError(error: unknown): string { const message = error instanceof Error ? error.message : String(error); return message.length > 200 ? `${message.slice(0, 197)}...` : message; }