705750453d
- 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.
219 lines
6.9 KiB
TypeScript
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();
|
|
}
|