# Runtime Engine: Lemniscate Scope: the runtime loop for the agent (Nyx). It is a server, not a UI and not a borrowed coding harness. ## Runtime decision - Interactive harnesses (pi-agent-core, omp) are hosts: they wait for a human, end on session close, and do not run unattended. - Nimmerworld implements its own loop (this engine) and runs indefinitely. - Harnesses host nimmer-lib only. Clients never implement the agent brain; the runtime never borrows one. ## Topology Figure-eight with two lobes and two crossings: ``` Gate Summary In Action Result | | v Second Crossing v +--------------+ +--------------+ | LEFT LOBE | | RIGHT LOBE | | (intake) | | (effect) | +--------------+ +--------------+ ^ First Crossing ^ | | Gate Feedback Action Call (tool/actuator) ``` ### NATS vocabulary (label map) | Lemniscate label | NATS role | Transport | Delivery guarantee | |---|---|---|---| | sensor `pub ` | publisher | core NATS | fire-and-forget: only subscribers open at publish time get a copy; late subscribers get nothing | | `gate.summary`, `gate.decision` | subject (fan-in at L1, fan-out to gate layer) | core NATS | ephemeral, regenerated every pulse; nothing survives the cycle | | L1 (LLM) | subscriber on `gate.summary`, publisher on `gate.decision` | core NATS client | a swappable bus client, any language | | `action.call.` | stream (JetStream) | durable | at-least-once delivery, replayable | | actuator:`` | own JetStream consumer on its subject | durable | tracks its own position; a dead actuator stalls no one | | `action.result` | stream (JetStream) | durable | causality: `event_uuid -> cycle_uuid -> decision_uuid` | | client:`` | own durable consumer on `zone.state.>` / `action.result.>` | durable | replay-on-wake: reconnect and read what it missed | Rules in NATS words: fan-out is the subject matcher, not code (wildcard `*` = one token, `>` = one or more). Fan-in means many publishers onto one subject; each consumer position is independent, consumers never share state or corrupt each other. Idempotency is enforced by us, via dedupe on `decision_uuid`/`cycle_uuid`, because JetStream guarantees *at-least-once*, never *exactly-once*. Subject naming (open): the engine above uses short subjects (`gate.summary`, `action.call.`). Our live bus streams use `nimmerverse.register.>` / `nimmerverse.cycle.>`. Before any engine traffic flows, pick ONE naming convention and map it here. ### Bus transport (NATS / JetStream) The engine is host-agnostic: it never speaks to a model or an actuator directly, everything crosses the bus as structured JSON. ``` sensor/camera -> pub (raw events; gates subscribe via catalog) gate layer -> pub gate.summary (composer renders summary table, once per cycle) L1 (LLM) -> sub gate.summary / pub gate.decision (core NATS, ephemeral, cycle-scoped) L2 composer -> sub gate.decision validate targets vs drives (table lookup, once, upstream of fan-out) then pub action.call. per validated action actuator: -> own JetStream consumer on action.call..> client: -> own durable consumer on zone.state.> / action.result.> (replay-on-wake) ``` Placement rules: - `gate.summary` / `gate.decision` stay on core NATS: ephemeral, regenerated every pulse; never block the pulse loop on storage I/O. - Consequence subjects (`action.call.>`, `action.result`, causal records) are JetStream streams: at-least-once, survive restart, replayable via `event_uuid -> cycle_uuid -> decision_uuid`. - Fan-out is the subject matcher, not code: the L2 composer publishes to `action.call.` from the catalog; each actuator consumes its own subject and tracks its own position; a dead actuator stalls no one. - Validation happens once, in the composer, before the fan-out; consumers execute or nack, they never re-decide. - Idempotency rides the envelope: every action carries `decision_uuid` + `cycle_uuid`; actuators dedupe on them so redelivery cannot double-fire. - The LLM endpoint is a swappable bus client. vLLM today, harness tomorrow, zero engine changes. - Left lobe: intake. Gate summaries, evidence pulls, thresholds. - Right lobe: effect. Synthesized answer, action calls, client output. - L1 (first crossing): admission decision. Open/hold gates; shift thresholds; request evidence. - L2 (second crossing): synthesis. Produce answer and action-call list from the admitted content. - The crossings are the only synchronous points. All state resolves atomically there. ## Loop semantics - One traversal = one cycle. The lemniscate closes exactly once: L1 then L2. - Between crossings, traversal is local; only the crossing touches shared state. - The cycle is a transaction: no new input lands while it is open. - Feedback: the L2 answer is the salience signal for the next gate summary. It is not a gate. ## Data flow L1 inputs: - gate summary table: gate, impulses, threshold, last_opened, drives, last_decision_cycle, tier. - request parameters (evidence pull, k frames, probe query). L1 outputs: - gate decision: decision_uuid, opened gate list, parameters, threshold deltas. L2 inputs: - composed content from the opened gates (evidence rows, frames, text). - catalog of valid output gates from each opened gate's `drives`. L2 outputs: - answer text. - action-call list: action_uuid, target actuator/tool, parameters, tier. - gesture accumulator update. - salience signal for next summary. Validation: - output gate must be in the union of opened input gates' `drives`. - invalid decision: cycle re-runs. Membership is a table lookup. Gate summary envelope (what the model actually reads at L1): ```json { "cycle_uuid": "c-042", "gates": [ {"gate": "k8s_alert", "impulses": 4, "threshold": 10, "last_opened": 12, "drives": ["k8s_action"], "tier": "T1", "last_decision_cycle": "c-041"} ], "request_params": {"k_frames": 3, "probe": null} } ``` L1 replies exactly: ```json { "decision_uuid": "d-117", "opened": ["k8s_alert"], "params": {"k_frames": 5}, "threshold_deltas": {"k8s_alert": -2} } ``` ## Pulse clock ``` elapsed += dt while elapsed >= interval: crossing() elapsed -= interval ``` - Interval = mean(L1_RTT) + mean(L2_RTT) + safety_margin. - RTT is derived from v7 timestamps (impulse to decision), not from wall-clock probes. - Sliding-mean estimator with hysteresis. A single slow cycle must not stretch the interval. - Probe latencies are metadata on their gates; they never enter the cycle interval. - dt >= 1/pulse_rate would fire every tick once pulse_rate > 62; the accumulator prevents that. ## Map | Lemniscate | Nimmerworld | |---|---| | Director | runtime | | Zone / EmergentZone | bounded event episode | | Crossing L1 | gate summary read; gate open/hold | | Crossing L2 | composed input synthesized; answer + action call | | Slot tokens | participants | | Gesture accumulator | nimmer-lib trait algebra | | update_all_zones | NATS event loop | | Gate / Sensor | threshold channel upstream | | UUID causality | event_uuid -> cycle_uuid -> decision_uuid | | Intake tiers | summary poll + evidence pull | | Policy layers | hard floor (operator) > presence policy (harness) > attention policy (runtime) | | Gate tags | T0-T3 declared importance | | Adaptive timing | round-trips regulate interval and animation duration | | Register | declared sensors/gates/actuators/clients and `drives` links | ## Engine corrections 1. `_should_crossing` must accumulate time: - `elapsed += dt; while elapsed >= interval: crossing`. - `dt >= 1/pulse_rate` fires every tick once pulse_rate > 62 and makes the pulse rate meaningless. 2. Loops A/B must carry distinct semantics: - the crossing callback must know which loop it is on. - symmetric loops degrade the figure-eight into round-robin. - resolved by the L1/L2 split (intake vs effect). 3. `Director.update_all_zones` must call `zone.execute_turn(dt)`, not `zone.run_crossing()`: - otherwise the state machine never ticks. - terminal reports must be collected, not dropped. ## Interfaces (v1) - Core NATS (ephemeral): `gate.summary`, `gate.decision`. - JetStream (durable, at-least-once): `action.call.`, `action.result`, causal records. - Register/catalog: sensors, gates, actuators, clients, `drives`. Catalog entry declares the class path; runtime imports an explicit registry and fails loud on a missing class. - Clients hold own durable consumers on `zone.state.>` and `action.result.>` (replay-on-wake). Version: 0.12 | Updated: 2026-11-06