diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 2cd007a9de5..93b07d9c61f 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -280,10 +280,10 @@ async function runLogParser(options) { parserName, }); - // Wrap the agent log in a details/summary section (open by default) + // Wrap rendered summary in a closed details/summary section to reduce initial visual clutter. const wrappedAgentLog = wrapAgentLogInSection(copilotCliStyleMarkdown, { parserName, - open: true, + open: false, }); // Add safe outputs preview to step summary @@ -313,10 +313,10 @@ async function runLogParser(options) { } } - // Wrap the original markdown in a details/summary section (open by default) + // Wrap fallback markdown in a closed details/summary section to reduce initial visual clutter. const wrappedAgentLog = wrapAgentLogInSection(markdown, { parserName, - open: true, + open: false, }); // Write wrapped markdown to step summary if available diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index f8e703f1b59..651773e358f 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -46,7 +46,7 @@ describe("log_parser_bootstrap.cjs", () => { (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), expect(mockParseLog).toHaveBeenCalledWith("Test log content"), expect(mockCore.info).toHaveBeenCalledWith("TestParser log parsed successfully"), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith("
\nAgentic Conversation\n\n## Parsed Log\n\nSuccess!\n
"), + expect(mockCore.summary.addRaw).toHaveBeenCalledWith("
\nAgentic Conversation\n\n## Parsed Log\n\nSuccess!\n
"), expect(mockCore.summary.write).toHaveBeenCalled(), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); @@ -58,7 +58,7 @@ describe("log_parser_bootstrap.cjs", () => { const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: !1 }); (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), expect(mockCore.info).toHaveBeenCalledWith("TestParser log parsed successfully"), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith("
\nAgentic Conversation\n\n## Result\n\n
"), + expect(mockCore.summary.addRaw).toHaveBeenCalledWith("
\nAgentic Conversation\n\n## Result\n\n
"), expect(mockCore.setFailed).not.toHaveBeenCalled(), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs index 2ae3fa29438..ed0aaa02e18 100644 --- a/actions/setup/js/log_parser_format.cjs +++ b/actions/setup/js/log_parser_format.cjs @@ -57,6 +57,9 @@ function createLogParserFormatters(deps) { } = deps; const INTERNAL_TOOLS = ["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"]; + const AWF_TOKEN_WARNING_RE = /\[AWF TOKEN WARNING\][^\n\r]+/g; + const AWF_STEERING_MESSAGE_RE = /Agent is still running\.[^\n\r]*A completion notification will arrive as a new turn[^.\n\r]*\.?/g; + const AWF_WAITING_GUIDANCE_RE = /Consider telling the user you're waiting[^.\n\r]*\.?/g; /** * Selects an outer markdown code fence that is longer than any backtick run @@ -500,6 +503,69 @@ function createLogParserFormatters(deps) { appendConversationLine(lines, "", state); } + function collectAwfSteeringMessages(renderEntries) { + /** @type {string[]} */ + const messages = []; + const seen = new Set(); + + const addMatches = (value, pattern) => { + if (typeof value !== "string") return; + const matches = value.match(pattern); + if (!matches) return; + for (const match of matches) { + const normalized = match.trim(); + if (!normalized || seen.has(normalized)) continue; + seen.add(normalized); + messages.push(normalized); + } + }; + + const addFromValue = value => { + if (typeof value !== "string") return; + addMatches(value, AWF_TOKEN_WARNING_RE); + addMatches(value, AWF_STEERING_MESSAGE_RE); + addMatches(value, AWF_WAITING_GUIDANCE_RE); + }; + + for (const entry of renderEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "text") addFromValue(content.text); + if (content.type === "thinking") addFromValue(content.thinking); + } + } + if (entry.type === "user" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type !== "tool_result") continue; + if (typeof content.content === "string") { + addFromValue(content.content); + continue; + } + if (content.content && typeof content.content === "object") { + try { + addFromValue(JSON.stringify(content.content)); + } catch { + // ignore non-serializable content + } + } + } + } + } + + return messages; + } + + function appendAwfSteering(lines, messages) { + if (!Array.isArray(messages) || messages.length === 0) { + return; + } + lines.push("AWF Steering:"); + for (const message of messages) { + lines.push(` - ${message}`); + } + lines.push(""); + } + function appendStatistics(lines, logEntries, toolUsePairs) { const lastEntry = logEntries[logEntries.length - 1]; lines.push("Statistics:"); @@ -559,6 +625,7 @@ function createLogParserFormatters(deps) { const renderEntries = normalizeEntriesForRendering(logEntries); const lines = []; const toolUsePairs = collectToolUsePairs(renderEntries); + const awfSteeringMessages = collectAwfSteeringMessages(renderEntries); const state = { conversationLineCount: 0, @@ -604,6 +671,7 @@ function createLogParserFormatters(deps) { lines.push(""); } + appendAwfSteering(lines, awfSteeringMessages); appendStatistics(lines, renderEntries, toolUsePairs); return lines; diff --git a/actions/setup/js/parse_copilot_log.cjs b/actions/setup/js/parse_copilot_log.cjs index c94b994e0d5..a1759fdfa44 100644 --- a/actions/setup/js/parse_copilot_log.cjs +++ b/actions/setup/js/parse_copilot_log.cjs @@ -22,6 +22,8 @@ const main = createEngineLogParser({ }); const AWF_TOKEN_WARNING_RE = /\[AWF TOKEN WARNING\][^\n\r]+/g; +const AWF_STEERING_MESSAGE_RE = /Agent is still running\.[^\n\r]*A completion notification will arrive as a new turn[^.\n\r]*\.?/g; +const AWF_WAITING_GUIDANCE_RE = /Consider telling the user you're waiting[^.\n\r]*\.?/g; /** * Extracts AWF token steering warnings from parsed Copilot log entries. @@ -35,9 +37,9 @@ function extractAwfTokenWarnings(logEntries) { const warnings = []; const seen = new Set(); - const addMatches = value => { + const addMatches = (value, pattern) => { if (typeof value !== "string") return; - const matches = value.match(AWF_TOKEN_WARNING_RE); + const matches = value.match(pattern); if (!matches) return; for (const match of matches) { const normalized = match.trim(); @@ -50,7 +52,9 @@ function extractAwfTokenWarnings(logEntries) { const visit = value => { if (!value) return; if (typeof value === "string") { - addMatches(value); + addMatches(value, AWF_TOKEN_WARNING_RE); + addMatches(value, AWF_STEERING_MESSAGE_RE); + addMatches(value, AWF_WAITING_GUIDANCE_RE); return; } if (Array.isArray(value)) { diff --git a/actions/setup/js/parse_copilot_log.test.cjs b/actions/setup/js/parse_copilot_log.test.cjs index 28acbddbd8a..1cc2998bf88 100644 --- a/actions/setup/js/parse_copilot_log.test.cjs +++ b/actions/setup/js/parse_copilot_log.test.cjs @@ -153,6 +153,20 @@ describe("parse_copilot_log.cjs", () => { expect(result.markdown).toContain("fileB.txt"); }); + it("renders AWF steering guidance from Copilot SDK events.jsonl tool results", () => { + const eventsLog = [ + '{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}', + '{"type":"tool.execution_start","timestamp":"2026-06-05T00:44:04.520Z","data":{"toolName":"read_agent","mcpServerName":""}}', + '{"type":"tool.execution_complete","timestamp":"2026-06-05T00:44:04.700Z","data":{"toolName":"read_agent","mcpServerName":"","success":true,"result":{"content":"Agent is still running. agent_id: helper. Consider telling the user you\'re waiting, then end your response with no further tool calls. A completion notification will arrive as a new turn; no need to poll or redo its work."}}}', + '{"type":"assistant.message","timestamp":"2026-06-05T00:44:59.769Z","data":{"content":"Waiting for sub-agent completion."}}', + ].join("\n"); + + const result = parseCopilotLog(eventsLog); + + expect(result.markdown).toContain("Agent is still running."); + expect(result.markdown).toContain("A completion notification will arrive as a new turn"); + }); + it("should handle tool calls with details in HTML format", () => { const logWithHtmlDetails = JSON.stringify([ { type: "system", subtype: "init", session_id: "html-test", tools: ["Bash"], model: "gpt-5" }, @@ -449,7 +463,6 @@ describe("parse_copilot_log.cjs", () => { const result = parseCopilotLog(structuredLog); - expect(result.markdown).toContain("Firewall Steering"); expect(result.markdown).toContain("[AWF TOKEN WARNING] You have used 90% of your effective token budget."); }); }); @@ -477,6 +490,34 @@ describe("parse_copilot_log.cjs", () => { } }); + it("includes AWF steering guidance in plain logs and step summary", async () => { + const steeringEventsLog = [ + '{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}', + '{"type":"tool.execution_start","timestamp":"2026-06-05T00:44:04.520Z","data":{"toolName":"read_agent","mcpServerName":""}}', + '{"type":"tool.execution_complete","timestamp":"2026-06-05T00:44:04.700Z","data":{"toolName":"read_agent","mcpServerName":"","success":true,"result":{"content":"Agent is still running. agent_id: helper. Consider telling the user you\'re waiting, then end your response with no further tool calls. A completion notification will arrive as a new turn; no need to poll or redo its work."}}}', + '{"type":"assistant.message","timestamp":"2026-06-05T00:44:59.769Z","data":{"content":"Waiting for sub-agent completion."}}', + ].join("\n"); + + const tempFile = path.join(process.cwd(), `test_log_${Date.now()}.jsonl`); + fs.writeFileSync(tempFile, steeringEventsLog); + process.env.GH_AW_AGENT_OUTPUT = tempFile; + + try { + await main(); + + const summaryText = String(mockCore.summary.addRaw.mock.calls[0]?.[0] || ""); + expect(summaryText).toContain("AWF Steering"); + expect(summaryText).toContain("Agent is still running."); + + const hasSteeringInInfo = mockCore.info.mock.calls.some(([message]) => String(message).includes("AWF Steering")); + expect(hasSteeringInInfo).toBe(true); + } finally { + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + } + }); + it("should handle missing log file", async () => { process.env.GH_AW_AGENT_OUTPUT = "/nonexistent/file.log"; await main();