Skip to content

Latest commit

 

History

History
290 lines (235 loc) · 19.9 KB

ARCHITECTURE.md

File metadata and controls

290 lines (235 loc) · 19.9 KB

Roundtable Architecture

Roundtable is a local-first desktop application for multi-agent ideation. It lets a human moderator run structured discussions between AI agents (Claude, Codex, Gemini) — agents never talk to each other directly; the user controls every round.

System Overview

┌─────────────────────────────────────────────────────────────────┐
│                     Electrobun Desktop Shell                    │
│  src/bun/index.ts — native window, dynamic port, HMR in dev    │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              React Frontend (Vite + TypeScript)           │  │
│  │  src/web/ — conversation UI, agent responses, settings    │  │
│  │                        ▲                                  │  │
│  │                        │ WebSocket (bidirectional)        │  │
│  │                        ▼                                  │  │
│  │              Bun Backend (Hono framework)                 │  │
│  │  src/server/ — orchestrator, persistence, adapters        │  │
│  │                                                           │  │
│  │  ┌─────────────┬─────────────────┬─────────────────────┐  │  │
│  │  │ Claude      │ Codex           │ Gemini              │  │  │
│  │  │ Adapter     │ Adapter         │ Adapter             │  │  │
│  │  │             │                 │                     │  │  │
│  │  │ Agent SDK   │ JSON-RPC over   │ JSON-RPC over       │  │  │
│  │  │ (npm)       │ stdio           │ stdio               │  │  │
│  │  └──────┬──────┴────────┬────────┴──────────┬──────────┘  │  │
│  └─────────┼───────────────┼───────────────────┼─────────────┘  │
│            ▼               ▼                   ▼                │
│     Anthropic API    codex app-server       gemini CLI          │
│                      (child process)       (child process)      │
└─────────────────────────────────────────────────────────────────┘

Agent Adapters

All three agents implement a common AgentAdapter interface (src/server/adapters/types.ts):

interface AgentAdapter {
  checkAvailability(): Promise<{ available: boolean; version?: string }>;
  startSession(opts: { cwd: string; systemPrompt: string; sharedDir: string }): Promise<void>;
  sendTurn(input: TurnInput): Promise<TurnResult>;
  interrupt(): Promise<void>;
  destroy(): Promise<void>;
  getSessionId(): string | null;
  restoreSessionId(id: string): void;
}

Claude Adapter (src/server/adapters/claude.ts)

  • Protocol: @anthropic-ai/claude-agent-sdk npm package — direct in-process SDK calls
  • Connection: No subprocess. The SDK manages HTTP connections to the Anthropic API.
  • Session continuity: SDK session ID is persisted in conversation.json and restored on load.
  • Streaming: SDK provides a stream; the adapter buffers the full response while emitting deltas via onStream.
  • Tool approval: Delegated back to the Orchestrator → WebSocket → user.
  • Effort mapping: low|medium|high → Claude's low|medium|high

Codex Adapter (src/server/adapters/codex.ts)

  • Protocol: JSON-RPC 2.0 over stdio
  • Connection: Spawns codex app-server as a child process. A custom JSON-RPC client (src/server/adapters/codex-jsonrpc.ts) handles the bidirectional protocol.
  • Session continuity: Thread ID from thread/start is persisted; resumed via thread/resume.
  • Key RPC methods: initialize, thread/start, thread/resume, thread/send_message
  • Streaming: message/chunk notifications push text deltas in real time.
  • Tool approval: Server receives session/request_permission RPC requests and forwards to the user.

Gemini Adapter (src/server/adapters/gemini.ts)

  • Protocol: JSON-RPC 2.0 over stdio (ACP — Agent Communication Protocol)
  • Connection: Spawns gemini CLI with ACP flag as a child process, using the same JSON-RPC transport as Codex.
  • Session continuity: Session ID from session/new; reloaded via session/load.
  • Key RPC methods: initialize, session/new, session/load, session/prompt
  • Streaming: session/update notifications deliver agent_message_chunk and agent_thought_chunk events.
  • Tool approval: Via session/request_permission RPC. Certain tool kinds (read, search, fetch, think) are auto-approved.
  • Model config: Writes custom Gemini settings with thinking budget calibrated to the effort level.

Session Import (src/server/session-import/)

Roundtable can ingest past conversations from each agent's CLI home directory so that agents have prior context when starting a new roundtable discussion.

Discovery (discover.ts)

Agent Source directory Format
Claude ~/.claude/projects/ JSONL session files, or sessions-index.json for rich metadata
Codex ~/.codex/ Codex session files
Gemini ~/.gemini/ Gemini session transcript files

Discovery returns lightweight DiscoveredSession[] records (id, path, title, date, cwd, messageCount) for the UI to present before actually parsing.

Parsing (per-agent)

  • parse-claude.ts — reads JSONL, extracts user/assistant events and tool calls
  • parse-codex.ts — reads Codex session format
  • parse-gemini.ts — reads Gemini session transcripts

All produce a common ImportedSession type with a TranscriptMessage[] array.

Integration in Orchestrator

  1. Parse all selected sessions via importSessions()
  2. Create conversation normally (clean system prompts)
  3. Store import metadata in conversation.importedSessions
  4. Round 0 (Context Load): Each agent receives its own prior session as a formatted context message. Agents without imported sessions get a standby message.
  5. Round 1+: The TranscriptManager prepends a cross-agent preamble (all imported sessions) on the first real user message so every agent can reference every other agent's prior work.

Session Storage (~/.roundtable/)

All Roundtable data lives in ~/.roundtable/ (configurable via ROUNDTABLE_HOME env var).

~/.roundtable/
├── {conversation-id}/
│   ├── conversation.json      # Full conversation state (rounds, metadata, session IDs)
│   ├── plan.md                # Shared plan file (agents can write)
│   ├── memory.md              # Shared memory file (agents can write)
│   └── workspace → /path/to/project   # Symlink to project cwd
└── logs/
    └── *.log

ConversationStore (src/server/conversation-store.ts) handles atomic JSON writes (write + rename), listing, loading, and deletion.

Roundtable Discussion Flow

The core design principle: agents never communicate directly. The human moderator controls every round, and agents see each other's prior responses only when the user sends the next message.

Sequence Diagram

User                   Frontend (React)             Backend (Orchestrator)           Adapters (Claude/Codex/Gemini)
 │                          │                              │                                    │
 │  Type message & submit   │                              │                                    │
 │─────────────────────────>│                              │                                    │
 │                          │  WS: message.send            │                                    │
 │                          │─────────────────────────────>│                                    │
 │                          │                              │  Create Round N                    │
 │                          │                              │  Parse @mentions for targeting     │
 │                          │                              │                                    │
 │                          │  WS: round.created           │                                    │
 │                          │<─────────────────────────────│                                    │
 │                          │                              │                                    │
 │                          │                              │  For each recipient agent:         │
 │                          │                              │  ┌────────────────────────────────┐│
 │                          │                              │  │ Build turn text:               ││
 │                          │                              │  │  [user]: <message>             ││
 │                          │                              │  │  [other_agent]: <round N-1     ││
 │                          │                              │  │    response>                   ││
 │                          │                              │  │  (imported session preamble    ││
 │                          │                              │  │   on first message)            ││
 │                          │                              │  └────────────────────────────────┘│
 │                          │                              │                                    │
 │                          │                              │──── adapter.sendTurn() ──────────>│
 │                          │                              │     (concurrent fan-out)           │
 │                          │                              │                                    │
 │                          │                              │<─── onStream(delta) ──────────────│
 │                          │  WS: agent.streaming         │     (real-time text deltas)       │
 │  See live streaming      │<─────────────────────────────│                                    │
 │<─────────────────────────│                              │                                    │
 │                          │                              │                                    │
 │                          │                              │  (if agent requests tool use)      │
 │                          │  WS: tool.approval_request   │<─── onToolApproval(req) ─────────│
 │  Approve/deny tool       │<─────────────────────────────│                                    │
 │─────────────────────────>│                              │                                    │
 │                          │  WS: tool.approval_response  │                                    │
 │                          │─────────────────────────────>│──── resolve(approved) ───────────>│
 │                          │                              │                                    │
 │                          │                              │<─── TurnResult ──────────────────│
 │                          │  WS: agent.complete          │     (full text, duration, usage)   │
 │                          │<─────────────────────────────│                                    │
 │                          │                              │                                    │
 │                          │                              │  All agents done?                  │
 │                          │                              │  → Save conversation.json          │
 │                          │  WS: round.complete          │                                    │
 │  Input unlocked          │<─────────────────────────────│                                    │
 │<─────────────────────────│                              │                                    │
 │                          │                              │                                    │
 │  Read all responses,     │                              │                                    │
 │  compose next message    │                              │                                    │
 │─────────────────────────>│  (repeat for Round N+1)      │                                    │

Cross-Pollination Detail

When the user sends a message for Round N, each agent receives:

  1. The user's message (possibly @mention-targeted)
  2. Other agents' complete responses from Round N-1 — formatted as [agent_name]: <response>. An agent never sees its own prior response in the turn text (its SDK session already has it in context).
  3. Imported session preamble (first message only) — all imported sessions formatted as context so every agent knows what prior work was done.

This ensures agents are informed by each other's thinking without direct agent-to-agent communication. The user remains the moderator who decides when to advance the discussion.

Mentions System (src/server/mentions.ts)

  • @agent syntax targets specific agents — unmentioned agents skip that round
  • Multi-line fenced block syntax for directed messages to specific agents
  • Enables the user to steer the conversation (e.g., ask only Claude to elaborate, or give Gemini specific instructions)

Backend (src/server/)

The backend runs on Bun with Hono as the HTTP framework.

File Purpose
index.ts Hono app, WebSocket server, REST endpoints, static files
orchestrator.ts Round management, fan-out, context building
transcript.ts Per-agent turn text construction, imported session preamble
conversation-store.ts JSON persistence (atomic write + rename)
config.ts Settings and agent config management
mentions.ts @mention parsing and message targeting
storage.ts ~/.roundtable/ filesystem paths, workspace symlinks
summarize.ts Token-aware message summarization
logger.ts Structured logging
adapters/ Claude, Codex, Gemini adapter implementations
session-import/ Discovery, parsing, and formatting of prior CLI sessions

WebSocket Protocol

Frontend → Backend (ClientEvent):

  • message.send — user sends a message in a conversation
  • conversation.create / conversation.create_with_import — start new conversation
  • conversation.load / conversation.list / conversation.delete
  • settings.update — change agent models, effort levels, timeouts
  • agents.check — probe CLI availability
  • round.interrupt — cancel in-flight round
  • tool.approval_response — user approves/denies a tool call

Backend → Frontend (ServerEvent):

  • round.created / round.complete — round lifecycle
  • agent.thinking / agent.streaming / agent.complete / agent.error / agent.timeout — per-agent status
  • conversation.loaded / conversation.created / conversation.list
  • tool.approval_request — agent wants to use a tool
  • settings.updated / agents.availability
  • error — general errors

Frontend (src/web/)

React + Vite + TypeScript SPA.

File/Dir Purpose
App.tsx Root component, layout
components/ConversationView.tsx Main chat display
components/RoundView.tsx Single round with all agent responses
components/AgentResponse.tsx Formatted agent message (markdown + code highlight)
components/MessageInput.tsx User input with submit
components/Sidebar.tsx Conversation list
components/SettingsBar.tsx Agent toggles, model/effort selection
components/ImportSessionsDialog.tsx Session import UI
components/ToolApproval.tsx Inline tool approval prompts
hooks/useWebSocket.ts WebSocket connection, reconnect, event queue
hooks/useConversation.ts Conversation state (rounds, streaming responses)
hooks/useSettings.ts Settings state + persistence

Electrobun Desktop App

Roundtable is packaged as a native desktop app using Electrobun, which embeds Bun as the runtime and uses the OS native webview (no bundled Chromium).

Entry point: src/bun/index.ts

  1. Sets PORT=0 so the OS assigns a free port (no conflicts)
  2. Imports and starts the backend server in-process
  3. In dev mode, checks if Vite HMR is running on port 3000/5173 — if so, points the window there with ?desktop=1&port=<actualPort> for API connectivity
  4. In production, serves the pre-built frontend assets from the app bundle
  5. Creates a BrowserWindow with titleBarStyle: "hiddenInset" on macOS

Configuration: electrobun.config.ts

App name:    Roundtable
Identifier:  dev.roundtable.app
Build:       Copies Vite dist → Resources/static/
CEF:         Disabled (uses native webview)

Build modes

Command Description
bun run dev Browser: backend on 3001 + Vite on 3000
bun run dev:desktop Desktop app (one-shot frontend build)
bun run dev:desktop:hmr Desktop with live frontend hot reload
bun run build:desktop Full desktop app bundle
bun run build:binary Standalone single-file binary in dist/

Shared Code (src/shared/)

  • context.ts — token estimation, context window calculation (used by both backend transcript building and frontend display)