activegraph 1.1.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (173) hide show
  1. {activegraph-1.1.0/activegraph.egg-info → activegraph-1.2.0}/PKG-INFO +32 -12
  2. {activegraph-1.1.0 → activegraph-1.2.0}/README.md +23 -11
  3. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/__init__.py +7 -1
  4. activegraph-1.2.0/activegraph/core/__init__.py +8 -0
  5. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/core/graph.py +108 -96
  6. activegraph-1.2.0/activegraph/core/graph_store.py +356 -0
  7. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/core/view.py +11 -0
  8. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/budget.py +13 -0
  9. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/patterns.py +39 -67
  10. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/registry.py +3 -1
  11. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/runtime.py +39 -2
  12. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/view_builder.py +9 -4
  13. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/__init__.py +5 -0
  14. activegraph-1.2.0/activegraph/store/falkordb.py +647 -0
  15. activegraph-1.2.0/activegraph/store/graph_conformance.py +491 -0
  16. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/memory.py +10 -0
  17. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/decorators.py +16 -0
  18. {activegraph-1.1.0 → activegraph-1.2.0/activegraph.egg-info}/PKG-INFO +32 -12
  19. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph.egg-info/SOURCES.txt +5 -0
  20. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph.egg-info/requires.txt +6 -0
  21. {activegraph-1.1.0 → activegraph-1.2.0}/pyproject.toml +17 -1
  22. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_doc_links.py +6 -4
  23. activegraph-1.2.0/tests/test_falkordb_store.py +446 -0
  24. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_fork.py +20 -0
  25. activegraph-1.2.0/tests/test_graph_store.py +123 -0
  26. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llms_txt.py +49 -1
  27. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_persistence.py +23 -0
  28. activegraph-1.1.0/activegraph/core/__init__.py +0 -1
  29. {activegraph-1.1.0 → activegraph-1.2.0}/LICENSE +0 -0
  30. {activegraph-1.1.0 → activegraph-1.2.0}/NOTICE +0 -0
  31. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/__main__.py +0 -0
  32. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/behaviors/__init__.py +0 -0
  33. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/behaviors/base.py +0 -0
  34. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/behaviors/decorators.py +0 -0
  35. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/cli/__init__.py +0 -0
  36. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/cli/main.py +0 -0
  37. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/cli/quickstart.py +0 -0
  38. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/cli/renderers.py +0 -0
  39. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/core/clock.py +0 -0
  40. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/core/event.py +0 -0
  41. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/core/ids.py +0 -0
  42. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/core/patch.py +0 -0
  43. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/errors.py +0 -0
  44. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/frame.py +0 -0
  45. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/__init__.py +0 -0
  46. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/anthropic.py +0 -0
  47. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/cache.py +0 -0
  48. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/errors.py +0 -0
  49. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/openai.py +0 -0
  50. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/parsing.py +0 -0
  51. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/prompt.py +0 -0
  52. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/provider.py +0 -0
  53. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/recorded.py +0 -0
  54. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/llm/types.py +0 -0
  55. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/__init__.py +0 -0
  56. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/logging.py +0 -0
  57. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/metrics.py +0 -0
  58. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/migration.py +0 -0
  59. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/otel.py +0 -0
  60. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/prometheus.py +0 -0
  61. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/observability/status.py +0 -0
  62. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/__init__.py +0 -0
  63. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/__init__.py +0 -0
  64. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/behaviors.py +0 -0
  65. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  66. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  67. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/object_types.py +0 -0
  68. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  69. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  70. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  71. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  72. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/settings.py +0 -0
  73. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/diligence/tools.py +0 -0
  74. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/loader.py +0 -0
  75. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/packs/scaffold.py +0 -0
  76. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/policy.py +0 -0
  77. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/__init__.py +0 -0
  78. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/_live.py +0 -0
  79. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/behavior_graph.py +0 -0
  80. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/config_errors.py +0 -0
  81. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/diff.py +0 -0
  82. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/errors.py +0 -0
  83. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/exec_errors.py +0 -0
  84. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/queue.py +0 -0
  85. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/registration_errors.py +0 -0
  86. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/runtime/scheduler.py +0 -0
  87. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/base.py +0 -0
  88. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/conformance.py +0 -0
  89. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/errors.py +0 -0
  90. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/postgres.py +0 -0
  91. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/serde.py +0 -0
  92. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/sqlite.py +0 -0
  93. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/store/url.py +0 -0
  94. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/__init__.py +0 -0
  95. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/base.py +0 -0
  96. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/cache.py +0 -0
  97. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/context.py +0 -0
  98. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/errors.py +0 -0
  99. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/graph_query.py +0 -0
  100. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/recorded.py +0 -0
  101. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/tools/web_fetch.py +0 -0
  102. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/trace/__init__.py +0 -0
  103. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/trace/causal.py +0 -0
  104. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph/trace/printer.py +0 -0
  105. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph.egg-info/dependency_links.txt +0 -0
  106. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph.egg-info/entry_points.txt +0 -0
  107. {activegraph-1.1.0 → activegraph-1.2.0}/activegraph.egg-info/top_level.txt +0 -0
  108. {activegraph-1.1.0 → activegraph-1.2.0}/setup.cfg +0 -0
  109. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_activate_after.py +0 -0
  110. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_causal_cross_tool.py +0 -0
  111. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_cli.py +0 -0
  112. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_cli_docs_flags.py +0 -0
  113. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_clock.py +0 -0
  114. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_diff.py +0 -0
  115. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_diligence_pack.py +0 -0
  116. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_diligence_with_tools.py +0 -0
  117. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_doc_python_snippets.py +0 -0
  118. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_doc_site_reachable.py +0 -0
  119. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_errors_format.py +0 -0
  120. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_event.py +0 -0
  121. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_graph.py +0 -0
  122. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_ids.py +0 -0
  123. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_license.py +0 -0
  124. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_anthropic.py +0 -0
  125. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_behavior.py +0 -0
  126. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_budget.py +0 -0
  127. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_causal.py +0 -0
  128. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_claim_extraction.py +0 -0
  129. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_default_model.py +0 -0
  130. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_determinism.py +0 -0
  131. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_failure.py +0 -0
  132. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_openai.py +0 -0
  133. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_prompt.py +0 -0
  134. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_provider_fixtures.py +0 -0
  135. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_replay.py +0 -0
  136. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_tool_loop.py +0 -0
  137. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_trace.py +0 -0
  138. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_trace_snapshot.py +0 -0
  139. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_llm_types.py +0 -0
  140. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_migration.py +0 -0
  141. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_observability_logging.py +0 -0
  142. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_observability_metrics.py +0 -0
  143. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_operate_example.py +0 -0
  144. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_pack_scaffold.py +0 -0
  145. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_packs.py +0 -0
  146. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_patch.py +0 -0
  147. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_pattern_matcher.py +0 -0
  148. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_pattern_parser.py +0 -0
  149. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_pattern_subscriptions.py +0 -0
  150. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_postgres_store.py +0 -0
  151. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_quickstart.py +0 -0
  152. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_quickstart_snapshot.py +0 -0
  153. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_reason_codes_docs.py +0 -0
  154. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_replay.py +0 -0
  155. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_replay_trace_snapshot.py +0 -0
  156. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_requeue_unfired.py +0 -0
  157. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_resume_example.py +0 -0
  158. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_runtime.py +0 -0
  159. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_runtime_status.py +0 -0
  160. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_serde.py +0 -0
  161. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_store_conformance.py +0 -0
  162. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_store_url.py +0 -0
  163. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_tool_replay.py +0 -0
  164. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_tool_trace_snapshot.py +0 -0
  165. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_tools.py +0 -0
  166. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_trace.py +0 -0
  167. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_tutorial_snippets.py +0 -0
  168. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_v1_0_1_patches.py +0 -0
  169. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  170. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  171. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_version_sync.py +0 -0
  172. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_view.py +0 -0
  173. {activegraph-1.1.0 → activegraph-1.2.0}/tests/test_wheel_completeness.py +0 -0
@@ -1,10 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/yoheinakajima/activegraph
8
+ Project-URL: Documentation, https://docs.activegraph.ai/
9
+ Project-URL: Repository, https://github.com/yoheinakajima/activegraph
10
+ Project-URL: Changelog, https://docs.activegraph.ai/about/changelog/
11
+ Project-URL: Issues, https://github.com/yoheinakajima/activegraph/issues
8
12
  Keywords: graph,agents,event-sourcing,runtime
9
13
  Classifier: Programming Language :: Python :: 3
10
14
  Classifier: Programming Language :: Python :: 3.11
@@ -32,6 +36,10 @@ Requires-Dist: pydantic>=2; extra == "openai"
32
36
  Provides-Extra: sqlite
33
37
  Provides-Extra: postgres
34
38
  Requires-Dist: psycopg[binary]<4,>=3.1; extra == "postgres"
39
+ Provides-Extra: falkordb
40
+ Requires-Dist: falkordb>=1.0; extra == "falkordb"
41
+ Provides-Extra: falkordb-embedded
42
+ Requires-Dist: falkordblite>=0.10; extra == "falkordb-embedded"
35
43
  Provides-Extra: prometheus
36
44
  Requires-Dist: prometheus_client>=0.20; extra == "prometheus"
37
45
  Provides-Extra: opentelemetry
@@ -303,23 +311,34 @@ in [`docs/concepts/relations.md`](https://docs.activegraph.ai/concepts/relations
303
311
  - Not a rules engine, exactly. Rules engines forward-chain over
304
312
  facts. This event-sources over a graph and supports LLM behaviors
305
313
  as first-class.
306
- - Not a production graph database. The default store is SQLite,
307
- optionally Postgres. For a high-throughput graph backend, plug one
308
- in behind the `EventStore` protocol.
314
+ - Not a production graph database. The event log lives in SQLite
315
+ (default) or Postgres behind the `EventStore` protocol; the
316
+ materialized graph lives behind the `GraphStore` protocol
317
+ in-memory by default, or [FalkorDB](https://docs.activegraph.ai/guides/using-falkordb/)
318
+ for a real, traversable graph backend. For a different
319
+ high-throughput store, plug one in behind either protocol.
309
320
  - Not magic. Bad behaviors produce bad graphs. The runtime makes the
310
321
  badness inspectable, not absent.
311
322
 
312
323
  ## Status
313
324
 
314
- **v1.0 (stable)** (2026-05). The first-time-user gate per
315
- [CONTRACT v1.0 #C4](CONTRACT.md#v10-c4-v10-ships-as-v10-rc1-first-time-user-gate-is-owned-externally)
316
- ran through three rcs; v1.0 final ships rc3 plus a tutorial-step-7
317
- output fix and a README "Concepts at a glance" index. See
318
- [CHANGELOG.md](CHANGELOG.md) for the full v0 → v1.0 history and
319
- per-version migration notes.
325
+ **v1.2.0 (stable)** (2026-07). v1.0 shipped in May 2026 after a
326
+ three-rc external user-test gate per
327
+ [CONTRACT v1.0 #C4](CONTRACT.md#v10-c4-v10-ships-as-v10-rc1-first-time-user-gate-is-owned-externally);
328
+ v1.1.0 and v1.2.0 followed. See [CHANGELOG.md](CHANGELOG.md) for the
329
+ full v0 → v1.2 history and per-version migration notes.
320
330
 
321
331
  Major shipped milestones:
322
332
 
333
+ - **v1.2** — the `GraphStore` seam: the materialized projection
334
+ becomes pluggable, with `FalkorDBGraphStore` (native edges, Cypher
335
+ query push-down) as the first external backend, contributed by
336
+ [@dudizimber](https://github.com/dudizimber); the test suite
337
+ becomes a CI gate.
338
+ - **v1.1** — bounded LLM retries for transient provider failures,
339
+ `inspect --memo` / `inspect --search`, `fork --set`, OpenAI
340
+ tool-shape parity, `OpenTelemetryMetrics`, and the
341
+ spec-vs-impl drift gates.
323
342
  - **v1.0** — error hierarchy rewrite with per-error reference
324
343
  pages, doc site at [docs.activegraph.ai](https://docs.activegraph.ai/),
325
344
  `activegraph quickstart` command, mypy `--strict` and docstring
@@ -340,8 +359,9 @@ Major shipped milestones:
340
359
  patches with optimistic concurrency, views, frames, policies,
341
360
  budgets, the trace.
342
361
 
343
- Roadmap items planned for v1.1 are tracked in
344
- [CONTRACT.md § v1.1](CONTRACT.md).
362
+ Roadmap items for the current cycle are tracked in
363
+ [ROADMAP.md](ROADMAP.md); unscheduled candidates live in
364
+ [FUTURE_IDEAS.md](FUTURE_IDEAS.md).
345
365
 
346
366
  ## License
347
367
 
@@ -240,23 +240,34 @@ in [`docs/concepts/relations.md`](https://docs.activegraph.ai/concepts/relations
240
240
  - Not a rules engine, exactly. Rules engines forward-chain over
241
241
  facts. This event-sources over a graph and supports LLM behaviors
242
242
  as first-class.
243
- - Not a production graph database. The default store is SQLite,
244
- optionally Postgres. For a high-throughput graph backend, plug one
245
- in behind the `EventStore` protocol.
243
+ - Not a production graph database. The event log lives in SQLite
244
+ (default) or Postgres behind the `EventStore` protocol; the
245
+ materialized graph lives behind the `GraphStore` protocol
246
+ in-memory by default, or [FalkorDB](https://docs.activegraph.ai/guides/using-falkordb/)
247
+ for a real, traversable graph backend. For a different
248
+ high-throughput store, plug one in behind either protocol.
246
249
  - Not magic. Bad behaviors produce bad graphs. The runtime makes the
247
250
  badness inspectable, not absent.
248
251
 
249
252
  ## Status
250
253
 
251
- **v1.0 (stable)** (2026-05). The first-time-user gate per
252
- [CONTRACT v1.0 #C4](CONTRACT.md#v10-c4-v10-ships-as-v10-rc1-first-time-user-gate-is-owned-externally)
253
- ran through three rcs; v1.0 final ships rc3 plus a tutorial-step-7
254
- output fix and a README "Concepts at a glance" index. See
255
- [CHANGELOG.md](CHANGELOG.md) for the full v0 → v1.0 history and
256
- per-version migration notes.
254
+ **v1.2.0 (stable)** (2026-07). v1.0 shipped in May 2026 after a
255
+ three-rc external user-test gate per
256
+ [CONTRACT v1.0 #C4](CONTRACT.md#v10-c4-v10-ships-as-v10-rc1-first-time-user-gate-is-owned-externally);
257
+ v1.1.0 and v1.2.0 followed. See [CHANGELOG.md](CHANGELOG.md) for the
258
+ full v0 → v1.2 history and per-version migration notes.
257
259
 
258
260
  Major shipped milestones:
259
261
 
262
+ - **v1.2** — the `GraphStore` seam: the materialized projection
263
+ becomes pluggable, with `FalkorDBGraphStore` (native edges, Cypher
264
+ query push-down) as the first external backend, contributed by
265
+ [@dudizimber](https://github.com/dudizimber); the test suite
266
+ becomes a CI gate.
267
+ - **v1.1** — bounded LLM retries for transient provider failures,
268
+ `inspect --memo` / `inspect --search`, `fork --set`, OpenAI
269
+ tool-shape parity, `OpenTelemetryMetrics`, and the
270
+ spec-vs-impl drift gates.
260
271
  - **v1.0** — error hierarchy rewrite with per-error reference
261
272
  pages, doc site at [docs.activegraph.ai](https://docs.activegraph.ai/),
262
273
  `activegraph quickstart` command, mypy `--strict` and docstring
@@ -277,8 +288,9 @@ Major shipped milestones:
277
288
  patches with optimistic concurrency, views, frames, policies,
278
289
  budgets, the trace.
279
290
 
280
- Roadmap items planned for v1.1 are tracked in
281
- [CONTRACT.md § v1.1](CONTRACT.md).
291
+ Roadmap items for the current cycle are tracked in
292
+ [ROADMAP.md](ROADMAP.md); unscheduled candidates live in
293
+ [FUTURE_IDEAS.md](FUTURE_IDEAS.md).
282
294
 
283
295
  ## License
284
296
 
@@ -61,7 +61,10 @@ from activegraph.store import (
61
61
  DuplicateEventError,
62
62
  EventNotFoundError,
63
63
  EventStore,
64
+ FalkorDBGraphStore,
65
+ GraphStore,
64
66
  InMemoryEventStore,
67
+ InMemoryGraphStore,
65
68
  InvalidStoreURL,
66
69
  NonSerializableEventError,
67
70
  RunRecord,
@@ -142,11 +145,14 @@ __all__ = [
142
145
  "EventNotFoundError",
143
146
  "EventStore",
144
147
  "ExecutionError",
148
+ "FalkorDBGraphStore",
145
149
  "Frame",
146
150
  "FrozenClock",
147
151
  "Graph",
152
+ "GraphStore",
148
153
  "IDGen",
149
154
  "InMemoryEventStore",
155
+ "InMemoryGraphStore",
150
156
  "IncompatibleRuntimeState",
151
157
  "InternalEvaluatorError",
152
158
  "InvalidActivateAfter",
@@ -224,4 +230,4 @@ __all__ = [
224
230
  "tool",
225
231
  ]
226
232
 
227
- __version__ = "1.1.0"
233
+ __version__ = "1.2.0"
@@ -0,0 +1,8 @@
1
+ """Core primitives. Knows nothing about runtime or behaviors."""
2
+
3
+ from activegraph.core.graph_store import GraphStore, InMemoryGraphStore
4
+
5
+ __all__ = [
6
+ "GraphStore",
7
+ "InMemoryGraphStore",
8
+ ]
@@ -28,6 +28,7 @@ from typing import Any, Callable, Optional
28
28
 
29
29
  from activegraph.core.clock import Clock
30
30
  from activegraph.core.event import Event
31
+ from activegraph.core.graph_store import GraphStore, InMemoryGraphStore
31
32
  from activegraph.core.ids import IDGen
32
33
  from activegraph.core.patch import Patch
33
34
 
@@ -104,16 +105,19 @@ class Graph:
104
105
  ids: Optional[IDGen] = None,
105
106
  clock: Optional[Clock] = None,
106
107
  run_id: Optional[str] = None,
108
+ graph_store: Optional[GraphStore] = None,
107
109
  ) -> None:
108
110
  self.ids = ids or IDGen()
109
111
  self.clock = clock or Clock()
110
112
  # CONTRACT v0.5 #6: every graph has a run_id.
111
113
  self.run_id: str = run_id or self.ids.run()
112
114
 
113
- # projected state — touched ONLY by apply_event (CONTRACT v0.5 #15)
114
- self._objects: dict[str, Object] = {}
115
- self._relations: dict[str, Relation] = {}
116
- self._patches: dict[str, Patch] = {}
115
+ # projected state — touched ONLY by apply_event (CONTRACT v0.5 #15).
116
+ # v1.2: the projection lives behind a GraphStore so it can be kept
117
+ # in process memory (the default) or pushed into an external graph
118
+ # database. `self._state` is the materialized current-state view; it
119
+ # is NOT the durable event log (that is `self._store`, an EventStore).
120
+ self._state: GraphStore = graph_store or InMemoryGraphStore()
117
121
 
118
122
  # the log
119
123
  self._events: list[Event] = []
@@ -147,19 +151,19 @@ class Graph:
147
151
  return frozenset(self._replayed_ids)
148
152
 
149
153
  def get_object(self, id_: str) -> Optional[Object]:
150
- return self._objects.get(id_)
154
+ return self._state.get_object(id_)
151
155
 
152
156
  def get_relation(self, id_: str) -> Optional[Relation]:
153
- return self._relations.get(id_)
157
+ return self._state.get_relation(id_)
154
158
 
155
159
  def get_patch(self, id_: str) -> Optional[Patch]:
156
- return self._patches.get(id_)
160
+ return self._state.get_patch(id_)
157
161
 
158
162
  def all_objects(self) -> list[Object]:
159
- return list(self._objects.values())
163
+ return self._state.all_objects()
160
164
 
161
165
  def all_relations(self) -> list[Relation]:
162
- return list(self._relations.values())
166
+ return self._state.all_relations()
163
167
 
164
168
  def get_relations(
165
169
  self,
@@ -167,19 +171,25 @@ class Graph:
167
171
  type: Optional[str] = None,
168
172
  direction: str = "both",
169
173
  ) -> list[Relation]:
170
- out: list[Relation] = []
171
- for r in self._relations.values():
172
- if type is not None and r.type != type:
173
- continue
174
- if object_id is not None:
175
- if direction == "outgoing" and r.source != object_id:
176
- continue
177
- if direction == "incoming" and r.target != object_id:
178
- continue
179
- if direction == "both" and object_id not in (r.source, r.target):
180
- continue
181
- out.append(r)
182
- return out
174
+ # Pushed down to the store via find_relations (see graph_store.py).
175
+ # Decompose the (object_id, direction) axis into source/target slots:
176
+ # "outgoing"/"incoming" map to a single directional filter; "both"
177
+ # needs source-OR-target, which find_relations (AND-only) can't express,
178
+ # so we push the type filter and apply the membership test here. An
179
+ # unrecognized direction keeps the v0 quirk of ignoring object_id.
180
+ if object_id is None:
181
+ return self._state.find_relations(type=type)
182
+ if direction == "outgoing":
183
+ return self._state.find_relations(source=object_id, type=type)
184
+ if direction == "incoming":
185
+ return self._state.find_relations(target=object_id, type=type)
186
+ if direction == "both":
187
+ return [
188
+ r
189
+ for r in self._state.find_relations(type=type)
190
+ if object_id in (r.source, r.target)
191
+ ]
192
+ return self._state.find_relations(type=type)
183
193
 
184
194
  def relations(
185
195
  self,
@@ -197,40 +207,27 @@ class Graph:
197
207
  relation. ``Graph.get_relations(object_id=, type=, direction=)``
198
208
  stays as a backward-compatible alias.
199
209
  """
200
- out: list[Relation] = []
201
- for r in self._relations.values():
202
- if source is not None and r.source != source:
203
- continue
204
- if target is not None and r.target != target:
205
- continue
206
- if type is not None and r.type != type:
207
- continue
208
- out.append(r)
209
- return out
210
+ # Pushed down to the store (find_relations applies the same AND filter).
211
+ return self._state.find_relations(source=source, target=target, type=type)
210
212
 
211
213
  def neighborhood(self, object_id: str, depth: int = 1) -> tuple[list[Object], list[Relation]]:
212
- if object_id not in self._objects:
213
- return ([], [])
214
- seen_objs = {object_id}
215
- frontier = {object_id}
216
- seen_rels: set[str] = set()
217
- for _ in range(depth):
218
- next_frontier: set[str] = set()
219
- for r in self._relations.values():
220
- if r.source in frontier or r.target in frontier:
221
- seen_rels.add(r.id)
222
- if r.source not in seen_objs:
223
- next_frontier.add(r.source)
224
- if r.target not in seen_objs:
225
- next_frontier.add(r.target)
226
- seen_objs |= next_frontier
227
- frontier = next_frontier
228
- if not frontier:
229
- break
230
- return (
231
- [self._objects[i] for i in seen_objs if i in self._objects],
232
- [self._relations[i] for i in seen_rels if i in self._relations],
233
- )
214
+ # Pushed down to the store (default is the same BFS; FalkorDB uses a
215
+ # native variable-length path query). See GraphStore.neighborhood.
216
+ return self._state.neighborhood(object_id, depth)
217
+
218
+ def match_chain(self, node_types, rels):
219
+ # Pushed down to the store: the default mirrors the matcher's
220
+ # structural chain walk; FalkorDB resolves the whole chain in one
221
+ # Cypher query. Node {prop: value} equality and WHERE stay in the
222
+ # matcher (Python). See GraphStore.match_chain.
223
+ return self._state.match_chain(node_types, rels)
224
+
225
+ def objects_in_types(self, types: list[str]) -> list[Object]:
226
+ # Pushed down to the store: OR-of-types in one query (FalkorDB uses
227
+ # ``type IN [...]``); the default single-pass scan preserves order.
228
+ # Used by the view builder for type-scoped views. See
229
+ # GraphStore.find_objects_in_types.
230
+ return self._state.find_objects_in_types(types)
234
231
 
235
232
  def objects(
236
233
  self,
@@ -244,10 +241,10 @@ class Graph:
244
241
  and outside behaviors. `Graph.query(object_type=...)` is kept
245
242
  as a backward-compatible alias.
246
243
  """
244
+ # Type filter is pushed down to the store via find_objects; the
245
+ # ``where`` predicate is evaluated in Python over that subset.
247
246
  out: list[Object] = []
248
- for o in self._objects.values():
249
- if type is not None and o.type != type:
250
- continue
247
+ for o in self._state.find_objects(type):
251
248
  if where and not _eval_where_on_object(where, o):
252
249
  continue
253
250
  out.append(o)
@@ -267,7 +264,8 @@ class Graph:
267
264
  return self.objects(type=object_type, where=where)
268
265
 
269
266
  def has_object_of_type(self, type_: str) -> bool:
270
- return any(o.type == type_ for o in self._objects.values())
267
+ # Pushed down via find_objects (scopes the scan to ``type_``).
268
+ return bool(self._state.find_objects(type_))
271
269
 
272
270
  # ---------- listener API (runtime hooks here) ----------
273
271
 
@@ -399,7 +397,7 @@ class Graph:
399
397
  timestamp=self.clock.now(),
400
398
  )
401
399
  self.emit(event)
402
- return self._objects[obj_id]
400
+ return self._state.get_object(obj_id)
403
401
 
404
402
  def add_relation(
405
403
  self,
@@ -418,8 +416,8 @@ class Graph:
418
416
  clean = _strip_provenance(copy.deepcopy(data or {}))
419
417
  # v0.9: relation type validation (source/target type rules).
420
418
  if self._pack_relation_validator is not None:
421
- src_obj = self._objects.get(source)
422
- tgt_obj = self._objects.get(target)
419
+ src_obj = self._state.get_object(source)
420
+ tgt_obj = self._state.get_object(target)
423
421
  self._pack_relation_validator(
424
422
  type,
425
423
  src_obj.type if src_obj else None,
@@ -456,7 +454,7 @@ class Graph:
456
454
  timestamp=self.clock.now(),
457
455
  )
458
456
  self.emit(event)
459
- return self._relations[rel_id]
457
+ return self._state.get_relation(rel_id)
460
458
 
461
459
  def remove_relation(
462
460
  self,
@@ -466,7 +464,7 @@ class Graph:
466
464
  caused_by: Optional[str] = None,
467
465
  frame_id: Optional[str] = None,
468
466
  ) -> None:
469
- if relation_id not in self._relations:
467
+ if self._state.get_relation(relation_id) is None:
470
468
  return
471
469
  event = Event(
472
470
  id=self.ids.event(),
@@ -487,7 +485,7 @@ class Graph:
487
485
  caused_by: Optional[str] = None,
488
486
  frame_id: Optional[str] = None,
489
487
  ) -> None:
490
- if object_id not in self._objects:
488
+ if self._state.get_object(object_id) is None:
491
489
  return
492
490
  event = Event(
493
491
  id=self.ids.event(),
@@ -514,7 +512,7 @@ class Graph:
514
512
  tool_request_event_ids: Optional[list[str]] = None,
515
513
  ) -> Patch:
516
514
  """Auto-apply shortcut: build patch, version-check, emit applied/rejected."""
517
- obj = self._objects.get(target)
515
+ obj = self._state.get_object(target)
518
516
  if obj is None:
519
517
  raise KeyError(f"unknown object: {target}")
520
518
  clean = _strip_provenance(copy.deepcopy(updates))
@@ -552,7 +550,7 @@ class Graph:
552
550
  timestamp=self.clock.now(),
553
551
  )
554
552
  self.emit(event)
555
- return self._patches[patch.id]
553
+ return self._state.get_patch(patch.id)
556
554
 
557
555
  def propose_patch(
558
556
  self,
@@ -570,7 +568,7 @@ class Graph:
570
568
  ) -> Patch:
571
569
  # Strip "object:" / "relation:" prefix if present (README sugar).
572
570
  normalized = target.split(":", 1)[1] if ":" in target else target
573
- obj = self._objects.get(normalized)
571
+ obj = self._state.get_object(normalized)
574
572
  expected_version = obj.version if obj else 0
575
573
  clean = _strip_provenance(copy.deepcopy(value))
576
574
  patch = Patch(
@@ -602,7 +600,7 @@ class Graph:
602
600
  timestamp=self.clock.now(),
603
601
  )
604
602
  self.emit(event)
605
- return self._patches[patch.id]
603
+ return self._state.get_patch(patch.id)
606
604
 
607
605
  def apply_patch(
608
606
  self,
@@ -612,7 +610,7 @@ class Graph:
612
610
  caused_by: Optional[str] = None,
613
611
  frame_id: Optional[str] = None,
614
612
  ) -> Event:
615
- patch = self._patches.get(patch_id)
613
+ patch = self._state.get_patch(patch_id)
616
614
  if patch is None:
617
615
  raise KeyError(f"unknown patch: {patch_id}")
618
616
  if patch.status != "proposed":
@@ -620,7 +618,7 @@ class Graph:
620
618
  raise InvalidPatchLifecycleState(
621
619
  patch_id=patch_id, current_status=patch.status,
622
620
  )
623
- target_obj = self._objects.get(patch.target)
621
+ target_obj = self._state.get_object(patch.target)
624
622
  current_version = target_obj.version if target_obj else 0
625
623
  if current_version != patch.expected_version:
626
624
  return self._reject(
@@ -667,8 +665,8 @@ class Graph:
667
665
  caused_by: Optional[str],
668
666
  frame_id: Optional[str],
669
667
  ) -> Event:
670
- patch = self._patches[patch_id]
671
- current = self._objects.get(patch.target)
668
+ patch = self._state.get_patch(patch_id)
669
+ current = self._state.get_object(patch.target)
672
670
  event = Event(
673
671
  id=self.ids.event(),
674
672
  type="patch.rejected",
@@ -732,57 +730,71 @@ def apply_event(graph: Graph, event: Event) -> None:
732
730
 
733
731
  if t == "object.created":
734
732
  o = p["object"]
735
- graph._objects[o["id"]] = Object(
736
- id=o["id"],
737
- type=o["type"],
738
- data=copy.deepcopy(o["data"]),
739
- version=o["version"],
740
- provenance=copy.deepcopy(o["provenance"]),
733
+ graph._state.put_object(
734
+ Object(
735
+ id=o["id"],
736
+ type=o["type"],
737
+ data=copy.deepcopy(o["data"]),
738
+ version=o["version"],
739
+ provenance=copy.deepcopy(o["provenance"]),
740
+ )
741
741
  )
742
742
 
743
743
  elif t == "object.removed":
744
- graph._objects.pop(p["id"], None)
745
- # cascade: drop relations touching it
746
- for rid in [
747
- r.id for r in graph._relations.values() if p["id"] in (r.source, r.target)
748
- ]:
749
- graph._relations.pop(rid, None)
744
+ graph._state.remove_object(p["id"])
745
+ # cascade: drop relations touching it. Pushed down to two scoped
746
+ # lookups (out-edges + in-edges) instead of scanning every relation;
747
+ # on FalkorDB these are index-backed queries. Dedupe by id so a
748
+ # self-loop (source == target) is removed once.
749
+ touching = graph._state.find_relations(
750
+ source=p["id"]
751
+ ) + graph._state.find_relations(target=p["id"])
752
+ seen: set[str] = set()
753
+ for r in touching:
754
+ if r.id in seen:
755
+ continue
756
+ seen.add(r.id)
757
+ graph._state.remove_relation(r.id)
750
758
 
751
759
  elif t == "relation.created":
752
760
  r = p["relation"]
753
- graph._relations[r["id"]] = Relation(
754
- id=r["id"],
755
- source=r["source"],
756
- target=r["target"],
757
- type=r["type"],
758
- data=copy.deepcopy(r["data"]),
759
- provenance=copy.deepcopy(r["provenance"]),
761
+ graph._state.put_relation(
762
+ Relation(
763
+ id=r["id"],
764
+ source=r["source"],
765
+ target=r["target"],
766
+ type=r["type"],
767
+ data=copy.deepcopy(r["data"]),
768
+ provenance=copy.deepcopy(r["provenance"]),
769
+ )
760
770
  )
761
771
 
762
772
  elif t == "relation.removed":
763
- graph._relations.pop(p["id"], None)
773
+ graph._state.remove_relation(p["id"])
764
774
 
765
775
  elif t == "patch.proposed":
766
776
  patch_dict = p["patch"]
767
- graph._patches[patch_dict["id"]] = _patch_from_dict(patch_dict)
777
+ graph._state.put_patch(_patch_from_dict(patch_dict))
768
778
 
769
779
  elif t == "patch.applied":
770
780
  patch_dict = p["patch"]
771
781
  patch = _patch_from_dict({**patch_dict, "status": "applied"})
772
- graph._patches[patch.id] = patch
773
- obj = graph._objects.get(patch.target)
782
+ graph._state.put_patch(patch)
783
+ obj = graph._state.get_object(patch.target)
774
784
  if obj is not None:
775
785
  if patch.op == "update":
776
786
  obj.data.update(patch.value)
777
787
  elif patch.op == "replace":
778
788
  obj.data = copy.deepcopy(patch.value)
779
789
  obj.version += 1
790
+ graph._state.put_object(obj)
780
791
 
781
792
  elif t == "patch.rejected":
782
- existing = graph._patches.get(p["patch_id"])
793
+ existing = graph._state.get_patch(p["patch_id"])
783
794
  if existing is not None:
784
795
  existing.status = "rejected"
785
796
  existing.rejection_reason = p["reason"]
797
+ graph._state.put_patch(existing)
786
798
 
787
799
 
788
800
  def _patch_from_dict(d: dict[str, Any]) -> Patch: