The Agent Contract
You are an agent or a UI. runex is the thing you drive. This page is the contract between you and it — the only interface you need, and the guarantees you can build on.
The rule that makes everything else fall out:
JSON is the truth. Every rendering is a view of it. Every operation returns one envelope. The closed event log is what happened.
You never parse prose. You never scrape source. You read structured data, act, and observe structured events back.
1. The Result envelope
Every operation — load, define_*, dispatch, reload, list_*, describe_*, manifest, events, analyze_conflicts — returns the same shape:
jsonc
{
"ok": true, // did the operation itself succeed?
"data": { ... }, // operation-specific payload (structured)
"error": null, // or {code, message, hint?} when !ok
"events": [ /* Event ... */ ], // the typed cascade this caused
"cursor": 412 // resume point for the event stream, or null
}Hard rules you can rely on:
- It is never raised, always returned. An operation does not throw at you; failure is
ok:falsewith a structurederror. (The one deliberate exception istrace()— a debug tool, see §7.) okmeans "the operation ran", not "nothing is wrong". A successfulanalyze_conflicts()that finds conflicts is stillok:true— the finding is indata. Branch ondata, not onok, for domain signals.error.codeis stable;error.messageis for humans. Branch oncode. Never regexmessage.hint(optional) is the suggested next move.cursoris the resume point for the event stream (§4). It is the tail of the scanned window, so it always moves forward even when a filter matched nothing.
jsonc
// failure example
{ "ok": false, "data": null,
"error": { "code": "unknown_action",
"message": "no action named 'promote_lead'",
"hint": "list actions via manifest()" },
"events": [], "cursor": null }CLI parity: every runex ontology <cmd> --json prints this envelope verbatim and exits non-zero iff ok:false. The --json output is the contract; the default Rich output is a courtesy view.
2. The closed Event taxonomy
Everything that happens is an Event:
jsonc
{
"kind": "field_set", // from a CLOSED vocabulary
"actor": "reactive:rollup_x", // provenance — see below
"target": "01HX…", // node id, or null
"payload": { "name": "score" }, // structured detail
"message": "rolled up contact", // human sentence, may be null
"ts": "2026-05-15T…Z", // when, or null for in-memory
"cursor": 412 // tx_log id, or null
}kind is drawn from a closed set. Do not hardcode it — read the authoritative list from manifest().data.event_kinds and match against that. It covers graph mutations (node_created, field_set, link_created, tagged, …), state (transitioned), and reactive decisions (dispatch_plan, conflict_blocked, guard_rejected, illegal_transition, dispatch_error).
The actor provenance invariant
actor tells you who caused the event, and it carries one invariant you can reason against:
reactive:<action>⟺ that action's effect committed. If you see this actor, the write happened.system:reactive⟺ a decision/observation, not a commit — a guard rejection, an illegal transition, a blocked conflict, a dispatch plan. The thing was considered and did not write.pipeline:ingest,cli:*,agent,system:bootstrap_*— origin of a non-reactive write.
So "did my rule actually fire?" is answered by the presence of a reactive:<name> actor, and "why didn't it?" by the matching system:reactive decision event. Nothing is swallowed — reactive rejections and errors are typed events, not silence.
3. manifest() — read the world before acting
One call returns the entire introspectable surface:
jsonc
{
"supertags": [ /* the type system: names, fields, natural_key */ ],
"machines": [ /* {name, supertag, initial, states, description} */ ],
"actions": [ /* {name, machine, from_states, trigger, guard,
effect, priority, conflict_policy, …} */ ],
"datasources": [ /* what can be ingested + what each needs, §5 */ ],
"latent_conflicts": { /* static hazards, §6 */ },
"kernels": [ "extract-wiki-links", … ], // the I/O escape hatches
"event_kinds": [ "field_set", "transitioned", … ] // the closed vocab
}This is your "what can I do, and what will I see back" — answer it from manifest(), never from documentation or code reading. It is the single source of truth for capability discovery.
runex ontology manifest (defaults to --json).
4. The event stream — observe over time
events(since=<cursor>, limit=…, kinds=[…]) replays the semantic log forward from a cursor:
jsonc
// Result of events(since=400)
{ "ok": true,
"data": { "count": 12 },
"events": [ /* 12 Events, ascending */ ],
"cursor": 412 } // ← pass this as the next `since`- Resumable, gapless, no replay.
since=cursorreturns strictlytx_id > cursor. Persist the cursor; you can stop and resume exactly. - Cross-process. It reads the append-only
tx_log, so you can tail events while another process ingests or dispatches. This is how a UI tray drives notifications and how a background agent watches the business change. - Filter without wedging.
kinds=[…]filters the payload, butcursorstill advances past unmatched traffic — a follower watching a rare kind never gets stuck replaying the same window.
CLI: runex ontology events --follow --since <cursor> --kind transitioned --json — a cross-process-safe tail.
This stream is the substrate for closed-loop behaviour: you act, you read the events your action caused (returned inline on dispatch), and a separate consumer tails the same log to react to the whole business, including cascades you did not initiate.
5. Datasources — what can flow in
manifest().data.datasources describes every ingest source as data, so you (or a product offering "connect a source") never read CLI source:
jsonc
{
"key": "wechat-session",
"summary": "WeChat conversations → WeChatConversation, rolled onto Person",
"supertag": "WeChatConversation",
"ontologies": ["wechat.scm"], // reactive bundles it activates
"params": [ { "name": "wechat_start", "type": "str",
"required": false, "help": "start date YYYY-MM-DD" }, … ],
"watch": null // or {kind, glob, default_path}
}A missing required param is refused with a structured error naming exactly what is missing — the registry validates, the transport just relays.
6. Pre-flight: analyze_conflicts()
Before you trust an ontology you just authored, ask whether two actions can fight over the same derived field:
jsonc
{ "ok": true,
"data": {
"field_conflicts": [
{ "field": "score",
"actions": ["on_chat", "on_friend"],
"error_on_conflict": ["on_chat"],
"severity": "blocking" } ], // "blocking" | "advisory"
"opaque_guarded_actions": [],
"blocking_count": 1,
"clean": false } }blocking— anerror-on-conflictaction is co-written elsewhere, including across different triggers the per-event plan can't see. This is a violated hard guarantee; fix it before relying on it.advisory— same-field writers under last-write-wins; maybe intended.clean— no findings at all (advisories count as findings).
CLI: runex ontology check --strict exits 1 on blocking_count > 0 — drop it in a pipeline as a real gate.
7. dispatch and the one debug exception
dispatch(action, target, actor=…) runs an action imperatively and returns the full envelope. data.ran is true iff the guard passed and the effect committed; events is the entire cascade it caused (reactive hops included). It never raises — a bad action name is ok:false, error.code:"unknown_action"; a guard rejection is ok:true, data.ran:false with the decision in events.
trace(action, target) is the single deliberate exception to "every operation returns a Result": it returns a richer OntologyTrace (.render(), .commits, .events) because it is a single-dispatch debug introspection tool, a different job from the operation contract. Use dispatch in production paths; use trace to understand a cascade.
8. The boundary you are on
runex owns what is true and why: the graph, the cascade, the audit log, the determinism guarantees. A product/UI owns how a human experiences it: notification policy, presentation, packaging.
The consequence for you: an autonomous agent and a human-facing UI consume the same Result / Event / manifest. There is no divergence between what you believe and what the user sees, because both are projections of one source of truth. Map event.kind → notification; render error.message; never re-derive truth in the view.
9. Guarantees you can build on
- Observable. No outcome is silent. Guard rejections, illegal transitions, dispatch errors, blocked conflicts are typed events.
- Runtime-malleable. New supertags / machines / actions load at runtime through one language (see
ontology-authoring.md), behind a migration gate that fails closed on destructive change — you can model generatively without corrupting stored data. - Deterministic. Reactive order is
priority ASC, name ASC, enforced at the point of dispatch.error-on-conflictfails closed (it refuses rather than risk an accidental winner, even for dynamic field targets). Same inputs → same cascade. Ambiguity is refused loudly, never resolved by accident.
These three — observable, runtime-malleable, deterministic — are the trinity that lets you safely model a user's business at runtime.