feat: add opencode provider and refactor stream model to head/tail architecture

This commit is contained in:
Mohamed Boudra
2026-01-11 16:00:25 +07:00
parent 7fe10f9b4a
commit ebabcc09e5
42 changed files with 4416 additions and 2926 deletions
+9
View File
@@ -0,0 +1,9 @@
---
id: 1a4d69cf
title: OpenCode: Interruption test
status: done
deps: [d0dc7bd2]
created: 2026-01-11T04:30:18.687Z
parentId: 90e02e62
---
+9
View File
@@ -0,0 +1,9 @@
---
id: 4672647b
title: OpenCode: Prompting and text response test
status: done
deps: [d0dc7bd2]
created: 2026-01-11T04:30:17.444Z
parentId: 90e02e62
---
+332
View File
@@ -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.
+92
View File
@@ -0,0 +1,92 @@
---
id: 9b83b90a
title: Refactor stream buffering to head/tail model
status: open
deps: []
created: 2026-01-11T08:48:35.444Z
---
## Goal
Replace the current special-case buffering logic with a unified head/tail model where:
- **Tail**: Committed StreamItem[] (history)
- **Head**: Active StreamItem[] (being streamed, same type as tail)
Both use the same `reduceStreamUpdate()` function. The only difference is that head items get flushed to tail when a stream item completes.
## Current Problems
1. `applyStreamEventWithBuffer()` has hardcoded assistant_message buffering
2. `StreamingBufferEntry` is a different type than `StreamItem`
3. Reasoning chunks go through reducer directly (no buffering)
4. Duplicate logic between `appendAssistantMessage()` and buffer handling
## Design
### Types
```typescript
// Head is just StreamItem[] - no special type needed
// Tail is just StreamItem[] - same as before
interface StreamState {
tail: StreamItem[]; // committed history
head: StreamItem[]; // active streaming items
}
```
### Core Function
```typescript
function applyStreamEvent(params: {
tail: StreamItem[];
head: StreamItem[];
event: AgentStreamEventPayload;
timestamp: Date;
}): {
tail: StreamItem[];
head: StreamItem[];
changedTail: boolean;
changedHead: boolean;
}
```
### Flush Logic
Head items are flushed to tail when:
1. A different item kind arrives (thought → tool_call flushes the thought)
2. Turn completion events (turn_completed, turn_failed, turn_canceled)
3. User message arrives (new turn starting)
### Implementation Steps
1. Remove `StreamingBufferEntry` type
2. Remove `StreamingHead` type (or repurpose)
3. Modify `applyStreamEventWithBuffer` → `applyStreamEvent` to work with head/tail
4. Head uses same `reduceStreamUpdate` as tail
5. Add flush logic based on item kind transitions
6. Update session-context.tsx to use new API
7. Update agent-stream-view.tsx to render head items
8. Update session-store.ts types
## Flush Conditions
```typescript
function shouldFlushHead(head: StreamItem[], incomingKind: StreamItemKind): boolean {
if (head.length === 0) return false;
const lastHeadKind = head[head.length - 1].kind;
// Different kind arriving = flush
if (lastHeadKind !== incomingKind) return true;
return false;
}
```
## Rendering
In agent-stream-view.tsx:
- Tail is rendered in FlatList (reversed, virtualized)
- Head is rendered in ListHeaderComponent (always visible, not virtualized since it's small)
This is similar to current approach but head can now contain any StreamItem types.
+9
View File
@@ -0,0 +1,9 @@
---
id: b694a5c0
title: OpenCode: Tool calls test (file edits)
status: done
deps: [d0dc7bd2]
created: 2026-01-11T04:30:19.923Z
parentId: 90e02e62
---
+9
View File
@@ -0,0 +1,9 @@
---
id: d0dc7bd2
title: OpenCode: Basic session creation test
status: done
deps: []
created: 2026-01-11T04:29:55.160Z
parentId: 90e02e62
---
+9
View File
@@ -0,0 +1,9 @@
---
id: ea3f3266
title: OpenCode: Permission flow test
status: done
deps: [d0dc7bd2]
created: 2026-01-11T04:30:21.283Z
parentId: 90e02e62
---