From 015e39e3cf9e2e90b3f0a8c29dc0050dc6a5c203 Mon Sep 17 00:00:00 2001 From: Alec Hrdina Date: Tue, 14 Apr 2026 21:20:20 +0000 Subject: [PATCH] agents/cli: unwrap nested claude result json --- src/agents/cli-output.test.ts | 59 +++++++++++++++++++++++++++++++++++ src/agents/cli-output.ts | 29 +++++++++++++++-- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/agents/cli-output.test.ts b/src/agents/cli-output.test.ts index 5fde541a612..c951564dae8 100644 --- a/src/agents/cli-output.test.ts +++ b/src/agents/cli-output.test.ts @@ -125,6 +125,33 @@ describe("parseCliJson", () => { }); }); + it("unwraps nested Claude result JSON from JSON output", () => { + const result = parseCliJson( + JSON.stringify({ + session_id: "session-nested-json", + result: JSON.stringify({ + type: "result", + result: JSON.stringify({ + type: "result", + subtype: "success", + result: "actual response text", + }), + }), + }), + { + command: "claude", + output: "json", + sessionIdFields: ["session_id"], + }, + ); + + expect(result).toEqual({ + text: "actual response text", + sessionId: "session-nested-json", + usage: undefined, + }); + }); + it("parses nested OpenAI-style cached token details from CLI json payloads", () => { const result = parseCliJson( JSON.stringify({ @@ -295,6 +322,38 @@ describe("parseCliJsonl", () => { }); }); + it("unwraps nested Claude agent result JSON from stream-json output", () => { + const result = parseCliJsonl( + [ + JSON.stringify({ type: "init", session_id: "session-nested-jsonl" }), + JSON.stringify({ + type: "result", + session_id: "session-nested-jsonl", + result: JSON.stringify({ + type: "result", + result: JSON.stringify({ + type: "result", + subtype: "success", + result: "actual response text", + }), + }), + }), + ].join("\n"), + { + command: "claude", + output: "jsonl", + sessionIdFields: ["session_id"], + }, + "claude-cli", + ); + + expect(result).toEqual({ + text: "actual response text", + sessionId: "session-nested-jsonl", + usage: undefined, + }); + }); + it("parses multiple JSON objects embedded on the same line", () => { const result = parseCliJsonl( '{"type":"init","session_id":"session-999"} {"type":"result","session_id":"session-999","result":"done"}', diff --git a/src/agents/cli-output.ts b/src/agents/cli-output.ts index 25bfd7dbdb3..979b2d10f0a 100644 --- a/src/agents/cli-output.ts +++ b/src/agents/cli-output.ts @@ -193,6 +193,31 @@ function collectCliText(value: unknown): string { return ""; } +function unwrapNestedCliResultText(raw: string): string { + let text = raw; + for (let depth = 0; depth < 8; depth += 1) { + const trimmed = text.trim(); + if (!trimmed.startsWith("{")) { + return text; + } + try { + const parsed = JSON.parse(trimmed); + if ( + !isRecord(parsed) || + typeof parsed.type !== "string" || + parsed.type !== "result" || + typeof parsed.result !== "string" + ) { + return text; + } + text = parsed.result; + } catch { + return text; + } + } + return text; +} + function collectExplicitCliErrorText(parsed: Record): string { const nested = readNestedErrorMessage(parsed); if (nested) { @@ -260,7 +285,7 @@ export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput collectCliText(parsed.result) || collectCliText(parsed.response) || collectCliText(parsed); - const trimmedText = nextText.trim(); + const trimmedText = unwrapNestedCliResultText(nextText).trim(); if (trimmedText) { text = trimmedText; sawStructuredOutput = true; @@ -292,7 +317,7 @@ function parseClaudeCliJsonlResult(params: { params.parsed.type === "result" && typeof params.parsed.result === "string" ) { - const resultText = params.parsed.result.trim(); + const resultText = unwrapNestedCliResultText(params.parsed.result).trim(); if (resultText) { return { text: resultText, sessionId: params.sessionId, usage: params.usage }; }