|
|
|
@@ -0,0 +1,332 @@
|
|
|
|
|
---
|
|
|
|
|
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 <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<void> {
|
|
|
|
|
// 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.
|