From c832de495d00c015409d6a2b7c258b50ce3eecd2 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Mon, 12 Jan 2026 11:45:05 +0100 Subject: [PATCH] feat(acp): add session/list and session/fork support --- bun.lock | 6 +- packages/opencode/package.json | 2 +- packages/opencode/src/acp/agent.ts | 107 +++++++++++++++++++++++++---- 3 files changed, 95 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index 8e8dab33ed2..5824b486750 100644 --- a/bun.lock +++ b/bun.lock @@ -255,7 +255,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.5.1", + "@agentclientprotocol/sdk": "0.12.0", "@ai-sdk/amazon-bedrock": "3.0.57", "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/azure": "2.0.82", @@ -540,7 +540,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.12.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.57", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.45", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mOUSLe+RgZzx0rtL1p9QXmSd/08z1EkBR+vQ1ydpd1t5P0Nx2kB8afiukEgM8nuDvmO9eYQlp7VTy1n5ffPs2g=="], @@ -3908,8 +3908,6 @@ "@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ba0722045e0..ae7d3200ee0 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -49,7 +49,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.5.1", + "@agentclientprotocol/sdk": "0.12.0", "@ai-sdk/amazon-bedrock": "3.0.57", "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/azure": "2.0.82", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 6d8a64b7d02..94d5eae2013 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -5,13 +5,18 @@ import { type AuthenticateRequest, type AuthMethod, type CancelNotification, + type ForkSessionRequest, + type ForkSessionResponse, type InitializeRequest, type InitializeResponse, + type ListSessionsRequest, + type ListSessionsResponse, type LoadSessionRequest, type NewSessionRequest, type PermissionOption, type PlanEntry, type PromptRequest, + type SessionInfo, type SetSessionModelRequest, type SetSessionModeRequest, type SetSessionModeResponse, @@ -362,6 +367,10 @@ export namespace ACP { embeddedContext: true, image: true, }, + sessionCapabilities: { + fork: {}, + list: {}, + }, }, authMethods: [authMethod], agentInfo: { @@ -431,25 +440,76 @@ export namespace ACP { this.setupEventSubscriptions(state) - // Replay session history - const messages = await this.sdk.session - .messages( + await this.replaySession(sessionId, directory) + + return mode + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: this.config.defaultModel?.providerID ?? "unknown", + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + async unstable_listSessions(params: ListSessionsRequest): Promise { + const input = params.cwd ? { directory: params.cwd } : undefined + const sessions = await this.sdk.session + .list(input, { throwOnError: true }) + .then((x) => x.data ?? []) + const cursor = params.cursor ? Number(params.cursor) : undefined + const ordered = sessions.toSorted((left, right) => right.time.updated - left.time.updated) + const filtered = + cursor !== undefined && Number.isFinite(cursor) + ? ordered.filter((session) => session.time.updated < cursor) + : ordered + const limit = 100 + const page = filtered.slice(0, limit) + const entries: SessionInfo[] = page.map((session) => ({ + sessionId: session.id, + cwd: session.directory, + title: session.title, + updatedAt: new Date(session.time.updated).toISOString(), + })) + const next = filtered.length > limit ? String(page[page.length - 1]?.time.updated ?? "") : undefined + const response: ListSessionsResponse = { + sessions: entries, + } + if (next) response.nextCursor = next + return response + } + + async unstable_forkSession(params: ForkSessionRequest): Promise { + const directory = params.cwd + const mcpServers = params.mcpServers ?? [] + + try { + const model = await defaultModel(this.config, directory) + + const forked = await this.sdk.session + .fork( { - sessionID: sessionId, + sessionID: params.sessionId, directory, }, { throwOnError: true }, ) - .then((x) => x.data) - .catch((err) => { - log.error("unexpected error when fetching message", { error: err }) - return undefined - }) - - for (const msg of messages ?? []) { - log.debug("replay message", msg) - await this.processMessage(msg) - } + .then((x) => x.data!) + + const sessionId = forked.id + const state = await this.sessionManager.load(sessionId, directory, mcpServers, model) + + log.info("fork_session", { sessionId, mcpServers: mcpServers.length }) + + const mode = await this.loadSessionMode({ + cwd: directory, + mcpServers, + sessionId, + }) + + this.setupEventSubscriptions(state) return mode } catch (e) { @@ -463,6 +523,23 @@ export namespace ACP { } } + private async replaySession(sessionId: string, directory: string) { + const messages = await this.sdk.session + .messages( + { + sessionID: sessionId, + directory, + }, + { throwOnError: true }, + ) + .then((x) => x.data ?? []) + + for (const msg of messages) { + log.debug("replay message", msg) + await this.processMessage(msg) + } + } + private async processMessage(message: SessionMessageResponse) { log.debug("process message", message) if (message.info.role !== "assistant" && message.info.role !== "user") return @@ -658,7 +735,7 @@ export namespace ACP { const model = await defaultModel(this.config, directory) const sessionId = params.sessionId - const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) + const providers = await this.sdk.config.providers({ directory }).then((x) => x.data?.providers ?? []) const entries = providers.sort((a, b) => { const nameA = a.name.toLowerCase() const nameB = b.name.toLowerCase()