Files
oh-my-pi/packages/coding-agent/src/session/session-dump-format.ts
T
can1357 705750453d fix(coding-agent-turn-interrupt/queue-ux): resolved steering abort state
- Replaced queued-message interrupt flow with session abort calls on empty submit and escape.
- Removed interrupting state and notifyInterrupting teardown paths from abort handling.
- Updated AgentSession queue operations to use shared steering and follow-up queue views.
- Propagated isAborting through session state and collab payloads to suppress late updates.
2026-06-13 17:31:25 +02:00

219 lines
6.9 KiB
TypeScript

/**
* 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 { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
import { getVisibleThinkingText } from "../utils/thinking-display";
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<string, unknown> = {};
for (const [k, v] of Object.entries(obj)) {
if (!k.startsWith("TypeBox.")) {
result[k] = stripTypeBoxFields(v);
}
}
return result;
}
return obj;
}
/** Resolve tool parameters to a plain JSON Schema object for dump output. */
function toolParametersToJsonSchema(parameters: unknown): unknown {
if (isZodSchema(parameters)) return zodToWireSchema(parameters);
return stripTypeBoxFields(parameters);
}
/** Serialize an object as XML parameter elements, one per key. */
function formatArgsAsXml(args: Record<string, unknown>, 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}<parameter name="${key}">${text}</parameter>`);
}
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(`<tool name="${tool.name}">`);
lines.push(tool.description);
const parametersClean = toolParametersToJsonSchema(tool.parameters);
lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
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") {
const thinking = getVisibleThinkingText(c);
if (thinking.length === 0) continue;
lines.push("<thinking>");
lines.push(thinking);
lines.push("</thinking>\n");
} else if (c.type === "toolCall") {
lines.push(`<invoke name="${c.name}">`);
if (c.arguments && typeof c.arguments === "object") {
lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
}
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(`<file path="${file.path}">`);
if (file.content) {
lines.push(file.content);
}
if (file.image) {
lines.push("[Image attached]");
}
lines.push("</file>\n");
}
lines.push("\n");
}
}
return lines.join("\n").trim();
}