Skip to content

Commit 3f44ede

Browse files
committed
use zod for agent events to make sure they conform to schema
1 parent 13eb030 commit 3f44ede

File tree

7 files changed

+318
-204
lines changed

7 files changed

+318
-204
lines changed

apps/array/src/renderer/features/task-detail/stores/taskExecutionStore.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useAuthStore } from "@features/auth/stores/authStore";
22
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
33
import { useSettingsStore } from "@features/settings/stores/settingsStore";
4-
import type { AgentEvent } from "@posthog/agent";
4+
import { type AgentEvent, parseAgentEvents } from "@posthog/agent";
55
import { track } from "@renderer/lib/analytics";
66
import { logger } from "@renderer/lib/logger";
77
import { queryClient } from "@renderer/lib/queryClient";
@@ -94,10 +94,19 @@ async function fetchLogsFromS3Url(logUrl: string): Promise<AgentEvent[]> {
9494
return [];
9595
}
9696

97-
return content
97+
const rawEntries = content
9898
.trim()
9999
.split("\n")
100-
.map((line: string) => JSON.parse(line) as AgentEvent);
100+
.map((line: string) => {
101+
try {
102+
return JSON.parse(line);
103+
} catch {
104+
return null;
105+
}
106+
})
107+
.filter(Boolean);
108+
109+
return parseAgentEvents(rawEntries);
101110
} catch (err) {
102111
log.warn("Failed to fetch task logs from S3", err);
103112
return [];

packages/agent/example.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env bun
22

3-
import { config } from "dotenv";
43
import { execSync } from "node:child_process";
54
import * as readline from "node:readline";
5+
import { config } from "dotenv";
66

77
config();
88

@@ -40,7 +40,7 @@ Create a simple hello.txt file in the root of the repository with the text "Hell
4040
`;
4141

4242
async function testAgent() {
43-
let TASK_ID = process.argv[2];
43+
let TASK_ID = process.argv[2];
4444

4545
if (!process.env.POSTHOG_API_KEY) {
4646
console.error("❌ POSTHOG_API_KEY required");
@@ -57,7 +57,7 @@ async function testAgent() {
5757
process.exit(1);
5858
}
5959

60-
const REPO_PATH = process.env.REPO_PATH
60+
const REPO_PATH = process.env.REPO_PATH;
6161

6262
if (!REPO_PATH) {
6363
console.error("❌ REPO_PATH required");
@@ -69,7 +69,9 @@ async function testAgent() {
6969
// Check for uncommitted changes
7070
if (hasUncommittedChanges(REPO_PATH)) {
7171
console.log("⚠️ Warning: There are uncommitted changes in the repository.");
72-
const proceed = await promptUser("These changes will be discarded. Continue? (y/n): ");
72+
const proceed = await promptUser(
73+
"These changes will be discarded. Continue? (y/n): ",
74+
);
7375
if (!proceed) {
7476
console.log("❌ Aborted.");
7577
process.exit(0);

packages/agent/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ export type {
6161
UserMessageEvent,
6262
WorktreeInfo,
6363
} from "./src/types.js";
64-
export { PermissionMode } from "./src/types.js";
64+
export {
65+
AgentEventSchema,
66+
PermissionMode,
67+
parseAgentEvent,
68+
parseAgentEvents,
69+
} from "./src/types.js";
6570
export type { LoggerConfig } from "./src/utils/logger.js";
6671
export {
6772
Logger,

packages/agent/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
},
4040
"devDependencies": {
4141
"@changesets/cli": "^2.27.8",
42-
"tsx": "^4.20.6",
4342
"@rollup/plugin-commonjs": "^25.0.7",
4443
"@rollup/plugin-node-resolve": "^15.2.3",
4544
"@types/bun": "latest",
@@ -48,12 +47,14 @@
4847
"rollup": "^4.24.0",
4948
"rollup-plugin-copy": "^3.5.0",
5049
"rollup-plugin-typescript2": "^0.36.0",
50+
"tsx": "^4.20.6",
5151
"typescript": "^5.5.0"
5252
},
5353
"dependencies": {
5454
"@anthropic-ai/claude-agent-sdk": "^0.1.47",
5555
"dotenv": "^17.2.3",
56-
"yoga-wasm-web": "^0.3.3"
56+
"yoga-wasm-web": "^0.3.3",
57+
"zod": "^4.1.12"
5758
},
5859
"files": [
5960
"dist/**/*",

packages/agent/src/schemas.ts

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { z } from "zod";
2+
3+
// Base event schema with timestamp
4+
const BaseEventSchema = z.object({
5+
ts: z.number(),
6+
});
7+
8+
// Streaming content events
9+
export const TokenEventSchema = BaseEventSchema.extend({
10+
type: z.literal("token"),
11+
content: z.string(),
12+
contentType: z.enum(["text", "thinking", "tool_input"]).optional(),
13+
});
14+
15+
export const ContentBlockStartEventSchema = BaseEventSchema.extend({
16+
type: z.literal("content_block_start"),
17+
index: z.number(),
18+
contentType: z.enum(["text", "tool_use", "thinking"]),
19+
toolName: z.string().optional(),
20+
toolId: z.string().optional(),
21+
});
22+
23+
export const ContentBlockStopEventSchema = BaseEventSchema.extend({
24+
type: z.literal("content_block_stop"),
25+
index: z.number(),
26+
});
27+
28+
// Tool events
29+
export const ToolCallEventSchema = BaseEventSchema.extend({
30+
type: z.literal("tool_call"),
31+
toolName: z.string(),
32+
callId: z.string(),
33+
args: z.record(z.string(), z.unknown()),
34+
parentToolUseId: z.string().nullable().optional(),
35+
tool: z.unknown().optional(),
36+
category: z.unknown().optional(),
37+
});
38+
39+
export const ToolResultEventSchema = BaseEventSchema.extend({
40+
type: z.literal("tool_result"),
41+
toolName: z.string(),
42+
callId: z.string(),
43+
result: z.unknown(),
44+
isError: z.boolean().optional(),
45+
parentToolUseId: z.string().nullable().optional(),
46+
tool: z.unknown().optional(),
47+
category: z.unknown().optional(),
48+
});
49+
50+
// Message lifecycle events
51+
export const MessageStartEventSchema = BaseEventSchema.extend({
52+
type: z.literal("message_start"),
53+
messageId: z.string().optional(),
54+
model: z.string().optional(),
55+
});
56+
57+
export const MessageDeltaEventSchema = BaseEventSchema.extend({
58+
type: z.literal("message_delta"),
59+
stopReason: z.string().optional(),
60+
stopSequence: z.string().optional(),
61+
usage: z
62+
.object({
63+
outputTokens: z.number(),
64+
})
65+
.optional(),
66+
});
67+
68+
export const MessageStopEventSchema = BaseEventSchema.extend({
69+
type: z.literal("message_stop"),
70+
});
71+
72+
// User message events
73+
export const UserMessageEventSchema = BaseEventSchema.extend({
74+
type: z.literal("user_message"),
75+
content: z.string(),
76+
isSynthetic: z.boolean().optional(),
77+
});
78+
79+
// System events
80+
export const StatusEventSchema = BaseEventSchema.extend({
81+
type: z.literal("status"),
82+
phase: z.string(),
83+
kind: z.string().optional(),
84+
branch: z.string().optional(),
85+
prUrl: z.string().optional(),
86+
taskId: z.string().optional(),
87+
messageId: z.string().optional(),
88+
model: z.string().optional(),
89+
}).passthrough(); // Allow additional fields
90+
91+
export const InitEventSchema = BaseEventSchema.extend({
92+
type: z.literal("init"),
93+
model: z.string(),
94+
tools: z.array(z.string()),
95+
permissionMode: z.string(),
96+
cwd: z.string(),
97+
apiKeySource: z.string(),
98+
agents: z.array(z.string()).optional(),
99+
slashCommands: z.array(z.string()).optional(),
100+
outputStyle: z.string().optional(),
101+
mcpServers: z
102+
.array(z.object({ name: z.string(), status: z.string() }))
103+
.optional(),
104+
});
105+
106+
// Console event for log-style output
107+
export const ConsoleEventSchema = BaseEventSchema.extend({
108+
type: z.literal("console"),
109+
level: z.enum(["debug", "info", "warn", "error"]),
110+
message: z.string(),
111+
});
112+
113+
export const CompactBoundaryEventSchema = BaseEventSchema.extend({
114+
type: z.literal("compact_boundary"),
115+
trigger: z.enum(["manual", "auto"]),
116+
preTokens: z.number(),
117+
});
118+
119+
// Result events
120+
export const DoneEventSchema = BaseEventSchema.extend({
121+
type: z.literal("done"),
122+
result: z.string().optional(),
123+
durationMs: z.number().optional(),
124+
durationApiMs: z.number().optional(),
125+
numTurns: z.number().optional(),
126+
totalCostUsd: z.number().optional(),
127+
usage: z.unknown().optional(),
128+
modelUsage: z
129+
.record(
130+
z.string(),
131+
z.object({
132+
inputTokens: z.number(),
133+
outputTokens: z.number(),
134+
cacheReadInputTokens: z.number(),
135+
cacheCreationInputTokens: z.number(),
136+
webSearchRequests: z.number(),
137+
costUSD: z.number(),
138+
contextWindow: z.number(),
139+
}),
140+
)
141+
.optional(),
142+
permissionDenials: z
143+
.array(
144+
z.object({
145+
tool_name: z.string(),
146+
tool_use_id: z.string(),
147+
tool_input: z.record(z.string(), z.unknown()),
148+
}),
149+
)
150+
.optional(),
151+
});
152+
153+
export const ErrorEventSchema = BaseEventSchema.extend({
154+
type: z.literal("error"),
155+
message: z.string(),
156+
error: z.unknown().optional(),
157+
errorType: z.string().optional(),
158+
context: z.record(z.string(), z.unknown()).optional(),
159+
sdkError: z.unknown().optional(),
160+
});
161+
162+
// Metric and artifact events
163+
export const MetricEventSchema = BaseEventSchema.extend({
164+
type: z.literal("metric"),
165+
key: z.string(),
166+
value: z.number(),
167+
unit: z.string().optional(),
168+
});
169+
170+
export const ArtifactEventSchema = BaseEventSchema.extend({
171+
type: z.literal("artifact"),
172+
kind: z.string(),
173+
content: z.unknown(),
174+
});
175+
176+
export const RawSDKEventSchema = BaseEventSchema.extend({
177+
type: z.literal("raw_sdk_event"),
178+
sdkMessage: z.unknown(),
179+
});
180+
181+
export const AgentEventSchema = z.discriminatedUnion("type", [
182+
TokenEventSchema,
183+
ContentBlockStartEventSchema,
184+
ContentBlockStopEventSchema,
185+
ToolCallEventSchema,
186+
ToolResultEventSchema,
187+
MessageStartEventSchema,
188+
MessageDeltaEventSchema,
189+
MessageStopEventSchema,
190+
UserMessageEventSchema,
191+
StatusEventSchema,
192+
InitEventSchema,
193+
ConsoleEventSchema,
194+
CompactBoundaryEventSchema,
195+
DoneEventSchema,
196+
ErrorEventSchema,
197+
MetricEventSchema,
198+
ArtifactEventSchema,
199+
RawSDKEventSchema,
200+
]);
201+
202+
export type TokenEvent = z.infer<typeof TokenEventSchema>;
203+
export type ContentBlockStartEvent = z.infer<
204+
typeof ContentBlockStartEventSchema
205+
>;
206+
export type ContentBlockStopEvent = z.infer<typeof ContentBlockStopEventSchema>;
207+
export type ToolCallEvent = z.infer<typeof ToolCallEventSchema>;
208+
export type ToolResultEvent = z.infer<typeof ToolResultEventSchema>;
209+
export type MessageStartEvent = z.infer<typeof MessageStartEventSchema>;
210+
export type MessageDeltaEvent = z.infer<typeof MessageDeltaEventSchema>;
211+
export type MessageStopEvent = z.infer<typeof MessageStopEventSchema>;
212+
export type UserMessageEvent = z.infer<typeof UserMessageEventSchema>;
213+
export type StatusEvent = z.infer<typeof StatusEventSchema>;
214+
export type InitEvent = z.infer<typeof InitEventSchema>;
215+
export type ConsoleEvent = z.infer<typeof ConsoleEventSchema>;
216+
export type CompactBoundaryEvent = z.infer<typeof CompactBoundaryEventSchema>;
217+
export type DoneEvent = z.infer<typeof DoneEventSchema>;
218+
export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
219+
export type MetricEvent = z.infer<typeof MetricEventSchema>;
220+
export type ArtifactEvent = z.infer<typeof ArtifactEventSchema>;
221+
export type RawSDKEvent = z.infer<typeof RawSDKEventSchema>;
222+
export type AgentEvent = z.infer<typeof AgentEventSchema>;
223+
224+
/**
225+
* Parse and validate an AgentEvent from unknown input.
226+
* Returns the parsed event if valid, or null if invalid.
227+
*/
228+
export function parseAgentEvent(input: unknown): AgentEvent | null {
229+
const result = AgentEventSchema.safeParse(input);
230+
return result.success ? result.data : null;
231+
}
232+
233+
/**
234+
* Parse and validate multiple AgentEvents from an array of unknown inputs.
235+
* Invalid entries are discarded.
236+
*/
237+
export function parseAgentEvents(inputs: unknown[]): AgentEvent[] {
238+
return inputs
239+
.map((input) => parseAgentEvent(input))
240+
.filter((event): event is AgentEvent => event !== null);
241+
}

0 commit comments

Comments
 (0)