From 1b41513b3be8e0c9da3c6c7f2f1c3062732157fc Mon Sep 17 00:00:00 2001 From: Alec Hrdina Date: Tue, 14 Apr 2026 21:32:18 +0000 Subject: [PATCH] agents/cli: scope nested result unwrapping --- src/agents/cli-output.test.ts | 29 +++++++++++++++++++++++++++++ src/agents/cli-output.ts | 28 +++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/agents/cli-output.test.ts b/src/agents/cli-output.test.ts index c951564dae8..51a390fe3ca 100644 --- a/src/agents/cli-output.test.ts +++ b/src/agents/cli-output.test.ts @@ -143,6 +143,7 @@ describe("parseCliJson", () => { output: "json", sessionIdFields: ["session_id"], }, + "claude-cli", ); expect(result).toEqual({ @@ -152,6 +153,34 @@ describe("parseCliJson", () => { }); }); + it("does not unwrap nested result-shaped JSON for non-claude json backends", () => { + const nestedResult = JSON.stringify({ + type: "result", + result: JSON.stringify({ + type: "result", + result: "actual response text", + }), + }); + const result = parseCliJson( + JSON.stringify({ + session_id: "gemini-session-nested-json", + result: nestedResult, + }), + { + command: "gemini", + output: "json", + sessionIdFields: ["session_id"], + }, + "gemini", + ); + + expect(result).toEqual({ + text: nestedResult, + sessionId: "gemini-session-nested-json", + usage: undefined, + }); + }); + it("parses nested OpenAI-style cached token details from CLI json payloads", () => { const result = parseCliJson( JSON.stringify({ diff --git a/src/agents/cli-output.ts b/src/agents/cli-output.ts index 979b2d10f0a..109335ad500 100644 --- a/src/agents/cli-output.ts +++ b/src/agents/cli-output.ts @@ -266,7 +266,25 @@ function pickCliSessionId( return undefined; } -export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput | null { +function shouldUnwrapNestedCliResultText(params: { + backend: CliBackendConfig; + providerId?: string; + parsed: Record; +}): boolean { + const isClaudeBackend = + (params.providerId && isClaudeCliProvider(params.providerId)) || + /^claude(?:$|[\\/-])/i.test(params.backend.command.trim()); + if (!isClaudeBackend) { + return false; + } + return !Object.hasOwn(params.parsed, "type") || params.parsed.type === "result"; +} + +export function parseCliJson( + raw: string, + backend: CliBackendConfig, + providerId?: string, +): CliOutput | null { const parsedRecords = parseJsonRecordCandidates(raw); if (parsedRecords.length === 0) { return null; @@ -285,7 +303,11 @@ export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput collectCliText(parsed.result) || collectCliText(parsed.response) || collectCliText(parsed); - const trimmedText = unwrapNestedCliResultText(nextText).trim(); + const trimmedText = ( + shouldUnwrapNestedCliResultText({ backend, providerId, parsed }) + ? unwrapNestedCliResultText(nextText) + : nextText + ).trim(); if (trimmedText) { text = trimmedText; sawStructuredOutput = true; @@ -509,7 +531,7 @@ export function parseCliOutput(params: { ); } return ( - parseCliJson(params.raw, params.backend) ?? { + parseCliJson(params.raw, params.backend, params.providerId) ?? { text: params.raw.trim(), sessionId: params.fallbackSessionId, }