diff options
| -rw-r--r-- | README.md | 271 |
1 files changed, 194 insertions, 77 deletions
@@ -8,6 +8,7 @@ in a separate repo and talks to the same typed contracts over HTTP + a surface W the Vercel AI SDK for providers. - **Architecture:** `kernel → core extensions → standard extensions`. The kernel touches no I/O and names no concrete feature; effects live in extensions, injected through typed contracts. +- **Tests:** 1453 vitest + bun:sqlite integration tests, zero internal mocks on pure-core packages. --- @@ -25,25 +26,21 @@ bun install --- -## Deploy the server +## Quick start (dev) -1. **Create a `.env`** in the repo root (it is gitignored): +1. **Create a `.env`** in the repo root (gitignored; see `.env.example`): ```sh DISPATCH_API_KEY=sk-... # your OpenAI-compatible API key (the secret) DISPATCH_BASE_URL=https://opencode.ai/zen/go/v1 # the provider base URL DISPATCH_MODEL=deepseek-v4-flash # default model when a request omits one - BACKEND_PORT=24203 # port the HTTP server listens on - FRONTEND_PORT=24204 # reserved for the future web UI + BACKEND_PORT=24203 # HTTP server port (dev default) + SURFACE_WS_PORT=24205 # surface WebSocket port (dev default) # Optional — Umans AI Coding Plan provider (https://code.umans.ai) UMANS_API_KEY=sk-... # if set, the "umans" provider is registered - # UMANS_BASE_URL=https://api.code.umans.ai/v1 # override the default base URL - # UMANS_MODEL=umans-coder # default model (umans-coder|umans-kimi-k2.7|umans-glm-5.2|umans-flash) ``` - Bun auto-loads `.env`. (If your shell also needs the vars: `set -a; source .env; set +a`.) - 2. **Boot the server:** ```sh @@ -51,16 +48,11 @@ bun install ``` It loads config, activates every extension through the host, and serves HTTP on - `BACKEND_PORT`. It also spawns and supervises an out-of-process **observability collector** - (restart-on-crash, drain-on-shutdown) and writes a structured journal to `.dispatch/journal/` - plus a trace database. A collector failure never crashes the server. - - ``` - Dispatch listening on http://localhost:24203 - ``` - - It also serves a **surface WebSocket** on `:24205` (the `transport-ws` extension) — the channel - the web frontend uses to discover and render backend-declared *surfaces*. + `BACKEND_PORT` (default 24203). It also spawns and supervises an out-of-process + **observability collector** (restart-on-crash, drain-on-shutdown) and writes a structured + journal to `.dispatch/journal/` plus a trace database. A collector failure never crashes the + server. A **surface WebSocket** on `SURFACE_WS_PORT` (default 24205) carries live updates + to connected frontends. 3. **Smoke-test it:** @@ -71,15 +63,87 @@ bun install # one turn (NDJSON stream of events back); X-Conversation-Id header threads multi-turn curl -s -X POST localhost:24203/chat \ -H 'content-type: application/json' \ - -d '{"model":"opencode/deepseek-v4-flash","message":"Say hello in 3 words."}' + -d '{"conversationId":"c1","message":"Say hello in 3 words."}' ``` -### HTTP API (for any client) +--- + +## Deploy as a systemd service (Arch Linux) + +`bin/install` builds the binaries + frontend, installs them system-wide, and sets up a +systemd service. + +```sh +sudo bin/install # build + install + enable + start +sudo bin/install --no-build # install only (skip the build step) +sudo bin/install --uninstall # stop + disable + remove files (keeps config + data) +``` + +**What it installs:** + +| Path | Description | +|---|---| +| `/usr/bin/dispatch-server` | Standalone backend binary (Bun compile) | +| `/usr/bin/dispatch` | Standalone CLI binary (Bun compile) | +| `/usr/share/dispatch/web/` | Built frontend static files | +| `/etc/dispatch/env` | Server config (systemd EnvironmentFile) | +| `/etc/systemd/system/dispatch.service` | systemd unit | +| `/var/lib/dispatch/` | Data directory (SQLite DBs) | +| `/var/log/dispatch/` | Journal + trace logs | + +The production config (`systemd/dispatch.env`) uses ports **24991** (HTTP) and **24990** +(surface WS), distinct from the dev defaults (24203/24205). After install: + +```sh +systemctl status dispatch +journalctl -u dispatch -f # live logs +curl -s localhost:24991/health # → {"ok":true} +``` + +`bin/sync-env` updates the API keys in `/etc/dispatch/env` without touching the ports. +`bin/setup-env` is the interactive first-time setup (prompts for keys, writes the env file). + +--- + +## HTTP API | Method & path | Body / params | Returns | |---|---|---| -| `GET /models` | — | `{ "models": ["opencode/<model>", ...] }` — the model catalog | -| `POST /chat` | `{ conversationId?, message, model?, cwd? }` | NDJSON stream of `AgentEvent`s; resolved id in the `X-Conversation-Id` header | +| `GET /health` | — | `{ "ok": true }` | +| `GET /models` | — | `{ "models": ["opencode/<model>", ...] }` — the catalog | +| `POST /chat` | `{ conversationId?, message, model?, cwd?, reasoningEffort? }` | NDJSON stream of `AgentEvent`s; resolved id in the `X-Conversation-Id` header | +| `POST /chat/warm` | `{ conversationId, model?, cwd? }` | Cache-warming result (tokens + cache %) | +| `GET /conversations` | `?q=<prefix>&status=<active|idle|closed>&workspaceId=<id>` | Conversation list | +| `GET /conversations/:id` | `?sinceSeq=<n>&beforeSeq=<n>&limit=<n>` | Conversation history (chunk log) | +| `GET /conversations/:id/status` | — | `{ conversationId, isActive, status }` | +| `GET /conversations/:id/last` | — | Last assistant message (blocks until turn settles) | +| `GET /conversations/:id/metrics` | — | Per-turn + per-step token/timing metrics | +| `GET /conversations/:id/cwd` | — | Persisted working directory | +| `PUT /conversations/:id/cwd` | `{ cwd, workspaceId? }` | Set working directory | +| `DELETE /conversations/:id/cwd` | — | Clear working directory | +| `GET /conversations/:id/model` | — | Persisted model selection | +| `PUT /conversations/:id/model` | `{ model }` | Set model selection | +| `GET /conversations/:id/reasoning-effort` | — | Persisted reasoning effort | +| `PUT /conversations/:id/reasoning-effort` | `{ reasoningEffort }` | Set reasoning effort | +| `GET /conversations/:id/lsp` | — | LSP server status for the conversation's cwd | +| `POST /conversations/:id/queue` | `{ message }` | Enqueue a steering message | +| `POST /conversations/:id/stop` | — | Stop generation (aborts in-flight turn) | +| `POST /conversations/:id/close` | — | Close conversation (aborts turn, marks closed) | +| `POST /conversations/:id/open` | — | Signal frontend to open a tab | +| `PUT /conversations/:id/title` | `{ title }` | Set conversation title | +| `POST /conversations/:id/compact` | — | Compact history (non-destructive fork + summary) | +| `GET /conversations/:id/compact-percent` | — | Auto-compaction threshold | +| `PUT /conversations/:id/compact-percent` | `{ percent }` | Set auto-compaction threshold | +| `GET /workspaces` | — | Workspace list | +| `PUT /workspaces/:id` | `{ title?, defaultCwd? }` | Create/update workspace | +| `GET /workspaces/:id` | — | Workspace detail | +| `PUT /workspaces/:id/title` | `{ title }` | Rename workspace | +| `PUT /workspaces/:id/default-cwd` | `{ defaultCwd }` | Set workspace default cwd | +| `DELETE /workspaces/:id` | — | Delete workspace (closes conversations, reassigns to default) | +| `GET /system-prompt` | — | Current system prompt template | +| `PUT /system-prompt` | `{ template }` | Set system prompt template | +| `GET /system-prompt/variables` | — | Available template variables | +| `GET /metrics/throughput` | — | Aggregate throughput metrics | The request/response shapes are the `@dispatch/transport-contract` package — import it to build any new frontend. @@ -89,30 +153,33 @@ any new frontend. ## Use the CLI The CLI (`packages/cli`) is a one-shot HTTP client of the server above, so **the server must be -running** (the CLI reads `BACKEND_PORT`, or pass `--server <url>`). Run it via: +running** (the CLI reads `BACKEND_PORT`, or pass `--server <url>`). -```sh -bun run dispatch -- <args> -# or directly: -bun packages/cli/src/main.ts <args> ``` - -### Commands - -``` -dispatch models [--server <url>] -dispatch <modelName> --text "..." [--file <path>] [--cwd <dir>] [--conversation <id>] [--server <url>] [--show-reasoning] -dispatch --help +Usage: + dispatch models [--server <url>] + dispatch list [<prefix>] [--status <active|idle|closed>] [--all] [--server <url>] + dispatch stop <conversationId> [--server <url>] + dispatch compact <conversationId> [--server <url>] + dispatch read <conversationId> [--server <url>] + dispatch open <conversationId> [--server <url>] + dispatch send <conversationId> --text "..." [--queue] [--open] [--cwd <dir>] [--effort <level>] [--workspace <id>] [--server <url>] + dispatch <modelName> --text "..." [--file <path>] [--cwd <dir>] [--conversation <id>] [--effort <level>] [--workspace <id>] [--server <url>] [--show-reasoning] [--open] + dispatch --help + +Effort levels: low, medium, high (default), xhigh, max ``` - **`<modelName>`** is a **model name** in `<credentialName>/<model>` form — exactly a line from `dispatch models` (e.g. `opencode/deepseek-v4-flash`). -- **`--text`** and/or **`--file`** supply the message (at least one is required); `--file` folds - the file's contents into the message. -- **`--cwd`** sets the working directory for tools this turn (defaults to the current directory). -- **`--conversation <id>`** continues a prior conversation; each turn prints its id so you can - pass it back. -- **`--show-reasoning`** also prints the model's reasoning stream (hidden by default). +- **`send <id> --text "..."`** sends a message to an existing conversation (`--queue` for + non-blocking enqueue, `--open` to signal the frontend to open a tab). +- **`read <id>`** blocks until the turn settles, then prints the last assistant message. +- **`list`** shows conversations (short ID + title + activity); `--status` filters, `--all` + includes closed. +- **`compact <id>`** manually compacts a conversation's history. +- **`--effort`** sets reasoning effort for the turn (low|medium|high|xhigh|max; default high). +- **`--workspace <id>`** scopes the conversation to a workspace. ### Examples @@ -126,40 +193,38 @@ bun run dispatch -- opencode/deepseek-v4-flash --text "Say hello in 3 words." # let the model read a file in a given directory (uses the read_file tool, contained to --cwd) bun run dispatch -- opencode/deepseek-v4-flash --cwd ./src --text "Read main.ts and summarize it." -# attach a file's contents to your message -bun run dispatch -- opencode/deepseek-v4-flash --file notes.md --text "Summarize this." - # continue a conversation (id is printed after each turn) bun run dispatch -- opencode/deepseek-v4-flash --conversation <id> --text "and in French?" + +# list conversations, read the last reply, send a queued message +bun run dispatch -- list +bun run dispatch -- read <id> +bun run dispatch -- send <id> --text "follow up" --queue --open ``` --- ## Web frontend (dispatch-web) -The web UI is a **separate repo** at `../dispatch-web` (Svelte 5 + Vite), built to the same -methodology and consuming the backend's typed contracts. As of slice 1 it renders the backend's -**surface system** (e.g. the live "Loaded Extensions" surface); chat UI is a later slice. +The web UI is a **separate repo** at `../dispatch-web` (Svelte 5 + Vite + DaisyUI), built to the +same methodology and consuming the backend's typed contracts (`@dispatch/wire`, +`@dispatch/transport-contract`, `@dispatch/ui-contract`). The browser chat MVP is in progress — it +streams turns over the chat WebSocket, renders the surface system (loaded extensions, cache-warming +controls, message queue, todo list), and supports conversation lifecycle, workspaces, LSP status, +and per-conversation settings. -It needs **this server running** — it connects to the surface WebSocket on `:24205`. To run both: +**Run both at once with live reload:** ```sh -# terminal 1 — backend (this repo) -bun run dev # HTTP :24203 + surface WS :24205 - -# terminal 2 — frontend (sibling repo) -cd ../dispatch-web -bun install # links @dispatch/ui-contract via a file: dep to this repo -bun run dev # Vite dev server on http://localhost:24204 +cd /home/tradam/projects/dispatch +bin/up # backend (bun --watch :24203 + WS :24205) + frontend (vite HMR :24204) ``` -Then open **http://localhost:24204**. See `../dispatch-web/README.md` for full setup, including -visiting over a LAN / Tailscale. +`bin/up2` starts a second, stable stack on ports 25203/25205/25204 with isolated data — runs +alongside `bin/up` without interference. Both Ctrl-C cleanly (including the collector child). -**Or run both at once with live reload:** `bin/up` (also `bun run dev:all`) starts the backend -(`bun --watch`) and the frontend (Vite HMR) together and **Ctrl-C stops both** — including the -backend's observability collector. (The backend reloads via a full process restart; the frontend -hot-reloads in place.) +Then open **http://localhost:24204** (or your Tailscale hostname). See `../dispatch-web/README.md` +for full setup. --- @@ -170,20 +235,45 @@ hot-reloads in place.) The **Depends on** column is each extension's manifest `dependsOn` (other extensions, resolved topologically at activation). Every extension also depends implicitly on the kernel ABI. -| Package | Tier | Description | Depends on | -|---|---|---|---| -| **kernel** | kernel | The minimal runtime core — contracts (the ABI), the extension host, the turn loop (`runTurn`), and the event/hook/service bus; touches no I/O and names no concrete feature. | — | -| **storage-sqlite** | core | Concrete `bun:sqlite` backend behind the kernel's storage interface (a host bootstrap dependency; its `activate` is an intentional no-op). | — | -| **auth-apikey** | core | Resolves an API key (the secret) from the environment into `ApiKeyCredentials` for a provider to consume. | — | -| **credential-store** | core | Owns named **credentials** and the **model catalog** — resolves a `<credential>/<model>` model name to a provider + model and aggregates `GET /models`. | — | -| **provider-openai-compat** | core | Wraps an OpenAI-compatible LLM backend (streaming chat + `listModels`); the OpenCode Go path, holding opencode-go specifics for now. | auth-apikey | -| **conversation-store** | core | Append-only persistence of the turn/chunk log, with a pure `reconcile` that repairs any interrupted turn on load. | — | -| **session-orchestrator** | core | Drives one turn end-to-end: load history → resolve provider/model/tools → call `runTurn` → persist. | conversation-store, credential-store | -| **transport-http** | core | Hono HTTP transport exposing `POST /chat` (NDJSON event stream) and `GET /models` (the catalog). | credential-store, session-orchestrator | -| **tool-read-file** | standard | A `read_file` tool with offset/limit pagination and two-layer workdir containment, honoring the per-turn `cwd`. | — | -| **surface-registry** | standard | In-process registry where extensions contribute UI **surfaces** (frontend-agnostic data); exposes a typed `surfaceRegistryHandle` service. | — | -| **transport-ws** | standard | WebSocket transport (`:24205`) serving the surface catalog + per-surface subscribe / update / invoke to clients. | surface-registry | -| **surface-loaded-extensions** | standard | Contributes the live "Loaded Extensions" surface (a `stat` per activated extension) — the first real surface. | surface-registry | +#### Kernel (not an extension) + +| Package | Description | +|---|---| +| **kernel** | The minimal runtime core — contracts (the ABI), the extension host, the turn loop (`runTurn`), and the event/hook/service bus; touches no I/O and names no concrete feature. | + +#### Core extensions (minimum to complete one turn end-to-end) + +| Package | Description | Depends on | +|---|---|---| +| **storage-sqlite** | Concrete `bun:sqlite` backend behind the kernel's storage interface (a host bootstrap dependency; its `activate` is an intentional no-op). | — | +| **auth-apikey** | Resolves an API key (the secret) from the environment into `ApiKeyCredentials` for a provider to consume. | — | +| **credential-store** | Owns named **credentials** and the **model catalog** — resolves a `<credential>/<model>` model name to a provider + model and aggregates `GET /models`. | — | +| **provider-openai-compat** | Wraps an OpenAI-compatible LLM backend (streaming chat + `listModels`); the OpenCode Go path. | auth-apikey | +| **conversation-store** | Append-only persistence of the turn/chunk log, with a pure `reconcile` that repairs any interrupted turn on load. | — | +| **session-orchestrator** | Drives one turn end-to-end: load history → resolve provider/model/tools → call `runTurn` → persist. | conversation-store, credential-store | +| **transport-http** | Hono HTTP transport exposing `POST /chat` (NDJSON event stream), `GET /models`, and the full conversation/workspace/LSP/system-prompt API. | session-orchestrator | + +#### Standard extensions (shipped on-by-default) + +| Package | Description | Depends on | +|---|---|---| +| **tool-read-file** | `read_file` tool with offset/limit pagination, directory listing, and workdir containment. | — | +| **tool-shell** | `run_shell` tool — foreground, streamed output, process-group kill on abort/timeout. | — | +| **tool-edit-file** | `edit_file` tool — exact-string replacement, `replaceAll` flag, workdir-contained. | — | +| **tool-write-file** | `write_file` tool — explicit `overwrite` flag, no parent auto-create. | — | +| **tool-web-search** | `web_search` tool — Firecrawl-backed (search, scrape, crawl, map). | — | +| **tool-youtube-transcript** | `youtube_transcript` tool — fetches transcripts from a transcriber service. | — | +| **todo** | `todo_write` tool — per-conversation task list with a surface. | surface-registry | +| **skills** | `load_skill` tool + per-turn tools filter that rewrites the skill list per cwd. | session-orchestrator | +| **system-prompt** | Template-based system prompt builder with variable placeholders (`[type:name]`) and conditionals. | — | +| **cache-warming** | Per-conversation prompt-cache warming timers + manual trigger (`POST /chat/warm`) + a surface. | session-orchestrator, surface-registry | +| **message-queue** | Per-conversation steering queue + surface; enqueue when idle starts a new turn. | surface-registry | +| **lsp** | Language Server Protocol client (hand-rolled JSON-RPC over stdio) — lazy-spawn, per-cwd config, diagnostics, `lsp` tool. | — | +| **provider-umans** | Umans OpenAI-compatible provider (`api.code.umans.ai`); self-contained (reads `UMANS_API_KEY` from env). | — | +| **surface-registry** | In-process registry where extensions contribute UI **surfaces** (frontend-agnostic data); exposes a typed `surfaceRegistryHandle` service. | — | +| **transport-ws** | WebSocket transport serving the surface catalog + per-surface subscribe / update / invoke to clients. | surface-registry | +| **surface-loaded-extensions** | Contributes the live "Loaded Extensions" surface (a `stat` per activated extension). | surface-registry | +| **throughput-store** | Aggregate throughput metrics storage + `GET /metrics/throughput`. | — | ### Supporting packages (not extensions) @@ -191,13 +281,15 @@ The **Depends on** column is each package's `@dispatch/*` workspace dependencies | Package | Description | Depends on | |---|---|---| -| **transport-contract** | Types-only description of the HTTP API (`ChatRequest`, `ModelsResponse`, `AgentEvent`) shared by the server and every client. | kernel | -| **ui-contract** | Types-only, **frontend-agnostic** vocabulary for backend-declared **surfaces** (`SurfaceSpec`, field kinds, the surface WS protocol) — shared by the backend and any client (web, CLI). | — | +| **wire** | Types-only wire ABI (`AgentEvent` + conversation model + `Usage`); kernel + transport-contract re-export it so clients consume the wire without the kernel runtime. | — | +| **transport-contract** | Types-only description of the full HTTP API shared by the server and every client. | wire, kernel | +| **ui-contract** | Types-only, **frontend-agnostic** vocabulary for backend-declared **surfaces** (`SurfaceSpec`, field kinds, the surface WS protocol). | — | +| **openai-stream** | Generic OpenAI-compatible stream/convert/listModels library extracted from `provider-openai-compat` (shared by `provider-umans`). | wire | | **cli** | The bundled one-shot terminal client documented above. | transport-contract | | **host-bin** | The composition root: loads config, activates all extensions through the host, serves HTTP, and supervises the observability collector. | kernel, all extensions, journal-sink | | **journal-sink** | Bootstrap `LogSink` that appends structured logs/spans to an NDJSON journal (rotation, fail-safe). | kernel | | **observability-collector** | Out-of-process binary that tails the journal and inserts records into the trace store (idempotent, at-least-once). | kernel, trace-store | -| **trace-store** | `bun:sqlite` store for trace records/bodies, plus a `trace` CLI to render a turn's timeline. | kernel | +| **trace-store** | `bun:sqlite` store for trace records/bodies (content-addressed dedup + retention), plus a `trace` CLI. | kernel | | **trace-replay** | Generic HTTP-exchange record/replay library for hermetic, network-free provider tests. | — | --- @@ -208,7 +300,26 @@ The **Depends on** column is each package's `@dispatch/*` workspace dependencies bun run typecheck # tsc -b --pretty bun run test # vitest (pure/unit + integration) bun run test:bun # bun:sqlite-backed tests +bun run test:all # both test suites bun run check # biome (lint + format) +bun run check:fix # biome --write (auto-fix) +``` + +### Dev stacks + +| Script | Backend | Frontend | Notes | +|---|---|---|---| +| `bin/up` | `:24203` + WS `:24205` (`bun --watch`) | `:24204` (vite HMR) | Full restart on backend change; loads `../claude` as external extensions | +| `bin/up2` | `:25203` + WS `:25205` (no watch) | `:25204` (vite preview) | Stable second stack; isolated data; runs alongside `bin/up` | + +### Workspace layout + +``` +/home/tradam/projects/dispatch/ +├── dispatch-backend/ this repo (branch dev) +├── dispatch-web/ separate repo — web frontend (Svelte + DaisyUI) +├── claude/ separate repo — Claude provider-anthropic extension +└── bin/ shared dev scripts (up, up2) ``` --- @@ -220,3 +331,9 @@ bun run check # biome (lint + format) - **Orchestration workflow:** `ORCHESTRATOR.md` - **Canonical vocabulary:** `GLOSSARY.md` - **Live status / task log:** `tasks.md` +- **Observability design:** `notes/observability-design.md` +- **LSP design:** `notes/lsp-design.md` +- **System prompt design:** `notes/system-prompt-design.md` +- **Turn continuity design:** `notes/turn-continuity-design.md` +- **CLI design:** `notes/cli-design.md` +- **Frontend design:** `notes/frontend-design.md` |
