Răsfoiți Sursa

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

Aiden Cline 1 lună în urmă
părinte
comite
c17b9557f1

+ 6 - 1
packages/core/src/fs-util.ts

@@ -14,7 +14,12 @@ export namespace FSUtil {
   export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
     method: Schema.String,
     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
 

+ 8 - 1
packages/core/src/process.ts

@@ -10,7 +10,14 @@ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()(
   exitCode: Schema.optional(Schema.Number),
   stderr: Schema.optional(Schema.String),
   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 {
   readonly maxOutputBytes?: number

+ 5 - 1
packages/core/src/session/error.ts

@@ -5,7 +5,11 @@ import { SessionSchema } from "./schema"
 export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeError>()("Session.MessageDecodeError", {
   sessionID: SessionSchema.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>()(
   "Session.ContextSnapshotDecodeError",

+ 20 - 4
packages/core/src/session/runner/model.ts

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

+ 5 - 1
packages/core/src/system-context/index.ts

@@ -82,7 +82,11 @@ export type ReconcileResult = { readonly _tag: "Unchanged" } | Updated | Replace
 export class InitializationBlocked extends Schema.TaggedErrorClass<InitializationBlocked>()(
   "SystemContext.InitializationBlocked",
   { 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", {
   key: Key,

+ 6 - 1
packages/core/src/tool-output-store.ts

@@ -29,7 +29,12 @@ export interface BoundResult {
 export class StorageError extends Schema.TaggedErrorClass<StorageError>()("ToolOutputStore.StorageError", {
   operation: Schema.Literals(["encode", "write"]),
   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
 

+ 1 - 0
packages/core/test/process/process.test.ts

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

+ 2 - 0
packages/core/test/session-runner-model.test.ts

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

+ 1 - 0
packages/core/test/session-runner-tool-registry.test.ts

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

+ 17 - 0
packages/opencode/src/provider/provider.ts

@@ -1078,6 +1078,11 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
   suggestions: Schema.optional(Schema.Array(Schema.String)),
   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 {
     return input instanceof ModelNotFoundError
   }
@@ -1087,12 +1092,20 @@ export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderIni
   providerID: ProviderV2.ID,
   cause: Schema.optional(Schema.Defect()),
 }) {
+  override get message() {
+    return `Failed to initialize provider: ${this.providerID}`
+  }
+
   static isInstance(input: unknown): input is InitError {
     return input instanceof InitError
   }
 }
 
 export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>()("ProviderNoProvidersError", {}) {
+  override get message() {
+    return "No providers are available"
+  }
+
   static isInstance(input: unknown): input is NoProvidersError {
     return input instanceof NoProvidersError
   }
@@ -1101,6 +1114,10 @@ export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>(
 export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("ProviderNoModelsError", {
   providerID: ProviderV2.ID,
 }) {
+  override get message() {
+    return `No models are available for provider: ${this.providerID}`
+  }
+
   static isInstance(input: unknown): input is NoModelsError {
     return input instanceof NoModelsError
   }

+ 2 - 0
packages/opencode/test/provider/provider.test.ts

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