activegraph 1.1.0__tar.gz → 1.3.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 (187) hide show
  1. {activegraph-1.1.0/activegraph.egg-info → activegraph-1.3.0}/PKG-INFO +32 -12
  2. {activegraph-1.1.0 → activegraph-1.3.0}/README.md +23 -11
  3. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/__init__.py +19 -1
  4. activegraph-1.3.0/activegraph/_signature.py +222 -0
  5. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/behaviors/base.py +35 -6
  6. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/behaviors/decorators.py +43 -8
  7. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/cli/main.py +121 -2
  8. activegraph-1.3.0/activegraph/core/__init__.py +8 -0
  9. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/core/clock.py +21 -4
  10. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/core/event.py +10 -0
  11. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/core/graph.py +153 -102
  12. activegraph-1.3.0/activegraph/core/graph_store.py +356 -0
  13. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/core/ids.py +9 -1
  14. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/core/patch.py +11 -0
  15. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/core/view.py +11 -0
  16. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/errors.py +22 -3
  17. activegraph-1.3.0/activegraph/frame.py +24 -0
  18. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/__init__.py +8 -0
  19. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/anthropic.py +86 -13
  20. activegraph-1.3.0/activegraph/llm/embedding.py +94 -0
  21. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/errors.py +38 -0
  22. activegraph-1.3.0/activegraph/llm/native.py +173 -0
  23. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/openai.py +114 -20
  24. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/prompt.py +24 -2
  25. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/provider.py +16 -0
  26. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/recorded.py +46 -2
  27. activegraph-1.3.0/activegraph/llm/wire.py +140 -0
  28. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/logging.py +4 -4
  29. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/migration.py +26 -7
  30. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/otel.py +2 -2
  31. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/prometheus.py +4 -4
  32. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/status.py +11 -1
  33. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/__init__.py +111 -25
  34. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/settings.py +1 -1
  35. activegraph-1.3.0/activegraph/packs/manifest.py +503 -0
  36. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/policy.py +11 -0
  37. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/budget.py +13 -0
  38. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/diff.py +32 -4
  39. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/exec_errors.py +117 -0
  40. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/patterns.py +58 -76
  41. activegraph-1.3.0/activegraph/runtime/promote.py +411 -0
  42. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/registration_errors.py +4 -1
  43. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/registry.py +3 -1
  44. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/runtime.py +749 -62
  45. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/view_builder.py +9 -4
  46. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/__init__.py +5 -0
  47. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/base.py +23 -3
  48. activegraph-1.3.0/activegraph/store/falkordb.py +647 -0
  49. activegraph-1.3.0/activegraph/store/graph_conformance.py +491 -0
  50. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/memory.py +10 -0
  51. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/postgres.py +3 -3
  52. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/serde.py +2 -1
  53. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/sqlite.py +10 -3
  54. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/url.py +8 -1
  55. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/base.py +14 -2
  56. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/context.py +11 -0
  57. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/decorators.py +40 -3
  58. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/trace/printer.py +85 -0
  59. {activegraph-1.1.0 → activegraph-1.3.0/activegraph.egg-info}/PKG-INFO +32 -12
  60. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph.egg-info/SOURCES.txt +18 -0
  61. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph.egg-info/requires.txt +6 -0
  62. {activegraph-1.1.0 → activegraph-1.3.0}/pyproject.toml +61 -6
  63. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_cli.py +118 -0
  64. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_doc_links.py +6 -4
  65. activegraph-1.3.0/tests/test_embedding_provider.py +97 -0
  66. activegraph-1.3.0/tests/test_falkordb_store.py +446 -0
  67. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_fork.py +20 -0
  68. activegraph-1.3.0/tests/test_graph_store.py +123 -0
  69. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_anthropic.py +100 -0
  70. activegraph-1.3.0/tests/test_llm_native_structured_output.py +466 -0
  71. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_openai.py +183 -4
  72. activegraph-1.3.0/tests/test_llm_wire.py +186 -0
  73. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llms_txt.py +49 -1
  74. activegraph-1.3.0/tests/test_pack_manifest.py +247 -0
  75. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_persistence.py +98 -0
  76. activegraph-1.3.0/tests/test_promote.py +821 -0
  77. activegraph-1.3.0/tests/test_registration_validation.py +301 -0
  78. activegraph-1.3.0/tests/test_trace_accessors.py +75 -0
  79. activegraph-1.1.0/activegraph/core/__init__.py +0 -1
  80. activegraph-1.1.0/activegraph/frame.py +0 -15
  81. {activegraph-1.1.0 → activegraph-1.3.0}/LICENSE +0 -0
  82. {activegraph-1.1.0 → activegraph-1.3.0}/NOTICE +0 -0
  83. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/__main__.py +0 -0
  84. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/behaviors/__init__.py +0 -0
  85. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/cli/__init__.py +0 -0
  86. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/cli/quickstart.py +0 -0
  87. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/cli/renderers.py +0 -0
  88. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/cache.py +0 -0
  89. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/parsing.py +0 -0
  90. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/llm/types.py +0 -0
  91. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/__init__.py +0 -0
  92. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/observability/metrics.py +0 -0
  93. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/__init__.py +0 -0
  94. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/behaviors.py +0 -0
  95. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  96. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  97. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/object_types.py +0 -0
  98. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  99. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  100. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  101. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  102. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/diligence/tools.py +0 -0
  103. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/loader.py +0 -0
  104. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/packs/scaffold.py +0 -0
  105. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/__init__.py +0 -0
  106. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/_live.py +0 -0
  107. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/behavior_graph.py +0 -0
  108. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/config_errors.py +0 -0
  109. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/errors.py +0 -0
  110. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/queue.py +0 -0
  111. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/runtime/scheduler.py +0 -0
  112. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/conformance.py +0 -0
  113. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/store/errors.py +0 -0
  114. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/__init__.py +0 -0
  115. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/cache.py +0 -0
  116. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/errors.py +0 -0
  117. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/graph_query.py +0 -0
  118. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/recorded.py +0 -0
  119. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/tools/web_fetch.py +0 -0
  120. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/trace/__init__.py +0 -0
  121. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph/trace/causal.py +0 -0
  122. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph.egg-info/dependency_links.txt +0 -0
  123. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph.egg-info/entry_points.txt +0 -0
  124. {activegraph-1.1.0 → activegraph-1.3.0}/activegraph.egg-info/top_level.txt +0 -0
  125. {activegraph-1.1.0 → activegraph-1.3.0}/setup.cfg +0 -0
  126. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_activate_after.py +0 -0
  127. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_causal_cross_tool.py +0 -0
  128. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_cli_docs_flags.py +0 -0
  129. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_clock.py +0 -0
  130. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_diff.py +0 -0
  131. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_diligence_pack.py +0 -0
  132. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_diligence_with_tools.py +0 -0
  133. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_doc_python_snippets.py +0 -0
  134. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_doc_site_reachable.py +0 -0
  135. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_errors_format.py +0 -0
  136. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_event.py +0 -0
  137. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_graph.py +0 -0
  138. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_ids.py +0 -0
  139. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_license.py +0 -0
  140. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_behavior.py +0 -0
  141. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_budget.py +0 -0
  142. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_causal.py +0 -0
  143. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_claim_extraction.py +0 -0
  144. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_default_model.py +0 -0
  145. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_determinism.py +0 -0
  146. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_failure.py +0 -0
  147. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_prompt.py +0 -0
  148. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_provider_fixtures.py +0 -0
  149. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_replay.py +0 -0
  150. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_tool_loop.py +0 -0
  151. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_trace.py +0 -0
  152. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_trace_snapshot.py +0 -0
  153. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_llm_types.py +0 -0
  154. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_migration.py +0 -0
  155. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_observability_logging.py +0 -0
  156. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_observability_metrics.py +0 -0
  157. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_operate_example.py +0 -0
  158. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_pack_scaffold.py +0 -0
  159. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_packs.py +0 -0
  160. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_patch.py +0 -0
  161. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_pattern_matcher.py +0 -0
  162. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_pattern_parser.py +0 -0
  163. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_pattern_subscriptions.py +0 -0
  164. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_postgres_store.py +0 -0
  165. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_quickstart.py +0 -0
  166. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_quickstart_snapshot.py +0 -0
  167. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_reason_codes_docs.py +0 -0
  168. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_replay.py +0 -0
  169. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_replay_trace_snapshot.py +0 -0
  170. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_requeue_unfired.py +0 -0
  171. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_resume_example.py +0 -0
  172. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_runtime.py +0 -0
  173. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_runtime_status.py +0 -0
  174. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_serde.py +0 -0
  175. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_store_conformance.py +0 -0
  176. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_store_url.py +0 -0
  177. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_tool_replay.py +0 -0
  178. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_tool_trace_snapshot.py +0 -0
  179. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_tools.py +0 -0
  180. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_trace.py +0 -0
  181. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_tutorial_snippets.py +0 -0
  182. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_v1_0_1_patches.py +0 -0
  183. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  184. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  185. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_version_sync.py +0 -0
  186. {activegraph-1.1.0 → activegraph-1.3.0}/tests/test_view.py +0 -0
  187. {activegraph-1.1.0 → activegraph-1.3.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.3.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
 
@@ -42,6 +42,11 @@ from activegraph.llm.errors import LLMBehaviorError, MissingProviderError
42
42
  from activegraph.policy import Policy
43
43
  from activegraph.runtime.budget import Budget
44
44
  from activegraph.runtime.diff import Diff, DivergentObject, DivergentRelation
45
+ from activegraph.runtime.promote import (
46
+ PromoteConflict,
47
+ PromotePlan,
48
+ PromoteResult,
49
+ )
45
50
  from activegraph.runtime.config_errors import (
46
51
  IncompatibleRuntimeState,
47
52
  InvalidArgumentType,
@@ -52,6 +57,8 @@ from activegraph.runtime.exec_errors import (
52
57
  ApprovalNotFoundError,
53
58
  InternalEvaluatorError,
54
59
  InvalidPatchLifecycleState,
60
+ PromoteConflictError,
61
+ PromoteLineageError,
55
62
  RuntimeContextRequiredError,
56
63
  )
57
64
  from activegraph.runtime.patterns import UnsupportedPatternError
@@ -61,7 +68,10 @@ from activegraph.store import (
61
68
  DuplicateEventError,
62
69
  EventNotFoundError,
63
70
  EventStore,
71
+ FalkorDBGraphStore,
72
+ GraphStore,
64
73
  InMemoryEventStore,
74
+ InMemoryGraphStore,
65
75
  InvalidStoreURL,
66
76
  NonSerializableEventError,
67
77
  RunRecord,
@@ -142,11 +152,14 @@ __all__ = [
142
152
  "EventNotFoundError",
143
153
  "EventStore",
144
154
  "ExecutionError",
155
+ "FalkorDBGraphStore",
145
156
  "Frame",
146
157
  "FrozenClock",
147
158
  "Graph",
159
+ "GraphStore",
148
160
  "IDGen",
149
161
  "InMemoryEventStore",
162
+ "InMemoryGraphStore",
150
163
  "IncompatibleRuntimeState",
151
164
  "InternalEvaluatorError",
152
165
  "InvalidActivateAfter",
@@ -184,6 +197,11 @@ __all__ = [
184
197
  "PendingApproval",
185
198
  "Policy",
186
199
  "PrometheusMetrics",
200
+ "PromoteConflict",
201
+ "PromoteConflictError",
202
+ "PromoteLineageError",
203
+ "PromotePlan",
204
+ "PromoteResult",
187
205
  "Relation",
188
206
  "RelationBehavior",
189
207
  "RegistrationError",
@@ -224,4 +242,4 @@ __all__ = [
224
242
  "tool",
225
243
  ]
226
244
 
227
- __version__ = "1.1.0"
245
+ __version__ = "1.3.0"
@@ -0,0 +1,222 @@
1
+ """Registration-time handler-signature validation.
2
+
3
+ Shared by the ``@behavior`` / ``@relation_behavior`` / ``@llm_behavior``
4
+ decorators (both the global registry in
5
+ :mod:`activegraph.behaviors.decorators` and the pack-scoped variants in
6
+ :mod:`activegraph.packs`) and the ``@tool`` decorators.
7
+
8
+ Rationale (July 2026 agent-readiness review): a handler with the wrong
9
+ arity — ``def h(ctx)``, ``def h(event, ctx)`` — previously registered
10
+ fine and failed at first invocation with a ``TypeError`` swallowed into
11
+ a ``behavior.failed`` event, painful to notice during pack development.
12
+ Validating at decoration time names the mistake at the line that made
13
+ it, matching the precedent set by ``@llm_behavior``'s
14
+ ``output_schema=`` strict validation (CONTRACT v1.0.3 #2).
15
+
16
+ The check is deliberately permissive:
17
+
18
+ * Objects without an inspectable signature (builtins, some callables,
19
+ mocks with ``(*args, **kwargs)``) pass through unvalidated — the
20
+ goal is catching the common authoring mistake, not gatekeeping
21
+ every callable Python can construct.
22
+ * ``*args`` satisfies any positional arity.
23
+ * Extra parameters beyond the contract are allowed when the call can
24
+ still succeed: they need a default, or — for behaviors, where the
25
+ pack loader injects typed settings by keyword
26
+ (``packs/loader.py:_wrap_with_injection``) — a type annotation
27
+ that marks them as injection candidates.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import inspect
33
+ from typing import Any
34
+
35
+
36
+ def validate_handler_signature(
37
+ fn: Any,
38
+ *,
39
+ expected_params: tuple[str, ...],
40
+ decorator: str,
41
+ allow_annotated_extras: bool,
42
+ ) -> None:
43
+ """Raise ``TypeError`` when ``fn`` cannot be invoked with the
44
+ decorator's positional calling convention.
45
+
46
+ ``expected_params`` names the contract's positional parameters
47
+ (documentation for the error message; only arity is checked, not
48
+ names). ``decorator`` is the user-facing decorator name for the
49
+ message (e.g. ``"@tool"``). ``allow_annotated_extras=True`` treats
50
+ a type annotation on an extra parameter as evidence it will be
51
+ keyword-injected at call time (the pack settings pattern
52
+ ``*, settings: MyPackSettings``); ``False`` (tools) requires extras
53
+ to carry defaults, because tools are always invoked as exactly
54
+ ``fn(args, ctx)``.
55
+
56
+ Callables whose signature cannot be inspected are skipped, and
57
+ ``*args`` / ``**kwargs`` satisfy the positional / extra checks
58
+ respectively — the validation is a loud guard for the common
59
+ mistake, not an exhaustive gate.
60
+ """
61
+ if not callable(fn):
62
+ raise TypeError(
63
+ f"{decorator} must decorate a callable, got "
64
+ f"{type(fn).__name__}."
65
+ )
66
+ try:
67
+ sig = inspect.signature(fn)
68
+ except (TypeError, ValueError):
69
+ return
70
+
71
+ params = list(sig.parameters.values())
72
+ has_var_positional = any(
73
+ p.kind is inspect.Parameter.VAR_POSITIONAL for p in params
74
+ )
75
+ positional = [
76
+ p
77
+ for p in params
78
+ if p.kind
79
+ in (
80
+ inspect.Parameter.POSITIONAL_ONLY,
81
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
82
+ )
83
+ ]
84
+ n_expected = len(expected_params)
85
+ expected_sig = f"({', '.join(expected_params)})"
86
+
87
+ if len(positional) < n_expected and not has_var_positional:
88
+ found = (
89
+ f"({', '.join(p.name for p in positional)})"
90
+ if positional
91
+ else "()"
92
+ )
93
+ raise TypeError(
94
+ f"{decorator} function {_name_of(fn)!r} must accept "
95
+ f"{n_expected} positional parameters {expected_sig}, but its "
96
+ f"signature is {found}.\n"
97
+ f"\n"
98
+ f"The runtime invokes it as fn{expected_sig}. "
99
+ f"{_extra_hint(decorator)}\n"
100
+ f"\n"
101
+ f"Example:\n"
102
+ f" {decorator}(...)\n"
103
+ f" def {_example_name(fn)}{expected_sig}: ..."
104
+ )
105
+
106
+ problems: list[str] = []
107
+ for p in positional[n_expected:]:
108
+ if _extra_is_satisfiable(p, allow_annotated_extras):
109
+ continue
110
+ problems.append(p.name)
111
+ for p in params:
112
+ if p.kind is not inspect.Parameter.KEYWORD_ONLY:
113
+ continue
114
+ if _extra_is_satisfiable(p, allow_annotated_extras):
115
+ continue
116
+ problems.append(p.name)
117
+
118
+ if problems:
119
+ if allow_annotated_extras:
120
+ fix = (
121
+ "give it a default value, or annotate it with your "
122
+ "pack's settings class so the pack loader injects it "
123
+ "(e.g. `*, settings: MyPackSettings`)"
124
+ )
125
+ else:
126
+ fix = "give it a default value or remove it"
127
+ raise TypeError(
128
+ f"{decorator} function {_name_of(fn)!r} has required "
129
+ f"parameter(s) beyond the {expected_sig} contract that the "
130
+ f"runtime will never pass: {', '.join(problems)}.\n"
131
+ f"\n"
132
+ f"The runtime invokes it as fn{expected_sig}, so every "
133
+ f"extra parameter must be optional at call time — {fix}."
134
+ )
135
+
136
+
137
+ def infer_tool_input_schema(fn: Any) -> Any:
138
+ """Infer a tool's ``input_schema`` from its first parameter's
139
+ type annotation, or return ``None``.
140
+
141
+ v1.3: with ``input_schema=None``, ``@tool`` previously sent the
142
+ model an empty parameters schema (``{"type": "object",
143
+ "properties": {}}``) even when the function body plainly declared
144
+ its shape via ``def fetch(args: FetchArgs, ctx)``. Inference
145
+ closes that gap: when the first positional parameter is annotated
146
+ with a Pydantic ``BaseModel`` subclass, that class becomes the
147
+ tool's ``input_schema`` — the model sees real parameters and the
148
+ runtime validates arguments before invocation, exactly as if
149
+ ``input_schema=FetchArgs`` had been passed explicitly.
150
+
151
+ Anything else — no annotation, a non-model annotation
152
+ (``dict``, ``Any``), an uninspectable callable — returns ``None``
153
+ and the tool behaves exactly as before. Explicit ``input_schema=``
154
+ always wins; the decorators only call this when it was omitted.
155
+ """
156
+ try:
157
+ sig = inspect.signature(fn)
158
+ except (TypeError, ValueError):
159
+ return None
160
+ positional = [
161
+ p
162
+ for p in sig.parameters.values()
163
+ if p.kind
164
+ in (
165
+ inspect.Parameter.POSITIONAL_ONLY,
166
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
167
+ )
168
+ ]
169
+ if not positional:
170
+ return None
171
+ annotation = positional[0].annotation
172
+ if annotation is inspect.Parameter.empty:
173
+ return None
174
+ # PEP 563 / `from __future__ import annotations` leaves string
175
+ # annotations; resolve through get_type_hints (module-global
176
+ # visibility), the same tolerance the pack loader's settings
177
+ # injection applies.
178
+ if isinstance(annotation, str):
179
+ import typing
180
+
181
+ try:
182
+ hints = typing.get_type_hints(fn)
183
+ except Exception:
184
+ return None
185
+ annotation = hints.get(positional[0].name)
186
+ if annotation is None:
187
+ return None
188
+ try:
189
+ from pydantic import BaseModel
190
+ except ImportError: # pragma: no cover — Pydantic is a hard dep
191
+ return None
192
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
193
+ return annotation
194
+ return None
195
+
196
+
197
+ def _extra_is_satisfiable(
198
+ p: inspect.Parameter, allow_annotated_extras: bool
199
+ ) -> bool:
200
+ if p.default is not inspect.Parameter.empty:
201
+ return True
202
+ if allow_annotated_extras and p.annotation is not inspect.Parameter.empty:
203
+ return True
204
+ return False
205
+
206
+
207
+ def _name_of(fn: Any) -> str:
208
+ return getattr(fn, "__name__", repr(fn))
209
+
210
+
211
+ def _example_name(fn: Any) -> str:
212
+ name = getattr(fn, "__name__", "my_handler")
213
+ return name if name.isidentifier() else "my_handler"
214
+
215
+
216
+ def _extra_hint(decorator: str) -> str:
217
+ if decorator == "@tool":
218
+ return (
219
+ "`args` is the validated input model (or a plain dict when "
220
+ "no input_schema is set) and `ctx` is the ToolContext."
221
+ )
222
+ return "See the behavior-handler contract in docs/concepts/behaviors."
@@ -20,12 +20,15 @@ from typing import TYPE_CHECKING, Any, Callable, Optional
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from activegraph.core.event import Event
23
- from activegraph.core.graph import Graph
23
+ from activegraph.core.graph import Graph, Relation
24
24
  from activegraph.frame import Frame
25
25
  from activegraph.llm.prompt import AssembledPrompt
26
+ from activegraph.runtime.runtime import Context
26
27
 
27
28
 
28
- def _llm_behavior_fn_placeholder(event, graph, ctx) -> None: # pragma: no cover
29
+ def _llm_behavior_fn_placeholder(
30
+ event: "Event", graph: "Graph", ctx: "Context"
31
+ ) -> None: # pragma: no cover
29
32
  raise RuntimeError(
30
33
  "LLMBehavior.fn invoked directly. The runtime owns LLM behavior "
31
34
  "invocation via _invoke_llm; calling .run() bypasses prompt "
@@ -35,6 +38,18 @@ def _llm_behavior_fn_placeholder(event, graph, ctx) -> None: # pragma: no cover
35
38
 
36
39
  @dataclass
37
40
  class Behavior:
41
+ """Metadata plus the callable for an event-driven behavior.
42
+
43
+ A Behavior is data, not magic: ``on`` (event types), ``where``
44
+ (payload filter), ``pattern`` (Cypher-subset subscription),
45
+ ``activate_after`` (event-count delay), and ``view_spec`` (what
46
+ the runtime builds into ``ctx.view``) describe *when* it fires;
47
+ the 3-arg ``fn`` — invoked as ``fn(event, graph, ctx)`` — is
48
+ *what* runs. Instances come from ``@behavior`` or a pack; the
49
+ runtime introspects the metadata to match events, and nothing
50
+ here executes on its own.
51
+ """
52
+
38
53
  name: str
39
54
  fn: Callable[..., None]
40
55
  on: list[str] = field(default_factory=list)
@@ -51,12 +66,22 @@ class Behavior:
51
66
  # CONTRACT v0.7 #13. Event-count delay. None = fire immediately.
52
67
  activate_after: Optional[int] = None
53
68
 
54
- def run(self, event, graph, ctx) -> None:
69
+ def run(self, event: "Event", graph: "Graph", ctx: "Context") -> None:
55
70
  self.fn(event, graph, ctx)
56
71
 
57
72
 
58
73
  @dataclass
59
74
  class RelationBehavior:
75
+ """A behavior that fires once per matching relation edge.
76
+
77
+ Same metadata surface as :class:`Behavior` plus
78
+ ``relation_type``: when a triggering event touches a relation of
79
+ that type, the 4-arg ``fn`` runs as
80
+ ``fn(relation, event, graph, ctx)`` — once per (event, relation)
81
+ pair. Deliberately not a subclass of ``Behavior``; the runtime
82
+ dispatches the two kinds separately.
83
+ """
84
+
60
85
  name: str
61
86
  fn: Callable[..., None]
62
87
  relation_type: str
@@ -72,7 +97,9 @@ class RelationBehavior:
72
97
  pattern_matcher: Any = None
73
98
  activate_after: Optional[int] = None
74
99
 
75
- def run(self, relation, event, graph, ctx) -> None:
100
+ def run(
101
+ self, relation: "Relation", event: "Event", graph: "Graph", ctx: "Context"
102
+ ) -> None:
76
103
  self.fn(relation, event, graph, ctx)
77
104
 
78
105
 
@@ -98,7 +125,7 @@ class LLMBehavior(Behavior):
98
125
  # at registration time. Existing call sites that pass an explicit
99
126
  # string keep working unchanged.
100
127
  model: Optional[str] = None
101
- output_schema: Optional[type] = None
128
+ output_schema: Optional[type[Any]] = None
102
129
  deterministic: bool = False
103
130
  max_tokens: int = 4096
104
131
  temperature: float = 0.7
@@ -106,7 +133,7 @@ class LLMBehavior(Behavior):
106
133
  timeout_seconds: float = 60.0
107
134
  prompt_template: Optional[str] = None
108
135
  # v0.7
109
- tools: list = field(default_factory=list)
136
+ tools: list[Any] = field(default_factory=list) # Tool | str names
110
137
  max_tool_turns: int = 6
111
138
 
112
139
  def build_prompt(
@@ -115,6 +142,7 @@ class LLMBehavior(Behavior):
115
142
  graph: "Graph",
116
143
  *,
117
144
  frame: Optional["Frame"] = None,
145
+ structured_output_mode: str = "prompt",
118
146
  ) -> "AssembledPrompt":
119
147
  """Assemble the prompt that would be sent for this event.
120
148
 
@@ -161,4 +189,5 @@ class LLMBehavior(Behavior):
161
189
  top_p=self.top_p,
162
190
  deterministic=self.deterministic,
163
191
  prompt_template=self.prompt_template,
192
+ structured_output_mode=structured_output_mode,
164
193
  )
@@ -94,7 +94,15 @@ def clear_registry() -> list[Union[Behavior, RelationBehavior]]:
94
94
 
95
95
 
96
96
  def get_registry() -> list[Union[Behavior, RelationBehavior]]:
97
- """Snapshot of the global behavior registry (a shallow copy)."""
97
+ """Snapshot of the global behavior registry (a shallow copy).
98
+
99
+ Decoration with ``@behavior`` / ``@relation_behavior`` /
100
+ ``@llm_behavior`` appends to a module-level list; this returns a
101
+ copy, so callers can filter or iterate without mutating
102
+ registration state. ``Runtime`` snapshots it at construction when
103
+ ``behaviors=`` is omitted — later registrations don't leak into
104
+ already-built runtimes.
105
+ """
98
106
  return list(_REGISTRY)
99
107
 
100
108
 
@@ -152,7 +160,7 @@ def behavior(
152
160
  *,
153
161
  pattern: Optional[str] = None,
154
162
  activate_after: Any = None,
155
- ) -> Callable[[Callable], Behavior]:
163
+ ) -> Callable[[Callable[..., None]], Behavior]:
156
164
  """Decorate a function as an event-driven behavior.
157
165
 
158
166
  v0.7 additions (both keyword-only):
@@ -175,7 +183,16 @@ def behavior(
175
183
  if activate_after is not None:
176
184
  delay_n = _parse_aa(activate_after)
177
185
 
178
- def wrap(fn: Callable) -> Behavior:
186
+ def wrap(fn: Callable[..., None]) -> Behavior:
187
+ # v1.3: arity check at decoration time (see activegraph/_signature.py).
188
+ from activegraph._signature import validate_handler_signature
189
+
190
+ validate_handler_signature(
191
+ fn,
192
+ expected_params=("event", "graph", "ctx"),
193
+ decorator="@behavior",
194
+ allow_annotated_extras=True,
195
+ )
179
196
  b = Behavior(
180
197
  name=name or fn.__name__,
181
198
  fn=fn,
@@ -215,9 +232,9 @@ def llm_behavior(
215
232
  priority: int = 0,
216
233
  pattern: Optional[str] = None,
217
234
  activate_after: Any = None,
218
- tools: Optional[list] = None,
235
+ tools: Optional[list[Any]] = None,
219
236
  max_tool_turns: int = 6,
220
- ) -> Callable[[Callable], LLMBehavior]:
237
+ ) -> Callable[[Callable[..., None]], LLMBehavior]:
221
238
  """Decorate a function as an LLM-driven behavior.
222
239
 
223
240
  The decorated function's signature is
@@ -281,7 +298,16 @@ def llm_behavior(
281
298
  # call; failing here names the cause at the @llm_behavior line.
282
299
  _validate_output_schema(output_schema)
283
300
 
284
- def wrap(fn: Callable) -> LLMBehavior:
301
+ def wrap(fn: Callable[..., None]) -> LLMBehavior:
302
+ # v1.3: arity check at decoration time (see activegraph/_signature.py).
303
+ from activegraph._signature import validate_handler_signature
304
+
305
+ validate_handler_signature(
306
+ fn,
307
+ expected_params=("event", "graph", "ctx", "llm_output"),
308
+ decorator="@llm_behavior",
309
+ allow_annotated_extras=True,
310
+ )
285
311
  b = LLMBehavior(
286
312
  name=name or fn.__name__,
287
313
  fn=_llm_behavior_fn_placeholder,
@@ -331,7 +357,7 @@ def relation_behavior(
331
357
  *,
332
358
  pattern: Optional[str] = None,
333
359
  activate_after: Any = None,
334
- ) -> Callable[[Callable], RelationBehavior]:
360
+ ) -> Callable[[Callable[..., None]], RelationBehavior]:
335
361
  """Decorate a function as a relation behavior — fires once per matching edge.
336
362
 
337
363
  v0.7: also accepts `pattern=` and `activate_after=` per CONTRACT
@@ -348,7 +374,16 @@ def relation_behavior(
348
374
  if activate_after is not None:
349
375
  delay_n = _parse_aa(activate_after)
350
376
 
351
- def wrap(fn: Callable) -> RelationBehavior:
377
+ def wrap(fn: Callable[..., None]) -> RelationBehavior:
378
+ # v1.3: arity check at decoration time (see activegraph/_signature.py).
379
+ from activegraph._signature import validate_handler_signature
380
+
381
+ validate_handler_signature(
382
+ fn,
383
+ expected_params=("relation", "event", "graph", "ctx"),
384
+ decorator="@relation_behavior",
385
+ allow_annotated_extras=True,
386
+ )
352
387
  rb = RelationBehavior(
353
388
  name=name or fn.__name__,
354
389
  fn=fn,