Update files
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "agent_stream_snapshot",
|
||||
"payload": {
|
||||
"agentId": "agent_fixture_legacy",
|
||||
"events": [
|
||||
{
|
||||
"timestamp": "2026-02-08T20:00:00.000Z",
|
||||
"event": {
|
||||
"type": "timeline",
|
||||
"provider": "codex",
|
||||
"item": {
|
||||
"type": "tool_call",
|
||||
"callId": "call_fixture_legacy",
|
||||
"name": "shell",
|
||||
"status": "inProgress",
|
||||
"input": {
|
||||
"command": "pwd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
AgentStreamMessageSchema,
|
||||
AgentStreamSnapshotMessageSchema,
|
||||
WSOutboundMessageSchema,
|
||||
} from "./messages.js";
|
||||
|
||||
function loadFixture(name: string): unknown {
|
||||
const url = new URL(`./__fixtures__/${name}`, import.meta.url);
|
||||
return JSON.parse(readFileSync(url, "utf8"));
|
||||
}
|
||||
|
||||
describe("shared messages stream parsing", () => {
|
||||
it("parses legacy inProgress tool_call snapshots and normalizes status", () => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
it("parses representative agent_stream tool_call event", () => {
|
||||
const parsed = AgentStreamMessageSchema.parse({
|
||||
type: "agent_stream",
|
||||
payload: {
|
||||
agentId: "agent_live",
|
||||
timestamp: "2026-02-08T20:10:00.000Z",
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call_live",
|
||||
name: "shell",
|
||||
status: "running",
|
||||
input: { command: "ls" },
|
||||
output: null,
|
||||
error: null,
|
||||
detail: {
|
||||
type: "shell",
|
||||
command: "ls",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload.event.type).toBe("timeline");
|
||||
if (parsed.payload.event.type === "timeline") {
|
||||
expect(parsed.payload.event.item.type).toBe("tool_call");
|
||||
if (parsed.payload.event.item.type === "tool_call") {
|
||||
expect(parsed.payload.event.item.status).toBe("running");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("parses 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({
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AgentTimelineItemPayloadSchema } from "./messages.js";
|
||||
|
||||
function canonicalBase() {
|
||||
return {
|
||||
type: "tool_call" as const,
|
||||
callId: "call_123",
|
||||
name: "shell",
|
||||
input: { command: "pwd" },
|
||||
output: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("shared messages tool_call schema", () => {
|
||||
it("parses each status-discriminated tool_call variant at runtime", () => {
|
||||
const running = AgentTimelineItemPayloadSchema.parse({
|
||||
...canonicalBase(),
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "shell",
|
||||
command: "pwd",
|
||||
},
|
||||
});
|
||||
|
||||
const completed = AgentTimelineItemPayloadSchema.parse({
|
||||
...canonicalBase(),
|
||||
status: "completed",
|
||||
error: null,
|
||||
output: { output: "/tmp/repo" },
|
||||
});
|
||||
|
||||
const failed = AgentTimelineItemPayloadSchema.parse({
|
||||
...canonicalBase(),
|
||||
status: "failed",
|
||||
error: { message: "command failed" },
|
||||
});
|
||||
|
||||
const canceled = AgentTimelineItemPayloadSchema.parse({
|
||||
...canonicalBase(),
|
||||
status: "canceled",
|
||||
error: null,
|
||||
});
|
||||
|
||||
expect(running.type).toBe("tool_call");
|
||||
expect(completed.type).toBe("tool_call");
|
||||
expect(failed.type).toBe("tool_call");
|
||||
expect(canceled.type).toBe("tool_call");
|
||||
});
|
||||
|
||||
it("rejects non-recoverable invalid tool_call payloads", () => {
|
||||
const missingCallId = AgentTimelineItemPayloadSchema.safeParse({
|
||||
type: "tool_call",
|
||||
name: "shell",
|
||||
status: "running",
|
||||
input: { command: "pwd" },
|
||||
output: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const unknownStatus = AgentTimelineItemPayloadSchema.safeParse({
|
||||
...canonicalBase(),
|
||||
status: "mystery_status",
|
||||
error: null,
|
||||
});
|
||||
|
||||
expect(missingCallId.success).toBe(false);
|
||||
expect(unknownStatus.success).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes recoverable legacy status/error combinations", () => {
|
||||
const completedWithError = AgentTimelineItemPayloadSchema.safeParse({
|
||||
...canonicalBase(),
|
||||
status: "completed",
|
||||
error: { message: "unexpected" },
|
||||
});
|
||||
|
||||
const failedWithoutError = AgentTimelineItemPayloadSchema.safeParse({
|
||||
...canonicalBase(),
|
||||
status: "failed",
|
||||
error: null,
|
||||
});
|
||||
|
||||
const missingOutput = AgentTimelineItemPayloadSchema.safeParse({
|
||||
type: "tool_call",
|
||||
callId: "call_missing_output",
|
||||
name: "shell",
|
||||
status: "running",
|
||||
input: { command: "pwd" },
|
||||
error: null,
|
||||
});
|
||||
|
||||
expect(completedWithError.success).toBe(true);
|
||||
expect(failedWithoutError.success).toBe(true);
|
||||
expect(missingOutput.success).toBe(true);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
AgentPersistenceHandle,
|
||||
AgentRuntimeInfo,
|
||||
AgentTimelineItem,
|
||||
ToolCallDetail,
|
||||
ToolCallTimelineItem,
|
||||
AgentUsage,
|
||||
} from "../server/agent/agent-sdk-types.js";
|
||||
|
||||
@@ -147,8 +149,150 @@ export type StructuredToolResult =
|
||||
| { type: "file_read"; filePath: string; content: string }
|
||||
| { type: "generic"; data: unknown };
|
||||
|
||||
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
|
||||
z.discriminatedUnion("type", [
|
||||
const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("shell"),
|
||||
command: z.string(),
|
||||
cwd: z.string().optional(),
|
||||
output: z.string().optional(),
|
||||
exitCode: z.number().nullable().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("read"),
|
||||
filePath: z.string(),
|
||||
content: z.string().optional(),
|
||||
offset: z.number().optional(),
|
||||
limit: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("edit"),
|
||||
filePath: z.string(),
|
||||
oldString: z.string().optional(),
|
||||
newString: z.string().optional(),
|
||||
unifiedDiff: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("write"),
|
||||
filePath: z.string(),
|
||||
content: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("search"),
|
||||
query: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const NonUndefinedUnknownSchema = z.union([
|
||||
z.null(),
|
||||
z.boolean(),
|
||||
z.number(),
|
||||
z.string(),
|
||||
z.array(z.unknown()),
|
||||
z.object({}).passthrough(),
|
||||
]);
|
||||
|
||||
const NonNullUnknownSchema = z.union([
|
||||
z.boolean(),
|
||||
z.number(),
|
||||
z.string(),
|
||||
z.array(z.unknown()),
|
||||
z.object({}).passthrough(),
|
||||
]);
|
||||
|
||||
const ToolCallBasePayloadSchema = z.object({
|
||||
type: z.literal("tool_call"),
|
||||
callId: z.string(),
|
||||
name: z.string(),
|
||||
input: NonUndefinedUnknownSchema,
|
||||
output: NonUndefinedUnknownSchema,
|
||||
detail: ToolCallDetailPayloadSchema.optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const ToolCallRunningPayloadSchema = ToolCallBasePayloadSchema.extend({
|
||||
status: z.literal("running"),
|
||||
error: z.null(),
|
||||
});
|
||||
|
||||
const ToolCallCompletedPayloadSchema = ToolCallBasePayloadSchema.extend({
|
||||
status: z.literal("completed"),
|
||||
error: z.null(),
|
||||
});
|
||||
|
||||
const ToolCallFailedPayloadSchema = ToolCallBasePayloadSchema.extend({
|
||||
status: z.literal("failed"),
|
||||
error: NonNullUnknownSchema,
|
||||
});
|
||||
|
||||
const ToolCallCanceledPayloadSchema = ToolCallBasePayloadSchema.extend({
|
||||
status: z.literal("canceled"),
|
||||
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,
|
||||
z.union([
|
||||
ToolCallRunningPayloadSchema,
|
||||
ToolCallCompletedPayloadSchema,
|
||||
ToolCallFailedPayloadSchema,
|
||||
ToolCallCanceledPayloadSchema,
|
||||
])
|
||||
);
|
||||
|
||||
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, z.ZodTypeDef, unknown> =
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal("user_message"),
|
||||
text: z.string(),
|
||||
@@ -162,16 +306,7 @@ export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
|
||||
type: z.literal("reasoning"),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("tool_call"),
|
||||
name: z.string(),
|
||||
callId: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
input: z.unknown().optional(),
|
||||
output: z.unknown().optional(),
|
||||
error: z.unknown().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
}),
|
||||
ToolCallTimelineItemPayloadSchema,
|
||||
z.object({
|
||||
type: z.literal("todo"),
|
||||
items: z.array(
|
||||
|
||||
Reference in New Issue
Block a user