diff --git a/packages/opencode/dev.log b/packages/opencode/dev.log new file mode 100644 index 000000000..beafde9c0 --- /dev/null +++ b/packages/opencode/dev.log @@ -0,0 +1,129 @@ +INFO 2025-11-07T19:00:19 +115ms service=default version=local args=["acp"] opencode +INFO 2025-11-07T19:00:19 +0ms service=project directory=/Users/aidencline/development/opencode2/packages/opencode fromDirectory +INFO 2025-11-07T19:00:20 +218ms service=config path=/Users/aidencline/.config/opencode/config.json loading +INFO 2025-11-07T19:00:20 +1ms service=config path=/Users/aidencline/.config/opencode/opencode.json loading +INFO 2025-11-07T19:00:20 +1ms service=config path=/Users/aidencline/.config/opencode/opencode.jsonc loading +INFO 2025-11-07T19:00:20 +10ms service=config path=/Users/aidencline/development/opencode2/opencode.json loading +INFO 2025-11-07T19:00:20 +9ms service=plugin path=opencode-openai-codex-auth loading plugin +INFO 2025-11-07T19:00:20 +27ms service=plugin path=file:///Users/aidencline/.config/opencode/plugin/plugin.ts loading plugin +INFO 2025-11-07T19:00:20 +1ms service=plugin path=opencode-copilot-auth@0.0.4 loading plugin +INFO 2025-11-07T19:00:20 +3ms service=plugin path=opencode-anthropic-auth@0.0.2 loading plugin +INFO 2025-11-07T19:00:20 +1ms service=bus type=* subscribing +INFO 2025-11-07T19:00:20 +0ms service=bus type=session.updated subscribing +INFO 2025-11-07T19:00:20 +0ms service=bus type=message.updated subscribing +INFO 2025-11-07T19:00:20 +0ms service=bus type=message.part.updated subscribing +INFO 2025-11-07T19:00:20 +0ms service=format init +INFO 2025-11-07T19:00:20 +0ms service=lsp serverIds=deno, typescript, vue, eslint, gopls, ruby-lsp, pyright, elixir-ls, zls, csharp, rust, clangd, svelte, astro, jdtls, lua-ls enabled LSP servers +INFO 2025-11-07T19:00:20 +1ms service=bus type=command.executed subscribing +INFO 2025-11-07T19:00:20 +12ms service=server method=GET path=/config request +INFO 2025-11-07T19:00:20 +1ms service=server duration=1 response +INFO 2025-11-07T19:00:20 +1ms service=acp-command setup connection +INFO 2025-11-07T19:00:20 +1ms service=acp-agent protocolVersion=1 initialize +INFO 2025-11-07T19:00:20 +3ms service=server method=POST path=/session request +INFO 2025-11-07T19:00:20 +0ms service=project directory=/Users/aidencline/development/opencode2 fromDirectory +INFO 2025-11-07T19:00:20 +54ms service=config path=/Users/aidencline/development/opencode2/opencode.json loading +INFO 2025-11-07T19:00:20 +1ms service=plugin path=opencode-openai-codex-auth loading plugin +INFO 2025-11-07T19:00:20 +1ms service=plugin path=file:///Users/aidencline/.config/opencode/plugin/plugin.ts loading plugin +INFO 2025-11-07T19:00:20 +0ms service=plugin path=opencode-copilot-auth@0.0.4 loading plugin +INFO 2025-11-07T19:00:20 +0ms service=plugin path=opencode-anthropic-auth@0.0.2 loading plugin +INFO 2025-11-07T19:00:20 +0ms service=bus type=* subscribing +INFO 2025-11-07T19:00:20 +0ms service=bus type=session.updated subscribing +INFO 2025-11-07T19:00:20 +0ms service=bus type=message.updated subscribing +INFO 2025-11-07T19:00:20 +0ms service=bus type=message.part.updated subscribing +INFO 2025-11-07T19:00:20 +0ms service=format init +INFO 2025-11-07T19:00:20 +0ms service=lsp serverIds=deno, typescript, vue, eslint, gopls, ruby-lsp, pyright, elixir-ls, zls, csharp, rust, clangd, svelte, astro, jdtls, lua-ls enabled LSP servers +INFO 2025-11-07T19:00:20 +0ms service=bus type=command.executed subscribing +INFO 2025-11-07T19:00:20 +1ms service=session id=ses_5a04f19a0ffeJKEIITzQLVT2lM version=local projectID=4b0ea68d7af9a6031a7ffda7ad66e0cb83315750 directory=/Users/aidencline/development/opencode2 title=ACP Session 82ad3f07-82f6-4ae4-9e02-a27305bbbecc time={"created":1762542020191,"updated":1762542020191} created +INFO 2025-11-07T19:00:20 +1ms service=bus type=session.created publishing +INFO 2025-11-07T19:00:20 +0ms service=bus type=session.updated publishing +INFO 2025-11-07T19:00:20 +0ms service=server duration=58 response +INFO 2025-11-07T19:00:20 +1ms service=acp-session-manager state={"id":"ses_5a04f19a0ffeJKEIITzQLVT2lM","cwd":"/Users/aidencline/development/opencode2","mcpServers":[],"createdAt":"2025-11-07T19:00:20.193Z","model":{"providerID":"opencode","modelID":"big-pickle"}} creating_session +INFO 2025-11-07T19:00:20 +0ms service=acp-agent sessionId=ses_5a04f19a0ffeJKEIITzQLVT2lM mcpServers=0 creating_session +INFO 2025-11-07T19:00:20 +2ms service=server method=GET path=/config/providers request +INFO 2025-11-07T19:00:20 +1ms service=models.dev file={} refreshing +INFO 2025-11-07T19:00:20 +2ms service=provider init +INFO 2025-11-07T19:00:20 +4ms service=provider providerID=azure found +INFO 2025-11-07T19:00:20 +0ms service=provider providerID=openai found +INFO 2025-11-07T19:00:20 +0ms service=provider providerID=openrouter found +INFO 2025-11-07T19:00:20 +0ms service=provider providerID=anthropic found +INFO 2025-11-07T19:00:20 +0ms service=provider providerID=opencode found +INFO 2025-11-07T19:00:20 +1ms service=provider providerID=github-copilot found +INFO 2025-11-07T19:00:20 +2ms service=server duration=9 response +INFO 2025-11-07T19:00:20 +2ms service=server method=GET path=/agent request +INFO 2025-11-07T19:00:20 +1ms service=server duration=1 response +INFO 2025-11-07T19:00:20 +0ms service=server method=GET path=/command request +INFO 2025-11-07T19:00:20 +0ms service=server duration=0 response +INFO 2025-11-07T19:00:20 +2ms service=server method=GET path=/event request +INFO 2025-11-07T19:00:20 +0ms service=server event connected +INFO 2025-11-07T19:00:20 +1ms service=bus type=* subscribing +INFO 2025-11-07T19:00:20 +0ms service=server duration=1 response +INFO 2025-11-07T19:00:21 +1183ms service=acp-agent parts=[{"type":"text","text":"Hello"}] parts +INFO 2025-11-07T19:00:21 +1ms service=server method=POST path=/session/ses_5a04f19a0ffeJKEIITzQLVT2lM/message request +INFO 2025-11-07T19:00:21 +2ms service=session.prompt session=ses_5a04f19a0ffeJKEIITzQLVT2lM prompt +INFO 2025-11-07T19:00:21 +0ms service=server duration=2 response +INFO 2025-11-07T19:00:21 +4ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:21 +1ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:21 +1ms service=bus type=session.updated publishing +INFO 2025-11-07T19:00:21 +0ms service=provider providerID=opencode modelID=big-pickle getModel +INFO 2025-11-07T19:00:21 +1ms service=provider status=started providerID=opencode getSDK +INFO 2025-11-07T19:00:21 +57ms service=provider status=completed duration=57 providerID=opencode getSDK +INFO 2025-11-07T19:00:21 +1ms service=provider providerID=opencode modelID=big-pickle found +INFO 2025-11-07T19:00:21 +0ms service=session.lock sessionID=ses_5a04f19a0ffeJKEIITzQLVT2lM locked +INFO 2025-11-07T19:00:21 +0ms service=session.prompt session=ses_5a04f19a0ffeJKEIITzQLVT2lM sessionID=ses_5a04f19a0ffeJKEIITzQLVT2lM locking +INFO 2025-11-07T19:00:21 +34ms service=mcp key=context7 type=remote found +INFO 2025-11-07T19:00:21 +0ms service=mcp key=gh_grep type=remote found +INFO 2025-11-07T19:00:21 +0ms service=mcp key=weather type=local found +INFO 2025-11-07T19:00:21 +348ms service=mcp key=context7 transport=StreamableHTTP connected +INFO 2025-11-07T19:00:22 +160ms service=mcp key=gh_grep transport=StreamableHTTP connected +INFO 2025-11-07T19:00:23 +1265ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:23 +5ms service=session.prompt session=ses_5a04f19a0ffeJKEIITzQLVT2lM process +INFO 2025-11-07T19:00:23 +6ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:23 +0ms service=provider providerID=opencode modelID=claude-haiku-4-5 getModel +INFO 2025-11-07T19:00:23 +1ms service=provider status=started providerID=opencode getSDK +INFO 2025-11-07T19:00:23 +0ms service=bus type=session.updated publishing +INFO 2025-11-07T19:00:23 +0ms service=bus type=session.diff publishing +INFO 2025-11-07T19:00:23 +5ms service=provider status=completed duration=5 providerID=opencode getSDK +INFO 2025-11-07T19:00:23 +0ms service=provider providerID=opencode modelID=claude-haiku-4-5 found +INFO 2025-11-07T19:00:24 +1455ms service=snapshot hash=af2dd1a43d3ed11688756bc848a1803307be9925 + cwd=/Users/aidencline/development/opencode2 git=/Users/aidencline/.local/share/opencode/snapshot/4b0ea68d7af9a6031a7ffda7ad66e0cb83315750 tracking +INFO 2025-11-07T19:00:24 +1ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +62ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +47ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +50ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +1ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +46ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +1ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +34ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:24 +8ms service=session.summary title=Greeting title +INFO 2025-11-07T19:00:24 +0ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:25 +615ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:25 +0ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:25 +33ms service=snapshot hash=8fe0798d0105e13db6691dddbda02aa2facc14fb + cwd=/Users/aidencline/development/opencode2 git=/Users/aidencline/.local/share/opencode/snapshot/4b0ea68d7af9a6031a7ffda7ad66e0cb83315750 tracking +INFO 2025-11-07T19:00:25 +1ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:25 +0ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:25 +29ms service=bus type=message.part.updated publishing +INFO 2025-11-07T19:00:25 +0ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:25 +1ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:25 +0ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:25 +0ms service=session.compaction pruning +INFO 2025-11-07T19:00:25 +0ms service=session.lock sessionID=ses_5a04f19a0ffeJKEIITzQLVT2lM unlocked +INFO 2025-11-07T19:00:25 +0ms service=session.prompt session=ses_5a04f19a0ffeJKEIITzQLVT2lM sessionID=ses_5a04f19a0ffeJKEIITzQLVT2lM unlocking +INFO 2025-11-07T19:00:25 +1ms service=bus type=session.idle publishing +INFO 2025-11-07T19:00:25 +0ms service=server method=POST path=/tui/show-toast request +INFO 2025-11-07T19:00:25 +0ms service=bus type=tui.toast.show publishing +INFO 2025-11-07T19:00:25 +0ms service=server duration=0 response +INFO 2025-11-07T19:00:25 +2ms service=session.compaction pruned=0 total=0 found +INFO 2025-11-07T19:00:25 +22ms service=bus type=message.updated publishing +INFO 2025-11-07T19:00:25 +1ms service=bus type=session.updated publishing +INFO 2025-11-07T19:00:25 +1ms service=bus type=session.diff publishing +INFO 2025-11-07T19:00:27 +2162ms service=session.summary body=I greeted you and offered assistance with your development tasks. I was initiating a conversation to understand how I could best help you. body +INFO 2025-11-07T19:00:27 +1ms service=bus type=message.updated publishing diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 38263a7d4..7ac52b8f1 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -20,13 +20,11 @@ import { } from "@agentclientprotocol/sdk" import { Log } from "../util/log" import { ACPSessionManager } from "./session" -import type { ACPConfig } from "./types" +import type { ACPConfig, ACPSessionState } from "./types" import { Provider } from "../provider/provider" import { Installation } from "@/installation" import { Bus } from "@/bus" import { MessageV2 } from "@/session/message-v2" -import { Storage } from "@/storage/storage" -import { Permission } from "@/permission" import { Config } from "@/config/config" import { MCP } from "@/mcp" import { Todo } from "@/session/todo" @@ -59,270 +57,284 @@ export namespace ACP { this.connection = connection this.config = config this.sdk = config.sdk - this.setupEventSubscriptions() } - private setupEventSubscriptions() { + private setupEventSubscriptions(session: ACPSessionState) { + const sessionId = session.id + const directory = session.cwd + const options: PermissionOption[] = [ { optionId: "once", kind: "allow_once", name: "Allow once" }, { optionId: "always", kind: "allow_always", name: "Always allow" }, { optionId: "reject", kind: "reject_once", name: "Reject" }, ] - Bus.subscribe(Permission.Event.Updated, async (event) => { - const acpSession = this.sessionManager.get(event.properties.sessionID) - const directory = acpSession.cwd - - try { - const permission = event.properties - const res = await this.connection - .requestPermission({ - sessionId: acpSession.id, - toolCall: { - toolCallId: permission.callID ?? permission.id, - status: "pending", - title: permission.title, - rawInput: permission.metadata, - kind: toToolKind(permission.type), - locations: toLocations(permission.type, permission.metadata), - }, - options, - }) - .catch(async (error) => { - log.error("failed to request permission from ACP", { - error, - permissionID: permission.id, - sessionID: permission.sessionID, - }) - await this.config.sdk.postSessionIdPermissionsPermissionId({ - path: { id: permission.sessionID, permissionID: permission.id }, - body: { - response: "reject", - }, - query: { directory }, - }) - return - }) - if (!res) return - if (res.outcome.outcome !== "selected") { - await this.config.sdk.postSessionIdPermissionsPermissionId({ - path: { id: permission.sessionID, permissionID: permission.id }, - body: { - response: "reject", - }, - query: { directory }, - }) - return - } - await this.config.sdk.postSessionIdPermissionsPermissionId({ - path: { id: permission.sessionID, permissionID: permission.id }, - body: { - response: res.outcome.optionId as "once" | "always" | "reject", - }, - query: { directory }, - }) - } catch (err) { - log.error("unexpected error when handling permission", { error: err }) - throw err - } - }) - - Bus.subscribe(MessageV2.Event.PartUpdated, async (event) => { - const props = event.properties - const { part } = props - const acpSession = this.sessionManager.get(part.sessionID) - const directory = acpSession.cwd - - const message = await this.config.sdk.session - .message({ - throwOnError: true, - path: { - id: part.sessionID, - messageID: part.messageID, - }, - query: { directory }, - }) - .then((x) => x.data) - .catch(() => undefined) - - if (!message || message.info.role !== "assistant") return - - if (part.type === "tool") { - switch (part.state.status) { - case "pending": - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "tool_call", - toolCallId: part.callID, - title: part.tool, - kind: toToolKind(part.tool), - status: "pending", - locations: [], - rawInput: {}, - }, - }) - .catch((err) => { - log.error("failed to send tool pending to ACP", { error: err }) - }) - break - case "running": - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "in_progress", - locations: toLocations(part.tool, part.state.input), - rawInput: part.state.input, - }, - }) - .catch((err) => { - log.error("failed to send tool in_progress to ACP", { error: err }) - }) - break - case "completed": - const kind = toToolKind(part.tool) - const content: ToolCallContent[] = [ - { - type: "content", - content: { - type: "text", - text: part.state.output, - }, - }, - ] - - if (kind === "edit") { - const input = part.state.input - const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" - const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" - const newText = - typeof input["newString"] === "string" - ? input["newString"] - : typeof input["content"] === "string" - ? input["content"] - : "" - content.push({ - type: "diff", - path: filePath, - oldText, - newText, - }) - } - - if (part.tool === "todowrite") { - const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) - if (parsedTodos.success) { - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "plan", - entries: parsedTodos.data.map((todo) => { - const status: PlanEntry["status"] = - todo.status === "cancelled" - ? "completed" - : (todo.status as PlanEntry["status"]) - return { - priority: "medium", - status, - content: todo.content, - } - }), + this.config.sdk.event.subscribe().then(async (events) => { + for await (const event of events.stream) { + switch (event.type) { + case "permission.updated": + try { + const permission = event.properties + const res = await this.connection + .requestPermission({ + sessionId, + toolCall: { + toolCallId: permission.callID ?? permission.id, + status: "pending", + title: permission.title, + rawInput: permission.metadata, + kind: toToolKind(permission.type), + locations: toLocations(permission.type, permission.metadata), + }, + options, + }) + .catch(async (error) => { + log.error("failed to request permission from ACP", { + error, + permissionID: permission.id, + sessionID: permission.sessionID, + }) + await this.config.sdk.postSessionIdPermissionsPermissionId({ + path: { id: permission.sessionID, permissionID: permission.id }, + body: { + response: "reject", }, + query: { directory }, }) - .catch((err) => { - log.error("failed to send session update for todo", { error: err }) - }) - } else { - log.error("failed to parse todo output", { error: parsedTodos.error }) + return + }) + if (!res) return + if (res.outcome.outcome !== "selected") { + await this.config.sdk.postSessionIdPermissionsPermissionId({ + path: { id: permission.sessionID, permissionID: permission.id }, + body: { + response: "reject", + }, + query: { directory }, + }) + return } + await this.config.sdk.postSessionIdPermissionsPermissionId({ + path: { id: permission.sessionID, permissionID: permission.id }, + body: { + response: res.outcome.optionId as "once" | "always" | "reject", + }, + query: { directory }, + }) + } catch (err) { + log.error("unexpected error when handling permission", { error: err }) + } finally { + break } - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "completed", - kind, - content, - title: part.state.title, - rawOutput: { - output: part.state.output, - metadata: part.state.metadata, + case "message.part.updated": + log.info("message part updated", { event: event.properties }) + try { + const props = event.properties + const { part } = props + + const message = await this.config.sdk.session + .message({ + throwOnError: true, + path: { + id: part.sessionID, + messageID: part.messageID, }, - }, - }) - .catch((err) => { - log.error("failed to send tool completed to ACP", { error: err }) - }) - break - case "error": - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "failed", - content: [ - { - type: "content", - content: { - type: "text", - text: part.state.error, + query: { directory }, + }) + .then((x) => x.data) + .catch((err) => { + log.error("unexpected error when fetching message", { error: err }) + return undefined + }) + + if (!message || message.info.role !== "assistant") return + + if (part.type === "tool") { + switch (part.state.status) { + case "pending": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((err) => { + log.error("failed to send tool pending to ACP", { error: err }) + }) + break + case "running": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + }, + }) + .catch((err) => { + log.error("failed to send tool in_progress to ACP", { error: err }) + }) + break + case "completed": + const kind = toToolKind(part.tool) + const content: ToolCallContent[] = [ + { + type: "content", + content: { + type: "text", + text: part.state.output, + }, }, - }, - ], - rawOutput: { - error: part.state.error, - }, - }, - }) - .catch((err) => { - log.error("failed to send tool error to ACP", { error: err }) - }) - break - } - } else if (part.type === "text") { - const delta = props.delta - if (delta && part.synthetic !== true) { - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: delta, - }, - }, - }) - .catch((err) => { - log.error("failed to send text to ACP", { error: err }) - }) - } - } else if (part.type === "reasoning") { - const delta = props.delta - if (delta) { - await this.connection - .sessionUpdate({ - sessionId: acpSession.id, - update: { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text: delta, - }, - }, - }) - .catch((err) => { - log.error("failed to send reasoning to ACP", { error: err }) - }) + ] + + if (kind === "edit") { + const input = part.state.input + const filePath = + typeof input["filePath"] === "string" ? input["filePath"] : "" + const oldText = + typeof input["oldString"] === "string" ? input["oldString"] : "" + const newText = + typeof input["newString"] === "string" + ? input["newString"] + : typeof input["content"] === "string" + ? input["content"] + : "" + content.push({ + type: "diff", + path: filePath, + oldText, + newText, + }) + } + + if (part.tool === "todowrite") { + const parsedTodos = z + .array(Todo.Info) + .safeParse(JSON.parse(part.state.output)) + if (parsedTodos.success) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "plan", + entries: parsedTodos.data.map((todo) => { + const status: PlanEntry["status"] = + todo.status === "cancelled" + ? "completed" + : (todo.status as PlanEntry["status"]) + return { + priority: "medium", + status, + content: todo.content, + } + }), + }, + }) + .catch((err) => { + log.error("failed to send session update for todo", { error: err }) + }) + } else { + log.error("failed to parse todo output", { error: parsedTodos.error }) + } + } + + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "completed", + kind, + content, + title: part.state.title, + rawOutput: { + output: part.state.output, + metadata: part.state.metadata, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool completed to ACP", { error: err }) + }) + break + case "error": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "failed", + content: [ + { + type: "content", + content: { + type: "text", + text: part.state.error, + }, + }, + ], + rawOutput: { + error: part.state.error, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool error to ACP", { error: err }) + }) + break + } + } else if (part.type === "text") { + const delta = props.delta + if (delta && part.synthetic !== true) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: delta, + }, + }, + }) + .catch((err) => { + log.error("failed to send text to ACP", { error: err }) + }) + } + } else if (part.type === "reasoning") { + const delta = props.delta + if (delta) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: delta, + }, + }, + }) + .catch((err) => { + log.error("failed to send reasoning to ACP", { error: err }) + }) + } + } + } finally { + break + } } } }) @@ -378,24 +390,27 @@ export namespace ACP { try { const model = await defaultModel(this.config) - // Create session via SDK - const result = await this.sdk.session.create({ - body: { - title: `ACP Session ${crypto.randomUUID()}`, - }, - query: { - directory, - }, - }) + const session = await this.sdk.session + .create({ + body: { + title: `ACP Session ${crypto.randomUUID()}`, + }, + query: { + directory, + }, + throwOnError: true, + }) + .then((x) => x.data) - if (!result.data?.id) { - throw new Error("Failed to create session") - } - - const sessionId = result.data.id + const sessionId = session.id // Store ACP session state - await this.sessionManager.create(sessionId, params.cwd, params.mcpServers, model) + const state = await this.sessionManager.create( + sessionId, + params.cwd, + params.mcpServers, + model, + ) log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length }) @@ -405,6 +420,8 @@ export namespace ACP { sessionId, }) + this.setupEventSubscriptions(state) + return { sessionId, models: load.models, diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 57fc64171..fc7fa1ab6 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -1,6 +1,9 @@ import { RequestError, type McpServer } from "@agentclientprotocol/sdk" import { Provider } from "../provider/provider" import type { ACPSessionState } from "./types" +import { Log } from "@/util/log" + +const log = Log.create({ service: "acp-session-manager" }) export class ACPSessionManager { private sessions = new Map() @@ -20,6 +23,7 @@ export class ACPSessionManager { createdAt: new Date(), model: resolvedModel, } + log.info("creating_session", { state }) this.sessions.set(sessionId, state) return state @@ -27,8 +31,10 @@ export class ACPSessionManager { get(sessionId: string) { const session = this.sessions.get(sessionId) - if (!session) + if (!session) { + log.error("session not found", { sessionId }) throw RequestError.invalidParams(JSON.stringify({ error: `Session not found: ${sessionId}` })) + } return session } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index e771a903a..ef3d83ea3 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -59,8 +59,9 @@ export namespace Log { cleanup(Global.Path.log) if (options.print) return logpath = path.join( - Global.Path.log, - options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", + "dev.log", + // Global.Path.log, + // options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", ) const logfile = Bun.file(logpath) await fs.truncate(logpath).catch(() => {})