Skip to content

Commit 51334b9

Browse files
committed
refactor(core): migrate MessageV2 errors to Schema-backed named errors
Adds namedSchemaError helper that preserves the existing NamedError API surface (.Schema, .isInstance, .toObject, new X({...}, { cause })) while backing the schema with Schema.Struct under the hood. Wire shape stays {name, data}, so the OpenAPI output is byte-identical.
1 parent 7d2ea87 commit 51334b9

3 files changed

Lines changed: 86 additions & 33 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,8 @@ export const GithubRunCommand = cmd({
985985
const err = result.info.error
986986
console.error("Agent error:", err)
987987
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
988-
throw new Error(`${err.name}: ${err.data?.message || ""}`)
988+
const message = "message" in err.data ? err.data.message : ""
989+
throw new Error(`${err.name}: ${message}`)
989990
}
990991

991992
const text = extractResponseText(result.parts)
@@ -1014,7 +1015,8 @@ export const GithubRunCommand = cmd({
10141015
const err = summary.info.error
10151016
console.error("Summary agent error:", err)
10161017
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
1017-
throw new Error(`${err.name}: ${err.data?.message || ""}`)
1018+
const message = "message" in err.data ? err.data.message : ""
1019+
throw new Error(`${err.name}: ${message}`)
10181020
}
10191021

10201022
const summaryText = extractResponseText(summary.parts)

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

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ModelID, ProviderID } from "@/provider/schema"
1818
import { Effect, Schema, Types } from "effect"
1919
import { zod, ZodOverride } from "@/util/effect-zod"
2020
import { withStatics } from "@/util/schema"
21+
import { namedSchemaError } from "@/util/named-schema-error"
2122
import { EffectLogger } from "@/effect"
2223

2324
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
@@ -30,38 +31,29 @@ interface FetchDecompressionError extends Error {
3031
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:"
3132
export { isMedia }
3233

33-
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
34-
export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
35-
export const StructuredOutputError = NamedError.create(
36-
"StructuredOutputError",
37-
z.object({
38-
message: z.string(),
39-
retries: z.number(),
40-
}),
41-
)
42-
export const AuthError = NamedError.create(
43-
"ProviderAuthError",
44-
z.object({
45-
providerID: z.string(),
46-
message: z.string(),
47-
}),
48-
)
49-
export const APIError = NamedError.create(
50-
"APIError",
51-
z.object({
52-
message: z.string(),
53-
statusCode: z.number().optional(),
54-
isRetryable: z.boolean(),
55-
responseHeaders: z.record(z.string(), z.string()).optional(),
56-
responseBody: z.string().optional(),
57-
metadata: z.record(z.string(), z.string()).optional(),
58-
}),
59-
)
34+
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
35+
export const AbortedError = namedSchemaError("MessageAbortedError", { message: Schema.String })
36+
export const StructuredOutputError = namedSchemaError("StructuredOutputError", {
37+
message: Schema.String,
38+
retries: Schema.Number,
39+
})
40+
export const AuthError = namedSchemaError("ProviderAuthError", {
41+
providerID: Schema.String,
42+
message: Schema.String,
43+
})
44+
export const APIError = namedSchemaError("APIError", {
45+
message: Schema.String,
46+
statusCode: Schema.optional(Schema.Number),
47+
isRetryable: Schema.Boolean,
48+
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
49+
responseBody: Schema.optional(Schema.String),
50+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
51+
})
6052
export type APIError = z.infer<typeof APIError.Schema>
61-
export const ContextOverflowError = NamedError.create(
62-
"ContextOverflowError",
63-
z.object({ message: z.string(), responseBody: z.string().optional() }),
64-
)
53+
export const ContextOverflowError = namedSchemaError("ContextOverflowError", {
54+
message: Schema.String,
55+
responseBody: Schema.optional(Schema.String),
56+
})
6557

6658
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
6759
type: Schema.Literal("text"),
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Schema } from "effect"
2+
import z from "zod"
3+
import { zod } from "@/util/effect-zod"
4+
5+
/**
6+
* Create a Schema-backed NamedError-shaped class.
7+
*
8+
* Drop-in replacement for `NamedError.create(tag, zodShape)` but backed by
9+
* `Schema.Struct` under the hood. The wire shape emitted by the derived
10+
* `.Schema` is still `{ name: tag, data: {...fields} }` so the generated
11+
* OpenAPI/SDK output is byte-identical to the original NamedError schema.
12+
*
13+
* Preserves the existing surface:
14+
* - static `Schema` (Zod schema of the wire shape)
15+
* - static `isInstance(x)`
16+
* - instance `toObject()` returning `{ name, data }`
17+
* - `new X({ ...data }, { cause })`
18+
*/
19+
export function namedSchemaError<Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) {
20+
// Wire shape matches the original NamedError output so the SDK stays stable.
21+
const dataSchema = Schema.Struct(fields)
22+
const wire = z
23+
.object({
24+
name: z.literal(tag),
25+
data: zod(dataSchema),
26+
})
27+
.meta({ ref: tag })
28+
29+
type Data = Schema.Schema.Type<typeof dataSchema>
30+
31+
class NamedSchemaError extends Error {
32+
static readonly Schema = wire
33+
static readonly tag = tag
34+
public static isInstance(input: unknown): input is NamedSchemaError {
35+
return (
36+
typeof input === "object" &&
37+
input !== null &&
38+
"name" in input &&
39+
(input as { name: unknown }).name === tag
40+
)
41+
}
42+
43+
public override readonly name: Tag = tag
44+
public readonly data: Data
45+
46+
constructor(data: Data, options?: ErrorOptions) {
47+
super(tag, options)
48+
this.data = data
49+
}
50+
51+
toObject(): { name: Tag; data: Data } {
52+
return { name: tag, data: this.data }
53+
}
54+
}
55+
56+
Object.defineProperty(NamedSchemaError, "name", { value: tag })
57+
58+
return NamedSchemaError
59+
}

0 commit comments

Comments
 (0)