diff --git a/packages/opencode/src/acp-next/agent.ts b/packages/opencode/src/acp-next/agent.ts
index b656b6331f65..4290117f585d 100644
--- a/packages/opencode/src/acp-next/agent.ts
+++ b/packages/opencode/src/acp-next/agent.ts
@@ -10,6 +10,7 @@ import {
} from "@agentclientprotocol/sdk"
import { Effect } from "effect"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
+import * as ACPNextError from "./error"
import * as ACPNextService from "./service"
export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
@@ -45,16 +46,10 @@ export class Agent implements ACPAgent {
}
function run(effect: Effect.Effect) {
- return Effect.runPromise(effect.pipe(Effect.mapError(toRequestError)))
-}
-
-function toRequestError(error: ACPNextService.Error) {
- switch (error._tag) {
- case "ACPNextUnknownAuthMethodError":
- return RequestError.invalidParams({ methodId: error.methodId }, `unknown auth method: ${error.methodId}`)
- case "ACPNextUnsupportedOperationError":
- return RequestError.methodNotFound(error.method)
- }
+ return Effect.runPromise(effect.pipe(Effect.mapError(ACPNextError.toRequestError))).catch((defect: unknown) => {
+ if (defect instanceof RequestError) throw defect
+ throw ACPNextError.toRequestError(ACPNextError.fromUnknownDefect(defect))
+ })
}
export * as ACPNext from "./agent"
diff --git a/packages/opencode/src/acp-next/error.ts b/packages/opencode/src/acp-next/error.ts
new file mode 100644
index 000000000000..1d4af53b5030
--- /dev/null
+++ b/packages/opencode/src/acp-next/error.ts
@@ -0,0 +1,93 @@
+import { RequestError } from "@agentclientprotocol/sdk"
+import { Schema } from "effect"
+
+export class SessionNotFoundError extends Schema.TaggedErrorClass()(
+ "ACPNextSessionNotFoundError",
+ {
+ sessionId: Schema.String,
+ },
+) {}
+
+export class InvalidConfigOptionError extends Schema.TaggedErrorClass()(
+ "ACPNextInvalidConfigOptionError",
+ {
+ configId: Schema.String,
+ },
+) {}
+
+export class InvalidModelError extends Schema.TaggedErrorClass()("ACPNextInvalidModelError", {
+ modelId: Schema.String,
+ providerId: Schema.optional(Schema.String),
+}) {}
+
+export class InvalidEffortError extends Schema.TaggedErrorClass()("ACPNextInvalidEffortError", {
+ effort: Schema.String,
+}) {}
+
+export class InvalidModeError extends Schema.TaggedErrorClass()("ACPNextInvalidModeError", {
+ mode: Schema.String,
+}) {}
+
+export class AuthRequiredError extends Schema.TaggedErrorClass()("ACPNextAuthRequiredError", {
+ providerId: Schema.optional(Schema.String),
+}) {}
+
+export class UnknownAuthMethodError extends Schema.TaggedErrorClass()(
+ "ACPNextUnknownAuthMethodError",
+ {
+ methodId: Schema.String,
+ },
+) {}
+
+export class UnsupportedOperationError extends Schema.TaggedErrorClass()(
+ "ACPNextUnsupportedOperationError",
+ {
+ method: Schema.String,
+ },
+) {}
+
+export class ServiceFailureError extends Schema.TaggedErrorClass()("ACPNextServiceFailureError", {
+ safeMessage: Schema.String,
+ service: Schema.optional(Schema.String),
+}) {}
+
+export type Error =
+ | SessionNotFoundError
+ | InvalidConfigOptionError
+ | InvalidModelError
+ | InvalidEffortError
+ | InvalidModeError
+ | AuthRequiredError
+ | UnknownAuthMethodError
+ | UnsupportedOperationError
+ | ServiceFailureError
+
+export function toRequestError(error: Error) {
+ switch (error._tag) {
+ case "ACPNextSessionNotFoundError":
+ return RequestError.invalidParams({ sessionId: error.sessionId }, `session not found: ${error.sessionId}`)
+ case "ACPNextInvalidConfigOptionError":
+ return RequestError.invalidParams({ configId: error.configId }, `unknown config option: ${error.configId}`)
+ case "ACPNextInvalidModelError":
+ return RequestError.invalidParams(
+ { providerId: error.providerId, modelId: error.modelId },
+ `model not found: ${error.modelId}`,
+ )
+ case "ACPNextInvalidEffortError":
+ return RequestError.invalidParams({ effort: error.effort }, `effort not found: ${error.effort}`)
+ case "ACPNextInvalidModeError":
+ return RequestError.invalidParams({ mode: error.mode }, `mode not found: ${error.mode}`)
+ case "ACPNextAuthRequiredError":
+ return RequestError.authRequired({ providerId: error.providerId }, "provider authentication required")
+ case "ACPNextUnknownAuthMethodError":
+ return RequestError.invalidParams({ methodId: error.methodId }, `unknown auth method: ${error.methodId}`)
+ case "ACPNextUnsupportedOperationError":
+ return RequestError.methodNotFound(error.method)
+ case "ACPNextServiceFailureError":
+ return RequestError.internalError({ service: error.service }, error.safeMessage)
+ }
+}
+
+export function fromUnknownDefect(_defect: unknown, safeMessage = "Internal service failure") {
+ return new ServiceFailureError({ safeMessage })
+}
diff --git a/packages/opencode/src/acp-next/service.ts b/packages/opencode/src/acp-next/service.ts
index b90c9c17547e..8ee1a8bd292c 100644
--- a/packages/opencode/src/acp-next/service.ts
+++ b/packages/opencode/src/acp-next/service.ts
@@ -11,25 +11,12 @@ import {
type PromptResponse,
} from "@agentclientprotocol/sdk"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
-import { Context, Effect, Schema } from "effect"
+import { Context, Effect } from "effect"
+import * as ACPNextError from "./error"
export const AuthMethodID = "opencode-login"
-export class UnknownAuthMethodError extends Schema.TaggedErrorClass()(
- "ACPNextUnknownAuthMethodError",
- {
- methodId: Schema.String,
- },
-) {}
-
-export class UnsupportedOperationError extends Schema.TaggedErrorClass()(
- "ACPNextUnsupportedOperationError",
- {
- method: Schema.String,
- },
-) {}
-
-export type Error = UnknownAuthMethodError | UnsupportedOperationError
+export type Error = ACPNextError.Error
export type Interface = {
readonly initialize: (input: InitializeRequest) => Effect.Effect
@@ -81,7 +68,7 @@ export function make(): Interface {
const authenticate = Effect.fn("ACPNext.authenticate")(function* (params: AuthenticateRequest) {
if (params.methodId !== AuthMethodID) {
- return yield* new UnknownAuthMethodError({ methodId: params.methodId })
+ return yield* new ACPNextError.UnknownAuthMethodError({ methodId: params.methodId })
}
return {}
})
@@ -90,13 +77,13 @@ export function make(): Interface {
initialize,
authenticate,
newSession: Effect.fn("ACPNext.newSession")(function* (_input: NewSessionRequest) {
- return yield* new UnsupportedOperationError({ method: "session/new" })
+ return yield* new ACPNextError.UnsupportedOperationError({ method: "session/new" })
}),
prompt: Effect.fn("ACPNext.prompt")(function* (_input: PromptRequest) {
- return yield* new UnsupportedOperationError({ method: "session/prompt" })
+ return yield* new ACPNextError.UnsupportedOperationError({ method: "session/prompt" })
}),
cancel: Effect.fn("ACPNext.cancel")(function* (_input: CancelNotification) {
- return yield* new UnsupportedOperationError({ method: "session/cancel" })
+ return yield* new ACPNextError.UnsupportedOperationError({ method: "session/cancel" })
}),
}
}
diff --git a/packages/opencode/test/acp-next/error.test.ts b/packages/opencode/test/acp-next/error.test.ts
new file mode 100644
index 000000000000..a82c6c576e11
--- /dev/null
+++ b/packages/opencode/test/acp-next/error.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, test } from "bun:test"
+import { RequestError } from "@agentclientprotocol/sdk"
+import * as ACPNextError from "../../src/acp-next/error"
+
+describe("acp-next.error", () => {
+ test("maps validation failures to invalid params", () => {
+ const cases: ACPNextError.Error[] = [
+ new ACPNextError.SessionNotFoundError({ sessionId: "ses_missing" }),
+ new ACPNextError.InvalidConfigOptionError({ configId: "temperature" }),
+ new ACPNextError.InvalidModelError({ providerId: "anthropic", modelId: "claude-missing" }),
+ new ACPNextError.InvalidEffortError({ effort: "extreme" }),
+ new ACPNextError.InvalidModeError({ mode: "turbo" }),
+ ]
+
+ expect(cases.map((error) => ACPNextError.toRequestError(error).code)).toEqual([
+ -32602, -32602, -32602, -32602, -32602,
+ ])
+ })
+
+ test("includes safe validation details", () => {
+ expect(ACPNextError.toRequestError(new ACPNextError.SessionNotFoundError({ sessionId: "ses_123" }))).toMatchObject({
+ code: -32602,
+ data: { sessionId: "ses_123" },
+ })
+ expect(ACPNextError.toRequestError(new ACPNextError.InvalidModelError({ modelId: "gpt-missing" }))).toMatchObject({
+ code: -32602,
+ data: { modelId: "gpt-missing" },
+ })
+ })
+
+ test("maps auth required to the SDK auth error", () => {
+ const requestError = ACPNextError.toRequestError(new ACPNextError.AuthRequiredError({ providerId: "anthropic" }))
+
+ expect(requestError).toBeInstanceOf(RequestError)
+ expect(requestError.code).toBe(-32000)
+ expect(requestError.message).toBe("Authentication required: provider authentication required")
+ expect(requestError.data).toEqual({ providerId: "anthropic" })
+ })
+
+ test("maps unsupported operations to method not found", () => {
+ const requestError = ACPNextError.toRequestError(
+ new ACPNextError.UnsupportedOperationError({ method: "session/new" }),
+ )
+
+ expect(requestError.code).toBe(-32601)
+ expect(requestError.data).toEqual({ method: "session/new" })
+ })
+
+ test("maps service failures to safe internal errors", () => {
+ const requestError = ACPNextError.toRequestError(
+ new ACPNextError.ServiceFailureError({ service: "provider", safeMessage: "Provider request failed" }),
+ )
+
+ expect(requestError.code).toBe(-32603)
+ expect(requestError.message).toBe("Internal error: Provider request failed")
+ expect(requestError.data).toEqual({ service: "provider" })
+ })
+
+ test("wraps unknown defects without leaking raw details", () => {
+ const requestError = ACPNextError.toRequestError(
+ ACPNextError.fromUnknownDefect(new Error("stack has sk-ant-secret and oauth refresh token")),
+ )
+ const serialized = JSON.stringify(requestError.toErrorResponse())
+
+ expect(requestError.code).toBe(-32603)
+ expect(requestError.message).toBe("Internal error: Internal service failure")
+ expect(serialized).not.toContain("sk-ant-secret")
+ expect(serialized).not.toContain("oauth refresh token")
+ expect(serialized).not.toContain("stack")
+ })
+})