mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
110d44b07e
`man` and `math` duplicated capabilities already reachable through `bash`; `plan_agent` is better expressed as a `task_agent` running a planning skill, and carried a large amount of special-case machinery (plan-review gate, refinement loop, per-kind model routing). Removing all three shrinks the tool surface and cuts per-call token cost. Also removed, as dead-once-the-tools-are-gone: - the `math` sandbox executor (`turnstone.core.sandbox`) and its `[sandbox]` extra; the eval analyst now runs bash-only - the read-only `AGENT_TOOLS` sub-agent tool set and the `agent` tool-metadata key (`task_agent`/`TASK_AGENT_TOOLS` retained) - the plan-review protocol end to end: the `on_plan_review` UI hook, `resolve_plan`, `POST /v1/api/plan` + `POST /v1/api/route/plan`, the `plan_review`/`plan_resolved` SSE events, and their Python SDK / TypeScript SDK / OpenAPI / frontend / Discord+Slack bindings - the `model.plan_alias` / `model.plan_effort` settings and the registry `plan_model` / `plan_effort` routing fields TOOLS 31->28, TASK_AGENT_TOOLS 13->11; COORDINATOR_TOOLS unchanged. BREAKING CHANGE: removes the `man`, `math`, `plan_agent` tools, the plan-review SSE/HTTP/SDK surface, and the plan_* model-routing settings from the experimental 1.6 line.
2203 lines
67 KiB
Markdown
2203 lines
67 KiB
Markdown
# turnstone Web Server API Reference
|
||
|
||
## Overview
|
||
|
||
`turnstone-server` exposes a browser-based chat UI backed by a
|
||
**Starlette** ASGI application served by **uvicorn**. The server uses
|
||
**Server-Sent Events (SSE)** via `sse-starlette` for real-time streaming
|
||
and **HTTP POST** for user actions.
|
||
|
||
All API responses use `Content-Type: application/json` unless otherwise noted.
|
||
CORS headers (`Access-Control-Allow-Origin: *`) are included on every response.
|
||
|
||
The server supports multiple concurrent **workstreams** (tabs), each backed by
|
||
an independent `ChatSession` and event queue.
|
||
|
||
---
|
||
|
||
## API Versioning
|
||
|
||
All API endpoints use the `/v1/` prefix. Non-API endpoints (`/`, `/health`, `/metrics`, `/openapi.json`, `/docs`, `/static/*`, `/shared/*`) are unversioned.
|
||
|
||
### Interactive Documentation
|
||
|
||
- **OpenAPI spec**: `GET /openapi.json` — machine-readable OpenAPI 3.1 schema
|
||
- **Swagger UI**: `GET /docs` — interactive API explorer (loads from CDN)
|
||
|
||
### Client SDKs
|
||
|
||
Typed client libraries for programmatic access to both the server and console APIs.
|
||
|
||
**Python** (included in the `turnstone` package):
|
||
|
||
```python
|
||
from turnstone.sdk import TurnstoneServer
|
||
|
||
with TurnstoneServer("http://localhost:8080", token="tok_xxx") as client:
|
||
ws = client.create_workstream(name="demo")
|
||
result = client.send_and_wait("Hello!", ws.ws_id)
|
||
print(result.content)
|
||
```
|
||
|
||
Async variant: `AsyncTurnstoneServer` / `AsyncTurnstoneConsole`.
|
||
|
||
**TypeScript** (`sdk/typescript/`):
|
||
|
||
```typescript
|
||
import { TurnstoneServer } from "@turnstone/sdk";
|
||
|
||
const client = new TurnstoneServer({ baseUrl: "http://localhost:8080", token: "tok_xxx" });
|
||
const ws = await client.createWorkstream({ name: "demo" });
|
||
const result = await client.sendAndWait("Hello!", ws.ws_id);
|
||
console.log(result.content);
|
||
```
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
Auth is always enabled. All API endpoints except public paths require a valid token.
|
||
|
||
### Sending Credentials
|
||
|
||
Include a token in one of two ways:
|
||
|
||
- **Bearer header**: `Authorization: Bearer <token>`
|
||
- **Cookie**: `turnstone_auth=<token>` (set automatically by the login endpoint)
|
||
|
||
The server accepts two token types:
|
||
|
||
| Type | Format | Example |
|
||
|------|--------|---------|
|
||
| JWT | Base64 segments separated by dots | `eyJhbG...` |
|
||
| API token | `ts_` prefix + 64 hex chars | `ts_a1b2c3d4...` |
|
||
|
||
JWTs are the recommended credential for browser sessions. API tokens are suitable for programmatic access and CI/CD.
|
||
|
||
### `POST /v1/api/auth/login`
|
||
|
||
Authenticate with credentials and receive a JWT. Accepts two credential formats:
|
||
|
||
**Username + password:**
|
||
|
||
```json
|
||
{"username": "alice", "password": "hunter2"}
|
||
```
|
||
|
||
**API token:**
|
||
|
||
```json
|
||
{"token": "ts_a1b2c3d4e5f6..."}
|
||
```
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"role": "full",
|
||
"scopes": "approve,read,write",
|
||
"jwt": "eyJhbGciOiJIUzI1NiIs...",
|
||
"user_id": "u_abc123"
|
||
}
|
||
```
|
||
|
||
The response also sets a `turnstone_auth` HttpOnly cookie containing the JWT.
|
||
|
||
**Response (failure):** `401`
|
||
|
||
```json
|
||
{"error": "Invalid credentials"}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/auth/logout`
|
||
|
||
Clears the `turnstone_auth` cookie. No request body required.
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
The response includes a `Set-Cookie` header that expires the auth cookie.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/auth/status`
|
||
|
||
Returns the current authentication state. Works with or without a valid token.
|
||
|
||
**Response (authenticated):** `200`
|
||
|
||
```json
|
||
{
|
||
"authenticated": true,
|
||
"user_id": "u_abc123",
|
||
"scopes": ["approve", "read", "write"],
|
||
"source": "jwt"
|
||
}
|
||
```
|
||
|
||
**Response (not authenticated):** `200`
|
||
|
||
```json
|
||
{
|
||
"authenticated": false,
|
||
"user_id": null,
|
||
"scopes": [],
|
||
"source": null
|
||
}
|
||
```
|
||
|
||
**Response (auth disabled):** `200`
|
||
|
||
```json
|
||
{
|
||
"authenticated": false,
|
||
"auth_enabled": false
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/auth/setup`
|
||
|
||
Creates the first admin user when no users exist in the database. This is a
|
||
public endpoint (no authentication required) that only succeeds when auth is
|
||
enabled and the user database is empty. Both the server and console expose
|
||
this endpoint.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"username": "admin",
|
||
"display_name": "Admin",
|
||
"password": "strongpass"
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Validation |
|
||
|----------------|--------|----------|-----------------------------|
|
||
| `username` | string | yes | 1-64 ASCII characters |
|
||
| `display_name` | string | yes | Non-empty |
|
||
| `password` | string | yes | Minimum 8 characters |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"user_id": "u_abc123",
|
||
"username": "admin",
|
||
"role": "full",
|
||
"scopes": "approve,read,write",
|
||
"jwt": "eyJhbGciOiJIUzI1NiIs..."
|
||
}
|
||
```
|
||
|
||
The response also sets a `turnstone_auth` HttpOnly cookie containing the JWT.
|
||
|
||
**Response (already set up):** `409`
|
||
|
||
```json
|
||
{"error": "Setup already completed"}
|
||
```
|
||
|
||
Returned when one or more users already exist in the database.
|
||
|
||
**Response (auth disabled):** `400`
|
||
|
||
```json
|
||
{"error": "Auth is not enabled"}
|
||
```
|
||
|
||
---
|
||
|
||
## Endpoints
|
||
|
||
### `GET /`
|
||
|
||
Serves the embedded single-page application (HTML, CSS, and JavaScript inlined
|
||
in a single document). The SPA connects to the SSE and POST endpoints listed
|
||
below.
|
||
|
||
**Response:** `text/html; charset=utf-8`
|
||
|
||
---
|
||
|
||
### `GET /v1/api/workstreams/{ws_id}/events`
|
||
|
||
Opens a Server-Sent Events stream scoped to a single workstream. The connection
|
||
remains open indefinitely; the server pushes events as they occur.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|--------|----------|----------------------------|
|
||
| `ws_id` | string | yes | Workstream identifier |
|
||
|
||
**Error:** Returns `404` with `{"error": "Unknown workstream"}` if `ws_id` is
|
||
not recognized.
|
||
|
||
#### Connection lifecycle
|
||
|
||
1. **`connected`** -- sent immediately on connect.
|
||
|
||
```json
|
||
{
|
||
"type": "connected",
|
||
"model": "kappa_20b_131k",
|
||
"model_alias": "default",
|
||
"skip_permissions": false
|
||
}
|
||
```
|
||
|
||
`skip_permissions` reflects the workstream's current auto-approve state. It is
|
||
`true` if the server was started with `--skip-permissions` or if the user chose
|
||
"Always approve" via the approval prompt during the session.
|
||
|
||
2. **`history`** -- replays the full conversation history so the client can
|
||
rebuild its UI.
|
||
|
||
```json
|
||
{
|
||
"type": "history",
|
||
"messages": [
|
||
{"role": "user", "content": "Hello"},
|
||
{"role": "assistant", "content": "Hi there!", "tool_calls": null},
|
||
{"role": "tool", "content": "..."}
|
||
]
|
||
}
|
||
```
|
||
|
||
Each message in the `messages` array has:
|
||
|
||
| Field | Type | Description |
|
||
|--------------|-------------------|-----------------------------------------------|
|
||
| `role` | string | `"user"`, `"assistant"`, or `"tool"` |
|
||
| `content` | string or null | Text content of the message |
|
||
| `tool_calls` | array or null | Present only on assistant messages with calls |
|
||
| `reasoning` | string (optional) | Concatenated reasoning / chain-of-thought text on assistant turns whose `provider_data` carried reasoning-bearing blocks (Anthropic `thinking`, OpenAI Responses `reasoning`, or synthetic `reasoning_text` from local-model servers). Present only when the active model's `surface_persisted_reasoning` flag is True. |
|
||
|
||
Each entry in `tool_calls`:
|
||
|
||
| Field | Type | Description |
|
||
|-------------|--------|------------------------------------|
|
||
| `name` | string | Function name (e.g. `"bash"`) |
|
||
| `arguments` | string | JSON-encoded argument string |
|
||
|
||
#### Streaming events
|
||
|
||
After the initial `connected` and `history` frames, the server streams
|
||
real-time events as the model generates a response:
|
||
|
||
**`thinking_start`** -- the model has begun generating (shown as a spinner).
|
||
|
||
```json
|
||
{"type": "thinking_start"}
|
||
```
|
||
|
||
**`thinking_stop`** -- the spinner phase is over.
|
||
|
||
```json
|
||
{"type": "thinking_stop"}
|
||
```
|
||
|
||
**`reasoning`** -- a chunk of chain-of-thought reasoning text.
|
||
|
||
```json
|
||
{"type": "reasoning", "text": "Let me think about this..."}
|
||
```
|
||
|
||
**`content`** -- a chunk of the assistant's visible reply.
|
||
|
||
```json
|
||
{"type": "content", "text": "Here is the answer: "}
|
||
```
|
||
|
||
**`stream_end`** -- the model has finished generating. The client should
|
||
finalize any in-progress assistant message.
|
||
|
||
```json
|
||
{"type": "stream_end"}
|
||
```
|
||
|
||
**`state_change`** -- the worker thread transitioned to a new state. Drives
|
||
the client's busy-mode (composer in send vs. stop, spinner indicators,
|
||
auto-focus on idle). Sent live during normal operation AND on every fresh
|
||
SSE subscribe (so a mid-stream page refresh restores the correct composer
|
||
state without waiting for the next live transition).
|
||
|
||
```json
|
||
{"type": "state_change", "state": "running"}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|----------|--------|----------------------------------------------------------------------|
|
||
| `state` | string | One of `"running"`, `"thinking"`, `"attention"`, `"idle"`, `"error"` |
|
||
|
||
**`in_progress_snapshot`** -- one-shot replay of the in-progress turn's
|
||
content + reasoning text-so-far when this client connects mid-stream.
|
||
Lets a refreshing browser tab restore partial assistant text immediately
|
||
instead of waiting for the response to complete. Yielded once after the
|
||
kind-specific replay phase (history + pending), only when at least one
|
||
of `content` / `reasoning` is non-empty. Both halves render into the same
|
||
assistant bubble the live `content` / `reasoning` events would target;
|
||
clients should treat the snapshot as idempotent (skip overwrite if the
|
||
current local buffer is already a superset prefix — covers EventSource
|
||
auto-reconnect re-replays).
|
||
|
||
```json
|
||
{
|
||
"type": "in_progress_snapshot",
|
||
"content": "Here is the answer so far: it depends on ",
|
||
"reasoning": "The user is asking about a comparison; let me think about..."
|
||
}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|--------------|--------|------------------------------------------------------------|
|
||
| `content` | string | Joined assistant content text accumulated this turn |
|
||
| `reasoning` | string | Joined reasoning / chain-of-thought text accumulated |
|
||
|
||
**`tool_info`** -- one or more tool calls that were auto-approved (no user
|
||
action required).
|
||
|
||
```json
|
||
{
|
||
"type": "tool_info",
|
||
"items": [
|
||
{
|
||
"call_id": "call_abc123",
|
||
"header": "bash: ls -la",
|
||
"preview": "",
|
||
"func_name": "bash",
|
||
"approval_label": "bash",
|
||
"needs_approval": false,
|
||
"error": null
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**`approve_request`** -- one or more tool calls that require user approval. The
|
||
client must respond via `POST /v1/api/workstreams/{ws_id}/approve`.
|
||
|
||
```json
|
||
{
|
||
"type": "approve_request",
|
||
"items": [
|
||
{
|
||
"call_id": "call_def456",
|
||
"header": "bash: rm -rf /tmp/build",
|
||
"preview": "",
|
||
"func_name": "bash",
|
||
"approval_label": "bash",
|
||
"needs_approval": true,
|
||
"error": null
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Each item in `items` (shared by `tool_info` and `approve_request`):
|
||
|
||
| Field | Type | Description |
|
||
|------------------|-------------|--------------------------------------------------|
|
||
| `call_id` | string | Unique tool call ID (links chunks to results) |
|
||
| `header` | string | Human-readable header line for the tool call |
|
||
| `preview` | string | Diff or argument preview (may be empty) |
|
||
| `func_name` | string | Function name (e.g. `"bash"`, `"edit_file"`) |
|
||
| `approval_label` | string | Display label for the approval prompt |
|
||
| `needs_approval` | bool | Whether this call requires explicit approval |
|
||
| `error` | string/null | Error description if the call was malformed |
|
||
|
||
**`tool_output_chunk`** -- incremental streaming output from a bash tool execution. Sent line-by-line as stdout is produced. The `call_id` identifies the specific tool invocation (multiple bash tools may run in parallel).
|
||
|
||
```json
|
||
{"type": "tool_output_chunk", "call_id": "call_abc123", "chunk": "Building project...\n"}
|
||
```
|
||
|
||
**`tool_result`** -- final output from a completed tool execution. The `call_id` matches the corresponding `tool_info`/`approve_request` item and any preceding `tool_output_chunk` events. For bash tools, this arrives after all streaming chunks and includes both stdout and stderr. The `is_error` field is `true` when the tool execution failed (e.g. bash exit code >= 2 or signal, file not found, timeout). Exit code 1 is ambiguous (e.g. `grep` no-match) and is not flagged. User denials are tracked separately via a `denied` flag. Clients should use `is_error` instead of text-prefix heuristics.
|
||
|
||
```json
|
||
{"type": "tool_result", "call_id": "call_abc123", "name": "bash", "output": "file1.py\nfile2.py\n", "is_error": false}
|
||
```
|
||
|
||
**`status`** -- token usage statistics, sent after each model turn.
|
||
|
||
```json
|
||
{
|
||
"type": "status",
|
||
"prompt_tokens": 1024,
|
||
"completion_tokens": 256,
|
||
"total_tokens": 1280,
|
||
"context_window": 131072,
|
||
"pct": 1.0,
|
||
"effort": "medium",
|
||
"cache_creation_tokens": 800,
|
||
"cache_read_tokens": 200
|
||
}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|--------------------------|--------|------------------------------------------------------|
|
||
| `prompt_tokens` | int | Tokens in the prompt |
|
||
| `completion_tokens` | int | Tokens generated by the model |
|
||
| `total_tokens` | int | `prompt_tokens + completion_tokens` |
|
||
| `context_window` | int | Total context window size in tokens |
|
||
| `pct` | float | Percentage of context window used |
|
||
| `effort` | string | Reasoning effort level (`low`/`medium`/`high`) |
|
||
| `cache_creation_tokens` | int | Tokens written to prompt cache (Anthropic) |
|
||
| `cache_read_tokens` | int | Tokens served from prompt cache (Anthropic + OpenAI) |
|
||
|
||
**`info`** -- an informational message (e.g. command output).
|
||
|
||
```json
|
||
{"type": "info", "message": "Session cleared."}
|
||
```
|
||
|
||
**`error`** -- an error message.
|
||
|
||
```json
|
||
{"type": "error", "message": "Error: connection timed out"}
|
||
```
|
||
|
||
**`busy_error`** -- sent when a new message arrives while the model is already
|
||
processing.
|
||
|
||
```json
|
||
{"type": "busy_error", "message": "Already processing a request. Please wait."}
|
||
```
|
||
|
||
**`clear_ui`** -- instructs the client to clear all displayed messages (sent
|
||
after `/clear` or `/new` commands).
|
||
|
||
```json
|
||
{"type": "clear_ui"}
|
||
```
|
||
|
||
**`cancelled`** -- a cancel request was acknowledged (via the Stop button or
|
||
`POST /v1/api/workstreams/{ws_id}/cancel`). This signals that cancellation is in progress, not
|
||
that it is complete. The worker thread may still be finishing — wait for
|
||
`stream_end` before transitioning to a ready state. The client should clear
|
||
any in-progress assistant rendering but not re-enable the send button until
|
||
`stream_end` arrives.
|
||
|
||
```json
|
||
{"type": "cancelled"}
|
||
```
|
||
|
||
**`intent_verdict`** -- delivered asynchronously when the LLM judge completes
|
||
its evaluation of a pending tool call. Only sent when intent validation is
|
||
enabled (`--judge` or `[judge] enabled = true`). The `call_id` correlates with
|
||
the item in the preceding `approve_request` event.
|
||
|
||
```json
|
||
{
|
||
"type": "intent_verdict",
|
||
"verdict_id": "f7e8d9c0b1a2",
|
||
"call_id": "call_abc123",
|
||
"func_name": "bash",
|
||
"intent_summary": "Install Express.js web framework via npm",
|
||
"risk_level": "medium",
|
||
"confidence": 0.85,
|
||
"recommendation": "review",
|
||
"reasoning": "The command installs express from npm. This is a well-known package but will modify node_modules and package.json.",
|
||
"evidence": ["Checked package.json -- express is not currently a dependency"],
|
||
"tier": "llm",
|
||
"judge_model": "gpt-5",
|
||
"latency_ms": 2340
|
||
}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|------------------|------------|--------------------------------------------------------|
|
||
| `verdict_id` | string | Unique verdict identifier |
|
||
| `call_id` | string | Tool call ID (matches `approve_request` item) |
|
||
| `func_name` | string | Tool function name |
|
||
| `intent_summary` | string | One-sentence description of the tool call's intent |
|
||
| `risk_level` | string | `"low"`, `"medium"`, `"high"`, or `"critical"` |
|
||
| `confidence` | float | 0.0--1.0 confidence in the assessment |
|
||
| `recommendation` | string | `"approve"`, `"review"`, or `"deny"` |
|
||
| `reasoning` | string | Evidence-based explanation |
|
||
| `evidence` | list | Supporting evidence (file excerpts, rule names) |
|
||
| `tier` | string | Always `"llm"` for this event |
|
||
| `judge_model` | string | Model that produced the verdict |
|
||
| `latency_ms` | int | Evaluation time in milliseconds |
|
||
|
||
When intent validation is active, the `approve_request` event is also extended:
|
||
each item in `items` gains a `verdict` field containing the heuristic verdict
|
||
(same schema as above but with `tier: "heuristic"`), and the event gains a
|
||
top-level `judge_pending` boolean indicating whether an LLM verdict is in
|
||
flight.
|
||
|
||
#### Keepalive
|
||
|
||
The server sends an SSE comment every 5 seconds when no events are pending:
|
||
|
||
```
|
||
: keepalive
|
||
|
||
```
|
||
|
||
This prevents proxies and browsers from closing the connection due to
|
||
inactivity.
|
||
|
||
#### Multi-consumer fan-out
|
||
|
||
Each SSE connection to a workstream receives its own delivery queue. Events
|
||
produced by the worker thread are fanned out to all registered listener queues,
|
||
so multiple consumers (browser, console proxy, SDK) can connect
|
||
simultaneously and each receives every event. On reconnect the client receives
|
||
the kind-specific replay (`connected` + `status` + `history` + pending
|
||
approval / plan for interactive; `connected` + `status` + pending for coord)
|
||
followed by a `state_change` carrying the current worker state and an
|
||
optional `in_progress_snapshot` carrying any partial content / reasoning
|
||
buffered for the in-progress turn — so a mid-stream refresh restores both
|
||
the busy-mode UI and the partial assistant text without waiting for the
|
||
response to complete.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/events/global`
|
||
|
||
Opens a Server-Sent Events stream that broadcasts state-change events across
|
||
all workstreams. This is used by the tab bar to display per-workstream activity
|
||
indicators.
|
||
|
||
**Events:**
|
||
|
||
```json
|
||
{"type": "ws_state", "ws_id": "abc123", "state": "thinking"}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|---------|--------|--------------------------|
|
||
| `ws_id` | string | Workstream identifier |
|
||
| `state` | string | Current workstream state |
|
||
|
||
Possible `state` values:
|
||
|
||
| State | Description |
|
||
|-------------|-------------------------------------------------|
|
||
| `idle` | No active processing |
|
||
| `thinking` | Model is generating a response |
|
||
| `running` | Tool execution in progress |
|
||
| `attention` | Waiting for user input (approval or plan review)|
|
||
| `error` | An error occurred |
|
||
|
||
**Fan-out pattern:** Each connected client receives its own bounded queue
|
||
(`maxsize=1000`). A dedicated fan-out thread reads from the shared global queue
|
||
and copies each event to every client queue. If a client queue is full, the
|
||
event is silently dropped for that client.
|
||
|
||
**Keepalive:** Same as `/v1/api/workstreams/{ws_id}/events` -- an SSE comment every 5 seconds.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/workstreams`
|
||
|
||
Returns a list of all active workstreams.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"workstreams": [
|
||
{"ws_id": "abc123", "name": "default", "state": "idle"},
|
||
{"ws_id": "def456", "name": "hacker-news", "state": "thinking"}
|
||
]
|
||
}
|
||
```
|
||
|
||
Each workstream object:
|
||
|
||
| Field | Type | Description |
|
||
|--------------|-------------|--------------------------------------------------------|
|
||
| `ws_id` | string | Unique workstream routing identifier |
|
||
| `name` | string | Display name (alias if set, otherwise `ws-xxxx`) |
|
||
| `state` | string | Current state (see state values above) |
|
||
|
||
---
|
||
|
||
### `GET /v1/api/workstreams/saved`
|
||
|
||
Returns a list of saved workstreams from the database, ordered by most recently
|
||
updated.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"workstreams": [
|
||
{
|
||
"ws_id": "a1b2c3d4e5f6",
|
||
"alias": "refactor",
|
||
"title": "JWT Authentication Refactor",
|
||
"created": "2026-03-01 10:00:00",
|
||
"updated": "2026-03-01 11:30:00",
|
||
"message_count": 42
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Each saved workstream object:
|
||
|
||
| Field | Type | Description |
|
||
|-----------------|-------------|--------------------------------------------|
|
||
| `ws_id` | string | Unique workstream identifier |
|
||
| `alias` | string/null | User-assigned short name |
|
||
| `title` | string/null | LLM-generated title |
|
||
| `created` | string | ISO timestamp of workstream creation |
|
||
| `updated` | string | ISO timestamp of last message |
|
||
| `message_count` | int | Number of messages in the workstream |
|
||
|
||
---
|
||
|
||
### `GET /v1/api/skills`
|
||
|
||
Returns a summary list of all available skills. This is a read-only
|
||
endpoint (requires `read` scope) that exposes skill names and categories
|
||
without revealing skill content. Useful for populating skill selectors
|
||
in UIs or discovering available skills before creating a workstream.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"skills": [
|
||
{"name": "safety-guidelines", "category": "safety", "is_default": true, "origin": "manual"},
|
||
{"name": "mcp__server__code", "category": "", "is_default": false, "origin": "mcp"}
|
||
]
|
||
}
|
||
```
|
||
|
||
Each skill summary:
|
||
|
||
| Field | Type | Description |
|
||
|--------------|--------|------------------------------------------------------|
|
||
| `name` | string | Skill name (used in `skill` field on workstream creation) |
|
||
| `category` | string | Skill category |
|
||
| `is_default` | bool | Whether skill is auto-applied to all sessions |
|
||
| `origin` | string | Skill origin: `manual` or `mcp` |
|
||
|
||
> **Note:** For full skill management (create, update, delete, view content),
|
||
> use the admin endpoints at `GET /v1/api/admin/skills` (requires `admin.skills` permission).
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/send`
|
||
|
||
Sends a user message to a workstream. Spawns a daemon worker thread that calls
|
||
`session.send()` and streams results back via the SSE channel.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|--------|----------|----------------------|
|
||
| `ws_id` | string | yes | Target workstream ID |
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"message": "Explain how the server works"}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|-----------|--------|----------|-------------------------|
|
||
| `message` | string | yes | The user's message text |
|
||
|
||
**Response (success):**
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
**Response (busy):** Returned if the workstream's worker thread is still alive
|
||
from a previous request. Also pushes a `busy_error` event to the SSE stream.
|
||
|
||
```json
|
||
{"status": "busy"}
|
||
```
|
||
|
||
**Error responses:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------------------------------------|------------------------|
|
||
| 400 | `{"error": "Empty message"}` | Message is empty |
|
||
| 404 | `{"error": "Unknown workstream"}` | `ws_id` not found |
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/approve`
|
||
|
||
Responds to a tool approval request. The SSE stream must have previously sent
|
||
an `approve_request` event for the given workstream.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|--------|----------|----------------------|
|
||
| `ws_id` | string | yes | Target workstream ID |
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"approved": true, "feedback": null, "always": false}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|------------|-------------|----------|--------------------------------------------------|
|
||
| `approved` | bool | yes | `true` to approve, `false` to deny |
|
||
| `feedback` | string/null | no | Optional feedback text (sent as denial reason) |
|
||
| `always` | bool | no | If `true` and `approved`, enables auto-approve |
|
||
|
||
When `always` is `true` and `approved` is `true`, the workstream's WebUI
|
||
instance sets `auto_approve = True`, causing all subsequent tool calls to be
|
||
automatically approved without prompting.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
**Error:** `404` with `{"error": "Unknown workstream"}` if `ws_id` is invalid.
|
||
|
||
---
|
||
|
||
### `POST /v1/api/command`
|
||
|
||
Executes a slash command in the given workstream.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"command": "/clear", "ws_id": "abc123"}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|-----------|--------|----------|------------------------------------|
|
||
| `command` | string | yes | The slash command (e.g. `/clear`) |
|
||
| `ws_id` | string | yes | Target workstream ID |
|
||
|
||
If the command is `/clear` or `/new`, the server pushes a `clear_ui` SSE event
|
||
to instruct the client to reset its message display. If the command is
|
||
`/resume`, the server pushes `clear_ui` followed by a `history` event
|
||
containing the resumed session's messages.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
**Error responses:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------------------------------------|----------------------|
|
||
| 400 | `{"error": "Empty command"}` | Command is empty |
|
||
| 404 | `{"error": "Unknown workstream"}` | `ws_id` not found |
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/cancel`
|
||
|
||
Cancels the active generation in a workstream. Sets a cooperative cancellation
|
||
flag that is checked at multiple points in the generation loop (per streaming
|
||
chunk, before tool execution, inside bash commands). Also closes the underlying
|
||
HTTP stream to the LLM provider, unblocking any pending read immediately.
|
||
The session transitions to `idle` state and preserves any partial content
|
||
already streamed.
|
||
|
||
If the workstream is waiting for tool approval or plan review, the pending
|
||
prompt is automatically denied/rejected to unblock the worker thread.
|
||
|
||
Calling this endpoint when the workstream is already idle is a harmless no-op.
|
||
|
||
**Force cancel:** When `force` is `true`, the server abandons the stuck worker
|
||
thread immediately and transitions the workstream to `idle`. The abandoned
|
||
thread continues to wind down in the background (killing any running
|
||
subprocesses and exiting at the next cancellation checkpoint). During this
|
||
wind-down it may emit a final `stream_end` event which the server suppresses
|
||
for the orphaned thread. Use force cancel when cooperative cancel has not
|
||
resolved within a few seconds — the web UI offers this as a "Force Stop"
|
||
button automatically.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|--------|----------|----------------------|
|
||
| `ws_id` | string | yes | Target workstream ID |
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"force": false}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|--------|--------|----------|----------------------|
|
||
| `force`| bool | no | Abandon stuck worker immediately (default: `false`) |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
**Error responses:**
|
||
|
||
| Status | Body | Condition |
|
||
|--------|------------------------------------|------------------------|
|
||
| 400 | `{"error": "No session"}` | Session not initialized|
|
||
| 404 | `{"error": "Unknown workstream"}` | `ws_id` not found |
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/new`
|
||
|
||
Creates a new workstream. The server supports up to 10 concurrent workstreams.
|
||
|
||
The endpoint accepts **either** `application/json` (legacy shape) **or**
|
||
`multipart/form-data` when you want to upload attachments at creation
|
||
time. Multipart requests carry one `meta` field containing the JSON body
|
||
shown below plus zero-or-more `file` parts; each file is validated and
|
||
reserved onto the new workstream's first turn before the dispatch worker
|
||
runs, so queued multimodal turns cannot lose files to racing sends. If
|
||
validation fails the fresh workstream is rolled back so no orphan rows
|
||
leak.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"name": "my-ws", "model": "openai"}
|
||
```
|
||
|
||
All fields are optional. The body can be empty or an empty JSON object.
|
||
|
||
| Field | Type | Default | Description |
|
||
|------------------|--------|---------|----------------------------------------------------------------|
|
||
| `name` | string | auto | Workstream display name |
|
||
| `model` | string | default | Model alias from the registry (`[models.*]`) |
|
||
| `auto_approve` | bool | false | Auto-approve all tool calls for this workstream |
|
||
| `resume_ws` | string | "" | Workstream ID to resume atomically during creation (empty = fresh)|
|
||
| `skill` | string | "" | Skill name. Applies content (system prompt), model, temperature, reasoning effort, max tokens, auto-approve policy, token budget, and other session config from the skill. Returns 400 if not found or disabled. Ignored when `resume_ws` is set (resumed sessions restore their own skill). |
|
||
| `judge_model` | string | "" | Optional model alias for the judge (overrides default judge model for this workstream) |
|
||
|
||
> **Skill behavior:** When `skill` is specified, the skill's content is injected as a system message and its session config fields (model, temperature, auto-approve, token budget, etc.) override system defaults for the new workstream.
|
||
|
||
**Response (success):**
|
||
|
||
```json
|
||
{"ws_id": "ghi789", "name": "ws-3", "resumed": false, "message_count": 0}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|-----------------|--------|-----------------------------------------------------|
|
||
| `ws_id` | string | Unique ID of the new workstream |
|
||
| `name` | string | Auto-generated workstream name |
|
||
| `resumed` | bool | Whether a previous session was successfully resumed |
|
||
| `message_count` | int | Number of messages in the resumed session (0 if fresh) |
|
||
|
||
**Error (limit reached):**
|
||
|
||
```json
|
||
{"error": "Maximum of 10 workstreams reached"}
|
||
```
|
||
|
||
Status code: `400`
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/close`
|
||
|
||
Closes and removes a workstream. The last remaining workstream cannot be
|
||
closed.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|--------|----------|------------------------|
|
||
| `ws_id` | string | yes | Workstream ID to close |
|
||
|
||
**Request body:**
|
||
|
||
The body must be valid JSON. If you are not supplying any optional
|
||
fields, send `{}` — an empty / non-JSON body is rejected with a
|
||
`400`.
|
||
|
||
| Field | Type | Required | Description |
|
||
|----------|--------|----------|----------------------------------------------------------|
|
||
| `reason` | string | no | Optional close reason persisted to `workstream_config`. |
|
||
|
||
The `reason` is capped at **512 UTF-8 bytes** (multibyte-safe — the
|
||
cap holds for CJK and emoji payloads), and the output guard's
|
||
credential-redaction pass strips secrets before the value is
|
||
persisted. A non-string `reason` is silently coerced to empty and
|
||
the close proceeds without writing the field.
|
||
|
||
**Response (success):**
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
**Error (last workstream):**
|
||
|
||
```json
|
||
{"error": "Cannot close last workstream"}
|
||
```
|
||
|
||
Status code: `400`
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/attachments`
|
||
|
||
Upload an image or text document and attach it to the caller's next user
|
||
turn on this workstream.
|
||
|
||
- Images (png/jpeg/gif/webp) are capped at **4 MiB** and validated via
|
||
magic-byte sniff on upload.
|
||
- Text documents (any `text/*` MIME, allow-listed application MIMEs, or
|
||
known text extensions) are capped at **512 KiB** and must be UTF-8.
|
||
- Per-(workstream, user) pending cap is **10** attachments.
|
||
|
||
The attachment moves through three states: `pending → reserved →
|
||
consumed`. Reservation tokens are threaded through
|
||
`POST /v1/api/workstreams/{ws_id}/send` so a queued multimodal turn cannot lose its file to
|
||
an overlapping send.
|
||
|
||
Ownership failures are masked as `404` so non-owners cannot enumerate
|
||
workstream existence.
|
||
|
||
**Content-Type:** `multipart/form-data` with a single `file` field.
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{
|
||
"attachment_id": "att_abc123",
|
||
"kind": "image",
|
||
"mime_type": "image/png",
|
||
"size_bytes": 73240,
|
||
"filename": "screenshot.png",
|
||
"state": "pending"
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Code | Meaning |
|
||
|------|---------------------------------------------------------|
|
||
| 400 | Missing/invalid form, unsupported MIME, not UTF-8, etc. |
|
||
| 403 | Auth/scope failure |
|
||
| 404 | Workstream not found / not owned by caller |
|
||
| 409 | Pending-cap reached |
|
||
| 413 | Payload exceeds size cap |
|
||
|
||
---
|
||
|
||
### `GET /v1/api/workstreams/{ws_id}/attachments`
|
||
|
||
List the caller's **pending** (unconsumed) attachments for this
|
||
workstream. Ownership failures are masked as `404`.
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{
|
||
"attachments": [
|
||
{
|
||
"attachment_id": "att_abc123",
|
||
"kind": "image",
|
||
"mime_type": "image/png",
|
||
"size_bytes": 73240,
|
||
"filename": "screenshot.png",
|
||
"state": "pending"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /v1/api/workstreams/{ws_id}/attachments/{attachment_id}/content`
|
||
|
||
Returns the raw bytes of an attachment with its stored `Content-Type`.
|
||
Useful for previewing an image or replaying a document. Ownership
|
||
failures are masked as `404`.
|
||
|
||
**Response:** `200` — binary body, original `Content-Type`.
|
||
|
||
---
|
||
|
||
### `DELETE /v1/api/workstreams/{ws_id}/attachments/{attachment_id}`
|
||
|
||
Remove a pending attachment. Consumed attachments return `404` (they
|
||
are part of a committed conversation turn). Ownership failures are also
|
||
masked as `404`.
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{"deleted": "att_abc123"}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/delete`
|
||
|
||
Permanently delete a saved workstream and all its messages from storage.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|----------------------|
|
||
| `ws_id` | string | Workstream ID |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"deleted": "a1b2c3d4"}
|
||
```
|
||
|
||
**Response (not found):** `404`
|
||
|
||
```json
|
||
{"error": "Workstream not found"}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/open`
|
||
|
||
Load a saved workstream into memory with its original `ws_id`. If the
|
||
workstream is already loaded, returns immediately with `already_loaded: true`.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|----------------------|
|
||
| `ws_id` | string | Workstream ID |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"ws_id": "a1b2c3d4", "name": "refactor"}
|
||
```
|
||
|
||
**Response (already loaded):** `200`
|
||
|
||
```json
|
||
{"ws_id": "a1b2c3d4", "name": "refactor", "already_loaded": true}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/title`
|
||
|
||
Set a workstream title manually. The title is stored as the workstream alias.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|----------------------|
|
||
| `ws_id` | string | Workstream ID |
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"title": "JWT Authentication Refactor"}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|---------|--------|----------|------------------------|
|
||
| `title` | string | yes | New workstream title |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"status": "ok", "title": "JWT Authentication Refactor"}
|
||
```
|
||
|
||
**Response (conflict):** `409`
|
||
|
||
```json
|
||
{"error": "That name is already used by another workstream"}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/workstreams/{ws_id}/refresh-title`
|
||
|
||
Regenerate the workstream title via LLM based on conversation content.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|----------------------|
|
||
| `ws_id` | string | Workstream ID |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/settings`
|
||
|
||
List `interface.*` settings with their current values and sources. Requires
|
||
`read` scope on the server.
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{
|
||
"settings": [
|
||
{
|
||
"key": "interface.close_tab_action",
|
||
"value": "last_used",
|
||
"source": "default",
|
||
"type": "str",
|
||
"description": "Determines which workstream to switch to after closing a tab."
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST|PUT /v1/api/admin/settings/{key}`
|
||
|
||
Update an `interface.*` setting. Only keys in the `interface` section are
|
||
accepted; other keys return `400`.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|-------------------------------------|
|
||
| `key` | string | Setting key (e.g. `interface.theme`) |
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{"value": "light"}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|---------|------|----------|----------------|
|
||
| `value` | any | yes | New value |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"status": "ok", "key": "interface.theme", "value": "light"}
|
||
```
|
||
|
||
**Error:** `400` if the key is not in the `interface` section.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/watches`
|
||
|
||
List active watches on this server node. Optionally filter by workstream.
|
||
Requires `write` scope.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|-----------|--------|----------|------------------------------------|
|
||
| `ws_id` | string | no | Filter to watches for this workstream. If omitted, returns all watches on the node. |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"watches": [
|
||
{
|
||
"watch_id": "abc123def456...",
|
||
"ws_id": "ws-1",
|
||
"node_id": "host_a1b2",
|
||
"name": "pr-review",
|
||
"command": "gh pr view --json state",
|
||
"interval_secs": 300.0,
|
||
"stop_on": "data[\"state\"] == \"MERGED\"",
|
||
"max_polls": 100,
|
||
"poll_count": 5,
|
||
"last_output": "{\"state\": \"OPEN\"}",
|
||
"last_poll": "2026-03-09T12:00:00",
|
||
"next_poll": "2026-03-09T12:05:00",
|
||
"active": 1,
|
||
"created": "2026-03-09T11:30:00"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/watches/{watch_id}/cancel`
|
||
|
||
Cancel an active watch. Sets `active=0` and clears `next_poll`.
|
||
Requires `write` scope. Verifies node ownership in multi-node deployments.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|------------|--------|-----------------|
|
||
| `watch_id` | string | Watch ID to cancel |
|
||
|
||
**Response (success):**
|
||
|
||
```json
|
||
{"status": "ok", "watch_id": "abc123def456..."}
|
||
```
|
||
|
||
**Error (not found):**
|
||
|
||
```json
|
||
{"error": "Watch not found"}
|
||
```
|
||
|
||
Status code: `404`
|
||
|
||
**Error (wrong node):**
|
||
|
||
```json
|
||
{"error": "Watch belongs to another node"}
|
||
```
|
||
|
||
Status code: `403`
|
||
|
||
---
|
||
|
||
### `GET /v1/api/memories`
|
||
|
||
List structured memories with optional filters. Requires `read` scope.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Default | Description |
|
||
|------------|--------|----------|---------|------------------------------|
|
||
| `type` | string | no | `""` | Filter by memory type (user, project, feedback, reference) |
|
||
| `scope` | string | no | `""` | Filter by scope (global, workstream, user) |
|
||
| `scope_id` | string | no | `""` | Scope qualifier. Auto-resolved for `scope=user` when auth is active. |
|
||
| `limit` | int | no | `100` | Max results (capped at 200) |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"memories": [
|
||
{
|
||
"memory_id": "a1b2c3d4-e5f6-...",
|
||
"name": "project_architecture",
|
||
"description": "Core architecture patterns",
|
||
"type": "project",
|
||
"scope": "global",
|
||
"scope_id": "",
|
||
"content": "The project uses a hexagonal architecture...",
|
||
"created": "2026-03-10T10:00:00",
|
||
"updated": "2026-03-12T14:30:00"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/memories`
|
||
|
||
Save or upsert a structured memory. Requires `write` scope. Returns `201` on
|
||
create, `200` on update.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"name": "deployment_process",
|
||
"content": "Deploy via GitHub Actions. Staging auto-deploys on push to main.",
|
||
"description": "CI/CD deployment workflow",
|
||
"type": "project",
|
||
"scope": "global",
|
||
"scope_id": ""
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Default | Description |
|
||
|--------------|--------|----------|-------------|--------------------------------------|
|
||
| `name` | string | yes | -- | Memory name (max 256 chars) |
|
||
| `content` | string | yes | -- | Memory content (max 65536 chars) |
|
||
| `description`| string | no | `""` | Short description for search ranking |
|
||
| `type` | string | no | `"project"` | One of: user, project, feedback, reference |
|
||
| `scope` | string | no | `"global"` | One of: global, workstream, user |
|
||
| `scope_id` | string | no | `""` | Scope qualifier (auto-resolved for user scope) |
|
||
|
||
**Response (created):** `201`
|
||
|
||
```json
|
||
{
|
||
"memory_id": "a1b2c3d4-e5f6-...",
|
||
"name": "deployment_process",
|
||
"description": "CI/CD deployment workflow",
|
||
"type": "project",
|
||
"scope": "global",
|
||
"scope_id": "",
|
||
"content": "Deploy via GitHub Actions...",
|
||
"created": "2026-03-14T10:00:00",
|
||
"updated": "2026-03-14T10:00:00"
|
||
}
|
||
```
|
||
|
||
**Error responses:**
|
||
|
||
| Status | Condition |
|
||
|--------|--------------------------------------------------------|
|
||
| 400 | Missing name, empty content, invalid type/scope, name too long, content too long |
|
||
|
||
---
|
||
|
||
### `POST /v1/api/memories/search`
|
||
|
||
Search memories by query. Uses POST for the request body but is non-mutating
|
||
(requires only `read` scope).
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"query": "authentication",
|
||
"type": "project",
|
||
"scope": "",
|
||
"limit": 20
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Default | Description |
|
||
|------------|--------|----------|---------|--------------------------------|
|
||
| `query` | string | yes | -- | Search query |
|
||
| `type` | string | no | `""` | Filter by type |
|
||
| `scope` | string | no | `""` | Filter by scope |
|
||
| `scope_id` | string | no | `""` | Filter by scope ID |
|
||
| `limit` | int | no | `20` | Max results (capped at 50) |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"memories": [
|
||
{
|
||
"memory_id": "a1b2c3d4-e5f6-...",
|
||
"name": "auth_patterns",
|
||
"description": "Authentication architecture",
|
||
"type": "project",
|
||
"scope": "global",
|
||
"scope_id": "",
|
||
"content": "JWT tokens with HS256...",
|
||
"created": "2026-03-10T10:00:00",
|
||
"updated": "2026-03-12T14:30:00"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
**Error:** `400` with `{"error": "query is required"}` if `query` is empty.
|
||
|
||
---
|
||
|
||
### `DELETE /v1/api/memories/{name}`
|
||
|
||
Delete a memory by name and scope. Requires `write` scope.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|----------------------|
|
||
| `name` | string | Memory name |
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Default | Description |
|
||
|------------|--------|----------|------------|---------------------|
|
||
| `scope` | string | no | `"global"` | Scope of the memory |
|
||
| `scope_id` | string | no | `""` | Scope qualifier |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"status": "ok", "name": "deployment_process"}
|
||
```
|
||
|
||
**Error (not found):** `404`
|
||
|
||
```json
|
||
{"error": "Memory 'deployment_process' not found"}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/memories` (Console)
|
||
|
||
List structured memories across all scopes. Requires `admin.memories`
|
||
permission.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Default | Description |
|
||
|------------|--------|----------|---------|------------------------------|
|
||
| `type` | string | no | `""` | Filter by type |
|
||
| `scope` | string | no | `""` | Filter by scope |
|
||
| `scope_id` | string | no | `""` | Filter by scope ID |
|
||
| `limit` | int | no | `100` | Max results (capped at 200) |
|
||
|
||
**Response:** `200` -- same schema as `GET /v1/api/memories`.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/memories/search` (Console)
|
||
|
||
Search memories by query. Requires `admin.memories` permission.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Default | Description |
|
||
|------------|--------|----------|---------|-------------------------------|
|
||
| `q` | string | yes | -- | Search query |
|
||
| `type` | string | no | `""` | Filter by type |
|
||
| `scope` | string | no | `""` | Filter by scope |
|
||
| `scope_id` | string | no | `""` | Filter by scope ID |
|
||
| `limit` | int | no | `20` | Max results (capped at 50) |
|
||
|
||
**Response:** `200` -- same schema as `GET /v1/api/memories`.
|
||
|
||
**Error:** `400` with `{"error": "q is required"}` if `q` is empty.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/memories/{memory_id}` (Console)
|
||
|
||
Get a single memory by ID. Requires `admin.memories` permission.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-------------|--------|------------------------|
|
||
| `memory_id` | string | Memory UUID |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{
|
||
"memory_id": "a1b2c3d4-e5f6-...",
|
||
"name": "project_architecture",
|
||
"description": "Core architecture patterns",
|
||
"type": "project",
|
||
"scope": "global",
|
||
"scope_id": "",
|
||
"content": "The project uses...",
|
||
"created": "2026-03-10T10:00:00",
|
||
"updated": "2026-03-12T14:30:00"
|
||
}
|
||
```
|
||
|
||
**Error (not found):** `404`
|
||
|
||
```json
|
||
{"error": "Memory not found"}
|
||
```
|
||
|
||
---
|
||
|
||
### `DELETE /v1/api/admin/memories/{memory_id}` (Console)
|
||
|
||
Delete a memory by ID. Records an audit event (`memory.delete`). Requires
|
||
`admin.memories` permission.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-------------|--------|------------------------|
|
||
| `memory_id` | string | Memory UUID |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"status": "ok"}
|
||
```
|
||
|
||
**Error (not found):** `404`
|
||
|
||
```json
|
||
{"error": "Memory not found"}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/verdicts` (Console)
|
||
|
||
List intent validation verdicts from the `intent_verdicts` table. This endpoint
|
||
is on the **console** server and requires the `admin.judge` permission.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|--------------|--------|----------|----------------------------------------------------|
|
||
| `ws_id` | string | no | Filter by workstream ID |
|
||
| `since` | string | no | ISO timestamp lower bound |
|
||
| `until` | string | no | ISO timestamp upper bound |
|
||
| `risk_level` | string | no | Filter by risk level (`low`/`medium`/`high`/`critical`) |
|
||
| `limit` | int | no | Max results (default 100, max 500) |
|
||
| `offset` | int | no | Pagination offset (default 0) |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"verdicts": [
|
||
{
|
||
"verdict_id": "a1b2c3d4e5f6",
|
||
"ws_id": "ws-1",
|
||
"call_id": "call_abc123",
|
||
"func_name": "bash",
|
||
"func_args": "{\"command\": \"npm install express\"}",
|
||
"intent_summary": "Package installation: npm install express",
|
||
"risk_level": "medium",
|
||
"confidence": 0.70,
|
||
"recommendation": "review",
|
||
"reasoning": "Command installs a software package which may modify the environment.",
|
||
"evidence": "[\"Matched rule: package-install\"]",
|
||
"tier": "heuristic",
|
||
"judge_model": "",
|
||
"latency_ms": 0,
|
||
"user_decision": "approved",
|
||
"created": "2026-03-13T10:00:00"
|
||
}
|
||
],
|
||
"total": 42
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/output-assessments` (Console)
|
||
|
||
List output guard assessments from the `output_assessments` table. This endpoint
|
||
is on the **console** server and requires the `admin.judge` permission.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Description |
|
||
|--------------|--------|----------|----------------------------------------------------|
|
||
| `ws_id` | string | no | Filter by workstream ID |
|
||
| `risk_level` | string | no | Filter by risk level (`low`/`medium`/`high`) |
|
||
| `since` | string | no | ISO timestamp lower bound |
|
||
| `until` | string | no | ISO timestamp upper bound |
|
||
| `limit` | int | no | Max results (default 100, max 500) |
|
||
| `offset` | int | no | Pagination offset (default 0) |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"assessments": [
|
||
{
|
||
"assessment_id": "a1b2c3d4e5f6",
|
||
"ws_id": "ws-1",
|
||
"call_id": "call_abc123",
|
||
"func_name": "bash",
|
||
"flags": "[\"credential_leak\"]",
|
||
"risk_level": "high",
|
||
"annotations": "[\"API key detected (sk-proj-...)\"]",
|
||
"output_length": 1024,
|
||
"redacted": 1,
|
||
"created": "2026-03-16T10:00:00"
|
||
}
|
||
],
|
||
"total": 7
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /v1/api/admin/skills/{skill_id}/rescan` (Console)
|
||
|
||
Re-scan a skill's content for security signals using the current scanner
|
||
version. Requires the `admin.skills` permission.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|------------|--------|-------------|
|
||
| `skill_id` | string | Skill (prompt template) ID |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"risk_level": "medium",
|
||
"scan_report": "{\"composite\": 1.75, \"details\": {...}}",
|
||
"scan_version": "1"
|
||
}
|
||
```
|
||
|
||
**Error:** `404` if skill not found.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/skills/discover` (Console)
|
||
|
||
Search external skill registries for available skills. Requires the
|
||
`admin.skills` permission.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Default | Description |
|
||
|-----------|--------|---------|-------------|
|
||
| `q` | string | `""` | Search query |
|
||
| `limit` | int | `20` | Max results (1–100) |
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"skills": [
|
||
{
|
||
"id": "owner/repo/skill-name",
|
||
"name": "skill-name",
|
||
"description": "A skill description",
|
||
"author": "Author Name",
|
||
"source": "skills.sh",
|
||
"source_url": "https://github.com/owner/repo",
|
||
"install_count": 42,
|
||
"tags": ["coding", "review"],
|
||
"installed": false
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Error:** `502` if the registry is unreachable.
|
||
|
||
---
|
||
|
||
### `POST /v1/api/admin/skills/install` (Console)
|
||
|
||
Install a skill from an external source (skills.sh registry or GitHub).
|
||
Requires the `admin.skills` permission.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"source": "github",
|
||
"url": "https://github.com/owner/skill-repo"
|
||
}
|
||
```
|
||
|
||
Or for skills.sh:
|
||
|
||
```json
|
||
{
|
||
"source": "skills.sh",
|
||
"skill_id": "owner/skill-name"
|
||
}
|
||
```
|
||
|
||
**Response:** Same as `GET /v1/api/admin/skills/{skill_id}` — the created
|
||
skill object.
|
||
|
||
**Errors:** `400` invalid source or missing fields, `404` SKILL.md not found,
|
||
`409` skill already installed (duplicate source_url or name), `502` source
|
||
unreachable.
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/settings` (Console)
|
||
|
||
List all settings with their effective values, defaults, and metadata. Requires
|
||
the `admin.settings` permission.
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{
|
||
"settings": [
|
||
{
|
||
"key": "model.temperature",
|
||
"value": 0.7,
|
||
"source": "storage",
|
||
"type": "float",
|
||
"description": "Sampling temperature",
|
||
"section": "model",
|
||
"is_secret": false,
|
||
"node_id": "",
|
||
"changed_by": "admin",
|
||
"updated": "2026-03-14T10:00:00",
|
||
"restart_required": false
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /v1/api/admin/settings/schema` (Console)
|
||
|
||
Return the full registry catalog (all defined settings with metadata). Requires
|
||
the `admin.settings` permission. Useful for building dynamic admin UIs.
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{
|
||
"schema": [
|
||
{
|
||
"key": "model.temperature",
|
||
"type": "float",
|
||
"default": 0.5,
|
||
"description": "Sampling temperature",
|
||
"section": "model",
|
||
"is_secret": false,
|
||
"min_value": 0.0,
|
||
"max_value": 2.0,
|
||
"choices": null,
|
||
"restart_required": false
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `PUT /v1/api/admin/settings/{key}` (Console)
|
||
|
||
Update a setting. Requires the `admin.settings` permission. The value is
|
||
validated against the registry definition (type coercion, range checks, choices).
|
||
Secret settings (`is_secret=true`) return `403`.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|-------------|
|
||
| `key` | string | Dotted setting key (e.g. `model.temperature`) |
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"value": 0.7,
|
||
"node_id": ""
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Default | Description |
|
||
|-----------|--------|----------|---------|-------------|
|
||
| `value` | any | yes | -- | New value (type-coerced against registry) |
|
||
| `node_id` | string | no | `""` | Node ID for per-node override |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{
|
||
"key": "model.temperature",
|
||
"value": 0.7,
|
||
"source": "storage",
|
||
"type": "float",
|
||
"description": "Sampling temperature",
|
||
"section": "model",
|
||
"is_secret": false,
|
||
"node_id": "",
|
||
"changed_by": "admin",
|
||
"updated": "",
|
||
"restart_required": false
|
||
}
|
||
```
|
||
|
||
**Errors:**
|
||
|
||
| Status | Condition |
|
||
|--------|-----------|
|
||
| 400 | Unknown key, invalid value, type mismatch, out of range, missing `value` field |
|
||
| 403 | Secret setting (must use config.toml or env) |
|
||
|
||
---
|
||
|
||
### `DELETE /v1/api/admin/settings/{key}` (Console)
|
||
|
||
Reset a setting to its registry default by removing it from storage. Requires
|
||
the `admin.settings` permission.
|
||
|
||
**Path parameters:**
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|--------|-------------|
|
||
| `key` | string | Dotted setting key |
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Required | Default | Description |
|
||
|-----------|--------|----------|---------|-------------|
|
||
| `node_id` | string | no | `""` | Node ID (empty = global) |
|
||
|
||
**Response (success):** `200`
|
||
|
||
```json
|
||
{"status": "ok", "key": "model.temperature", "default": 0.5}
|
||
```
|
||
|
||
**Response (not found):** `404`
|
||
|
||
```json
|
||
{"error": "Setting 'model.temperature' has no stored value"}
|
||
```
|
||
|
||
---
|
||
|
||
### MCP Servers
|
||
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| GET | `/v1/api/admin/mcp-servers` | List all MCP server definitions with live node status. Query: `?reveal=true` to show env/header secrets. |
|
||
| POST | `/v1/api/admin/mcp-servers` | Create an MCP server definition. Body: `{name, transport, command?, args?, url?, headers?, env?, auto_approve?, enabled?}` |
|
||
| GET | `/v1/api/admin/mcp-servers/{server_id}` | Get a single MCP server with per-node connection status. |
|
||
| PUT | `/v1/api/admin/mcp-servers/{server_id}` | Update an MCP server definition. Partial updates supported. |
|
||
| DELETE | `/v1/api/admin/mcp-servers/{server_id}` | Delete an MCP server definition. |
|
||
| POST | `/v1/api/admin/mcp-servers/reload` | Tell all cluster nodes to re-read the `mcp_servers` DB table and reconcile (add new, remove stale, reconnect changed). |
|
||
| POST | `/v1/api/admin/mcp-servers/import` | Import servers from a pasted JSON config. Body: `{config: {mcpServers: {...}}}`. Skips existing names. |
|
||
|
||
Permission: `admin.mcp`
|
||
|
||
Secrets (`env`, `headers` fields) are masked with `***` by default. Use `?reveal=true` on GET endpoints to see actual values.
|
||
|
||
---
|
||
|
||
### MCP Registry
|
||
|
||
#### Search Registry
|
||
|
||
`GET /v1/api/admin/mcp-registry/search`
|
||
|
||
Search the official MCP Registry for available servers. Permission: `admin.mcp`.
|
||
|
||
**Query parameters:**
|
||
|
||
| Parameter | Type | Default | Description |
|
||
|-----------|------|---------|-------------|
|
||
| `search` | string | `""` | Search query. Empty returns a browsable listing. |
|
||
| `limit` | integer | `20` | Results per page (max 100). |
|
||
| `cursor` | string | — | Opaque cursor for pagination. |
|
||
|
||
**Response:** `200`
|
||
|
||
```json
|
||
{
|
||
"servers": [
|
||
{
|
||
"name": "io.example/mcp-server",
|
||
"description": "...",
|
||
"title": "Example Server",
|
||
"version": "1.0.0",
|
||
"website_url": "https://example.com",
|
||
"repository": {"url": "...", "source": "github"},
|
||
"icons": [],
|
||
"remotes": [{"type": "streamable-http", "url": "...", "headers": [...], "variables": {...}}],
|
||
"packages": [{"registry_type": "npm", "identifier": "@example/server", "version": "1.0.0", "transport_type": "stdio", "environment_variables": [...]}],
|
||
"meta": {"status": "active", "is_latest": true},
|
||
"installed": false,
|
||
"installed_server_id": "",
|
||
"installed_version": "",
|
||
"update_available": false
|
||
}
|
||
],
|
||
"total": 100,
|
||
"next_cursor": "abc123"
|
||
}
|
||
```
|
||
|
||
**Errors:** `502` (registry unreachable).
|
||
|
||
#### Install from Registry
|
||
|
||
`POST /v1/api/admin/mcp-registry/install`
|
||
|
||
Install an MCP server from the registry. Auto-reloads all cluster nodes. Permission: `admin.mcp`.
|
||
|
||
**Request body:**
|
||
|
||
```json
|
||
{
|
||
"registry_name": "io.example/mcp-server",
|
||
"source": "remote",
|
||
"index": 0,
|
||
"name": "",
|
||
"variables": {},
|
||
"env": {"API_KEY": "sk-..."},
|
||
"headers": {"Authorization": "Bearer ..."}
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `registry_name` | string | yes | Server name from registry search results. |
|
||
| `source` | string | yes | `"remote"` (streamable-http) or `"package"` (npm/pypi). |
|
||
| `index` | integer | no (default `0`) | Which remote or package entry to use. |
|
||
| `name` | string | no | Custom server name. Auto-derived from registry name if empty. |
|
||
| `variables` | object | no | Values for URL template `{var}` placeholders. |
|
||
| `env` | object | no | Environment variable values for package servers. |
|
||
| `headers` | object | no | Header values for remote servers. |
|
||
|
||
**Response:** Same as `POST /v1/api/admin/mcp-servers` (McpServerDetail).
|
||
|
||
**Errors:** `400` (validation), `404` (not in registry), `409` (already installed or name collision), `502` (registry unreachable).
|
||
|
||
---
|
||
|
||
### `OPTIONS` (any path)
|
||
|
||
Handles CORS preflight requests.
|
||
|
||
**Response headers:**
|
||
|
||
```
|
||
Access-Control-Allow-Origin: *
|
||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||
Access-Control-Allow-Headers: Content-Type
|
||
```
|
||
|
||
Status code: `200` with an empty body.
|
||
|
||
---
|
||
|
||
## Error Handling
|
||
|
||
| Condition | Behavior |
|
||
|------------------------------------|------------------------------------------------------------|
|
||
| Malformed or unparseable JSON body | Treated as an empty dict `{}`; missing fields use defaults |
|
||
| Unknown `ws_id` | `404` with `{"error": "Unknown workstream"}` |
|
||
| Unknown path (GET or POST) | `404` with plain-text body `Not found` |
|
||
| Empty `message` on `/v1/api/workstreams/{ws_id}/send` | `400` with `{"error": "Empty message"}` |
|
||
| Empty `command` on `/v1/api/command` | `400` with `{"error": "Empty command"}` |
|
||
| Rate limit exceeded | `429` with `Retry-After` header (see below) |
|
||
|
||
### `429 Too Many Requests`
|
||
|
||
Returned when the per-IP rate limiter rejects a request. `/health` and
|
||
`/metrics` are exempt.
|
||
|
||
**Response headers:**
|
||
|
||
```
|
||
Retry-After: 2
|
||
```
|
||
|
||
**Response body:**
|
||
|
||
```json
|
||
{"error": "Rate limit exceeded", "retry_after": 2}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|---------------|--------|------------------------------------------------|
|
||
| `error` | string | `"Rate limit exceeded"` |
|
||
| `retry_after` | number | Seconds until the client should retry |
|
||
|
||
---
|
||
|
||
## SSE Reconnection
|
||
|
||
The embedded JavaScript client implements exponential backoff for SSE
|
||
reconnection:
|
||
|
||
| Parameter | Value |
|
||
|--------------------|-------------------------------------------|
|
||
| Base delay | 1 second |
|
||
| Backoff multiplier | 2x on each consecutive failure |
|
||
| Maximum delay | 30 seconds |
|
||
| Reset | Delay resets to 1 second on first success |
|
||
|
||
On reconnect, the server replays the full conversation history via the
|
||
`history` event, so the client can rebuild its UI state without data loss. The
|
||
same reconnection strategy applies to both the per-workstream SSE stream
|
||
(`/v1/api/workstreams/{ws_id}/events`) and the global state stream (`/v1/api/events/global`).
|
||
|
||
---
|
||
|
||
## Observability
|
||
|
||
### `GET /health`
|
||
|
||
Returns server health status. Always returns `200 OK` while the server process
|
||
is running. `"status": "degraded"` indicates the server is up but the LLM
|
||
backend is unreachable. Suitable for load-balancer health checks and Kubernetes
|
||
liveness probes.
|
||
|
||
**Response:** `application/json`
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"version": "0.4.0",
|
||
"node_id": "worker-01_a3f2",
|
||
"uptime_seconds": 3614.72,
|
||
"model": "llama-3.1-70b-instruct",
|
||
"workstreams": {
|
||
"total": 2,
|
||
"idle": 1,
|
||
"thinking": 1,
|
||
"running": 0,
|
||
"attention": 0,
|
||
"error": 0
|
||
},
|
||
"backend": {
|
||
"status": "up",
|
||
"circuit_state": "closed"
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `status` | string | `"ok"` or `"degraded"` (degraded when backend unreachable) |
|
||
| `version` | string | turnstone server version |
|
||
| `node_id` | string | Server-generated node identity (`{hostname}_{4hex}`) |
|
||
| `uptime_seconds` | number | Seconds since the server process started |
|
||
| `model` | string | Model name detected or configured at startup |
|
||
| `workstreams.total` | integer | Total active workstreams |
|
||
| `workstreams.idle` | integer | Workstreams waiting for user input |
|
||
| `workstreams.thinking` | integer | Workstreams with LLM currently streaming |
|
||
| `workstreams.running` | integer | Workstreams executing tools |
|
||
| `workstreams.attention` | integer | Workstreams blocked on approval or plan review |
|
||
| `workstreams.error` | integer | Workstreams in error state |
|
||
| `backend.status` | string | `"up"` or `"down"` — LLM backend reachability |
|
||
| `backend.circuit_state` | string | `"closed"`, `"open"`, or `"half_open"` |
|
||
|
||
---
|
||
|
||
### `GET /metrics`
|
||
|
||
Returns operational metrics in **Prometheus text exposition format v0.0.4**.
|
||
Compatible with Prometheus `scrape_configs`, VictoriaMetrics, Grafana Agent,
|
||
and any other OpenMetrics-compatible collector.
|
||
|
||
**Response:** `text/plain; version=0.0.4; charset=utf-8`
|
||
|
||
#### Prometheus scrape config example
|
||
|
||
```yaml
|
||
scrape_configs:
|
||
- job_name: turnstone
|
||
static_configs:
|
||
- targets: ["localhost:8080"]
|
||
metrics_path: /metrics
|
||
```
|
||
|
||
#### Metrics reference
|
||
|
||
| Metric | Type | Labels | Description |
|
||
|--------|------|--------|-------------|
|
||
| `turnstone_build_info` | gauge | `version`, `model` | Always 1; carries version/model as labels |
|
||
| `turnstone_uptime_seconds` | gauge | — | Seconds since server start |
|
||
| `turnstone_workstreams_active_total` | gauge | — | Number of active workstreams |
|
||
| `turnstone_workstreams_by_state` | gauge | `state` | Workstream count per state (`idle`, `thinking`, `running`, `attention`, `error`) |
|
||
| `turnstone_http_requests_total` | counter | `method`, `endpoint`, `status_code` | Total HTTP requests handled |
|
||
| `turnstone_http_request_duration_seconds` | histogram | `method`, `endpoint` | Request latency distribution (11 buckets: 5ms–10s) |
|
||
| `turnstone_messages_sent_total` | counter | — | User messages dispatched to the AI |
|
||
| `turnstone_tokens_total` | counter | `type` | Tokens consumed (`type="prompt"` or `type="completion"`) |
|
||
| `turnstone_tool_calls_total` | counter | `tool` | Tool executions by name (e.g. `tool="bash"`) |
|
||
| `turnstone_errors_total` | counter | — | Errors reported by workstreams |
|
||
| `turnstone_context_window_used_ratio` | gauge | — | Last known fraction of context window in use (0.0–1.0) |
|
||
| `turnstone_sse_connections_active` | gauge | — | Number of open SSE connections |
|
||
| `turnstone_ratelimit_rejected_total` | counter | — | Requests rejected by the per-IP rate limiter |
|
||
| `turnstone_backend_up` | gauge | — | LLM backend reachability (1 = up, 0 = down) |
|
||
| `turnstone_circuit_state` | gauge | — | Circuit breaker state (0 = closed, 1 = open, 2 = half_open) |
|
||
| `turnstone_workstreams_evicted_total` | counter | — | Workstreams auto-evicted when at capacity |
|
||
|
||
#### Example output
|
||
|
||
```
|
||
# HELP turnstone_build_info Server version and model info
|
||
# TYPE turnstone_build_info gauge
|
||
turnstone_build_info{version="0.2.0",model="llama-3.1-70b-instruct"} 1
|
||
# HELP turnstone_uptime_seconds Server uptime in seconds
|
||
# TYPE turnstone_uptime_seconds gauge
|
||
turnstone_uptime_seconds 3614.72
|
||
# HELP turnstone_workstreams_active_total Number of active workstreams
|
||
# TYPE turnstone_workstreams_active_total gauge
|
||
turnstone_workstreams_active_total 1
|
||
# HELP turnstone_http_requests_total Total HTTP requests handled
|
||
# TYPE turnstone_http_requests_total counter
|
||
turnstone_http_requests_total{method="GET",endpoint="/health",status_code="200"} 42
|
||
turnstone_http_requests_total{method="GET",endpoint="/metrics",status_code="200"} 7
|
||
turnstone_http_requests_total{method="POST",endpoint="/v1/api/workstreams/{ws_id}/send",status_code="200"} 18
|
||
# HELP turnstone_tokens_total Total tokens consumed
|
||
# TYPE turnstone_tokens_total counter
|
||
turnstone_tokens_total{type="prompt"} 84320
|
||
turnstone_tokens_total{type="completion"} 12150
|
||
# HELP turnstone_tool_calls_total Total tool executions by name
|
||
# TYPE turnstone_tool_calls_total counter
|
||
turnstone_tool_calls_total{tool="bash"} 7
|
||
turnstone_tool_calls_total{tool="read_file"} 3
|
||
```
|
||
|
||
---
|
||
|
||
## Console Routing Proxy Endpoints
|
||
|
||
These endpoints are served by the console (`turnstone-console`) and proxy
|
||
requests to the correct server node via rendezvous (HRW) hashing over the
|
||
live service registry. In multi-node deployments, clients (SDK, channel
|
||
gateway) talk to the console instead of individual server nodes.
|
||
|
||
### `POST /v1/api/route/workstreams/new`
|
||
|
||
Create a workstream via rendezvous routing. The console generates the `ws_id`,
|
||
routes to the rendezvous-selected node, and includes `node_url` in the
|
||
response for direct SSE connections.
|
||
|
||
### `POST /v1/api/route/send`
|
||
|
||
Proxy a message to the workstream's assigned server node.
|
||
|
||
### `POST /v1/api/route/approve`
|
||
|
||
Proxy an approval response to the workstream's assigned server node.
|
||
|
||
### `POST /v1/api/route/cancel`
|
||
|
||
Cancel generation on a workstream.
|
||
|
||
### `POST /v1/api/route/command`
|
||
|
||
Send a slash command to a workstream.
|
||
|
||
### `POST /v1/api/route/plan`
|
||
|
||
Send plan review feedback to a workstream.
|
||
|
||
### `POST /v1/api/route/workstreams/close`
|
||
|
||
Close a workstream.
|
||
|
||
### `GET /v1/api/route?ws_id=X`
|
||
|
||
Look up which server node owns a workstream. Returns `{"node_url": "...", "node_id": "..."}`.
|
||
Used by channel adapters to open direct SSE connections to the correct server node.
|
||
|
||
### `GET /metrics` (Console)
|
||
|
||
Prometheus metrics for the console routing layer. Includes:
|
||
`turnstone_router_requests_total`, `turnstone_router_request_duration_seconds`,
|
||
`turnstone_router_membership_size`, `turnstone_router_refresh_total`.
|