Skip to content

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 and concepts/behaviors.

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, and metrics. 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 property

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).

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.

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).

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, graph_store=None) classmethod

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.

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, graph_store=None)

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.

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).

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.

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

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.

Bases: Clock

Always returns the same timestamp. For tests and snapshots.

Bases: Clock

Monotonically advances by step seconds on every call. For tests that care about ordering but don't want wall-clock noise.

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.

'INFO'
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], dict]]

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).

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

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.