activegraph 1.0.1__tar.gz → 1.0.2.post1__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 (156) hide show
  1. {activegraph-1.0.1 → activegraph-1.0.2.post1}/PKG-INFO +1 -1
  2. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/__init__.py +1 -1
  3. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/behaviors/base.py +13 -2
  4. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/behaviors/decorators.py +26 -1
  5. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/anthropic.py +8 -0
  6. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/openai.py +20 -0
  7. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/provider.py +34 -3
  8. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/recorded.py +26 -0
  9. activegraph-1.0.2.post1/activegraph/runtime/_live.py +173 -0
  10. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/runtime.py +54 -0
  11. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph.egg-info/PKG-INFO +1 -1
  12. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph.egg-info/SOURCES.txt +2 -0
  13. {activegraph-1.0.1 → activegraph-1.0.2.post1}/pyproject.toml +1 -1
  14. activegraph-1.0.2.post1/tests/test_llm_default_model.py +554 -0
  15. {activegraph-1.0.1 → activegraph-1.0.2.post1}/README.md +0 -0
  16. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/__main__.py +0 -0
  17. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/behaviors/__init__.py +0 -0
  18. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/cli/__init__.py +0 -0
  19. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/cli/main.py +0 -0
  20. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/cli/quickstart.py +0 -0
  21. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/__init__.py +0 -0
  22. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/clock.py +0 -0
  23. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/event.py +0 -0
  24. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/graph.py +0 -0
  25. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/ids.py +0 -0
  26. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/patch.py +0 -0
  27. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/core/view.py +0 -0
  28. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/errors.py +0 -0
  29. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/frame.py +0 -0
  30. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/__init__.py +0 -0
  31. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/cache.py +0 -0
  32. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/errors.py +0 -0
  33. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/parsing.py +0 -0
  34. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/prompt.py +0 -0
  35. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/llm/types.py +0 -0
  36. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/observability/__init__.py +0 -0
  37. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/observability/logging.py +0 -0
  38. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/observability/metrics.py +0 -0
  39. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/observability/migration.py +0 -0
  40. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/observability/prometheus.py +0 -0
  41. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/observability/status.py +0 -0
  42. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/__init__.py +0 -0
  43. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/__init__.py +0 -0
  44. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/behaviors.py +0 -0
  45. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  46. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  47. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/object_types.py +0 -0
  48. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  49. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  50. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  51. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  52. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/settings.py +0 -0
  53. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/diligence/tools.py +0 -0
  54. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/loader.py +0 -0
  55. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/packs/scaffold.py +0 -0
  56. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/policy.py +0 -0
  57. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/__init__.py +0 -0
  58. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/behavior_graph.py +0 -0
  59. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/budget.py +0 -0
  60. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/config_errors.py +0 -0
  61. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/diff.py +0 -0
  62. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/errors.py +0 -0
  63. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/exec_errors.py +0 -0
  64. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/patterns.py +0 -0
  65. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/queue.py +0 -0
  66. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/registration_errors.py +0 -0
  67. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/registry.py +0 -0
  68. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/scheduler.py +0 -0
  69. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/runtime/view_builder.py +0 -0
  70. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/__init__.py +0 -0
  71. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/base.py +0 -0
  72. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/conformance.py +0 -0
  73. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/errors.py +0 -0
  74. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/memory.py +0 -0
  75. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/postgres.py +0 -0
  76. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/serde.py +0 -0
  77. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/sqlite.py +0 -0
  78. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/store/url.py +0 -0
  79. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/__init__.py +0 -0
  80. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/base.py +0 -0
  81. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/cache.py +0 -0
  82. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/context.py +0 -0
  83. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/decorators.py +0 -0
  84. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/errors.py +0 -0
  85. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/graph_query.py +0 -0
  86. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/recorded.py +0 -0
  87. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/tools/web_fetch.py +0 -0
  88. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/trace/__init__.py +0 -0
  89. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/trace/causal.py +0 -0
  90. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph/trace/printer.py +0 -0
  91. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph.egg-info/dependency_links.txt +0 -0
  92. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph.egg-info/entry_points.txt +0 -0
  93. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph.egg-info/requires.txt +0 -0
  94. {activegraph-1.0.1 → activegraph-1.0.2.post1}/activegraph.egg-info/top_level.txt +0 -0
  95. {activegraph-1.0.1 → activegraph-1.0.2.post1}/setup.cfg +0 -0
  96. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_activate_after.py +0 -0
  97. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_causal_cross_tool.py +0 -0
  98. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_cli.py +0 -0
  99. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_clock.py +0 -0
  100. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_diff.py +0 -0
  101. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_diligence_pack.py +0 -0
  102. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_diligence_with_tools.py +0 -0
  103. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_doc_links.py +0 -0
  104. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_doc_site_reachable.py +0 -0
  105. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_errors_format.py +0 -0
  106. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_event.py +0 -0
  107. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_fork.py +0 -0
  108. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_graph.py +0 -0
  109. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_ids.py +0 -0
  110. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_anthropic.py +0 -0
  111. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_behavior.py +0 -0
  112. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_budget.py +0 -0
  113. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_causal.py +0 -0
  114. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_claim_extraction.py +0 -0
  115. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_determinism.py +0 -0
  116. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_failure.py +0 -0
  117. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_openai.py +0 -0
  118. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_prompt.py +0 -0
  119. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_provider_fixtures.py +0 -0
  120. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_replay.py +0 -0
  121. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_tool_loop.py +0 -0
  122. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_trace.py +0 -0
  123. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_trace_snapshot.py +0 -0
  124. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_llm_types.py +0 -0
  125. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_migration.py +0 -0
  126. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_observability_logging.py +0 -0
  127. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_observability_metrics.py +0 -0
  128. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_operate_example.py +0 -0
  129. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_pack_scaffold.py +0 -0
  130. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_packs.py +0 -0
  131. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_patch.py +0 -0
  132. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_pattern_matcher.py +0 -0
  133. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_pattern_parser.py +0 -0
  134. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_pattern_subscriptions.py +0 -0
  135. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_persistence.py +0 -0
  136. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_postgres_store.py +0 -0
  137. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_quickstart.py +0 -0
  138. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_quickstart_snapshot.py +0 -0
  139. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_replay.py +0 -0
  140. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_replay_trace_snapshot.py +0 -0
  141. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_requeue_unfired.py +0 -0
  142. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_resume_example.py +0 -0
  143. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_runtime.py +0 -0
  144. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_runtime_status.py +0 -0
  145. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_serde.py +0 -0
  146. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_store_conformance.py +0 -0
  147. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_store_url.py +0 -0
  148. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_tool_replay.py +0 -0
  149. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_tool_trace_snapshot.py +0 -0
  150. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_tools.py +0 -0
  151. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_trace.py +0 -0
  152. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_tutorial_snippets.py +0 -0
  153. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_v1_0_1_patches.py +0 -0
  154. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_version_sync.py +0 -0
  155. {activegraph-1.0.1 → activegraph-1.0.2.post1}/tests/test_view.py +0 -0
  156. {activegraph-1.0.1 → activegraph-1.0.2.post1}/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.1
3
+ Version: 1.0.2.post1
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
@@ -221,4 +221,4 @@ __all__ = [
221
221
  "tool",
222
222
  ]
223
223
 
224
- __version__ = "1.0.1"
224
+ __version__ = "1.0.2.post1"
@@ -93,7 +93,11 @@ class LLMBehavior(Behavior):
93
93
 
94
94
  handler: Optional[Callable[..., None]] = None
95
95
  description: str = ""
96
- model: str = "claude-sonnet-4-5"
96
+ # v1.0.2 #1: model is optional. None = "use the configured provider's
97
+ # default_model"; the runtime resolves and stamps the concrete string
98
+ # at registration time. Existing call sites that pass an explicit
99
+ # string keep working unchanged.
100
+ model: Optional[str] = None
97
101
  output_schema: Optional[type] = None
98
102
  deterministic: bool = False
99
103
  max_tokens: int = 4096
@@ -134,10 +138,17 @@ class LLMBehavior(Behavior):
134
138
  if ar_expr:
135
139
  around_id = _resolve_event_path(ar_expr, event)
136
140
  depth = self.view_spec.get("depth")
141
+ # v1.0.2 #1: if model is still None, the runtime hasn't stamped
142
+ # a provider default yet — `build_prompt` is for inspection
143
+ # (CONTRACT v0.6 #20) and must work without a Runtime, so fall
144
+ # back to the v1.0.1 hardcoded default for inspection-time hash
145
+ # stability. Real provider calls always go through Runtime,
146
+ # which resolves the configured provider's default_model first.
147
+ inspection_model = self.model or "claude-sonnet-4-5"
137
148
  return assemble_prompt(
138
149
  behavior_name=self.name,
139
150
  description=self.description,
140
- model=self.model,
151
+ model=inspection_model,
141
152
  output_schema=self.output_schema,
142
153
  creates=self.creates,
143
154
  view=view,
@@ -85,6 +85,12 @@ def register(behavior_obj: Union[Behavior, RelationBehavior]) -> None:
85
85
  f"decorators to construct one."
86
86
  )
87
87
  _REGISTRY.append(behavior_obj)
88
+ # v1.0.2.post1 #1 (b): if any Runtime is already alive, validate the
89
+ # new behavior against each one's configured provider so cross-
90
+ # provider model mismatches fire at the @llm_behavior / register()
91
+ # line rather than at first run_goal.
92
+ from activegraph.runtime._live import validate_behavior_against_live_runtimes
93
+ validate_behavior_against_live_runtimes(behavior_obj)
88
94
 
89
95
 
90
96
  def behavior(
@@ -147,7 +153,7 @@ def llm_behavior(
147
153
  on: Optional[list[str]] = None,
148
154
  where: Optional[dict[str, Any]] = None,
149
155
  description: str = "",
150
- model: str = "claude-sonnet-4-5",
156
+ model: Optional[str] = None,
151
157
  output_schema: Optional[type] = None,
152
158
  view: Optional[dict[str, Any]] = None,
153
159
  creates: Optional[list[str]] = None,
@@ -177,6 +183,19 @@ def llm_behavior(
177
183
  Keyword-only on purpose — `@llm_behavior` carries enough
178
184
  parameters that positional binding would be a footgun.
179
185
 
186
+ ``model=`` is optional (v1.0.2 #1). Omitted, the runtime resolves
187
+ it to the configured provider's ``default_model`` at registration
188
+ time — ``"claude-sonnet-4-5"`` for ``AnthropicProvider``,
189
+ ``"gpt-4o-mini"`` for ``OpenAIProvider``. Passing an explicit
190
+ model string still works byte-identically; the runtime additionally
191
+ validates the name against the configured provider's
192
+ ``recognizes_model()`` and raises
193
+ :class:`InvalidRuntimeConfiguration` at registration time if the
194
+ name belongs to a different shipped provider's family (e.g.
195
+ ``model="gpt-4o-mini"`` on a runtime configured with
196
+ ``AnthropicProvider``). Names no shipped provider recognizes
197
+ (custom or fine-tuned models) pass through silently.
198
+
180
199
  `prompt_template=` is the only escape hatch from the
181
200
  runtime-assembled prompt. It is a `str.format`-style template that
182
201
  receives four placeholders:
@@ -235,6 +254,12 @@ def llm_behavior(
235
254
  max_tool_turns=max_tool_turns,
236
255
  )
237
256
  _REGISTRY.append(b)
257
+ # v1.0.2.post1 #1 (b): validate against any live Runtime's
258
+ # provider. Mirrors the eager check in register() so decorated
259
+ # behaviors with cross-provider model names fire at the
260
+ # @llm_behavior line instead of at first run_goal.
261
+ from activegraph.runtime._live import validate_behavior_against_live_runtimes
262
+ validate_behavior_against_live_runtimes(b)
238
263
  return b
239
264
 
240
265
  return wrap
@@ -62,6 +62,10 @@ def _pricing_for(model: str, pricing: Mapping[str, Mapping[str, str]]) -> tuple[
62
62
 
63
63
 
64
64
  class AnthropicProvider(LLMProvider):
65
+ # v1.0.2 #1: provider-aware default model. @llm_behavior(model=None)
66
+ # resolves to this string at registration time.
67
+ default_model: str = "claude-sonnet-4-5"
68
+
65
69
  def __init__(
66
70
  self,
67
71
  *,
@@ -205,6 +209,10 @@ class AnthropicProvider(LLMProvider):
205
209
  result = client.messages.count_tokens(**kwargs)
206
210
  return int(getattr(result, "input_tokens", 0) or 0)
207
211
 
212
+ def recognizes_model(self, name: str) -> bool:
213
+ """True for the ``claude-*`` model family. v1.0.2 #1."""
214
+ return name.startswith("claude-")
215
+
208
216
 
209
217
  # ---- helpers ---------------------------------------------------------------
210
218
 
@@ -82,6 +82,20 @@ def _pricing_for(
82
82
 
83
83
 
84
84
  class OpenAIProvider(LLMProvider):
85
+ # v1.0.2 #1: provider-aware default model. @llm_behavior(model=None)
86
+ # resolves to this string at registration time. gpt-4o-mini is the
87
+ # cheap, fast member of the GPT-4o family — matches Anthropic's
88
+ # default-model shape (cheapest sensible default; override for prod).
89
+ default_model: str = "gpt-4o-mini"
90
+
91
+ # v1.0.2 #1: the model-name prefixes this provider recognizes.
92
+ # `gpt-` covers the GPT-3.5 / GPT-4 / GPT-4o families; the
93
+ # `o1-` / `o3-` / `o4-` prefixes cover the reasoning-model line
94
+ # (o1, o3, o4 and their dated variants). Names matching none of
95
+ # these pass through registration silently per v1.0.2 #1 (b)'s
96
+ # permissive-default rule.
97
+ _RECOGNIZED_PREFIXES: tuple[str, ...] = ("gpt-", "o1-", "o3-", "o4-")
98
+
85
99
  def __init__(
86
100
  self,
87
101
  *,
@@ -267,6 +281,12 @@ class OpenAIProvider(LLMProvider):
267
281
  total += 4
268
282
  return max(1, total)
269
283
 
284
+ def recognizes_model(self, name: str) -> bool:
285
+ """True for OpenAI model families (``gpt-*``, ``o1-*``, ``o3-*``,
286
+ ``o4-*``). v1.0.2 #1.
287
+ """
288
+ return any(name.startswith(p) for p in self._RECOGNIZED_PREFIXES)
289
+
270
290
  def _heuristic_count(self, system: str, messages: list[LLMMessage]) -> int:
271
291
  if not self._heuristic_warned:
272
292
  _log.debug(
@@ -1,11 +1,13 @@
1
1
  """The `LLMProvider` Protocol every provider implements.
2
2
 
3
- CONTRACT v0.6 #3, extended in v0.7. Narrow, explicit, keyword-only.
4
- Shipped reference implementation is `AnthropicProvider`. Tests use
3
+ CONTRACT v0.6 #3, extended in v0.7, additively widened in v1.0.2 #1.
4
+ Narrow, explicit, keyword-only. Shipped reference implementations are
5
+ `AnthropicProvider` and `OpenAIProvider`. Tests use
5
6
  `RecordedLLMProvider` + `RecordingLLMProvider`. The demo ships its
6
7
  own scripted provider.
7
8
 
8
- A provider does three things:
9
+ A provider does three things plus two declarations:
10
+
9
11
  * `complete()`: run a single non-streaming completion. v0.7 adds
10
12
  an optional `tools=` parameter; when non-empty, the model is
11
13
  allowed to return tool_use blocks in the response.
@@ -14,6 +16,18 @@ A provider does three things:
14
16
  prompt that's about to be sent. Used for pre-call budget gating
15
17
  when `budget.max_cost_usd` is set; otherwise skipped (see
16
18
  CONTRACT v0.6 #4 / decision 10).
19
+ * `default_model`: the model name to use when an `@llm_behavior`
20
+ didn't pin one (v1.0.2 #1).
21
+ * `recognizes_model(name)`: True when `name` belongs to a model
22
+ family this provider serves. Used by the runtime at
23
+ registration time to flag cross-provider mismatches before the
24
+ first network call (v1.0.2 #1).
25
+
26
+ `default_model` and `recognizes_model` are additive: the runtime
27
+ guards their use with `getattr(...)`, so custom providers that
28
+ pre-date v1.0.2 keep working — they just require an explicit
29
+ `model=` on every `@llm_behavior` and don't participate in
30
+ cross-provider validation.
17
31
 
18
32
  No streaming, no multi-model orchestration — those are deferred to
19
33
  v0.8+. Tool use IS in v0.7, but the loop is orchestrated by the
@@ -32,6 +46,11 @@ from activegraph.llm.types import LLMMessage, LLMResponse
32
46
 
33
47
  @runtime_checkable
34
48
  class LLMProvider(Protocol):
49
+ # v1.0.2 #1: declared as an attribute on the Protocol. Concrete
50
+ # providers set it as a class attribute; custom providers may
51
+ # omit it (the runtime falls back to getattr-with-default).
52
+ default_model: str
53
+
35
54
  def complete(
36
55
  self,
37
56
  *,
@@ -64,3 +83,15 @@ class LLMProvider(Protocol):
64
83
  model: str,
65
84
  ) -> int:
66
85
  ...
86
+
87
+ def recognizes_model(self, name: str) -> bool:
88
+ """True when `name` belongs to a model family this provider serves.
89
+
90
+ v1.0.2 #1. Used at registration time to flag cross-provider
91
+ mismatches (e.g. ``model="claude-sonnet-4-5"`` on a runtime
92
+ configured with ``OpenAIProvider``). Permissive by default:
93
+ unknown names — fine-tuned models, internal deployment names,
94
+ experimental prefixes — should return False so the runtime
95
+ passes them through without a diagnostic.
96
+ """
97
+ ...
@@ -110,9 +110,21 @@ class RecordedLLMProvider(LLMProvider):
110
110
  silent fallthrough to a real call.
111
111
  """
112
112
 
113
+ # v1.0.2 #1: fixtures already carry their own model name in the hash
114
+ # input, so default-model resolution only matters when an
115
+ # @llm_behavior omits model=. The recorded provider has no opinion
116
+ # about which family it's replaying; declare the historical default
117
+ # so existing fixtures stay reachable.
118
+ default_model: str = "claude-sonnet-4-5"
119
+
113
120
  def __init__(self, fixtures_dir: str) -> None:
114
121
  self._dir = fixtures_dir
115
122
 
123
+ def recognizes_model(self, name: str) -> bool:
124
+ # Fixture-backed: claim every name so cross-provider validation
125
+ # doesn't fire against recorded responses.
126
+ return True
127
+
116
128
  # ---- LLMProvider methods ----
117
129
 
118
130
  def complete(
@@ -207,6 +219,20 @@ class RecordingLLMProvider(LLMProvider):
207
219
  self._dir = fixtures_dir
208
220
  os.makedirs(self._dir, exist_ok=True)
209
221
 
222
+ # v1.0.2 #1: delegate default_model + recognizes_model to the inner
223
+ # provider so wrapping doesn't change the resolution surface. Falls
224
+ # back to the historical default if the inner provider pre-dates
225
+ # v1.0.2 and doesn't declare one.
226
+ @property
227
+ def default_model(self) -> str:
228
+ return getattr(self._inner, "default_model", "claude-sonnet-4-5")
229
+
230
+ def recognizes_model(self, name: str) -> bool:
231
+ recognizes = getattr(self._inner, "recognizes_model", None)
232
+ if recognizes is None:
233
+ return True
234
+ return bool(recognizes(name))
235
+
210
236
  def complete(
211
237
  self,
212
238
  *,
@@ -0,0 +1,173 @@
1
+ """Live-Runtime tracking + single-behavior cross-provider validation.
2
+
3
+ CONTRACT v1.0.2 #1 (b). The validation fires at *both* binding
4
+ moments: ``Runtime(graph, llm_provider=...)`` construction (against
5
+ the existing registry) and ``register()`` / ``@llm_behavior``
6
+ decoration (against any live Runtime via the WeakSet below).
7
+
8
+ The WeakSet is module-level so the registration decorators can
9
+ look it up without importing ``Runtime`` at module load. Runtimes
10
+ self-register inside ``__init__`` after wiring; the WeakSet
11
+ auto-cleans GC'd Runtimes — no explicit ``Runtime.close()`` is
12
+ required.
13
+
14
+ The single-behavior validator here is intentionally narrow: it
15
+ only does the cross-provider mismatch check (recognized name
16
+ belonging to a different shipped provider). It does *not* stamp
17
+ provider defaults onto ``model=None`` behaviors — that side
18
+ effect lives in ``_resolve_and_validate_llm_models`` in
19
+ ``runtime.py``, which still runs at first ``_ensure_registry``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import weakref
25
+ from typing import TYPE_CHECKING, Any
26
+
27
+ from activegraph.behaviors.base import LLMBehavior
28
+
29
+ if TYPE_CHECKING:
30
+ from activegraph.runtime.runtime import Runtime
31
+
32
+
33
+ # WeakSet so abandoned Runtimes don't leak validation calls. The
34
+ # fork/replay paths construct fresh Runtimes that self-register; the
35
+ # parent stays in the set until GC'd.
36
+ _LIVE_RUNTIMES: "weakref.WeakSet[Runtime]" = weakref.WeakSet()
37
+
38
+
39
+ def track_runtime(rt: "Runtime") -> None:
40
+ """Register a live Runtime for cross-provider validation. Called
41
+ by ``Runtime.__init__`` after the provider and graph are wired."""
42
+ _LIVE_RUNTIMES.add(rt)
43
+
44
+
45
+ def live_runtimes() -> list["Runtime"]:
46
+ """Snapshot of currently-alive Runtimes (a list of strong refs that
47
+ the caller releases at end-of-call). Used by ``register()`` and the
48
+ decorators to validate a new behavior against each live provider."""
49
+ return list(_LIVE_RUNTIMES)
50
+
51
+
52
+ def _clear_for_test() -> None:
53
+ """Empty the live-Runtime set. Test-only — used by the conftest
54
+ fixture that isolates the global registry between tests. Production
55
+ code shouldn't call this: the WeakSet cleans itself on GC."""
56
+ _LIVE_RUNTIMES.clear()
57
+
58
+
59
+ def validate_behavior_against_live_runtimes(behavior: Any) -> None:
60
+ """Validate a freshly-registered behavior against every live
61
+ Runtime's provider. Raises :class:`InvalidRuntimeConfiguration` on
62
+ the first cross-provider mismatch.
63
+
64
+ No-op when ``behavior`` isn't an :class:`LLMBehavior`, when no
65
+ Runtime is live, or when the behavior has no explicit model (model
66
+ resolution against the provider's default happens at Runtime
67
+ construction or first-run, not here).
68
+ """
69
+ if not isinstance(behavior, LLMBehavior):
70
+ return
71
+ if behavior.model is None:
72
+ return
73
+ for rt in live_runtimes():
74
+ provider = getattr(rt, "llm_provider", None)
75
+ if provider is None:
76
+ continue
77
+ _validate_one(behavior, provider)
78
+
79
+
80
+ def _validate_one(behavior: LLMBehavior, provider: Any) -> None:
81
+ """Cross-provider mismatch check for a single behavior against a
82
+ single provider. Pure: no side effects, no model-default stamping.
83
+
84
+ Permissive by default per v1.0.2 #1 (b): names no shipped provider
85
+ recognizes pass silently. Only recognized cross-provider mismatches
86
+ raise. The runtime-side ``_resolve_and_validate_llm_models``
87
+ delegates to this for its per-behavior pass so the check lives in
88
+ one place.
89
+ """
90
+ from activegraph.runtime.config_errors import InvalidRuntimeConfiguration
91
+
92
+ model = behavior.model
93
+ if model is None:
94
+ return
95
+ recognizes = getattr(provider, "recognizes_model", None)
96
+ if recognizes is None or recognizes(model):
97
+ return
98
+ claimed_by = _which_shipped_provider_claims(model, exclude=type(provider))
99
+ if claimed_by is None:
100
+ return
101
+
102
+ provider_class = type(provider).__name__
103
+ provider_default = getattr(provider, "default_model", None) or "claude-sonnet-4-5"
104
+ claimed_by_default = getattr(claimed_by, "default_model", "")
105
+ provider_default_hint = (
106
+ f"or remove the model= argument to use {provider_class}'s "
107
+ f"default ({provider_default!r})"
108
+ if provider_default
109
+ else f"or set a {provider_class}-compatible model name"
110
+ )
111
+ raise InvalidRuntimeConfiguration(
112
+ (
113
+ f"@llm_behavior(name={behavior.name!r}, model={model!r}) "
114
+ f"names a {claimed_by.__name__}-family model, but the "
115
+ f"runtime is configured with {provider_class}"
116
+ ),
117
+ what_failed=(
118
+ f"The behavior {behavior.name!r} pinned model={model!r}. "
119
+ f"That name belongs to {claimed_by.__name__}'s model "
120
+ f"family, but this Runtime was constructed with a "
121
+ f"{provider_class} instance. Sending the name to the "
122
+ f"wrong provider produces an HTTP 404 (or equivalent "
123
+ f"'unknown model' response) at first LLM call, with no "
124
+ f"hint that the mismatch is the cause."
125
+ ),
126
+ why=(
127
+ "v1.0.2 #1 validates explicit model names at both binding "
128
+ "moments (Runtime construction and register()/decoration) "
129
+ "against each shipped provider's recognizes_model() "
130
+ "method. The configured provider doesn't claim this name, "
131
+ "but another shipped provider does — that's a "
132
+ "configuration mismatch worth surfacing before the first "
133
+ "network call rather than after."
134
+ ),
135
+ how_to_fix=(
136
+ f"Either swap the provider — Runtime(graph, "
137
+ f"llm_provider={claimed_by.__name__}()) — "
138
+ f"{provider_default_hint}, or pass an explicit name "
139
+ f"from {provider_class}'s model families."
140
+ ),
141
+ context={
142
+ "behavior": behavior.name,
143
+ "model": model,
144
+ "configured_provider": provider_class,
145
+ "claimed_by_provider": claimed_by.__name__,
146
+ "claimed_by_default_model": claimed_by_default,
147
+ },
148
+ )
149
+
150
+
151
+ def _which_shipped_provider_claims(name: str, *, exclude: type) -> Any:
152
+ """Return the first shipped provider class that recognizes `name`,
153
+ excluding `exclude`. Returns None when no other shipped provider
154
+ claims the name (permissive default per v1.0.2 #1 (b))."""
155
+ from activegraph.llm.anthropic import AnthropicProvider
156
+ from activegraph.llm.openai import OpenAIProvider
157
+
158
+ candidates = [AnthropicProvider, OpenAIProvider]
159
+ for cls in candidates:
160
+ if cls is exclude:
161
+ continue
162
+ recognizes = getattr(cls, "recognizes_model", None)
163
+ if recognizes is None:
164
+ continue
165
+ try:
166
+ inst = cls()
167
+ except Exception:
168
+ # Defensive: if a provider's no-arg constructor ever requires
169
+ # real credentials, skip it for the validation lookup.
170
+ continue
171
+ if inst.recognizes_model(name):
172
+ return cls
173
+ return None
@@ -303,6 +303,23 @@ class Runtime:
303
303
  self._pack_behaviors: list = []
304
304
  self._pack_tools: list = []
305
305
 
306
+ # ---- v1.0.2.post1 #1 (b): eager cross-provider validation ----
307
+ # First run the bulk validation pass against whatever's already
308
+ # in the registry; only if it passes do we add this Runtime to
309
+ # the live-set. Failed constructions stay out of the WeakSet so
310
+ # they can't influence subsequent register() / @llm_behavior
311
+ # validation calls. The lazy path inside _ensure_registry()
312
+ # stays as a defensive double-check.
313
+ if self.llm_provider is not None:
314
+ source = (
315
+ self._explicit_behaviors
316
+ if self._explicit_behaviors is not None
317
+ else get_registry()
318
+ )
319
+ _resolve_and_validate_llm_models(source, self.llm_provider)
320
+ from activegraph.runtime._live import track_runtime
321
+ track_runtime(self)
322
+
306
323
  # ---------- public surface ----------
307
324
 
308
325
  @property
@@ -376,6 +393,11 @@ class Runtime:
376
393
  for b in source:
377
394
  if isinstance(b, LLMBehavior):
378
395
  raise MissingProviderError(behavior_name=b.name)
396
+ else:
397
+ # CONTRACT v1.0.2 #1: resolve the provider's default model for
398
+ # behaviors that didn't pin one, and validate explicit model
399
+ # names against cross-provider mismatches.
400
+ _resolve_and_validate_llm_models(source, self.llm_provider)
379
401
  self.registry = Registry(source)
380
402
 
381
403
  # v0.7: assemble the tool registry. Explicit tools= override the
@@ -2180,6 +2202,38 @@ _BUDGET_REASON_MAP = {
2180
2202
  }
2181
2203
 
2182
2204
 
2205
+ def _resolve_and_validate_llm_models(source, provider) -> None:
2206
+ """Stamp provider defaults onto LLMBehaviors with model=None, and
2207
+ raise InvalidRuntimeConfiguration for explicit names that belong to
2208
+ a different shipped provider. CONTRACT v1.0.2 #1.
2209
+
2210
+ The cross-provider mismatch check delegates to
2211
+ :func:`activegraph.runtime._live._validate_one` so the same check
2212
+ fires from both binding moments (Runtime construction here,
2213
+ register()/decoration in ``_live.validate_behavior_against_live_runtimes``).
2214
+ """
2215
+ from activegraph.runtime._live import _validate_one
2216
+
2217
+ # Custom providers that pre-date v1.0.2 don't declare `default_model`.
2218
+ # Fall back to the v1.0.1 hardcoded default — keeps every pre-v1.0.2
2219
+ # call site (which silently inherited "claude-sonnet-4-5") working
2220
+ # byte-identically without code changes. The runtime only swaps in a
2221
+ # different default when the provider opts in by declaring one.
2222
+ provider_default = getattr(provider, "default_model", None) or "claude-sonnet-4-5"
2223
+
2224
+ for b in source:
2225
+ if not isinstance(b, LLMBehavior):
2226
+ continue
2227
+ if b.model is None:
2228
+ # v1.0.2 #1 (a): resolve to the provider's default_model
2229
+ # (or the v1.0.1 fallback for custom providers that don't
2230
+ # declare one).
2231
+ b.model = provider_default
2232
+ continue
2233
+ # v1.0.2 #1 (b): cross-provider mismatch check.
2234
+ _validate_one(b, provider)
2235
+
2236
+
2183
2237
  def _budget_reason(name: Optional[str]) -> str:
2184
2238
  if name is None:
2185
2239
  return "budget.exhausted"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.0.1
3
+ Version: 1.0.2.post1
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
@@ -55,6 +55,7 @@ activegraph/packs/diligence/prompts/memo_synthesizer.md
55
55
  activegraph/packs/diligence/prompts/question_generator.md
56
56
  activegraph/packs/diligence/prompts/risk_identifier.md
57
57
  activegraph/runtime/__init__.py
58
+ activegraph/runtime/_live.py
58
59
  activegraph/runtime/behavior_graph.py
59
60
  activegraph/runtime/budget.py
60
61
  activegraph/runtime/config_errors.py
@@ -108,6 +109,7 @@ tests/test_llm_behavior.py
108
109
  tests/test_llm_budget.py
109
110
  tests/test_llm_causal.py
110
111
  tests/test_llm_claim_extraction.py
112
+ tests/test_llm_default_model.py
111
113
  tests/test_llm_determinism.py
112
114
  tests/test_llm_failure.py
113
115
  tests/test_llm_openai.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "activegraph"
7
- version = "1.0.1"
7
+ version = "1.0.2.post1"
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"