// Pi RPC events to CHAT-01 events (#1507, CHAT-03 ยง3 "Events"). // // Pinned Pi 0.85.1 (docs/rpc.md, rpc-types.d.ts) emits JSON lines. Mapped: // // message_start -> message-start // message_update text_delta -> text-delta (append) // message_update thinking_delta -> thinking-delta (append) // tool_execution_start -> tool-start // tool_execution_update -> tool-update (replace) // tool_execution_end -> tool-end (replace) // message_end -> message-end (replace, one or more parts) // agent_settled -> run-settled (never cohort termination) // // Known and deliberately not shown: turn_start, turn_end, agent_start, // agent_end, queue_update, compaction_*, auto_retry_*, summarization_retry_*, // bash_execution_update, extension_error, extension_ui_request (P3 has its own notice), and the // message_update subtypes that message-end supersedes (text_start, text_end, // thinking_start, thinking_end, toolcall_*, start, done, error). The // controller counts them in its evidence. Anything else is an unknown event: // no client event, counted with its type and byte size, and never passed // through raw. // // Pi's stream carries no entry ID. `entry` on message-end is a stream-local // ID, not a session entry ID, so the seam between a history page and the // stream can't be deduplicated by ID (E4). import { fragments, safeId, LIMITS } from "./parts.mjs"; export const KNOWN_UNSHOWN = Object.freeze(new Set([ "turn_start", "turn_end", "agent_start", "agent_end", "queue_update", "compaction_start", "compaction_end", "auto_retry_start", "auto_retry_end", "summarization_retry_scheduled", "summarization_retry_attempt_start", "summarization_retry_finished", "bash_execution_update", "extension_error", "extension_ui_request", "response", ])); export const MAPPED = Object.freeze(new Set(["message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "agent_settled"])); const FOLDED_UPDATES = new Set(["start", "text_start", "text_end", "thinking_start", "thinking_end", "toolcall_start", "toolcall_delta", "toolcall_end", "done", "error"]); export const DIALOG_METHODS = Object.freeze(new Set(["select", "confirm", "input", "editor"])); export const NOTIFY_METHODS = Object.freeze(new Set(["notify", "setStatus", "setWidget", "setTitle", "set_editor_text"])); export function roleOf(message) { switch (message?.role) { case "user": return "user"; case "assistant": return "assistant"; case "toolResult": return "tool"; case "compactionSummary": return "compaction"; default: return "notice"; } } const textOf = (content) => { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content.filter((c) => c && c.type === "text" && typeof c.text === "string").map((c) => c.text).join(""); }; function pushText(out, type, text, block, extra = {}) { const parts = fragments(text); parts.forEach((t, i) => out.push({ type, ...extra, text: t, block, fragment: i, lastFragment: i === parts.length - 1 })); } // Every content block of a finished native message, in CHAT-01 form. export function messageBlocks(message) { const out = []; const role = roleOf(message); if (role === "tool") { pushText(out, "tool-result", textOf(message.content), 0, { call: safeId(message.toolCallId), isError: message.isError === true }); return out; } if (role === "compaction") { const parts = fragments(typeof message.summary === "string" ? message.summary : ""); parts.forEach((t, i) => out.push({ type: "compaction", summary: t, nativeEntry: safeId(message.firstKeptEntryId ?? "compaction"), block: 0, fragment: i, lastFragment: i === parts.length - 1 })); return out; } const content = typeof message?.content === "string" ? [{ type: "text", text: message.content }] : Array.isArray(message?.content) ? message.content : []; content.forEach((c, block) => { if (!c || typeof c !== "object") return; if (c.type === "text") pushText(out, "text", String(c.text ?? ""), block); else if (c.type === "thinking") { if (c.redacted) out.push({ type: "thinking", text: "", visibility: "unavailable", block, fragment: 0, lastFragment: true }); else pushText(out, "thinking", String(c.thinking ?? ""), block, { visibility: "permitted-visible" }); } else if (c.type === "toolCall") { const parts = fragments(JSON.stringify(c.arguments ?? {})); parts.forEach((t, i) => out.push({ type: "tool-call", call: safeId(c.id), name: safeId(c.name), argumentsText: t, block, fragment: i, lastFragment: i === parts.length - 1 })); } else if (c.type === "image") { out.push({ type: "attachment", attachment: safeId(`image-${block}`), block, fragment: 0, lastFragment: true }); } else { pushText(out, "text", `[${String(c.type).slice(0, 40)} block not shown]`, block); } }); return out; } // Splits blocks into message-end parts of at most LIMITS.blocks blocks. export function partsOf(blocks) { if (blocks.length === 0) return [[]]; const out = []; for (let i = 0; i < blocks.length; i += LIMITS.blocks) out.push(blocks.slice(i, i + LIMITS.blocks)); return out; } export function deltaBlocks(kind, delta, contentIndex) { const out = []; const block = Number.isInteger(contentIndex) && contentIndex >= 0 ? contentIndex : 0; if (kind === "thinking") pushText(out, "thinking", String(delta ?? ""), block, { visibility: "permitted-visible" }); else pushText(out, "text", String(delta ?? ""), block); return out; } export function isFoldedUpdate(type) { return FOLDED_UPDATES.has(type); } export function toolResultText(result) { if (result && typeof result === "object") return textOf(result.content); return typeof result === "string" ? result : ""; }