Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
147 changes: 147 additions & 0 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -430,6 +437,11 @@ export namespace ACP {
embeddedContext: true,
image: true,
},
sessionCapabilities: {
fork: {},
list: {},
resume: {},
},
},
authMethods: [authMethod],
agentInfo: {
Expand Down Expand Up @@ -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
Expand Down