Skip to content

Commit f3f2119

Browse files
josephschmittopencode-botthdxractions-user
authored
feat: Add ACP (Agent Client Protocol) support (#2947)
Co-authored-by: opencode-bot <[email protected]> Co-authored-by: Dax Raad <[email protected]> Co-authored-by: GitHub Action <[email protected]>
1 parent 835fa9f commit f3f2119

File tree

13 files changed

+991
-160
lines changed

13 files changed

+991
-160
lines changed

bun.lock

Lines changed: 157 additions & 148 deletions
Large diffs are not rendered by default.

packages/opencode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@opencode-ai/sdk": "workspace:*",
4848
"@parcel/watcher": "2.5.1",
4949
"@standard-schema/spec": "1.0.0",
50+
"@zed-industries/agent-client-protocol": "0.4.5",
5051
"@zip.js/zip.js": "2.7.62",
5152
"ai": "catalog:",
5253
"chokidar": "4.0.3",
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# ACP (Agent Client Protocol) Implementation
2+
3+
This directory contains a clean, protocol-compliant implementation of the [Agent Client Protocol](https://agentclientprotocol.com/) for opencode.
4+
5+
## Architecture
6+
7+
The implementation follows a clean separation of concerns:
8+
9+
### Core Components
10+
11+
- **`agent.ts`** - Implements the `Agent` interface from `@zed-industries/agent-client-protocol`
12+
- Handles initialization and capability negotiation
13+
- Manages session lifecycle (`session/new`, `session/load`)
14+
- Processes prompts and returns responses
15+
- Properly implements ACP protocol v1
16+
17+
- **`client.ts`** - Implements the `Client` interface for client-side capabilities
18+
- File operations (`readTextFile`, `writeTextFile`)
19+
- Permission requests (auto-approves for now)
20+
- Terminal support (stub implementation)
21+
22+
- **`session.ts`** - Session state management
23+
- Creates and tracks ACP sessions
24+
- Maps ACP sessions to internal opencode sessions
25+
- Maintains working directory context
26+
- Handles MCP server configurations
27+
28+
- **`server.ts`** - ACP server startup and lifecycle
29+
- Sets up JSON-RPC over stdio using the official library
30+
- Manages graceful shutdown on SIGTERM/SIGINT
31+
- Provides Instance context for the agent
32+
33+
- **`types.ts`** - Type definitions for internal use
34+
35+
## Usage
36+
37+
### Command Line
38+
39+
```bash
40+
# Start the ACP server in the current directory
41+
opencode acp
42+
43+
# Start in a specific directory
44+
opencode acp --cwd /path/to/project
45+
```
46+
47+
### Programmatic
48+
49+
```typescript
50+
import { ACPServer } from "./acp/server"
51+
52+
await ACPServer.start()
53+
```
54+
55+
### Integration with Zed
56+
57+
Add to your Zed configuration (`~/.config/zed/settings.json`):
58+
59+
```json
60+
{
61+
"agent_servers": {
62+
"OpenCode": {
63+
"command": "opencode",
64+
"args": ["acp"]
65+
}
66+
}
67+
}
68+
```
69+
70+
## Protocol Compliance
71+
72+
This implementation follows the ACP specification v1:
73+
74+
**Initialization**
75+
76+
- Proper `initialize` request/response with protocol version negotiation
77+
- Capability advertisement (`agentCapabilities`)
78+
- Authentication support (stub)
79+
80+
**Session Management**
81+
82+
- `session/new` - Create new conversation sessions
83+
- `session/load` - Resume existing sessions (basic support)
84+
- Working directory context (`cwd`)
85+
- MCP server configuration support
86+
87+
**Prompting**
88+
89+
- `session/prompt` - Process user messages
90+
- Content block handling (text, resources)
91+
- Response with stop reasons
92+
93+
**Client Capabilities**
94+
95+
- File read/write operations
96+
- Permission requests
97+
- Terminal support (stub for future)
98+
99+
## Current Limitations
100+
101+
### Not Yet Implemented
102+
103+
1. **Streaming Responses** - Currently returns complete responses instead of streaming via `session/update` notifications
104+
2. **Tool Call Reporting** - Doesn't report tool execution progress
105+
3. **Session Modes** - No mode switching support yet
106+
4. **Authentication** - No actual auth implementation
107+
5. **Terminal Support** - Placeholder only
108+
6. **Session Persistence** - `session/load` doesn't restore actual conversation history
109+
110+
### Future Enhancements
111+
112+
- **Real-time Streaming**: Implement `session/update` notifications for progressive responses
113+
- **Tool Call Visibility**: Report tool executions as they happen
114+
- **Session Persistence**: Save and restore full conversation history
115+
- **Mode Support**: Implement different operational modes (ask, code, etc.)
116+
- **Enhanced Permissions**: More sophisticated permission handling
117+
- **Terminal Integration**: Full terminal support via opencode's bash tool
118+
119+
## Testing
120+
121+
```bash
122+
# Run ACP tests
123+
bun test test/acp.test.ts
124+
125+
# Test manually with stdio
126+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}' | opencode acp
127+
```
128+
129+
## Design Decisions
130+
131+
### Why the Official Library?
132+
133+
We use `@zed-industries/agent-client-protocol` instead of implementing JSON-RPC ourselves because:
134+
135+
- Ensures protocol compliance
136+
- Handles edge cases and future protocol versions
137+
- Reduces maintenance burden
138+
- Works with other ACP clients automatically
139+
140+
### Clean Architecture
141+
142+
Each component has a single responsibility:
143+
144+
- **Agent** = Protocol interface
145+
- **Client** = Client-side operations
146+
- **Session** = State management
147+
- **Server** = Lifecycle and I/O
148+
149+
This makes the codebase maintainable and testable.
150+
151+
### Mapping to OpenCode
152+
153+
ACP sessions map cleanly to opencode's internal session model:
154+
155+
- ACP `session/new` → creates internal Session
156+
- ACP `session/prompt` → uses SessionPrompt.prompt()
157+
- Working directory context preserved per-session
158+
- Tool execution uses existing ToolRegistry
159+
160+
## References
161+
162+
- [ACP Specification](https://agentclientprotocol.com/)
163+
- [TypeScript Library](https://github.com/zed-industries/agent-client-protocol/tree/main/typescript)
164+
- [Protocol Examples](https://github.com/zed-industries/agent-client-protocol/tree/main/typescript/examples)

packages/opencode/src/acp/agent.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type {
2+
Agent,
3+
AgentSideConnection,
4+
AuthenticateRequest,
5+
AuthenticateResponse,
6+
CancelNotification,
7+
InitializeRequest,
8+
InitializeResponse,
9+
LoadSessionRequest,
10+
LoadSessionResponse,
11+
NewSessionRequest,
12+
NewSessionResponse,
13+
PromptRequest,
14+
PromptResponse,
15+
} from "@zed-industries/agent-client-protocol"
16+
import { Log } from "../util/log"
17+
import { ACPSessionManager } from "./session"
18+
import type { ACPConfig } from "./types"
19+
import { Provider } from "../provider/provider"
20+
import { SessionPrompt } from "../session/prompt"
21+
import { Identifier } from "../id/id"
22+
23+
export class OpenCodeAgent implements Agent {
24+
private log = Log.create({ service: "acp-agent" })
25+
private sessionManager = new ACPSessionManager()
26+
private connection: AgentSideConnection
27+
private config: ACPConfig
28+
29+
constructor(connection: AgentSideConnection, config: ACPConfig = {}) {
30+
this.connection = connection
31+
this.config = config
32+
}
33+
34+
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
35+
this.log.info("initialize", { protocolVersion: params.protocolVersion })
36+
37+
return {
38+
protocolVersion: 1,
39+
agentCapabilities: {
40+
loadSession: false,
41+
},
42+
_meta: {
43+
opencode: {
44+
version: await import("../installation").then((m) => m.Installation.VERSION),
45+
},
46+
},
47+
}
48+
}
49+
50+
async authenticate(params: AuthenticateRequest): Promise<void | AuthenticateResponse> {
51+
this.log.info("authenticate", { methodId: params.methodId })
52+
throw new Error("Authentication not yet implemented")
53+
}
54+
55+
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
56+
this.log.info("newSession", { cwd: params.cwd, mcpServers: params.mcpServers.length })
57+
58+
const session = await this.sessionManager.create(params.cwd, params.mcpServers)
59+
60+
return {
61+
sessionId: session.id,
62+
_meta: {},
63+
}
64+
}
65+
66+
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
67+
this.log.info("loadSession", { sessionId: params.sessionId, cwd: params.cwd })
68+
69+
await this.sessionManager.load(params.sessionId, params.cwd, params.mcpServers)
70+
71+
return {
72+
_meta: {},
73+
}
74+
}
75+
76+
async prompt(params: PromptRequest): Promise<PromptResponse> {
77+
this.log.info("prompt", {
78+
sessionId: params.sessionId,
79+
promptLength: params.prompt.length,
80+
})
81+
82+
const acpSession = this.sessionManager.get(params.sessionId)
83+
if (!acpSession) {
84+
throw new Error(`Session not found: ${params.sessionId}`)
85+
}
86+
87+
const model = this.config.defaultModel || (await Provider.defaultModel())
88+
89+
const parts = params.prompt.map((content) => {
90+
if (content.type === "text") {
91+
return {
92+
type: "text" as const,
93+
text: content.text,
94+
}
95+
}
96+
if (content.type === "resource") {
97+
const resource = content.resource
98+
let text = ""
99+
if ("text" in resource && typeof resource.text === "string") {
100+
text = resource.text
101+
}
102+
return {
103+
type: "text" as const,
104+
text,
105+
}
106+
}
107+
return {
108+
type: "text" as const,
109+
text: JSON.stringify(content),
110+
}
111+
})
112+
113+
await SessionPrompt.prompt({
114+
sessionID: acpSession.openCodeSessionId,
115+
messageID: Identifier.ascending("message"),
116+
model: {
117+
providerID: model.providerID,
118+
modelID: model.modelID,
119+
},
120+
parts,
121+
acpConnection: {
122+
connection: this.connection,
123+
sessionId: params.sessionId,
124+
},
125+
})
126+
127+
this.log.debug("prompt response completed")
128+
129+
// Streaming notifications are now handled during prompt execution
130+
// No need to send final text chunk here
131+
132+
return {
133+
stopReason: "end_turn",
134+
_meta: {},
135+
}
136+
}
137+
138+
async cancel(params: CancelNotification): Promise<void> {
139+
this.log.info("cancel", { sessionId: params.sessionId })
140+
}
141+
}

0 commit comments

Comments
 (0)