11 KiB
id, title, status, deps, created
| id | title | status | deps | created |
|---|---|---|---|---|
| 90e02e62 | Add OpenCode agent provider with feature parity to Claude/Codex | open | 2026-01-11T04:14:38.396Z |
OpenCode Agent Provider Integration
Overview
Add OpenCode as a third agent provider in Paseo alongside Claude and Codex. OpenCode is an open-source AI coding assistant by SST that supports multiple LLM providers (Anthropic, OpenAI, etc.) through user configuration.
Repository & Architecture
- Repository: https://github.com/sst/opencode
- Packages:
packages/opencode- Core TypeScript/Bun library (CLI + server)packages/sdk- JavaScript SDK for programmatic access (@opencode/sdk)
Server Architecture
OpenCode uses a server-based architecture:
- HTTP server with SSE for events (Hono framework on Bun)
- One server can handle multiple directories via `x-opencode-directory` header
- One server can handle multiple concurrent sessions in the same directory
- Storage is global at `~/.local/share/opencode/` (XDG data dir)
- Sessions stored at: `session/{projectID}/{sessionID}.json` where projectID = git root commit hash
Key Server Endpoints
| Endpoint | Method | Description |
|---|---|---|
| `/session` | POST | Create new session |
| `/session/:id/message` | POST | Send prompt (blocking) |
| `/session/:id/prompt_async` | POST | Send prompt (non-blocking) |
| `/session/:id/abort` | POST | Cancel/interrupt session |
| `/event` | GET (SSE) | Subscribe to all events |
| `/permission/:requestID/reply` | POST | Respond to permission request |
| `/config` | GET | Get configuration |
| `/instance/dispose` | POST | Dispose instance (for config reload) |
Directory Routing
Every API request specifies its working directory: ```typescript // Via header fetch('/session', { headers: { 'x-opencode-directory': '/path/to/project' } })
// Via query param fetch('/session?directory=/path/to/project') ```
The server maintains per-directory "Instance" contexts that are lazily created and cached.
Port Selection
When starting with `--port 0`:
- Tries port 4096 first
- Falls back to random port if 4096 is busy
- Returns actual port via server URL
For Paseo: We should choose our own random port to avoid race conditions and have predictable control.
SDK Usage
The JS SDK (`@opencode/sdk`) provides typed client:
```typescript import { Server } from "@opencode/sdk/v2"
const client = new Server("http://localhost:PORT")
// Create session const session = await client.session.create({ directory: "/path/to/project" }, { headers: { 'x-opencode-directory': '/path/to/project' } })
// Subscribe to events (SSE) for await (const event of client.event.subscribe()) { // Handle events }
// Send prompt await client.session.prompt_async({ sessionID: session.id, parts: [{ type: "text", text: "Your prompt" }], providerID: "anthropic", modelID: "claude-sonnet-4-20250514", agent: "coder" })
// Respond to permission await client.permission.reply({ requestID: "...", reply: "allow" // or "deny", "always", "session" }) ```
Event Types
Events come via SSE at `/event`. Key event types:
- `session.created` / `session.updated` / `session.deleted`
- `message.updated` - Message content updates
- `message.part.updated` - Streaming part updates (text, tool calls, reasoning)
- `permission.request` - Permission request from agent
- `session.error` - Error events
Message Part Types
```typescript type Part = | { type: "text"; content: string } | { type: "reasoning"; content: string } | { type: "tool-invocation"; toolName: string; input: object; state: "pending" | "running" | "complete" | "error"; output?: object } ```
Permission System
OpenCode has a permission system similar to Claude/Codex:
Permission Request
```typescript { id: string sessionID: string tool: string // e.g., "bash", "write", "edit" input: object // tool input parameters metadata?: object } ```
Permission Reply Options
- `"allow"` - Allow this specific request
- `"deny"` - Deny this specific request
- `"always"` - Always allow this tool
- `"session"` - Allow for this session
Mapping to Paseo Types
| Paseo | OpenCode |
|---|---|
| `AgentPermissionRequest.id` | `request.id` |
| `AgentPermissionRequest.name` | `request.tool` |
| `AgentPermissionRequest.kind` | Derive from tool name |
| `AgentPermissionRequest.input` | `request.input` |
| `AgentPermissionResponse.behavior: "allow"` | `reply: "allow"` |
| `AgentPermissionResponse.behavior: "deny"` | `reply: "deny"` |
Config & Model Selection
Users configure OpenCode in `~/.config/opencode/opencode.json` or project `opencode.json`:
```json { "model": "anthropic/claude-sonnet-4-20250514", "provider": { "anthropic": {}, "openai": { "apiKey": "..." } } } ```
For Paseo:
- Default to user's configured model
- Can override per-prompt via `providerID` + `modelID` in API call
Config Reload
To reload config without restarting server:
- Send `SIGUSR2` to server process, OR
- `POST /instance/dispose?directory=/path` to dispose specific instance
Implementation Requirements
1. Server Lifecycle Management
Create lazy singleton server manager: ```typescript class OpenCodeServerManager { private server: ChildProcess | null = null private port: number | null = null
async ensureRunning(): Promise<{ port: number; url: string }> { if (this.server && this.isHealthy()) { return { port: this.port!, url: `http://localhost:${this.port}` } }
// Choose random available port
this.port = await getRandomAvailablePort()
// Launch: opencode serve --port <port>
this.server = spawn('opencode', ['serve', '--port', String(this.port)])
// Wait for health check
await this.waitForHealth()
return { port: this.port, url: \`http://localhost:\${this.port}\` }
}
async shutdown(): Promise { // Graceful shutdown } } ```
2. Agent Types Update
```typescript // agent-sdk-types.ts export type AgentProvider = "codex" | "claude" | "opencode" ```
3. OpenCodeAgentClient Implementation
Implement `AgentClient` interface:
- `createSession(config)` - Create new session via POST /session
- `resumeSession(handle)` - Resume existing session
- `listPersistedAgents()` - List sessions via GET /session
4. OpenCodeAgentSession Implementation
Implement `AgentSession` interface:
| Method | OpenCode Implementation |
|---|---|
| `stream(prompt)` | POST /session/:id/prompt_async + SSE /event |
| `run(prompt)` | POST /session/:id/message (blocking) |
| `interrupt()` | POST /session/:id/abort |
| `respondToPermission()` | POST /permission/:id/reply |
| `getAvailableModes()` | Return static modes or from config |
| `setMode()` | Update session permission ruleset |
| `getRuntimeInfo()` | Return provider/model from session |
5. Event Translation
Map OpenCode events to Paseo `AgentStreamEvent`:
| OpenCode Event | Paseo Event |
|---|---|
| `session.created` | `thread_started` |
| `message.part.updated` (text) | `timeline` with `assistant_message` |
| `message.part.updated` (reasoning) | `timeline` with `reasoning` |
| `message.part.updated` (tool-invocation) | `timeline` with `tool_call` |
| `permission.request` | `permission_requested` |
| `session.error` | `turn_failed` |
6. Tool Call Mapping
Map OpenCode tool names to Paseo `ToolCallKind`:
| Tool Name Pattern | Kind |
|---|---|
| `read`, `glob`, `grep` | `read` |
| `write`, `edit` | `edit` |
| `bash`, `shell` | `execute` |
| `web_search`, `web_fetch` | `search` |
| Other | `other` |
Modes
OpenCode doesn't have the same mode system as Claude Code. Instead, it uses permission rulesets per-session. Map to equivalent behavior:
| Paseo Mode | OpenCode Equivalent |
|---|---|
| `auto` | Default permission rules |
| `acceptEdits` | Allow write/edit tools |
| `plan` | Limit to read-only tools |
| `bypassPermissions` | Allow all tools without prompting |
Testing Checklist
- Server starts on first agent creation
- Server reuses for subsequent agents
- Multiple agents in same directory work
- Multiple agents in different directories work
- Tool calls display correctly with input/output
- Permission requests show and can be approved/denied
- Interrupting prompts works
- Mode switching works
- Model ID is correctly reported
- Session resume works
- Graceful server shutdown on Paseo exit
Files to Create/Modify
- New: `packages/server/src/server/agent/providers/opencode-agent.ts`
- Modify: `packages/server/src/server/agent/agent-sdk-types.ts` - Add `"opencode"` to AgentProvider
- Modify: `packages/server/src/server/agent/agent-registry.ts` - Register OpenCode client
- New: `packages/server/src/server/agent/providers/opencode-server-manager.ts` - Server lifecycle
- Modify: `package.json` - Add `@opencode/sdk` dependency (if using SDK)
Open Questions
- Should we use the SDK or make raw HTTP calls? SDK provides types but adds dependency.
- How to handle OpenCode version mismatches between Paseo's expectation and user's installed version?
- Should Paseo show OpenCode's configured model in UI, or let user override?
Reference Files
- OpenCode server: `/tmp/opencode-repo/packages/opencode/src/server/server.ts`
- OpenCode session: `/tmp/opencode-repo/packages/opencode/src/session/index.ts`
- OpenCode permission: `/tmp/opencode-repo/packages/opencode/src/permission/index.ts`
- OpenCode SDK: `/tmp/opencode-repo/packages/sdk/js/src/v2/`
- Paseo Claude agent: `packages/server/src/server/agent/providers/claude-agent.ts`
- Paseo Codex agent: `packages/server/src/server/agent/providers/codex-mcp-agent.ts`
- Paseo agent types: `packages/server/src/server/agent/agent-sdk-types.ts`
Notes
2026-01-11T04:16:21.033Z
OpenCode repository cloned to /tmp/opencode-repo for inspection and debugging. This contains the full source code including server, SDK, CLI, and all internal modules.
2026-01-11T04:27:14.997Z
OpenCode CLI and server API verified working (v1.1.12). Server endpoints tested: POST /session creates session, POST /session/:id/message sends prompt and returns response. Response includes parts array with text content. Ready to implement provider integration.
2026-01-11T04:42:15.093Z
OpenCode agent provider implementation complete with TDD approach. All 5 tests passing:
- Session creation
- Text prompting/response
- Streaming interruption
- Tool call events (file edits)
- Permission flow
Uses @opencode-ai/sdk v1.1.12 with GLM-4.7-free model by default.