summaryrefslogtreecommitdiffhomepage
path: root/packages
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 /packages
parentaa599b4a7da3799dd170e1ece745b37fcc3c64a7 (diff)
downloadopencode-416aaff488f5022065f4442049b5aede7967219c.tar.gz
opencode-416aaff488f5022065f4442049b5aede7967219c.zip
feat(acp): add session/list and session/fork support (#7976)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/package.json2
-rw-r--r--packages/opencode/src/acp/agent.ts147
2 files changed, 148 insertions, 1 deletions
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