HTTP & WebSocket APIs
Runner HTTP API (local + hybrid)
Every deployment exposes an Express server on the configured port (default 3000). The dashboard static assets mount under /dashboard/.
Core job + health
| Method | Path | Purpose |
|---|---|---|
GET | /health | Liveness |
POST | /jobs | Create job (type, workflowPath, params) |
GET | /jobs | List jobs (filters, pagination — see handler) |
GET | /jobs/:jobId | Full job snapshot |
GET | /jobs/:jobId/stream | SSE log stream |
GET | /jobs/:jobId/artifacts/:artifactId/content | Download artefact bytes |
POST | /intake/stream | Coro plan mode — SSE stream for investigative run intake (see below) |
GET | /intake/sessions | List investigations (summaries). Query: limit (default 5, max 50), offset |
GET | /intake/sessions/:sessionId | Full investigation; rehydrates the runner’s in-memory LLM cache |
PUT | /intake/sessions/:sessionId | Dashboard transcript snapshot (items, readiness, model, counters). Empty chats are not inserted. |
DELETE | /intake/sessions/:sessionId | Discard a plan-mode conversation (in-memory cache and durable row) |
POST | /jobs/:jobId/resume | Manual / synthetic resume |
POST | /jobs/:jobId/cancel | Cooperative cancel |
POST | /jobs/:jobId/pause | Pause scheduling |
POST | /jobs/:jobId/message | Mid-flight developer message |
Campaign child controls
| Method | Path |
|---|---|
POST | /jobs/:jobId/children/:name/skip |
POST | /jobs/:jobId/children/:name/rerun |
POST | /jobs/:jobId/children/:name/cancel |
POST | /jobs/:jobId/children/:name/abandon |
POST | /jobs/:jobId/children/:name/resume |
POST | /jobs/:jobId/children/:name/start |
Intelligence editing (dashboard)
| Method | Path |
|---|---|
GET/PUT/DELETE | /intelligence/file |
GET | /intelligence/layers |
POST | /intelligence/preflight |
Plugins + diagnostics
| Method | Path |
|---|---|
GET | /plugins |
POST | /plugins/install |
DELETE | /plugins/:id |
GET | /plugins/:id/models |
POST | /plugins/:id/healthcheck |
POST | /test/plugin/:id |
POST | /config/plugins/:id/auth/detect |
POST | /config/plugins/:id/auth/detect/apply |
Config + proposals
| Method | Path |
|---|---|
GET/PUT | /config |
GET | /config/claude-code-mcps |
GET | /proposals |
GET | /jobs/:jobId/insights |
DELETE | /jobs/:jobId/insights/:insightId |
POST | /jobs/:jobId/phases/:phase/rerun |
Static UI
| Method | Path |
|---|---|
GET | /dashboard/* |
GET | / |
POST /intake/stream (Coro plan mode)
Server-Sent Events endpoint used by the New Run chat. Requires a healthy LLM executor plugin; returns 503 with reason: no-llm when plugins are not initialised.
The conversation is durable runner state keyed by sessionId. The dashboard posts the new message plus an optional transcript fallback. Successful turns are written to the investigations table; the UI transcript is saved with PUT /intake/sessions/:id.
Request body:
{ "sessionId": "uuid-or-stable-browser-id", "message": "Add rate limiting to /api/users", "transcript": [{ "role": "user", "content": "earlier turn" }], "model": "claude-sonnet-4-6", "provider": "anthropic", "context": { "recentRepos": ["org/api-service"], "recentReviewers": ["alice"], "availableWorkflows": [ { "id": "job", "name": "Implementation Job", "workflowPath": "workflows/job/workflow.md", "description": "Default lane for scoped feature work…" } ], "userLocale": "en" }}message is the new developer turn. transcript is the client’s copy of the earlier turns. It seeds a session the runner does not have (restart mid-investigation) and fills in turns the server never recorded (rate-limit, empty output, abort). It is a fallback, not the store — investigations live in SQLite (local) or Postgres (hybrid). A body that omits message but supplies a full messages array is still accepted: the last user entry becomes the message and the rest seeds the session.
model and provider are optional per-session overrides from the dashboard model picker. When omitted, the handler resolves the planning tier via settings.llm.aliases.
SSE events (JSON payloads on message frames):
type | Meaning |
|---|---|
token | { "text": "…" } assistant delta |
thinking | { "text": "…" } model reasoning (same live feed as a running job) |
tool_start / tool_end | Read-only tracker/SCM lookups while intake.toolsEnabled is on |
done | Turn complete; may include usage, plus contextTokens, sessionTokens, and turns |
error | { "message": "…", "reason?": "…" } provider failure |
The handler calls the executor’s optional chat() method. Continuity uses the same dual-shape session state as a job: Claude Code persists a sessionId and resumes it; OpenAI replays conversationHistory. messages (with tool <evidence>) is the fallback after a restart or provider switch. When tools are enabled, chat() runs a read-only tool-use loop bounded at 25 rounds per turn.
No session budgets. Turn and token caps were removed: every turn is developer-initiated, so there is no autonomous loop to bound, and a cap would end an investigation mid-way with nothing to dispatch. The counters on the done frame are for display only. The per-turn tool-round ceiling remains, since that is the only unattended spend.
Each assistant turn ends with a <readiness>{ "state": "investigating" | "ready" | "no-run-needed", "openQuestions": [], "note": "" }</readiness> block. A markdown <findings>…</findings> write-up is parsed into a Findings card and stored on the investigation as findings; the <run>{…}</run> payload appears only when the developer requests it or readiness is ready. All three are parsed client-side and hidden from the visible transcript.
Plan-mode streams intentionally do not abort when the HTTP request emits close immediately after body parsing (Express 4 + Node 20 quirk) — the LLM call runs to completion.
GET / PUT / DELETE /intake/sessions
Investigations are listed as summaries (GET /intake/sessions?limit=5&offset=0 returns { sessions, total, limit, offset }). GET /intake/sessions/:sessionId returns the full record (items, turns, resume blob) and rehydrates the in-memory LLM cache so the next chat turn can continue. PUT stores the dashboard transcript (items, readiness, findings — the current Findings card markdown — model choice, counters); the runner fills turns from the live session when it is present. Empty chats are never inserted.
DELETE drops both the hot cache and the durable row. The dashboard does not call it on New conversation or dispatch — those persist the current investigation and start a fresh sessionId.
See Coro plan mode for UX and troubleshooting.
Cloud control plane (hybrid mode)
When cloud.url + cloud.token are configured, a separate cloud service (packages/runner/src/cloud) fronts team operations:
| Surface | Purpose |
|---|---|
GET /health | Liveness |
/auth/* | OAuth / JWT for dashboard users |
/teams/* | Team CRUD |
/teams/:teamId/jobs/* | Remote job APIs (fan-out to connected runners) |
/teams/:teamId/proposals/* | Proposal review |
POST /webhook/:teamId/:pluginId | HMAC-verified ingress; forwards { pluginId, headers, rawBody } to runners |
GET /teams/:teamId/runners | Connection inventory |
WS /ws/runner | Authenticated channel for dispatch, state sync, forwarded webhooks |
Plugin manifests contribute webhook.algorithm, header, and format so the cloud verifier stays provider-agnostic.
Local vs hybrid event delivery
- Local mode:
PollingTransportpolls SCM for PR approvals/comments and injects synthetic events. - Hybrid mode: Real provider webhooks hit the cloud edge, verify HMAC, then flow across the runner WebSocket to parked jobs.
Full narrative + sequence diagrams live in docs/agent-host-spec.md within the Coro developer framework repository.