fix(core): preserve structured error messages (#33530)

This commit is contained in:
Aiden Cline 2026-06-23 18:43:59 -05:00 committed by GitHub
parent 10f6bb7878
commit c17b9557f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 73 additions and 9 deletions

View File

@ -14,7 +14,12 @@ export namespace FSUtil {
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", { export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
method: Schema.String, method: Schema.String,
cause: Schema.optional(Schema.Defect()), cause: Schema.optional(Schema.Defect()),
}) {} }) {
override get message() {
const detail = this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause)
return `Filesystem operation failed: ${this.method}${detail ? `: ${detail}` : ""}`
}
}
export type Error = PlatformError | FileSystemError export type Error = PlatformError | FileSystemError

View File

@ -10,7 +10,14 @@ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()(
exitCode: Schema.optional(Schema.Number), exitCode: Schema.optional(Schema.Number),
stderr: Schema.optional(Schema.String), stderr: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect()), cause: Schema.optional(Schema.Defect()),
}) {} }) {
override get message() {
const detail =
this.stderr?.trim() || (this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause))
const status = this.exitCode === undefined ? "" : ` (exit ${this.exitCode})`
return `Command failed${status}: ${this.command}${detail ? `: ${detail}` : ""}`
}
}
export interface RunOptions { export interface RunOptions {
readonly maxOutputBytes?: number readonly maxOutputBytes?: number

View File

@ -5,7 +5,11 @@ import { SessionSchema } from "./schema"
export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeError>()("Session.MessageDecodeError", { export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeError>()("Session.MessageDecodeError", {
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
messageID: SessionMessage.ID, messageID: SessionMessage.ID,
}) {} }) {
override get message() {
return `Failed to decode message ${this.messageID} in session ${this.sessionID}`
}
}
export class ContextSnapshotDecodeError extends Schema.TaggedErrorClass<ContextSnapshotDecodeError>()( export class ContextSnapshotDecodeError extends Schema.TaggedErrorClass<ContextSnapshotDecodeError>()(
"Session.ContextSnapshotDecodeError", "Session.ContextSnapshotDecodeError",

View File

@ -21,7 +21,11 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelec
{ {
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
}, },
) {} ) {
override get message() {
return `No model is available for session ${this.sessionID}`
}
}
export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavailableError>()( export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavailableError>()(
"SessionRunnerModel.ModelUnavailableError", "SessionRunnerModel.ModelUnavailableError",
@ -29,7 +33,11 @@ export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavaila
providerID: ProviderV2.ID, providerID: ProviderV2.ID,
modelID: ModelV2.ID, modelID: ModelV2.ID,
}, },
) {} ) {
override get message() {
return `Model unavailable: ${this.providerID}/${this.modelID}`
}
}
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()( export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError", "SessionRunnerModel.VariantUnavailableError",
@ -38,7 +46,11 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
modelID: ModelV2.ID, modelID: ModelV2.ID,
variant: ModelV2.VariantID, variant: ModelV2.VariantID,
}, },
) {} ) {
override get message() {
return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}`
}
}
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()( export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
"SessionRunnerModel.UnsupportedApiError", "SessionRunnerModel.UnsupportedApiError",
@ -47,7 +59,11 @@ export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiE
modelID: ModelV2.ID, modelID: ModelV2.ID,
api: Schema.String, api: Schema.String,
}, },
) {} ) {
override get message() {
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
}
}
export type Error = ModelNotSelectedError | ModelUnavailableError | VariantUnavailableError | UnsupportedApiError export type Error = ModelNotSelectedError | ModelUnavailableError | VariantUnavailableError | UnsupportedApiError

View File

@ -82,7 +82,11 @@ export type ReconcileResult = { readonly _tag: "Unchanged" } | Updated | Replace
export class InitializationBlocked extends Schema.TaggedErrorClass<InitializationBlocked>()( export class InitializationBlocked extends Schema.TaggedErrorClass<InitializationBlocked>()(
"SystemContext.InitializationBlocked", "SystemContext.InitializationBlocked",
{ keys: Schema.Array(Key) }, { keys: Schema.Array(Key) },
) {} ) {
override get message() {
return `System context initialization blocked by unavailable sources: ${this.keys.join(", ")}`
}
}
export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError>()("SystemContext.DuplicateKeyError", { export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError>()("SystemContext.DuplicateKeyError", {
key: Key, key: Key,

View File

@ -29,7 +29,12 @@ export interface BoundResult {
export class StorageError extends Schema.TaggedErrorClass<StorageError>()("ToolOutputStore.StorageError", { export class StorageError extends Schema.TaggedErrorClass<StorageError>()("ToolOutputStore.StorageError", {
operation: Schema.Literals(["encode", "write"]), operation: Schema.Literals(["encode", "write"]),
cause: Schema.Defect(), cause: Schema.Defect(),
}) {} }) {
override get message() {
const detail = this.cause instanceof Error ? this.cause.message : String(this.cause)
return `Failed to ${this.operation} tool output${detail ? `: ${detail}` : ""}`
}
}
export type Error = StorageError export type Error = StorageError

View File

@ -61,6 +61,7 @@ describe("AppProcess", () => {
if (reason && reason._tag === "Fail") { if (reason && reason._tag === "Fail") {
expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError) expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
expect((reason.error as AppProcess.AppProcessError).exitCode).toBe(1) expect((reason.error as AppProcess.AppProcessError).exitCode).toBe(1)
expect((reason.error as AppProcess.AppProcessError).message).toContain("Command failed (exit 1)")
} else { } else {
throw new Error("expected fail reason") throw new Error("expected fail reason")
} }

View File

@ -212,6 +212,7 @@ describe("SessionRunnerModel", () => {
modelID: "test-model", modelID: "test-model",
variant: "unknown", variant: "unknown",
}) })
expect(failure.message).toBe("Variant unavailable for test-provider/test-model: unknown")
}), }),
) )
@ -328,6 +329,7 @@ describe("SessionRunnerModel", () => {
modelID: "test-model", modelID: "test-model",
api: "aisdk:@ai-sdk/google", api: "aisdk:@ai-sdk/google",
}) })
expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google")
}), }),
) )

View File

@ -206,6 +206,7 @@ describe("ToolRegistry", () => {
expect(Exit.isFailure(exit)).toBe(true) expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBe(retentionFailure) if (Exit.isFailure(exit)) expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBe(retentionFailure)
expect(retentionFailure.message).toBe("Failed to write tool output: disk full")
}), }),
) )

View File

@ -1078,6 +1078,11 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
suggestions: Schema.optional(Schema.Array(Schema.String)), suggestions: Schema.optional(Schema.Array(Schema.String)),
cause: Schema.optional(Schema.Defect()), cause: Schema.optional(Schema.Defect()),
}) { }) {
override get message() {
const suggestions = this.suggestions?.length ? ` Did you mean: ${this.suggestions.join(", ")}?` : ""
return `Model not found: ${this.providerID}/${this.modelID}.${suggestions}`
}
static isInstance(input: unknown): input is ModelNotFoundError { static isInstance(input: unknown): input is ModelNotFoundError {
return input instanceof ModelNotFoundError return input instanceof ModelNotFoundError
} }
@ -1087,12 +1092,20 @@ export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderIni
providerID: ProviderV2.ID, providerID: ProviderV2.ID,
cause: Schema.optional(Schema.Defect()), cause: Schema.optional(Schema.Defect()),
}) { }) {
override get message() {
return `Failed to initialize provider: ${this.providerID}`
}
static isInstance(input: unknown): input is InitError { static isInstance(input: unknown): input is InitError {
return input instanceof InitError return input instanceof InitError
} }
} }
export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>()("ProviderNoProvidersError", {}) { export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>()("ProviderNoProvidersError", {}) {
override get message() {
return "No providers are available"
}
static isInstance(input: unknown): input is NoProvidersError { static isInstance(input: unknown): input is NoProvidersError {
return input instanceof NoProvidersError return input instanceof NoProvidersError
} }
@ -1101,6 +1114,10 @@ export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>(
export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("ProviderNoModelsError", { export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("ProviderNoModelsError", {
providerID: ProviderV2.ID, providerID: ProviderV2.ID,
}) { }) {
override get message() {
return `No models are available for provider: ${this.providerID}`
}
static isInstance(input: unknown): input is NoModelsError { static isInstance(input: unknown): input is NoModelsError {
return input instanceof NoModelsError return input instanceof NoModelsError
} }

View File

@ -1016,6 +1016,8 @@ it.instance("ModelNotFoundError includes suggestions for typos", () =>
.pipe(Effect.flip) .pipe(Effect.flip)
expect(error.suggestions).toBeDefined() expect(error.suggestions).toBeDefined()
expect((error.suggestions ?? []).length).toBeGreaterThan(0) expect((error.suggestions ?? []).length).toBeGreaterThan(0)
expect(error.message).toContain("Model not found: anthropic/claude-sonet-4")
expect(error.message).toContain("Did you mean:")
}), }),
) )