* 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)
6.4 KiB
Cluster Simulator
The simulator (turnstone-sim) creates lightweight simulated nodes that talk to a real Redis instance using the standard turnstone protocol. External observers — TurnstoneClient, turnstone-console, real bridges — see identical behavior. No LLM backend is needed.
Quick Start
pip install turnstone[sim]
# 10 nodes, steady load, 60 seconds
turnstone-sim --nodes 10 --scenario steady --duration 60 --mps 5
# 100 nodes via Docker
docker compose --profile sim up redis console sim
How It Works
Each simulated node is an asyncio coroutine (not a thread or process), so 1000 nodes run efficiently on a single event loop. The simulator:
- Registers nodes via Redis heartbeats (same keys as real bridges)
- Accepts messages from per-node and shared inbound queues
- Simulates LLM responses with configurable latency and token generation
- Simulates tool execution with configurable latency and failure rates
- Publishes real protocol events (
ContentEvent,StateChangeEvent,TurnCompleteEvent, etc.) - Reports latency, throughput, and utilization metrics at completion
TurnstoneClient → Redis Queue → SimNode → Redis Pub/Sub → TurnstoneClient
↓
turnstone-console (cluster dashboard)
Scenarios
| Scenario | Description |
|---|---|
steady |
Inject messages at a constant rate (--mps) for --duration seconds |
burst |
Push --burst-size messages instantly, then wait for completion |
node_failure |
Steady load + periodically kill nodes to test redistribution |
directed |
Send messages to specific nodes via target_node routing |
lifecycle |
Create, use, and close workstreams across nodes |
CLI Reference
turnstone-sim [options]
Cluster
| Flag | Default | Description |
|---|---|---|
--nodes |
10 |
Number of simulated nodes |
Scenario
| Flag | Default | Description |
|---|---|---|
--scenario |
steady |
Scenario name |
--duration |
60 |
Duration in seconds |
--mps |
5.0 |
Messages per second (steady) |
--burst-size |
100 |
Messages to send (burst) |
--node-kill-interval |
15 |
Seconds between kills (node_failure) |
--node-kill-count |
1 |
Nodes per kill cycle |
Simulation
| Flag | Default | Description |
|---|---|---|
--llm-latency |
2.0 |
Mean LLM response latency (seconds) |
--tool-latency |
0.5 |
Mean tool execution latency (seconds) |
--tool-failure-rate |
0.02 |
Tool failure probability (0.0–1.0) |
--seed |
— | Random seed for reproducibility |
Redis
| Flag | Default | Description |
|---|---|---|
--redis-host |
localhost |
Redis host |
--redis-port |
6379 |
Redis port |
--redis-password |
— | Redis password |
--prefix |
turnstone |
Redis key prefix |
Output
| Flag | Default | Description |
|---|---|---|
--metrics-file |
— | Write JSON report to file |
--log-level |
INFO |
Log verbosity |
Example: Load Testing
# 100 nodes, high throughput, 2 minutes
turnstone-sim --nodes 100 --scenario steady --duration 120 --mps 50
# Burst of 500 messages across 50 nodes
turnstone-sim --nodes 50 --scenario burst --burst-size 500 --duration 60
# Node failure resilience (kill 2 nodes every 10 seconds)
turnstone-sim --nodes 20 --scenario node_failure --duration 120 \
--node-kill-interval 10 --node-kill-count 2
# Fast simulation (low latency, no failures)
turnstone-sim --nodes 10 --scenario steady --duration 30 \
--llm-latency 0.1 --tool-latency 0.05 --tool-failure-rate 0 --mps 10
Metrics Report
The simulator prints a summary at completion:
============================================================
SIMULATION REPORT
============================================================
Scenario: steady
Nodes: 100
Duration: 60.2s
Total turns: 295
Total errors: 5
Node kills: 0
------------------------------------------------------------
THROUGHPUT
Messages/sec: 4.97
Turns/sec: 4.89
------------------------------------------------------------
LATENCY (seconds)
p50: 3.21
p90: 5.44
p99: 8.12
mean: 3.56
max: 12.1
------------------------------------------------------------
UTILIZATION
Mean ws/node: 2.3
Max ws/node: 8
Idle nodes: 12
============================================================
Use --metrics-file report.json to write the full report as JSON.
Console Integration
The simulator's nodes appear in turnstone-console exactly like real nodes. Run them together to see the dashboard populate with simulated workstreams:
# Terminal 1: start Redis and console
docker compose up redis console
# Terminal 2: run simulator
docker compose --profile sim up sim
Or all at once:
SIM_NODES=50 SIM_DURATION=120 docker compose --profile sim up redis console sim
Open http://localhost:8090 to see simulated nodes, workstream states, token counts, and load bars updating in real time.
Architecture
See also: Simulator Architecture diagram
turnstone/sim/
├── __init__.py # Public API: SimCluster, SimConfig
├── config.py # SimConfig — all simulation parameters
├── engine.py # SimEngine — LLM + tool execution simulation
├── node.py # SimNode + SimWorkstream — protocol-compatible node
├── cluster.py # SimCluster + InboundDispatcher + PooledBroker
├── scenario.py # 5 scenario classes
├── metrics.py # MetricsCollector — latency, throughput, utilization
└── cli.py # CLI entry point
Key design: The InboundDispatcher batches ~50 node queues into a single Redis BLPOP call, keeping connection count bounded at ~20 regardless of node count. All nodes share a single ConnectionPool(max_connections=64).
Programmatic Use
import asyncio
from turnstone.sim import SimCluster, SimConfig
async def main():
config = SimConfig(
num_nodes=10,
scenario="steady",
duration=30,
messages_per_second=2.0,
llm_latency_mean=0.5,
)
cluster = SimCluster(config)
await cluster.start()
await cluster.run_scenario()
print(cluster.report())
await cluster.stop()
asyncio.run(main())