Skip to content

Commit 8beb390

Browse files
committed
Add workers and opencode docs, commit bun.lock for CI builds
1 parent 30313f4 commit 8beb390

File tree

5 files changed

+1147
-2
lines changed

5 files changed

+1147
-2
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55
# Interface
66
interface/node_modules/
77
interface/dist/
8-
interface/bun.lock
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"title": "Features",
3-
"pages": ["tools", "browser", "cron", "ingestion"]
3+
"pages": ["workers", "opencode", "tools", "browser", "cron", "ingestion"]
44
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
title: OpenCode
3+
description: Spawn full coding agents as worker processes via the OpenCode integration.
4+
---
5+
6+
# OpenCode
7+
8+
Spacebot can spawn [OpenCode](https://opencode.ai) as a worker backend. Instead of running a Rig agent with shell/file/exec tools, an OpenCode worker delegates to a persistent OpenCode subprocess that has its own tool suite, codebase exploration, and context management.
9+
10+
Use OpenCode workers for multi-file coding tasks. Use builtin workers for one-shot commands, file operations, and non-coding work.
11+
12+
## Enabling
13+
14+
OpenCode is disabled by default. Enable it in your config:
15+
16+
```toml
17+
[defaults.opencode]
18+
enabled = true
19+
path = "opencode" # path to the opencode binary
20+
```
21+
22+
The `path` field supports `env:VAR_NAME` resolution:
23+
24+
```toml
25+
path = "env:OPENCODE_PATH"
26+
```
27+
28+
Once enabled, the `spawn_worker` tool gains a `worker_type` parameter. The channel LLM decides whether to use `"builtin"` (default) or `"opencode"` based on the task.
29+
30+
## How It Works
31+
32+
```
33+
Channel: "spawn_worker: refactor the auth module, worker_type: opencode, directory: /code/myapp"
34+
→ Spacebot gets/creates an OpenCode server for /code/myapp
35+
→ Creates an HTTP session
36+
→ Sends the task as a prompt
37+
→ Monitors SSE events for progress
38+
→ Tool calls show up as status updates in the channel
39+
→ Result text is returned as WorkerComplete
40+
```
41+
42+
The OpenCode worker runs its own agent loop internally. Spacebot monitors it via SSE and translates tool events into status updates visible to the channel.
43+
44+
## Server Pool
45+
46+
OpenCode runs as `opencode serve --port <port>` — a persistent HTTP server per working directory. Spacebot manages a pool of these servers.
47+
48+
**Deterministic ports**: Each directory gets a port derived from its path hash (range 10000-60000). The same directory always maps to the same port.
49+
50+
**Server reattach**: After a Spacebot restart, the pool tries to reconnect to existing OpenCode servers via health check on their deterministic port. If the server is still running, it's reused without spawning a new process.
51+
52+
**Pool limits**: Controlled by `max_servers` (default: 5). When the limit is reached, spawning a worker for a new directory fails.
53+
54+
**Auto-restart**: If a server dies, the pool restarts it automatically (up to `max_restart_retries` times, default: 5).
55+
56+
## Communication Protocol
57+
58+
All communication is localhost HTTP:
59+
60+
| Endpoint | Method | Purpose |
61+
|----------|--------|---------|
62+
| `/global/health` | GET | Health check |
63+
| `/session` | POST | Create session |
64+
| `/session/{id}/prompt_async` | POST | Send prompt (non-blocking) |
65+
| `/session/{id}/abort` | POST | Abort session |
66+
| `/event` | GET | SSE event stream |
67+
| `/permission/{id}/reply` | POST | Reply to permission request |
68+
| `/question/{id}/reply` | POST | Reply to question request |
69+
70+
All requests include `?directory=<path>` as a query parameter.
71+
72+
### SSE Events
73+
74+
Spacebot subscribes to the SSE stream and processes:
75+
76+
- **Tool events** — translated to `set_status` updates (e.g. "running: bash", "running: edit")
77+
- **Session idle** — signals task completion
78+
- **Session error** — signals failure
79+
- **Permission asked** — auto-approved (configurable)
80+
- **Question asked** — auto-selects first option
81+
- **Retry status** — reports rate limit retries
82+
83+
## OpenCode vs Builtin Workers
84+
85+
| | Builtin Worker | OpenCode Worker |
86+
|---|---|---|
87+
| **Agent loop** | Rig agent in-process | OpenCode subprocess |
88+
| **Tools** | shell, file, exec, set_status, browser | OpenCode's full tool suite (bash, read, edit, glob, grep, write, webfetch, task) |
89+
| **Context** | Fresh prompt + task | Full OpenCode session with codebase awareness |
90+
| **Model** | Configured via Spacebot routing | Configured via OpenCode or Spacebot override |
91+
| **Best for** | One-shot commands, file reads, non-coding tasks | Multi-file refactors, feature implementation, code exploration |
92+
| **Interactive** | Supported | Supported (same session preserved across follow-ups) |
93+
94+
The channel system prompt includes a decision guide when OpenCode is enabled:
95+
96+
- Need to run a command? Builtin worker.
97+
- Need to read a file? Builtin worker.
98+
- Need to write or modify code across multiple files? OpenCode worker.
99+
100+
## Permissions
101+
102+
OpenCode has its own permission system for dangerous operations. Spacebot controls the defaults:
103+
104+
```toml
105+
[defaults.opencode.permissions]
106+
edit = "allow" # file editing
107+
bash = "allow" # shell commands
108+
webfetch = "allow" # web fetching
109+
```
110+
111+
With all permissions set to `"allow"`, OpenCode suppresses most permission prompts. When a permission prompt does fire, Spacebot auto-approves it and emits a `WorkerPermission` event.
112+
113+
These settings are passed to OpenCode via the `OPENCODE_CONFIG_CONTENT` environment variable. LSP and formatter are disabled for headless operation.
114+
115+
## Interactive Sessions
116+
117+
OpenCode workers support the same interactive pattern as builtin workers:
118+
119+
```
120+
spawn_worker: task="set up the project", worker_type: opencode, interactive: true, directory: /code/myapp
121+
→ OpenCode creates a session, runs initial task
122+
→ Worker enters WaitingForInput
123+
route: worker_id=abc, message="now add the database layer"
124+
→ Follow-up sent to the same OpenCode session
125+
→ Context from the first task is preserved
126+
```
127+
128+
The OpenCode session accumulates context across follow-ups, so subsequent messages benefit from everything the agent learned during earlier work.
129+
130+
## Model Override
131+
132+
You can override the model used by OpenCode workers:
133+
134+
```toml
135+
[routing]
136+
worker = "anthropic/claude-haiku-4.5-20250514"
137+
138+
[routing.task_overrides]
139+
coding = "anthropic/claude-sonnet-4-20250514"
140+
```
141+
142+
When the worker spawns, the routing config determines the model. The model string is split into `provider_id/model_id` and passed to OpenCode's prompt API.
143+
144+
## Full Configuration
145+
146+
```toml
147+
[defaults.opencode]
148+
enabled = true
149+
path = "opencode" # binary path or env:VAR_NAME
150+
max_servers = 5 # max concurrent OpenCode server processes
151+
server_startup_timeout_secs = 30 # how long to wait for server health
152+
max_restart_retries = 5 # auto-restart attempts on server death
153+
154+
[defaults.opencode.permissions]
155+
edit = "allow"
156+
bash = "allow"
157+
webfetch = "allow"
158+
```
159+
160+
## Architecture
161+
162+
```
163+
┌─────────────┐ HTTP/SSE ┌──────────────────┐
164+
│ Spacebot │ ←───────────────→ │ OpenCode Server │
165+
│ (worker.rs) │ │ (port 12345) │
166+
└─────────────┘ └──────────────────┘
167+
│ │
168+
│ ProcessEvent::WorkerStatus │ opencode agent loop
169+
│ ProcessEvent::WorkerComplete │ (bash, edit, read, etc.)
170+
↓ ↓
171+
┌─────────────┐ ┌──────────────────┐
172+
│ Channel │ │ Working Dir │
173+
│ (status │ │ /code/myapp │
174+
│ block) │ └──────────────────┘
175+
└─────────────┘
176+
```
177+
178+
The OpenCode server is a child process managed by Spacebot. It persists across worker invocations for the same directory. Multiple workers targeting the same directory share the same server (different sessions).
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
title: Workers
3+
description: Independent processes that execute tasks with shell, file, and browser tools.
4+
---
5+
6+
# Workers
7+
8+
Workers are independent processes that do a job. They get a specific task, a focused system prompt, and task-appropriate tools. No channel context, no personality, no conversation history.
9+
10+
The channel delegates work to workers so it stays responsive. While a worker runs a shell command or edits files, the channel keeps talking to the user.
11+
12+
## Two Kinds
13+
14+
### Fire-and-forget
15+
16+
Does a job and returns a result. The channel spawns it, gets a `worker_id`, and later receives a `WorkerComplete` event with the result injected into its history.
17+
18+
```
19+
Channel: "spawn_worker: run the test suite"
20+
→ Worker starts, runs tests
21+
→ Worker returns: "12 passed, 0 failed"
22+
→ Channel sees result, replies to user
23+
```
24+
25+
### Interactive
26+
27+
Long-running worker that accepts follow-up input. The channel uses the `route` tool to send additional messages to it. The worker maintains its history across follow-ups.
28+
29+
```
30+
Channel: "spawn_worker: coding session, interactive: true"
31+
→ Worker starts, does initial task
32+
→ Worker enters WaitingForInput state
33+
Channel: "route: now add error handling"
34+
→ Worker continues with accumulated context
35+
Channel: "route: looks good, run the tests"
36+
→ Worker runs tests, returns result
37+
```
38+
39+
Interactive workers stay alive until the input channel is dropped, a follow-up fails, or the channel cancels them.
40+
41+
## Tools
42+
43+
Every worker gets a ToolServer with:
44+
45+
| Tool | Purpose |
46+
|------|---------|
47+
| `shell` | Run shell commands (`sh -c`) with configurable timeout |
48+
| `file` | Read, write, and list files |
49+
| `exec` | Run subprocesses with explicit args and environment |
50+
| `set_status` | Report progress to the channel's status block |
51+
52+
Conditionally added:
53+
54+
| Tool | Condition |
55+
|------|-----------|
56+
| `browser` | When `browser.enabled = true` in agent config |
57+
| `web_search` | When a Brave Search API key is configured |
58+
59+
Workers don't get memory tools, channel tools, or branch tools. They can't talk to the user, recall memories, or spawn other processes. They execute their task and report status.
60+
61+
## State Machine
62+
63+
```
64+
Running ──→ Done (fire-and-forget completed)
65+
Running ──→ Failed (error or cancellation)
66+
Running ──→ WaitingForInput (interactive worker finished initial task)
67+
WaitingForInput ──→ Running (follow-up message received via route)
68+
WaitingForInput ──→ Failed (follow-up processing failed)
69+
```
70+
71+
`Done` and `Failed` are terminal. Illegal transitions are runtime errors.
72+
73+
## Context and History
74+
75+
Workers start with a **fresh empty history**. They have no access to the channel's conversation. Their only context is:
76+
77+
1. The system prompt (from `prompts/en/worker.md.j2`)
78+
2. The task description (first user message)
79+
3. Optional skill instructions (prepended to system prompt)
80+
81+
This isolation is by design. If a process needs conversation context, it's a branch, not a worker.
82+
83+
## Compaction
84+
85+
Workers do inline programmatic compaction (no LLM call):
86+
87+
- **>70% context usage**: Background compaction removes 50% of oldest messages
88+
- **Context overflow**: Force compaction removes 75% of oldest messages (up to 3 retries)
89+
90+
Compacted messages are summarized into a recap that preserves tool call names, arguments, and results. This recap is injected as a system message at the top of history so the worker doesn't repeat completed work.
91+
92+
## Segment Loop
93+
94+
Workers run in segments of 25 turns each. After each segment:
95+
96+
- If the agent returned a result: done
97+
- If max turns hit: compact if needed, continue with "Continue where you left off"
98+
- If cancelled: state = Failed
99+
- If context overflow: force compact, retry
100+
101+
This prevents runaway workers and handles long tasks that exceed a single agent loop.
102+
103+
## Status Reporting
104+
105+
Workers report progress via the `set_status` tool. The status string (max 256 chars) appears in the channel's status block, which is injected into the channel's system prompt every turn.
106+
107+
```
108+
## Active Workers
109+
- [abc123] run test suite (2m, 8 tool calls): running pytest, 7/12 suites done
110+
```
111+
112+
The channel LLM sees this and can decide whether to wait, ask for more info, or cancel.
113+
114+
## Concurrency
115+
116+
Workers run concurrently. The default limit is `max_concurrent_workers: 5` per channel (configurable per agent). Attempting to spawn beyond the limit returns an error to the LLM so it can wait or cancel an existing worker.
117+
118+
## Model Routing
119+
120+
Workers default to `anthropic/claude-haiku-4.5-20250514`. Task-type overrides apply — for example, a `coding` task type routes to `anthropic/claude-sonnet-4-20250514`. Fallback chains are supported. All hot-reloadable.
121+
122+
See [Routing](/docs/routing) for the full routing config.
123+
124+
## Skills
125+
126+
Workers can be spawned with a skill — a set of instructions loaded from `{instance_dir}/skills/` or `{workspace}/skills/`. The skill content is prepended to the worker's system prompt.
127+
128+
```
129+
spawn_worker: task="check the weather for SF", skill="weather"
130+
```
131+
132+
The channel sees a summary of available skills. The worker receives the full skill instructions. See the skills directory for the format.
133+
134+
## Logging
135+
136+
Worker execution logs are controlled by `worker_log_mode`:
137+
138+
| Mode | Behavior |
139+
|------|----------|
140+
| `errors_only` (default) | Only write logs on failure |
141+
| `all_separate` | Write to `logs/successful/` and `logs/failed/` subdirectories |
142+
| `all_combined` | Write all logs to `logs/` |
143+
144+
Logs include: worker ID, channel ID, timestamp, state, task, error (if any), and the full message history with tool calls and results.
145+
146+
## Protected Paths
147+
148+
The `file` and `shell` tools reject operations on protected paths:
149+
150+
- Identity files: `SOUL.md`, `IDENTITY.md`, `USER.md`
151+
- System paths: `/prompts/`, `/data/spacebot.db`, `/data/lancedb/`, `/data/config.redb`
152+
- Archives and ingest directories
153+
154+
This prevents workers from accidentally corrupting system state.
155+
156+
## Configuration
157+
158+
```toml
159+
[defaults]
160+
max_concurrent_workers = 5 # per channel
161+
context_window = 128000 # tokens
162+
163+
[routing]
164+
worker = "anthropic/claude-haiku-4.5-20250514"
165+
166+
[routing.task_overrides]
167+
coding = "anthropic/claude-sonnet-4-20250514"
168+
```
169+
170+
## OpenCode Workers
171+
172+
Workers can also be backed by an OpenCode subprocess instead of the built-in Rig agent. OpenCode workers are full coding agents with their own tool suite, codebase exploration, and context management.
173+
174+
See [OpenCode](/docs/opencode) for details.

0 commit comments

Comments
 (0)