activegraph 1.0.3__tar.gz → 1.0.5__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 (159) hide show
  1. {activegraph-1.0.3 → activegraph-1.0.5}/PKG-INFO +9 -1
  2. {activegraph-1.0.3 → activegraph-1.0.5}/README.md +7 -0
  3. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/__init__.py +1 -1
  4. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/graph.py +27 -0
  5. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph.egg-info/PKG-INFO +9 -1
  6. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph.egg-info/SOURCES.txt +1 -0
  7. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph.egg-info/requires.txt +1 -0
  8. {activegraph-1.0.3 → activegraph-1.0.5}/pyproject.toml +7 -1
  9. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_doc_links.py +20 -0
  10. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_graph.py +98 -0
  11. activegraph-1.0.5/tests/test_llms_txt.py +184 -0
  12. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_requeue_unfired.py +64 -0
  13. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/__main__.py +0 -0
  14. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/behaviors/__init__.py +0 -0
  15. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/behaviors/base.py +0 -0
  16. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/behaviors/decorators.py +0 -0
  17. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/cli/__init__.py +0 -0
  18. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/cli/main.py +0 -0
  19. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/cli/quickstart.py +0 -0
  20. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/__init__.py +0 -0
  21. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/clock.py +0 -0
  22. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/event.py +0 -0
  23. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/ids.py +0 -0
  24. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/patch.py +0 -0
  25. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/core/view.py +0 -0
  26. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/errors.py +0 -0
  27. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/frame.py +0 -0
  28. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/__init__.py +0 -0
  29. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/anthropic.py +0 -0
  30. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/cache.py +0 -0
  31. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/errors.py +0 -0
  32. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/openai.py +0 -0
  33. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/parsing.py +0 -0
  34. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/prompt.py +0 -0
  35. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/provider.py +0 -0
  36. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/recorded.py +0 -0
  37. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/llm/types.py +0 -0
  38. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/observability/__init__.py +0 -0
  39. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/observability/logging.py +0 -0
  40. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/observability/metrics.py +0 -0
  41. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/observability/migration.py +0 -0
  42. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/observability/prometheus.py +0 -0
  43. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/observability/status.py +0 -0
  44. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/__init__.py +0 -0
  45. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/__init__.py +0 -0
  46. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/behaviors.py +0 -0
  47. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  48. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  49. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/object_types.py +0 -0
  50. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  51. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  52. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  53. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  54. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/settings.py +0 -0
  55. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/diligence/tools.py +0 -0
  56. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/loader.py +0 -0
  57. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/packs/scaffold.py +0 -0
  58. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/policy.py +0 -0
  59. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/__init__.py +0 -0
  60. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/_live.py +0 -0
  61. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/behavior_graph.py +0 -0
  62. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/budget.py +0 -0
  63. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/config_errors.py +0 -0
  64. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/diff.py +0 -0
  65. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/errors.py +0 -0
  66. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/exec_errors.py +0 -0
  67. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/patterns.py +0 -0
  68. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/queue.py +0 -0
  69. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/registration_errors.py +0 -0
  70. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/registry.py +0 -0
  71. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/runtime.py +0 -0
  72. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/scheduler.py +0 -0
  73. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/runtime/view_builder.py +0 -0
  74. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/__init__.py +0 -0
  75. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/base.py +0 -0
  76. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/conformance.py +0 -0
  77. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/errors.py +0 -0
  78. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/memory.py +0 -0
  79. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/postgres.py +0 -0
  80. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/serde.py +0 -0
  81. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/sqlite.py +0 -0
  82. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/store/url.py +0 -0
  83. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/__init__.py +0 -0
  84. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/base.py +0 -0
  85. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/cache.py +0 -0
  86. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/context.py +0 -0
  87. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/decorators.py +0 -0
  88. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/errors.py +0 -0
  89. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/graph_query.py +0 -0
  90. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/recorded.py +0 -0
  91. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/tools/web_fetch.py +0 -0
  92. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/trace/__init__.py +0 -0
  93. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/trace/causal.py +0 -0
  94. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph/trace/printer.py +0 -0
  95. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph.egg-info/dependency_links.txt +0 -0
  96. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph.egg-info/entry_points.txt +0 -0
  97. {activegraph-1.0.3 → activegraph-1.0.5}/activegraph.egg-info/top_level.txt +0 -0
  98. {activegraph-1.0.3 → activegraph-1.0.5}/setup.cfg +0 -0
  99. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_activate_after.py +0 -0
  100. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_causal_cross_tool.py +0 -0
  101. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_cli.py +0 -0
  102. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_clock.py +0 -0
  103. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_diff.py +0 -0
  104. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_diligence_pack.py +0 -0
  105. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_diligence_with_tools.py +0 -0
  106. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_doc_site_reachable.py +0 -0
  107. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_errors_format.py +0 -0
  108. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_event.py +0 -0
  109. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_fork.py +0 -0
  110. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_ids.py +0 -0
  111. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_anthropic.py +0 -0
  112. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_behavior.py +0 -0
  113. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_budget.py +0 -0
  114. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_causal.py +0 -0
  115. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_claim_extraction.py +0 -0
  116. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_default_model.py +0 -0
  117. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_determinism.py +0 -0
  118. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_failure.py +0 -0
  119. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_openai.py +0 -0
  120. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_prompt.py +0 -0
  121. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_provider_fixtures.py +0 -0
  122. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_replay.py +0 -0
  123. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_tool_loop.py +0 -0
  124. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_trace.py +0 -0
  125. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_trace_snapshot.py +0 -0
  126. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_llm_types.py +0 -0
  127. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_migration.py +0 -0
  128. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_observability_logging.py +0 -0
  129. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_observability_metrics.py +0 -0
  130. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_operate_example.py +0 -0
  131. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_pack_scaffold.py +0 -0
  132. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_packs.py +0 -0
  133. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_patch.py +0 -0
  134. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_pattern_matcher.py +0 -0
  135. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_pattern_parser.py +0 -0
  136. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_pattern_subscriptions.py +0 -0
  137. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_persistence.py +0 -0
  138. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_postgres_store.py +0 -0
  139. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_quickstart.py +0 -0
  140. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_quickstart_snapshot.py +0 -0
  141. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_replay.py +0 -0
  142. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_replay_trace_snapshot.py +0 -0
  143. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_resume_example.py +0 -0
  144. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_runtime.py +0 -0
  145. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_runtime_status.py +0 -0
  146. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_serde.py +0 -0
  147. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_store_conformance.py +0 -0
  148. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_store_url.py +0 -0
  149. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_tool_replay.py +0 -0
  150. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_tool_trace_snapshot.py +0 -0
  151. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_tools.py +0 -0
  152. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_trace.py +0 -0
  153. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_tutorial_snippets.py +0 -0
  154. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_v1_0_1_patches.py +0 -0
  155. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  156. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  157. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_version_sync.py +0 -0
  158. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_view.py +0 -0
  159. {activegraph-1.0.3 → activegraph-1.0.5}/tests/test_wheel_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License: MIT
@@ -49,6 +49,7 @@ Provides-Extra: docs
49
49
  Requires-Dist: mkdocs>=1.6; extra == "docs"
50
50
  Requires-Dist: mkdocs-material>=9.5; extra == "docs"
51
51
  Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
52
+ Requires-Dist: mkdocs-llmstxt>=0.2; extra == "docs"
52
53
 
53
54
  # Active Graph
54
55
 
@@ -219,6 +220,13 @@ in [`docs/concepts/relations.md`](https://docs.activegraph.ai/concepts/relations
219
220
  per-error catalog.
220
221
  - **[10-minute tutorial](https://docs.activegraph.ai/quickstart/)** — install
221
222
  to a working custom behavior, including fork-and-diff.
223
+ - **AI coding assistants** — the docs are machine-readable at
224
+ [docs.activegraph.ai/llms.txt](https://docs.activegraph.ai/llms.txt)
225
+ (structured index) and
226
+ [docs.activegraph.ai/llms-full.txt](https://docs.activegraph.ai/llms-full.txt)
227
+ (concatenated full content), generated from the same source markdown
228
+ as the rendered site. Built for AI agents evaluating the framework
229
+ via Claude Code, Cursor, Replit, and similar tooling.
222
230
  - **[CHANGELOG.md](CHANGELOG.md)** — every release, with per-version
223
231
  migration notes.
224
232
  - **[CONTRACT.md](CONTRACT.md)** — locked design decisions, version
@@ -167,6 +167,13 @@ in [`docs/concepts/relations.md`](https://docs.activegraph.ai/concepts/relations
167
167
  per-error catalog.
168
168
  - **[10-minute tutorial](https://docs.activegraph.ai/quickstart/)** — install
169
169
  to a working custom behavior, including fork-and-diff.
170
+ - **AI coding assistants** — the docs are machine-readable at
171
+ [docs.activegraph.ai/llms.txt](https://docs.activegraph.ai/llms.txt)
172
+ (structured index) and
173
+ [docs.activegraph.ai/llms-full.txt](https://docs.activegraph.ai/llms-full.txt)
174
+ (concatenated full content), generated from the same source markdown
175
+ as the rendered site. Built for AI agents evaluating the framework
176
+ via Claude Code, Cursor, Replit, and similar tooling.
170
177
  - **[CHANGELOG.md](CHANGELOG.md)** — every release, with per-version
171
178
  migration notes.
172
179
  - **[CONTRACT.md](CONTRACT.md)** — locked design decisions, version
@@ -222,4 +222,4 @@ __all__ = [
222
222
  "tool",
223
223
  ]
224
224
 
225
- __version__ = "1.0.3"
225
+ __version__ = "1.0.5"
@@ -181,6 +181,33 @@ class Graph:
181
181
  out.append(r)
182
182
  return out
183
183
 
184
+ def relations(
185
+ self,
186
+ source: Optional[str] = None,
187
+ target: Optional[str] = None,
188
+ type: Optional[str] = None,
189
+ ) -> list[Relation]:
190
+ """Return relations filtered by ``source``, ``target``, and/or ``type``.
191
+
192
+ v1.0.4 #1: the canonical filter API on ``Graph``. Decomposes the
193
+ v0 ``get_relations(object_id=, direction=)`` axis into separate
194
+ ``source`` and ``target`` slots so the call reads the way users
195
+ already write it (matches ``docs/concepts/graph.md``). Filter
196
+ kwargs compose by AND; calling with no kwargs returns every
197
+ relation. ``Graph.get_relations(object_id=, type=, direction=)``
198
+ stays as a backward-compatible alias.
199
+ """
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
+
184
211
  def neighborhood(self, object_id: str, depth: int = 1) -> tuple[list[Object], list[Relation]]:
185
212
  if object_id not in self._objects:
186
213
  return ([], [])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License: MIT
@@ -49,6 +49,7 @@ Provides-Extra: docs
49
49
  Requires-Dist: mkdocs>=1.6; extra == "docs"
50
50
  Requires-Dist: mkdocs-material>=9.5; extra == "docs"
51
51
  Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
52
+ Requires-Dist: mkdocs-llmstxt>=0.2; extra == "docs"
52
53
 
53
54
  # Active Graph
54
55
 
@@ -219,6 +220,13 @@ in [`docs/concepts/relations.md`](https://docs.activegraph.ai/concepts/relations
219
220
  per-error catalog.
220
221
  - **[10-minute tutorial](https://docs.activegraph.ai/quickstart/)** — install
221
222
  to a working custom behavior, including fork-and-diff.
223
+ - **AI coding assistants** — the docs are machine-readable at
224
+ [docs.activegraph.ai/llms.txt](https://docs.activegraph.ai/llms.txt)
225
+ (structured index) and
226
+ [docs.activegraph.ai/llms-full.txt](https://docs.activegraph.ai/llms-full.txt)
227
+ (concatenated full content), generated from the same source markdown
228
+ as the rendered site. Built for AI agents evaluating the framework
229
+ via Claude Code, Cursor, Replit, and similar tooling.
222
230
  - **[CHANGELOG.md](CHANGELOG.md)** — every release, with per-version
223
231
  migration notes.
224
232
  - **[CONTRACT.md](CONTRACT.md)** — locked design decisions, version
@@ -120,6 +120,7 @@ tests/test_llm_tool_loop.py
120
120
  tests/test_llm_trace.py
121
121
  tests/test_llm_trace_snapshot.py
122
122
  tests/test_llm_types.py
123
+ tests/test_llms_txt.py
123
124
  tests/test_migration.py
124
125
  tests/test_observability_logging.py
125
126
  tests/test_observability_metrics.py
@@ -23,6 +23,7 @@ psycopg[binary]<4,>=3.1
23
23
  mkdocs>=1.6
24
24
  mkdocs-material>=9.5
25
25
  mkdocstrings[python]>=0.24
26
+ mkdocs-llmstxt>=0.2
26
27
 
27
28
  [llm]
28
29
  anthropic>=0.40
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "activegraph"
7
- version = "1.0.3"
7
+ version = "1.0.5"
8
8
  description = "An event-sourced reactive graph runtime for long-running, auditable, agentic systems."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -65,6 +65,12 @@ docs = [
65
65
  "mkdocs>=1.6",
66
66
  "mkdocs-material>=9.5",
67
67
  "mkdocstrings[python]>=0.24",
68
+ # v1.0.5 #1: generates /llms.txt and /llms-full.txt at the site
69
+ # root so AI coding assistants fetching docs.activegraph.ai get
70
+ # clean markdown instead of mkdocs-rendered HTML. Same maintainer
71
+ # as mkdocstrings; native dual-output via the `full_output:` config
72
+ # key. Surfaced by the v1.0.4 external user-test.
73
+ "mkdocs-llmstxt>=0.2",
68
74
  ]
69
75
 
70
76
  [project.scripts]
@@ -66,6 +66,22 @@ _DOCS_URL_RE = re.compile(
66
66
  )
67
67
 
68
68
 
69
+ # URLs that are generated at build time and do NOT correspond to a
70
+ # `docs/*.md` source file. These bypass the source-presence check
71
+ # because their existence is verified by a dedicated build-time gate
72
+ # rather than by source-tree presence.
73
+ #
74
+ # `/llms.txt` and `/llms-full.txt` (CONTRACT v1.0.5 #1) are emitted by
75
+ # the `mkdocs-llmstxt` plugin during `mkdocs build`; their existence
76
+ # and well-formedness are verified by `tests/test_llms_txt.py`. Any
77
+ # URL added here must have a paired build-time test asserting the
78
+ # file lands at the site root.
79
+ _BUILD_GENERATED_URLS = frozenset({
80
+ "/llms.txt",
81
+ "/llms-full.txt",
82
+ })
83
+
84
+
69
85
  def _strip_trailing_punct(s: str) -> str:
70
86
  """URLs in prose often end at sentence boundaries; trim trailing
71
87
  punctuation that isn't part of the URL."""
@@ -181,6 +197,10 @@ def test_every_referenced_docs_url_has_a_source_page() -> None:
181
197
  missing: dict[Path, list[tuple[str, list[Path]]]] = {}
182
198
  unmapped: list[tuple[str, list[Path]]] = []
183
199
  for url_path, sources in referenced.items():
200
+ if url_path in _BUILD_GENERATED_URLS:
201
+ # Existence guaranteed by tests/test_llms_txt.py at build
202
+ # time; no docs/*.md source to check.
203
+ continue
184
204
  expected = _url_to_source_path(url_path)
185
205
  if expected is None:
186
206
  unmapped.append((url_path, sources))
@@ -129,6 +129,104 @@ def test_query_alias_still_works_with_positional_arg():
129
129
  assert {o.type for o in g.query("claim")} == {"claim"}
130
130
 
131
131
 
132
+ # v1.0.4 #1: graph.relations(source=, target=, type=) as the canonical
133
+ # filter API, decomposing the v0 get_relations(object_id=, direction=)
134
+ # axis into separate source/target slots. The contract claim is the
135
+ # eight filter combinations enumerated in CONTRACT v1.0.4 #1; the tests
136
+ # anchor on each combination directly, not on equivalence to the alias.
137
+ # The alias-still-works check is its own test.
138
+
139
+
140
+ def _three_node_graph():
141
+ g = _g()
142
+ a = g.add_object("task", {"k": "a"})
143
+ b = g.add_object("task", {"k": "b"})
144
+ c = g.add_object("task", {"k": "c"})
145
+ rel_ab_dep = g.add_relation(a.id, b.id, "depends_on")
146
+ rel_ac_dep = g.add_relation(a.id, c.id, "depends_on")
147
+ rel_bc_blk = g.add_relation(b.id, c.id, "blocks")
148
+ rel_ba_dep = g.add_relation(b.id, a.id, "depends_on")
149
+ return g, a, b, c, rel_ab_dep, rel_ac_dep, rel_bc_blk, rel_ba_dep
150
+
151
+
152
+ def test_relations_no_kwargs_returns_every_relation():
153
+ # Contract row 1: (None, None, None) -> every relation.
154
+ g, _, _, _, *rels = _three_node_graph()
155
+ out = g.relations()
156
+ assert {r.id for r in out} == {r.id for r in rels}
157
+
158
+
159
+ def test_relations_source_only_returns_outgoing_from_source():
160
+ # Contract row 2: (A, None, None) -> relations where source == A.
161
+ g, a, b, c, rel_ab, rel_ac, _, _ = _three_node_graph()
162
+ out = g.relations(source=a.id)
163
+ assert {r.id for r in out} == {rel_ab.id, rel_ac.id}
164
+ assert all(r.source == a.id for r in out)
165
+
166
+
167
+ def test_relations_target_only_returns_incoming_to_target():
168
+ # Contract row 3: (None, B, None) -> relations where target == B.
169
+ g, a, b, c, rel_ab, _, _, _ = _three_node_graph()
170
+ out = g.relations(target=b.id)
171
+ assert {r.id for r in out} == {rel_ab.id}
172
+ assert all(r.target == b.id for r in out)
173
+
174
+
175
+ def test_relations_source_and_target_returns_intersection():
176
+ # Contract row 4: (A, B, None) -> relations from A to B.
177
+ g, a, b, c, rel_ab, _, _, rel_ba = _three_node_graph()
178
+ out = g.relations(source=a.id, target=b.id)
179
+ assert {r.id for r in out} == {rel_ab.id}
180
+ # Not the reverse direction.
181
+ assert rel_ba.id not in {r.id for r in out}
182
+
183
+
184
+ def test_relations_type_only_returns_every_relation_of_type():
185
+ # Contract row 5: (None, None, T) -> every relation of type T.
186
+ g, *_, rel_bc_blk, _ = _three_node_graph()
187
+ out = g.relations(type="blocks")
188
+ assert {r.id for r in out} == {rel_bc_blk.id}
189
+ assert all(r.type == "blocks" for r in out)
190
+
191
+
192
+ def test_relations_source_and_type_returns_outgoing_of_type():
193
+ # Contract row 6: (A, None, T) -> relations from A of type T.
194
+ g, a, _, _, rel_ab, rel_ac, _, _ = _three_node_graph()
195
+ out = g.relations(source=a.id, type="depends_on")
196
+ assert {r.id for r in out} == {rel_ab.id, rel_ac.id}
197
+
198
+
199
+ def test_relations_target_and_type_returns_incoming_of_type():
200
+ # Contract row 7: (None, B, T) -> relations to B of type T.
201
+ g, a, b, _, rel_ab, _, _, _ = _three_node_graph()
202
+ out = g.relations(target=b.id, type="depends_on")
203
+ assert {r.id for r in out} == {rel_ab.id}
204
+
205
+
206
+ def test_relations_source_target_and_type_most_specific():
207
+ # Contract row 8: (A, B, T) -> relations from A to B of type T.
208
+ g, a, b, _, rel_ab, _, _, rel_ba = _three_node_graph()
209
+ out = g.relations(source=a.id, target=b.id, type="depends_on")
210
+ assert {r.id for r in out} == {rel_ab.id}
211
+ # Different-direction same-type relation is excluded.
212
+ assert rel_ba.id not in {r.id for r in out}
213
+ # Wrong-type from A->B does not exist; sanity check empty for a wrong type.
214
+ assert g.relations(source=a.id, target=b.id, type="blocks") == []
215
+
216
+
217
+ def test_get_relations_alias_still_works():
218
+ # Backward-compat: the v0 shape continues to work byte-identically.
219
+ # This is the alias relationship, not the contract claim — kept as
220
+ # its own test per CONTRACT v1.0.4 #1's Standing Rule §2 carve-out.
221
+ g, a, b, c, rel_ab, rel_ac, _, rel_ba = _three_node_graph()
222
+ out = g.get_relations(object_id=a.id, direction="outgoing")
223
+ assert {r.id for r in out} == {rel_ab.id, rel_ac.id}
224
+ incoming = g.get_relations(object_id=a.id, direction="incoming")
225
+ assert {r.id for r in incoming} == {rel_ba.id}
226
+ both = g.get_relations(object_id=a.id, direction="both")
227
+ assert {r.id for r in both} == {rel_ab.id, rel_ac.id, rel_ba.id}
228
+
229
+
132
230
  def test_neighborhood_walks_to_depth():
133
231
  g = _g()
134
232
  a = g.add_object("task", {})
@@ -0,0 +1,184 @@
1
+ """llms.txt / llms-full.txt generation gate. CONTRACT v1.0.5 #1.
2
+
3
+ Builds the mkdocs site into a temp directory and asserts that
4
+ ``site/llms.txt`` and ``site/llms-full.txt`` exist after the build,
5
+ are well-formed (H1, blockquote summary, at least one H2 section),
6
+ and reference the nav-anchor pages named in CONTRACT v1.0.5 #1's
7
+ contract claim. The "content coverage" assertion is the boundary
8
+ guard against the failure mode where the build succeeds but emits
9
+ an llms.txt that contains only the H1 — file existence alone is
10
+ not sufficient.
11
+
12
+ Standing Rule §2 shape: the test anchors on the contract boundary
13
+ the v1.0.5 #1 amendment names ("both files exist after build and
14
+ are well-formed"), not on the implementation path of the
15
+ `mkdocs-llmstxt` plugin. If a future release swaps the plugin for
16
+ a custom build script or a different plugin, this test stays
17
+ correct without modification.
18
+
19
+ Marked ``slow`` because ``mkdocs build`` takes several seconds.
20
+ Local ``pytest`` skips it by default; the ``.github/workflows/docs.yml``
21
+ workflow runs it explicitly via ``pytest -m slow tests/test_llms_txt.py``.
22
+ The precedent is ``tests/test_doc_site_reachable.py`` (CONTRACT v1.1
23
+ #9), which uses the same marker for the same reason.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import shutil
29
+ import subprocess
30
+ from pathlib import Path
31
+
32
+ import pytest
33
+
34
+
35
+ REPO_ROOT = Path(__file__).resolve().parent.parent
36
+ MKDOCS_CONFIG = REPO_ROOT / "mkdocs.yml"
37
+
38
+
39
+ @pytest.fixture(scope="module")
40
+ def built_site(tmp_path_factory: pytest.TempPathFactory) -> Path:
41
+ """Run ``mkdocs build`` into a temp dir; return the site/ path.
42
+
43
+ Module-scoped: the build is shared across every assertion in
44
+ this file. The fixture skips the entire module if ``mkdocs`` is
45
+ not installed — keeps the gate honest about its hard dependency
46
+ on the docs-build environment without polluting unrelated test
47
+ runs.
48
+ """
49
+ if shutil.which("mkdocs") is None:
50
+ pytest.skip("mkdocs binary not available; install '.[docs]' to run")
51
+
52
+ site_dir = tmp_path_factory.mktemp("llms_txt_gate") / "site"
53
+ result = subprocess.run(
54
+ [
55
+ "mkdocs",
56
+ "build",
57
+ "--config-file",
58
+ str(MKDOCS_CONFIG),
59
+ "--site-dir",
60
+ str(site_dir),
61
+ "--clean",
62
+ ],
63
+ capture_output=True,
64
+ text=True,
65
+ cwd=REPO_ROOT,
66
+ )
67
+ if result.returncode != 0:
68
+ # Surface the actual mkdocs error so a failing gate is
69
+ # diagnosable from the CI log — not just "build failed."
70
+ pytest.fail(
71
+ "mkdocs build failed:\n"
72
+ f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
73
+ )
74
+ return site_dir
75
+
76
+
77
+ @pytest.mark.slow
78
+ def test_llms_txt_exists(built_site: Path) -> None:
79
+ """Contract: ``/llms.txt`` is generated at site root after build."""
80
+ llms = built_site / "llms.txt"
81
+ assert llms.is_file(), (
82
+ f"site/llms.txt missing after mkdocs build (expected at {llms}). "
83
+ "Check the `llmstxt` plugin block in mkdocs.yml."
84
+ )
85
+
86
+
87
+ @pytest.mark.slow
88
+ def test_llms_full_txt_exists(built_site: Path) -> None:
89
+ """Contract: ``/llms-full.txt`` is generated at site root after build."""
90
+ llms_full = built_site / "llms-full.txt"
91
+ assert llms_full.is_file(), (
92
+ f"site/llms-full.txt missing after mkdocs build (expected at {llms_full}). "
93
+ "Check the `full_output:` key in the `llmstxt` plugin block."
94
+ )
95
+
96
+
97
+ @pytest.mark.slow
98
+ def test_llms_txt_h1_and_blockquote(built_site: Path) -> None:
99
+ """Contract: ``llms.txt`` starts with the H1 and a blockquote summary.
100
+
101
+ The plugin derives the H1 from ``site_name`` and the blockquote
102
+ from ``site_description``. The assertion shape catches both the
103
+ plugin-not-running case and the case where ``mkdocs.yml`` loses
104
+ one of those keys (which would break the llms.txt convention
105
+ described at llmstxt.org).
106
+ """
107
+ content = (built_site / "llms.txt").read_text(encoding="utf-8")
108
+ lines = content.splitlines()
109
+
110
+ assert lines[0] == "# Active Graph", (
111
+ f"llms.txt does not start with the expected H1; got: {lines[0]!r}"
112
+ )
113
+ # The blockquote summary line — first non-empty line after the H1.
114
+ blockquote = next((ln for ln in lines[1:] if ln.strip()), "")
115
+ assert blockquote.startswith("> "), (
116
+ f"llms.txt does not carry a blockquote summary; got: {blockquote!r}"
117
+ )
118
+ assert len(blockquote) > len("> "), (
119
+ "llms.txt blockquote summary is empty after the '> ' marker"
120
+ )
121
+
122
+
123
+ @pytest.mark.slow
124
+ def test_llms_txt_has_section_headers(built_site: Path) -> None:
125
+ """Contract: ``llms.txt`` carries H2 sections (navigation aid shape)."""
126
+ content = (built_site / "llms.txt").read_text(encoding="utf-8")
127
+ h2_count = sum(1 for ln in content.splitlines() if ln.startswith("## "))
128
+ assert h2_count >= 4, (
129
+ f"llms.txt has {h2_count} H2 section headers; expected at least 4 "
130
+ "(Quickstart / Concepts / Cookbook / Reference at minimum). "
131
+ "A file with the H1 but no sections is not useful as a nav aid."
132
+ )
133
+
134
+
135
+ @pytest.mark.slow
136
+ def test_llms_txt_references_nav_anchor_pages(built_site: Path) -> None:
137
+ """Contract: ``llms.txt`` references the nav-anchor pages by name.
138
+
139
+ The v1.0.5 #1 amendment names three anchors for the coverage
140
+ check: ``concepts/graph.md``, ``quickstart.md``, and at least
141
+ one cookbook page. These are the load-bearing entry points for
142
+ a first-time AI reader walking the index; if any one is
143
+ missing, the file fails to deliver on its navigation-aid
144
+ purpose.
145
+ """
146
+ content = (built_site / "llms.txt").read_text(encoding="utf-8")
147
+
148
+ assert "concepts/graph/" in content, (
149
+ "llms.txt does not reference docs/concepts/graph.md — the "
150
+ "primary entry point for the framework's primitive model."
151
+ )
152
+ assert "quickstart/" in content, (
153
+ "llms.txt does not reference docs/quickstart.md — the "
154
+ "ten-minute tutorial that the README points new users at."
155
+ )
156
+ assert "cookbook/" in content, (
157
+ "llms.txt does not reference any cookbook page — at least "
158
+ "one of common-patterns.md / debugging.md / multi-run-scripts.md "
159
+ "is expected."
160
+ )
161
+
162
+
163
+ @pytest.mark.slow
164
+ def test_llms_full_txt_well_formed(built_site: Path) -> None:
165
+ """Contract: ``llms-full.txt`` carries the H1 + actual page bodies.
166
+
167
+ Asserts on a distinctive prose marker from ``docs/quickstart.md``
168
+ so a file that contains the H1 and blockquote but no concatenated
169
+ bodies fails. The marker is the tutorial's opening sentence,
170
+ voice-locked since v1.0-rc1 and unlikely to churn.
171
+ """
172
+ content = (built_site / "llms-full.txt").read_text(encoding="utf-8")
173
+
174
+ assert content.startswith("# Active Graph"), (
175
+ "llms-full.txt does not start with the expected H1"
176
+ )
177
+ # Distinctive phrase from docs/quickstart.md's first paragraph —
178
+ # if llms-full.txt contains it, the quickstart body made it
179
+ # into the concatenated output.
180
+ assert "Ten minutes from install to a working custom behavior" in content, (
181
+ "llms-full.txt does not contain the quickstart body marker; "
182
+ "the file may be the index-only output instead of the "
183
+ "concatenated full content."
184
+ )
@@ -128,6 +128,70 @@ class TestRequeueUnfiredDoesNotFalselyRequeue:
128
128
  except FileNotFoundError:
129
129
  pass
130
130
 
131
+ def test_zero_subscriber_event_ids_are_absent_from_requeue_set_on_load(self):
132
+ """Boundary-anchored sibling to the queue_depth assertion above.
133
+
134
+ CONTRACT v0.5 #8 names the boundary as `Runtime.load` and the
135
+ subject as the requeue set itself: "events with NO
136
+ `behavior.started` referencing them are re-queued; events that
137
+ any behavior started on are NOT re-queued." A zero-subscriber
138
+ event has no `behavior.started` referencing it but is
139
+ popped-and-discarded by the runtime loop — so the contract
140
+ carve-out is that zero-subscriber events are NOT in the
141
+ requeue set on load.
142
+
143
+ The sibling test above asserts `queue_depth == 0` (the
144
+ implementation's symptom). This test asserts directly on the
145
+ boundary the contract names: it identifies the zero-subscriber
146
+ event IDs from the saved log, reloads, and verifies those IDs
147
+ are absent from `rt._queue` after `_requeue_unfired` ran on
148
+ load. Anchors on what the contract says, not on whether the
149
+ post-load queue depth happens to be zero (CONTRACT v1.0.4 #4,
150
+ Standing Rule §2).
151
+ """
152
+ _register_subscriber_for_goal_only()
153
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
154
+ path = f.name
155
+ os.remove(path)
156
+ try:
157
+ g = Graph()
158
+ rt = Runtime(g, persist_to=path)
159
+ rt.run_goal("test_goal")
160
+ rt.save_state()
161
+
162
+ zero_subscriber_event_ids = {
163
+ e.id for e in rt.graph.events
164
+ if e.type == "downstream.no_subscribers"
165
+ }
166
+ assert zero_subscriber_event_ids, (
167
+ "Fixture is supposed to produce at least one "
168
+ "downstream.no_subscribers event. Without it, the "
169
+ "contract claim's specific subject (the zero-"
170
+ "subscriber carve-out) isn't being exercised."
171
+ )
172
+
173
+ _register_subscriber_for_goal_only()
174
+ rt2 = Runtime.load(path, run_id=rt.run_id)
175
+
176
+ requeued_event_ids = {e.id for e in rt2._queue._q} # noqa: SLF001
177
+ falsely_requeued = zero_subscriber_event_ids & requeued_event_ids
178
+ assert not falsely_requeued, (
179
+ f"CONTRACT v0.5 #8 carve-out: zero-subscriber events are "
180
+ f"popped-and-discarded with no behavior.started emitted, "
181
+ f"so on Runtime.load they must NOT appear in the requeue "
182
+ f"set. The following zero-subscriber event IDs were "
183
+ f"falsely requeued: {sorted(falsely_requeued)}. This is "
184
+ f"the v0.5 #8 boundary the contract names; failure here "
185
+ f"means the requeue set itself violates the contract "
186
+ f"regardless of what queue_depth summarizes."
187
+ )
188
+ finally:
189
+ for suffix in ("", "-wal", "-shm"):
190
+ try:
191
+ os.remove(path + suffix)
192
+ except FileNotFoundError:
193
+ pass
194
+
131
195
 
132
196
  class TestRequeueUnfiredPreservesCrashRecovery:
133
197
  """The fix narrows the requeue scope to events after the last drain.
File without changes