Coarsen message stream diagnostics

This commit is contained in:
murashit 2026-06-22 20:53:45 +09:00
parent 7d36b552a0
commit 612782ded9
6 changed files with 44 additions and 43 deletions

View file

@ -1,8 +1,8 @@
import type { CommandMessageStreamTarget, MessageStreamItem } from "../../../domain/message-stream/items";
import type { CommandMessageStreamTarget, MessageStreamDiagnosticSection, MessageStreamItem } from "../../../domain/message-stream/items";
import type { MessageStreamItemProvenance } from "../../../domain/message-stream/provenance";
import type { HistoricalTurn } from "../../../../../domain/threads/history";
import type { TurnItem } from "../../../../../app-server/protocol/turn";
import { definedProp } from "../../../../../utils";
import { definedProp, jsonPreview } from "../../../../../utils";
import { referencedThreadMetadataFromPrompt } from "../../../../../domain/threads/reference";
import { turnUserItemText } from "../../../../../app-server/protocol/turn";
import { agentMessageStreamItem } from "./agent-items";
@ -201,11 +201,14 @@ function mcpToolCallMessageStreamItem(item: McpToolCallItem, turnId?: string): M
...(target ? { primaryTarget: { kind: "value" as const, value: target } } : {}),
...(item.error?.message ? { failureReason: item.error.message } : {}),
status: item.status,
toolCall: {
arguments: item.arguments,
result: item.result,
error: item.error,
},
...definedProp(
"diagnostics",
jsonDiagnosticSections(
{ title: "Arguments JSON", value: item.arguments },
{ title: "Result JSON", value: item.result },
{ title: "Error JSON", value: item.error },
),
),
output: "",
executionState: mcpToolCallExecutionState(item.status),
};
@ -223,10 +226,10 @@ function dynamicToolCallMessageStreamItem(item: DynamicToolCallItem, turnId?: st
...(target ? { primaryTarget: { kind: "value" as const, value: target } } : {}),
...(failure ? { failureReason: failure } : {}),
status: item.status,
toolCall: {
arguments: item.arguments,
result: item.contentItems,
},
...definedProp(
"diagnostics",
jsonDiagnosticSections({ title: "Arguments JSON", value: item.arguments }, { title: "Result JSON", value: item.contentItems }),
),
output: "",
executionState: dynamicToolCallExecutionState(item.status, item.success),
};
@ -502,6 +505,15 @@ function jsonTargetLabel(value: unknown): string | null {
return firstEntry ? jsonTargetPrimitive(firstEntry[1]) : null;
}
function jsonDiagnosticSections(
...sections: readonly { readonly title: string; readonly value: unknown }[]
): readonly MessageStreamDiagnosticSection[] | undefined {
const diagnostics = sections
.filter((section) => section.value !== null && section.value !== undefined)
.map((section) => ({ title: section.title, body: jsonPreview(section.value) }));
return diagnostics.length > 0 ? diagnostics : undefined;
}
function jsonTargetPrimitive(value: unknown): string | null {
if (typeof value === "string" && value.trim().length > 0) return value.trim();
if (typeof value === "number" || typeof value === "boolean") return String(value);

View file

@ -41,10 +41,9 @@ export type MessageStreamPrimaryTarget =
readonly value: string;
};
interface MessageStreamToolCallDetails {
readonly arguments?: unknown;
readonly result?: unknown;
readonly error?: unknown;
export interface MessageStreamDiagnosticSection {
readonly title: string;
readonly body: string;
}
interface MessageStreamWebSearchDetails {
@ -227,7 +226,7 @@ interface ToolMessageStreamBase extends MessageStreamBase {
export interface ToolCallMessageStreamItem extends ToolMessageStreamBase {
readonly kind: "tool";
readonly toolCall?: MessageStreamToolCallDetails;
readonly diagnostics?: readonly MessageStreamDiagnosticSection[];
readonly webSearch?: MessageStreamWebSearchDetails;
readonly imageGeneration?: MessageStreamImageGenerationDetails;
}

View file

@ -1,4 +1,4 @@
import { jsonPreview, shortThreadId, truncate } from "../../../../utils";
import { shortThreadId, truncate } from "../../../../utils";
import { failedStatusLabel } from "../../domain/message-stream/execution-state";
import { pathRelativeToRoot } from "../../domain/message-stream/format/path-labels";
import type {
@ -265,7 +265,7 @@ function resultDetails(item: ApprovalResultMessageStreamItem | ReviewResultMessa
function genericToolDetails(item: ToolCallMessageStreamItem | HookMessageStreamItem): DetailSection[] {
if (item.kind === "hook") return hookRunDetails(item);
return [...toolCallDetails(item), ...webSearchDetails(item), ...imageGenerationDetails(item)];
return [...diagnosticDetails(item), ...webSearchDetails(item), ...imageGenerationDetails(item)];
}
function genericDetailSections(item: MessageStreamItem, workspaceRoot?: string | null): DetailSection[] {
@ -279,14 +279,8 @@ function genericDetailSections(item: MessageStreamItem, workspaceRoot?: string |
return [...(rows.length > 0 ? [{ kind: "kv" as const, rows }] : []), ...outputSection("Output", outputField(item))];
}
function toolCallDetails(item: ToolCallMessageStreamItem): DetailSection[] {
const details = item.toolCall;
if (!details) return [];
return [
...jsonOutputSection("Arguments JSON", details.arguments),
...jsonOutputSection("Result JSON", details.result),
...jsonOutputSection("Error JSON", details.error),
];
function diagnosticDetails(item: ToolCallMessageStreamItem): DetailSection[] {
return item.diagnostics?.map((section) => ({ kind: "output" as const, title: section.title, body: section.body })) ?? [];
}
function webSearchDetails(item: ToolCallMessageStreamItem): DetailSection[] {
@ -328,10 +322,6 @@ function outputSection(title: string, body: string | null | undefined): DetailSe
return body ? [{ kind: "output", title, body }] : [];
}
function jsonOutputSection(title: string, value: unknown): DetailSection[] {
return value === null || value === undefined ? [] : outputSection(title, jsonPreview(value));
}
function metaRow(key: string, value: string | null | undefined): { key: string; value: string }[] {
return value ? [{ key, value }] : [];
}

View file

@ -636,10 +636,10 @@ describe("turn item conversion preserves app-server semantics", () => {
primaryTarget: { kind: "value", value: "123" },
failureReason: "Not found",
toolName: "github.pull_request_read",
toolCall: {
arguments: expect.objectContaining({ id: 123 }),
error: { message: "Not found" },
},
diagnostics: expect.arrayContaining([
{ title: "Arguments JSON", body: expect.stringContaining('"id": 123') },
{ title: "Error JSON", body: expect.stringContaining("Not found") },
]),
executionState: "failed",
});
});
@ -661,10 +661,10 @@ describe("turn item conversion preserves app-server semantics", () => {
kind: "tool",
primaryTarget: { kind: "value", value: "https://example.com" },
toolName: "web.open",
toolCall: {
arguments: { url: "https://example.com" },
result: [{ type: "inputText", text: "ok" }],
},
diagnostics: expect.arrayContaining([
{ title: "Arguments JSON", body: expect.stringContaining("https://example.com") },
{ title: "Result JSON", body: expect.stringContaining("inputText") },
]),
executionState: "completed",
});
});

View file

@ -76,7 +76,7 @@ describe("message stream semantic classification", () => {
changes: [{ kind: "update", path: "src/main.ts", diff: "@@" }],
executionState: "completed",
},
{ id: "tool", kind: "tool", role: "tool", text: "tool", toolCall: { arguments: { k: "v" } } },
{ id: "tool", kind: "tool", role: "tool", text: "tool", diagnostics: [{ title: "Arguments JSON", body: '{"k":"v"}' }] },
{ id: "hook", kind: "hook", role: "tool", text: "hook" },
{ id: "reasoning", kind: "reasoning", role: "tool", text: "thinking" },
{ id: "wait", kind: "wait", role: "tool", text: "Waited 2.5s", executionState: "completed" },

View file

@ -33,10 +33,10 @@ describe("message stream item renderer decisions", () => {
toolName: "github.pull_request_read",
turnId: "turn",
status: "completed",
toolCall: {
arguments: { id: 123 },
result: { ok: true },
},
diagnostics: [
{ title: "Arguments JSON", body: '{\n "id": 123\n}' },
{ title: "Result JSON", body: '{\n "ok": true\n}' },
],
},
],
disclosures: emptyDisclosures(),