Architecture
High-Level Overview
Components
React Frontend
The frontend is a React 19 single-page application built with Vite and TypeScript.
Responsibilities:
- Renders all UI screens
- Receives streamed agent tokens, phase changes, presence updates, intervention events, and vote events via SSE
- Maintains a WebSocket connection per active discussion for presence and participant events
- Sends user interventions and votes via REST
- Uses REST for all other non-real-time operations
The frontend never calls LLM providers directly. All inference is server-side.
FastAPI Backend (REST API)
The REST API handles all CRUD operations and non-streaming endpoints.
Responsibilities:
- Session management
- Panel management
- Voting
- Export
- Audit log
- Authentication
- Cost estimation
Stack: FastAPI, Pydantic v2, SQLAlchemy 2.x async, Alembic migrations, Uvicorn workers.
Starting a discussion enqueues an Arq job to Redis (arq:queue); the API does not run the discussion loop itself.
WebSocket Gateway
A FastAPI WebSocket endpoint (mounted on the API service) that handles long-lived bidirectional connections.
Responsibilities:
- Authenticate WebSocket connections via JWT handshake
- Register and track presence in Redis with TTL-based heartbeats, publishing presence updates to the discussion's Redis pub/sub channel
- Accept inbound user events (interventions, votes, presence pings, source flags), persist them, and publish them to the discussion's Redis pub/sub channel
Real-time outbound streaming (tokens, phase changes, presence, vote updates) is delivered to clients over SSE, which subscribes to the same discussion:{id} Redis pub/sub channel.
Discussion Orchestrator (Worker Pool)
The core of the product. Arq async workers execute discussion state machines backed by LangGraph.
Responsibilities:
- Pick up
run_discussionorresume_discussionjobs from the Arq Redis queue (arq:queue) - Instantiate and run the LangGraph StateGraph for the discussion
- Execute the discussion lifecycle as explicit graph nodes:
commit_phase— hidden-position commitment protocolopening_phase— transition to running, emit phase_changemoderator_turn— moderator decides next speaker, checks phase transitions and cost capcheck_interventions— query the DB for human interventions written since the last agent turnagent_turn— build prompt, call LLM with streaming, persist messagevoting_phase— collect agent votes, compute outcome, conclude session
- Handle web search and RAG tool calls inline within
agent_turn - Bridge LangGraph streaming events to Redis pub/sub
Resilience via LangGraph checkpointing. LangGraph's AsyncPostgresSaver checkpoints graph state to PostgreSQL after every completed node. If a worker pod dies mid-discussion, the next resume_discussion() call re-enters the graph at the last committed node automatically — no data loss, no manual reconstruction.
Human-in-the-loop. Human interventions are written to the messages table by the API (via REST or the WebSocket gateway). The check_interventions node runs at the start of every agent turn and queries the DB for any human messages added since the last agent turn, carrying them into the next agent_turn context. Because the discussion loop is naturally gated by LLM call latency, interventions written between turns are reliably collected without needing to suspend the graph.
LLM Provider Abstraction Layer
A shared Python module used by the orchestrator. Provides a common interface across all four providers.
Interface:
async def chat(model, messages, tools, system, max_tokens) -> LLMResponse
async def chat_stream(model, messages, tools, system, max_tokens) -> AsyncIterator[str | ToolCall]
Each provider implementation handles:
- Tool-use schema translation (Anthropic, OpenAI, and Google each use different formats)
- Streaming normalization (each provider streams differently)
- Exponential backoff retry on transient errors
- Rate limit header reading and back-off
- Token counting and cost calculation per pricing tables
- Timeout and circuit breaker per provider
The LLM Router maps model name prefixes (claude-*, gpt-*, gemini-*) to the appropriate client. Everything else routes to the Ollama client.
PostgreSQL
The system of record for all durable data.
Key schemas:
users,teams,team_memberships— identitypanels— curated and custom panel definitionssessions— top-level discussion recordsmessages— every agent and human message (append-only)sources— web search and RAG results cited during discussionsvotes— agent and human votesoutcomes— final recommendation documentsaudit_events— complete audit log (append-only, no deletes ever)- LangGraph checkpointer tables — execution state (not audit-relevant)
Full-text search on messages.content via PostgreSQL tsvector. Append-only enforcement via database-level triggers blocking UPDATE and DELETE on message and audit tables.
Redis
Serves as both the job queue backend (via Arq) and the real-time pub/sub layer. Not a system of record — Redis data loss causes degradation but not data loss.
Job queue (arq:queue):
run_discussion— start a new discussionresume_discussion— resume after pause or crashinject_intervention— push a human intervention into a suspended graphrun_voting— run the voting phase after force-end
Pub/sub (discussion:{id} channels):
- Token streaming fan-out to all connected clients
- Phase change events, presence updates, intervention broadcasts, vote updates
Other uses:
- Presence tracking via TTL-keyed hashes
Key Data Flows
Starting a Discussion
- User clicks Start on the Discussion Rules screen
- Frontend POSTs to
POST /sessions/{id}/start - FastAPI validates, updates session status to
queued, enqueues arun_discussionArq job onarq:queue - An orchestrator worker picks up the job, begins the LangGraph state machine
- Frontend opens SSE stream and WebSocket connection to the session
- Tokens stream as agents begin speaking
Agent Turn Execution
- Orchestrator
moderator_turnnode determines the next speaker agent_turnnode builds the agent's prompt from system prompt, persona, history, and any queued interventions- Calls the LLM via the provider abstraction with streaming
- As tokens arrive, they are published to
discussion:{id}on Redis pub/sub - The SSE stream forwards tokens to all connected clients
- When the message is complete, the full message is written to PostgreSQL with metadata, cost, and source citations
Human Intervention
- User types an intervention and clicks Send
- Frontend POSTs to
POST /sessions/{id}/interventions(the WebSocket gateway also accepts interventions) - The API writes to the
messagestable and publishes todiscussion:{id}on Redis pub/sub for live clients - On its next turn, the orchestrator's
check_interventionsnode queries the DB and picks up the new message intopending_interventions - The intervention enters the next
agent_turncontext
Voting and Outcome
- Moderator determines discussion is complete
- Orchestrator transitions to
votingstatus, emitsphase_changeevent - Each agent receives a structured voting prompt, returns a structured vote
- Frontend navigates to the voting screen via
phase_changeSSE event - Humans cast votes via REST
- When voting closes, orchestrator writes the
outcomesrow, transitions toconcluded - Audit record is now immutable
LangGraph Policy
langgraph and langgraph-checkpoint-postgres are pinned to minor-version ranges. LangGraph is still maturing; minor version bumps require staging tests against in-flight discussion checkpoints before production rollout.
langchain, langchain-core, and related LangChain packages are not application dependencies. If LangGraph pulls in langchain-core transitively, that is acceptable — but no LangChain abstractions appear in application code.
