-
Notifications
You must be signed in to change notification settings - Fork 3
Architecture
Clio Coder is a coding agent for HPC and scientific software, built by IOWarp and the Gnosis Research Center at Illinois Tech. The package (@iowarp/clio-coder) provides an interactive terminal interface for repository work, a headless single-turn runner, an ACP v1 agent over stdio, and a local graphical interface. It orchestrates configured model targets and dispatches bounded workers as subprocesses. The authored architecture defines the maintained contracts for these layers.
The source tree is organized into eight functional layers under src/ plus an application under apps/:
| Layer | Path | Role |
|---|---|---|
| Core infrastructure | src/core/ |
Event bus, config, domain loader, shared state |
| Engine boundary | src/engine/ |
The only code that touches @earendil-works/pi-*
|
| Domains | src/domains/ |
Independent business modules (context, dispatch, safety, providers, etc.) |
| Tools | src/tools/ |
Surface-agnostic tool specifications and execution |
| Interactive | src/interactive/ |
TUI rendering, chat loop, overlays, keybinding |
| Entry | src/entry/ |
Boot composition root and mode wiring |
| Worker | src/worker/ |
Subprocess runtime for dispatched workers |
| CLI | src/cli/ |
Argument parsing, subcommand dispatch, boot options |
| GUI app | apps/clio-coder-gui/ |
Optional alpha web application |
The composition backbone is the manifest-driven domain loader in src/core/domain-loader.ts. Every domain exports a DomainModule with a manifest (declaring its name and dependsOn list) and a createExtension(context) factory. The loader:
- Topologically sorts modules by their dependency manifests.
- Instantiates each in order, passing a
DomainContextthat exposes the shared event bus andgetContract<T>(name). - Calls
extension.start()and stores the resulting contract under the domain name. - On shutdown, calls
stop()in reverse dependency order, unwinding bus listeners and releasing resources.
The orchestrator in src/entry/orchestrator.ts (bootOrchestrator) composes all domain modules for a production boot. The module list includes Config, Extensions, Plugins, Interop, Resources, Share, Context, Providers, Safety, Prompts, Agents, Middleware, Session, Observability, Scheduling, and conditionally Mux (panes) and Dispatch. Each domain receives a DomainContext whose getContract returns only the query-only contract from previously loaded domains, never the full extension, enforcing the cross-domain boundary.
The focused test in tests/contracts/domain-lifecycle.test.ts verifies three lifecycle guarantees:
- A failed domain startup releases its listeners and earlier dependencies in reverse order.
- A rejected domain factory unwinds started dependencies.
- Concurrent shutdown callers await one cleanup, and stopped contracts are withdrawn.
Three process modes share the domain loader:
-
Interactive (
src/cli/index.ts→src/cli/clio.ts→src/entry/orchestrator.ts): Opens the TUI via the terminal lease, boots all domains, creates the chat loop, and runs the interactive session. The Stage 0 shell (terminal lease) answers the terminal immediately while Stage 1 hydrates, yielding at each phase boundary viabootPhaseBoundary. -
Headless (
clio-coder run): Loads the same domains, resolves the model target, and runs a single main-agent turn viarunHeadlessMainAgentfromsrc/cli/modes/print.ts. -
ACP (
clio-coder acp): Serves the ACP v1 protocol over stdio viaserveClioAcpAgentinsrc/engine/acp/server.ts. The CLI dispatches this through theCOMMAND_HANDLERSmap insrc/cli/index.ts, dynamically importing each subcommand's module to avoid cold module-load tax.
The GUI server in apps/clio-coder-gui/server/main.ts runs a Hono-based HTTP application that spawns worker hosts for reads and operations, connects to an ACP supervisor, and exposes session, workspace, and service endpoints over REST.
The file src/engine/types.ts is the single re-export point for engine package types consumed by the rest of the codebase. It exposes erased shapes like EngineModel (which is PiModel<PiApi>), AgentMessage, Usage, and TUI types. The comment in that file states the rule explicitly: "Importing engine package types from anywhere else in the codebase violates the engine boundary."
The boundary checker in tests/boundaries/check-boundaries.ts enforces this as rule 1: no file outside src/engine/** may import @earendil-works/pi-* at all, not even type-only imports (since the 0.83.0 rework). Domains take erased engine shapes from src/engine/types.ts instead.
The tool system lives in src/tools/. Tools are defined as ToolSpec objects registered in a ToolRegistry. The gateway tool (src/tools/gateway/index.ts, exported as createGatewayTool) provides a unified find/describe/call interface through which the model reaches secondary capabilities that are not attached directly to the model's tool surface. A gateway call resolves the capability and invokes it through the registry under the capability's own name, so safety, skill policy, autonomy mapping, and audit all apply identically to a direct call.
The context tool (src/tools/context/index.ts, exported as createContextTool) provides workspace, settings, skills, recall, and budget scopes. The skills scope handles the skill activation policy including pending-request gates, drift detection, and marketplace rows. The docs and library scopes are gateway capabilities (clio_docs, clio_library).
The data tool (src/tools/data/index.ts) exports inspectData, selectData, and validateData for CSV/TSV, JSON, and JSON Lines files. Format detection follows an explicit-argument → extension → content-sniffing cascade, and each function streams the file, returning a JSON-serializable result or a typed refusal.
Workers run as separate Node.js subprocesses launched by the dispatch domain. The worker entry point (src/worker/entry.ts) reads a WorkerSpec JSON document from stdin, rehydrates the runtime descriptor from the runtime registry, and dispatches to startWorkerRun from the engine boundary. Workers emit NDJSON events on stdout and communicate with the orchestrator through a control lane.
The worker boundary is strict: src/worker/** may only value-import from src/domains/providers/plugins.ts, src/domains/providers/registry.ts, and src/domains/providers/runtimes/builtins.ts (the provider runtime rehydration modules). All other domain imports must be type-only. This rule is enforced as rule 2 in the boundary checker.
For a clio-coder run "<task>" invocation:
-
src/cli/index.tsparses flags, dispatches to therunhandler, which dynamically importssrc/cli/run.ts. -
src/cli/run.tscallsbootOrchestratorwith headless options. -
bootOrchestratorinsrc/entry/orchestrator.tsloads all domains vialoadDomains, resolves the model target through the Providers contract, and registers background memory routing. - The chat loop (
src/interactive/chat-loop.ts) executes the turn, invoking tools through the registry. - Tool results pass through the observation budget system (
src/tools/observation.ts) for per-turn byte caps. - The session domain persists entries to the JSONL ledger.
- On completion, the termination coordinator drains dispatch and closes all extensions in reverse order.
Six static import rules are checked by tests/boundaries/check-boundaries.ts:
| Rule | Constraint |
|---|---|
| 1 |
src/engine/** is the only code that may value-import @earendil-works/pi-*; all other layers use erased shapes from src/engine/types.ts. |
| 2 |
src/worker/** never value-imports src/domains/** except provider runtime rehydration modules. |
| 3 |
src/domains/<x> never imports src/domains/<y>/extension.ts for y != x; use the contract from src/domains/<y>/index.ts. |
| 4 |
src/tools/** never imports src/interactive/**; the tool substrate is surface-agnostic. |
| 5 | Chat loop turn modules never import src/entry/**; composition flows one direction. |
| 6 | External runtime importers reach src/interactive/** and src/engine/** only through declared seams in STAGE0_SEAMS. |
The Stage 0 closure (owned by src/interactive/terminal-lease.ts) is held to 16 chunks, 700,000 total bytes, and 175,000 Clio source bytes by tests/contracts/instant-shell-import-graph.test.ts. Rule 6 protects this budget by preventing external importers from dragging the Stage 0 closure into their own module graph.
New capabilities are added at specific seams:
-
New domain module: Export a
DomainModulefromsrc/domains/<name>/index.tswith a manifest andcreateExtension. Register it in the orchestrator'sloadDomainsarray. The module receives aDomainContextand returns aDomainBundlewith an extension (lifecycle) and a contract (query surface). -
New tool: Define a
ToolSpecwithname,description,parameters(TypeBox schema),baseActionClass, andrun. Register it in the tool bootstrap (src/tools/bootstrap.ts). Tools marked as "direct" are attached to the model surface; others are reachable through the gateway. -
New CLI subcommand: Add an entry to the
COMMAND_HANDLERSmap insrc/cli/index.tsthat dynamically imports a new module fromsrc/cli/. The dynamic import is literal so tsup can split it into its own chunk. -
New worker runtime: Register a
RuntimeDescriptorinsrc/domains/providers/runtimes/builtins.ts. The worker rehydrates it from stdin viasrc/worker/runtime-registry.ts. -
New MCP server: Declare the server in the project's MCP configuration. The gateway's MCP capability source (
src/tools/gateway/mcp-capabilities.ts) discovers it and exposes its tools as capabilities.
-
Engine boundary is absolute. Since the 0.83.0 rework, there is no type-only exception to rule 1. If a domain needs a new engine type, add the re-export to
src/engine/types.tsin the same commit as the consumer. -
Domain contract is the only cross-domain surface. A domain's
extension.tsmay hold internal state, event subscriptions, and side effects. Other domains must go through the contract fromindex.ts. The boundary checker (rule 3) fails the build if a domain reaches into another's extension. -
The gateway is not a second authority. A
gateway(op="call")goes through the same registry invocation as a direct tool call. The safety net, skill policy, autonomy mapping, parking, before/after hooks, and audit row all apply under the capability's own name. If you add a tool, its action class and approval behavior are the same whether called directly or through the gateway. -
Stage 0 is a cold-start budget, not a code-quality rule. Adding a new reacher to
src/interactive/**from outside the Stage 0 closure requires a declared seam inSTAGE0_SEAMSwith a reason. The seam's own closure must stay off the Stage 0 modules, or the instant shell's chunk graph splits. -
Worker subprocesses are process-isolated by design. They have their own tool registry, their own session ledger mirror, and their own event bus. The orchestrator never observes worker tool calls directly; worker skill activations fold into the session ledger only through the dispatch terminal payload. Code that assumes shared state between orchestrator and worker will fail.
Source and generation metadata
title: "Architecture"
summary: "How Clio Coder composes its entry points, domain modules, tool surface, and worker runtime into a single coding agent process, and the static import rules that keep those layers decoupled."
sources:
- "src/core/domain-loader.ts"
- "src/entry/orchestrator.ts"
- "src/cli/index.ts"
- "src/engine/types.ts"
- "src/tools/registry.ts"
- "src/worker/entry.ts"
- "tests/boundaries/check-boundaries.ts"
symbols:
- "bootOrchestrator"
- "loadDomains"
- "createGatewayTool"
- "createContextTool"
- "createBackgroundMemoryModelClient"
tests:
- "tests/contracts/domain-lifecycle.test.ts"
- "tests/boundaries/check-boundaries.ts"
- "tests/contracts/gateway-authority.test.ts"
invariants:
- "Only `src/engine/**` may value-import `@earendil-works/pi-*` packages; all other layers consume erased shapes from `src/engine/types.ts`."
- "`src/worker/**` never value-imports `src/domains/**` except the provider runtime registry, builtin descriptors, and plugin loader modules used to rehydrate runtimes from stdin."
- "`src/domains/<x>/**` never imports `src/domains/<y>/extension.ts` for `y != x`; cross-domain access goes through the contract exported from `src/domains/<y>/index.ts`."
- "`src/tools/**` never imports `src/interactive/**` because the tool substrate is surface-agnostic and shared across headless, interactive, ACP, and worker runs."
- "The chat loop's turn modules (`src/interactive/turn-*.ts`, `chat-loop.ts`) never import `src/entry/**`; composition flows one direction from entry point to loop."
- "External runtime importers outside the Stage 0 closure may reach `src/interactive/**` and `src/engine/**` only through seams declared in `STAGE0_SEAMS` in `tests/boundaries/check-boundaries.ts`."
validate:
- "node --import tsx tests/boundaries/check-boundaries.ts"Clio Coder · Repository · Website · Documentation
Wiki v0.1 · Developing implementation reference · Source snapshot: 657dce13d. Authored architecture documents define the product contracts.
- Clio Coder GUI Client
- apps / clio-coder-gui
- Apps clio coder gui server
- Apps clio coder gui tests
- apps
- Architecture
- Command-line surfaces
- Core
- Domains agents
- Config Domain
- Context Domain
- Dispatch domain
- Domains evidence
- Domains extensions
- Domains gateway
- domains
- Domains interop
- Domains lifecycle
- Domains memory
- Middleware Domain
- Domains mux
- Domains observability
- Domains plugins
- Prompt Compiler
- Domains providers
- Domains quota
- Domains resources
- Domains safety
- Domains scheduling
- Domains session
- Vendored Tool Registry and Resolution
- Engine
- Engine acp
- Engine apis
- engine
- Entry point
- Interactive
- interactive
- Interactive overlays
- Interactive renderers
- clio-coder wiki
- Scripts
- Contract tests
- Tests extended
- tests
- Tools
- Tools data
- tools
- Tools verify
- Worker runtime