fix(ui): clean streaming Agent result output by unwrapping JSON and stripping task_id prefix

Refactor cleanAgentOutput to a sequential non-recursive pipeline: unwrap
JSON containers first, then strip task_id session lines, then extract
<task_result> content. Also apply cleaning to in-progress agent output
during streaming, not just completed results.
This commit is contained in:
xintaofei
2026-04-17 09:12:54 +08:00
parent 4f41a217c4
commit 73a910bb62

View File

@@ -199,13 +199,14 @@ function getJoinedChunks(chunks: readonly string[]): string {
*/ */
function cleanAgentOutput(output: string | null): string | null { function cleanAgentOutput(output: string | null): string | null {
if (!output) return null if (!output) return null
const trimmed = output.trim() let text = output.trim()
if (!trimmed) return null if (!text) return null
// Step 1: Unwrap JSON containers (no recursion — single-level unwrap)
// JSON array of content blocks: [{"type":"text","text":"..."},...] // JSON array of content blocks: [{"type":"text","text":"..."},...]
if (trimmed.startsWith("[")) { if (text.startsWith("[")) {
try { try {
const arr = JSON.parse(trimmed) const arr = JSON.parse(text)
if (Array.isArray(arr)) { if (Array.isArray(arr)) {
const texts: string[] = [] const texts: string[] = []
for (const item of arr) { for (const item of arr) {
@@ -217,37 +218,43 @@ function cleanAgentOutput(output: string | null): string | null {
texts.push(item.text) texts.push(item.text)
} }
} }
if (texts.length > 0) return texts.join("\n") if (texts.length > 0) text = texts.join("\n")
} }
} catch { } catch {
/* not valid JSON */ /* not valid JSON */
} }
} } else if (text.startsWith("{")) {
// JSON object with common result fields
// JSON object with common result fields
if (trimmed.startsWith("{")) {
try { try {
const obj = JSON.parse(trimmed) as Record<string, unknown> const obj = JSON.parse(text) as Record<string, unknown>
for (const key of ["result", "output", "text", "content", "completed"]) { for (const key of ["result", "output", "text", "content", "completed"]) {
if (typeof obj[key] === "string") return obj[key] as string if (typeof obj[key] === "string") {
text = (obj[key] as string).trim()
break
}
} }
} catch { } catch {
/* not valid JSON */ /* not valid JSON */
} }
} }
// <task_result> XML wrapper (OpenCode) // Step 2: Strip leading session / task_id lines that some agents prepend
const tagStart = trimmed.indexOf("<task_result>") // before the actual result (e.g. "task_id: ses_xxx (for resuming ...)").
text = text.replace(/^task_id:\s*\S+[^\n]*\n+/, "").trim()
if (!text) return null
// Step 3: Extract from <task_result> XML wrapper (OpenCode)
const tagStart = text.indexOf("<task_result>")
if (tagStart !== -1) { if (tagStart !== -1) {
const contentStart = tagStart + "<task_result>".length const contentStart = tagStart + "<task_result>".length
const contentEnd = trimmed.indexOf("</task_result>", contentStart) const contentEnd = text.indexOf("</task_result>", contentStart)
const extracted = trimmed const extracted = text
.substring(contentStart, contentEnd === -1 ? undefined : contentEnd) .substring(contentStart, contentEnd === -1 ? undefined : contentEnd)
.trim() .trim()
if (extracted) return extracted if (extracted) return extracted
} }
return output return text
} }
function buildStreamingTurnsFromLiveMessage( function buildStreamingTurnsFromLiveMessage(
@@ -441,7 +448,9 @@ function buildStreamingTurnsFromLiveMessage(
currentBlocks.push({ currentBlocks.push({
type: "tool_result", type: "tool_result",
tool_use_id: block.info.tool_call_id, tool_use_id: block.info.tool_call_id,
output_preview: resolvedOutput ?? null, output_preview: isAgent
? cleanAgentOutput(resolvedOutput)
: (resolvedOutput ?? null),
is_error: false, is_error: false,
...(agentStats ? { agent_stats: agentStats } : {}), ...(agentStats ? { agent_stats: agentStats } : {}),
}) })