OpenProse
Reactor CLI

Compile, run, serve

The three core verbs in depth, the content-addressed compile cache, and the durable daemon's seven HTTP routes.

Compile, run, serve

These are the three core verbs. compile freezes intelligence into deterministic artifacts. run drains once. serve runs the durable daemon. All three reach the model surface and need a live key (OPENROUTER_API_KEY) plus the @openai/agents and zod peer deps. The keyless inspection commands (observability, DevTools replay) read what these verbs leave behind, so a single compiled state directory is the seam between the live side and the offline side.

compile

reactor compile [--force] [--check]

compile runs the intelligent compile sessions (Forme topology, the per-node canonicalizer, postconditions) and freezes them into a content-addressed IR cache under <state-dir>/compile/.

The content-addressed cache

The cache key is (contract-set fingerprint, SDK version, model id). Cost is never part of cache identity. An unchanged contract set recompiles at zero session cost, a cache hit.

The IR persists a serializable spec. A fresh process re-lowers each node's canonicalizer with the keyless compileNode(spec) call, with no model and no network, to mount it. That re-lowering is why the offline observability commands work after a compile with no key present. The programmatic equivalents are compileProject and runProject, the model-bearing boundary the CLI drives.

Flags

FlagMeaning
--forceRecompile regardless of cache freshness.
--checkExit non-zero if the cache is stale, and do not compile. The --check path is offline-safe and meant for CI.

Wire --check into CI to catch un-compiled contract changes:

reactor compile --check        # exit 1 when the cache is stale

run

reactor run

run is the one-shot verb. It ensures the IR is fresh (compiling if stale), boots the reactor, drains to quiescence, prints per-node dispositions plus cost, then exits. Use it for batch jobs and CI, where you want the system to settle once and report.

serve

reactor serve [--http <port>] [--host <addr>] [--concurrency <n>] [--poll-interval <ms>]

serve boots the durable host and blocks on the continuity driver loop until it receives SIGINT or SIGTERM.

The durable substrate

The host builds a durable substrate: a flat append-only receipt trail at <state-dir>/receipts.json and a filesystem world-model under <state-dir>/world-models. The reactor self-mounts and runs a boot cold-miss sweep, so a restart resumes from durable state rather than re-ingesting the backlog. reactor run and reactor trigger persist to the same flat layout, so any of them produces a state directory you can replay directly with reactor-devtools <state-dir>.

The continuity loop

Each tick, the driver polls gateways for ingress, then polls continuity across every reactor, surfaces a live cost line, and sleeps to the cadence ceiling. Gateways poll before continuity each tick so a freshly-staged arrival is visible to the same tick's continuity sweep.

--poll-interval <ms> sets the continuity cadence (default 60000). In v1 the loop sleeps this fixed interval between ticks. (Sleeping adaptively to the soonest armed self-recheck is deferred along with the default valid_until freshness projector -- until that ships, no self-recheck instants are armed, so the loop polls on the flat interval.)

The HTTP surface

--http <port> binds a zero-framework node:http server. It ships seven routes: one ingress and six read-only projections off the reactor's already-booted substrate. The GET routes never touch the model surface, so the surface is safe to poll for liveness and cost without spend.

Method and pathPurposeShape
POST /trigger/<node>An external wake of <node> (the webhook or manual ingress), serialized behind that reactor's queue.{ reactor, triggered, receiptsAdded, data?, dataDelivered? }
GET /healthLiveness: boot done, reactor count.{ ok, reactors, reactor }
GET /statusThe cost rollup plus node count and queue depth.{ reactor, nodes, queueDepth, cost }
GET /costThe cost rollup (the headline observability).{ reactor, ...costRollup }
GET /topologyThe node ids only -- a thin id list, not the wired DAG.{ reactor, nodes: NodeId[] }
GET /receiptsThe full ledger receipt stream.{ reactor, receipts: Receipt[] }
GET /nodes/<node>A node's published fingerprints, its last receipt, and a receipt count -- thin, not the node's full history.{ reactor, node, fingerprints, lastReceipt, receipts }

Two of the GET routes are deliberately thin. GET /topology returns the node ids as a flat list, not the wired DAG with edges (use reactor topology for the rendered graph). GET /nodes/<node> returns the node's published fingerprints, its single last receipt, and a count -- not the full per-node receipt history. GET /receipts is the one full projection: it streams the entire ledger.

The HTTP surface is namespaced per reactor under /<name>/..., with the prefix omitted for a single-reactor host. So a single-reactor host answers GET /cost, while a multi-reactor host answers GET /sales/cost and rejects an unprefixed GET /cost with a 404 telling you to prefix the path. There is no auth in v1: the host is one process for one operator.

POST /trigger/<node> accepts an optional JSON body and goes through the reactor's serialization queue, so an HTTP trigger never overlaps an in-flight drain. The body is validated as JSON (a malformed body is a 400) and, when the node is a configured gateway, staged into the node's ingress so it actually reaches the render. The response's dataDelivered reports whether that staging happened.

Bind address and safety

--host <addr> sets the bind address. The default is 127.0.0.1 -- loopback only.

v1 has no auth, and an unauthenticated POST /trigger/<node> can cause model spend. The server binds loopback by default for exactly this reason. Pass --host 0.0.0.0 only behind a trusted proxy that adds its own auth; serve prints a warning when it binds to a non-loopback address.

Graceful shutdown

On SIGINT or SIGTERM, the host stops arming new work, drains the in-flight queue for every reactor, closes the HTTP server, and exits 0. The SDK keeps no process alive; the CLI owns the loop.

The multi-reactor host and --concurrency

A reactors: list in reactor.yml hosts N isolated reactors, each with its own state directory, substrate, schedule, and cursors. The single-reactor case is just N=1: the host synthesizes one default reactor and the HTTP surface omits the /<name> prefix.

--concurrency N is an across-reactor worker-pool bound (default 1). Independent reactors render in parallel up to N; the v1 default of 1 means no cross-reactor parallelism unless you raise it.

Within a single reactor, drains stay strictly serial. At most one drain is in flight per reactor, behind a per-reactor serialization queue, because the SDK's single-flight atomicity requires it.

Within-reactor parallelism is a future enhancement. The current SDK has no within-reactor concurrency option, so --concurrency parallelizes reactors, not nodes within a reactor.

trigger

reactor trigger <node> [--data <json>|@file]

trigger fires an external wake at one node. In v1 it is a one-shot mount: it boots a transient reactor over the durable substrate, ingests the named node with a full external wake, drains to quiescence, and reports the dispositions. Like run and serve, it persists to the same flat <state-dir>/receipts.json trail, so the wake it injects is durable and replayable.

--data accepts inline JSON or @path to a JSON file. The wake itself carries no payload slot, so the parsed data is validated and surfaced in the report. For a running daemon, POST /trigger/<node> is the equivalent ingress.

There is no separate pull command. Ingest happens through the serve continuity cadence and through POST /trigger/<node>.

On this page