diff --git a/bun.lock b/bun.lock index 10001bb6195..7a7a3bae573 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.73", "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/azure": "2.0.91", @@ -542,7 +542,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.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@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-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="], @@ -3922,8 +3922,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.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], diff --git a/package.json b/package.json index d134a187a7c..361987355ee 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.5", + "packageManager": "bun@1.3.6", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 07fee7d730d..403515d442f 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.73", "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/azure": "2.0.91", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index ebd65bb26da..f2fbf2a0bbf 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, @@ -379,6 +384,10 @@ export namespace ACP { embeddedContext: true, image: true, }, + sessionCapabilities: { + fork: {}, + list: {}, + }, }, authMethods: [authMethod], agentInfo: { @@ -480,6 +489,110 @@ export namespace ACP { } } + async unstable_listSessions(params: ListSessionsRequest): Promise { + try { + 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 last = page[page.length - 1] + const next = filtered.length > limit && last ? String(last.time.updated) : undefined + const response: ListSessionsResponse = { + sessions: entries, + } + if (next) response.nextCursor = next + return response + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: this.config.defaultModel?.providerID ?? "unknown", + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + 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: params.sessionId, + directory, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + + if (!forked) { + throw new Error("Fork session returned no 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) + + // Replay forked session history + const messages = await this.sdk.session + .messages( + { + sessionID: 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) + } + + return mode + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: this.config.defaultModel?.providerID ?? "unknown", + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + private async processMessage(message: SessionMessageResponse) { log.debug("process message", message) if (message.info.role !== "assistant" && message.info.role !== "user") return