/** * Plain-text / markdown session formatting (same shape as /dump clipboard export). */ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core"; import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai"; import { type BashExecutionMessage, type BranchSummaryMessage, bashExecutionToText, type CompactionSummaryMessage, type CustomMessage, type FileMentionMessage, type HookMessage, type PythonExecutionMessage, pythonExecutionToText, } from "./messages"; /** Minimal tool shape for dump output (matches AgentTool fields used by formatSessionDumpText). */ export interface SessionDumpToolInfo { name: string; description: string; parameters: unknown; } export interface FormatSessionDumpTextOptions { messages: readonly AgentMessage[]; systemPrompt?: readonly string[] | null; model?: Model | null; thinkingLevel?: ThinkingLevel | string | null; tools?: readonly SessionDumpToolInfo[]; } function stripTypeBoxFields(obj: unknown): unknown { if (Array.isArray(obj)) { return obj.map(stripTypeBoxFields); } if (obj && typeof obj === "object") { const result: Record = {}; for (const [k, v] of Object.entries(obj)) { if (!k.startsWith("TypeBox.")) { result[k] = stripTypeBoxFields(v); } } return result; } return obj; } /** Serialize an object as XML parameter elements, one per key. */ function formatArgsAsXml(args: Record, indent = "\t"): string { const parts: string[] = []; for (const [key, value] of Object.entries(args)) { if (key === INTENT_FIELD) continue; const text = typeof value === "string" ? value : JSON.stringify(value); parts.push(`${indent}${text}`); } return parts.join("\n"); } /** * Format messages and session metadata as markdown/plain text (same as AgentSession.formatSessionAsText / /dump). */ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string { const lines: string[] = []; const systemPrompt = options.systemPrompt?.filter(prompt => prompt.length > 0) ?? []; if (systemPrompt.length > 0) { lines.push("## System Prompt\n"); for (let index = 0; index < systemPrompt.length; index++) { if (systemPrompt.length > 1) { lines.push(`### System Prompt ${index + 1}\n`); } lines.push(systemPrompt[index]); lines.push("\n"); } } const model = options.model; const thinkingLevel = options.thinkingLevel; lines.push("## Configuration\n"); lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`); lines.push(`Thinking Level: ${thinkingLevel ?? ""}`); lines.push("\n"); const tools = options.tools ?? []; if (tools.length > 0) { lines.push("## Available Tools\n"); for (const tool of tools) { lines.push(``); lines.push(tool.description); const parametersClean = stripTypeBoxFields(tool.parameters); lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record)}`); lines.push("<" + "/tool>\n"); } lines.push("\n"); } for (const msg of options.messages) { if (msg.role === "user" || msg.role === "developer") { lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n"); if (typeof msg.content === "string") { lines.push(msg.content); } else { for (const c of msg.content) { if (c.type === "text") { lines.push(c.text); } else if (c.type === "image") { lines.push("[Image]"); } } } lines.push("\n"); } else if (msg.role === "assistant") { const assistantMsg = msg as AssistantMessage; lines.push("## Assistant\n"); for (const c of assistantMsg.content) { if (c.type === "text") { lines.push(c.text); } else if (c.type === "thinking") { if (c.thinking.trim().length === 0) continue; lines.push(""); lines.push(c.thinking); lines.push("\n"); } else if (c.type === "toolCall") { lines.push(``); if (c.arguments && typeof c.arguments === "object") { lines.push(formatArgsAsXml(c.arguments as Record)); } lines.push("<" + "/invoke>\n"); } } lines.push(""); } else if (msg.role === "toolResult") { lines.push(`### Tool Result: ${msg.toolName}`); if (msg.isError) { lines.push("(error)"); } for (const c of msg.content) { if (c.type === "text") { lines.push("```"); lines.push(c.text); lines.push("```"); } else if (c.type === "image") { lines.push("[Image output]"); } } lines.push(""); } else if (msg.role === "bashExecution") { const bashMsg = msg as BashExecutionMessage; if (!bashMsg.excludeFromContext) { lines.push("## Bash Execution\n"); lines.push(bashExecutionToText(bashMsg)); lines.push("\n"); } } else if (msg.role === "pythonExecution") { const pythonMsg = msg as PythonExecutionMessage; if (!pythonMsg.excludeFromContext) { lines.push("## Python Execution\n"); lines.push(pythonExecutionToText(pythonMsg)); lines.push("\n"); } } else if (msg.role === "custom" || msg.role === "hookMessage") { const customMsg = msg as CustomMessage | HookMessage; lines.push(`## ${customMsg.customType}\n`); if (typeof customMsg.content === "string") { lines.push(customMsg.content); } else { for (const c of customMsg.content) { if (c.type === "text") { lines.push(c.text); } else if (c.type === "image") { lines.push("[Image]"); } } } lines.push("\n"); } else if (msg.role === "branchSummary") { const branchMsg = msg as BranchSummaryMessage; lines.push("## Branch Summary\n"); lines.push(`(from branch: ${branchMsg.fromId})\n`); lines.push(branchMsg.summary); lines.push("\n"); } else if (msg.role === "compactionSummary") { const compactMsg = msg as CompactionSummaryMessage; lines.push("## Compaction Summary\n"); lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`); lines.push(compactMsg.summary); lines.push("\n"); } else if (msg.role === "fileMention") { const fileMsg = msg as FileMentionMessage; lines.push("## File Mention\n"); for (const file of fileMsg.files) { lines.push(``); if (file.content) { lines.push(file.content); } if (file.image) { lines.push("[Image attached]"); } lines.push("\n"); } lines.push("\n"); } } return lines.join("\n").trim(); }