* Quality overhaul: code tooling, CI/CD, architecture diagrams, UI redesign, and legacy cleanup - Add ruff (lint+format) and mypy (strict) with zero errors across 37 source files - Add GitHub Actions CI (lint, typecheck, test matrix 3.11/3.12/3.13) and PyPI publish workflow - Create 12 PlantUML architecture diagrams with PNG renders covering all subsystems - Refresh README and docs with badges, diagram links, and current descriptions - Refactor test_server_live.py with mock streaming helpers for deterministic CI testing - Update dependencies to current versions (openai>=2.24, httpx>=0.28, redis>=7.2) Console dashboard: - Move state indicators from top cards to fixed bottom status bar with cluster metrics - Replace flat 50-node list with hostname-prefix grouped nodes (expand/collapse, up to 1000) - Apply "Instrument Panel" visual redesign: IBM Plex Mono + Outfit fonts, warm amber accent, LED glow state indicators, deep charcoal surfaces, WCAG AA contrast compliance - Add render cache, stale indicator, active filter highlight, loading states Server web UI: - Apply matching Instrument Panel aesthetic for visual consistency with console - Fix branding (pcode → turnstone), extract inline styles to CSS classes - Rename pcode localStorage keys and history state to turnstone Legacy cleanup: - Remove persona-model-specific --persona flag and /persona slash command - Remove model_identity from chat_template_kwargs (vLLM-specific mechanism) - Refactor plan agent to use standard developer message instead of model_identity - Remove dead code (unused date/has_tools variables, noqa suppressions) * Fix CI typecheck: add mypy overrides for optional sympy/numpy imports The math sandbox optionally imports sympy and numpy at runtime (try/except ImportError). In CI these packages are not installed, so mypy raises import-not-found rather than import-untyped. Add mypy overrides to ignore missing imports for these optional dependencies. * Fix Copilot review findings: ARIA role, status bar cache, and pulse opacity - Change #node-table from role="tree" to role="list" and group elements from role="treeitem" to role="listitem" (proper ARIA semantics) - Include currentView and currentFilter.state in renderStatusBar cache key so active pill highlight updates when switching views - Align pulse animation to 0.35 opacity (already applied in CSS)
22 KiB
turnstone Web Server API Reference
Overview
See also: MQ Protocol diagram | Message Routing diagram | Redis Key Schema diagram
turnstone-server exposes a browser-based chat UI backed by a Python stdlib HTTP
server (socketserver.ThreadingMixIn + http.server.HTTPServer). The server
uses Server-Sent Events (SSE) 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.
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 /api/events?ws_id=<id>
Opens a Server-Sent Events stream scoped to a single workstream. The connection remains open indefinitely; the server pushes events as they occur.
Query 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
connected-- sent immediately on connect.
{
"type": "connected",
"model": "kappa_20b_131k",
"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.
history-- replays the full conversation history so the client can rebuild its UI.
{
"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 |
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).
{"type": "thinking_start"}
thinking_stop -- the spinner phase is over.
{"type": "thinking_stop"}
reasoning -- a chunk of chain-of-thought reasoning text.
{"type": "reasoning", "text": "Let me think about this..."}
content -- a chunk of the assistant's visible reply.
{"type": "content", "text": "Here is the answer: "}
stream_end -- the model has finished generating. The client should
finalize any in-progress assistant message.
{"type": "stream_end"}
tool_info -- one or more tool calls that were auto-approved (no user
action required).
{
"type": "tool_info",
"items": [
{
"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 /api/approve.
{
"type": "approve_request",
"items": [
{
"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 |
|---|---|---|
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_result -- output from a completed tool execution.
{"type": "tool_result", "name": "bash", "output": "file1.py\nfile2.py\n"}
status -- token usage statistics, sent after each model turn.
{
"type": "status",
"prompt_tokens": 1024,
"completion_tokens": 256,
"total_tokens": 1280,
"context_window": 131072,
"pct": 1.0,
"effort": "medium"
}
| 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) |
plan_review -- the model is proposing a plan and wants feedback. The
client must respond via POST /api/plan.
{"type": "plan_review", "content": "Step 1: ...\nStep 2: ..."}
info -- an informational message (e.g. command output).
{"type": "info", "message": "Session cleared."}
error -- an error message.
{"type": "error", "message": "Error: connection timed out"}
busy_error -- sent when a new message arrives while the model is already
processing.
{"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).
{"type": "clear_ui"}
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.
Generation mechanism
Each new SSE connection to a workstream increments an internal
_sse_generation counter. The previous SSE handler detects the generation
mismatch and exits its event loop, ensuring only one active SSE connection per
workstream at a time. The event queue is drained of stale events before the new
connection begins streaming.
GET /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:
{"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=500). 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 /api/events -- an SSE comment every 5 seconds.
GET /api/workstreams
Returns a list of all active workstreams.
Response:
{
"workstreams": [
{"id": "abc123", "name": "default", "state": "idle", "session_id": "a1b2c3d4e5f6"},
{"id": "def456", "name": "hacker-news", "state": "thinking", "session_id": "c5d6e7f8a9b0"}
]
}
Each workstream object:
| Field | Type | Description |
|---|---|---|
id |
string | Unique workstream routing identifier |
name |
string | Display name (alias if set, otherwise ws-xxxx) |
state |
string | Current state (see state values above) |
session_id |
string/null | Session ID of the workstream's ChatSession, used for deduplication against /api/sessions |
GET /api/sessions
Returns a list of saved sessions from the database, ordered by most recently updated.
Response:
{
"sessions": [
{
"session_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 session object:
| Field | Type | Description |
|---|---|---|
session_id |
string | Unique 12-char hex session identifier |
alias |
string/null | User-assigned short name |
title |
string/null | LLM-generated title |
created |
string | ISO timestamp of session creation |
updated |
string | ISO timestamp of last message |
message_count |
int | Number of messages in the session |
POST /api/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.
Request body:
{"message": "Explain how the server works", "ws_id": "abc123"}
| Field | Type | Required | Description |
|---|---|---|---|
message |
string | yes | The user's message text |
ws_id |
string | yes | Target workstream ID |
Response (success):
{"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.
{"status": "busy"}
Error responses:
| Status | Body | Condition |
|---|---|---|
| 400 | {"error": "Empty message"} |
Message is empty |
| 404 | {"error": "Unknown workstream"} |
ws_id not found |
POST /api/approve
Responds to a tool approval request. The SSE stream must have previously sent
an approve_request event for the given workstream.
Request body:
{"approved": true, "feedback": null, "always": false, "ws_id": "abc123"}
| 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 |
ws_id |
string | yes | Target workstream ID |
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:
{"status": "ok"}
Error: 404 with {"error": "Unknown workstream"} if ws_id is invalid.
POST /api/plan
Responds to a plan review dialog. The SSE stream must have previously sent a
plan_review event for the given workstream.
Request body:
{"feedback": "", "ws_id": "abc123"}
| Field | Type | Required | Description |
|---|---|---|---|
feedback |
string | yes | Feedback text; empty string means approval |
ws_id |
string | yes | Target workstream ID |
To approve the plan, send an empty string for feedback. To reject or request
changes, send a non-empty feedback string (e.g. "reject" or specific
revision instructions).
Response:
{"status": "ok"}
Error: 404 with {"error": "Unknown workstream"} if ws_id is invalid.
POST /api/command
Executes a slash command in the given workstream.
Request body:
{"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:
{"status": "ok"}
Error responses:
| Status | Body | Condition |
|---|---|---|
| 400 | {"error": "Empty command"} |
Command is empty |
| 404 | {"error": "Unknown workstream"} |
ws_id not found |
POST /api/workstreams/new
Creates a new workstream. The server supports up to 10 concurrent workstreams.
Request body:
{}
No fields are required. The body can be empty or an empty JSON object.
Response (success):
{"ws_id": "ghi789", "name": "ws-3"}
| Field | Type | Description |
|---|---|---|
ws_id |
string | Unique ID of the new workstream |
name |
string | Auto-generated workstream name |
Error (limit reached):
{"error": "Maximum of 10 workstreams reached"}
Status code: 400
POST /api/workstreams/close
Closes and removes a workstream. The last remaining workstream cannot be closed.
Request body:
{"ws_id": "abc123"}
| Field | Type | Required | Description |
|---|---|---|---|
ws_id |
string | yes | Workstream ID to close |
Response (success):
{"status": "ok"}
Error (last workstream):
{"error": "Cannot close last workstream"}
Status code: 400
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 /api/send |
400 with {"error": "Empty message"} |
Empty command on /api/command |
400 with {"error": "Empty command"} |
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
(/api/events) and the global state stream (/api/events/global).
Observability
GET /health
Returns server health status. Always returns 200 OK while the server process
is running. Suitable for load-balancer health checks and Kubernetes liveness
probes.
Response: application/json
{
"status": "ok",
"version": "0.2.0",
"uptime_seconds": 3614.72,
"model": "llama-3.1-70b-instruct",
"workstreams": {
"total": 2,
"idle": 1,
"thinking": 1,
"running": 0,
"attention": 0,
"error": 0
}
}
| Field | Type | Description |
|---|---|---|
status |
string | Always "ok" while server is running |
version |
string | turnstone server version |
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 |
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
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) |
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="/api/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