Skip to content

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:false with a structured error. (The one deliberate exception is trace() — a debug tool, see §7.)
  • ok means "the operation ran", not "nothing is wrong". A successful analyze_conflicts() that finds conflicts is still ok:true — the finding is in data. Branch on data, not on ok, for domain signals.
  • error.code is stable; error.message is for humans. Branch on code. Never regex message. hint (optional) is the suggested next move.
  • cursor is 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=cursor returns strictly tx_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, but cursor still 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 — an error-on-conflict action 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-conflict fails 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.