mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
9be155b97a
* 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)
114 lines
4.3 KiB
Plaintext
114 lines
4.3 KiB
Plaintext
@startuml
|
||
!theme plain
|
||
title Turnstone — Simulator Architecture
|
||
|
||
skinparam component {
|
||
BackgroundColor<<cluster>> #E1BEE7
|
||
BackgroundColor<<node>> #CE93D8
|
||
BackgroundColor<<engine>> #F3E5F5
|
||
BackgroundColor<<scenario>> #FFF3E0
|
||
BackgroundColor<<metrics>> #E8F5E9
|
||
BackgroundColor<<redis>> #FFCDD2
|
||
}
|
||
|
||
package "SimCluster" as cluster <<cluster>> {
|
||
|
||
component [**ThreadPoolExecutor**\nmax_workers=64\n(blocking Redis ops)] as executor <<cluster>>
|
||
|
||
component [**redis.ConnectionPool**\nmax_connections=64\ndecode_responses=True\n(shared across all nodes)] as pool <<redis>>
|
||
|
||
package "InboundDispatchers" {
|
||
component [**Dispatcher 0**\nnodes 0-49] as d0
|
||
component [**Dispatcher 1**\nnodes 50-99] as d1
|
||
component [**...**\n(ceil(N/50) total)] as dn
|
||
|
||
note bottom of d0
|
||
Each dispatcher calls BLPOP on a single Redis
|
||
connection for up to 50 node queues + shared queue.
|
||
Keys: [prefix:inbound:sim-0000, ..., prefix:inbound]
|
||
Per-node keys have BLPOP priority over shared.
|
||
end note
|
||
}
|
||
|
||
package "SimNodes (N instances)" {
|
||
component [**SimNode sim-0000**] as n0 <<node>>
|
||
component [**SimNode sim-0001**] as n1 <<node>>
|
||
component [**...**] as nn <<node>>
|
||
|
||
component [**SimEngine**\n(per node, seeded RNG)\n\nLLM simulation:\n gaussian(μ=2s, σ=0.5s) latency\n gaussian(μ=200, σ=50) tokens\n random word content\n P(tool_calls) = 0.6/0.3\n\nTool simulation:\n gaussian(μ=0.5s, σ=0.2s) latency\n P(failure) = 0.02] as engine <<engine>>
|
||
|
||
component [**SimWorkstream**\n(0..max_ws per node)\n\nState: idle→thinking→running→idle\nToken accounting: word_count × 3\nContent: 8-chunk streaming] as ws <<node>>
|
||
}
|
||
|
||
component [**MetricsCollector**\n(thread-safe, shared)\n\nTracks: turn latencies,\nthroughput, utilization,\nerrors, node kills] as metrics <<metrics>>
|
||
}
|
||
|
||
package "Scenarios (5 workload patterns)" <<scenario>> {
|
||
component [**SteadyState**\nConstant rate:\n1/mps interval\nfor duration secs] as steady <<scenario>>
|
||
|
||
component [**Burst**\nburst_size messages\nas fast as possible\nthen wait] as burst <<scenario>>
|
||
|
||
component [**NodeFailure**\nSteadyState + periodic\nnode kills (up to N/2)] as failure <<scenario>>
|
||
|
||
component [**Directed**\nMessages targeted to\nspecific nodes via\ntarget_node field] as directed <<scenario>>
|
||
|
||
component [**Lifecycle**\n3 phases:\n1. Create workstreams\n2. Send messages\n3. Close half] as lifecycle <<scenario>>
|
||
}
|
||
|
||
database "Redis" as redis <<redis>>
|
||
|
||
' Scenario -> Redis
|
||
steady --> redis : RPUSH prefix:inbound\n(SendMessage)
|
||
burst --> redis : RPUSH prefix:inbound\n(burst)
|
||
failure --> redis : RPUSH prefix:inbound
|
||
directed --> redis : RPUSH prefix:inbound:{node}\n(directed)
|
||
lifecycle --> redis : RPUSH prefix:inbound\n(Create/Send/Close)
|
||
|
||
' Dispatchers -> Redis -> Nodes
|
||
d0 --> redis : BLPOP [per-node..., shared]
|
||
d1 --> redis : BLPOP [per-node..., shared]
|
||
d0 --> n0 : handle_message(raw)
|
||
d0 --> n1 : handle_message(raw)
|
||
|
||
' Nodes internal
|
||
n0 --> engine : simulate_llm_response()\nsimulate_tool_execution()
|
||
n0 --> ws : process_turn()
|
||
|
||
' Nodes -> Redis (events)
|
||
n0 --> redis : PUBLISH prefix:events:global\n(StateChangeEvent)
|
||
n0 --> redis : PUBLISH prefix:events:{ws_id}\n(ContentEvent, ToolResultEvent, ...)
|
||
n0 --> redis : PUBLISH prefix:events:cluster\n(ClusterStateEvent)
|
||
n0 --> redis : SET prefix:node:sim-0000\nEX 60 (heartbeat)
|
||
n0 --> redis : SET prefix:ws:{ws_id}\n(ownership)
|
||
|
||
' Shared pool
|
||
n0 ..> pool : PooledBroker\n(shared connection)
|
||
n1 ..> pool : PooledBroker
|
||
d0 ..> pool
|
||
d0 ..> executor : asyncio.to_thread()
|
||
|
||
' Metrics
|
||
ws --> metrics : record_turn(ws_id, node_id, latency)
|
||
steady --> metrics : record_inject()
|
||
burst --> metrics : record_inject()
|
||
directed --> metrics : record_inject()
|
||
lifecycle --> metrics : record_inject()
|
||
cluster --> metrics : record_node_kill(node_id)
|
||
cluster --> metrics : snapshot_utilization()\n(every metrics_interval)
|
||
|
||
note bottom of cluster
|
||
**SimConfig** controls all simulation parameters:
|
||
num_nodes, max_ws_per_node, redis settings,
|
||
llm_latency_mean/stddev, tool_failure_rate,
|
||
scenario, duration, messages_per_second, seed
|
||
end note
|
||
|
||
note right of redis
|
||
Simulator uses **real Redis** —
|
||
not a mock. Console dashboard
|
||
can monitor a running simulation
|
||
via the same cluster channel.
|
||
end note
|
||
|
||
@enduml
|