diff --git a/actions/setup/js/parse_pi_log.cjs b/actions/setup/js/parse_pi_log.cjs index 80f91396a83..8641c7000b2 100644 --- a/actions/setup/js/parse_pi_log.cjs +++ b/actions/setup/js/parse_pi_log.cjs @@ -87,6 +87,7 @@ function parsePiLog(logContent) { }, duration_ms: stats.duration_ms || 0, num_turns: stats.turns || 0, + errors: stats.errors && stats.errors.length > 0 ? stats.errors : undefined, }; markdown += generateInformationSection(syntheticEntry); @@ -96,6 +97,7 @@ function parsePiLog(logContent) { type: "result", num_turns: syntheticEntry.num_turns, usage: syntheticEntry.usage, + errors: syntheticEntry.errors, }); } else { markdown += generateInformationSection(null); @@ -365,13 +367,15 @@ function isPiV3ResultError(result) { * finalized turns. * * @param {Array} rawEntries - Raw parsed JSONL entries - * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number}|null} Stats or null when unavailable + * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number, errors:Array}|null} Stats or null when unavailable */ function computePiV3Stats(rawEntries) { let outputTokens = 0; let inputTokens = 0; let turns = 0; let sawUsage = false; + /** @type {Array} */ + const errors = []; for (const raw of rawEntries) { if (raw.type !== "turn_end") { @@ -388,6 +392,14 @@ function computePiV3Stats(rawEntries) { inputTokens += usage.input; } } + // A turn can end with an empty content array and no toolCalls when the provider + // request itself failed (e.g. a 400 model_not_supported response); the only trace + // of the failure is this top-level errorMessage, which would otherwise render as a + // silent, content-free turn in the step summary. + const errorMessage = raw.message && typeof raw.message.errorMessage === "string" ? raw.message.errorMessage : undefined; + if (errorMessage) { + errors.push(errorMessage); + } } if (turns === 0 && !sawUsage) { @@ -399,13 +411,14 @@ function computePiV3Stats(rawEntries) { output_tokens: outputTokens, turns: turns, duration_ms: 0, + errors: errors, }; } /** * Extracts stats from a legacy Pi `result` event, preserving the original flat-schema behavior. * @param {Array} rawEntries - Raw parsed JSONL entries - * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number}|null} Stats or null when absent + * @returns {{input_tokens:number, output_tokens:number, turns:number, duration_ms:number, errors:Array}|null} Stats or null when absent */ function legacyPiStats(rawEntries) { const resultEntry = rawEntries.find(e => e.type === "result"); @@ -418,6 +431,7 @@ function legacyPiStats(rawEntries) { output_tokens: stats.output_tokens || 0, turns: stats.turns || 0, duration_ms: stats.duration_ms || 0, + errors: [], }; } diff --git a/actions/setup/js/parse_pi_log.test.cjs b/actions/setup/js/parse_pi_log.test.cjs index bf3ec24631a..bc7708d1456 100644 --- a/actions/setup/js/parse_pi_log.test.cjs +++ b/actions/setup/js/parse_pi_log.test.cjs @@ -304,6 +304,41 @@ describe("parse_pi_log.cjs", () => { expect(toolResult.message.content[0].is_error).toBe(true); expect(toolResult.message.content[0].content).toContain("boom"); }); + + it("surfaces a provider-level errorMessage from an empty-content turn_end (e.g. model_not_supported)", () => { + // Reproduces a real failed run: the provider request itself fails (HTTP 400), so + // turn_end carries content: [] with no assistant text or tool calls — the only trace + // of the failure is the top-level errorMessage on the turn_end message. + const failedTurnLines = [ + { type: "session", version: 3, id: "s1" }, + { type: "agent_start" }, + { type: "turn_start" }, + { + type: "turn_end", + message: { + role: "assistant", + model: "auto", + content: [], + stopReason: "error", + usage: { input: 0, output: 0 }, + errorMessage: '400: {"message":"The requested model is not supported.","code":"model_not_supported","param":"model","type":"invalid_request_error"}', + }, + toolResults: [], + }, + { type: "agent_end", messages: [], willRetry: false }, + { type: "agent_settled" }, + ]; + const failedLog = failedTurnLines.map(l => JSON.stringify(l)).join("\n"); + + const stats = computePiV3Stats(failedTurnLines); + expect(stats.errors).toEqual([expect.stringContaining("model_not_supported")]); + + const result = parsePiLog(failedLog); + expect(result.markdown).toContain("model_not_supported"); + + const resultEntry = result.logEntries.find(e => e.type === "result"); + expect(resultEntry.errors).toEqual([expect.stringContaining("model_not_supported")]); + }); }); describe("normalizePiToolName", () => {