opencode-cursor-agent/docs/architecture/2026-04-14-cursor-adapter.md

344 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Architecture: Cursor Adapter
## Overview
Cursor Adapter 是一個本機 HTTP proxy server將 OpenAI-compatible API 請求轉換為 Cursor CLI headless 模式指令,並將 Cursor CLI 的 streaming JSON 輸出轉換為 OpenAI SSE 格式回傳給 CLI 工具。
核心架構:**單一 binary、無狀態、spawn 子程序模式**。
### Requirement Traceability
| PRD Requirement | Architectural Component |
|----------------|------------------------|
| FR1: OpenAI-compatible API | HTTP Server (net/http + chi router) |
| FR2: Cursor CLI Integration | CLI Bridge (os/exec subprocess) |
| FR3: Streaming Response Conversion | Stream Converter (goroutine pipeline) |
| FR4: Model Listing | Model Registry |
| FR5: Configuration | Config Module (YAML) |
| FR6: Error Handling | Error Handler (middleware) |
| NFR1: Performance < 500ms overhead | goroutine pipeline, zero-copy streaming |
| NFR2: Concurrent requests 5 | semaphore (buffered channel) |
## System Architecture
### Technology Stack
| Layer | Technology | Justification |
|-------|-----------|---------------|
| Language | Go 1.22+ | 單一 binarysubprocess 管理好goroutine 天然適合 streaming |
| HTTP Router | go-chi/chi v5 | 輕量相容 net/httpmiddleware 支援好 |
| Config | gopkg.in/yaml.v3 | 標準 YAML 解析 |
| CLI | spf13/cobra | Go 標準 CLI 框架 |
| Testing | testify + stdlib | table-driven test + assertion helper |
### Component Architecture
```
┌─────────────────────────────────────────────────┐
│ cursor-adapter (single binary) │
│ │
│ cmd/ │
│ └── cursor-adapter/main.go (cobra entrypoint) │
│ │
│ internal/ │
│ ├── server/ HTTP Server (chi router) │
│ │ ├── handler.go route handlers │
│ │ ├── middleware.go error + logging │
│ │ └── sse.go SSE writer helpers │
│ ├── bridge/ CLI Bridge │
│ │ ├── bridge.go spawn subprocess │
│ │ └── scanner.go stdout line reader │
│ ├── converter/ Stream Converter │
│ │ └── convert.go cursor-json → OpenAI SSE │
│ └── config/ Config Module │
│ └── config.go YAML loading + defaults │
│ │
└─────────────────────────────────────────────────┘
│ os/exec.CommandContext
┌──────────────────┐
│ Cursor CLI │
│ agent -p ... │
│ --model ... │
│ --output-format│
│ stream-json │
└──────────────────┘
```
## Service Boundaries
單一 binary4 internal package
| Package | Responsibility | Exported |
|---------|---------------|----------|
| cmd/cursor-adapter | CLI 入口wiring | main() |
| internal/server | HTTP routes + middleware | NewServer(), Server.Run() |
| internal/bridge | spawn/manage Cursor CLI subprocess | Bridge interface + CLIBridge |
| internal/converter | stream-json OpenAI SSE 轉換 | Convert() functions |
| internal/config | YAML config 載入/驗證 | Config struct, Load() |
### Communication Matrix
| From | To | Pattern | Purpose |
|------|----|---------|---------|
| server/handler | bridge | interface call | 啟動子程序 |
| bridge | converter | channel (chan string) | 逐行傳遞 stdout |
| converter | server/handler | channel (chan SSEChunk) | 回傳轉換後的 chunk |
| server/handler | client | HTTP SSE | 回傳給 CLI 工具 |
## Data Flow
### Chat Completion (Streaming)
```
1. Client → POST /v1/chat/completions (stream: true)
2. handler → 驗證 request body
3. handler → 從 messages[] 組合 prompt
4. bridge → ctx, cancel := context.WithTimeout(...)
5. bridge → cmd := exec.CommandContext(ctx, "agent", "-p", prompt, "--model", model, "--output-format", "stream-json")
6. bridge → cmd.Stdout pipe → goroutine scanner 逐行讀
7. scanner → 每行送入 outputChan (chan string)
8. converter → 讀 outputChan轉換為 SSEChunk送入 sseChan
9. handler → flush SSE chunk 到 client
10. bridge → process 結束 → close channels → handler 發送 [DONE]
```
## Database Schema
N/A無狀態設計不需要資料庫
## API Contract
### POST /v1/chat/completions
Request:
```json
{
"model": "claude-sonnet-4-20250514",
"messages": [
{"role": "user", "content": "hello"}
],
"stream": true
}
```
Response (SSE when stream: true):
```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
```
Response (JSON when stream: false):
```json
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello!"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
}
```
### GET /v1/models
Response:
```json
{
"object": "list",
"data": [
{"id": "claude-sonnet-4-20250514", "object": "model", "created": 0, "owned_by": "cursor"}
]
}
```
### GET /health
Response:
```json
{"status": "ok", "cursor_cli": "available", "version": "0.1.0"}
```
### Error Codes
| Status | Code | When |
|--------|------|------|
| 400 | invalid_request | messages 為空或格式錯誤 |
| 404 | model_not_found | 指定的 model 不存在 |
| 500 | internal_error | Cursor CLI 子程序崩潰 |
| 504 | timeout | Cursor CLI 超時未回應 |
## Async / Queue Design
N/A不需要 queuegoroutine + channel 直接串接
## Consistency Model
N/A無狀態 proxy每次請求獨立
## Error Model
| Category | Examples | Handling |
|----------|---------|----------|
| Client Error | invalid request, unknown model | 4xx spawn 子程序 |
| CLI Spawn Error | agent not found, not logged in | 500 + stderr message |
| Timeout | model thinking too long | kill subprocess 504 |
| Crash | unexpected exit | 500 + exit code |
## Security Boundaries
N/A本機 personal toolbind 127.0.0.1無認證
## Integration Boundaries
### Cursor CLI
| Property | Value |
|----------|-------|
| Integration Pattern | subprocess (os/exec.CommandContext) |
| Protocol | CLI binary (stdout pipe) |
| Authentication | 本機 `agent login` 狀態 |
| Failure Mode | binary not found / not logged in |
| Data Contract | `--output-format stream-json` |
| Timeout | 可配置預設 300s |
## Observability
- structlog (slog) loggingINFO 請求/完成ERROR 錯誤/timeout
- `/health` endpoint
- DEBUG level 時印出 Cursor CLI 原始 stdout
## Scaling Strategy
N/A個人本機工具單實例semaphore 限制並發子程序數預設 5)。
## Non-Functional Requirements
| NFR | Requirement | Decision | Verification |
|-----|-------------|----------|-------------|
| Performance | overhead < 500ms | goroutine pipeline, streaming pipe | 實際測量 |
| Reliability | 並發 5 | buffered channel semaphore | 併發測試 |
| Usability | 一行啟動 | cobra CLI, sensible defaults | 手動測試 |
| Distribution | 單一 binary | Go cross-compile | `go build` |
## Mermaid Diagrams
### System Architecture
```mermaid
graph LR
CLI[Hermes/OpenCode/Claude] -->|POST /v1/chat/completions| Adapter[Cursor Adapter]
Adapter -->|exec: agent -p ... --output-format stream-json| Cursor[Cursor CLI]
Cursor -->|streaming JSON stdout| Adapter
Adapter -->|SSE streaming| CLI
```
### Sequence Diagram
```mermaid
sequenceDiagram
participant C as CLI Tool
participant H as HTTP Handler
participant B as CLI Bridge
participant A as Cursor CLI
C->>H: POST /v1/chat/completions
H->>H: validate, extract prompt
H->>B: Execute(ctx, prompt, model)
B->>A: exec.CommandContext("agent", "-p", ...)
loop streaming
A-->>B: stdout line (JSON)
B-->>H: outputChan <- line
H->>H: convert to SSE chunk
H-->>C: data: {...}\n\n
end
A-->>B: process exit
B-->>H: close channels
H-->>C: data: [DONE]
```
### Data Flow Diagram
```mermaid
flowchart TD
A[Client Request] --> B{Validate}
B -->|invalid| C[400]
B -->|valid| D[Extract Prompt]
D --> E[exec.CommandContext]
E --> F{spawn OK?}
F -->|no| G[500]
F -->|yes| H[goroutine: scan stdout]
H --> I[outputChan]
I --> J[converter: JSON→SSE]
J --> K[flush to client]
K --> L{more?}
L -->|yes| H
L -->|no| M[send DONE]
```
## ADR
### ADR-001: Go 而非 Python
**Context**: 選擇實作語言候選為 Go Python (FastAPI)。
**Decision**: Go 1.22+。
**Consequences**:
- + 單一 binary不需要使用者裝 Python/pip
- + `os/exec.CommandContext` 子程序管理比 Python `asyncio` 更直覺
- + goroutine + channel 天然適合 streaming pipeline
- + cross-compilemacOS/Linux/Windows 一個 `go build`
- - SSE 手動處理但不複雜
**Alternatives**:
- Python + FastAPI生態好但需要 runtime部署麻煩
- Rust效能最好但開發速度慢
### ADR-002: chi router 而非 stdlib mux
**Context**: Go 1.22 `net/http` 已支援 method-based routing
**Decision**: 使用 chi v5
**Consequences**:
- + middleware 生態好loggerrecoverertimeout
- + route grouping 更乾淨
- + 相容 net/http Handler
- - 多一個 dependency
**Alternatives**:
- stdlib net/http夠用 middleware 要自己寫
- gin太重對這個規模 overkill
### ADR-003: spawn 子程序而非 ACP
**Context**: Cursor CLI 支援 headless print mode ACP (JSON-RPC)。
**Decision**: headless print mode (`agent -p --output-format stream-json`)。
**Consequences**:
- + 實作簡單spawn + stdout
- + 不需要 JSON-RPC
- - 無法做 tool usePRD 不需要
**Alternatives**:
- ACP (JSON-RPC over stdio)功能完整但複雜度高很多
## Risks
| Risk | Impact | Likelihood | Mitigation |
|------|--------|-----------|------------|
| Cursor CLI stream-json 格式變更 | High | Medium | 抽象 converter格式在 const 定義 |
| Cursor CLI 不支援並發實例 | Medium | Low | semaphore + queue |
| 子程序 zombie | Medium | Low | CommandContext + Wait() |
## Open Questions
1. Cursor CLI stream-json 的確切 schema需實際測試
2. Cursor CLI 能否同時跑多個 headless 實例