--- id: 90e02e62 title: Add OpenCode agent provider with feature parity to Claude/Codex status: open deps: [] created: 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\`: 1. Tries port 4096 first 2. Falls back to random port if 4096 is busy 3. 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: 1. Send \`SIGUSR2\` to server process, OR 2. \`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 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 1. **New**: \`packages/server/src/server/agent/providers/opencode-agent.ts\` 2. **Modify**: \`packages/server/src/server/agent/agent-sdk-types.ts\` - Add \`"opencode"\` to AgentProvider 3. **Modify**: \`packages/server/src/server/agent/agent-registry.ts\` - Register OpenCode client 4. **New**: \`packages/server/src/server/agent/providers/opencode-server-manager.ts\` - Server lifecycle 5. **Modify**: \`package.json\` - Add \`@opencode/sdk\` dependency (if using SDK) ## Open Questions 1. Should we use the SDK or make raw HTTP calls? SDK provides types but adds dependency. 2. How to handle OpenCode version mismatches between Paseo's expectation and user's installed version? 3. 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: 1. Session creation 2. Text prompting/response 3. Streaming interruption 4. Tool call events (file edits) 5. Permission flow Uses @opencode-ai/sdk v1.1.12 with GLM-4.7-free model by default.