From a3140da3a512069642240523222181c8fc88aee8 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Mon, 6 Apr 2026 03:43:12 -0700 Subject: [PATCH] docs: update documentation for PRs #312-#316 (#324) - README: add Google Gemini to multi-provider feature list and requirements - architecture.md: add GoogleProvider, update supported provider values, file listing, config example - judge.md: document cancel_on_approval, fresh-client lifecycle, fallback delivery, Google compatibility - settings.md: add judge.cancel_on_approval, new interface.* section (close_tab_action, theme), update total count - api-reference.md: document 6 new workstream/settings endpoints, add judge_model to workstreams/new - console.md: add judge model to modal fields, add keyboard shortcuts - console_schemas.py: add judge_model field to ConsoleCreateWsRequest - server_spec.py: add 6 new EndpointSpec entries - diagrams: add GoogleProvider to package structure and class diagram --- README.md | 4 +- docs/api-reference.md | 156 ++++++++++++++++++++++ docs/architecture.md | 16 ++- docs/console.md | 3 + docs/diagrams/02-package-structure.puml | 2 +- docs/diagrams/03-core-engine-classes.puml | 13 ++ docs/judge.md | 12 ++ docs/settings.md | 5 +- turnstone/api/console_schemas.py | 3 + turnstone/api/server_spec.py | 49 +++++++ 10 files changed, 257 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 696d6434..0fafbdfd 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Turnstone gives LLMs tools — shell, files, search, web, planning — and orche - **Cluster dashboard** — real-time view of all nodes and workstreams with console routing proxy - **Intent validation** — LLM judge evaluates every tool call with risk assessments and evidence - **Governance** — RBAC, OIDC SSO, tool policies, skills, usage tracking, audit logs -- **Multi-provider** — OpenAI-compatible APIs (vLLM, llama.cpp, NIM) and Anthropic Messages API +- **Multi-provider** — OpenAI-compatible APIs (vLLM, llama.cpp, NIM), Anthropic Messages API, and Google Gemini - **MCP support** — external tool servers with native deferred loading (Anthropic/OpenAI) or BM25 fallback

@@ -132,7 +132,7 @@ UML diagrams in [`docs/diagrams/`](docs/diagrams/): ## Requirements - Python 3.11+ -- An OpenAI-compatible API endpoint or Anthropic API key +- An OpenAI-compatible API endpoint, Anthropic API key, or Google Gemini API key - Optional: PostgreSQL (`pip install turnstone[postgres]`), Anthropic (`pip install turnstone[anthropic]`) - [Git LFS](https://git-lfs.com/) for cloning (diagram PNGs) diff --git a/docs/api-reference.md b/docs/api-reference.md index 034f216a..f0d6a8df 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -857,6 +857,7 @@ All fields are optional. The body can be empty or an empty JSON object. | `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. @@ -914,6 +915,161 @@ Status code: `400` --- +### `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. diff --git a/docs/architecture.md b/docs/architecture.md index 43bc0f4c..1f342377 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -38,6 +38,7 @@ turnstone/ _protocol.py LLMProvider protocol, ModelCapabilities, StreamChunk, CompletionResult _openai.py OpenAIProvider — OpenAI, vLLM, llama.cpp, any compatible API _anthropic.py AnthropicProvider — Anthropic Messages API, native streaming, thinking + _google.py GoogleProvider — Google Gemini via OpenAI-compat endpoint __init__.py create_provider() + create_client() factory functions workstream.py Parallel workstream manager (WorkstreamState, Workstream, WorkstreamManager) tools.py Tool schema loader (JSON -> OpenAI function-calling format) @@ -593,6 +594,7 @@ LLMProvider (protocol) | +--- OpenAIProvider --- OpenAI, vLLM, llama.cpp, any /v1/chat/completions API +--- AnthropicProvider --- Anthropic Messages API (native streaming, thinking) + +--- GoogleProvider --- Google Gemini via /v1beta/openai/ (extends OpenAIProvider) ``` **Protocol methods:** @@ -646,6 +648,13 @@ both streaming and non-streaming responses. The `anthropic` SDK is imported lazily so it remains an optional dependency (`pip install turnstone[anthropic]`). +**GoogleProvider** (`_google.py`): extends `OpenAIChatCompletionsProvider` for +the Gemini `/v1beta/openai/` endpoint. Uses a single default +`ModelCapabilities` (2M context window, 65K max output tokens, +`token_param=max_tokens`) since Google updates models frequently. No static +per-model capability table. Google's endpoint is wire-compatible with the +OpenAI SDK, so no extra dependency is needed. + **Factory functions** (`__init__.py`): `create_provider(name)` returns a singleton provider instance (thread-safe). `create_client(name, base_url, api_key)` creates the appropriate SDK client. @@ -674,6 +683,10 @@ api_key = "sk-..." model = "gpt-5" context_window = 400000 +[models.gemini] +provider = "google" +model = "gemini-2.5-pro" + [model] default = "local" fallback = ["claude", "openai"] @@ -681,7 +694,8 @@ agent_model = "claude" ``` Each `[models.*]` entry produces a `ModelConfig` with a `provider` field -(default: `"openai"`). Supported values: `"openai"` and `"anthropic"`. +(default: `"openai"`). Supported values: `"openai"`, `"anthropic"`, `"google"`, +and `"openai-compatible"`. An optional `[models.*.capabilities]` sub-table overrides per-model `ModelCapabilities` flags (useful for local models whose capabilities cannot be detected programmatically): diff --git a/docs/console.md b/docs/console.md index 0f347237..d1b4c367 100644 --- a/docs/console.md +++ b/docs/console.md @@ -382,6 +382,9 @@ Triggered by the "+ new" header button. A modal dialog with: - **Profile** — optional dropdown listing enabled skills. Applies the skill's model, auto-approve policy, token budget, and other behavioral settings at creation time. - **Name** — optional text input. Auto-generated if left empty. - **Model** — optional text input for a model alias from the target node's registry. +- **Judge Model** — optional text input for the judge model alias (overrides the default judge model for this workstream). + +Keyboard shortcuts: Ctrl+Shift+R (refresh title), Ctrl+Shift+E (edit title), Ctrl+Shift+F (fork), Ctrl+Shift+X (delete). Press ? for full shortcut help. On submit, `POST /v1/api/cluster/workstreams/new` dispatches the creation request. A toast confirms success; the SSE stream delivers the `ws_created` event to update the dashboard. diff --git a/docs/diagrams/02-package-structure.puml b/docs/diagrams/02-package-structure.puml index c053952c..51d59551 100644 --- a/docs/diagrams/02-package-structure.puml +++ b/docs/diagrams/02-package-structure.puml @@ -25,7 +25,7 @@ package "Entry Points" <> { ' Core engine package "turnstone/core/" <> { component [session.py\nChatSession, SessionUI] as session <> - component [providers/\nLLMProvider, OpenAI, Anthropic] as providers <> + component [providers/\nLLMProvider, OpenAI, Anthropic, Google] as providers <> component [workstream.py\nWorkstreamManager] as workstream <> component [tools.py\nTool loader] as tools <> component [memory.py\nPersistence facade] as memory <> diff --git a/docs/diagrams/03-core-engine-classes.puml b/docs/diagrams/03-core-engine-classes.puml index 4c92ba15..d80fe7e0 100644 --- a/docs/diagrams/03-core-engine-classes.puml +++ b/docs/diagrams/03-core-engine-classes.puml @@ -103,6 +103,18 @@ class "AnthropicProvider" as AnthropicProv { core/providers/_anthropic.py } +class "GoogleProvider" as GoogleProv { + + provider_name: str + + get_capabilities(model) -> ModelCapabilities + -- + Extends OpenAIChatCompletionsProvider + for Gemini /v1beta/openai/ endpoint. + Single default ModelCapabilities + (2M context, 65K output). + -- + core/providers/_google.py +} + ' ModelCapabilities class "ModelCapabilities" as ModelCaps <> { + context_window: int @@ -360,6 +372,7 @@ SessionUI <|.. NullUI LLMProvider <|.. OpenAIProv LLMProvider <|.. AnthropicProv +OpenAIProv <|-- GoogleProv ChatSession --> SessionUI : uses ChatSession --> LLMProvider : delegates LLM calls diff --git a/docs/judge.md b/docs/judge.md index 137e7120..fa9a5583 100644 --- a/docs/judge.md +++ b/docs/judge.md @@ -41,6 +41,7 @@ confidence_threshold = 0.7 # reserved for v2 smart approvals (not used in v1) max_context_ratio = 0.5 # max % of judge context window for history timeout = 60.0 # seconds (generous for local models) read_only_tools = true # judge can use read_file/list_directory +cancel_on_approval = false # stop judging remaining tool calls once user decides ``` All fields are optional. The judge is enabled by default; use `enabled = false` @@ -72,6 +73,17 @@ CLI flags override `config.toml` values. - **Cross-provider**: When both `model` and `provider` are set, the judge creates its own LLM client. You can optionally specify `base_url` and `api_key` for non-default endpoints. +- **Google models**: The judge supports `google` as a provider. Note that + read-only tools are disabled for Google models (the Gemini API requires + `thought_signature` in tool call round-trips which the judge's normalized + format does not preserve). + +The judge creates a fresh HTTP client for each evaluation run and closes it +when done, avoiding stale connection issues across runs. + +If the LLM judge fails or returns no verdict, a fallback verdict with tier +`llm_fallback` is delivered via the callback, ensuring the UI always receives +a result. --- diff --git a/docs/settings.md b/docs/settings.md index 0f6f4473..fc6005f6 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -49,7 +49,7 @@ connection, Redis, auth secrets, server bind address). These stay in | Auth | `[auth]` | config.toml / env | | Console bind | `[console]` | config.toml / env | -**ConfigStore settings** (48 settings) are loaded from the database after +**ConfigStore settings** (51 settings) are loaded from the database after storage initialization: | Section | Settings | @@ -62,7 +62,8 @@ storage initialization: | `mcp` | config_path, refresh_interval, registry_url | | `ratelimit` | enabled, requests_per_second, burst, trusted_proxies | | `health` | backend_probe_interval, backend_probe_timeout, circuit_breaker_threshold, circuit_breaker_cooldown | -| `judge` | enabled, model, provider, base_url, api_key, confidence_threshold, max_context_ratio, timeout, read_only_tools, output_guard, redact_secrets | +| `judge` | enabled, model, provider, base_url, api_key, confidence_threshold, max_context_ratio, timeout, read_only_tools, output_guard, redact_secrets, cancel_on_approval | +| `interface` | close_tab_action, theme | | `skills` | discovery_url | | `memory` | relevance_k, fetch_limit, max_content, nudge_cooldown, nudges | diff --git a/turnstone/api/console_schemas.py b/turnstone/api/console_schemas.py index 621db941..3469c47a 100644 --- a/turnstone/api/console_schemas.py +++ b/turnstone/api/console_schemas.py @@ -147,6 +147,9 @@ class ConsoleCreateWsRequest(BaseModel): resume_ws: str = Field( default="", description="Workstream ID to resume (loads previous conversation)" ) + judge_model: str = Field( + default="", description="Override judge model alias for this workstream" + ) class ConsoleCreateWsResponse(BaseModel): diff --git a/turnstone/api/server_spec.py b/turnstone/api/server_spec.py index 0279facb..0acaf67a 100644 --- a/turnstone/api/server_spec.py +++ b/turnstone/api/server_spec.py @@ -144,6 +144,34 @@ SERVER_ENDPOINTS: list[EndpointSpec] = [ "Pass ?expected_node_id=X for identity verification (returns 409 on mismatch).", tags=["Streaming"], ), + EndpointSpec( + "/v1/api/workstreams/{ws_id}/delete", + "POST", + "Permanently delete a saved workstream", + error_codes=[400, 404, 500], + tags=["Workstreams"], + ), + EndpointSpec( + "/v1/api/workstreams/{ws_id}/open", + "POST", + "Load a saved workstream into memory", + error_codes=[400, 404, 500], + tags=["Workstreams"], + ), + EndpointSpec( + "/v1/api/workstreams/{ws_id}/title", + "POST", + "Set workstream title manually", + error_codes=[400, 409], + tags=["Workstreams"], + ), + EndpointSpec( + "/v1/api/workstreams/{ws_id}/refresh-title", + "POST", + "Regenerate workstream title via LLM", + error_codes=[404], + tags=["Workstreams"], + ), # --- Saved workstreams --- EndpointSpec( "/v1/api/workstreams/saved", @@ -269,6 +297,27 @@ SERVER_ENDPOINTS: list[EndpointSpec] = [ error_codes=[404], tags=["Memories"], ), + # --- Admin settings --- + EndpointSpec( + "/v1/api/admin/settings", + "GET", + "List interface.* settings with values and sources", + tags=["Admin"], + ), + EndpointSpec( + "/v1/api/admin/settings/{key}", + "PUT", + "Update an interface.* setting", + error_codes=[400, 503], + tags=["Admin"], + ), + EndpointSpec( + "/v1/api/admin/settings/{key}", + "POST", + "Update an interface.* setting (alias for PUT)", + error_codes=[400, 503], + tags=["Admin"], + ), # --- Observability --- EndpointSpec( "/health",