// Narrow browser protocol, inspired by letta-ai/letta-agent-sdk-react-chat. // No SDK payload, tool arguments/results, or reasoning crosses this boundary. export type ChatRow = | { kind: "user" | "assistant"; key: string; text: string; otid?: string } | { kind: "tool_call"; key: string; name: string; outcome: "pending" | "complete" | "failed" }; export type TurnStatus = "dispatching" | "running" | "stopping" | "completed" | "interrupted" | "failed" | "unknown"; export interface TurnReceipt { requestId: string; status: TurnStatus; createdAt: string; updatedAt: string; runIds: string[]; } export interface WebConversation { id: string; title: string; createdAt: string; updatedAt: string; turn?: TurnReceipt; } export interface ChatSnapshot { conversation: WebConversation; rows: ChatRow[]; model?: string; historyPending?: boolean; nextBefore?: string | null; hasMore?: boolean; runtime?: { online: boolean; processing: boolean; permissionMode: string }; } export interface ChatContext { source: string; observedAt: string; excerpt: string } export interface ChatSend { requestId: string; text: string; context?: ChatContext | undefined } export function rowIdentity(row: ChatRow): string { return row.kind !== "tool_call" && row.otid ? `${row.kind}:otid:${row.otid}` : row.key; } export function mergeRows(previous: readonly ChatRow[], current: readonly ChatRow[]): ChatRow[] { const keys = new Set(current.map(rowIdentity)); return [...previous.filter((row) => !keys.has(rowIdentity(row))), ...current]; } export function isUnsettled(status: TurnStatus | undefined): boolean { return status !== undefined && !["completed", "interrupted", "failed"].includes(status); }