Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,8 @@ export const GithubRunCommand = cmd({
const err = result.info.error
console.error("Agent error:", err)
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
throw new Error(`${err.name}: ${err.data?.message || ""}`)
const message = "message" in err.data ? err.data.message : ""
throw new Error(`${err.name}: ${message}`)
}

const text = extractResponseText(result.parts)
Expand Down Expand Up @@ -1014,7 +1015,8 @@ export const GithubRunCommand = cmd({
const err = summary.info.error
console.error("Summary agent error:", err)
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
throw new Error(`${err.name}: ${err.data?.message || ""}`)
const message = "message" in err.data ? err.data.message : ""
throw new Error(`${err.name}: ${message}`)
}

const summaryText = extractResponseText(summary.parts)
Expand Down
54 changes: 23 additions & 31 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Schema, Types } from "effect"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { namedSchemaError } from "@/util/named-schema-error"
import { EffectLogger } from "@/effect"

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

export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
export const StructuredOutputError = NamedError.create(
"StructuredOutputError",
z.object({
message: z.string(),
retries: z.number(),
}),
)
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
export const APIError = NamedError.create(
"APIError",
z.object({
message: z.string(),
statusCode: z.number().optional(),
isRetryable: z.boolean(),
responseHeaders: z.record(z.string(), z.string()).optional(),
responseBody: z.string().optional(),
metadata: z.record(z.string(), z.string()).optional(),
}),
)
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
export const AbortedError = namedSchemaError("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = namedSchemaError("StructuredOutputError", {
message: Schema.String,
retries: Schema.Number,
})
export const AuthError = namedSchemaError("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const APIError = namedSchemaError("APIError", {
message: Schema.String,
statusCode: Schema.optional(Schema.Number),
isRetryable: Schema.Boolean,
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
responseBody: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
})
export type APIError = z.infer<typeof APIError.Schema>
export const ContextOverflowError = NamedError.create(
"ContextOverflowError",
z.object({ message: z.string(), responseBody: z.string().optional() }),
)
export const ContextOverflowError = namedSchemaError("ContextOverflowError", {
message: Schema.String,
responseBody: Schema.optional(Schema.String),
})

export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
type: Schema.Literal("text"),
Expand Down
59 changes: 59 additions & 0 deletions packages/opencode/src/util/named-schema-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Schema } from "effect"
import z from "zod"
import { zod } from "@/util/effect-zod"

/**
* Create a Schema-backed NamedError-shaped class.
*
* Drop-in replacement for `NamedError.create(tag, zodShape)` but backed by
* `Schema.Struct` under the hood. The wire shape emitted by the derived
* `.Schema` is still `{ name: tag, data: {...fields} }` so the generated
* OpenAPI/SDK output is byte-identical to the original NamedError schema.
*
* Preserves the existing surface:
* - static `Schema` (Zod schema of the wire shape)
* - static `isInstance(x)`
* - instance `toObject()` returning `{ name, data }`
* - `new X({ ...data }, { cause })`
*/
export function namedSchemaError<Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) {
// Wire shape matches the original NamedError output so the SDK stays stable.
const dataSchema = Schema.Struct(fields)
const wire = z
.object({
name: z.literal(tag),
data: zod(dataSchema),
})
.meta({ ref: tag })

type Data = Schema.Schema.Type<typeof dataSchema>

class NamedSchemaError extends Error {
static readonly Schema = wire
static readonly tag = tag
public static isInstance(input: unknown): input is NamedSchemaError {
return (
typeof input === "object" &&
input !== null &&
"name" in input &&
(input as { name: unknown }).name === tag
)
}

public override readonly name: Tag = tag
public readonly data: Data

constructor(data: Data, options?: ErrorOptions) {
super(tag, options)
this.data = data
}

toObject(): { name: Tag; data: Data } {
return { name: tag, data: this.data }
}
}

Object.defineProperty(NamedSchemaError, "name", { value: tag })

return NamedSchemaError
}
Loading