Files
paseo/.tasks/90e02e62.md
T

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`:

  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 { // 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.