/** * The Digest (CONTEXT.md *Digest*): what code knows about the span, built once and sent whole at the * start of the turn conversation -- the state so far, on a switchover the previous summary and the * user's earlier rule-shaped messages, the span's user messages verbatim with ordinals, the ledger's * commands and errors with outcomes, the file lists, World, and the condensed transcript. `show(ref)` * answers the one read-only tool: a user message, command or error record, or a whole tool output. */ import { renderCommandLine, renderErrorLine } from "../../ledger/handoff-render.ts"; import type { CommandRecord, ErrorRecord } from "../../ledger/handoff.ts"; import type { FileLists } from "../../carry.ts"; import { renderFilesSection } from "../../files-render.ts"; import { stateBlock, type PromptState } from "../prompt.ts"; import type { CondensedTranscript } from "./transcript.ts"; export interface DigestInput { /** The span's user messages, whole. */ userMessages: Array<{ ordinal: string; text: string }>; /** Any user message on the branch by ordinal, for show(uN). */ textOf: (ordinal: string) => string | undefined; commands: CommandRecord[]; errors: ErrorRecord[]; files: FileLists; filesChars: number; worldLines: string[]; promptState: PromptState; transcript: CondensedTranscript; } export interface Digest { text: string; show(ref: string): string | undefined; } /** A user message over this is a paste: head and tail in the digest, the whole of it behind show(uN). */ export const USER_MESSAGE_CAP = { head: 3000, tail: 1000 }; /** One show() answer, at most. */ export const SHOW_CHARS = 8000; function userMessageLine(message: { ordinal: string; text: string }): string { const { head, tail } = USER_MESSAGE_CAP; if (message.text.length <= head + tail) return `[${message.ordinal}] ${message.text}`; return `[${message.ordinal}] ${message.text.slice(0, head)}\n[… ${message.text.length - head - tail} chars, show ${message.ordinal} …]\n${message.text.slice(-tail)}`; } const block = (tag: string, lines: string[], empty = "(none)") => `<${tag}>\n${lines.length ? lines.join("\n") : empty}\n`; function seedBlocks(state: PromptState): string[] { if (!state.seed) return []; const rules = state.seedRules?.length ? [block("earlier-user-messages", state.seedRules.map((rule) => `[${rule.ordinal}] ${rule.text}`))] : []; return [`\n${state.seed}\n`, ...rules]; } const capShow = (text: string) => (text.length <= SHOW_CHARS ? text : `${text.slice(0, SHOW_CHARS)}\n[… ${text.length - SHOW_CHARS} more chars not shown]`); export function buildDigest(input: DigestInput): Digest { const parts = [ stateBlock(input.promptState), ...seedBlocks(input.promptState), block("user-messages", input.userMessages.map(userMessageLine), "(none in this span)"), block("commands", input.commands.map(renderCommandLine)), block("errors", input.errors.map((record) => renderErrorLine(record, input.commands))), renderFilesSection(input.files, input.filesChars).join("\n"), block("world", input.worldLines), block("transcript", input.transcript.text ? [input.transcript.text] : [], "(empty)"), ]; const show = (ref: string): string | undefined => { if (/^u\d+$/.test(ref)) return mapDefined(input.textOf(ref), capShow); if (/^C\d+$/.test(ref)) return mapDefined(input.commands.find((record) => record.id === ref), renderCommandLine); if (/^E\d+$/.test(ref)) return mapDefined(input.errors.find((record) => record.id === ref), (record) => renderErrorLine(record, input.commands)); if (/^T\d+$/.test(ref)) return mapDefined(input.transcript.outputs.get(ref), capShow); return undefined; }; return { text: parts.join("\n\n"), show }; } function mapDefined(value: T | undefined, fn: (value: T) => U): U | undefined { return value === undefined ? undefined : fn(value); }