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.
408 lines
13 KiB
Markdown
408 lines
13 KiB
Markdown
# System Settings
|
|
|
|
> See also: [Settings Architecture diagram](diagrams/png/24-settings-architecture.png)
|
|
|
|
The system settings feature provides database-backed configuration for server
|
|
nodes. Settings are stored in the `system_settings` table and managed through
|
|
the admin API or console Settings tab. This replaces `config.toml` for
|
|
non-bootstrap settings on server entry points, while the CLI continues to read
|
|
`config.toml` directly.
|
|
|
|
## Overview
|
|
|
|
Settings follow a typed registry pattern: every storable setting has a
|
|
`SettingDef` entry in `settings_registry.py` with type, default, description,
|
|
validation constraints, and a `restart_required` flag. Unknown keys are rejected
|
|
at the API boundary.
|
|
|
|
At runtime, `ConfigStore` loads all settings from storage into an in-memory
|
|
cache. Reads are lock-free dict lookups on an immutable snapshot. Writes acquire
|
|
a lock, persist to storage, and swap the cache atomically.
|
|
|
|
---
|
|
|
|
## Precedence
|
|
|
|
Settings resolution differs between entry points:
|
|
|
|
| Entry point | Chain |
|
|
|-------------|-------|
|
|
| **Server** (`turnstone-server`) | CLI flag > ConfigStore > registry default |
|
|
| **CLI** (`turnstone`) | CLI flag > config.toml > argparse default |
|
|
|
|
The server's `apply_config()` ignores config.toml sections that overlap with
|
|
ConfigStore. A startup warning is logged for each overlapping key, directing
|
|
users to the admin Settings API.
|
|
|
|
---
|
|
|
|
## Per-Model Sampling Overrides
|
|
|
|
The global `model.temperature`, `model.max_tokens`, and `model.reasoning_effort`
|
|
settings serve as cluster-wide defaults. Individual models can override these
|
|
via per-model settings in the `model_definitions` table (admin Models tab).
|
|
|
|
Resolution order for sampling parameters:
|
|
|
|
| Priority | Source |
|
|
|----------|--------|
|
|
| 1 (highest) | Per-model override (set in Models tab) |
|
|
| 2 | Global default (set in Settings tab) |
|
|
| 3 | Registry default (code) |
|
|
|
|
When a per-model override is `NULL` (empty in the UI), the global default is
|
|
used. Switching models via `/model <alias>` re-resolves sampling parameters
|
|
from the new model's overrides or global defaults.
|
|
|
|
**Removed settings:** `model.name` and `model.context_window` have been removed
|
|
from ConfigStore. Model names and context windows are now configured per-model
|
|
in the Models tab. A startup warning is logged if these keys appear in
|
|
`config.toml`.
|
|
|
|
### Reasoning persistence (per-model)
|
|
|
|
Two boolean flags on `model_definitions` (migration 052) control how
|
|
reasoning text round-trips per model:
|
|
|
|
| Flag | Default | Effect |
|
|
|------|---------|--------|
|
|
| `surface_persisted_reasoning` | `True` | Surface stored reasoning text on `/history` payloads so a page reload re-renders the reasoning bubble. **Storage of reasoning bytes is independent of this flag** — they ride in `provider_data` regardless. |
|
|
| `replay_reasoning_to_model` | `False` | Send stored reasoning blocks back to the provider on subsequent turns. Capability-gated: only takes effect when the model's `ModelCapabilities.supports_reasoning_replay` is also `True`. Set on canonical OpenAI gpt-5*/o-series and Anthropic Claude entries; unknown / local-server models default to `False` so an operator who flips the flag on a model whose API doesn't understand reasoning replay silently no-ops rather than 400-ing. |
|
|
|
|
Edit both via the admin Models tab. See the architecture doc for the
|
|
provider-side mechanics (Anthropic `thinking`, OpenAI Responses
|
|
`reasoning` + `include=["reasoning.encrypted_content"]`, synthetic
|
|
`reasoning_text` for Chat Completions / vLLM / llama.cpp / Gemini-compat).
|
|
|
|
### Task agent overrides
|
|
|
|
`task_agent` sub-sessions resolve independently from the conversation model
|
|
so operators can pick a cheaper/faster model for autonomous loops:
|
|
|
|
| Setting | Purpose |
|
|
|---------|---------|
|
|
| `model.task_alias` | Alias used for `task_agent` sub-sessions. Falls back to `[model].agent_model` in config.toml, then the session's active model. |
|
|
| `model.task_effort` | Reasoning effort for `task_agent`. Empty string means "inherit from the session". |
|
|
|
|
Both are live-editable from the Settings tab and take effect on the
|
|
next sub-agent invocation — no restart required.
|
|
|
|
---
|
|
|
|
## Bootstrap vs ConfigStore
|
|
|
|
**Bootstrap settings** are required before storage is available (database
|
|
connection, Redis, auth secrets, server bind address). These stay in
|
|
`config.toml` and environment variables.
|
|
|
|
| Category | Section | Where |
|
|
|----------|---------|-------|
|
|
| API credentials | `[api]` | config.toml / env |
|
|
| Database | `[database]` | config.toml / env |
|
|
| Auth | `[auth]` | config.toml / env |
|
|
| Console bind | `[console]` | config.toml / env |
|
|
|
|
**ConfigStore settings** are loaded from the database after storage
|
|
initialization:
|
|
|
|
| Section | Settings |
|
|
|---------|----------|
|
|
| `model` | default_alias, temperature, max_tokens, reasoning_effort, task_alias, task_effort |
|
|
| `session` | instructions, retention_days, compact_max_tokens, auto_compact_pct |
|
|
| `tools` | timeout, truncation, agent_max_turns, skip_permissions, search, search_threshold, search_max_results |
|
|
| `server` | workstream_idle_timeout, max_workstreams |
|
|
| `cluster` | node_fan_out_limit, mcp_max_servers |
|
|
| `mcp` | config_path, 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, cancel_on_approval |
|
|
| `interface` | close_tab_action, theme |
|
|
| `skills` | discovery_url |
|
|
| `memory` | relevance_k, fetch_limit, max_content, nudge_cooldown, nudges |
|
|
|
|
Settings are addressed by dotted key (e.g. `memory.relevance_k`). Each has a
|
|
declared type (`int`, `float`, `str`, `bool`), optional `min_value`/`max_value`
|
|
range, optional `choices` list, and an `is_secret` flag.
|
|
|
|
---
|
|
|
|
## Storage
|
|
|
|
The `system_settings` table (migration 015) stores settings as JSON-encoded
|
|
values with a composite primary key of `(key, node_id)`:
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| `key` | text | Dotted setting key (e.g. `model.temperature`) |
|
|
| `value` | text | JSON-encoded value |
|
|
| `node_id` | text | Node ID for per-node overrides (empty string = global) |
|
|
| `is_secret` | int | 1 if the setting contains secrets |
|
|
| `changed_by` | text | Username of last editor |
|
|
| `created` | text | ISO timestamp |
|
|
| `updated` | text | ISO timestamp |
|
|
|
|
Per-node overrides layer on top of global settings. When `ConfigStore` loads,
|
|
it fetches global settings first, then overlays per-node values.
|
|
|
|
---
|
|
|
|
## Admin API
|
|
|
|
Four endpoints on the **console** server, all requiring the `admin.settings`
|
|
permission.
|
|
|
|
### `GET /v1/api/admin/settings`
|
|
|
|
List all settings with their effective values, defaults, and metadata.
|
|
|
|
**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`
|
|
|
|
Return the full registry catalog (all defined settings with metadata). 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}`
|
|
|
|
Update a setting. The value is validated against the registry (type coercion,
|
|
range, choices). Secret settings (`is_secret=true`) cannot be written via the
|
|
API -- they must be configured via config.toml or environment variables.
|
|
|
|
**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 |
|
|
| 403 | Secret setting (must use config.toml or env) |
|
|
|
|
---
|
|
|
|
### `DELETE /v1/api/admin/settings/{key}`
|
|
|
|
Reset a setting to its registry default by removing it from storage.
|
|
|
|
**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"}
|
|
```
|
|
|
|
---
|
|
|
|
## Secret Settings
|
|
|
|
Settings with `is_secret=True` (currently only `judge.api_key`) are blocked
|
|
from the write API with a `403` response. This prevents accidental exposure
|
|
through the admin UI or audit logs. Secret settings must be configured via
|
|
`config.toml` or environment variables.
|
|
|
|
The list endpoint masks secret values: stored secrets appear as `"***"`
|
|
rather than their actual value.
|
|
|
|
---
|
|
|
|
## Hot Reload
|
|
|
|
`ConfigStore` caches all settings in memory for fast, lock-free reads. To
|
|
refresh the cache after external changes (e.g. direct database edits or
|
|
cluster-wide propagation):
|
|
|
|
```
|
|
POST /v1/api/_internal/config-reload
|
|
```
|
|
|
|
This triggers `ConfigStore.reload()`, which re-reads all settings from storage
|
|
and atomically swaps the cache. The `version` counter increments on every
|
|
reload.
|
|
|
|
**Behavior after reload:**
|
|
|
|
- New workstreams pick up updated values immediately (via `session_factory`)
|
|
- Existing sessions keep their frozen configuration (settings are captured at
|
|
workstream creation time, not read on every turn)
|
|
- Settings marked `restart_required=True` need a server restart to take effect
|
|
|
|
---
|
|
|
|
## Migration from config.toml
|
|
|
|
On startup, `warn_migrated_settings()` scans `config.toml` for keys that are
|
|
now managed by ConfigStore. Each overlap produces a warning:
|
|
|
|
```
|
|
WARNING config.toml [model] temperature is now managed via Settings API —
|
|
this value will be ignored. Use the admin Settings tab or
|
|
PUT /v1/api/admin/settings/model.temperature to configure.
|
|
```
|
|
|
|
To migrate:
|
|
|
|
1. Note the values from `config.toml` for sections that overlap with ConfigStore
|
|
2. Use `PUT /v1/api/admin/settings/{key}` or the console Settings tab to set
|
|
each value
|
|
3. Remove the migrated sections from `config.toml`
|
|
4. Restart the server to verify no warnings
|
|
|
|
---
|
|
|
|
## SDK
|
|
|
|
### Python
|
|
|
|
```python
|
|
from turnstone.sdk import TurnstoneConsole
|
|
|
|
with TurnstoneConsole("http://localhost:9090", token="tok_xxx") as admin:
|
|
# List all settings with effective values
|
|
result = admin.list_settings()
|
|
for s in result["settings"]:
|
|
print(f"{s['key']} = {s['value']} (source: {s['source']})")
|
|
|
|
# Get the schema catalog
|
|
schema = admin.get_settings_schema()
|
|
|
|
# Update a setting
|
|
admin.update_setting("model.temperature", value=0.7)
|
|
|
|
# Update with per-node override
|
|
admin.update_setting("model.temperature", value=0.3, node_id="node-2")
|
|
|
|
# Reset to default
|
|
admin.delete_setting("model.temperature")
|
|
```
|
|
|
|
### TypeScript
|
|
|
|
```typescript
|
|
import { TurnstoneConsole } from "@turnstone/sdk";
|
|
|
|
const admin = new TurnstoneConsole({
|
|
baseUrl: "http://localhost:9090",
|
|
token: "tok_xxx",
|
|
});
|
|
|
|
// List all settings
|
|
const result = await admin.listSettings();
|
|
for (const s of result.settings) {
|
|
console.log(`${s.key} = ${s.value} (source: ${s.source})`);
|
|
}
|
|
|
|
// Get schema catalog
|
|
const schema = await admin.getSettingsSchema();
|
|
|
|
// Update a setting
|
|
await admin.updateSetting("model.temperature", { value: 0.7 });
|
|
|
|
// Reset to default
|
|
await admin.deleteSetting("model.temperature");
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
See [Settings Architecture diagram](diagrams/png/24-settings-architecture.png)
|
|
for the full data flow covering server startup, admin API writes, hot reload,
|
|
and settings precedence.
|