refactor: tighten canonical tool-call typing and display mapping

This commit is contained in:
Mohamed Boudra
2026-02-09 08:53:27 +07:00
parent 0176614467
commit 6d4992b2f5
25 changed files with 2833 additions and 1764 deletions
@@ -13,17 +13,10 @@ function loadFixture(name: string): unknown {
}
describe("shared messages stream parsing", () => {
it("parses legacy inProgress tool_call snapshots and normalizes status", () => {
it("rejects legacy inProgress tool_call snapshots", () => {
const fixture = loadFixture("legacy-agent-stream-snapshot-inProgress.json");
const parsed = AgentStreamSnapshotMessageSchema.parse(fixture);
const first = parsed.payload.events[0]?.event;
expect(first?.type).toBe("timeline");
if (first?.type === "timeline" && first.item.type === "tool_call") {
expect(first.item.status).toBe("running");
expect(first.item.error).toBeNull();
expect(first.item.output).toBeNull();
}
const parsed = AgentStreamSnapshotMessageSchema.safeParse(fixture);
expect(parsed.success).toBe(false);
});
it("parses representative agent_stream tool_call event", () => {
@@ -61,24 +54,16 @@ describe("shared messages stream parsing", () => {
}
});
it("parses websocket envelope for agent_stream_snapshot with legacy status", () => {
it("rejects websocket envelope for agent_stream_snapshot with legacy status", () => {
const fixture = loadFixture("legacy-agent-stream-snapshot-inProgress.json") as {
type: "agent_stream_snapshot";
payload: unknown;
};
const wrapped = WSOutboundMessageSchema.parse({
const wrapped = WSOutboundMessageSchema.safeParse({
type: "session",
message: fixture,
});
if (wrapped.type === "session" && wrapped.message.type === "agent_stream_snapshot") {
const first = wrapped.message.payload.events[0]?.event;
expect(first?.type).toBe("timeline");
if (first?.type === "timeline" && first.item.type === "tool_call") {
expect(first.item.status).toBe("running");
expect(first.item.error).toBeNull();
}
}
expect(wrapped.success).toBe(false);
});
});
@@ -69,7 +69,7 @@ describe("shared messages tool_call schema", () => {
expect(unknownStatus.success).toBe(false);
});
it("normalizes recoverable legacy status/error combinations", () => {
it("rejects legacy status/error combinations without normalization", () => {
const completedWithError = AgentTimelineItemPayloadSchema.safeParse({
...canonicalBase(),
status: "completed",
@@ -91,18 +91,15 @@ describe("shared messages tool_call schema", () => {
error: null,
});
expect(completedWithError.success).toBe(true);
expect(failedWithoutError.success).toBe(true);
expect(missingOutput.success).toBe(true);
const legacyStatus = AgentTimelineItemPayloadSchema.safeParse({
...canonicalBase(),
status: "inProgress",
error: null,
});
if (completedWithError.success && completedWithError.data.type === "tool_call") {
expect(completedWithError.data.error).toBeNull();
}
if (failedWithoutError.success && failedWithoutError.data.type === "tool_call") {
expect(failedWithoutError.data.error).toEqual({ message: "Tool call failed" });
}
if (missingOutput.success && missingOutput.data.type === "tool_call") {
expect(missingOutput.data.output).toBeNull();
}
expect(completedWithError.success).toBe(false);
expect(failedWithoutError.success).toBe(false);
expect(missingOutput.success).toBe(false);
expect(legacyStatus.success).toBe(false);
});
});
+3 -57
View File
@@ -229,67 +229,13 @@ const ToolCallCanceledPayloadSchema = ToolCallBasePayloadSchema.extend({
error: z.null(),
});
const LEGACY_TOOL_CALL_STATUS_MAP: Record<string, ToolCallTimelineItem["status"]> = {
inprogress: "running",
in_progress: "running",
started: "running",
complete: "completed",
done: "completed",
success: "completed",
errored: "failed",
error: "failed",
cancelled: "canceled",
};
function normalizeLegacyToolCallPayload(value: unknown): unknown {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return value;
}
const record = value as Record<string, unknown>;
if (record.type !== "tool_call") {
return value;
}
const normalized: Record<string, unknown> = { ...record };
const rawStatus = typeof record.status === "string" ? record.status.trim() : "";
if (rawStatus.length > 0) {
const statusKey = rawStatus.toLowerCase().replace(/[\s-]+/g, "_");
const mappedStatus = LEGACY_TOOL_CALL_STATUS_MAP[statusKey];
if (mappedStatus) {
normalized.status = mappedStatus;
}
}
if (!("input" in normalized)) {
normalized.input = null;
}
if (!("output" in normalized)) {
normalized.output = null;
}
if (normalized.status === "failed") {
if (normalized.error === undefined || normalized.error === null) {
normalized.error = { message: "Tool call failed" };
}
} else if (normalized.error === undefined || normalized.error !== null) {
normalized.error = null;
}
return normalized;
}
const ToolCallTimelineItemPayloadSchema: z.ZodType<ToolCallTimelineItem, z.ZodTypeDef, unknown> = z.preprocess(
normalizeLegacyToolCallPayload,
const ToolCallTimelineItemPayloadSchema: z.ZodType<ToolCallTimelineItem, z.ZodTypeDef, unknown> =
z.union([
ToolCallRunningPayloadSchema,
ToolCallCompletedPayloadSchema,
ToolCallFailedPayloadSchema,
ToolCallCanceledPayloadSchema,
])
);
]);
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, z.ZodTypeDef, unknown> =
z.union([
@@ -388,7 +334,7 @@ const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> =
.object({
provider: AgentProviderSchema,
sessionId: z.string(),
nativeHandle: z.any().optional(),
nativeHandle: z.string().optional(),
metadata: z.record(z.unknown()).optional(),
})
.nullable();
@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { buildToolCallDisplayModel } from "./tool-call-display.js";
describe("shared tool-call display mapping", () => {
it("builds summary from canonical detail", () => {
const display = buildToolCallDisplayModel({
name: "read_file",
status: "running",
input: { path: "/tmp/repo/src/index.ts" },
output: null,
error: null,
detail: {
type: "read",
filePath: "/tmp/repo/src/index.ts",
},
cwd: "/tmp/repo",
});
expect(display).toEqual({
displayName: "Read",
summary: "src/index.ts",
});
});
it("does not infer summaries from raw input when detail is missing", () => {
const display = buildToolCallDisplayModel({
name: "exec_command",
status: "running",
input: { command: "npm test" },
output: null,
error: null,
});
expect(display).toEqual({
displayName: "Exec Command",
});
});
it("keeps task metadata summary without detail", () => {
const display = buildToolCallDisplayModel({
name: "task",
status: "running",
input: null,
output: null,
error: null,
metadata: {
subAgentActivity: "Running tests",
},
});
expect(display).toEqual({
displayName: "Task",
summary: "Running tests",
});
});
it("provides errorText for failed calls", () => {
const display = buildToolCallDisplayModel({
name: "shell",
status: "failed",
input: null,
output: null,
error: { message: "boom" },
});
expect(display.errorText).toBe('{\n "message": "boom"\n}');
});
});
@@ -0,0 +1,119 @@
import type { ToolCallTimelineItem } from "../server/agent/agent-sdk-types.js";
export type ToolCallDisplayInput = Pick<
ToolCallTimelineItem,
"name" | "status" | "input" | "output" | "error" | "metadata" | "detail"
> & {
cwd?: string;
};
export type ToolCallDisplayModel = {
displayName: string;
summary?: string;
errorText?: string;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function humanizeToolName(name: string): string {
const trimmed = name.trim();
if (!trimmed) {
return name;
}
if (/[:./]/.test(trimmed) || trimmed.includes("__")) {
return trimmed;
}
return trimmed
.replace(/[._-]+/g, " ")
.split(" ")
.filter((segment) => segment.length > 0)
.map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
.join(" ");
}
function stripCwdPrefix(filePath: string, cwd?: string): string {
if (!cwd || !filePath) {
return filePath;
}
const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
const normalizedPath = filePath.replace(/\\/g, "/");
const prefix = `${normalizedCwd}/`;
if (normalizedPath.startsWith(prefix)) {
return normalizedPath.slice(prefix.length);
}
if (normalizedPath === normalizedCwd) {
return ".";
}
return filePath;
}
function formatErrorText(error: unknown): string | undefined {
if (error === null || error === undefined) {
return undefined;
}
if (typeof error === "string") {
return error;
}
if (isRecord(error) && typeof error.content === "string") {
return error.content;
}
try {
return JSON.stringify(error, null, 2);
} catch {
return String(error);
}
}
export function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCallDisplayModel {
const lowerName = input.name.trim().toLowerCase();
let displayName = humanizeToolName(input.name);
let summary: string | undefined;
if (input.detail) {
switch (input.detail.type) {
case "shell":
displayName = "Shell";
summary = input.detail.command;
break;
case "read":
displayName = "Read";
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
break;
case "edit":
displayName = "Edit";
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
break;
case "write":
displayName = "Write";
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
break;
case "search":
displayName = "Search";
summary = input.detail.query;
break;
}
} else if (lowerName === "task") {
displayName = "Task";
summary = isRecord(input.metadata) ? readString(input.metadata.subAgentActivity) : undefined;
} else if (lowerName === "thinking") {
displayName = "Thinking";
}
const errorText = input.status === "failed" ? formatErrorText(input.error) : undefined;
return {
displayName,
...(summary ? { summary } : {}),
...(errorText ? { errorText } : {}),
};
}