Architecture
Simplex separates the worker's execution engine from the service that accepts operator input. Each worker process owns one application instance and session. The Hub can manage many such workers without sharing their AgentInputState.
Browser / operator
│ panel HTTP + WebSocket
▼
Hub ───────── configuration library / session metadata / plans
▲
│ persistent event socket + one-shot confirmation / tool sockets
│ (worker initiates every connection)
▼
core::Application
├── load: YAML, plugins, prompts, JSON codec
├── persistence: authoritative state, compact archives, retention
├── manager: target-local allocation records, leases, managed configuration
├── io / intercom: routing, queues, reconnecting transport
├── loop: request → model → tools → model → settled outcome
├── llm: provider instance → endpoint / protocol adapter
├── tools: session ToolRegistry and process/file/remote operations
└── hooks: session LoopHookRegistry and synchronous loop eventsResponsibilities
| Layer | Owns | Does not own |
|---|---|---|
core | Startup, admission, run lifecycle, checkpoints, shutdown | Provider-specific wire parsing |
load | Config parsing, plugin discovery and JSON codec | A running agent loop |
persistence | Authoritative state, compact archives and retention through a backend contract | Execution policy or a storage format outside a backend |
manager | Target-local allocation records, leases and managed configuration resolution | Model execution or Hub session policy |
loop | Exchange progression, commits, event boundaries, recovery phases | Hub sockets and startup YAML |
model_io dataclasses | Serializable conversation and loop progress | Live sockets, subscriptions, process handles |
llm | Configured provider instances and model exchange interfaces | Tool execution and disk saves |
tools | Dispatch, trust checks, complete tool results | Model inference policy |
process | Child lifecycle policy, deadlines, serialized observation and rollback | Native launch/signal/reap implementation |
process_io | I/O environment, input ordering, raw capture and pump joins | Child lifetime and tool policy |
utils/subprocess | Owned native child, opaque launch attachment, shell and executable-location helpers | Session IDs, tool approval, capture policy |
io / intercom | JSON routing / reusable text WebSocket transport | Conversation semantics |
| Hub | Sessions, launch supervision, operator access, event presentation | Authoritative conversation state |
Dependency injection and state
The loop receives its model, registry, event bus, state, and options explicitly. The application assembles those dependencies. Provider and hook instances may hold process-local state, but restartable conversation state belongs in the serializable dataclasses. A saved state is not a serialization of the worker process.
The driver model controls the ordinary conversation. An optional modality assistant is a separate model instance used by a tool for a single isolated request; it does not replace the driver's history or silently intercept inputs.
Startup and ownership
Startup validates configuration, discovers plugin descriptors, constructs the selected models and extensions, creates session registries, and restores or initializes state. Persistence ownership prevents cooperating workers from using the same session root simultaneously. Only after initialization does normal payload consumption begin.
The application owns the dependency lifetimes through shutdown. Registry handles keep plugin code alive while instances or tools still need it. Hook subscriptions are disconnected before their objects are released. Connections and background process tasks are joined as part of cleanup.
Read the loop lifecycle, persistence, and security boundaries before extending execution behavior.
Process identifiers are diagnostic uint64_t values, not handles for resource actions. Process management hides Boost.Process behind an owned implementation. The normal asynchronous shutdown joins tasks; emergency synchronous cleanup requires stopping the context and joining its worker threads first. This platform boundary preserves default pipe behavior and supports an opt-in tty tool argument. Rebuild C++ hosts and plugins together after these public layout/type changes; see the ABI contract.
Process output capture uses one raw-byte budget per process: stdout and stderr share LaunchSpec::max_output_bytes (default 64 KiB; zero means unlimited). Retained prefixes depend on read scheduling, with no fixed split between streams. Readers continue draining after exhaustion and mark each stream that loses bytes. Full/delta reads do not replenish capacity. This replaces the earlier independent per-stream limits; allocator/task overhead and formatted output are outside the raw-data budget. The Linux PosixPtyIo backend uses the same capture component for a single merged terminal stream. Native PTY allocation, controlling-terminal launch and resize stay in utils/subprocess; the environment supervises input and output tasks. Host code can compose it with subprocess::Child now, while tools select it with tty: true and return one generic output block. Defaults are canonical input, echo off, 80×24 and TERM=dumb; terminal EOF is a conditional request, not a pipe half-close. Shutdown can drain to a deadline, then close and join even if descendants retain the slave.