# Runtime

The runtime loop. Constructed with a `Graph`, an optional set of behaviors, an optional LLM provider, an optional budget, and an optional store. Drives goal runs to completion and persists state through the attached store.

For the conceptual model, see [`concepts/graph`](https://docs.activegraph.ai/concepts/graph/index.md) and [`concepts/behaviors`](https://docs.activegraph.ai/concepts/behaviors/index.md).

Drives behaviors over an event-sourced :class:`~activegraph.core.graph.Graph`.

The runtime owns the dispatch loop: an entry point emits an event (`run_goal`, or any mutation on the graph), matching behaviors fire against a runtime-built read-only :class:`~activegraph.core.view.View` each, and whatever they propose lands back in the log as more events, until the graph goes idle or the :class:`~activegraph.runtime.budget.Budget` ends the run. Construction wires the run-level choices: behaviors and tools (defaulting to the decorator registries), persistence (`persist_to=` / `store=`), the LLM provider with its retry and replay caches, policy, frame, budget, metrics, and outbound sinks. Failures inside behaviors become `behavior.failed` events, not exceptions (CONTRACT v1.0 #4b) — read them from :attr:`errors`; exceptions surface only at construction and entry points. :meth:`Runtime.load` rebuilds a recorded run from its log; :meth:`Runtime.fork` branches one from any historical event.

## `errors`

Accumulated `behavior.failed` events as structured tuples.

v1.0.3 #3. Reads from `self.graph._events` on each access — the events are the source of truth and this property is a projection. No caching, no listener registration, no new state. Callers can inspect failures programmatically without reaching into `graph._events` or parsing payload dicts.

Each :class:`BehaviorFailure` carries five operationally useful fields plus the underlying `behavior.failed` event id for callers that want to re-read the full payload (e.g., traceback, LLM payload extras).

## `add_sink(sink, *, name=None, queue_capacity=1024, overflow_policy=OverflowPolicy.DROP_NEWEST)`

Attach a bounded, isolated observer for future accepted events.

Historical events already reconstructed by `load` or `fork` are never offered. The runtime's Metrics backend records queue depth, successful delivery, declared drops, and adapter errors.

## `remove_sink(sink, *, timeout=5.0)`

Detach, drain, and close one sink within `timeout`.

## `sink_statuses()`

Return exact local status snapshots for attached sinks.

## `flush_sinks(timeout=5.0)`

Flush all attached sinks, applying `timeout` to each worker.

## `close_sinks(timeout=5.0)`

Detach and close all sinks, applying `timeout` to each worker.

## `dev_override(*, actor, reason, target_gate, scope, resulting_authority)`

Record and return one exact, run-local developer override receipt.

The event must cross normal graph/store acceptance before this method returns. Promotion, event logging, and R4 governance authority are rejected before emission.

## `dev_overrides()`

Reconstruct accepted developer override receipts from the log.

## `validate_dev_override(receipt, *, target_gate, scope, required_authority)`

Validate an exact receipt for one local gate decision.

No wildcard, prefix, cross-run, promotion, event-log, or R4 match is possible. The referenced event must still exist with identical fields.

## `authority_ceiling()`

The instance automatic-authority ceiling currently in force.

The last accepted `authority.ceiling_changed` event decides; with none recorded the default is `"none"` (nothing auto-approves). Reading from the log — like `dev_overrides` — means `load`, `fork`, and replay see the same ceiling without any snapshot state.

## `set_authority_ceiling(ceiling, *, actor, reason)`

Change the instance automatic-authority ceiling. Logged, explicit.

`ceiling` must be in the closed set `none | R0 | R1 | R2` — `R3`/`R4` are rejected loudly because outward and governance actions can never be made routine (CONTRACT v1.9 #2). `actor` and `reason` are mandatory provenance, exactly like a dev override. Emits `authority.ceiling_changed` (the durable record) and returns the accepted event id. The runtime stays level-agnostic: a product REQUESTS a ceiling here; enforcement of classes and gates never moves.

## `evaluate_capability_authority(*, capability, action_class, capability_ceiling=None, actor='runtime', caused_by=None)`

Evaluate one capability action on the canonical authority path.

The fixed evaluation order (CONTRACT v1.9 #2): missing/invalid `action_class` fails closed to approval; `R4` routes to the dedicated governance gate, always; `R3` requires approval, always; `R0`–`R2` auto-approve iff at or below the EFFECTIVE ceiling — the stricter of the instance ceiling and `capability_ceiling` (per-capability local policy, which can only ever lower). The legacy `risk_class` label is not an input and is never consulted or mapped (ADR 0016).

Every call emits an `authority.decision` audit event naming the capability, the declared class, the ceilings, the matched policy, and the decision (CONTRACT v1.9 #3); the returned frozen :class:`AuthorityDecision` carries the accepted event id.

## `run_quantum(*, max_queue_events=25, max_seconds=0.25)`

Drain a bounded cooperative quantum without claiming false idle.

Hosts with a single graph-writer thread can interleave reads and commands between quanta. Bounds are checked between queue events; one behavior invocation remains atomic. When work remains, no `runtime.idle` marker is emitted. The normal idle/budget marker is emitted exactly when this quantum actually reaches that state.

## `embed(texts, *, model=None, actor='runtime', caused_by=None)`

Embed `texts` through a recorded, content-keyed runtime path.

The request event stores only a content hash, never the input text; the response event stores the ordered vectors so replay can return without provider contact. Behavior code normally reaches this path through :meth:`Context.embed`, which supplies causal metadata.

## `status(recent=20)`

Frozen snapshot of the runtime. CONTRACT v0.8 #11.

Cheap to call. No graph traversal beyond a tail-slice of the event log. Returns immutable data; mutating any field raises.

`recent` controls the length of the `recent_events` tail. The CLI's `inspect --tail N` passes through.

## `load_pack(pack, settings=None)`

Load a pack into the runtime.

Returns True on first load, False if the same `(name, version)` was already loaded (CONTRACT v0.9 #6 idempotency). Raises `PackVersionConflictError` for name-match-version-mismatch and `PackConflictError` for any contributor name collision. Pre-mutation: a failed load leaves the runtime exactly as it was.

## `loaded_packs()`

List of currently-loaded packs.

## `disable_pack(name)`

Deregister a loaded pack: its behaviors stop firing NOW.

CONTRACT v1.4 #3. The pack's behaviors, tools, typed-object schemas, relation specs, gating policies, and settings are removed from this runtime's live registries and the registry is rebuilt — nothing pack-owned matches another event from this call onward. What deliberately does NOT happen:

- **State is untouched.** Objects and relations the pack created remain; disabling code never rewrites history.
- **Memory is not reclaimed.** Python cannot honestly unload imported code; the objects stay in memory, inert. Restart to evict (hosts doing boot-time pack loading key off the `pack.disabled` event this method emits).
- **Pending approvals remain pending** — resolvable or not, they are recorded state, not registry state.

Emits `pack.disabled` (queue-visible, like `pack.loaded`) with the deregistered surface, so behaviors and boot loaders can react from the log. Idempotent: disabling an already-disabled pack returns `False` and emits nothing. Re-enabling is `load_pack` again (which clears the disabled flag). Raises :class:`~activegraph.packs.PackNotFoundError` for a name this runtime never loaded.

Returns `True` when the pack was live and is now disabled.

## `get_behavior(name)`

Look up a registered behavior by canonical or short name.

Short names resolve when unambiguous (load-time conflict check guarantees this invariant). Raises `LookupError` if not found or `ValueError` if ambiguous. CONTRACT v0.9 #8.

## `get_tool(name)`

Look up a registered tool by canonical or short name.

Same resolution rule as `get_behavior`. CONTRACT v0.9 #8 / #9.

## `pending_approvals()`

List of currently-pending approvals (in creation order).

v1.4: graph-backed across restarts. `approval.proposed` events carry the full deferred payload, and `Runtime.load` / `fork()` rebuild this queue from the log (proposed minus granted), so a proposal survives a restart and can be approved by the reloaded runtime. Proposals recorded before v1.4 lack the payload data in their events and cannot be reconstructed — they surface only in the runtime instance that created them.

## `approve(approval_id, approved_by=None)`

Materialize a pending approval. Returns the new object id.

Raises `LookupError` if `approval_id` is not pending. Emits an `approval.granted` event followed by the deferred `object.created`.

## `save_state(path=None)`

Persist the event log.

- With a store already attached: flush (no path needed). If `path` is given it must match the attached store's path.
- Without a store: late-bind a SQLite store at `path` and append all in-memory events to it (CONTRACT v0.5 #5). Returns the path the events were written to.

## `load(path, run_id=None, *, behaviors=None, frame=None, policy=None, budget=None, seed=0, replay_strict=False, llm_provider=None, replay_llm_cache=False, llm_retry_max_attempts=3, llm_retry_initial_delay_seconds=0.5, llm_retry_max_delay_seconds=8.0, tools=None, replay_tool_cache=False, replay_reinvoke_deterministic=False, metrics=None, sinks=None, graph_store=None, native_structured_output=False, embedding_provider=None, replay_embedding_cache=False, trace_context_reads=False)`

Open `path`, choose a run, replay its events, return a Runtime wired to continue from where the log left off.

If `run_id` is None, loads the most recently appended-to run (CONTRACT v0.5 #6).

`replay_strict=True` re-fires behaviors from the recorded seed events and compares the resulting event-type stream (id, type) to the log. KNOWN LIMITATION (v0.5): payload-only drift is not detected; see CONTRACT v0.5 #7. Tightens in v0.6 with LLMs.

v0.8: `path` accepts a URL (sqlite:///... or postgres://...) in addition to a bare SQLite path. Backward-compatible.

v1.2: `graph_store` selects where the materialized projection lives while the log is replayed into it. Defaults to the in-memory store; pass a :class:`~activegraph.core.graph_store.GraphStore` (e.g. `FalkorDBGraphStore`) to rebuild the current-state view in an external graph database. The event log remains the source of truth — this only changes where the projection is materialized.

v1.10 #1: `trace_context_reads=True` turns on context-read tracing for execution that CONTINUES from the loaded log. Recorded `context.read` events replay like any other event either way, and strict replay never diverges on them.

## `fork(at_event, label=None, *, behaviors=None, llm_provider=None, replay_llm_cache=False, llm_retry_max_attempts=None, llm_retry_initial_delay_seconds=None, llm_retry_max_delay_seconds=None, tools=None, replay_tool_cache=False, replay_reinvoke_deterministic=False, metrics=None, sinks=None, graph_store=None, embedding_provider=None, replay_embedding_cache=False)`

Branch this run at `at_event` into an independent new run.

Requires a SQLite store. Copies events from the parent's log up to and including `at_event` into a fresh `run_id`, replays them into a new Graph, then returns a Runtime that operates on that Graph. Forks-of-forks work the same way (CONTRACT v0.5 #9).

v1.2: `graph_store` selects where the fork's materialized projection lives while the copied log is replayed into it. Defaults to the in-memory store; pass a :class:`~activegraph.core.graph_store.GraphStore` (e.g. `FalkorDBGraphStore`) to rebuild the fork's current-state view in an external graph database. The fork's event log remains the source of truth — this only changes where the projection is materialized.

## `promote(fork, *, dry_run=False)`

Apply `fork`'s net structural delta to this runtime.

The third piece of the fork → test → promote loop (CONTRACT v1.3 #4; design in `promote-design.md`). The receiver is the destination (parent), the argument the source — the `rt.diff(fork)` orientation.

The delta is computed three-way against this run's state at the recorded fork point: fork-only changes are applied as ordinary parent events; both-sides changes raise :class:`~activegraph.runtime.exec_errors.PromoteConflictError` before ANY mutation (fail-closed, atomic, no semantic merge); this run's own post-fork work is left alone. Referential integrity is part of the conflict check: promoted relations with missing endpoints, and promoted removals that would cascade away parent relations, both conflict.

`dry_run=True` returns the advisory :class:`~activegraph.runtime.promote.PromotePlan` without mutating anything. Apply always recomputes against the parent's current state — a stale dry-run plan cannot be handed back in; `computed_against` on the plan/result records the parent tip event id the applied plan actually saw.

Application is quiescent: the delta events append, project, and persist, but do not fire behaviors — the single reaction point is the `promote.applied` marker event, which is queue-visible and fires subscribed behaviors once, after the full delta is in place, seeing post-promote state. Pack loads and settings overrides are never applied; they surface in `plan.warnings` (adopting code is an explicit `load_pack`).

Requires both runtimes on the same SQLite store and `fork` to be a direct fork of this run per the store's lineage records (:class:`~activegraph.runtime.exec_errors.PromoteLineageError` otherwise; promote grandchildren one level at a time).

Point-in-time snapshot of a runtime for inspection surfaces.

What `activegraph status` renders: run id, coarse `state`, queue depth, events processed, a budget snapshot, the frame, the registered behaviors, and recent events. A read-only value object produced by `Runtime.status()` and serialized by `status_to_dict` for `--json` consumers.

Mission context for a run: the goal plus its guardrails.

`goal` is the one-line mission; `constraints`, `success_criteria`, and `permissions` are declarative lists stamped into assembled LLM prompts and visible to behaviors as `ctx.frame`. A frame describes intent — enforcement lives in :class:`~activegraph.policy.Policy` and the budget, not here.

Hard limits on a run. Budgets end runs gracefully; they don't raise.

Construct with a dict over the `KNOWN_LIMITS` dimensions (`max_events`, `max_behavior_calls`, `max_llm_calls`, `max_tool_calls`, `max_patches`, `max_depth`, `max_seconds`, `max_cost_usd`); any omitted dimension is unlimited. When a limit is hit the runtime stops dispatching and emits `runtime.budget_exhausted` — the log records which dimension ended the run. `max_cost_usd` accumulates in `Decimal` so per-call sub-cent costs don't drift across thousands of LLM calls (CONTRACT v0.6 #9).

## `start(*, read_wall_clock=True)`

Start budget accounting, optionally without ambient clock I/O.

Strict replay passes `read_wall_clock=False` and stops at the recorded accepted-event sequence instead of re-racing monotonic time.

## `remaining(*, check_wall_clock=True)`

Whether every enabled limit still has capacity.

## `mark_exhausted(key)`

Set the authoritative exhaustion reason for recorded replay.

## `cost_remaining(prospective_cost)`

Would `prospective_cost` push us past the ceiling? Returns True if it's safe to spend, False if it would exceed.

One accepted `dev.override` receipt scoped to a run and gate.

The receipt is evidence a local operator recorded an exact bypass intent; it grants nothing until that same gate validates it through the runtime.

Per-graph monotonic ID generator. Not thread-safe (single-threaded loop).

Objects share one global counter prefixed by type — `task#1`, `task#2`, `claim#3`, not `claim#1` (CONTRACT #1); events, relations, patches, and frames each have their own `evt_` / `rel_` / `patch_` / `frame_` sequence. Replay does not call this: recorded events carry their ids, which is what keeps forked and reloaded runs aligned with their logs.

## `reseed_from_events(events)`

Set counters past the highest id seen in `events`.

Used after replay so subsequent `object()/event()/...` continue monotonically from where the loaded log ended. Forks call this too, which is why two forks at the same point produce IDs that diverge identically (decision #12 — fine because the IDs live in different runs).

## Clocks

Real wall-clock UTC. ISO 8601 second precision, Z suffix.

The default time source for event timestamps. The runtime reads time only through this interface, so deterministic runs swap in :class:`FrozenClock` or :class:`TickingClock` and replay never depends on the machine clock.

Bases: `Clock`

Always returns the same timestamp. For tests and snapshots.

Every `now()` call yields the constructor's `t` unchanged, so an event log written under a FrozenClock is byte-for-byte reproducible. Use :class:`TickingClock` when a test needs ordering across timestamps rather than strict equality.

Bases: `Clock`

Monotonically advances by `step` seconds on every call.

For tests that care about ordering but don't want wall-clock noise: timestamps increase deterministically from the start value, so before/after assertions hold without sleeping or freezing time entirely.

## Logging + registry helpers

Configure the activegraph logger hierarchy.

Idempotent: repeated calls replace the existing handler rather than stacking. Returns the activegraph root logger.

Parameters:

| Name               | Type                                                   | Description                                                                                              | Default                       |
| ------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `level`            | \`str                                                  | int\`                                                                                                    | numeric or string level name. |
| `json_output`      | `bool`                                                 | True for the documented JSON-line format; False for the stdlib default (one human-readable line).        | `True`                        |
| `stream`           | `Any`                                                  | where to write. Defaults to stderr (the logging default).                                                | `None`                        |
| `payload_redactor` | `Optional[Callable[[dict[str, Any]], dict[str, Any]]]` | optional callable(dict) -> dict applied to any payload before it's added to a log record's extra fields. | `None`                        |

Snapshot of the global behavior registry (a shallow copy).

Decoration with `@behavior` / `@relation_behavior` / `@llm_behavior` appends to a module-level list; this returns a copy, so callers can filter or iterate without mutating registration state. `Runtime` snapshots it at construction when `behaviors=` is omitted — later registrations don't leak into already-built runtimes.

Empty the global behavior registry and return what was cleared.

Tests that need isolation between cases call this in a fixture; the return value is the list of removed behaviors in registration order, so multi-run scripts can capture them once and re-register via :func:`register` on each subsequent run without re-importing the modules whose `@behavior` decorators populated the registry in the first place. See the *Multi-run scripts* cookbook recipe.

v1.0.1: the return value is new. v1.0 returned `None`; callers that ignored the return still work unchanged.

Append an already-constructed behavior to the global registry.

The decorators (:func:`behavior`, :func:`relation_behavior`, :func:`llm_behavior`) register on definition; this function exists for the case where definition and registration are decoupled — most commonly, multi-run scripts that call :func:`clear_registry` between runs and need to re-populate the registry without re-importing the decorator-bearing modules:

.. code-block:: python

```text
from activegraph import clear_registry, register

cleared = clear_registry()        # capture before the first run
rt1 = Runtime(graph1); rt1.run_goal("first")

for b in cleared:                 # restore for the next run
    register(b)
rt2 = Runtime(graph2); rt2.run_goal("second")
```

See the *Multi-run scripts* cookbook recipe.

v1.0.1: new. v1.0 required reaching into the private `_REGISTRY` list — the user-test gate surfaced that as a rough edge.
