Skip to content

Commit fd2f823

Browse files
fix(core): handle nanosecond timestamps and nullable attemptNumber in RunEvent
1 parent 0d361c8 commit fd2f823

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed

packages/core/src/v3/schemas/api-type.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ describe("RunEvent Schema", () => {
189189
expect(result.startTime.toISOString()).toBe("2024-03-14T00:00:00.000Z");
190190
});
191191

192+
it("handles 19-digit nanosecond startTime strings", () => {
193+
const event = { ...validEvent, startTime: "1710374400000000000" };
194+
const result = RunEvent.parse(event);
195+
expect(result.startTime).toBeInstanceOf(Date);
196+
// 1710374400000000000 ns = 1710374400000 ms = 2024-03-14T00:00:00Z
197+
expect(result.startTime.toISOString()).toBe("2024-03-14T00:00:00.000Z");
198+
});
199+
200+
it("handles bigint nanosecond startTime", () => {
201+
const event = { ...validEvent, startTime: 1710374400000000000n };
202+
const result = RunEvent.parse(event as any);
203+
expect(result.startTime).toBeInstanceOf(Date);
204+
expect(result.startTime.toISOString()).toBe("2024-03-14T00:00:00.000Z");
205+
});
206+
192207
it("allows optional/null parentId", () => {
193208
const eventWithoutParent = { ...validEvent };
194209
delete (eventWithoutParent as any).parentId;
@@ -197,6 +212,26 @@ describe("RunEvent Schema", () => {
197212
const eventWithNullParent = { ...validEvent, parentId: null };
198213
expect(RunEvent.safeParse(eventWithNullParent).success).toBe(true);
199214
});
215+
216+
it("allows nullish attemptNumber", () => {
217+
const eventWithNullAttempt = { ...validEvent, attemptNumber: null };
218+
const result = RunEvent.safeParse(eventWithNullAttempt);
219+
expect(result.success).toBe(true);
220+
if (result.success) {
221+
expect(result.data.attemptNumber).toBe(null);
222+
}
223+
224+
const eventWithoutAttempt = { ...validEvent };
225+
delete (eventWithoutAttempt as any).attemptNumber;
226+
const result2 = RunEvent.safeParse(eventWithoutAttempt);
227+
expect(result2.success).toBe(true);
228+
});
229+
230+
it("supports taskSlug", () => {
231+
const eventWithSlug = { ...validEvent, taskSlug: "my-task" };
232+
const result = RunEvent.parse(eventWithSlug);
233+
expect(result.taskSlug).toBe("my-task");
234+
});
200235
});
201236

202237
describe("ListRunEventsResponse Schema", () => {

packages/core/src/v3/schemas/api.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,22 +1643,44 @@ export const SendInputStreamResponseBody = z.object({
16431643
export type SendInputStreamResponseBody = z.infer<typeof SendInputStreamResponseBody>;
16441644
export const TaskEventLevel = z.enum(["TRACE", "DEBUG", "INFO", "LOG", "WARN", "ERROR"]);
16451645
export type TaskEventLevel = z.infer<typeof TaskEventLevel>;
1646+
export const NanosecondTimestampSchema = z
1647+
.union([z.string(), z.number(), z.bigint()])
1648+
.transform((val) => {
1649+
// If it's already a Date, return it (though union doesn't include it)
1650+
if (typeof val === "object" && val instanceof Date) return val;
1651+
1652+
const str = val.toString();
1653+
1654+
// 19 digits is nanoseconds (e.g., 1710374400000000000)
1655+
if (str.length === 19) {
1656+
return new Date(Number(BigInt(str) / 1000000n));
1657+
}
1658+
1659+
// 13 digits is milliseconds
1660+
if (str.length === 13) {
1661+
return new Date(Number(val));
1662+
}
1663+
1664+
// Default to standard date coercion
1665+
return new Date(val as any);
1666+
});
16461667

16471668
export const RunEvent = z.object({
16481669
spanId: z.string(),
16491670
parentId: z.string().nullish(),
16501671
runId: z.string(),
16511672
message: z.string(),
16521673
style: TaskEventStyle,
1653-
startTime: z.coerce.date(),
1674+
startTime: NanosecondTimestampSchema,
16541675
duration: z.number(),
16551676
isError: z.boolean(),
16561677
isPartial: z.boolean(),
16571678
isCancelled: z.boolean(),
16581679
level: TaskEventLevel,
16591680
events: SpanEvents.optional(),
16601681
kind: z.string(),
1661-
attemptNumber: z.number().optional(),
1682+
attemptNumber: z.number().nullish(),
1683+
taskSlug: z.string().optional(),
16621684
});
16631685

16641686
export type RunEvent = z.infer<typeof RunEvent>;

0 commit comments

Comments
 (0)