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, 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
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).
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.disabledevent 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
pathis given it must match the attached store's path. - Without a store: late-bind a SQLite store at
pathand 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)
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.
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. |
'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[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
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.