diff --git a/packages/opencode/src/acp-next/agent.ts b/packages/opencode/src/acp-next/agent.ts
new file mode 100644
index 000000000000..b656b6331f65
--- /dev/null
+++ b/packages/opencode/src/acp-next/agent.ts
@@ -0,0 +1,60 @@
+import {
+ RequestError,
+ type Agent as ACPAgent,
+ type AgentSideConnection,
+ type AuthenticateRequest,
+ type CancelNotification,
+ type InitializeRequest,
+ type NewSessionRequest,
+ type PromptRequest,
+} from "@agentclientprotocol/sdk"
+import { Effect } from "effect"
+import type { OpencodeClient } from "@opencode-ai/sdk/v2"
+import * as ACPNextService from "./service"
+
+export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
+ return {
+ create: (_connection: AgentSideConnection) => {
+ return new Agent(ACPNextService.make())
+ },
+ }
+}
+
+export class Agent implements ACPAgent {
+ constructor(private readonly service: ACPNextService.Interface) {}
+
+ initialize(params: InitializeRequest) {
+ return run(this.service.initialize(params))
+ }
+
+ authenticate(params: AuthenticateRequest) {
+ return run(this.service.authenticate(params))
+ }
+
+ newSession(params: NewSessionRequest) {
+ return run(this.service.newSession(params))
+ }
+
+ prompt(params: PromptRequest) {
+ return run(this.service.prompt(params))
+ }
+
+ cancel(params: CancelNotification) {
+ return run(this.service.cancel(params))
+ }
+}
+
+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)
+ }
+}
+
+export * as ACPNext from "./agent"
diff --git a/packages/opencode/src/acp-next/service.ts b/packages/opencode/src/acp-next/service.ts
new file mode 100644
index 000000000000..b90c9c17547e
--- /dev/null
+++ b/packages/opencode/src/acp-next/service.ts
@@ -0,0 +1,102 @@
+import {
+ type AuthenticateRequest,
+ type AuthenticateResponse,
+ type AuthMethod,
+ type CancelNotification,
+ type InitializeRequest,
+ type InitializeResponse,
+ type NewSessionRequest,
+ type NewSessionResponse,
+ type PromptRequest,
+ type PromptResponse,
+} from "@agentclientprotocol/sdk"
+import { InstallationVersion } from "@opencode-ai/core/installation/version"
+import { Context, Effect, Schema } from "effect"
+
+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 Interface = {
+ readonly initialize: (input: InitializeRequest) => Effect.Effect
+ readonly authenticate: (input: AuthenticateRequest) => Effect.Effect
+ readonly newSession: (input: NewSessionRequest) => Effect.Effect
+ readonly prompt: (input: PromptRequest) => Effect.Effect
+ readonly cancel: (input: CancelNotification) => Effect.Effect
+}
+
+export class Service extends Context.Service()("@opencode/ACPNext/Service") {}
+
+export function make(): Interface {
+ const initialize = Effect.fn("ACPNext.initialize")(function* (params: InitializeRequest) {
+ const authMethod: AuthMethod = {
+ description: "Run `opencode auth login` in the terminal",
+ name: "Login with opencode",
+ id: AuthMethodID,
+ }
+
+ if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
+ authMethod._meta = {
+ "terminal-auth": {
+ command: "opencode",
+ args: ["auth", "login"],
+ label: "OpenCode Login",
+ },
+ }
+ }
+
+ return {
+ protocolVersion: 1,
+ agentCapabilities: {
+ mcpCapabilities: {
+ http: true,
+ sse: true,
+ },
+ promptCapabilities: {
+ embeddedContext: true,
+ image: true,
+ },
+ },
+ authMethods: [authMethod],
+ agentInfo: {
+ name: "OpenCode",
+ version: InstallationVersion,
+ },
+ }
+ })
+
+ const authenticate = Effect.fn("ACPNext.authenticate")(function* (params: AuthenticateRequest) {
+ if (params.methodId !== AuthMethodID) {
+ return yield* new UnknownAuthMethodError({ methodId: params.methodId })
+ }
+ return {}
+ })
+
+ return {
+ initialize,
+ authenticate,
+ newSession: Effect.fn("ACPNext.newSession")(function* (_input: NewSessionRequest) {
+ return yield* new UnsupportedOperationError({ method: "session/new" })
+ }),
+ prompt: Effect.fn("ACPNext.prompt")(function* (_input: PromptRequest) {
+ return yield* new UnsupportedOperationError({ method: "session/prompt" })
+ }),
+ cancel: Effect.fn("ACPNext.cancel")(function* (_input: CancelNotification) {
+ return yield* new UnsupportedOperationError({ method: "session/cancel" })
+ }),
+ }
+}
diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts
index b3b7df486b31..b113a278f965 100644
--- a/packages/opencode/src/cli/cmd/acp.ts
+++ b/packages/opencode/src/cli/cmd/acp.ts
@@ -3,10 +3,12 @@ import { Effect } from "effect"
import { effectCmd } from "../effect-cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { ACP } from "@/acp/agent"
+import { ACPNext } from "@/acp-next/agent"
import { Server } from "@/server/server"
import { ServerAuth } from "@/server/auth"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
+import { RuntimeFlags } from "@/effect/runtime-flags"
const log = Log.create({ service: "acp-command" })
@@ -22,6 +24,7 @@ export const AcpCommand = effectCmd({
},
handler: Effect.fn("Cli.acp")(function* (args) {
process.env.OPENCODE_CLIENT = "acp"
+ const flags = yield* RuntimeFlags.Service
const opts = yield* resolveNetworkOptions(args)
const server = yield* Effect.promise(() => Server.listen(opts))
@@ -54,7 +57,7 @@ export const AcpCommand = effectCmd({
})
const stream = ndJsonStream(input, output)
- const agent = ACP.init({ sdk })
+ const agent = flags.acpNext ? ACPNext.init({ sdk }) : ACP.init({ sdk })
new AgentSideConnection((conn) => {
return agent.create(conn, { sdk })
diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts
index efa8a264bac4..520bcb30f893 100644
--- a/packages/opencode/src/effect/runtime-flags.ts
+++ b/packages/opencode/src/effect/runtime-flags.ts
@@ -48,6 +48,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime
experimentalEventSystem: enabledByExperimental("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
experimentalWorkspaces: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"),
+ acpNext: bool("OPENCODE_ACP_NEXT"),
outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
diff --git a/packages/opencode/test/cli/acp-next/acp-next-process.test.ts b/packages/opencode/test/cli/acp-next/acp-next-process.test.ts
new file mode 100644
index 000000000000..08d15b9bfc23
--- /dev/null
+++ b/packages/opencode/test/cli/acp-next/acp-next-process.test.ts
@@ -0,0 +1,104 @@
+import { describe, expect } from "bun:test"
+import type { AuthenticateResponse, InitializeResponse } from "@agentclientprotocol/sdk"
+import { Effect } from "effect"
+import { cliIt } from "../../lib/cli-process"
+import { createAcpClient, expectOk } from "../acp/acp-test-client"
+
+describe("opencode acp-next (subprocess)", () => {
+ cliIt.live(
+ "responds to initialize behind OPENCODE_ACP_NEXT",
+ ({ opencode }) =>
+ Effect.gen(function* () {
+ const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } }))
+ const initialized = expectOk(
+ yield* acp.request("initialize", {
+ protocolVersion: 1,
+ clientCapabilities: { _meta: { "terminal-auth": true } },
+ }),
+ )
+
+ expect(initialized.protocolVersion).toBe(1)
+ expect(initialized.agentCapabilities?.promptCapabilities?.embeddedContext).toBe(true)
+ expect(initialized.agentCapabilities?.promptCapabilities?.image).toBe(true)
+ expect(initialized.agentCapabilities?.mcpCapabilities?.http).toBe(true)
+ expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(true)
+ expect(initialized.agentCapabilities?.sessionCapabilities).toBeUndefined()
+ expect(initialized.agentInfo?.name).toBe("OpenCode")
+ expect(initialized.authMethods?.[0]?.id).toBe("opencode-login")
+ expect(initialized.authMethods?.[0]?._meta?.["terminal-auth"]).toBeDefined()
+ }),
+ 60_000,
+ )
+
+ cliIt.live(
+ "authenticate succeeds for the advertised auth method and rejects unknown methods safely",
+ ({ opencode }) =>
+ Effect.gen(function* () {
+ const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } }))
+ const initialized = expectOk(yield* acp.request("initialize", { protocolVersion: 1 }))
+ const methodId = initialized.authMethods?.[0]?.id
+ expect(methodId).toBe("opencode-login")
+
+ expectOk(yield* acp.request("authenticate", { methodId }))
+
+ const rejected = yield* acp.request("authenticate", { methodId: "missing-auth-method" })
+ expect(errorCode(rejected.error)).toBe(-32602)
+ }),
+ 60_000,
+ )
+
+ cliIt.live(
+ "SDK-required session stubs fail with safe unsupported errors",
+ ({ home, opencode }) =>
+ Effect.gen(function* () {
+ const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } }))
+ yield* acp.request("initialize", { protocolVersion: 1 })
+
+ const newSession = yield* acp.request("session/new", { cwd: home, mcpServers: [] })
+ expect(errorCode(newSession.error)).toBe(-32601)
+
+ const prompt = yield* acp.request("session/prompt", {
+ sessionId: "ses_missing",
+ prompt: [{ type: "text", text: "hello" }],
+ })
+ expect(errorCode(prompt.error)).toBe(-32601)
+ }),
+ 60_000,
+ )
+
+ cliIt.live(
+ "exits cleanly when flagged stdin is closed",
+ ({ opencode }) =>
+ Effect.gen(function* () {
+ const exitedPromise = yield* Effect.scoped(
+ Effect.gen(function* () {
+ const acp = yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } })
+ return acp.exited
+ }),
+ )
+
+ const code = yield* Effect.promise(() => exitedPromise)
+ expect(typeof code === "number" || code === null).toBe(true)
+ }),
+ 60_000,
+ )
+
+ cliIt.live(
+ "default unflagged path still uses production ACP",
+ ({ opencode }) =>
+ Effect.gen(function* () {
+ const acp = createAcpClient(yield* opencode.acp())
+ const initialized = expectOk(yield* acp.request("initialize", { protocolVersion: 1 }))
+
+ expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({})
+ expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
+ }),
+ 60_000,
+ )
+})
+
+function errorCode(error: unknown) {
+ if (!error || typeof error !== "object") return undefined
+ if (!("code" in error)) return undefined
+ return typeof error.code === "number" ? error.code : undefined
+}