axiongraph 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
axiongraph/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ """AxionGraph — append-only execution-graph event model and deterministic reducer.
2
+
3
+ The bare ``axiongraph`` import is the core API; ``axiongraph.store_local`` exposes the
4
+ zero-service reference stores. This mirrors the TypeScript package's ``.`` and ``/store-local``
5
+ subpath exports."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ from axiongraph_core import (
12
+ Anomaly,
13
+ AnomalyKind,
14
+ EdgePayload,
15
+ EdgeStatus,
16
+ GraphEvent,
17
+ GraphEventType,
18
+ GraphState,
19
+ GraphStore,
20
+ GraphVocabulary,
21
+ NodePayload,
22
+ OnAnomaly,
23
+ ValidationResult,
24
+ canonicalize,
25
+ empty_state,
26
+ example_vocabulary,
27
+ reduce,
28
+ reduce_all,
29
+ subgraph,
30
+ validate,
31
+ )
32
+
33
+ try:
34
+ __version__ = version("axiongraph")
35
+ except PackageNotFoundError: # pragma: no cover - only during local source runs
36
+ __version__ = "0.0.0"
37
+
38
+ __all__ = [
39
+ "Anomaly",
40
+ "AnomalyKind",
41
+ "EdgePayload",
42
+ "EdgeStatus",
43
+ "GraphEvent",
44
+ "GraphEventType",
45
+ "GraphState",
46
+ "GraphStore",
47
+ "GraphVocabulary",
48
+ "NodePayload",
49
+ "OnAnomaly",
50
+ "ValidationResult",
51
+ "__version__",
52
+ "canonicalize",
53
+ "empty_state",
54
+ "example_vocabulary",
55
+ "reduce",
56
+ "reduce_all",
57
+ "subgraph",
58
+ "validate",
59
+ ]
axiongraph/py.typed ADDED
File without changes
@@ -0,0 +1,7 @@
1
+ """The ``axiongraph.store_local`` subpath: zero-service reference GraphStore adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from axiongraph_store_local import InMemoryStore, SqliteStore
6
+
7
+ __all__ = ["InMemoryStore", "SqliteStore"]
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: axiongraph
3
+ Version: 0.0.1
4
+ Summary: Invisible events. Replayable graphs. An append-only execution-graph event model and deterministic reducer — provider-agnostic, storage is a port.
5
+ Project-URL: Homepage, https://github.com/cachetronaut/axiongraph-py
6
+ Project-URL: Repository, https://github.com/cachetronaut/axiongraph-py
7
+ Project-URL: Issues, https://github.com/cachetronaut/axiongraph-py/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agentic,agents,ai,ai-agents,append-only,audit,axiongraph,dag,deterministic,directed-graph,event-log,event-sourcing,event-store,execution-graph,genai,graph,graph-events,graph-state,graph-store,immutable,langgraph,lineage,llm,multi-agent,observability,orchestration,provenance,reducer,replay,snapshot,sqlite,telemetry,tracing,workflow
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+
24
+ # AxionGraph (Python)
25
+
26
+ > Invisible events. Replayable graphs.
27
+
28
+ AxionGraph is an append-only event model and deterministic reducer for execution graphs.
29
+ It records graph events from agents, tools, workflows, and connectors, then folds them into
30
+ portable graph state for storage, replay, testing, and visualization.
31
+
32
+ This is the Python mirror of [`axiongraph` on npm](https://www.npmjs.com/package/axiongraph);
33
+ the two cores are kept in lockstep by shared cross-language parity fixtures.
34
+
35
+ ```python
36
+ from axiongraph import reduce_all
37
+
38
+ events = [
39
+ {"id": "evt_01", "runId": "run_01", "seq": 1, "ts": "2026-06-02T12:00:00.000Z",
40
+ "type": "node_created", "node": {"id": "agent_research", "kind": "agent", "label": "Research Agent"}},
41
+ {"id": "evt_02", "runId": "run_01", "seq": 2, "ts": "2026-06-02T12:00:01.000Z",
42
+ "type": "node_created", "node": {"id": "tool_web", "kind": "tool", "label": "Web Search"}},
43
+ {"id": "evt_03", "runId": "run_01", "seq": 3, "ts": "2026-06-02T12:00:02.000Z",
44
+ "type": "edge_created",
45
+ "edge": {"id": "edge_01", "kind": "called_tool", "from": "agent_research", "to": "tool_web", "status": "completed"}},
46
+ ]
47
+
48
+ state = reduce_all("run_01", events)
49
+ print(len(state.nodes)) # 2
50
+ print(len(state.edges)) # 1
51
+ ```
52
+
53
+ ## Core ideas
54
+
55
+ - Append-only events are the source of truth; graph state is derived by folding them.
56
+ - The reducer is pure and deterministic — identical event logs fold to byte-identical state.
57
+ - A monotonic `seq` per run defines order; wall-clock `ts` is advisory.
58
+ - Node/edge `kind` is an open taxonomy; supply a `GraphVocabulary` to reject unknown kinds.
59
+ - Storage is a port (`GraphStore`); rendering and realtime transport are consumer concerns.
60
+ - Event/payload shapes use the same wire keys as the TypeScript package (`runId`, `from`) so
61
+ the same events flow across both runtimes.
62
+
63
+ ## One package, opt-in extras
64
+
65
+ AxionGraph ships as a single `axiongraph` distribution. The core API is the top-level import;
66
+ optional backends are installed as extras (`pip install axiongraph[...]`). The local stores
67
+ need no extra — `sqlite3` is in the standard library.
68
+
69
+ | Import | Description |
70
+ | --- | --- |
71
+ | `axiongraph` | Event model, deterministic reducer, canonicalizer, vocabulary machinery, and the `GraphStore` port. |
72
+ | `axiongraph.store_local` | Zero-service reference adapters: an in-memory store and a `sqlite3`-backed durable store. |
73
+
74
+ ## Install
75
+
76
+ ```sh
77
+ pip install axiongraph # or: uv add axiongraph
78
+ ```
79
+
80
+ ## Storing and replaying events
81
+
82
+ ```python
83
+ from axiongraph.store_local import SqliteStore # or InMemoryStore
84
+
85
+ store = SqliteStore("./run.db") # ":memory:" by default
86
+ await store.append(events) # idempotent on (runId, seq)
87
+ state = await store.snapshot("run_01")
88
+ ```
89
+
90
+ Both `InMemoryStore` and `SqliteStore` satisfy the same `GraphStore` protocol and are
91
+ interchangeable; any future adapter that passes the shared contract suite drops in the same way.
92
+
93
+ ## Development
94
+
95
+ Python 3.11+ and [uv](https://docs.astral.sh/uv/). The repo is an internal package set
96
+ (`packages/core`, `packages/store-local`) assembled by hatchling into the one `axiongraph`
97
+ distribution.
98
+
99
+ ```sh
100
+ uv sync --dev
101
+ uv run ruff check . && uv run ruff format --check .
102
+ uv run ty check
103
+ uv run pytest
104
+ ```
105
+
106
+ ## Status
107
+
108
+ Early development. The reducer and reference stores are implemented and pass the same
109
+ parity fixtures as the TypeScript package.
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,18 @@
1
+ axiongraph/__init__.py,sha256=ASWHmnNJWbMARmWxDe04q5QS08M6_Ugb6Vmc6LKKVgM,1287
2
+ axiongraph/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ axiongraph/store_local.py,sha256=TpF6Eun0Q4Eu9NyOgC7i_-ljKj67iyITBeLnwCHzU58,233
4
+ axiongraph_core/__init__.py,sha256=DfVJFh4dWrPpqfYyz8n3BdUAYR24fFYmLrUsr_an0wA,975
5
+ axiongraph_core/canonical.py,sha256=OXVBJ75eJSvWi9anlbhEx4Utw-7c3sU0xPg9YwOlmqU,1031
6
+ axiongraph_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ axiongraph_core/reducer.py,sha256=0UNhdt42mo16dLVKuslwlnslLLSVPY8QdL9xhbYeyKI,3865
8
+ axiongraph_core/store.py,sha256=R9mY9lyxo51t-YAznp22cnZiH6AaBmMDGPKGQ7QI0mk,1239
9
+ axiongraph_core/types.py,sha256=FroEsilc9aP-QEWKkmEiW-ynlP91tg1YVTDoKV-ksvc,1809
10
+ axiongraph_core/vocabulary.py,sha256=p5UGBMf6hGba95mSrGz4APvFyL5ovUk45Hw7S3EibEU,2217
11
+ axiongraph_store_local/__init__.py,sha256=MMhML7xCxNDGq-vWfOFm3o32UpxfGBrM4lHPPGmE4b0,331
12
+ axiongraph_store_local/in_memory.py,sha256=LcVbViCDwzgcypFHFHTEmgttTAfm1lmKaEo35FKdNTg,1238
13
+ axiongraph_store_local/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ axiongraph_store_local/sqlite.py,sha256=9XeYpedeQyEvAjxkDF7K7vaDFlmyvVPlGPnCz2-9N3Y,2156
15
+ axiongraph-0.0.1.dist-info/METADATA,sha256=CRU1k7ege739Yd6Je911VFW9l9mTAnLgTNo5oixEbd4,4776
16
+ axiongraph-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ axiongraph-0.0.1.dist-info/licenses/LICENSE,sha256=sY7MtwwAOv-F1CsV_S47AsnkT_IPK2ifi1WBbsjLF-Q,1069
18
+ axiongraph-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cachetronaut
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ """axiongraph-core — the append-only event model, deterministic reducer, canonicalizer,
2
+ vocabulary machinery, and the GraphStore port. Published as part of the ``axiongraph`` package."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from .canonical import canonicalize
7
+ from .reducer import Anomaly, AnomalyKind, OnAnomaly, empty_state, reduce, reduce_all, subgraph
8
+ from .store import GraphStore
9
+ from .types import (
10
+ EdgePayload,
11
+ EdgeStatus,
12
+ GraphEvent,
13
+ GraphEventType,
14
+ GraphState,
15
+ NodePayload,
16
+ )
17
+ from .vocabulary import GraphVocabulary, ValidationResult, example_vocabulary, validate
18
+
19
+ __all__ = [
20
+ "Anomaly",
21
+ "AnomalyKind",
22
+ "EdgePayload",
23
+ "EdgeStatus",
24
+ "GraphEvent",
25
+ "GraphEventType",
26
+ "GraphState",
27
+ "GraphStore",
28
+ "GraphVocabulary",
29
+ "NodePayload",
30
+ "OnAnomaly",
31
+ "ValidationResult",
32
+ "canonicalize",
33
+ "empty_state",
34
+ "example_vocabulary",
35
+ "reduce",
36
+ "reduce_all",
37
+ "subgraph",
38
+ "validate",
39
+ ]
@@ -0,0 +1,26 @@
1
+ """Deterministic, key-sorted JSON for a state. Identical event logs fold to byte-identical
2
+ output (spec D5) — this is the contract the cross-language parity fixtures pin.
3
+
4
+ Mirrors the TypeScript ``canonicalize``: a ``{runId, seq, nodes, edges}`` shape with nodes and
5
+ edges sorted by id, then ``json.dumps`` with sorted keys and no whitespace (matching
6
+ ``JSON.stringify`` over a recursively key-sorted value)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, cast
12
+
13
+ from .types import GraphState
14
+
15
+
16
+ def canonicalize(state: GraphState) -> str:
17
+ """Return the canonical, byte-stable JSON string for ``state``."""
18
+ nodes = sorted(state.nodes.values(), key=lambda node: node["id"])
19
+ edges = sorted(state.edges.values(), key=lambda edge: cast(dict[str, Any], edge)["id"])
20
+ shape: dict[str, Any] = {
21
+ "runId": state.run_id,
22
+ "seq": state.seq,
23
+ "nodes": nodes,
24
+ "edges": edges,
25
+ }
26
+ return json.dumps(shape, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
File without changes
@@ -0,0 +1,101 @@
1
+ """The deterministic reducer. Pure and side-effect free (spec D5): no clock, no I/O,
2
+ no randomness. Identical event sequences fold to identical state."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable, Iterable
7
+ from dataclasses import dataclass, replace
8
+ from typing import Any, Literal, cast
9
+
10
+ from .types import EdgePayload, GraphEvent, GraphState, NodePayload
11
+
12
+ AnomalyKind = Literal["wrong_run", "stale_seq", "update_unknown_node", "update_unknown_edge"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Anomaly:
17
+ """Something the reducer noticed but did not apply. Observation only, never raised."""
18
+
19
+ kind: AnomalyKind
20
+ event: GraphEvent
21
+
22
+
23
+ OnAnomaly = Callable[[Anomaly], None]
24
+
25
+
26
+ def empty_state(run_id: str) -> GraphState:
27
+ """The fold's seed. ``seq`` starts at 0; the first applied event must have ``seq >= 1``."""
28
+ return GraphState(run_id=run_id, nodes={}, edges={}, seq=0)
29
+
30
+
31
+ def _report(on_anomaly: OnAnomaly | None, kind: AnomalyKind, event: GraphEvent) -> None:
32
+ if on_anomaly is not None:
33
+ on_anomaly(Anomaly(kind=kind, event=event))
34
+
35
+
36
+ def reduce(state: GraphState, event: GraphEvent, on_anomaly: OnAnomaly | None = None) -> GraphState:
37
+ """Apply one event. Events for another run, or with a non-increasing ``seq``, are
38
+ ignored idempotently (spec D3). Updates to unknown ids are dropped (spec D6)."""
39
+ if event["runId"] != state.run_id:
40
+ _report(on_anomaly, "wrong_run", event)
41
+ return state
42
+ if event["seq"] <= state.seq:
43
+ _report(on_anomaly, "stale_seq", event)
44
+ return state
45
+
46
+ event_type = event["type"]
47
+
48
+ if event_type == "node_created":
49
+ nodes = dict(state.nodes)
50
+ node = cast(NodePayload, event["node"])
51
+ nodes[node["id"]] = node
52
+ return replace(state, nodes=nodes, seq=event["seq"])
53
+
54
+ if event_type == "node_updated":
55
+ update = event["node"]
56
+ existing = state.nodes.get(update["id"])
57
+ if existing is None:
58
+ _report(on_anomaly, "update_unknown_node", event)
59
+ return replace(state, seq=event["seq"])
60
+ nodes = dict(state.nodes)
61
+ nodes[update["id"]] = cast(NodePayload, {**existing, **update})
62
+ return replace(state, nodes=nodes, seq=event["seq"])
63
+
64
+ if event_type == "edge_created":
65
+ edges = dict(state.edges)
66
+ edge = cast(EdgePayload, event["edge"])
67
+ edges[edge["id"]] = edge
68
+ return replace(state, edges=edges, seq=event["seq"])
69
+
70
+ if event_type == "edge_updated":
71
+ update = event["edge"]
72
+ existing = state.edges.get(update["id"])
73
+ if existing is None:
74
+ _report(on_anomaly, "update_unknown_edge", event)
75
+ return replace(state, seq=event["seq"])
76
+ edges = dict(state.edges)
77
+ edges[update["id"]] = cast(EdgePayload, {**existing, **update})
78
+ return replace(state, edges=edges, seq=event["seq"])
79
+
80
+ return state
81
+
82
+
83
+ def reduce_all(
84
+ run_id: str, events: Iterable[GraphEvent], on_anomaly: OnAnomaly | None = None
85
+ ) -> GraphState:
86
+ """Fold an event log into state. Sorted by ``seq`` first, so arrival order does not matter."""
87
+ state = empty_state(run_id)
88
+ for event in sorted(events, key=lambda candidate: candidate["seq"]):
89
+ state = reduce(state, event, on_anomaly)
90
+ return state
91
+
92
+
93
+ def subgraph(state: GraphState, keep_node: Callable[[NodePayload], bool]) -> GraphState:
94
+ """Derive a filtered view: keep matching nodes and any edge whose endpoints both survive."""
95
+ nodes = {node_id: node for node_id, node in state.nodes.items() if keep_node(node)}
96
+ edges: dict[str, EdgePayload] = {}
97
+ for edge_id, edge in state.edges.items():
98
+ endpoints: dict[str, Any] = cast(dict[str, Any], edge)
99
+ if endpoints["from"] in nodes and endpoints["to"] in nodes:
100
+ edges[edge_id] = edge
101
+ return replace(state, nodes=nodes, edges=edges)
@@ -0,0 +1,28 @@
1
+ """The :class:`GraphStore` port (spec D4): the seam between the event model and any backend.
2
+ Core defines the protocol; adapters (``axiongraph.store_local``, and later convex/neo4j/postgres)
3
+ implement it. Writes are append-only and idempotent on ``(runId, seq)`` (ties back to spec D3)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import AsyncIterator, Sequence
8
+ from typing import Protocol, runtime_checkable
9
+
10
+ from .types import GraphEvent, GraphState
11
+
12
+
13
+ @runtime_checkable
14
+ class GraphStore(Protocol):
15
+ """Append-only event storage. Realtime fan-out (an optional ``subscribe``) is where the
16
+ hosted product lives; the local adapter omits it. Core never assumes a realtime transport."""
17
+
18
+ async def append(self, events: Sequence[GraphEvent]) -> None:
19
+ """Append events. Idempotent on ``(runId, seq)``: re-appending a known seq is a no-op."""
20
+ ...
21
+
22
+ def read_events(self, run_id: str, since_seq: int = 0) -> AsyncIterator[GraphEvent]:
23
+ """Read a run's events in ``seq`` order, optionally only those after ``since_seq``."""
24
+ ...
25
+
26
+ async def snapshot(self, run_id: str) -> GraphState:
27
+ """The reduced state for a run. May be a live fold or a materialized cache."""
28
+ ...
@@ -0,0 +1,62 @@
1
+ """The append-only event model. The event log is the source of truth (spec D1);
2
+ live :class:`GraphState` is always a fold over events, never mutated in place.
3
+
4
+ The wire shapes mirror the TypeScript contracts and use the same camelCase keys
5
+ (``runId``, ``from``) so the cross-language parity fixtures are byte-identical.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Literal, NotRequired, TypedDict
12
+
13
+ GraphEventType = Literal["node_created", "node_updated", "edge_created", "edge_updated"]
14
+
15
+ EdgeStatus = Literal["proposed", "active", "completed", "failed", "blocked"]
16
+
17
+
18
+ class NodePayload(TypedDict):
19
+ """A node. ``metadata`` is present only when supplied (open taxonomy, spec D2)."""
20
+
21
+ id: str
22
+ kind: str
23
+ label: str
24
+ metadata: NotRequired[dict[str, Any]]
25
+
26
+
27
+ # ``from`` is a reserved word, so the edge shape needs the functional TypedDict syntax.
28
+ EdgePayload = TypedDict(
29
+ "EdgePayload",
30
+ {
31
+ "id": str,
32
+ "kind": str,
33
+ "from": str,
34
+ "to": str,
35
+ "status": EdgeStatus,
36
+ "metadata": NotRequired[dict[str, Any]],
37
+ },
38
+ )
39
+
40
+
41
+ class GraphEvent(TypedDict):
42
+ """A single append-only event. ``node`` / ``edge`` carry full payloads for
43
+ ``*_created`` and partial payloads (``id`` plus changed fields) for ``*_updated``."""
44
+
45
+ id: str
46
+ runId: str
47
+ seq: int
48
+ ts: str
49
+ type: GraphEventType
50
+ actor: NotRequired[str]
51
+ node: NotRequired[dict[str, Any]]
52
+ edge: NotRequired[dict[str, Any]]
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class GraphState:
57
+ """The reduced live state: two id-keyed maps plus the last applied sequence."""
58
+
59
+ run_id: str
60
+ nodes: dict[str, NodePayload] = field(default_factory=dict)
61
+ edges: dict[str, EdgePayload] = field(default_factory=dict)
62
+ seq: int = 0
@@ -0,0 +1,79 @@
1
+ """Open-taxonomy machinery (spec D2). Core ships the machinery and a neutral example set;
2
+ it never hard-codes a domain's vocabulary into the model."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from .types import GraphEvent
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class GraphVocabulary:
13
+ """A declared closed vocabulary of allowed node and edge kinds."""
14
+
15
+ node_kinds: frozenset[str]
16
+ edge_kinds: frozenset[str]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ValidationResult:
21
+ """The typed result of :func:`validate`. ``validate`` never raises on bad input."""
22
+
23
+ ok: bool
24
+ reason: str | None = None
25
+
26
+
27
+ def validate(event: GraphEvent, vocab: GraphVocabulary) -> ValidationResult:
28
+ """Reject ``*_created`` events whose kind is outside the supplied vocabulary."""
29
+ if event["type"] == "node_created":
30
+ kind = event["node"]["kind"]
31
+ if kind not in vocab.node_kinds:
32
+ return ValidationResult(ok=False, reason=f"unknown node kind: {kind}")
33
+ if event["type"] == "edge_created":
34
+ kind = event["edge"]["kind"]
35
+ if kind not in vocab.edge_kinds:
36
+ return ValidationResult(ok=False, reason=f"unknown edge kind: {kind}")
37
+ return ValidationResult(ok=True)
38
+
39
+
40
+ # A neutral example vocabulary for docs and tests. Reveals no product domain.
41
+ example_vocabulary = GraphVocabulary(
42
+ node_kinds=frozenset(
43
+ {
44
+ "human",
45
+ "agent",
46
+ "task",
47
+ "delegation",
48
+ "connector",
49
+ "tool",
50
+ "artifact",
51
+ "source",
52
+ "approval",
53
+ "policy_decision",
54
+ "budget_check",
55
+ "error",
56
+ "model_call",
57
+ }
58
+ ),
59
+ edge_kinds=frozenset(
60
+ {
61
+ "created_task",
62
+ "delegated_to",
63
+ "handoff_to",
64
+ "called_tool",
65
+ "called_connector",
66
+ "used_model",
67
+ "read_source",
68
+ "created_artifact",
69
+ "requested_approval",
70
+ "approved_by",
71
+ "denied_by",
72
+ "blocked_by_policy",
73
+ "blocked_by_budget",
74
+ "derived_from",
75
+ "cited_source",
76
+ "failed_with",
77
+ }
78
+ ),
79
+ )
@@ -0,0 +1,9 @@
1
+ """axiongraph-store-local — zero-service reference GraphStore adapters (in-memory + sqlite).
2
+ Published as the ``axiongraph.store_local`` subpath of the ``axiongraph`` package."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from .in_memory import InMemoryStore
7
+ from .sqlite import SqliteStore
8
+
9
+ __all__ = ["InMemoryStore", "SqliteStore"]
@@ -0,0 +1,31 @@
1
+ """A dict-backed GraphStore for tests and ephemeral runs (spec D4). Append-only and
2
+ idempotent on ``(runId, seq)``. No durability; everything lives in process memory."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import AsyncIterator, Sequence
7
+
8
+ from axiongraph_core import GraphEvent, GraphState, reduce_all
9
+
10
+
11
+ class InMemoryStore:
12
+ """A :class:`~axiongraph_core.GraphStore` backed by per-run lists kept sorted by ``seq``."""
13
+
14
+ def __init__(self) -> None:
15
+ self._logs: dict[str, list[GraphEvent]] = {}
16
+
17
+ async def append(self, events: Sequence[GraphEvent]) -> None:
18
+ for event in events:
19
+ log = self._logs.setdefault(event["runId"], [])
20
+ if any(existing["seq"] == event["seq"] for existing in log):
21
+ continue # idempotent on (runId, seq)
22
+ log.append(event)
23
+ log.sort(key=lambda candidate: candidate["seq"])
24
+
25
+ async def read_events(self, run_id: str, since_seq: int = 0) -> AsyncIterator[GraphEvent]:
26
+ for event in self._logs.get(run_id, []):
27
+ if event["seq"] > since_seq:
28
+ yield event
29
+
30
+ async def snapshot(self, run_id: str) -> GraphState:
31
+ return reduce_all(run_id, self._logs.get(run_id, []))
File without changes
@@ -0,0 +1,53 @@
1
+ """A durable single-file GraphStore backed by the stdlib ``sqlite3`` (spec D4). One ``events``
2
+ table keyed on ``(run_id, seq)``; ``append`` uses ``INSERT OR IGNORE`` so it is idempotent on
3
+ ``(runId, seq)``. No server, survives restarts. Snapshots live-fold the log."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import sqlite3
9
+ from collections.abc import AsyncIterator, Sequence
10
+ from typing import cast
11
+
12
+ from axiongraph_core import GraphEvent, GraphState, reduce_all
13
+
14
+
15
+ class SqliteStore:
16
+ """A :class:`~axiongraph_core.GraphStore` persisted to a single SQLite database file."""
17
+
18
+ def __init__(self, location: str = ":memory:") -> None:
19
+ """``location`` is a file path, or ``":memory:"`` (default) for an ephemeral database."""
20
+ self._db = sqlite3.connect(location)
21
+ self._db.execute(
22
+ "CREATE TABLE IF NOT EXISTS events ("
23
+ " run_id TEXT NOT NULL,"
24
+ " seq INTEGER NOT NULL,"
25
+ " payload TEXT NOT NULL,"
26
+ " PRIMARY KEY (run_id, seq)"
27
+ ")"
28
+ )
29
+
30
+ async def append(self, events: Sequence[GraphEvent]) -> None:
31
+ self._db.executemany(
32
+ "INSERT OR IGNORE INTO events (run_id, seq, payload) VALUES (?, ?, ?)",
33
+ [(event["runId"], event["seq"], json.dumps(event)) for event in events],
34
+ )
35
+ self._db.commit()
36
+
37
+ async def read_events(self, run_id: str, since_seq: int = 0) -> AsyncIterator[GraphEvent]:
38
+ rows = self._db.execute(
39
+ "SELECT payload FROM events WHERE run_id = ? AND seq > ? ORDER BY seq",
40
+ (run_id, since_seq),
41
+ ).fetchall()
42
+ for (payload,) in rows:
43
+ yield cast(GraphEvent, json.loads(payload))
44
+
45
+ async def snapshot(self, run_id: str) -> GraphState:
46
+ rows = self._db.execute(
47
+ "SELECT payload FROM events WHERE run_id = ? ORDER BY seq", (run_id,)
48
+ ).fetchall()
49
+ return reduce_all(run_id, [cast(GraphEvent, json.loads(payload)) for (payload,) in rows])
50
+
51
+ def close(self) -> None:
52
+ """Release the underlying database handle. Not part of the ``GraphStore`` port."""
53
+ self._db.close()