// Show any JSON value well: images and audio carried as base64 or URLs, chat transcripts, tables for lists of // records, long text as text, nested objects as key/value rows that fold. Used for environment observations, Task // API tasks and the rows of custom environments. Everything is escaped; media only from data: or https: URLs. import { esc, fmt, md } from "./util.js"; const IMG_KEY = /(image|img|png|jpe?g|frame|screenshot|pixels|photo|thumbnail|render)/i; const AUDIO_KEY = /(audio|wav|speech|sound|waveform)/i; const B64 = /^[A-Za-z0-9+/=\s]+$/; const IMG_URL = /^https:\/\/[^\s"'<>]+\.(png|jpe?g|gif|webp|svg|avif)(\?[^\s"'<>]*)?$/i; const AUDIO_URL = /^https:\/\/[^\s"'<>]+\.(wav|mp3|ogg|flac|m4a)(\?[^\s"'<>]*)?$/i; function mediaOf(key, v, ctx) { if (typeof v !== "string" || v.length < 16) return null; if (key === "media_url" && /^https:\/\/[^\s"'<>]+$/.test(v)) { if (/^audio\/[\w.+-]+$/.test(ctx.mime || "")) return ``; if (/^image\/(png|jpeg|gif|webp|avif)$/.test(ctx.mime || "")) return `Task image`; if (/^video\/(mp4|webm|ogg)$/.test(ctx.mime || "")) return ``; } if (v.startsWith("data:image/") && /^data:image\/(png|jpe?g|gif|webp);base64,/.test(v)) return `${esc(key)}`; if (v.startsWith("data:audio/") && /^data:audio\/[\w.+-]+;base64,/.test(v)) return ``; if (IMG_URL.test(v)) return `${esc(key)}`; if (AUDIO_URL.test(v)) return ``; if (v.length > 200 && B64.test(v.slice(0, 400))) { if (IMG_KEY.test(key)) { const fmtName = String(ctx.image_format || ctx.format || "png").toLowerCase().replace("jpg", "jpeg"); return `${esc(key)}`; } if (AUDIO_KEY.test(key)) return ``; return `base64, ${fmt.format(Math.round(v.length * 0.75 / 1024))} KB`; } return null; } const isMessage = (x) => x && typeof x === "object" && !Array.isArray(x) && typeof x.role === "string" && ("content" in x || "tool_calls" in x); const scalar = (x) => x == null || ["string", "number", "boolean"].includes(typeof x); function text(s, key) { if (s === "") return `empty`; if (s.length > 160 || s.includes("\n")) { const looksMd = /(^|\n)(#{1,4} |[-*] |\d+\. |```)/.test(s); return looksMd && s.length < 40_000 ? `
${md(s)}
` : `
${esc(s.length > 60_000 ? s.slice(0, 60_000) + "\n… (cut)" : s)}
`; } return `${esc(s)}`; } // a long message shows its first lines and a "Show all" toggle (a checkbox: no script needed in a rendered string) let clipN = 0; const clip = (html, len) => (len < 1500 ? html : `
${html}
`); const lenOf = (c) => (typeof c === "string" ? c.length : JSON.stringify(c ?? "").length); function chat(msgs, depth) { return `
${msgs.slice(0, 200).map((m) => { const body = typeof m.content === "string" ? text(m.content, "content") : Array.isArray(m.content) ? m.content.map((p) => (p?.type === "text" ? text(p.text || "", "text") : value(p, "part", depth + 1))).join("") : m.content == null ? "" : value(m.content, "content", depth + 1); const calls = Array.isArray(m.tool_calls) ? m.tool_calls.map((c) => `
${esc(c.function?.name || c.name || "tool")} ${esc(typeof c.function?.arguments === "string" ? c.function.arguments.slice(0, 2000) : JSON.stringify(c.arguments ?? c.function?.arguments ?? {}).slice(0, 2000))}
`).join("") : ""; return `
${esc(m.role)}${m.name ? ` · ${esc(m.name)}` : ""}${clip(body, lenOf(m.content))}${calls}
`; }).join("")}${msgs.length > 200 ? `

and ${fmt.format(msgs.length - 200)} more messages

` : ""}
`; } function table(rows, depth) { const keys = [...new Set(rows.slice(0, 50).flatMap((r) => Object.keys(r)))].slice(0, 12); return `
${keys.map((k) => ``).join("")}${rows.slice(0, 100).map((r) => `${keys.map((k) => ``).join("")}`).join("")}
${esc(k)}
${scalar(r[k]) ? cell(r[k]) : value(r[k], k, depth + 1)}
${rows.length > 100 ? `

${fmt.format(rows.length - 100)} more rows

` : ""}
`; } const cell = (x) => (x == null ? `–` : typeof x === "string" ? `${esc(x.length > 140 ? x.slice(0, 140) + "…" : x)}` : `${esc(String(x))}`); export function value(v, key = "", depth = 0, ctx = {}) { if (depth > 7) return `…`; if (v == null) return `null`; if (typeof v === "boolean") return `${v}`; if (typeof v === "number") return `${esc(Number.isInteger(v) ? fmt.format(v) : String(+v.toFixed(6)))}`; if (typeof v === "string") return mediaOf(key, v, ctx) || text(v, key); if (Array.isArray(v)) { if (!v.length) return `empty list`; if (v.every(isMessage)) return chat(v, depth); if (v.every(scalar) && v.length <= 60 && v.every((x) => String(x).length < 60)) return `${v.map((x) => `${esc(String(x))}`).join("")}`; if (v.length > 1 && v.every((x) => x && typeof x === "object" && !Array.isArray(x)) && v.slice(0, 20).every((x) => Object.values(x).filter((y) => !scalar(y)).length <= 1)) return table(v, depth); return `
    ${v.slice(0, 100).map((x, i) => `
  1. ${value(x, key, depth + 1, ctx)}
  2. `).join("")}${v.length > 100 ? `
  3. ${fmt.format(v.length - 100)} more
  4. ` : ""}
`; } if (typeof v === "object") return object(v, depth); return esc(String(v)); } // key/value rows; a nested object folds when it's big export function object(o, depth = 0, { order = [], hide = [] } = {}) { const keys = Object.keys(o).filter((k) => !hide.includes(k)); const pri = (k) => (order.includes(k) ? order.indexOf(k) : order.length); keys.sort((a, b) => pri(a) - pri(b)); // stable: the rest keep the server's order if (!keys.length) return `empty`; return `
${keys.map((k) => { const x = o[k]; const big = x && typeof x === "object" && (Array.isArray(x) ? x.length > 8 : Object.keys(x).length > 6) && depth > 0; const inner = value(x, k, depth + 1, o); return `
${esc(k)}
${big ? `
${Array.isArray(x) ? `${fmt.format(x.length)} items` : `${fmt.format(Object.keys(x).length)} fields`}${inner}
` : inner}
`; }).join("")}
`; } // An observation from reset/step: reward and done up front, the rest below export function observation(data) { const obs = data && typeof data === "object" && "observation" in data ? data.observation : data; const reward = data?.reward ?? obs?.reward; const done = data?.done ?? obs?.done; const head = `
${reward != null ? `reward ${esc(typeof reward === "number" ? String(+reward.toFixed(4)) : String(reward))}` : ""} ${done != null ? `${done ? "done" : "not done"}` : ""}
`; const rest = obs && typeof obs === "object" && !Array.isArray(obs) ? object(obs, 0, { order: ["prompt", "feedback", "message", "text"], hide: ["reward", "done"] }) : value(obs, "observation"); return head + rest; } // An MCP tool result: its content parts (text, images), or its raw value export function toolResult(r) { if (r && Array.isArray(r.content)) { const parts = r.content.map((c) => (c?.type === "text" ? (() => { try { const j = JSON.parse(c.text); return typeof j === "object" && j ? value(j) : text(c.text, "text"); } catch { return text(c.text || "", "text"); } })() : c?.type === "image" && /^image\/(png|jpeg|gif|webp)$/.test(c.mimeType || c.mime_type) ? `` : c?.type === "audio" ? `` : value(c))).join(""); return `${r.isError || r.is_error ? `
error
` : ""}${parts || `no content`}`; } return value(r); }