activegraph 1.5.0__tar.gz → 1.6.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 (192) hide show
  1. {activegraph-1.5.0/activegraph.egg-info → activegraph-1.6.0}/PKG-INFO +1 -1
  2. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/__init__.py +1 -1
  3. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/loader.py +108 -0
  4. {activegraph-1.5.0 → activegraph-1.6.0/activegraph.egg-info}/PKG-INFO +1 -1
  5. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph.egg-info/SOURCES.txt +1 -0
  6. {activegraph-1.5.0 → activegraph-1.6.0}/pyproject.toml +1 -1
  7. activegraph-1.6.0/tests/test_manifest_warning_tier.py +171 -0
  8. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_promote.py +61 -0
  9. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_sandbox_trial.py +107 -0
  10. {activegraph-1.5.0 → activegraph-1.6.0}/LICENSE +0 -0
  11. {activegraph-1.5.0 → activegraph-1.6.0}/NOTICE +0 -0
  12. {activegraph-1.5.0 → activegraph-1.6.0}/README.md +0 -0
  13. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/__main__.py +0 -0
  14. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/_signature.py +0 -0
  15. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/behaviors/__init__.py +0 -0
  16. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/behaviors/base.py +0 -0
  17. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/behaviors/decorators.py +0 -0
  18. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/cli/__init__.py +0 -0
  19. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/cli/main.py +0 -0
  20. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/cli/quickstart.py +0 -0
  21. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/cli/renderers.py +0 -0
  22. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/__init__.py +0 -0
  23. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/clock.py +0 -0
  24. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/event.py +0 -0
  25. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/graph.py +0 -0
  26. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/graph_store.py +0 -0
  27. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/ids.py +0 -0
  28. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/patch.py +0 -0
  29. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/core/view.py +0 -0
  30. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/errors.py +0 -0
  31. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/frame.py +0 -0
  32. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/__init__.py +0 -0
  33. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/anthropic.py +0 -0
  34. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/cache.py +0 -0
  35. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/embedding.py +0 -0
  36. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/errors.py +0 -0
  37. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/native.py +0 -0
  38. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/openai.py +0 -0
  39. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/parsing.py +0 -0
  40. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/prompt.py +0 -0
  41. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/provider.py +0 -0
  42. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/recorded.py +0 -0
  43. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/types.py +0 -0
  44. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/llm/wire.py +0 -0
  45. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/__init__.py +0 -0
  46. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/logging.py +0 -0
  47. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/metrics.py +0 -0
  48. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/migration.py +0 -0
  49. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/otel.py +0 -0
  50. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/prometheus.py +0 -0
  51. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/observability/status.py +0 -0
  52. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/__init__.py +0 -0
  53. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/__init__.py +0 -0
  54. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/behaviors.py +0 -0
  55. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  56. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  57. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/object_types.py +0 -0
  58. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  59. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  60. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  61. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  62. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/settings.py +0 -0
  63. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/diligence/tools.py +0 -0
  64. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/manifest.py +0 -0
  65. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/packs/scaffold.py +0 -0
  66. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/policy.py +0 -0
  67. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/__init__.py +0 -0
  68. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/_live.py +0 -0
  69. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/behavior_graph.py +0 -0
  70. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/budget.py +0 -0
  71. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/config_errors.py +0 -0
  72. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/diff.py +0 -0
  73. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/errors.py +0 -0
  74. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/exec_errors.py +0 -0
  75. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/patterns.py +0 -0
  76. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/promote.py +0 -0
  77. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/queue.py +0 -0
  78. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/registration_errors.py +0 -0
  79. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/registry.py +0 -0
  80. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/runtime.py +0 -0
  81. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/scheduler.py +0 -0
  82. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/runtime/view_builder.py +0 -0
  83. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/sandbox/__init__.py +0 -0
  84. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/sandbox/_child.py +0 -0
  85. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/__init__.py +0 -0
  86. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/base.py +0 -0
  87. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/conformance.py +0 -0
  88. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/errors.py +0 -0
  89. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/falkordb.py +0 -0
  90. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/graph_conformance.py +0 -0
  91. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/memory.py +0 -0
  92. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/postgres.py +0 -0
  93. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/retention.py +0 -0
  94. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/serde.py +0 -0
  95. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/sqlite.py +0 -0
  96. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/store/url.py +0 -0
  97. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/__init__.py +0 -0
  98. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/base.py +0 -0
  99. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/cache.py +0 -0
  100. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/context.py +0 -0
  101. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/decorators.py +0 -0
  102. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/errors.py +0 -0
  103. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/graph_query.py +0 -0
  104. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/recorded.py +0 -0
  105. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/tools/web_fetch.py +0 -0
  106. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/trace/__init__.py +0 -0
  107. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/trace/causal.py +0 -0
  108. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph/trace/printer.py +0 -0
  109. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph.egg-info/dependency_links.txt +0 -0
  110. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph.egg-info/entry_points.txt +0 -0
  111. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph.egg-info/requires.txt +0 -0
  112. {activegraph-1.5.0 → activegraph-1.6.0}/activegraph.egg-info/top_level.txt +0 -0
  113. {activegraph-1.5.0 → activegraph-1.6.0}/setup.cfg +0 -0
  114. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_activate_after.py +0 -0
  115. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_causal_cross_tool.py +0 -0
  116. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_cli.py +0 -0
  117. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_cli_docs_flags.py +0 -0
  118. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_clock.py +0 -0
  119. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_compaction.py +0 -0
  120. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_diff.py +0 -0
  121. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_diligence_pack.py +0 -0
  122. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_diligence_with_tools.py +0 -0
  123. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_disable_pack.py +0 -0
  124. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_doc_links.py +0 -0
  125. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_doc_python_snippets.py +0 -0
  126. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_doc_site_reachable.py +0 -0
  127. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_embedding_provider.py +0 -0
  128. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_errors_format.py +0 -0
  129. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_event.py +0 -0
  130. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_falkordb_store.py +0 -0
  131. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_fork.py +0 -0
  132. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_graph.py +0 -0
  133. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_graph_store.py +0 -0
  134. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_ids.py +0 -0
  135. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_license.py +0 -0
  136. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_anthropic.py +0 -0
  137. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_behavior.py +0 -0
  138. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_budget.py +0 -0
  139. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_causal.py +0 -0
  140. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_claim_extraction.py +0 -0
  141. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_default_model.py +0 -0
  142. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_determinism.py +0 -0
  143. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_failure.py +0 -0
  144. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_native_structured_output.py +0 -0
  145. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_openai.py +0 -0
  146. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_prompt.py +0 -0
  147. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_provider_fixtures.py +0 -0
  148. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_replay.py +0 -0
  149. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_tool_loop.py +0 -0
  150. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_trace.py +0 -0
  151. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_trace_snapshot.py +0 -0
  152. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_types.py +0 -0
  153. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llm_wire.py +0 -0
  154. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_llms_txt.py +0 -0
  155. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_migration.py +0 -0
  156. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_observability_logging.py +0 -0
  157. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_observability_metrics.py +0 -0
  158. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_operate_example.py +0 -0
  159. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_pack_manifest.py +0 -0
  160. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_pack_scaffold.py +0 -0
  161. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_packs.py +0 -0
  162. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_patch.py +0 -0
  163. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_pattern_matcher.py +0 -0
  164. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_pattern_parser.py +0 -0
  165. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_pattern_subscriptions.py +0 -0
  166. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_persistence.py +0 -0
  167. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_postgres_store.py +0 -0
  168. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_quickstart.py +0 -0
  169. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_quickstart_snapshot.py +0 -0
  170. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_reason_codes_docs.py +0 -0
  171. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_registration_validation.py +0 -0
  172. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_replay.py +0 -0
  173. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_replay_trace_snapshot.py +0 -0
  174. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_requeue_unfired.py +0 -0
  175. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_resume_example.py +0 -0
  176. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_runtime.py +0 -0
  177. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_runtime_status.py +0 -0
  178. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_serde.py +0 -0
  179. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_store_conformance.py +0 -0
  180. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_store_url.py +0 -0
  181. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_tool_replay.py +0 -0
  182. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_tool_trace_snapshot.py +0 -0
  183. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_tools.py +0 -0
  184. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_trace.py +0 -0
  185. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_trace_accessors.py +0 -0
  186. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_tutorial_snippets.py +0 -0
  187. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_v1_0_1_patches.py +0 -0
  188. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  189. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  190. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_version_sync.py +0 -0
  191. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_view.py +0 -0
  192. {activegraph-1.5.0 → activegraph-1.6.0}/tests/test_wheel_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.5.0
3
+ Version: 1.6.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
@@ -242,4 +242,4 @@ __all__ = [
242
242
  "tool",
243
243
  ]
244
244
 
245
- __version__ = "1.5.0"
245
+ __version__ = "1.6.0"
@@ -27,6 +27,9 @@ from __future__ import annotations
27
27
  import copy
28
28
  import inspect
29
29
  import json
30
+ import logging
31
+ import sys
32
+ from pathlib import Path
30
33
  from typing import TYPE_CHECKING, Any, Callable, Optional
31
34
 
32
35
  from pydantic import BaseModel, ValidationError
@@ -309,9 +312,114 @@ def load_pack_into_runtime(
309
312
  timestamp=rt.graph.clock.now(),
310
313
  )
311
314
  )
315
+
316
+ # ---- 6. manifest warning tier (CONTRACT v1.6 #1) ----------------------
317
+ _warn_on_manifest_violations(pack)
312
318
  return True
313
319
 
314
320
 
321
+ # --------------------------------------------- manifest warning tier
322
+ # CONTRACT v1.6 #1: when a manifest.toml is discoverable next to the
323
+ # pack's defining modules, load_pack runs load_manifest +
324
+ # verify_surface and WARNs on violations — structured, once per pack
325
+ # per process, never an error before 2.0. Absent manifest: silent.
326
+ # Hash verification (content/bundle) stays host/CI territory: the
327
+ # loader never re-hashes a pack directory on the hot path.
328
+
329
+ _manifest_log = logging.getLogger("activegraph.packs.manifest")
330
+
331
+ # (pack name, pack version, manifest path) triples already validated
332
+ # this process — clean or not, the tier runs once per pack.
333
+ _manifest_checked: set[tuple[str, str, str]] = set()
334
+
335
+
336
+ def _locate_pack_manifest(pack: Pack) -> Optional[Path]:
337
+ """Best-effort pack-root discovery: resolve the modules that define
338
+ the pack's components and walk up (while still inside the package)
339
+ looking for a sibling ``manifest.toml``. Returns None when the
340
+ pack has no discoverable manifest — which is not a violation.
341
+ """
342
+ components: list[Any] = [getattr(b, "fn", None) for b in pack.behaviors]
343
+ components += [getattr(t, "fn", None) for t in pack.tools]
344
+ if pack.settings_schema is not EmptySettings:
345
+ components.append(pack.settings_schema)
346
+ components += [ot.schema for ot in pack.object_types]
347
+
348
+ seen_modules: set[str] = set()
349
+ for comp in components:
350
+ mod_name = getattr(comp, "__module__", None)
351
+ if not mod_name or mod_name == "__main__" or mod_name in seen_modules:
352
+ continue
353
+ seen_modules.add(mod_name)
354
+ mod_file = getattr(sys.modules.get(mod_name), "__file__", None)
355
+ if not mod_file:
356
+ continue
357
+ directory = Path(mod_file).resolve().parent
358
+ while True:
359
+ candidate = directory / "manifest.toml"
360
+ if candidate.is_file():
361
+ return candidate
362
+ # Stop once we step outside the package: the manifest
363
+ # lives at the pack root, and the pack root is a package.
364
+ if not (directory / "__init__.py").is_file():
365
+ break
366
+ if directory.parent == directory:
367
+ break
368
+ directory = directory.parent
369
+ return None
370
+
371
+
372
+ def _warn_on_manifest_violations(pack: Pack) -> None:
373
+ """The warning tier itself. Never raises: a broken manifest (or a
374
+ bug in discovery) must not break ``load_pack`` before 2.0.
375
+ """
376
+ try:
377
+ manifest_path = _locate_pack_manifest(pack)
378
+ if manifest_path is None:
379
+ return # no manifest: silent, per the Q2 schedule
380
+ key = (pack.name, pack.version, str(manifest_path))
381
+ if key in _manifest_checked:
382
+ return
383
+ _manifest_checked.add(key)
384
+ from activegraph.packs.manifest import (
385
+ PackManifestError,
386
+ load_manifest,
387
+ verify_surface,
388
+ )
389
+
390
+ try:
391
+ manifest = load_manifest(manifest_path)
392
+ verify_surface(manifest, pack)
393
+ except PackManifestError as err:
394
+ _manifest_log.warning(
395
+ "pack %s@%s: manifest.toml found at %s but validation "
396
+ "failed with %d violation(s). The pack still loads — "
397
+ "this stays a warning until activegraph 2.0. First "
398
+ "violation: %s",
399
+ pack.name,
400
+ pack.version,
401
+ manifest_path,
402
+ len(err.violations),
403
+ err.violations[0] if err.violations else "",
404
+ extra={
405
+ "pack": pack.name,
406
+ "pack_version": pack.version,
407
+ "manifest_path": str(manifest_path),
408
+ "violations": list(err.violations),
409
+ "reason": "pack.manifest_invalid",
410
+ },
411
+ )
412
+ except Exception:
413
+ # The tier is advisory; discovery/IO surprises are debug noise,
414
+ # not load failures.
415
+ _manifest_log.debug(
416
+ "manifest warning tier errored for pack %s@%s",
417
+ pack.name,
418
+ pack.version,
419
+ exc_info=True,
420
+ )
421
+
422
+
315
423
  # ---------------------------------------------------------------- state
316
424
 
317
425
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.5.0
3
+ Version: 1.6.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
@@ -147,6 +147,7 @@ tests/test_llm_trace_snapshot.py
147
147
  tests/test_llm_types.py
148
148
  tests/test_llm_wire.py
149
149
  tests/test_llms_txt.py
150
+ tests/test_manifest_warning_tier.py
150
151
  tests/test_migration.py
151
152
  tests/test_observability_logging.py
152
153
  tests/test_observability_metrics.py
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "activegraph"
10
- version = "1.5.0"
10
+ version = "1.6.0"
11
11
  description = "An event-sourced reactive graph runtime for long-running, auditable, agentic systems."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,171 @@
1
+ """Loader-side manifest validation, warning tier (CONTRACT v1.6 #1).
2
+
3
+ When a ``manifest.toml`` sits at the pack root, ``load_pack`` runs
4
+ ``load_manifest`` + ``verify_surface`` and WARNs on violations —
5
+ structured, once per pack per process, never an error before 2.0.
6
+ Absent manifest: silent. All offline and deterministic.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import logging
13
+ import sys
14
+
15
+ import pytest
16
+
17
+ from activegraph import Graph, Runtime
18
+ from activegraph.packs import loader as pack_loader
19
+ from activegraph.packs.manifest import compute_content_hash
20
+
21
+ PACK_INIT_TEMPLATE = '''
22
+ from activegraph.packs import Pack, behavior
23
+
24
+
25
+ @behavior(name="greeter", on=["goal.created"])
26
+ def greeter(event, graph, ctx):
27
+ graph.add_object("greeting", {{"text": "hi"}})
28
+
29
+
30
+ pack = Pack(name="{name}", version="0.1.0", behaviors=(greeter,))
31
+ '''
32
+
33
+ MANIFEST_TEMPLATE = """
34
+ [pack]
35
+ name = "{name}"
36
+ version = "0.1.0"
37
+ description = "Warning-tier fixture."
38
+ license = ""
39
+
40
+ [pack.provenance]
41
+ authored_by = "agent"
42
+ generator = "test"
43
+
44
+ [pack.integrity]
45
+ content_hash = "{content_hash}"
46
+
47
+ [dependencies]
48
+ activegraph = ">=1.5,<2.0"
49
+ python-deps = []
50
+
51
+ [surface]
52
+ object_types = []
53
+ relation_types = []
54
+ behaviors = [{behaviors}]
55
+ tools = []
56
+ settings_schema = ""
57
+
58
+ [fixtures]
59
+ entrypoint = "fixtures/run_fixtures.py"
60
+ deterministic = true
61
+ """
62
+
63
+
64
+ @pytest.fixture(autouse=True)
65
+ def _fresh_tier():
66
+ """Each test exercises the tier from scratch: the once-per-process
67
+ dedupe set is process state, so tests reset it explicitly."""
68
+ pack_loader._manifest_checked.clear()
69
+ yield
70
+ pack_loader._manifest_checked.clear()
71
+
72
+
73
+ def _make_pack(tmp_path, name, *, declared_behaviors='"greeter"',
74
+ manifest=True, manifest_text=None):
75
+ """Write a real pack package to disk, import it so its module has
76
+ a resolvable ``__file__``, and return the live Pack."""
77
+ root = tmp_path / name
78
+ root.mkdir()
79
+ (root / "__init__.py").write_text(PACK_INIT_TEMPLATE.format(name=name))
80
+ if manifest:
81
+ if manifest_text is None:
82
+ manifest_text = MANIFEST_TEMPLATE.format(
83
+ name=name,
84
+ content_hash=compute_content_hash(root),
85
+ behaviors=declared_behaviors,
86
+ )
87
+ (root / "manifest.toml").write_text(manifest_text)
88
+ spec = importlib.util.spec_from_file_location(
89
+ name, root / "__init__.py", submodule_search_locations=[str(root)]
90
+ )
91
+ module = importlib.util.module_from_spec(spec)
92
+ sys.modules[name] = module
93
+ try:
94
+ spec.loader.exec_module(module)
95
+ except BaseException:
96
+ del sys.modules[name]
97
+ raise
98
+ return module.pack
99
+
100
+
101
+ def _tier_records(caplog):
102
+ return [
103
+ r
104
+ for r in caplog.records
105
+ if r.name == "activegraph.packs.manifest"
106
+ and r.levelno >= logging.WARNING
107
+ ]
108
+
109
+
110
+ def test_matching_manifest_loads_silently(tmp_path, caplog):
111
+ pack = _make_pack(tmp_path, "warn_tier_clean")
112
+ with caplog.at_level(logging.WARNING, logger="activegraph.packs.manifest"):
113
+ rt = Runtime(Graph(), behaviors=[])
114
+ assert rt.load_pack(pack) is True
115
+ assert _tier_records(caplog) == []
116
+
117
+
118
+ def test_absent_manifest_stays_silent(tmp_path, caplog):
119
+ pack = _make_pack(tmp_path, "warn_tier_bare", manifest=False)
120
+ with caplog.at_level(logging.WARNING, logger="activegraph.packs.manifest"):
121
+ rt = Runtime(Graph(), behaviors=[])
122
+ assert rt.load_pack(pack) is True
123
+ assert _tier_records(caplog) == []
124
+
125
+
126
+ def test_surface_mismatch_warns_once_structured_and_still_loads(
127
+ tmp_path, caplog
128
+ ):
129
+ # The manifest declares a behavior the Pack never registers: a
130
+ # real violation. The pack STILL loads — warning tier only.
131
+ pack = _make_pack(
132
+ tmp_path, "warn_tier_drift", declared_behaviors='"greeter", "ghost"'
133
+ )
134
+ with caplog.at_level(logging.WARNING, logger="activegraph.packs.manifest"):
135
+ rt = Runtime(Graph(), behaviors=[])
136
+ assert rt.load_pack(pack) is True
137
+ # Loaded despite the violation: the behavior dispatches.
138
+ rt.run_goal("check")
139
+ assert [
140
+ o for o in rt.graph.all_objects() if o.type == "greeting"
141
+ ]
142
+ # A second runtime loading the same pack does not re-warn:
143
+ # once per pack per process.
144
+ rt2 = Runtime(Graph(), behaviors=[])
145
+ assert rt2.load_pack(pack) is True
146
+
147
+ records = _tier_records(caplog)
148
+ assert len(records) == 1
149
+ (record,) = records
150
+ # Structured: the record carries the machine-readable fields.
151
+ assert record.pack == "warn_tier_drift"
152
+ assert record.pack_version == "0.1.0"
153
+ assert record.reason == "pack.manifest_invalid"
154
+ assert record.manifest_path.endswith("manifest.toml")
155
+ assert any("ghost" in v for v in record.violations)
156
+ # And the human line says the pack still loads.
157
+ assert "still loads" in record.getMessage()
158
+
159
+
160
+ def test_malformed_manifest_warns_never_raises(tmp_path, caplog):
161
+ pack = _make_pack(
162
+ tmp_path,
163
+ "warn_tier_garbage",
164
+ manifest_text="this is not toml [[[",
165
+ )
166
+ with caplog.at_level(logging.WARNING, logger="activegraph.packs.manifest"):
167
+ rt = Runtime(Graph(), behaviors=[])
168
+ assert rt.load_pack(pack) is True # never an error before 2.0
169
+ records = _tier_records(caplog)
170
+ assert len(records) == 1
171
+ assert records[0].violations # the parse failure, verbatim
@@ -819,3 +819,64 @@ def test_promote_of_unknown_types_passes_untyped(tmp_path):
819
819
  n = fork.graph.add_object("freeform", {"anything": ["goes", 1]})
820
820
  parent.promote(fork)
821
821
  assert parent.graph.get_object(n.id).data == {"anything": ["goes", 1]}
822
+
823
+
824
+ # --------------------------- fork-tail removals are ordinary events
825
+ # The downstream residue policy (evolution pack): before promoting, a
826
+ # fork removes every fork-created object/relation it scaffolded so
827
+ # those entities read base-None/fork-None and vanish from the delta,
828
+ # while patches to shared state still promote. Promote must never
829
+ # special-case fork-tail removals — they are ordinary events
830
+ # (CONTRACT v1.3 #4 addendum 4d). This test is the pin.
831
+
832
+
833
+ def test_residue_policy_fork_tail_removal_of_fork_created_entities(tmp_path):
834
+ parent = _parent(tmp_path)
835
+ task = _task_id(parent)
836
+ fork = _fork_at_tip(parent)
837
+
838
+ # The fork does real work on shared state...
839
+ fork.graph.patch_object(task, {"status": "done"})
840
+
841
+ # ...and scaffolds trial residue: two fork-created objects, a
842
+ # relation between them, and a relation into shared state.
843
+ a = fork.graph.add_object("scaffold", {"n": 1})
844
+ b = fork.graph.add_object("scaffold", {"n": 2})
845
+ rel_ab = fork.graph.add_relation(a.id, b.id, "supports")
846
+ rel_task = fork.graph.add_relation(task, a.id, "tried")
847
+
848
+ # Fork tail: remove the residue with ordinary events — one
849
+ # explicit relation removal, then object removals whose cascade
850
+ # sweeps the relation into shared state.
851
+ fork.graph.remove_relation(rel_ab.id)
852
+ fork.graph.remove_object(a.id) # cascade removes rel_task too
853
+ fork.graph.remove_object(b.id)
854
+ assert fork.graph.get_relation(rel_task.id) is None
855
+
856
+ # The residue reads base-None/fork-None: not part of the delta.
857
+ plan = parent.promote(fork, dry_run=True)
858
+ assert plan.is_promotable
859
+ assert plan.object_creates == []
860
+ assert plan.relation_creates == []
861
+ assert plan.object_removes == []
862
+ assert plan.relation_removes == []
863
+ # The shared-state patch is the WHOLE delta, and it survives.
864
+ assert [o["id"] for o in plan.object_patches] == [task]
865
+
866
+ result = parent.promote(fork)
867
+ assert parent.graph.get_object(task).data["status"] == "done"
868
+ for residue_id in (a.id, b.id):
869
+ assert parent.graph.get_object(residue_id) is None
870
+ for residue_rel in (rel_ab.id, rel_task.id):
871
+ assert parent.graph.get_relation(residue_rel) is None
872
+ # And no removal events were manufactured for entities the parent
873
+ # never had: the promote block carries only the task patch.
874
+ marker = next(
875
+ e for e in parent.trace.events() if e.type == "promote.applied"
876
+ )
877
+ assert marker.payload["objects_patched"] == [task]
878
+ assert marker.payload["objects_created"] == []
879
+ assert marker.payload["objects_removed"] == []
880
+ assert marker.payload["relations_created"] == []
881
+ assert marker.payload["relations_removed"] == []
882
+ assert result.plan.is_promotable
@@ -258,3 +258,110 @@ pack = Pack(
258
258
  assert report.outcome == "materialization_failed"
259
259
  assert "undeclared" in report.detail
260
260
  _parent_untouched(path, parent_run, n_parent)
261
+
262
+
263
+ REPLAY_PACK_INIT = '''
264
+ from activegraph.packs import Pack, behavior
265
+
266
+
267
+ @behavior(name="responder", on=["chat.message"])
268
+ def responder(event, graph, ctx):
269
+ graph.add_object(
270
+ "reply", {"to": event.payload.get("text", ""), "text": "ack"}
271
+ )
272
+
273
+
274
+ pack = Pack(name="trial_candidate", version="0.1.0", behaviors=(responder,))
275
+ '''
276
+
277
+ REPLAY_MANIFEST = MANIFEST_TEMPLATE.replace(
278
+ 'behaviors = ["greeter"]', 'behaviors = ["responder"]'
279
+ )
280
+
281
+ REPLAY_SCENARIO = '''
282
+ from activegraph.core.event import Event
283
+
284
+
285
+ def main(rt):
286
+ # The recorded input segment IS the fork's history: read it back
287
+ # and re-inject each input as a fresh event so the candidate's
288
+ # behaviors process it live inside the trial.
289
+ segment = [e for e in rt.trace.events() if e.type == "chat.message"]
290
+ for recorded in segment:
291
+ rt.graph.emit(
292
+ Event(
293
+ id=rt.graph.ids.event(),
294
+ type="chat.message",
295
+ payload=dict(recorded.payload),
296
+ actor="trial.replay",
297
+ frame_id=None,
298
+ caused_by=None,
299
+ timestamp=rt.graph.clock.now(),
300
+ )
301
+ )
302
+ rt.run_until_idle()
303
+ '''
304
+
305
+
306
+ def test_recorded_segment_replay_inside_the_trial(tmp_path):
307
+ # THE consumer use case (evolution stage 3): fork a run, replay a
308
+ # recorded input segment against the fork inside the child, read
309
+ # failures + counts from the fork's run in the store afterward.
310
+ from activegraph.core.event import Event
311
+
312
+ clear_registry()
313
+ rt = Runtime(Graph())
314
+ # The parent's recorded inputs: three chat messages, no subscriber
315
+ # in the parent (they are raw recorded history).
316
+ for text in ("hello", "what's on today?", "thanks"):
317
+ rt.graph.emit(
318
+ Event(
319
+ id=rt.graph.ids.event(),
320
+ type="chat.message",
321
+ payload={"text": text},
322
+ actor="user",
323
+ frame_id=None,
324
+ caused_by=None,
325
+ timestamp=rt.graph.clock.now(),
326
+ )
327
+ )
328
+ rt.run_until_idle()
329
+ path = str(tmp_path / "run.db")
330
+ rt.save_state(path)
331
+ parent_run, tip, n_parent = (
332
+ rt.run_id,
333
+ rt.trace.events()[-1].id,
334
+ len(rt.graph.events),
335
+ )
336
+ del rt
337
+
338
+ root = tmp_path / "trial_candidate"
339
+ root.mkdir()
340
+ (root / "__init__.py").write_text(REPLAY_PACK_INIT)
341
+ (root / "scenario.py").write_text(REPLAY_SCENARIO)
342
+ content = compute_content_hash(root)
343
+ (root / "manifest.toml").write_text(
344
+ REPLAY_MANIFEST.format(content_hash=content)
345
+ )
346
+
347
+ report = run_forked_trial(
348
+ path,
349
+ parent_run_id=parent_run,
350
+ at_event=tip,
351
+ pack_source=PackSource(
352
+ root_dir=str(root),
353
+ expected_bundle_hash=compute_bundle_hash(root),
354
+ ),
355
+ scenario="scenario.py",
356
+ )
357
+ assert report.outcome == "completed", report.detail
358
+ assert report.behavior_failures == 0
359
+
360
+ # The candidate processed exactly the recorded segment, in order,
361
+ # inside the fork — and the evidence is ordinary fork history.
362
+ fork = Runtime.load(path, run_id=report.fork_run_id, behaviors=[])
363
+ replies = [
364
+ o.data["to"] for o in fork.graph.all_objects() if o.type == "reply"
365
+ ]
366
+ assert replies == ["hello", "what's on today?", "thanks"]
367
+ _parent_untouched(path, parent_run, n_parent)
File without changes
File without changes
File without changes