Skip to content

Graph

The graph and its primitives — objects, relations, patches, views, and events. The graph is a projection of the event log; mutations go through events. For the conceptual model see concepts/graph.

Event-sourced graph. The log is truth; objects/relations are projection.

The only mutator is :meth:emit: every add/patch/remove sugar method builds an event, appends it to the log, projects it into the materialized state (a pluggable :class:GraphStore, CONTRACT v1.2 #1), persists it when an EventStore is attached, offers isolated sinks, and notifies listeners. Read surfaces (objects, relations, neighborhood, match_chain) delegate to the projection's query hooks. Wiping the projection loses nothing — replaying the log rebuilds it. Accepted live events fan out through bounded EventSink workers; replay never does.

relations(source=None, target=None, type=None)

Return relations filtered by source, target, and/or type.

v1.0.4 #1: the canonical filter API on Graph. Decomposes the v0 get_relations(object_id=, direction=) axis into separate source and target slots so the call reads the way users already write it (matches docs/concepts/graph.md). Filter kwargs compose by AND; calling with no kwargs returns every relation. Graph.get_relations(object_id=, type=, direction=) stays as a backward-compatible alias.

objects(type=None, where=None)

Return objects matching type and/or where.

v1.0.3 #1: the canonical query API on Graph, mirroring View.objects(type=...) so call sites read the same inside and outside behaviors. Graph.query(object_type=...) is kept as a backward-compatible alias.

query(object_type=None, where=None)

Backward-compatible alias for :meth:objects. v1.0.3 #1.

New code should use graph.objects(type=...) — the kwarg type matches :meth:View.objects so the call reads the same in and out of behaviors.

add_sink(sink, *, name=None, queue_capacity=1024, overflow_policy='drop_newest', metrics=None)

Attach one isolated outbound observer of accepted live events.

The returned handle owns a bounded FIFO and daemon worker. A class name is used when name is omitted; names must be unique within the graph because they key both status and metrics. Historical events already present in the graph are never delivered.

remove_sink(sink, *, timeout=5.0)

Detach, drain, and close one sink within timeout.

sink_statuses()

Return active, closing, and retained failed status snapshots.

A timed-out close remains visible and retryable. A terminal close failure is retained until its name is reused or explicitly removed.

flush_sinks(timeout=5.0)

Flush every attached sink, with timeout applied per sink.

close_sinks(timeout=5.0)

Detach and close every sink, with timeout applied per sink.

attach_store(store)

Wire an EventStore as the durability sink. Idempotent on the same store. Calling with a different store after events exist is an error — events would be persisted in two places and you'd lose history.

emit(event)

Append, project, persist, offer sinks, then notify listeners.

patch_object(target, updates, *, actor='system', caused_by=None, frame_id=None, rationale=None, evidence=None, llm_request_event_id=None, tool_request_event_ids=None)

Auto-apply shortcut: build patch, version-check, emit applied/rejected.

Primitives

A typed node in the materialized graph projection.

data is the structured payload (schema-validated on add_object when a loaded pack owns the type); version increments once per applied patch (CONTRACT #4); provenance records the events that created and last touched it. A handle is for reading — mutations go through the graph's event surface (patch_object / patches), never by assigning to fields.

A typed edge between two object ids in the graph projection.

source and target hold object ids — dangling endpoints (ids with no object yet) are legal and queryable, so relations can arrive before the things they connect. type is the relation kind used by matching and traversal. Created via add_relation events; like Object, a projection of the log, not a hand-mutated record.

One immutable record in the append-only log. CONTRACT #3.

An event is a fact: type names what happened, payload carries the data, actor says who caused it, caused_by links the causal parent event, and frame_id scopes it to a mission frame. Events are never modified after emit — objects, relations, patches, and views are all projections derived from the sequence of these records (CONTRACT #2).

A proposed single-target mutation. CONTRACT #4 and #12.

op (create | update | replace | remove) targets exactly one object; expected_version makes application an optimistic concurrency check against the object's current version; status walks proposed -> applied or proposed -> rejected and an object is never mutated except by an applied patch's event. rationale and evidence carry the audit trail that approval flows read.

Read-only scoped slice of the graph, handed to behaviors as ctx.view.

Behaviors never query the live graph: they declare what they want via decorator metadata (view= specs like include_types or around) and the runtime builds the View before invocation (CONTRACT #11). A View is a point-in-time snapshot — filter it with :meth:objects / :meth:relations / :meth:events, but nothing done to it mutates the graph; mutations go through the context's propose/patch surface and land as events.

Diffs

Structural comparison of two runs (typically parent vs fork).

CONTRACT v0.5 #10: structural only. Non-lifecycle events split into the shared prefix and each side's tail; divergent_objects / divergent_relations list per-id final states that differ; is_identical is the no-divergence check. Semantic comparison ("do these two claims say the same thing?") is a behavior's job, not the runtime's.

One object id whose final state differs between parent and fork.

in_parent / in_fork are provenance-stripped to_dict snapshots so the comparison is structural, not timestamp noise; None on the side where the id doesn't exist. summary() renders the one-line form the CLI diff output prints.

One relation id whose final state differs between parent and fork.

Same shape as :class:DivergentObject: provenance-stripped snapshots per side, None where the id doesn't exist, and a summary() that names the endpoints and relation type for only-in-one-side cases.

Promote

What a promote would apply, computed against computed_against.

Returned by runtime.promote(fork, dry_run=True). Advisory only: apply always recomputes against parent-now, so a plan that was clean when computed can conflict by apply time (promote-design.md §3). is_promotable is the no-conflict check; warnings lists adjacent state promote deliberately does not move (fork-only pack loads / settings overrides — design §5).

is_promotable property

True when nothing conflicts — the plan can apply as-is.

is_empty property

True when the fork's delta is empty (nothing to promote).

A completed promote: the applied plan plus its audit anchors.

marker_event_id is the promote.applied event; every applied delta event is caused_by it and listed in applied_event_ids in emission order.

computed_against property

The parent tip event id the applied plan was computed against (design §3, review amendment #3). Delegates to the plan; surfaced here so results record it directly.

One entity that blocks a promote.

kind is a stable discriminator:

  • "both_changed" — the entity changed on both sides since the fork point (includes same-id both-created collisions, remove/modify pairs, and identical concurrent edits).
  • "dangling_relation" — a promoted relation's endpoint would not exist in parent-post-promote state.
  • "orphaning_removal" — a promoted object removal would cascade away a parent relation the delta doesn't remove.

in_base / in_parent / in_fork are normalized state snapshots (None where the entity doesn't exist on that side); detail is one human-readable sentence.