Files
turnstone/docs/diagrams/03-core-engine-classes.puml
T
Patrick Buckley 110d44b07e refactor(tools): remove man, math, and plan_agent built-in tools
`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.
2026-05-31 19:54:43 -07:00

411 lines
10 KiB
Plaintext

@startuml
!theme plain
title Turnstone — Core Engine Classes
skinparam classAttributeIconSize 0
' SessionUI Protocol
interface "SessionUI" as SessionUI <<Protocol>> {
+ on_thinking_start()
+ on_thinking_stop()
+ on_reasoning_token(text: str)
+ on_content_token(text: str)
+ on_stream_end()
+ approve_tools(items: list) → (bool, str|None)
+ on_tool_result(call_id: str, name: str, output: str, *, is_error: bool = False)
+ on_tool_output_chunk(call_id: str, chunk: str)
+ on_status(usage: dict, ctx_window: int, effort: str)
+ on_info(message: str)
+ on_error(message: str)
+ on_state_change(state: str)
+ on_rename(name: str)
}
' Implementations
class "TerminalUI" as TerminalUI {
Writes to stdout with ANSI colors
Prompts for approval via input()
--
cli.py
}
class "WorkstreamTerminalUI" as WsTermUI {
- _output_buffer: list[tuple]
- ws_id: str
- manager: WorkstreamManager
+ flush_buffer()
--
Buffers output when workstream
is not foregrounded
}
class "WebUI" as WebUI {
- _listeners: list[Queue]
- _approval_event: Event
- _ws_prompt_tokens: int
- _ws_tool_calls: dict
+ resolve_approval(approved, feedback)
--
Enqueues JSON events for SSE.
Blocks on threading.Event for
approval.
SSE handlers bridge Queue to
async via run_in_executor().
--
server.py
}
class "NullUI" as NullUI {
approve_tools() → (True, None)
All other methods: no-op
--
eval.py
}
' LLMProvider Protocol
interface "LLMProvider" as LLMProvider <<Protocol>> {
+ provider_name: str {property}
+ get_capabilities(model) → ModelCapabilities
+ create_streaming(client, model, messages, ..., replay_reasoning_to_model) → Iterator[StreamChunk]
+ create_completion(client, model, messages, ..., replay_reasoning_to_model) → CompletionResult
+ convert_tools(tools) → list[dict]
+ extract_reasoning_text(provider_blocks) → str
+ retryable_error_names: frozenset[str] {property}
--
core/providers/_protocol.py
}
class "OpenAIProvider" as OpenAIProv {
Model capability lookup table
(GPT-5.x, O-series, search)
Passthrough: messages already
in OpenAI format.
Search models: web_search_options
+ url_citation annotations.
Extended cache: 24h retention
for GPT-5.x (free).
--
core/providers/_openai.py
}
class "AnthropicProvider" as AnthropicProv {
Converts OpenAI messages to
Anthropic content blocks.
Adaptive + manual thinking.
Native web search via
web_search_20250305 server tool.
Auto prompt caching via
cache_control: ephemeral.
Lazy anthropic SDK import.
--
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 <<frozen>> {
+ context_window: int
+ max_output_tokens: int
+ supports_temperature: bool
+ token_param: str
+ thinking_mode: str
+ supports_effort: bool
+ supports_web_search: bool
+ supports_tool_search: bool
+ supports_vision: bool
+ supports_reasoning_replay: bool
}
' ChatSession
class "ChatSession" as ChatSession {
- client: Any
- provider: LLMProvider
- model: str
- ui: SessionUI
- messages: list[dict]
- _msg_tokens: list[int]
- _ws_id: str
- _mcp_client: MCPClientManager | None
- _tool_search: ToolSearchManager | None
- _registry: ModelRegistry | None
+ model_alias: str | None {property}
- _tools: list[dict]
- _task_tools: list[dict]
- _read_files: set[str]
- system_messages: list[dict]
--
+ send(user_input: str)
+ handle_command(command: str)
+ resume(ws_id: str)
- _save_config()
- _stream_response(stream) → dict
- _create_stream_with_retry(msgs) → Stream (+ fallback)
- _try_stream(client, model, msgs) → Stream
- _execute_tools(tool_calls) → (results, feedback)
- _prepare_tool(tc) → item dict
- _prepare_mcp_tool(call_id, name, args) → item dict
- _exec_mcp_tool(item) → (call_id, output)
- _get_active_tools() → list[dict]
- _prepare_tool_search() → None
- _exec_tool_search(item) → (call_id, output)
- _on_mcp_tools_changed()
- _rebuild_tool_search()
+ close()
- _run_agent(messages, tools, ...) → str
- _compact_messages(auto: bool)
- _full_messages() → list[dict]
- _update_token_table(msg)
- _emit_state(state: str)
- _generate_title()
}
' HeadlessSession
class "HeadlessSession" as HeadlessSession {
+ tool_call_log: list[dict]
+ auto_approve: bool = True
+ send_headless(input, max_turns, ...)
- _override_system_prompt(content)
--
eval.py: non-streaming,
records all tool calls
}
' WorkstreamManager
class "WorkstreamManager" as WsMgr {
- _session_factory: Callable[[SessionUI], ChatSession]
- _workstreams: dict[str, Workstream]
- _order: list[str]
- _active_id: str
- _on_state_change: Callable
--
+ create(name, ui_factory) → Workstream
+ close(ws_id)
+ get(ws_id) → Workstream
+ get_active() → Workstream
+ switch(ws_id)
+ set_state(ws_id, state)
+ close_idle(max_age_seconds)
- _max_workstreams: int
}
' Workstream
class "Workstream" as Ws <<dataclass>> {
+ id: str
+ name: str
+ state: WorkstreamState
+ session: ChatSession
+ ui: SessionUI
+ worker_thread: Thread
+ error_message: str
+ last_active: float
- _lock: Lock
}
' WorkstreamState
enum "WorkstreamState" as WsState {
IDLE
THINKING
RUNNING
ATTENTION
ERROR
}
' MCPClientManager
class "MCPClientManager" as MCPMgr {
- _sessions: dict[str, ClientSession]
- _per_server_tools: dict[str, list[dict]]
- _per_server_resources: dict[str, list[dict]]
- _per_server_prompts: dict[str, list[dict]]
- _tools: list[dict]
- _tool_map: dict[str, tuple]
- _resource_map: dict[str, tuple]
- _prompt_map: dict[str, tuple]
- _supports_list_changed: dict[str, bool]
- _listeners: list[Callable]
--
+ start()
+ get_tools() → list[dict]
+ get_resources() → list[dict]
+ get_prompts() → list[dict]
+ is_mcp_tool(name) → bool
+ call_tool_sync(name, args) → str
+ read_resource_sync(uri) → str
+ get_prompt_sync(name, args?) → list[dict]
+ refresh_sync(server?) → dict
+ add_listener(callback)
+ remove_listener(callback)
+ server_names: list[str] {property}
+ shutdown()
--
Background asyncio event loop
bridges async MCP SDK to
sync ChatSession dispatch.
Push + manual refresh.
Resources + prompts discovered
alongside tools at startup.
--
core/mcp_client.py
}
' ToolSearchManager
class "ToolSearchManager" as ToolSearchMgr {
- _always_on: list[dict]
- _deferred: list[dict]
- _expanded: dict[str, None]
- _index: BM25Index
--
+ get_visible_tools() → list[dict]
+ get_deferred_tools() → list[dict]
+ get_expanded_names() → list[str]
+ search(query, k) → list[dict]
+ expand_visible(names) → list[dict]
+ get_search_tool_definition() → dict
+ format_search_results(tools) → str
}
' ModelRegistry
class "ModelRegistry" as ModelReg {
- _models: dict[str, ModelConfig]
- _clients: dict[str, Any]
- _providers: dict[str, LLMProvider]
- _client_lock: Lock
+ default: str
+ fallback: list[str]
+ agent_model: str | None
--
+ resolve(alias) → (client, model, config)
+ get_client(alias) → Any
+ get_provider(alias) → LLMProvider
+ has_alias(alias) → bool
+ list_aliases() → list[str]
+ shutdown()
--
Thread-safe lazy client + provider
creation. Loaded by load_model_registry()
from DB + [models.*] config + CLI args.
--
core/model_registry.py
}
class "ModelConfig" as ModelCfg <<frozen>> {
+ alias: str
+ provider: str
+ base_url: str
+ model: str
+ context_window: int
+ temperature: float | None
+ max_tokens: int | None
+ reasoning_effort: str | None
}
' Circuit breaker state
enum "CircuitState" as CircuitState {
CLOSED
OPEN
HALF_OPEN
}
' BackendHealthMonitor
class "BackendHealthMonitor" as HealthMon {
- _state: CircuitState
- _consecutive_failures: int
- _probe_interval: float
- _probe_timeout: float
- _threshold: int
- _cooldown: float
--
+ start()
+ stop()
+ record_success()
+ record_failure()
+ should_allow_request() → bool
--
Daemon thread probes
client.models.list() on interval.
--
core/healthcheck.py
}
' RateLimiter
class "RateLimiter" as RateLimiter {
- _buckets: dict[str, TokenBucket]
- _rate: float
- _burst: int
--
+ check(client_ip: str, path: str) → (bool, float)
--
Per-IP token bucket.
Returns (allowed, retry_after).
--
core/ratelimit.py
}
' TokenBucket
class "TokenBucket" as TokenBucket {
- _tokens: float
- _rate: float
- _burst: int
- _last_refill: float
--
+ consume() → bool
+ retry_after: float {property}
--
Single-IP bucket with
refill rate and burst cap.
}
' Relationships
SessionUI <|.. TerminalUI
TerminalUI <|-- WsTermUI
SessionUI <|.. WebUI
SessionUI <|.. NullUI
LLMProvider <|.. OpenAIProv
LLMProvider <|.. AnthropicProv
OpenAIProv <|-- GoogleProv
ChatSession --> SessionUI : uses
ChatSession --> LLMProvider : delegates LLM calls
ChatSession --> MCPMgr : optional
ChatSession --o ToolSearchMgr : _tool_search
ChatSession --> ModelReg : optional
ChatSession <|-- HeadlessSession
WsMgr --> "*" Ws : manages
Ws --> "1" ChatSession : wraps
Ws --> "1" SessionUI : wraps
Ws --> "1" WsState : has
WsMgr ..> ChatSession : creates via\nsession_factory(ui, model_alias)
ModelReg --> "*" ModelCfg : holds
ModelReg --> "*" LLMProvider : caches
LLMProvider --> ModelCaps : returns
ChatSession --> HealthMon : checks circuit
HealthMon --> "1" CircuitState : has
RateLimiter --> "*" TokenBucket : per IP
note bottom of ChatSession
Central engine: multi-turn LLM loop
with tool dispatch, agent sub-sessions,
context compaction, and memory persistence.
Provider-agnostic — delegates all LLM
communication to LLMProvider adapters.
core/session.py (~2700 lines)
end note
@enduml