diff options
| author | Tommy D. Rossi <[email protected]> | 2026-01-21 21:14:56 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-21 14:14:56 -0600 |
| commit | 416aaff488f5022065f4442049b5aede7967219c (patch) | |
| tree | b92922e00552c7c481e0cea0215a3f6561fd7c2b /packages | |
| parent | aa599b4a7da3799dd170e1ece745b37fcc3c64a7 (diff) | |
| download | opencode-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.json | 2 | ||||
| -rw-r--r-- | packages/opencode/src/acp/agent.ts | 147 |
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 |
