summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTommy D. Rossi <[email protected]>2026-01-21 21:14:56 +0100
committerGitHub <[email protected]>2026-01-21 14:14:56 -0600
commit416aaff488f5022065f4442049b5aede7967219c (patch)
treeb92922e00552c7c481e0cea0215a3f6561fd7c2b
parentaa599b4a7da3799dd170e1ece745b37fcc3c64a7 (diff)
downloadopencode-416aaff488f5022065f4442049b5aede7967219c.tar.gz
opencode-416aaff488f5022065f4442049b5aede7967219c.zip
feat(acp): add session/list and session/fork support (#7976)
-rw-r--r--bun.lock6
-rw-r--r--packages/opencode/package.json2
-rw-r--r--packages/opencode/src/acp/agent.ts147
3 files changed, 150 insertions, 5 deletions
diff --git a/bun.lock b/bun.lock
index b06c04809..403427ff6 100644
--- a/bun.lock
+++ b/bun.lock
@@ -263,7 +263,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",
@@ -554,7 +554,7 @@
"@adobe/css-tools": ["@adobe/[email protected]", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
- "@agentclientprotocol/sdk": ["@agentclientprotocol/[email protected]", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="],
+ "@agentclientprotocol/sdk": ["@agentclientprotocol/[email protected]", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected]", "", { "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=="],
@@ -3978,8 +3978,6 @@
"@actions/http-client/undici": ["[email protected]", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
- "@agentclientprotocol/sdk/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
-
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "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/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/[email protected]", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index a76099905..c2eaccbf3 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 0f5f23bfe..e165509b9 100644
--- a/packages/opencode/src/acp/agent.ts
+++ b/packages/opencode/src/acp/agent.ts
@@ -5,14 +5,21 @@ 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 ResumeSessionRequest,
+ type ResumeSessionResponse,
type Role,
+ type SessionInfo,
type SetSessionModelRequest,
type SetSessionModeRequest,
type SetSessionModeResponse,
@@ -430,6 +437,11 @@ export namespace ACP {
embeddedContext: true,
image: true,
},
+ sessionCapabilities: {
+ fork: {},
+ list: {},
+ resume: {},
+ },
},
authMethods: [authMethod],
agentInfo: {
@@ -540,6 +552,141 @@ export namespace ACP {
}
}
+ async unstable_listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
+ try {
+ const cursor = params.cursor ? Number(params.cursor) : undefined
+ const limit = 100
+
+ const sessions = await this.sdk.session
+ .list(
+ {
+ directory: params.cwd ?? undefined,
+ roots: true,
+ },
+ { throwOnError: true },
+ )
+ .then((x) => x.data ?? [])
+
+ const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
+ const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
+ 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<ForkSessionResponse> {
+ 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
+ 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,
+ })
+
+ 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
+ }
+ }
+
+ async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
+ const directory = params.cwd
+ const sessionId = params.sessionId
+ const mcpServers = params.mcpServers ?? []
+
+ try {
+ const model = await defaultModel(this.config, directory)
+ await this.sessionManager.load(sessionId, directory, mcpServers, model)
+
+ log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
+
+ return this.loadSessionMode({
+ cwd: directory,
+ mcpServers,
+ sessionId,
+ })
+ } 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