Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions actions/setup/js/log_parser_bootstrap.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Comment on lines +283 to 287

// Add safe outputs preview to step summary
Expand Down Expand Up @@ -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,
});
Comment on lines +316 to 320

// Write wrapped markdown to step summary if available
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/log_parser_bootstrap.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<details open>\n<summary>Agentic Conversation</summary>\n\n## Parsed Log\n\nSuccess!\n</details>"),
expect(mockCore.summary.addRaw).toHaveBeenCalledWith("<details>\n<summary>Agentic Conversation</summary>\n\n## Parsed Log\n\nSuccess!\n</details>"),
expect(mockCore.summary.write).toHaveBeenCalled(),
fs.unlinkSync(logFile),
fs.rmdirSync(tmpDir));
Expand All @@ -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("<details open>\n<summary>Agentic Conversation</summary>\n\n## Result\n\n</details>"),
expect(mockCore.summary.addRaw).toHaveBeenCalledWith("<details>\n<summary>Agentic Conversation</summary>\n\n## Result\n\n</details>"),
expect(mockCore.setFailed).not.toHaveBeenCalled(),
fs.unlinkSync(logFile),
fs.rmdirSync(tmpDir));
Expand Down
68 changes: 68 additions & 0 deletions actions/setup/js/log_parser_format.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -604,6 +671,7 @@ function createLogParserFormatters(deps) {
lines.push("");
}

appendAwfSteering(lines, awfSteeringMessages);
appendStatistics(lines, renderEntries, toolUsePairs);

return lines;
Expand Down
10 changes: 7 additions & 3 deletions actions/setup/js/parse_copilot_log.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
Expand All @@ -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);
Comment on lines +55 to +57
return;
}
if (Array.isArray(value)) {
Expand Down
43 changes: 42 additions & 1 deletion actions/setup/js/parse_copilot_log.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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.");
});
});
Expand Down Expand Up @@ -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();
Expand Down
Loading