TX-1
Architecture

How TX-1 is built

A two-channel, three-layer system: a Tauri desktop shell, a Python FastAPI sidecar, and a LangGraph agent DAG. Each layer has a single responsibility.

System Overview
[Tauri Shell (React)]  ←──WebSocket──→  [FastAPI Sidecar (Python)]
        │                                         │
   Tauri invoke                      ┌────────────┤
  (commands, approve)                │             │
        │                       REST API      LangGraph DAG
  Workspace CRUD             (workspace,           │
  (projects, tabs,            integrations)  ┌──────┼──────┐
   integrations)                          Dispatcher Inspector Strategist
                                           (Haiku)  (Haiku)   (Sonnet)
                                                                  │
                                                        ┌─────────┴──────┐
                                                     OR-Tools        ChromaDB
                                                     Solver          Archivist
                                                        │
                                              SQLite (ERP + Workspace)

Two-channel communication

WebSocket
ws://localhost:8765/ws

Persistent streaming channel. On connect, sends a WORKSPACE_INIT payload with the full project/tab state. Then streams LOG, STATUS, and CARD messages — each tagged with a tab_id so the frontend routes output to the correct tab.

{ type: "WORKSPACE_INIT", payload: { projects, connections } }
{ type: "LOG", tab_id: "...", level: "INFO", text: "..." }
{ type: "STATUS", tab_id: "...", text: "SOLVER: SOLVING" }
{ type: "CARD", tab_id: "...", card_type: "...", payload: {} }
Tauri invoke + REST
Discrete user actions + workspace CRUD

Tauri invoke handles fire-and-forget commands (submit, approve, dismiss). The React frontend also calls the REST API directly for workspace mutations — project/tab CRUD and integration toggling. Every workspace endpoint returns the full updated state.

invoke("run_command", { query, scenarioId, tabId })
invoke("resolve_approval", { cardId, decision })
REST: POST/DELETE /workspace/project, /workspace/tab, PATCH /integrations/:id

LangGraph graph topology

START → intent_classifier
  → [HEALTH_CHECK]    → data_validator  → END
  → [SOLVE_REQUEST]   → solver_engine
      → [SUCCESS]       → END
      → [INFEASIBLE]    → analyst_proposer
          → human_approval_gate
              → [APPROVED]   → apply_fix → solver_engine  (loop)
              → [DISMISSED]  → END
  → [RESET_SCENARIO]  → reset_scenario  → END
  → [RESEARCH]        → research_stub   → END

Circuit breaker: iteration_count > 3 → MANUAL_REVIEW card → halt. Prevents infinite loops.

Agent personas

Agent
Model
Role
Dispatcher
claude-haiku-4-5
Intent classification. Returns JSON only — no prose. Extracts intent + network entity.
Inspector
claude-haiku-4-5
SQL schema scan, triage report. Produces health score 0–100 and issue list.
Strategist
claude-sonnet-4-6
Auto-fix reasoning. Returns ActionCard JSON. Dry-run validates every proposal.
Archivist
ChromaDB (no LLM)
Vector store retrieval. Top-3 similar past fixes before Strategist reasons.
Critical guardrail: The Strategist may only modify node_capacities.max_capacity_units and lanes.is_active. It must never modify nodes.latitude/longitude, nodes.type, or products.* without a high-severity warning card.

Shared state (TerminalState)

class TerminalState(TypedDict):
    query:           str    # "run baseline"
    scenario_id:     str    # "SCENARIO_BASELINE_Q3"
    network_id:      str    # "chicago" | "amsterdam"
    intent:          str    # HEALTH_CHECK | SOLVE_REQUEST | ...
    data_health:     dict   # Inspector output
    solver_output:   dict   # OR-Tools result
    proposal:        dict   # Strategist ActionCard payload
    ui_component:    str    # card type being rendered
    needs_approval:  bool   # True = graph paused at HITL gate
    iteration_count: int    # circuit breaker counter

Key architecture decisions

WebSocket for streaming, not Tauri events
All streaming output goes over WebSocket
Tauri events are fire-and-forget — they can't stream progress across a long-running background thread. WebSocket gives us persistent bidirectional flow with no polling.
Tauri invoke as HTTP proxy
Rust forwards user actions to FastAPI via reqwest
Tauri's webview security model blocks direct fetch() calls from the frontend to localhost. Routing through Rust avoids the CSP restriction without weakening it.
Fire-and-forget /command endpoint
FastAPI returns 200 immediately; graph runs in executor
The HITL gate can block for up to 5 minutes. An awaited HTTP response would timeout. The background executor thread keeps the graph alive while WebSocket streams all updates.
threading.Event for HITL pause
Approval registry uses threading.Event, not asyncio
The LangGraph executor runs in a synchronous thread pool. asyncio primitives don't cross thread boundaries cleanly. threading.Event.wait() blocks the executor thread without blocking the FastAPI event loop.
Dry-run in rolled-back transaction
Every SQL patch is tested inside BEGIN/ROLLBACK before proposal
Prevents the Strategist from hallucinating a patch that looks valid but would fail or not actually resolve the constraint. The user only ever sees proposals that are mathematically proven to work.
Context window guard
Only schema headers and violation-relevant rows sent to Claude
Sending the full database is slow, expensive, and risks prompt injection from data values. The Strategist only needs the bottleneck node's capacity and the downstream demand total.
DB-backed workspace with runtime merge
SQLite stores projects/tabs; React merges with runtime state
Projects and tabs persist across restarts via SQLite. Runtime-only state (busy, solverStatus) lives in React. On each DB write, a mergeWorkspace function reconciles incoming DB state with current runtime state — no stale flags.
Per-tab message routing
Every WS message carries a tab_id
Multiple tabs can run solver sessions concurrently. tab_id ensures LOG, STATUS, and CARD messages reach the correct stream. The frontend stores entries per-tab and only renders the active tab's stream.

SQLite schema

nodes
Physical network nodes. Type: FACTORY | WAREHOUSE | CUSTOMER. network_id tags Chicago vs Amsterdam.
node_capacities
Capacity bounds per node. max_capacity_units is the primary Auto-Fix target.
lanes
Edges connecting nodes. cost_per_unit drives the solver objective. lead_time_days < 0 = data quality flag.
demand_forecast
Customer demand by product and period (2026-Q3).
scenarios
One row per solve attempt. Status: DRAFT | SOLVING | SUCCESS | INFEASIBLE.
audit_log
Every agent change and human decision. user_approved=1|0|NULL. Enterprise compliance record.
products
Immutable master data. The Strategist is forbidden from modifying this table.
workspace_projects
Persistent projects. Each project has a name, active tab pointer, and creation timestamp.
workspace_tabs
Tabs within projects. Each tab binds to a network_id and has a tab_type (terminal | settings | integrations | data-import).
integrations
External tool connections. Status: connected | disconnected. Category, icon, description.