Controller, claim store, live-session guard, engine link and seal, turn tracker, cohort force stop and recovery, client library, transcript and mediated terminal, with the fake engine and tests. Fixtures only; no live cutover. Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694) approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files). Suites on an export: conversation 152/152, control-board 124, webui 14, seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green. Follow-ups for I3 are in DEFERRED. Gate E stays with Jason. Co-Authored-By: Claude Opus 5.5 <[email protected]>
122 lines
5.8 KiB
JavaScript
122 lines
5.8 KiB
JavaScript
// 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 : "";
|
|
}
|