Skip to content

Commit bc8ca4b

Browse files
committed
refactor(core): migrate MessageV2 message DTOs (User/Assistant/Part/Info/WithParts) to Effect Schema
Continues the session-domain Schema migration. The assistant error union still references NamedError-based Zod errors via ZodOverride; errors migrate in a separate slice.
1 parent 27da9ba commit bc8ca4b

6 files changed

Lines changed: 134 additions & 118 deletions

File tree

packages/opencode/src/cli/cmd/import.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export const ImportCommand = cmd({
168168
)
169169

170170
for (const msg of exportData.messages) {
171-
const msgInfo = MessageV2.Info.parse(msg.info)
171+
const msgInfo = MessageV2.Info.zod.parse(msg.info)
172172
const { id, sessionID: _, ...msgData } = msgInfo
173173
Database.use((db) =>
174174
db
@@ -184,7 +184,7 @@ export const ImportCommand = cmd({
184184
)
185185

186186
for (const part of msg.parts) {
187-
const partInfo = MessageV2.Part.parse(part)
187+
const partInfo = MessageV2.Part.zod.parse(part)
188188
const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
189189
Database.use((db) =>
190190
db

packages/opencode/src/server/routes/instance/session.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ export const SessionRoutes = lazy(() =>
611611
description: "List of messages",
612612
content: {
613613
"application/json": {
614-
schema: resolver(MessageV2.WithParts.array()),
614+
schema: resolver(MessageV2.WithParts.zod.array()),
615615
},
616616
},
617617
},
@@ -701,8 +701,8 @@ export const SessionRoutes = lazy(() =>
701701
"application/json": {
702702
schema: resolver(
703703
z.object({
704-
info: MessageV2.Info,
705-
parts: MessageV2.Part.array(),
704+
info: MessageV2.Info.zod,
705+
parts: MessageV2.Part.zod.array(),
706706
}),
707707
),
708708
},
@@ -813,7 +813,7 @@ export const SessionRoutes = lazy(() =>
813813
description: "Successfully updated part",
814814
content: {
815815
"application/json": {
816-
schema: resolver(MessageV2.Part),
816+
schema: resolver(MessageV2.Part.zod),
817817
},
818818
},
819819
},
@@ -828,7 +828,7 @@ export const SessionRoutes = lazy(() =>
828828
partID: PartID.zod,
829829
}),
830830
),
831-
validator("json", MessageV2.Part),
831+
validator("json", MessageV2.Part.zod),
832832
async (c) => {
833833
const params = c.req.valid("param")
834834
const body = c.req.valid("json")
@@ -856,8 +856,8 @@ export const SessionRoutes = lazy(() =>
856856
"application/json": {
857857
schema: resolver(
858858
z.object({
859-
info: MessageV2.Assistant,
860-
parts: MessageV2.Part.array(),
859+
info: MessageV2.Assistant.zod,
860+
parts: MessageV2.Part.zod.array(),
861861
}),
862862
),
863863
},
@@ -940,8 +940,8 @@ export const SessionRoutes = lazy(() =>
940940
"application/json": {
941941
schema: resolver(
942942
z.object({
943-
info: MessageV2.Assistant,
944-
parts: MessageV2.Part.array(),
943+
info: MessageV2.Assistant.zod,
944+
parts: MessageV2.Part.zod.array(),
945945
}),
946946
),
947947
},
@@ -976,7 +976,7 @@ export const SessionRoutes = lazy(() =>
976976
description: "Created message",
977977
content: {
978978
"application/json": {
979-
schema: resolver(MessageV2.WithParts),
979+
schema: resolver(MessageV2.WithParts.zod),
980980
},
981981
},
982982
},

packages/opencode/src/session/message-v2.ts

Lines changed: 115 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -368,37 +368,68 @@ export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart
368368
state: ToolState
369369
}
370370

371-
const Base = z.object({
372-
id: MessageID.zod,
373-
sessionID: SessionID.zod,
374-
})
371+
const messageBase = {
372+
id: MessageID,
373+
sessionID: SessionID,
374+
}
375375

376-
export const User = Base.extend({
377-
role: z.literal("user"),
378-
time: z.object({
379-
created: z.number(),
376+
export const User = Schema.Struct({
377+
...messageBase,
378+
role: Schema.Literal("user"),
379+
time: Schema.Struct({
380+
created: Schema.Number,
380381
}),
381-
format: Format.zod.optional(),
382-
summary: z
383-
.object({
384-
title: z.string().optional(),
385-
body: z.string().optional(),
386-
diffs: Snapshot.FileDiff.zod.array(),
387-
})
388-
.optional(),
389-
agent: z.string(),
390-
model: z.object({
391-
providerID: ProviderID.zod,
392-
modelID: ModelID.zod,
393-
variant: z.string().optional(),
382+
format: Schema.optional(_Format),
383+
summary: Schema.optional(
384+
Schema.Struct({
385+
title: Schema.optional(Schema.String),
386+
body: Schema.optional(Schema.String),
387+
diffs: Schema.Array(Snapshot.FileDiff),
388+
}),
389+
),
390+
agent: Schema.String,
391+
model: Schema.Struct({
392+
providerID: ProviderID,
393+
modelID: ModelID,
394+
variant: Schema.optional(Schema.String),
394395
}),
395-
system: z.string().optional(),
396-
tools: z.record(z.string(), z.boolean()).optional(),
397-
}).meta({
398-
ref: "UserMessage",
396+
system: Schema.optional(Schema.String),
397+
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
398+
})
399+
.annotate({ identifier: "UserMessage" })
400+
.pipe(withStatics((s) => ({ zod: zod(s) })))
401+
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
402+
403+
const _Part = Schema.Union([
404+
TextPart,
405+
SubtaskPart,
406+
ReasoningPart,
407+
FilePart,
408+
ToolPart,
409+
StepStartPart,
410+
StepFinishPart,
411+
SnapshotPart,
412+
PatchPart,
413+
AgentPart,
414+
RetryPart,
415+
CompactionPart,
416+
]).annotate({ discriminator: "type", identifier: "Part" })
417+
export const Part = Object.assign(_Part, {
418+
zod: zod(_Part) as unknown as z.ZodType<
419+
| TextPart
420+
| SubtaskPart
421+
| ReasoningPart
422+
| FilePart
423+
| ToolPart
424+
| StepStartPart
425+
| StepFinishPart
426+
| SnapshotPart
427+
| PatchPart
428+
| AgentPart
429+
| RetryPart
430+
| CompactionPart
431+
>,
399432
})
400-
export type User = z.infer<typeof User>
401-
402433
export type Part =
403434
| TextPart
404435
| SubtaskPart
@@ -413,82 +444,67 @@ export type Part =
413444
| RetryPart
414445
| CompactionPart
415446

416-
// The derived `.zod` on each leaf is typed as `z.ZodType<...>`, but the walker
417-
// always emits a `z.ZodObject` at runtime. `z.discriminatedUnion` and
418-
// `z.infer` both rely on the ZodObject structural type, so cast here so the
419-
// resulting Part behaves like the pre-migration Zod union.
420-
export const Part = z
421-
.discriminatedUnion("type", [
422-
TextPart.zod as unknown as z.ZodObject<any>,
423-
SubtaskPart.zod as unknown as z.ZodObject<any>,
424-
ReasoningPart.zod as unknown as z.ZodObject<any>,
425-
FilePart.zod as unknown as z.ZodObject<any>,
426-
ToolPart.zod as unknown as z.ZodObject<any>,
427-
StepStartPart.zod as unknown as z.ZodObject<any>,
428-
StepFinishPart.zod as unknown as z.ZodObject<any>,
429-
SnapshotPart.zod as unknown as z.ZodObject<any>,
430-
PatchPart.zod as unknown as z.ZodObject<any>,
431-
AgentPart.zod as unknown as z.ZodObject<any>,
432-
RetryPart.zod as unknown as z.ZodObject<any>,
433-
CompactionPart.zod as unknown as z.ZodObject<any>,
434-
])
435-
.meta({
436-
ref: "Part",
437-
}) as unknown as z.ZodType<Part>
438-
439-
export const Assistant = Base.extend({
440-
role: z.literal("assistant"),
441-
time: z.object({
442-
created: z.number(),
443-
completed: z.number().optional(),
447+
// Errors are still NamedError-based Zod; bridge via ZodOverride so the derived
448+
// Zod + JSON Schema emit the original discriminatedUnion shape. Migrating the
449+
// error classes to Schema.TaggedErrorClass is a separate slice.
450+
const AssistantErrorZod = z.discriminatedUnion("name", [
451+
AuthError.Schema,
452+
NamedError.Unknown.Schema,
453+
OutputLengthError.Schema,
454+
AbortedError.Schema,
455+
StructuredOutputError.Schema,
456+
ContextOverflowError.Schema,
457+
APIError.Schema,
458+
])
459+
type AssistantError = z.infer<typeof AssistantErrorZod>
460+
461+
export const Assistant = Schema.Struct({
462+
...messageBase,
463+
role: Schema.Literal("assistant"),
464+
time: Schema.Struct({
465+
created: Schema.Number,
466+
completed: Schema.optional(Schema.Number),
444467
}),
445-
error: z
446-
.discriminatedUnion("name", [
447-
AuthError.Schema,
448-
NamedError.Unknown.Schema,
449-
OutputLengthError.Schema,
450-
AbortedError.Schema,
451-
StructuredOutputError.Schema,
452-
ContextOverflowError.Schema,
453-
APIError.Schema,
454-
])
455-
.optional(),
456-
parentID: MessageID.zod,
457-
modelID: ModelID.zod,
458-
providerID: ProviderID.zod,
468+
error: Schema.optional(Schema.Any.annotate({ [ZodOverride]: AssistantErrorZod })),
469+
parentID: MessageID,
470+
modelID: ModelID,
471+
providerID: ProviderID,
459472
/**
460473
* @deprecated
461474
*/
462-
mode: z.string(),
463-
agent: z.string(),
464-
path: z.object({
465-
cwd: z.string(),
466-
root: z.string(),
475+
mode: Schema.String,
476+
agent: Schema.String,
477+
path: Schema.Struct({
478+
cwd: Schema.String,
479+
root: Schema.String,
467480
}),
468-
summary: z.boolean().optional(),
469-
cost: z.number(),
470-
tokens: z.object({
471-
total: z.number().optional(),
472-
input: z.number(),
473-
output: z.number(),
474-
reasoning: z.number(),
475-
cache: z.object({
476-
read: z.number(),
477-
write: z.number(),
481+
summary: Schema.optional(Schema.Boolean),
482+
cost: Schema.Number,
483+
tokens: Schema.Struct({
484+
total: Schema.optional(Schema.Number),
485+
input: Schema.Number,
486+
output: Schema.Number,
487+
reasoning: Schema.Number,
488+
cache: Schema.Struct({
489+
read: Schema.Number,
490+
write: Schema.Number,
478491
}),
479492
}),
480-
structured: z.any().optional(),
481-
variant: z.string().optional(),
482-
finish: z.string().optional(),
483-
}).meta({
484-
ref: "AssistantMessage",
493+
structured: Schema.optional(Schema.Any),
494+
variant: Schema.optional(Schema.String),
495+
finish: Schema.optional(Schema.String),
485496
})
486-
export type Assistant = z.infer<typeof Assistant>
497+
.annotate({ identifier: "AssistantMessage" })
498+
.pipe(withStatics((s) => ({ zod: zod(s) })))
499+
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
500+
error?: AssistantError
501+
}
487502

488-
export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({
489-
ref: "Message",
503+
const _Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
504+
export const Info = Object.assign(_Info, {
505+
zod: zod(_Info) as unknown as z.ZodType<User | Assistant>,
490506
})
491-
export type Info = z.infer<typeof Info>
507+
export type Info = User | Assistant
492508

493509
export const Event = {
494510
Updated: SyncEvent.define({
@@ -497,7 +513,7 @@ export const Event = {
497513
aggregate: "sessionID",
498514
schema: z.object({
499515
sessionID: SessionID.zod,
500-
info: Info,
516+
info: Info.zod,
501517
}),
502518
}),
503519
Removed: SyncEvent.define({
@@ -515,7 +531,7 @@ export const Event = {
515531
aggregate: "sessionID",
516532
schema: z.object({
517533
sessionID: SessionID.zod,
518-
part: Part,
534+
part: Part.zod,
519535
time: z.number(),
520536
}),
521537
}),
@@ -541,10 +557,10 @@ export const Event = {
541557
}),
542558
}
543559

544-
export const WithParts = z.object({
545-
info: Info,
546-
parts: z.array(Part),
547-
})
560+
export const WithParts = Schema.Struct({
561+
info: _Info,
562+
parts: Schema.Array(_Part),
563+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
548564
export type WithParts = {
549565
info: Info
550566
parts: Part[]

packages/opencode/src/session/prompt.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
12431243
{ message: info, parts },
12441244
)
12451245

1246-
const parsed = MessageV2.Info.safeParse(info)
1246+
const parsed = MessageV2.Info.zod.safeParse(info)
12471247
if (!parsed.success) {
12481248
log.error("invalid user message before save", {
12491249
sessionID: input.sessionID,
@@ -1254,7 +1254,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
12541254
})
12551255
}
12561256
parts.forEach((part, index) => {
1257-
const p = MessageV2.Part.safeParse(part)
1257+
const p = MessageV2.Part.zod.safeParse(part)
12581258
if (p.success) return
12591259
log.error("invalid user part before save", {
12601260
sessionID: input.sessionID,

packages/opencode/src/session/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export const Event = {
247247
z.object({
248248
sessionID: SessionID.zod.optional(),
249249
// z.lazy defers access to break circular dep: session → message-v2 → provider → plugin → session
250-
error: z.lazy(() => MessageV2.Assistant.shape.error),
250+
error: z.lazy(() => (MessageV2.Assistant.zod as unknown as z.ZodObject<any>).shape.error),
251251
}),
252252
),
253253
}

0 commit comments

Comments
 (0)