diff --git a/.changeset/patch-canonical-copilot-event-log.md b/.changeset/patch-canonical-copilot-event-log.md new file mode 100644 index 00000000000..8121679b7c4 --- /dev/null +++ b/.changeset/patch-canonical-copilot-event-log.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Refactored engine log parsing to use the canonical Copilot event format, including normalization of legacy engine log shapes. diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index 4718430c760..4b991a5c414 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -1718,7 +1718,7 @@ jobs: DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_MCP_SCRIPTS_PORT -e GH_AW_MCP_SCRIPTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -e GH_TOKEN -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_51d567a2fd4f7ca5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -1849,9 +1849,11 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -1889,7 +1891,6 @@ jobs: COPILOT_PROVIDER_BASE_URL: ${{ secrets.FOUNDRY_OPENAI_ENDPOINT }} COPILOT_PROVIDER_WIRE_API: responses GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} @@ -1910,7 +1911,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Stop CLI Proxy if: always() continue-on-error: true @@ -2452,7 +2452,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' @@ -2510,9 +2510,10 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -2567,7 +2568,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 99a06b93de1..8482b9d5da4 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -164,8 +164,8 @@ async function runLogParser(options) { // Generate lightweight plain text summary for core.info and Copilot CLI style for step summary if (logEntries && Array.isArray(logEntries) && logEntries.length > 0) { // Extract model from init entry if available - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - const model = initEntry?.model || null; + const initEntry = logEntries.find(entry => (entry.type === "system" && entry.subtype === "init") || entry.type === "session.init"); + const model = initEntry?.model || initEntry?.data?.model || null; const plainTextSummary = generatePlainTextSummary(logEntries, { model, diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs index 4c9a5cd3ebc..43fa01dc37d 100644 --- a/actions/setup/js/log_parser_format.cjs +++ b/actions/setup/js/log_parser_format.cjs @@ -15,6 +15,8 @@ * @property {(text: string) => number} estimateTokens * @property {(ms: number) => string} formatDuration * @property {(text: string) => string} unfenceMarkdown + * @property {(entries: Array) => boolean} isCopilotEventLogEntries + * @property {(entries: Array) => Array} convertCopilotEventsToLegacyLogEntries * @property {number} MAX_AGENT_TEXT_LENGTH * @property {string} SIZE_LIMIT_WARNING */ @@ -48,12 +50,21 @@ function createLogParserFormatters(deps) { estimateTokens, formatDuration, unfenceMarkdown, + isCopilotEventLogEntries, + convertCopilotEventsToLegacyLogEntries, MAX_AGENT_TEXT_LENGTH, SIZE_LIMIT_WARNING, } = deps; const INTERNAL_TOOLS = ["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"]; + function normalizeEntriesForRendering(logEntries) { + if (isCopilotEventLogEntries(logEntries)) { + return convertCopilotEventsToLegacyLogEntries(logEntries); + } + return logEntries; + } + /** * Generates markdown summary from conversation log entries * This is the core shared logic between Claude and Copilot log parsers @@ -70,8 +81,9 @@ function createLogParserFormatters(deps) { */ function generateConversationMarkdown(logEntries, options) { const { formatToolCallback, formatInitCallback, summaryTracker } = options; + const renderEntries = normalizeEntriesForRendering(logEntries); - const toolUsePairs = collectToolUsePairs(logEntries); + const toolUsePairs = collectToolUsePairs(renderEntries); let markdown = ""; let sizeLimitReached = false; @@ -85,7 +97,7 @@ function createLogParserFormatters(deps) { return true; } - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); + const initEntry = renderEntries.find(entry => entry.type === "system" && entry.subtype === "init"); if (initEntry && formatInitCallback) { if (!addContent("## 🚀 Initialization\n\n")) { @@ -110,7 +122,7 @@ function createLogParserFormatters(deps) { return { markdown, commandSummary: [], sizeLimitReached }; } - for (const entry of logEntries) { + for (const entry of renderEntries) { if (sizeLimitReached) break; if (entry.type === "assistant" && entry.message?.content) { @@ -158,7 +170,7 @@ function createLogParserFormatters(deps) { const commandSummary = []; - for (const entry of logEntries) { + for (const entry of renderEntries) { if (entry.type === "assistant" && entry.message?.content) { for (const content of entry.message.content) { if (content.type === "tool_use") { @@ -522,8 +534,9 @@ function createLogParserFormatters(deps) { } function generateSummaryLines(logEntries) { + const renderEntries = normalizeEntriesForRendering(logEntries); const lines = []; - const toolUsePairs = collectToolUsePairs(logEntries); + const toolUsePairs = collectToolUsePairs(renderEntries); const state = { conversationLineCount: 0, @@ -532,7 +545,7 @@ function createLogParserFormatters(deps) { traceEventCount: 0, }; - for (const entry of logEntries) { + for (const entry of renderEntries) { if (state.conversationLineCount >= state.maxConversationLines) { state.conversationTruncated = true; break; @@ -569,7 +582,7 @@ function createLogParserFormatters(deps) { lines.push(""); } - appendStatistics(lines, logEntries, toolUsePairs); + appendStatistics(lines, renderEntries, toolUsePairs); return lines; } diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs index 6fa5f8ee097..e0a5a06bb8a 100644 --- a/actions/setup/js/log_parser_shared.cjs +++ b/actions/setup/js/log_parser_shared.cjs @@ -610,6 +610,388 @@ function parseLogEntries(logContent) { return logEntries; } +/** + * Detects whether entries are in Copilot event log format. + * @param {Array} logEntries + * @returns {boolean} + */ +function isCopilotEventLogEntries(logEntries) { + if (!Array.isArray(logEntries) || logEntries.length === 0) { + return false; + } + + const eventTypePrefixes = ["user.", "assistant.", "tool.", "session."]; + let eventLikeCount = 0; + + for (const entry of logEntries) { + if (!entry || typeof entry !== "object" || typeof entry.type !== "string") continue; + if (entry.type === "assistant" || entry.type === "user" || entry.type === "system" || entry.type === "result") { + return false; + } + if (eventTypePrefixes.some(prefix => entry.type.startsWith(prefix))) { + eventLikeCount++; + } + } + + return eventLikeCount > 0; +} + +/** + * Converts legacy trace entries to Copilot event log format. + * @param {Array} logEntries + * @param {{sourceEngine?: string}} [options] + * @returns {Array} + */ +function convertLegacyLogEntriesToCopilotEvents(logEntries, options = {}) { + if (!Array.isArray(logEntries) || logEntries.length === 0) { + return []; + } + if (isCopilotEventLogEntries(logEntries)) { + return logEntries; + } + + const { sourceEngine = "unknown" } = options; + /** @type {Array} */ + const events = []; + const toolUsesById = new Map(); + + for (const entry of logEntries) { + if (!entry || typeof entry !== "object") continue; + + if (entry.type === "system" && entry.subtype === "init") { + events.push({ + type: "session.init", + data: { + sourceEngine, + model: entry.model, + sessionId: entry.session_id, + cwd: entry.cwd, + tools: Array.isArray(entry.tools) ? entry.tools : [], + mcpServers: Array.isArray(entry.mcp_servers) ? entry.mcp_servers : [], + slashCommands: Array.isArray(entry.slash_commands) ? entry.slash_commands : [], + modelInfo: entry.model_info, + }, + }); + continue; + } + + if (entry.type === "system" && entry.subtype && entry.subtype !== "init") { + if (entry.message?.content && Array.isArray(entry.message.content)) { + for (const content of entry.message.content) { + if (content?.type === "text" && typeof content.text === "string" && content.text.trim()) { + events.push({ + type: "assistant.message", + data: { content: content.text }, + }); + } + } + } else if (typeof entry.message === "string" && entry.message.trim()) { + events.push({ + type: "assistant.message", + data: { content: entry.message }, + }); + } + continue; + } + + if (entry.type === "assistant" && entry.message?.content && Array.isArray(entry.message.content)) { + for (const content of entry.message.content) { + if (!content || typeof content !== "object") continue; + + if (content.type === "text" && typeof content.text === "string" && content.text.trim()) { + events.push({ + type: "assistant.message", + data: { content: content.text }, + }); + } else if (content.type === "thinking" && typeof content.thinking === "string" && content.thinking.trim()) { + events.push({ + type: "assistant.reasoning", + data: { content: content.thinking }, + }); + } else if (content.type === "tool_use") { + const toolCallId = typeof content.id === "string" && content.id.trim() ? content.id : `tool_${events.length + 1}`; + toolUsesById.set(toolCallId, content); + events.push({ + type: "tool.execution_start", + data: { + toolCallId, + toolName: content.name, + input: content.input || {}, + }, + }); + } + } + continue; + } + + if (entry.type === "user" && entry.message?.content && Array.isArray(entry.message.content)) { + for (const content of entry.message.content) { + if (!content || content.type !== "tool_result") continue; + const toolCallId = typeof content.tool_use_id === "string" && content.tool_use_id.trim() ? content.tool_use_id : `tool_${events.length + 1}`; + const toolUse = toolUsesById.get(toolCallId); + events.push({ + type: "tool.execution_complete", + data: { + toolCallId, + toolName: toolUse?.name, + success: content.is_error !== true, + output: content.content, + durationMs: content.duration_ms, + }, + }); + } + continue; + } + + if (entry.type === "result") { + events.push({ + type: "session.result", + data: { + numTurns: entry.num_turns, + durationMs: entry.duration_ms, + totalCostUsd: entry.total_cost_usd, + usage: entry.usage, + errors: entry.errors, + permissionDenials: entry.permission_denials, + }, + }); + } + } + + return events; +} + +/** + * Converts Copilot event log entries to legacy trace entries used by renderers. + * @param {Array} logEntries + * @returns {Array} + */ +function convertCopilotEventsToLegacyLogEntries(logEntries) { + if (!Array.isArray(logEntries) || logEntries.length === 0) { + return []; + } + if (!isCopilotEventLogEntries(logEntries)) { + return logEntries; + } + + /** @type {Array} */ + const normalizedEntries = []; + const pendingByToolCallId = new Map(); + const pendingIdsByToolName = new Map(); + let toolCounter = 0; + let turnCount = 0; + let assistantMessageCount = 0; + + const addPendingId = (toolName, toolId) => { + const existing = pendingIdsByToolName.get(toolName); + if (existing) { + existing.push(toolId); + return; + } + pendingIdsByToolName.set(toolName, [toolId]); + }; + + const shiftPendingId = toolName => { + const existing = pendingIdsByToolName.get(toolName); + if (!existing || existing.length === 0) return null; + const toolId = existing.shift(); + if (existing.length === 0) { + pendingIdsByToolName.delete(toolName); + } + return toolId || null; + }; + + const removePendingId = (toolName, toolId) => { + const existing = pendingIdsByToolName.get(toolName); + if (!existing || existing.length === 0) return; + const idx = existing.indexOf(toolId); + if (idx === -1) return; + existing.splice(idx, 1); + if (existing.length === 0) { + pendingIdsByToolName.delete(toolName); + } + }; + + const normalizeToolName = (rawToolName, mcpServerName) => { + const toolName = typeof rawToolName === "string" && rawToolName.trim() ? rawToolName.trim() : "unknown"; + if (toolName.startsWith("mcp__")) { + return toolName; + } + const serverName = typeof mcpServerName === "string" ? mcpServerName.trim() : ""; + if (!serverName) { + return toolName; + } + return `mcp__${serverName}__${toolName}`; + }; + + const readString = (...values) => { + for (const value of values) { + if (typeof value === "string") return value; + } + return ""; + }; + + for (const entry of logEntries) { + if (!entry || typeof entry !== "object") continue; + const data = entry.data && typeof entry.data === "object" ? entry.data : {}; + + switch (entry.type) { + case "session.init": + normalizedEntries.push({ + type: "system", + subtype: "init", + model: data.model, + session_id: data.sessionId, + cwd: data.cwd, + tools: Array.isArray(data.tools) ? data.tools : [], + mcp_servers: Array.isArray(data.mcpServers) ? data.mcpServers : [], + slash_commands: Array.isArray(data.slashCommands) ? data.slashCommands : [], + model_info: data.modelInfo, + }); + break; + + case "user.message": + turnCount++; + break; + + case "assistant.message": { + const text = readString(data.content, data.message); + if (!text.trim()) break; + assistantMessageCount++; + normalizedEntries.push({ + type: "assistant", + message: { + content: [{ type: "text", text }], + }, + }); + break; + } + + case "assistant.reasoning": + case "reasoning": { + const text = typeof data.content === "string" ? data.content : ""; + if (!text.trim()) break; + normalizedEntries.push({ + type: "assistant", + message: { + content: [{ type: "thinking", thinking: text }], + }, + }); + break; + } + + case "tool.execution_start": { + const toolName = normalizeToolName(data.toolName, data.mcpServerName); + const toolCallId = typeof data.toolCallId === "string" && data.toolCallId.trim() ? data.toolCallId : null; + const resolvedToolId = toolCallId || `sdk_tool_${++toolCounter}`; + if (toolCallId) { + pendingByToolCallId.set(toolCallId, resolvedToolId); + } + addPendingId(toolName, resolvedToolId); + normalizedEntries.push({ + type: "assistant", + message: { + content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: data.input || data.parameters || {} }], + }, + }); + break; + } + + case "tool.execution_complete": { + const toolName = normalizeToolName(data.toolName, data.mcpServerName); + const toolCallId = typeof data.toolCallId === "string" && data.toolCallId.trim() ? data.toolCallId : null; + let resolvedToolId = null; + + if (toolCallId && pendingByToolCallId.has(toolCallId)) { + resolvedToolId = pendingByToolCallId.get(toolCallId); + pendingByToolCallId.delete(toolCallId); + if (resolvedToolId) { + removePendingId(toolName, resolvedToolId); + } + } + if (!resolvedToolId) { + resolvedToolId = shiftPendingId(toolName); + } + if (!resolvedToolId) { + resolvedToolId = `sdk_tool_${++toolCounter}`; + normalizedEntries.push({ + type: "assistant", + message: { + content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: data.input || data.parameters || {} }], + }, + }); + } + + const success = typeof data.success === "boolean" ? data.success : !data.error; + let output = ""; + if (typeof data.output === "string") { + output = data.output; + } else if (typeof data.result === "string") { + output = data.result; + } else if (data.error) { + output = String(data.error); + } else if (success) { + output = "success"; + } else { + output = "Tool execution failed"; + } + + normalizedEntries.push({ + type: "user", + message: { + content: [ + { + type: "tool_result", + tool_use_id: resolvedToolId, + content: output, + is_error: !success, + duration_ms: typeof data.durationMs === "number" ? data.durationMs : undefined, + }, + ], + }, + }); + break; + } + + case "session.result": { + const usage = data.usage && typeof data.usage === "object" ? data.usage : {}; + normalizedEntries.push({ + type: "result", + num_turns: typeof data.numTurns === "number" ? data.numTurns : undefined, + duration_ms: typeof data.durationMs === "number" ? data.durationMs : undefined, + total_cost_usd: typeof data.totalCostUsd === "number" ? data.totalCostUsd : undefined, + usage: { + input_tokens: usage.input_tokens ?? usage.inputTokens, + output_tokens: usage.output_tokens ?? usage.outputTokens, + cache_creation_input_tokens: usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens, + cache_read_input_tokens: usage.cache_read_input_tokens ?? usage.cacheReadInputTokens, + }, + errors: Array.isArray(data.errors) ? data.errors : undefined, + permission_denials: Array.isArray(data.permissionDenials) ? data.permissionDenials : undefined, + }); + break; + } + + default: + break; + } + } + + if (normalizedEntries.length === 0) { + return []; + } + + const hasResult = normalizedEntries.some(entry => entry.type === "result"); + if (!hasResult) { + normalizedEntries.push({ + type: "result", + num_turns: turnCount > 0 ? turnCount : assistantMessageCount, + }); + } + + return normalizedEntries; +} + const { generateConversationMarkdown, formatToolUse, generatePlainTextSummary, generateCopilotCliStyleSummary } = createLogParserFormatters({ formatBashCommand, formatMcpName, @@ -621,6 +1003,8 @@ const { generateConversationMarkdown, formatToolUse, generatePlainTextSummary, g estimateTokens, formatDuration, unfenceMarkdown, + isCopilotEventLogEntries, + convertCopilotEventsToLegacyLogEntries, MAX_AGENT_TEXT_LENGTH, SIZE_LIMIT_WARNING, }); @@ -963,6 +1347,9 @@ module.exports = { formatToolDisplayName, formatInitializationSummary, formatToolUse, + isCopilotEventLogEntries, + convertLegacyLogEntriesToCopilotEvents, + convertCopilotEventsToLegacyLogEntries, parseLogEntries, formatToolCallAsDetails, formatResultPreview, diff --git a/actions/setup/js/parse_antigravity_log.cjs b/actions/setup/js/parse_antigravity_log.cjs index f5bb0c16adb..cf38e08b240 100644 --- a/actions/setup/js/parse_antigravity_log.cjs +++ b/actions/setup/js/parse_antigravity_log.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { createEngineLogParser, generateInformationSection } = require("./log_parser_shared.cjs"); +const { createEngineLogParser, generateInformationSection, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs"); const main = createEngineLogParser({ parserName: "Antigravity", @@ -112,9 +112,11 @@ function parseAntigravityLog(logContent) { }); } + const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "antigravity" }); + return { markdown, - logEntries, + logEntries: canonicalLogEntries, mcpFailures: [], maxTurnsHit: false, }; diff --git a/actions/setup/js/parse_antigravity_log.test.cjs b/actions/setup/js/parse_antigravity_log.test.cjs index 699a984a53d..ef1b94e7167 100644 --- a/actions/setup/js/parse_antigravity_log.test.cjs +++ b/actions/setup/js/parse_antigravity_log.test.cjs @@ -61,7 +61,7 @@ describe("parse_antigravity_log.cjs", () => { const result = parseAntigravityLog(logContent); expect(result.markdown).toContain("Final answer"); - expect(result.logEntries).toHaveLength(1); + expect(result.logEntries.some(e => e.type === "assistant.message")).toBe(true); }); it("should use the last valid JSON line as the final response", () => { @@ -71,8 +71,9 @@ describe("parse_antigravity_log.cjs", () => { expect(result.markdown).toContain("Complete final answer"); expect(result.markdown).not.toContain("Partial answer"); - expect(result.logEntries).toHaveLength(1); - expect(result.logEntries[0].message.content[0].text).toBe("Complete final answer"); + const assistantMsg = result.logEntries.find(e => e.type === "assistant.message"); + expect(assistantMsg).toBeDefined(); + expect(assistantMsg.data?.content).toBe("Complete final answer"); }); it("should aggregate token counts across multiple models", () => { @@ -110,8 +111,7 @@ describe("parse_antigravity_log.cjs", () => { expect(result.markdown).toContain("Hello from Antigravity"); expect(result.markdown).toContain("500"); expect(result.markdown).toContain("200"); - expect(result.logEntries).toHaveLength(1); - expect(result.logEntries[0].type).toBe("assistant"); + expect(result.logEntries.some(e => e.type === "assistant.message")).toBe(true); }); it("should handle missing stats gracefully", () => { @@ -120,7 +120,7 @@ describe("parse_antigravity_log.cjs", () => { const result = parseAntigravityLog(logContent); expect(result.markdown).toContain("Response without stats"); - expect(result.logEntries).toHaveLength(1); + expect(result.logEntries.some(e => e.type === "assistant.message")).toBe(true); }); it("should handle empty response in the last JSONL entry", () => { diff --git a/actions/setup/js/parse_claude_log.cjs b/actions/setup/js/parse_claude_log.cjs index 1e20f934576..b681975f931 100644 --- a/actions/setup/js/parse_claude_log.cjs +++ b/actions/setup/js/parse_claude_log.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries } = require("./log_parser_shared.cjs"); +const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs"); const main = createEngineLogParser({ parserName: "Claude", @@ -30,7 +30,8 @@ function parseClaudeLog(logContent) { const mcpFailures = []; // Generate conversation markdown using shared function - const conversationResult = generateConversationMarkdown(logEntries, { + const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "claude" }); + const conversationResult = generateConversationMarkdown(canonicalLogEntries, { formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }), formatInitCallback: initEntry => { const result = formatInitializationSummary(initEntry, { @@ -98,7 +99,7 @@ function parseClaudeLog(logContent) { } } - return { markdown, mcpFailures, maxTurnsHit, logEntries }; + return { markdown, mcpFailures, maxTurnsHit, logEntries: canonicalLogEntries }; } // Export for testing diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index 36a88218f66..e61c1e5638c 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { createEngineLogParser, truncateString, estimateTokens, formatToolCallAsDetails } = require("./log_parser_shared.cjs"); +const { createEngineLogParser, truncateString, estimateTokens, formatToolCallAsDetails, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs"); const main = createEngineLogParser({ parserName: "Codex", @@ -643,9 +643,11 @@ function parseCodexLog(logContent) { // Check for MCP failures const mcpFailures = mcpInfo.servers.filter(server => server.status === "failed").map(server => server.name); + const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "codex" }); + return { markdown, - logEntries, + logEntries: canonicalLogEntries, mcpFailures, maxTurnsHit: false, // Codex doesn't have max-turns concept in logs }; diff --git a/actions/setup/js/parse_codex_log.test.cjs b/actions/setup/js/parse_codex_log.test.cjs index ccff5384d34..4a824dbf8a3 100644 --- a/actions/setup/js/parse_codex_log.test.cjs +++ b/actions/setup/js/parse_codex_log.test.cjs @@ -41,6 +41,8 @@ describe("parse_codex_log.cjs", () => { }); describe("parseCodexLog function", () => { + const getEventData = (entries, eventType) => entries.filter(e => e.type === eventType).map(e => e.data || {}); + it("should parse basic tool call with success", () => { const logContent = `tool github.list_pull_requests({"state":"open"}) github.list_pull_requests(...) success in 123ms: @@ -213,13 +215,9 @@ github.list_pull_requests(...) success in 123ms: expect(result.logEntries).toBeDefined(); expect(result.logEntries.length).toBeGreaterThan(0); - // Should contain a tool_use entry for the tool call - const assistantEntries = result.logEntries.filter(e => e.type === "assistant"); - expect(assistantEntries.length).toBeGreaterThan(0); - - const toolUseEntry = assistantEntries.find(e => e.message?.content?.some(c => c.type === "tool_use")); - expect(toolUseEntry).toBeDefined(); - expect(toolUseEntry.message.content[0].name).toBe("github__list_pull_requests"); + const toolStarts = getEventData(result.logEntries, "tool.execution_start"); + expect(toolStarts.length).toBeGreaterThan(0); + expect(toolStarts[0].toolName).toBe("github__list_pull_requests"); }); it("should populate logEntries with response for new-format tool calls", () => { @@ -231,13 +229,9 @@ github.create_issue(...) success in 200ms: expect(result.logEntries.length).toBeGreaterThan(0); - // Should have a tool_result entry (user entry with tool_result) - const userEntries = result.logEntries.filter(e => e.type === "user"); - expect(userEntries.length).toBeGreaterThan(0); - - const toolResultEntry = userEntries.find(e => e.message?.content?.some(c => c.type === "tool_result")); - expect(toolResultEntry).toBeDefined(); - expect(toolResultEntry.message.content[0].is_error).toBe(false); + const toolCompletes = getEventData(result.logEntries, "tool.execution_complete"); + expect(toolCompletes.length).toBeGreaterThan(0); + expect(toolCompletes[0].success).toBe(true); }); it("should mark failed new-format tool calls as errors in logEntries", () => { @@ -249,10 +243,9 @@ github.create_issue(...) failed in 100ms: expect(result.logEntries.length).toBeGreaterThan(0); - const userEntries = result.logEntries.filter(e => e.type === "user"); - const toolResultEntry = userEntries.find(e => e.message?.content?.some(c => c.type === "tool_result")); - expect(toolResultEntry).toBeDefined(); - expect(toolResultEntry.message.content[0].is_error).toBe(true); + const toolCompletes = getEventData(result.logEntries, "tool.execution_complete"); + expect(toolCompletes.length).toBeGreaterThan(0); + expect(toolCompletes[0].success).toBe(false); }); it("should handle tokens with commas in final count", () => { @@ -681,7 +674,7 @@ github.list_pull_requests(...) success in 123ms: it("should always include a system init entry", () => { const result = parseCodexLog("thinking\nsome thinking here"); - const initEntry = result.logEntries.find(e => e.type === "system" && e.subtype === "init"); + const initEntry = result.logEntries.find(e => e.type === "session.init"); expect(initEntry).toBeDefined(); }); @@ -703,9 +696,9 @@ Some analysis here`; const result = parseCodexLog(logContent); - const initEntry = result.logEntries.find(e => e.type === "system" && e.subtype === "init"); + const initEntry = result.logEntries.find(e => e.type === "session.init"); expect(initEntry).toBeDefined(); - expect(initEntry.model).toBe("gpt-4o"); + expect(initEntry.data?.model).toBe("gpt-4o"); }); it("should still include system init entry when model is absent from log", () => { @@ -714,9 +707,9 @@ Some analysis here`; const result = parseCodexLog(logContent); - const initEntry = result.logEntries.find(e => e.type === "system" && e.subtype === "init"); + const initEntry = result.logEntries.find(e => e.type === "session.init"); expect(initEntry).toBeDefined(); - expect(initEntry.model).toBeUndefined(); + expect(initEntry.data?.model).toBeUndefined(); }); it("should add error messages as assistant entries when there are no tool calls", () => { @@ -725,11 +718,9 @@ ERROR: cyber_policy_violation`; const result = parseCodexLog(logContent); - const assistantEntries = result.logEntries.filter(e => e.type === "assistant"); - expect(assistantEntries.length).toBeGreaterThan(0); - const textContent = assistantEntries.flatMap(e => e.message?.content || []).find(c => c.type === "text"); - expect(textContent).toBeDefined(); - expect(textContent.text).toContain("cyber_policy_violation"); + const assistantMessages = result.logEntries.filter(e => e.type === "assistant.message"); + expect(assistantMessages.length).toBeGreaterThan(0); + expect(assistantMessages[0].data?.content).toContain("cyber_policy_violation"); }); it("should add reconnect count as assistant entry when no tool calls and reconnects occurred", () => { @@ -739,11 +730,10 @@ ERROR: connection lost`; const result = parseCodexLog(logContent); - const assistantEntries = result.logEntries.filter(e => e.type === "assistant"); - const textContents = assistantEntries.flatMap(e => e.message?.content || []).filter(c => c.type === "text"); - const reconnectEntry = textContents.find(c => c.text.includes("Reconnect attempts:")); + const assistantMessages = result.logEntries.filter(e => e.type === "assistant.message"); + const reconnectEntry = assistantMessages.find(c => (c.data?.content || "").includes("Reconnect attempts:")); expect(reconnectEntry).toBeDefined(); - expect(reconnectEntry.text).toContain("2/3"); + expect(reconnectEntry.data?.content).toContain("2/3"); }); it("should not add error assistant entries when tool calls are present", () => { @@ -754,12 +744,11 @@ github.list_issues(...) success in 50ms: const result = parseCodexLog(logContent); - const assistantEntries = result.logEntries.filter(e => e.type === "assistant"); - const toolUseEntries = assistantEntries.filter(e => e.message?.content?.some(c => c.type === "tool_use")); + const toolUseEntries = result.logEntries.filter(e => e.type === "tool.execution_start"); expect(toolUseEntries.length).toBeGreaterThan(0); // Error messages should NOT be added as extra assistant text entries - const errorTextEntries = assistantEntries.filter(e => e.message?.content?.some(c => c.type === "text" && c.text.includes("some error"))); + const errorTextEntries = result.logEntries.filter(e => e.type === "assistant.message" && (e.data?.content || "").includes("some error")); expect(errorTextEntries.length).toBe(0); }); diff --git a/actions/setup/js/parse_copilot_log.cjs b/actions/setup/js/parse_copilot_log.cjs index dc1388e95fb..c94b994e0d5 100644 --- a/actions/setup/js/parse_copilot_log.cjs +++ b/actions/setup/js/parse_copilot_log.cjs @@ -1,7 +1,18 @@ // @ts-check /// -const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries, AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs"); +const { + createEngineLogParser, + generateConversationMarkdown, + generateInformationSection, + formatInitializationSummary, + formatToolUse, + parseLogEntries, + AWF_INFRA_LINE_RE, + isCopilotEventLogEntries, + convertLegacyLogEntriesToCopilotEvents, + convertCopilotEventsToLegacyLogEntries, +} = require("./log_parser_shared.cjs"); const { ERR_PARSE } = require("./error_codes.cjs"); const main = createEngineLogParser({ @@ -52,10 +63,12 @@ function extractAwfTokenWarnings(logEntries) { if (typeof value.message === "string") addMatches(value.message); if (typeof value.content === "string") addMatches(value.content); if (typeof value.system === "string") addMatches(value.system); + if (typeof value.data?.content === "string") addMatches(value.data.content); if (Array.isArray(value.content)) visit(value.content); if (Array.isArray(value.message?.content)) visit(value.message.content); if (Array.isArray(value.system)) visit(value.system); + if (value.data && typeof value.data === "object") visit(value.data); }; for (const entry of logEntries) visit(entry); @@ -69,208 +82,7 @@ function extractAwfTokenWarnings(logEntries) { * @returns {boolean} */ function isCopilotSdkEventsFormat(logEntries) { - if (!Array.isArray(logEntries) || logEntries.length === 0) { - return false; - } - - const sdkEventTypes = new Set(["user.message", "assistant.message", "tool.execution_start", "tool.execution_complete", "reasoning", "assistant.reasoning"]); - let sdkLikeCount = 0; - - for (const entry of logEntries) { - if (!entry || typeof entry !== "object") continue; - if (entry.type === "assistant" || entry.type === "user" || entry.type === "system" || entry.type === "result") { - return false; - } - if (typeof entry.type === "string" && sdkEventTypes.has(entry.type) && typeof entry.data === "object" && entry.data !== null) { - sdkLikeCount++; - } - } - - return sdkLikeCount > 0; -} - -/** - * Converts Copilot SDK events.jsonl entries into the normalized trace format - * expected by generateConversationMarkdown and copilot-style summary renderers. - * @param {Array} sdkEntries - * @returns {Array} - */ -function normalizeCopilotSdkEventsToTrace(sdkEntries) { - /** @type {Array} */ - const normalizedEntries = []; - const toolNames = new Set(); - const pendingByToolCallId = new Map(); - const pendingIdsByToolName = new Map(); - let toolCounter = 0; - let turnCount = 0; - let assistantMessageCount = 0; - let firstTimestampMs = null; - let lastTimestampMs = null; - - const addPendingId = (toolName, toolId) => { - const existing = pendingIdsByToolName.get(toolName); - if (existing) { - existing.push(toolId); - return; - } - pendingIdsByToolName.set(toolName, [toolId]); - }; - - const shiftPendingId = toolName => { - const existing = pendingIdsByToolName.get(toolName); - if (!existing || existing.length === 0) return null; - const toolId = existing.shift(); - if (existing.length === 0) { - pendingIdsByToolName.delete(toolName); - } - return toolId || null; - }; - - const removePendingId = (toolName, toolId) => { - const existing = pendingIdsByToolName.get(toolName); - if (!existing || existing.length === 0) return; - const idx = existing.indexOf(toolId); - if (idx === -1) return; - existing.splice(idx, 1); - if (existing.length === 0) { - pendingIdsByToolName.delete(toolName); - } - }; - - const normalizeToolName = (rawToolName, mcpServerName) => { - const toolName = typeof rawToolName === "string" && rawToolName.trim() ? rawToolName.trim() : "unknown"; - if (toolName.startsWith("mcp__")) { - return toolName; - } - const serverName = typeof mcpServerName === "string" ? mcpServerName.trim() : ""; - if (!serverName) { - return toolName; - } - return `mcp__${serverName}__${toolName}`; - }; - - const maybeTrackTimestamp = timestamp => { - if (typeof timestamp !== "string") return; - const ms = new Date(timestamp).getTime(); - if (!Number.isFinite(ms)) return; - if (firstTimestampMs === null || ms < firstTimestampMs) { - firstTimestampMs = ms; - } - if (lastTimestampMs === null || ms > lastTimestampMs) { - lastTimestampMs = ms; - } - }; - - for (const entry of sdkEntries) { - if (!entry || typeof entry !== "object") continue; - maybeTrackTimestamp(entry.timestamp); - - switch (entry.type) { - case "user.message": - turnCount++; - break; - - case "assistant.message": { - assistantMessageCount++; - const text = typeof entry.data?.content === "string" ? entry.data.content.trim() : ""; - if (!text) break; - normalizedEntries.push({ - type: "assistant", - message: { - content: [{ type: "text", text }], - }, - }); - break; - } - - case "tool.execution_start": { - const toolName = normalizeToolName(entry.data?.toolName, entry.data?.mcpServerName); - toolNames.add(toolName); - const toolCallId = typeof entry.data?.toolCallId === "string" && entry.data.toolCallId.trim() ? entry.data.toolCallId : null; - const resolvedToolId = toolCallId || `sdk_tool_${++toolCounter}`; - if (toolCallId) { - pendingByToolCallId.set(toolCallId, resolvedToolId); - } - addPendingId(toolName, resolvedToolId); - normalizedEntries.push({ - type: "assistant", - message: { - content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: {} }], - }, - }); - break; - } - - case "tool.execution_complete": { - const toolName = normalizeToolName(entry.data?.toolName, entry.data?.mcpServerName); - toolNames.add(toolName); - const toolCallId = typeof entry.data?.toolCallId === "string" && entry.data.toolCallId.trim() ? entry.data.toolCallId : null; - let resolvedToolId = null; - - if (toolCallId && pendingByToolCallId.has(toolCallId)) { - resolvedToolId = pendingByToolCallId.get(toolCallId); - pendingByToolCallId.delete(toolCallId); - if (resolvedToolId) { - removePendingId(toolName, resolvedToolId); - } - } - if (!resolvedToolId) { - resolvedToolId = shiftPendingId(toolName); - } - if (!resolvedToolId) { - resolvedToolId = `sdk_tool_${++toolCounter}`; - normalizedEntries.push({ - type: "assistant", - message: { - content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: {} }], - }, - }); - } - - const success = typeof entry.data?.success === "boolean" ? entry.data.success : !entry.data?.error; - normalizedEntries.push({ - type: "user", - message: { - content: [ - { - type: "tool_result", - tool_use_id: resolvedToolId, - content: success ? "success" : "Tool execution failed", - is_error: !success, - }, - ], - }, - }); - break; - } - - default: - break; - } - } - - if (normalizedEntries.length === 0) { - return []; - } - - normalizedEntries.unshift({ - type: "system", - subtype: "init", - model: "copilot-sdk", - session_id: null, - tools: Array.from(toolNames), - }); - - const resultEntry = { - type: "result", - num_turns: turnCount > 0 ? turnCount : assistantMessageCount, - }; - if (firstTimestampMs !== null && lastTimestampMs !== null && lastTimestampMs >= firstTimestampMs) { - resultEntry.duration_ms = lastTimestampMs - firstTimestampMs; - } - normalizedEntries.push(resultEntry); - - return normalizedEntries; + return isCopilotEventLogEntries(logEntries); } /** @@ -310,15 +122,26 @@ function parseCopilotLog(logContent) { return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] }; } - if (isCopilotSdkEventsFormat(logEntries)) { - const normalized = normalizeCopilotSdkEventsToTrace(logEntries); - if (normalized.length > 0) { - logEntries = normalized; - } + const isEventFormat = isCopilotSdkEventsFormat(logEntries); + let canonicalLogEntries = isEventFormat ? logEntries : convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "copilot" }); + const legacyRenderEntries = isEventFormat ? convertCopilotEventsToLegacyLogEntries(canonicalLogEntries) : logEntries; + if (isEventFormat && !canonicalLogEntries.some(entry => entry?.type === "session.result")) { + const legacyResult = legacyRenderEntries.find(entry => entry?.type === "result"); + canonicalLogEntries.push({ + type: "session.result", + data: { + numTurns: legacyResult?.num_turns, + durationMs: legacyResult?.duration_ms, + totalCostUsd: legacyResult?.total_cost_usd, + usage: legacyResult?.usage, + errors: legacyResult?.errors, + permissionDenials: legacyResult?.permission_denials, + }, + }); } // Generate conversation markdown using shared function - const conversationResult = generateConversationMarkdown(logEntries, { + const conversationResult = generateConversationMarkdown(canonicalLogEntries, { formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: true }), formatInitCallback: initEntry => formatInitializationSummary(initEntry, { @@ -364,7 +187,7 @@ function parseCopilotLog(logContent) { }); let markdown = conversationResult.markdown; - const awfTokenWarnings = extractAwfTokenWarnings(logEntries); + const awfTokenWarnings = extractAwfTokenWarnings(canonicalLogEntries); if (awfTokenWarnings.length > 0) { markdown += "## ⚠️ Firewall Steering\n\n"; @@ -375,14 +198,13 @@ function parseCopilotLog(logContent) { } // Add Information section - const lastEntry = logEntries[logEntries.length - 1]; - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); + const lastEntry = legacyRenderEntries[legacyRenderEntries.length - 1]; markdown += generateInformationSection(lastEntry, { additionalInfoCallback: () => "", }); - return { markdown, logEntries }; + return { markdown, logEntries: canonicalLogEntries }; } /** diff --git a/actions/setup/js/parse_copilot_log.test.cjs b/actions/setup/js/parse_copilot_log.test.cjs index b81fe3511c6..89ad0521417 100644 --- a/actions/setup/js/parse_copilot_log.test.cjs +++ b/actions/setup/js/parse_copilot_log.test.cjs @@ -55,6 +55,8 @@ describe("parse_copilot_log.cjs", () => { }); describe("parseCopilotLog function", () => { + const getSessionResultData = entries => entries.find(e => e.type === "session.result")?.data; + it("should parse JSON array format", () => { const jsonArrayLog = JSON.stringify([ { type: "system", subtype: "init", session_id: "copilot-test-123", tools: ["Bash", "Read", "mcp__github__create_issue"], model: "gpt-5" }, @@ -118,8 +120,8 @@ describe("parse_copilot_log.cjs", () => { expect(result.markdown).toContain("🤖 Commands and Tools"); expect(result.markdown).toContain("report_intent"); expect(result.markdown).toContain("Rendered summary content"); - const resultEntry = result.logEntries.find(e => e.type === "result"); - expect(resultEntry?.num_turns).toBe(1); + const resultData = getSessionResultData(result.logEntries); + expect(resultData?.numTurns).toBe(1); }); it("should handle tool calls with details in HTML format", () => { @@ -238,9 +240,9 @@ describe("parse_copilot_log.cjs", () => { // num_turns should be 5 (from Turns: line), not 2 (from toolEntries.length) expect(result.logEntries).toBeDefined(); - const resultEntry = result.logEntries.find(e => e.type === "result"); - expect(resultEntry).toBeDefined(); - expect(resultEntry?.num_turns).toBe(5); + const resultData = getSessionResultData(result.logEntries); + expect(resultData).toBeDefined(); + expect(resultData?.numTurns).toBe(5); }); it("strips harness driver lines from rendered pretty-print output", () => { @@ -276,7 +278,7 @@ describe("parse_copilot_log.cjs", () => { const prettyLog = ["● Bash", " └ ok", "The work is done.", "", "Changes +0 -0", "Duration 11s", "Tokens ↑ 163.9k • ↓ 567 • 149.2k (cached)"].join("\n"); const result = parseCopilotLog(prettyLog); - const resultEntry = result.logEntries.find(e => e.type === "result"); + const resultEntry = getSessionResultData(result.logEntries); expect(resultEntry).toBeDefined(); expect(resultEntry.usage).toEqual( @@ -299,7 +301,7 @@ describe("parse_copilot_log.cjs", () => { const prettyLog = ["● Bash", " └ ok", "The work is done.", "", "Changes +0 -0", "Duration 3m 13s", "Tokens ↑ 422.2k (375.0k cached) • ↓ 2.4k"].join("\n"); const result = parseCopilotLog(prettyLog); - const resultEntry = result.logEntries.find(e => e.type === "result"); + const resultEntry = getSessionResultData(result.logEntries); expect(resultEntry).toBeDefined(); expect(resultEntry.usage).toEqual( @@ -319,7 +321,7 @@ describe("parse_copilot_log.cjs", () => { const prettyLog = ["● Bash", " └ ok", "", "Tokens ↑ 1.2k • ↓ 50"].join("\n"); const result = parseCopilotLog(prettyLog); - const resultEntry = result.logEntries.find(e => e.type === "result"); + const resultEntry = getSessionResultData(result.logEntries); expect(resultEntry.usage.input_tokens).toBe(1200); expect(resultEntry.usage.output_tokens).toBe(50); diff --git a/actions/setup/js/parse_custom_log.test.cjs b/actions/setup/js/parse_custom_log.test.cjs index 7f552f5da96..48845772c81 100644 --- a/actions/setup/js/parse_custom_log.test.cjs +++ b/actions/setup/js/parse_custom_log.test.cjs @@ -21,7 +21,7 @@ describe("parseCustomLog", () => { const result = parseCustomLog(claudeLog); expect(result).toBeDefined(); - expect(result.markdown).toContain("Custom Engine Log (Claude format)"); + expect(result.markdown).toContain("Custom Engine Log"); expect(result.logEntries.length).toBeGreaterThan(0); }); diff --git a/actions/setup/js/parse_gemini_log.cjs b/actions/setup/js/parse_gemini_log.cjs index 30b38a061d4..9c3615eec7f 100644 --- a/actions/setup/js/parse_gemini_log.cjs +++ b/actions/setup/js/parse_gemini_log.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse } = require("./log_parser_shared.cjs"); +const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs"); const main = createEngineLogParser({ parserName: "Gemini", @@ -61,7 +61,8 @@ function parseGeminiLog(logContent) { const resultEntry = rawEntries.find(e => e.type === "result"); // Generate conversation markdown using shared function - const conversationResult = generateConversationMarkdown(logEntries, { + const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "gemini" }); + const conversationResult = generateConversationMarkdown(canonicalLogEntries, { formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }), formatInitCallback: initEntry => formatInitializationSummary(initEntry, { includeSlashCommands: false }), }); @@ -87,7 +88,7 @@ function parseGeminiLog(logContent) { return { markdown, - logEntries, + logEntries: canonicalLogEntries, mcpFailures: [], maxTurnsHit: false, }; diff --git a/actions/setup/js/parse_pi_log.cjs b/actions/setup/js/parse_pi_log.cjs index 31f3cf28ec3..52d539a9bdf 100644 --- a/actions/setup/js/parse_pi_log.cjs +++ b/actions/setup/js/parse_pi_log.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse } = require("./log_parser_shared.cjs"); +const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs"); const main = createEngineLogParser({ parserName: "Pi", @@ -57,7 +57,8 @@ function parsePiLog(logContent) { const resultEntry = rawEntries.find(e => e.type === "result"); - const conversationResult = generateConversationMarkdown(logEntries, { + const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "pi" }); + const conversationResult = generateConversationMarkdown(canonicalLogEntries, { formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }), formatInitCallback: initEntry => formatInitializationSummary(initEntry, { includeSlashCommands: false }), }); @@ -81,7 +82,7 @@ function parsePiLog(logContent) { return { markdown, - logEntries, + logEntries: canonicalLogEntries, mcpFailures: [], maxTurnsHit: false, }; diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 34df2779e61..118e5e2c7cd 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -163,18 +163,18 @@ func ResolveGhAwRef(ctx context.Context, ref string) (string, error) { return ref, nil } resolverLog.Printf("Resolving --gh-aw-ref %q to commit SHA via GitHub API", ref) - apiPath := fmt.Sprintf("/repos/github/gh-aw/commits/%s", ref) + apiPath := "/repos/github/gh-aw/commits/" + ref callCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() -cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha") -output, err := cmd.CombinedOutput() -if err != nil { - msg := strings.TrimSpace(string(output)) - if msg != "" { - return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err) + cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha") + output, err := cmd.CombinedOutput() + if err != nil { + msg := strings.TrimSpace(string(output)) + if msg != "" { + return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err) + } + return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err) } - return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err) -} sha := strings.TrimSpace(string(output)) if !gitutil.IsValidFullSHA(sha) { return "", fmt.Errorf("unexpected response resolving gh-aw ref %q: got %q (expected 40-char hex SHA)", ref, sha)