Skip to content

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.

text
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 events

Responsibilities ​

LayerOwnsDoes not own
coreStartup, admission, run lifecycle, checkpoints, shutdownProvider-specific wire parsing
loadConfig parsing, plugin discovery and JSON codecA running agent loop
persistenceAuthoritative state, compact archives and retention through a backend contractExecution policy or a storage format outside a backend
managerTarget-local allocation records, leases and managed configuration resolutionModel execution or Hub session policy
loopExchange progression, commits, event boundaries, recovery phasesHub sockets and startup YAML
model_io dataclassesSerializable conversation and loop progressLive sockets, subscriptions, process handles
llmConfigured provider instances and model exchange interfacesTool execution and disk saves
toolsDispatch, trust checks, complete tool resultsModel inference policy
processChild lifecycle policy, deadlines, serialized observation and rollbackNative launch/signal/reap implementation
process_ioI/O environment, input ordering, raw capture and pump joinsChild lifetime and tool policy
utils/subprocessOwned native child, opaque launch attachment, shell and executable-location helpersSession IDs, tool approval, capture policy
io / intercomJSON routing / reusable text WebSocket transportConversation semantics
HubSessions, launch supervision, operator access, event presentationAuthoritative 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.

Documentation tracks main (repository version 1.1.4); unreleased changes may differ from published packages.