diff --git a/bun.lock b/bun.lock index 53ed347d..866ce447 100644 --- a/bun.lock +++ b/bun.lock @@ -173,7 +173,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.4.9", + "@agentclientprotocol/sdk": "0.5.1", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", @@ -401,7 +401,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.4.9", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-ExwH828LaTGoTTjxuw49l+fwOLA+Yx0+qkWn1TcHMOsY5mVI9CUfkj7ZDhv2klgZ7mJeT+lxX/Dn/KINv1AkNQ=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 841f972b..d9fe076f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -43,7 +43,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.4.9", + "@agentclientprotocol/sdk": "0.5.1", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", @@ -57,8 +57,8 @@ "@opentui/core": "0.1.33", "@opentui/solid": "0.1.33", "@parcel/watcher": "2.5.1", - "@solid-primitives/event-bus": "1.1.2", "@pierre/precision-diffs": "catalog:", + "@solid-primitives/event-bus": "1.1.2", "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 1eae36e6..47bac4e5 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1,9 +1,12 @@ import { + RequestError, type Agent as ACPAgent, type AgentSideConnection, type AuthenticateRequest, + type AuthMethod, type CancelNotification, type InitializeRequest, + type InitializeResponse, type LoadSessionRequest, type NewSessionRequest, type PermissionOption, @@ -33,6 +36,7 @@ import type { Config } from "@/config/config" import { MCP } from "@/mcp" import { Todo } from "@/session/todo" import { z } from "zod" +import { LoadAPIKeyError } from "ai" export namespace ACP { const log = Log.create({ service: "acp-agent" }) @@ -302,9 +306,26 @@ export namespace ACP { }) } - async initialize(params: InitializeRequest) { + async initialize(params: InitializeRequest): Promise { log.info("initialize", { protocolVersion: params.protocolVersion }) + const authMethod: AuthMethod = { + description: "Run `opencode auth login` in the terminal", + name: "Login with opencode", + id: "opencode-login", + } + + // If client supports terminal-auth capability, use that instead. + if (params.clientCapabilities?._meta?.["terminal-auth"] === true) { + authMethod._meta = { + "terminal-auth": { + command: "opencode", + args: ["auth", "login"], + label: "OpenCode Login", + }, + } + } + return { protocolVersion: 1, agentCapabilities: { @@ -325,10 +346,9 @@ export namespace ACP { id: "opencode-login", }, ], - _meta: { - opencode: { - version: Installation.VERSION, - }, + agentInfo: { + name: "OpenCode", + version: Installation.VERSION, }, } } @@ -338,21 +358,31 @@ export namespace ACP { } async newSession(params: NewSessionRequest) { - const model = await defaultModel(this.config) - const session = await this.sessionManager.create(params.cwd, params.mcpServers, model) + try { + const model = await defaultModel(this.config) + const session = await this.sessionManager.create(params.cwd, params.mcpServers, model) - log.info("creating_session", { mcpServers: params.mcpServers.length }) - const load = await this.loadSession({ - cwd: params.cwd, - mcpServers: params.mcpServers, - sessionId: session.id, - }) + log.info("creating_session", { mcpServers: params.mcpServers.length }) + const load = await this.loadSession({ + cwd: params.cwd, + mcpServers: params.mcpServers, + sessionId: session.id, + }) - return { - sessionId: session.id, - models: load.models, - modes: load.modes, - _meta: {}, + return { + sessionId: session.id, + models: load.models, + modes: load.modes, + _meta: {}, + } + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: this.config.defaultModel?.providerID ?? "unknown", + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e } } @@ -387,16 +417,6 @@ export namespace ACP { description: "compact the session", }) - setTimeout(() => { - this.connection.sessionUpdate({ - sessionId, - update: { - sessionUpdate: "available_commands_update", - availableCommands, - }, - }) - }, 0) - const availableModes = (await Agents.list()) .filter((agent) => agent.mode !== "subagent") .map((agent) => ({ @@ -437,6 +457,16 @@ export namespace ACP { }), ) + setTimeout(() => { + this.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands, + }, + }) + }, 0) + return { sessionId, models: { diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 5d45ee28..d3ab73d2 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -6,13 +6,18 @@ import type { ACPSessionState } from "./types" export class ACPSessionManager { private sessions = new Map() - async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise { + async create( + cwd: string, + mcpServers: McpServer[], + model?: ACPSessionState["model"], + ): Promise { const session = await Session.create({ title: `ACP Session ${crypto.randomUUID()}` }) const sessionId = session.id const resolvedModel = model ?? (await Provider.defaultModel()) const state: ACPSessionState = { id: sessionId, + parentId: session.parentID, cwd, mcpServers, createdAt: new Date(), diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts index 56308cb7..119b335c 100644 --- a/packages/opencode/src/acp/types.ts +++ b/packages/opencode/src/acp/types.ts @@ -2,6 +2,7 @@ import type { McpServer } from "@agentclientprotocol/sdk" export interface ACPSessionState { id: string + parentId?: string cwd: string mcpServers: McpServer[] createdAt: Date diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index d6de4a59..fbc88703 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -77,7 +77,15 @@ export namespace MCP { } }, async (state) => { - await Promise.all(Object.values(state.clients).map((client) => client.close())) + await Promise.all( + Object.values(state.clients).map((client) => + client.close().catch((error) => { + log.error("Failed to close MCP client", { + error, + }) + }), + ), + ) }, ) @@ -201,7 +209,15 @@ export namespace MCP { const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch(() => {}) if (!result) { - await mcpClient.close() + await mcpClient.close().catch((error) => { + log.error("Failed to close MCP client", { + error, + }) + }) + status = { + status: "failed", + error: "Failed to get tools", + } return { mcpClient: undefined, status: {