Skip to content

Commit 30a0ebd

Browse files
Copilotpelikhan
andauthored
Render Copilot CLI conversation from events.jsonl, not raw debug logs
- Revert extractWireRequestToolResults (raw [DEBUG] Wire request: parsing) - Fix convertCopilotEventsToLegacyLogEntries to extract tool output from data.result.content (native Copilot CLI events.jsonl format) - Fix error message extraction to use data.error.message (not String(obj)) - Update copilot_sdk_session.cjs to include result in tool.execution_complete events so SDK-based sessions also get tool output previews in events.jsonl - Add test: renders tool output preview from result.content in events.jsonl Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent b30a1c8 commit 30a0ebd

4 files changed

Lines changed: 25 additions & 235 deletions

File tree

actions/setup/js/copilot_sdk_session.cjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* Event mapping:
1111
* SDK "user.message" → JSONL "user.message"
1212
* SDK "tool.execution_start" → JSONL "tool.execution_start" (toolName, mcpServerName)
13-
* SDK "tool.execution_complete" → JSONL "tool.execution_complete" (toolName, mcpServerName, success)
13+
* SDK "tool.execution_complete" → JSONL "tool.execution_complete" (toolName, mcpServerName, success, result)
1414
* SDK "assistant.message" → JSONL "assistant.message" (content)
1515
*
1616
* The JSONL file is written to:
@@ -265,9 +265,12 @@ async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, model, c
265265
const mcpServerName = pending?.mcpServerName ?? "";
266266
if (toolCallId) pendingToolCalls.delete(toolCallId);
267267
const success = event.data?.success ?? !event.data?.error;
268+
// Include result.content (concise LLM-facing output) so that the log
269+
// parser can render tool output previews from events.jsonl directly.
270+
const result = event.data?.result ?? undefined;
268271
// max-tool-denials intentionally tracks permission denials only.
269272
// Tool execution failures are still logged, but do not increment the guardrail counter.
270-
writeEvent("tool.execution_complete", { toolName, mcpServerName, success }, event.timestamp);
273+
writeEvent("tool.execution_complete", { toolName, mcpServerName, success, result }, event.timestamp);
271274
break;
272275
}
273276

actions/setup/js/log_parser_shared.cjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,8 +928,12 @@ function convertCopilotEventsToLegacyLogEntries(logEntries) {
928928
output = data.output;
929929
} else if (typeof data.result === "string") {
930930
output = data.result;
931+
} else if (data.result && typeof data.result.content === "string") {
932+
// Native Copilot CLI events.jsonl format: result.content is the concise
933+
// tool result text sent to the LLM (may be truncated for token efficiency).
934+
output = data.result.content;
931935
} else if (data.error) {
932-
output = String(data.error);
936+
output = typeof data.error === "object" && typeof data.error.message === "string" ? data.error.message : String(data.error);
933937
} else if (success) {
934938
output = "success";
935939
} else {

actions/setup/js/parse_copilot_log.cjs

Lines changed: 0 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -495,97 +495,6 @@ function scanForToolErrors(logContent) {
495495
return toolErrors;
496496
}
497497

498-
/**
499-
* Extracts actual tool output content from Wire request blocks in Copilot CLI debug logs.
500-
* Each Wire request contains the full conversation history up to that point, including
501-
* tool call results from prior turns. Parsing these allows us to populate tool result
502-
* previews instead of showing empty content.
503-
* @param {string[]} lines - Log lines (pre-split)
504-
* @returns {Map<string, string>} Map from tool_call_id to tool output content string
505-
*/
506-
function extractWireRequestToolResults(lines) {
507-
// Maximum number of lines to look ahead within a tool entry for tool_call_id / content
508-
const MAX_TOOL_RESULT_SCAN_LINES = 5;
509-
510-
const toolResultMap = new Map();
511-
let inWireBlock = false;
512-
513-
for (let i = 0; i < lines.length; i++) {
514-
const line = lines[i];
515-
516-
// Wire request starts: a timestamped line containing "[DEBUG] Wire request:"
517-
if (/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] Wire request:/.test(line)) {
518-
inWireBlock = true;
519-
continue;
520-
}
521-
522-
if (inWireBlock) {
523-
// Wire request body lines have no timestamp prefix; a new timestamped line ends the block
524-
if (/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /.test(line)) {
525-
inWireBlock = false;
526-
continue;
527-
}
528-
529-
// Detect tool result entries by role
530-
if (line.includes('"role": "tool"')) {
531-
let toolCallId = null;
532-
let contentValue = null;
533-
534-
// The next few lines contain tool_call_id and content in order
535-
for (let j = i + 1; j < Math.min(i + MAX_TOOL_RESULT_SCAN_LINES, lines.length); j++) {
536-
const nextLine = lines[j];
537-
538-
// Stop if we hit a timestamp line (end of wire block)
539-
if (/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /.test(nextLine)) break;
540-
541-
if (toolCallId === null) {
542-
const toolCallIdIdx = nextLine.indexOf('"tool_call_id":');
543-
if (toolCallIdIdx >= 0) {
544-
// Use JSON.parse to correctly handle any escape sequences in the ID value
545-
const rest = nextLine
546-
.slice(toolCallIdIdx + '"tool_call_id":'.length)
547-
.trim()
548-
.replace(/,\s*$/, "");
549-
try {
550-
const parsed = JSON.parse(rest);
551-
if (typeof parsed === "string") toolCallId = parsed;
552-
} catch (e) {
553-
// Skip unparseable ID
554-
}
555-
}
556-
}
557-
558-
if (contentValue === null) {
559-
const contentIdx = nextLine.indexOf('"content":');
560-
if (contentIdx >= 0) {
561-
// Parse the JSON string value to correctly handle escape sequences
562-
const rest = nextLine
563-
.slice(contentIdx + '"content":'.length)
564-
.trim()
565-
.replace(/,\s*$/, "");
566-
try {
567-
const parsed = JSON.parse(rest);
568-
if (typeof parsed === "string") contentValue = parsed;
569-
} catch (e) {
570-
// Skip non-string content (e.g. null or array)
571-
}
572-
}
573-
}
574-
575-
if (toolCallId !== null && contentValue !== null) break;
576-
}
577-
578-
// Wire requests repeat history across turns; only store the first occurrence of each ID
579-
if (toolCallId !== null && contentValue !== null && !toolResultMap.has(toolCallId)) {
580-
toolResultMap.set(toolCallId, contentValue);
581-
}
582-
}
583-
}
584-
}
585-
586-
return toolResultMap;
587-
}
588-
589498
/**
590499
* Parses Copilot CLI debug log format and reconstructs the conversation flow
591500
* @param {string} logContent - Raw debug log content
@@ -1053,22 +962,6 @@ function parseDebugLogFormat(logContent) {
1053962
}
1054963
}
1055964

1056-
// Populate tool result content from Wire request blocks.
1057-
// Wire requests contain the full conversation history including actual tool outputs,
1058-
// which we use to fill in the preview lines for each tool call.
1059-
const wireToolResults = extractWireRequestToolResults(lines);
1060-
if (wireToolResults.size > 0) {
1061-
for (const entry of entries) {
1062-
if (entry.type === "user" && entry.message?.content) {
1063-
for (const content of entry.message.content) {
1064-
if (content.type === "tool_result" && !content.is_error && wireToolResults.has(content.tool_use_id)) {
1065-
content.content = wireToolResults.get(content.tool_use_id);
1066-
}
1067-
}
1068-
}
1069-
}
1070-
}
1071-
1072965
// Add system init entry at the beginning if we have entries
1073966
if (entries.length > 0) {
1074967
const initEntry = {
@@ -1105,6 +998,5 @@ if (typeof module !== "undefined" && module.exports) {
1105998
main,
1106999
parseCopilotLog,
11071000
parsePrettyPrintFormat,
1108-
extractWireRequestToolResults,
11091001
};
11101002
}

actions/setup/js/parse_copilot_log.test.cjs

Lines changed: 15 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import path from "path";
44

55
describe("parse_copilot_log.cjs", () => {
66
let mockCore, originalConsole, originalProcess;
7-
let main, parseCopilotLog, extractWireRequestToolResults;
7+
let main, parseCopilotLog;
88

99
beforeEach(async () => {
1010
originalConsole = global.console;
@@ -45,7 +45,6 @@ describe("parse_copilot_log.cjs", () => {
4545
const module = await import("./parse_copilot_log.cjs?" + Date.now());
4646
main = module.main;
4747
parseCopilotLog = module.parseCopilotLog;
48-
extractWireRequestToolResults = module.extractWireRequestToolResults;
4948
});
5049

5150
afterEach(() => {
@@ -125,6 +124,20 @@ describe("parse_copilot_log.cjs", () => {
125124
expect(resultData?.numTurns).toBe(1);
126125
});
127126

127+
it("renders tool output preview from result.content in Copilot CLI events.jsonl", () => {
128+
const eventsLog = [
129+
'{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}',
130+
'{"type":"tool.execution_start","timestamp":"2026-06-05T00:44:04.520Z","data":{"toolName":"bash","mcpServerName":""}}',
131+
'{"type":"tool.execution_complete","timestamp":"2026-06-05T00:44:04.700Z","data":{"toolName":"bash","mcpServerName":"","success":true,"result":{"content":"file1.txt\\nfile2.txt\\nfile3.txt"}}}',
132+
'{"type":"assistant.message","timestamp":"2026-06-05T00:44:59.769Z","data":{"content":"Done"}}',
133+
].join("\n");
134+
135+
const result = parseCopilotLog(eventsLog);
136+
137+
expect(result.markdown).toContain("bash");
138+
expect(result.markdown).toContain("file1.txt");
139+
});
140+
128141
it("should handle tool calls with details in HTML format", () => {
129142
const logWithHtmlDetails = JSON.stringify([
130143
{ type: "system", subtype: "init", session_id: "html-test", tools: ["Bash"], model: "gpt-5" },
@@ -503,126 +516,4 @@ describe("parse_copilot_log.cjs", () => {
503516
expect(result.markdown).toContain("safe_outputs::create_issue");
504517
});
505518
});
506-
507-
describe("extractWireRequestToolResults function", () => {
508-
it("should extract tool results from Wire request blocks", () => {
509-
const lines = [
510-
"2026-06-18T16:00:00.000Z [DEBUG] Wire request: {",
511-
' "messages": [',
512-
' { "role": "user", "content": "list files" },',
513-
" {",
514-
' "role": "tool",',
515-
' "tool_call_id": "tooluse_abc123",',
516-
' "content": "file1.txt\\nfile2.txt\\nfile3.txt"',
517-
" }",
518-
" ]",
519-
"}",
520-
"2026-06-18T16:00:01.000Z [INFO] Done",
521-
];
522-
const result = extractWireRequestToolResults(lines);
523-
expect(result.get("tooluse_abc123")).toBe("file1.txt\nfile2.txt\nfile3.txt");
524-
});
525-
526-
it("should skip non-string content values (e.g. null)", () => {
527-
const lines = [
528-
"2026-06-18T16:00:00.000Z [DEBUG] Wire request: {",
529-
' "messages": [',
530-
" {",
531-
' "role": "tool",',
532-
' "tool_call_id": "tooluse_nullcontent",',
533-
' "content": null',
534-
" }",
535-
" ]",
536-
"}",
537-
"2026-06-18T16:00:01.000Z [INFO] Done",
538-
];
539-
const result = extractWireRequestToolResults(lines);
540-
expect(result.has("tooluse_nullcontent")).toBe(false);
541-
});
542-
543-
it("should only store the first occurrence of a tool_call_id (from earlier Wire request)", () => {
544-
const lines = [
545-
// First Wire request: only tool A
546-
"2026-06-18T16:00:00.000Z [DEBUG] Wire request: {",
547-
' "messages": [',
548-
" {",
549-
' "role": "tool",',
550-
' "tool_call_id": "tooluse_A",',
551-
' "content": "result A"',
552-
" }",
553-
" ]",
554-
"}",
555-
// Second Wire request: tool A repeated plus tool B
556-
"2026-06-18T16:00:01.000Z [DEBUG] Wire request: {",
557-
' "messages": [',
558-
" {",
559-
' "role": "tool",',
560-
' "tool_call_id": "tooluse_A",',
561-
' "content": "result A again"',
562-
" },",
563-
" {",
564-
' "role": "tool",',
565-
' "tool_call_id": "tooluse_B",',
566-
' "content": "result B"',
567-
" }",
568-
" ]",
569-
"}",
570-
"2026-06-18T16:00:02.000Z [INFO] Done",
571-
];
572-
const result = extractWireRequestToolResults(lines);
573-
expect(result.get("tooluse_A")).toBe("result A");
574-
expect(result.get("tooluse_B")).toBe("result B");
575-
});
576-
577-
it("should return an empty map when no Wire request blocks are present", () => {
578-
const lines = ["2026-06-18T16:00:00.000Z [DEBUG] data:", "2026-06-18T16:00:00.001Z [DEBUG] {", ' "choices": []', "}", "2026-06-18T16:00:01.000Z [INFO] Done"];
579-
const result = extractWireRequestToolResults(lines);
580-
expect(result.size).toBe(0);
581-
});
582-
});
583-
584-
describe("debug log format tool result previews", () => {
585-
it("should show tool output preview lines from Wire request blocks", () => {
586-
const debugLog = [
587-
"2026-06-18T16:00:00.000Z [INFO] Starting Copilot CLI: 0.0.412",
588-
"2026-06-18T16:00:01.000Z [DEBUG] data:",
589-
"2026-06-18T16:00:01.001Z [DEBUG] {",
590-
' "model": "claude-sonnet-4.6",',
591-
' "usage": { "prompt_tokens": 100, "completion_tokens": 50 },',
592-
' "choices": [',
593-
" {",
594-
' "message": {',
595-
' "content": null,',
596-
' "tool_calls": [',
597-
" {",
598-
' "id": "tooluse_abc123",',
599-
' "type": "function",',
600-
' "function": { "name": "bash", "arguments": "{\\"command\\": \\"ls /tmp\\"}" }',
601-
" }",
602-
" ]",
603-
" }",
604-
" }",
605-
" ]",
606-
"}",
607-
"2026-06-18T16:00:02.000Z [DEBUG] Wire request: {",
608-
' "messages": [',
609-
" {",
610-
' "role": "tool",',
611-
' "tool_call_id": "tooluse_abc123",',
612-
' "content": "file1.txt\\nfile2.txt\\nfile3.txt"',
613-
" }",
614-
" ]",
615-
"}",
616-
"2026-06-18T16:00:03.000Z [INFO] Done",
617-
].join("\n");
618-
619-
const result = parseCopilotLog(debugLog);
620-
621-
// Tool call should be shown
622-
expect(result.markdown).toContain("ls /tmp");
623-
// Preview lines from Wire request should be shown
624-
expect(result.markdown).toContain("file1.txt");
625-
expect(result.markdown).toContain("file2.txt");
626-
});
627-
});
628519
});

0 commit comments

Comments
 (0)