// Human views of structured data. Never use these to serialize agent payloads. import type { TokenUsage } from "./runner.ts"; function compactTokens(tokens: number): string { if (tokens < 1000) return String(tokens); const units = ["", "k", "M", "G"]; let unit = Math.min(Math.floor(Math.log10(tokens) / 3), units.length - 1); let value = Number((tokens / 1000 ** unit).toPrecision(3)); if (value >= 1000 && unit < units.length - 1) { value /= 1000; unit++; } return `${value}${units[unit]}`; } export function formatUsage(usage: TokenUsage): { summary: string; detail: string; } { const cost = usage.cost > 0 && usage.cost < 0.0001 ? "<$0.0001" : `$${usage.cost.toFixed(4)}`; return { summary: `${cost} · ${compactTokens(usage.output)} output`, detail: `${compactTokens(usage.input)} uncached input · ${compactTokens(usage.cacheRead)} cache read · ${compactTokens(usage.cacheWrite)} cache write`, }; } function label(key: string): string { const words = key .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/[_-]+/g, " "); return words.charAt(0).toUpperCase() + words.slice(1); } function structuredValue(value: unknown): unknown { if (typeof value !== "string") return value; const json = value.trim().replace(/^```json\s*\n([\s\S]*?)\n```$/i, "$1"); if (!json.startsWith("{") && !json.startsWith("[")) return value; try { return JSON.parse(json); } catch { return value; } } function scalar(value: unknown): string { if (value === null) return "None"; if (value === undefined) return "Not provided"; return String(value); } function count(value: object): string { const size = Array.isArray(value) ? value.length : Object.keys(value).length; const noun = Array.isArray(value) ? "item" : "field"; return `${size} ${noun}${size === 1 ? "" : "s"}`; } /** One bounded line for progress, never a nested payload. */ export function summarizeValue(value: unknown): string { value = structuredValue(value); let summary: string; if (value === undefined) return ""; if (Array.isArray(value)) summary = count(value); else if (value !== null && typeof value === "object") { const entries = Object.entries(value); const title = ["summary", "title", "task", "name", "file", "path", "id"] .map((key) => entries.find(([name]) => name === key)?.[1]) .find((entry) => typeof entry === "string" && entry.trim()); summary = typeof title === "string" ? title : entries .slice(0, 3) .map( ([key, entry]) => `${label(key)}: ${entry !== null && typeof entry === "object" ? count(entry) : scalar(entry)}`, ) .join(" · ") || count(value); if (!title && entries.length > 3) summary += ` · +${entries.length - 3} fields`; } else summary = scalar(value); summary = summary.replace(/\s+/g, " ").trim(); return summary.length > 120 ? `${summary.slice(0, 119)}…` : summary; } /** Complete field/list view of JSON values, with explicit empty collections. */ export function formatValueMarkdown(value: unknown): string { value = structuredValue(value); const indent = (text: string) => text .split("\n") .map((line) => ` ${line}`) .join("\n"); if (Array.isArray(value)) { if (value.length === 0) return "- No items"; return value .map(structuredValue) .map((entry, index) => entry !== null && typeof entry === "object" ? `- **Item ${index + 1}**\n${indent(formatValueMarkdown(entry))}` : formatValueMarkdown(entry), ) .join("\n"); } if (value !== null && typeof value === "object") { const entries = Object.entries(value); if (entries.length === 0) return "- No fields"; return entries .map(([key, raw]) => { const entry = structuredValue(raw); const heading = `- **${label(key).replace(/[\\`*_[\]<>]/g, "\\$&")}:**`; return entry !== null && typeof entry === "object" ? `${heading}\n${indent(formatValueMarkdown(entry))}` : `${heading} ${scalar(entry).replace(/\n/g, " \n ")}`; }) .join("\n"); } return `- ${scalar(value).replace(/\n/g, " \n ")}`; } /** Tool text may carry a complete JSON document; prose and incomplete data stay verbatim. */ export function formatStructuredText( text: string, render: (value: unknown) => string = formatValueMarkdown, ): string { const json = text.trim().replace(/^```json\s*\n([\s\S]*?)\n```$/i, "$1"); try { return render(JSON.parse(json)); } catch { return text; } }