Skip to content
Merged
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
3 changes: 3 additions & 0 deletions changelog.d/copilot-raw-tool-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed
- Copilot Chat keeps conversations readable when recorded tool arguments are strings, including
non-JSON input, while preserving bounded input display and verified tool output.
26 changes: 26 additions & 0 deletions server/copilot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,32 @@ it("reads a redacted recorded CLI session with root task ancestry and a tool-onl
expect(parseConversationMetadata(recorded, "copilot-transcript").model).toBe("fixture-model");
});

it.each(["assistant.message", "tool.execution_start"])("preserves string arguments from %s with bounded display and verified tool output", (type) => {
const dir = mkdtempSync(join(tmpdir(), "copilot-string-arguments-"));
const path = join(dir, "events.jsonl");
const inputs = ['{ "command": "pwd" }', "{not valid JSON", '"freeform"', "x".repeat(16_001)];
try {
writeFileSync(path, start + prompt("Check") + inputs.map((input, index) => {
const request = { toolCallId: `string-${index}`, arguments: input };
return line(type, type === "assistant.message"
? { toolRequests: [{ ...request, name: "shell" }] }
: { ...request, toolName: "shell" })
+ line("tool.execution_complete", { toolCallId: request.toolCallId, success: false, result: { content: `output ${index} `.repeat(1000) } });
}).join("") + answer("Finished."));
const page = transcriptPage("copilot-transcript", path);
const tools = page.turns.flatMap((turn) => turn.parts).filter((part) => part.kind === "tool");
expect(tools).toHaveLength(inputs.length);
for (const [index, tool] of tools.entries()) {
expect(tool.input).toBe(inputs[index]!.slice(0, 16_000));
expect(tool.error).toBe(true);
expect(tool.output_ref).toMatch(/^string-\d+@/);
expect(transcriptToolOutput("copilot-transcript", path, tool.output_ref!)).toBe(`output ${index} `.repeat(1000));
}
expect(tools[0]?.summary).toBe("pwd");
expect(page.turns.at(-1)?.parts.at(-1)).toEqual({ kind: "text", text: "Finished.", phase: "final_answer" });
} finally { forgetTranscriptState(); rmSync(dir, { recursive: true, force: true }); }
});

it("keeps root prompts carrying parentAgentTaskId and pages at those prompts", () => {
const dir = mkdtempSync(join(tmpdir(), "copilot-root-page-"));
const path = join(dir, "events.jsonl");
Expand Down
10 changes: 7 additions & 3 deletions server/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,14 @@ export function parseCopilotTranscript(jsonl: string, maxTurns = MAX_TURNS, base
const addTool = (data: Row, ts: string | null, name: string, intention: string | null = null, at?: number, raw?: string): void => {
const id = data.toolCallId;
if (typeof id !== "string" || !CALL_ID.test(id) || pending.has(id)) return;
const args = row(data.arguments) ?? {};
const rawArgs = data.arguments;
let args = row(rawArgs) ?? {};
if (typeof rawArgs === "string") {
try { args = row(JSON.parse(rawArgs)) ?? {}; } catch { /* Preserve raw input even when it cannot summarize a call. */ }
}
const part: Extract<ConversationPart, { kind: "tool" }> = {
kind: "tool", name, summary: text(data.toolTitle) ?? intention ?? toolSummary(name, args),
input: JSON.stringify(args, null, 2).slice(0, 16_000), output: "",
input: (typeof rawArgs === "string" ? rawArgs : JSON.stringify(args, null, 2)).slice(0, 16_000), output: "",
};
assistant(ts).parts.push(part);
pending.set(id, { part, requestAt: at, request: raw });
Expand Down Expand Up @@ -128,7 +132,7 @@ export function parseCopilotTranscript(jsonl: string, maxTurns = MAX_TURNS, base
const request = row(candidate);
const name = text(request?.name);
if (request === null || typeof request.toolCallId !== "string" || !CALL_ID.test(request.toolCallId)
|| name === null || (request.arguments != null && row(request.arguments) === null)) {
|| name === null || (request.arguments != null && typeof request.arguments !== "string" && row(request.arguments) === null)) {
throw new ConversationUnavailable("transcript_unreadable");
}
addTool(request, timestamp, name, text(request.intentionSummary), offset, raw);
Expand Down
Loading