mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-30 22:00:53 +08:00
refactor(core): migrate MessageV2.Format to Effect Schema (#23744)
This commit is contained in:
@@ -186,6 +186,80 @@ schema module with a clear domain.
|
||||
Major cluster. Message + event types flow through the SSE API and every SDK
|
||||
output, so byte-identical SDK surface is critical.
|
||||
|
||||
Suggested order for this cluster, starting from the leaves that `session.ts`
|
||||
and the SSE/event surface depend on:
|
||||
|
||||
1. `src/session/schema.ts` ✅ already migrated
|
||||
2. `src/provider/schema.ts` if `message-v2.ts` still relies on zod-first IDs
|
||||
3. `src/lsp/*` schema leaves needed by `LSP.Range`
|
||||
4. `src/snapshot/*` leaves used by `Snapshot.FileDiff`
|
||||
5. `src/session/message-v2.ts`
|
||||
6. `src/session/message.ts`
|
||||
7. `src/session/prompt.ts`
|
||||
8. `src/session/revert.ts`
|
||||
9. `src/session/summary.ts`
|
||||
10. `src/session/status.ts`
|
||||
11. `src/session/todo.ts`
|
||||
12. `src/session/session.ts`
|
||||
13. `src/session/compaction.ts`
|
||||
|
||||
Dependency sketch:
|
||||
|
||||
```text
|
||||
session.ts
|
||||
|- project/schema.ts
|
||||
|- control-plane/schema.ts
|
||||
|- permission/schema.ts
|
||||
|- snapshot/*
|
||||
|- message-v2.ts
|
||||
| |- provider/schema.ts
|
||||
| |- lsp/*
|
||||
| |- snapshot/*
|
||||
| |- sync/index.ts
|
||||
| `- bus/bus-event.ts
|
||||
|- sync/index.ts
|
||||
|- bus/bus-event.ts
|
||||
`- util/update-schema.ts
|
||||
```
|
||||
|
||||
Working rule for this cluster:
|
||||
|
||||
- migrate reusable leaf schemas and nested payload objects first
|
||||
- migrate aggregate DTOs like `Session.Info` after their nested pieces exist as
|
||||
named Schema values
|
||||
- leave zod-only event/update helpers in place temporarily when converting
|
||||
them would force unrelated churn across sync/bus boundaries
|
||||
|
||||
`message-v2.ts` first-pass outline:
|
||||
|
||||
1. Schema-backed imports already available
|
||||
- `SessionID`, `MessageID`, `PartID`
|
||||
- `ProviderID`, `ModelID`
|
||||
2. Local leaf objects to extract and migrate first
|
||||
- output format payloads
|
||||
- common part bases like `PartBase`
|
||||
- timestamp/range helper objects like `time.start/end`
|
||||
- file/source helper objects
|
||||
- token/cost/model helper objects
|
||||
3. Part variants built from those leaves
|
||||
- `SnapshotPart`, `PatchPart`, `TextPart`, `ReasoningPart`
|
||||
- `FilePart`, `AgentPart`, `CompactionPart`, `SubtaskPart`
|
||||
- retry/step/tool related parts
|
||||
4. Higher-level unions and DTOs
|
||||
- `FilePartSource`
|
||||
- part unions
|
||||
- message unions and assistant/user payloads
|
||||
5. Errors and event payloads last
|
||||
- `NamedError.create(...)` shapes can stay temporarily if converting them to
|
||||
`Schema.TaggedErrorClass` would force unrelated churn
|
||||
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can keep using
|
||||
derived `.zod` until the sync/bus layers are migrated
|
||||
|
||||
Possible later tightening after the Schema-first migration is stable:
|
||||
|
||||
- promote repeated opaque strings and timestamp numbers into branded/newtype
|
||||
leaf schemas where that adds domain value without changing the wire format
|
||||
|
||||
- [ ] `src/session/compaction.ts`
|
||||
- [ ] `src/session/message-v2.ts`
|
||||
- [ ] `src/session/message.ts`
|
||||
|
||||
@@ -15,7 +15,8 @@ import { isMedia } from "@/util/media"
|
||||
import type { SystemError } from "bun"
|
||||
import type { Provider } from "@/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { EffectLogger } from "@/effect"
|
||||
|
||||
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
|
||||
@@ -61,28 +62,28 @@ export const ContextOverflowError = NamedError.create(
|
||||
z.object({ message: z.string(), responseBody: z.string().optional() }),
|
||||
)
|
||||
|
||||
export const OutputFormatText = z
|
||||
.object({
|
||||
type: z.literal("text"),
|
||||
})
|
||||
.meta({
|
||||
ref: "OutputFormatText",
|
||||
})
|
||||
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
|
||||
type: Schema.Literal("text"),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
|
||||
export const OutputFormatJsonSchema = z
|
||||
.object({
|
||||
type: z.literal("json_schema"),
|
||||
schema: z.record(z.string(), z.any()).meta({ ref: "JSONSchema" }),
|
||||
retryCount: z.number().int().min(0).default(2),
|
||||
})
|
||||
.meta({
|
||||
ref: "OutputFormatJsonSchema",
|
||||
})
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: Schema.Number.check(Schema.isInt())
|
||||
.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
|
||||
export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]).meta({
|
||||
ref: "OutputFormat",
|
||||
const _Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "OutputFormat",
|
||||
})
|
||||
export type OutputFormat = z.infer<typeof Format>
|
||||
export const Format = Object.assign(_Format, { zod: zod(_Format) })
|
||||
export type OutputFormat = Schema.Schema.Type<typeof _Format>
|
||||
|
||||
const PartBase = z.object({
|
||||
id: PartID.zod,
|
||||
@@ -360,7 +361,7 @@ export const User = Base.extend({
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
}),
|
||||
format: Format.optional(),
|
||||
format: Format.zod.optional(),
|
||||
summary: z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
|
||||
@@ -1716,7 +1716,7 @@ export const PromptInput = z.object({
|
||||
.record(z.string(), z.boolean())
|
||||
.optional()
|
||||
.describe("@deprecated tools and permissions have been merged, you can set permissions on the session itself now"),
|
||||
format: MessageV2.Format.optional(),
|
||||
format: MessageV2.Format.zod.optional(),
|
||||
system: z.string().optional(),
|
||||
variant: z.string().optional(),
|
||||
parts: z.array(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
describe("structured-output.OutputFormat", () => {
|
||||
test("parses text format", () => {
|
||||
const result = MessageV2.Format.safeParse({ type: "text" })
|
||||
const result = MessageV2.Format.zod.safeParse({ type: "text" })
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe("text")
|
||||
@@ -13,7 +13,7 @@ describe("structured-output.OutputFormat", () => {
|
||||
})
|
||||
|
||||
test("parses json_schema format with defaults", () => {
|
||||
const result = MessageV2.Format.safeParse({
|
||||
const result = MessageV2.Format.zod.safeParse({
|
||||
type: "json_schema",
|
||||
schema: { type: "object", properties: { name: { type: "string" } } },
|
||||
})
|
||||
@@ -27,7 +27,7 @@ describe("structured-output.OutputFormat", () => {
|
||||
})
|
||||
|
||||
test("parses json_schema format with custom retryCount", () => {
|
||||
const result = MessageV2.Format.safeParse({
|
||||
const result = MessageV2.Format.zod.safeParse({
|
||||
type: "json_schema",
|
||||
schema: { type: "object" },
|
||||
retryCount: 5,
|
||||
@@ -39,17 +39,17 @@ describe("structured-output.OutputFormat", () => {
|
||||
})
|
||||
|
||||
test("rejects invalid type", () => {
|
||||
const result = MessageV2.Format.safeParse({ type: "invalid" })
|
||||
const result = MessageV2.Format.zod.safeParse({ type: "invalid" })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects json_schema without schema", () => {
|
||||
const result = MessageV2.Format.safeParse({ type: "json_schema" })
|
||||
const result = MessageV2.Format.zod.safeParse({ type: "json_schema" })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects negative retryCount", () => {
|
||||
const result = MessageV2.Format.safeParse({
|
||||
const result = MessageV2.Format.zod.safeParse({
|
||||
type: "json_schema",
|
||||
schema: { type: "object" },
|
||||
retryCount: -1,
|
||||
|
||||
Reference in New Issue
Block a user