Skip to content

03 — System Architecture

The Engine at thirty thousand feet — components, boundaries, and dataflow.

The Aetherium Engine is a .NET 10 monolith with a React single-page frontend, an in-process MCP host, and external dependencies on PostgreSQL, ChromaDB, and an OpenAI-compatible LLM server. That single sentence describes the architectural shape; the rest of this section unpacks it.

At a glance

One process serves everything. The same ASP.NET Core host that exposes the REST controllers also serves the compiled React client, hosts the SignalR hubs, owns the EF Core data layer, runs the orchestration loop, and hosts all the MCP tool servers in-process. There is no separate agent runner, no out-of-process tool sidecar, no message broker between the supervisor and its workers. When an agent calls write_file, the call resolves inside the same process that took the user's chat message. The same is true for delegate, run_shell, search_knowledge, and every other tool — they are method invocations on classes registered in the host's DI container, not network calls.

Around that process sit four external dependencies. PostgreSQL holds the relational data — projects, agent roles, conversations, logs. ChromaDB holds the vector data — per-project and global knowledge collections. LM Studio (or any OpenAI-compatible server) handles inference. The Docker daemon provisions ephemeral workspace containers that agents use as their shell and filesystem sandbox. Everything else — the web client, the workspace bind mounts, the embedded templates — is internal to the Engine's process or its file layout. This is deliberately a monolith. The boundaries that matter run through the process (controllers vs services, services vs MCP tools, EF Core vs Docker), not between processes.

What's running where

In a typical developer setup the Engine itself runs as a single dotnet process on the host. PostgreSQL and ChromaDB run as containers brought up by docker compose. LM Studio runs as a separate host process exposing http://localhost:1234. Project workspaces live on the host filesystem under AETHERIUM_WORKSPACE_ROOT, and the Engine bind-mounts each project's workspace into an ephemeral per-project container so agents can read, write, and execute inside an isolated tier. The browser the user opens to http://localhost:5257 is redirected to /app, where it downloads the React bundle from the same dotnet process, then talks back over HTTP, a SignalR WebSocket, and a server-sent-event stream. For multi-host topologies — splitting the LLM server onto another box, reverse-proxying the client, running under systemd — see §14 — Deployment.

Component overview

flowchart LR
    Browser[React Client<br/>SPA at /app]
    Server[Aetherium Server<br/>ASP.NET Core .NET 10]
    PG[(PostgreSQL)]
    CDB[(ChromaDB)]
    LM[LM Studio<br/>OpenAI-compatible]
    Docker[Docker host<br/>workspace containers]

    Browser <-->|HTTP REST| Server
    Browser <-->|SignalR WebSocket| Server
    Server -->|SSE: AG-UI event stream| Browser
    Server -->|EF Core| PG
    Server -->|HTTP| CDB
    Server -->|HTTP /v1/chat/completions| LM
    Server -->|Docker Engine API| Docker

    classDef server fill:#B87333,stroke:#2D2D2D,stroke-width:2px,color:#FFFFFF
    classDef store fill:#8FA68A,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
    classDef ext fill:#C48A4E,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
    classDef client fill:#D5C3B0,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
    class Server server
    class PG,CDB store
    class LM,Docker ext
    class Browser client

Read it center-out. The Server is the single ASP.NET Core host — controllers, services, MCP server classes, SignalR hubs, and the EF Core data layer all live here. The React Client is served from the host's own wwwroot/app and talks to the server over three channels: HTTP for command-style requests (create a project, list templates, fetch settings), a SignalR WebSocket for hub events, and a server-sent-event stream carrying the AG-UI-shaped run trajectory. Its TypeScript types are generated from the server's OpenAPI document rather than hand-written. PostgreSQL is the system of record. ChromaDB holds vector embeddings for the knowledge base. LM Studio is the inference engine — anything that speaks the OpenAI chat-completions API can stand in. Docker is how agents get a sandboxed shell and filesystem.

The .NET projects

The solution contains four projects, each with a clear role.

  • Aetherium_Refactor (server). The ASP.NET Core host. Owns Controllers, Services, McpServers, SignalR Hubs, Data (EF Core + migrations), and embedded Templates. All server-side logic lives here.
  • Aetherium.Monitor (console). A read-only run monitor that renders a live run from the event stream alone. It exists as proof the contract is complete: if a second, independent client can follow a run without sharing code with the first, the frontend is genuinely swappable.
  • Aetherium.Shared. DTOs, models, and interfaces the server and the monitor both depend on. No business logic — just the contracts that keep the sides aligned. The web client does not reference it directly; its TypeScript equivalents are generated from the server's OpenAPI document.
  • Aetherium_Refactor.Tests. xUnit suites for services, controllers, MCP servers, and the integration paths.

The web client is a fifth component but not a .NET project: Aetherium.Web is a React application built with Vite and TypeScript, compiled into the server's wwwroot/app so the same host serves it.

The full file tree, project-by-project breakdown, and where each folder fits lives in §05 — Solution Layout.

The services that do the work

The server hosts dozens of services. Four of them carry the architectural load and you will see their names repeatedly across this documentation.

  • OrchestratorService — the heart of the ReAct loop. It routes a user message through the configured topology, calls the LLM, dispatches the tool calls the model returns, feeds tool results back in, and synthesizes the final response.
  • ModelSchedulerService — a channel-based queue in front of the LLM server. It serializes inference requests so concurrent teams don't thrash the GPU by trying to swap models on top of each other.
  • McpToolRouter — discovers tools across the MCP server classes (a focused capability boundary that exposes tools to agents) and dispatches each call to the right handler. It enforces the three-layer permission model that gates which agents can call which tools.
  • DockerService — owns the lifecycle of workspace containers: create, start, exec, monitor, stop, destroy.

Each of these is treated in depth in §06 — Key Services, alongside the supporting cast (LMStudioService, LLMRouterService, ContextManager, TemplateService, and the background workers that drive cleanup and the scheduler loop).

The real-time layer

The Engine is a real-time system. Every token the LLM emits, every tool call an agent dispatches, every delegation a supervisor (an agent whose role is to plan and delegate) makes to a worker streams to the browser as it happens — this is what you watched the Engine do in §02 — Getting Started. Four SignalR hubs carry this traffic. NotificationHub carries cross-cutting user-facing notifications. ProjectRunHub carries the per-project run stream — chat tokens, tool call summaries, delegation breadcrumbs. ContainerHub carries container lifecycle events so the UI can show workspace ready, idle, stopped, destroyed. TeamHub carries team-status and inter-agent messaging updates, joined per project. The real-time layer is a defining property of the Engine, not a bolt-on. Implementation detail — what events flow on which channel, how the hubs interact with the event bus, how the client consumes them — lives in §06 — Key Services.

Request lifecycle

What actually happens when a user sends a chat message to a running team:

sequenceDiagram
    participant U as User
    participant C as React Client
    participant H as SignalR Hub
    participant API as Controller
    participant O as OrchestratorService
    participant S as ModelScheduler
    participant L as LM Studio
    participant R as McpToolRouter
    participant T as MCP Server<br/>(e.g. WorkspaceTools)

    U->>C: type message, press send
    C->>API: POST /api/orchestrator/...
    API->>O: dispatch to topology
    O->>S: enqueue LLM call
    S->>L: /v1/chat/completions
    L-->>S: response + tool_calls
    S-->>O: ChatDetailedResponse
    O->>R: route tool call
    R->>T: invoke tool method
    T-->>R: tool result
    R-->>O: result
    O->>S: next LLM turn (loop)
    O->>H: stream tokens + events
    H-->>C: SignalR push
    C-->>U: rendered response

Read it top to bottom. The user types and the client POSTs the message to a controller, which hands it to OrchestratorService. The orchestrator chooses what comes next based on the topology — Hierarchical, Sequential, Roundtable, or Single — and enqueues an LLM call with ModelSchedulerService. The scheduler serializes that request against the GPU and forwards it to LM Studio's OpenAI-compatible endpoint.

LM Studio returns a response that may contain tool calls. The orchestrator hands each tool call to McpToolRouter, which checks the calling agent's allowed-server list, dispatches the call to the right MCP server class — WorkspaceTools for file operations, ShellTools for shell execution, ResearchTools for web work, and so on — and returns the result. Tool execution happens in-process; only the side effects (a write to disk, a shell exec into a workspace container, an HTTP fetch) cross the process boundary.

The tool result flows back into the next LLM turn. The loop continues — reason, act, observe, repeat — until the agent produces a response with no tool calls. In a Hierarchical team the supervisor may delegate to a worker via the delegate tool, which itself spins up a nested orchestration turn for that worker. When the top-level agent has a final answer, OrchestratorService pushes the synthesized response and every intermediate event onto the appropriate SignalR hub, the client receives them on its open WebSocket connection, and the user watches it happen.

That is the ReAct loop in concrete terms: reason (LLM turn), act (tool call), observe (tool result), repeat. Everything else in this documentation set elaborates on a piece of this diagram.

Persistence layout

The Engine uses three persistence tiers. Each was picked for a specific data shape: relational data wants joins; vector data wants approximate-nearest-neighbor search; files want a filesystem.

Data Store
Projects, Roles, Conversations, InteractionLogs, SystemSettings PostgreSQL
Per-project and global knowledge embeddings ChromaDB
Project workspaces, generated source files, build artifacts Filesystem (workspace tiers)
Shipped templates Filesystem (embedded resources)
RAG index file Filesystem

ChromaDB carries both per-project collections (a project's own knowledge base) and global collections (cross-project knowledge shared across teams). Depth — schema, naming, retention, the precise table layout — is covered in §06 — Key Services for how the knowledge layer is implemented, and in §09 — Configuration for storage paths and tier policies.

Boundaries — in-process vs out-of-process

What runs in-process inside the Aetherium_Refactor host:

  • Controllers, all Services, all MCP server classes, all SignalR Hubs, background workers, the EF Core data layer.

This is the in-process MCP host: every MCP server class loads into the same process as the orchestrator that calls them. No socket, no subprocess, no broker. The architectural reasoning behind that choice lives in §17 — Architecture Decisions.

What runs out-of-process:

  • PostgreSQL (container).
  • ChromaDB (container).
  • LM Studio or equivalent (separate host process).
  • Workspace containers themselves (one ephemeral container per active project, managed by DockerService).

The React client also runs out-of-process in a sense — it executes in the user's browser, downloaded as static assets the first time the page loads. After that, every interaction is HTTP, SignalR or SSE back to the server.

Where deployment topology lives

This section describes the architectural shape, not the production deployment. If you need to separate the LLM server onto a different host, reverse-proxy the client behind nginx, run the Engine as a systemd unit, or wire the whole thing through a VPN, §14 — Deployment is the right place to look.

Where to go next

This is the architectural shape. The next four sections fill in how it actually works.