activegraph 1.0.1__tar.gz → 1.0.2__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 (155) hide show
  1. {activegraph-1.0.1 → activegraph-1.0.2}/PKG-INFO +1 -1
  2. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/__init__.py +1 -1
  3. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/behaviors/base.py +13 -2
  4. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/behaviors/decorators.py +14 -1
  5. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/anthropic.py +8 -0
  6. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/openai.py +20 -0
  7. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/provider.py +34 -3
  8. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/recorded.py +26 -0
  9. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/runtime.py +120 -0
  10. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph.egg-info/PKG-INFO +1 -1
  11. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph.egg-info/SOURCES.txt +1 -0
  12. {activegraph-1.0.1 → activegraph-1.0.2}/pyproject.toml +1 -1
  13. activegraph-1.0.2/tests/test_llm_default_model.py +328 -0
  14. {activegraph-1.0.1 → activegraph-1.0.2}/README.md +0 -0
  15. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/__main__.py +0 -0
  16. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/behaviors/__init__.py +0 -0
  17. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/cli/__init__.py +0 -0
  18. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/cli/main.py +0 -0
  19. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/cli/quickstart.py +0 -0
  20. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/__init__.py +0 -0
  21. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/clock.py +0 -0
  22. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/event.py +0 -0
  23. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/graph.py +0 -0
  24. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/ids.py +0 -0
  25. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/patch.py +0 -0
  26. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/core/view.py +0 -0
  27. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/errors.py +0 -0
  28. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/frame.py +0 -0
  29. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/__init__.py +0 -0
  30. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/cache.py +0 -0
  31. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/errors.py +0 -0
  32. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/parsing.py +0 -0
  33. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/prompt.py +0 -0
  34. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/llm/types.py +0 -0
  35. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/observability/__init__.py +0 -0
  36. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/observability/logging.py +0 -0
  37. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/observability/metrics.py +0 -0
  38. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/observability/migration.py +0 -0
  39. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/observability/prometheus.py +0 -0
  40. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/observability/status.py +0 -0
  41. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/__init__.py +0 -0
  42. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/__init__.py +0 -0
  43. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/behaviors.py +0 -0
  44. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  45. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  46. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/object_types.py +0 -0
  47. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  48. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  49. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  50. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  51. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/settings.py +0 -0
  52. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/diligence/tools.py +0 -0
  53. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/loader.py +0 -0
  54. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/packs/scaffold.py +0 -0
  55. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/policy.py +0 -0
  56. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/__init__.py +0 -0
  57. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/behavior_graph.py +0 -0
  58. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/budget.py +0 -0
  59. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/config_errors.py +0 -0
  60. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/diff.py +0 -0
  61. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/errors.py +0 -0
  62. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/exec_errors.py +0 -0
  63. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/patterns.py +0 -0
  64. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/queue.py +0 -0
  65. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/registration_errors.py +0 -0
  66. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/registry.py +0 -0
  67. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/scheduler.py +0 -0
  68. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/runtime/view_builder.py +0 -0
  69. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/__init__.py +0 -0
  70. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/base.py +0 -0
  71. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/conformance.py +0 -0
  72. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/errors.py +0 -0
  73. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/memory.py +0 -0
  74. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/postgres.py +0 -0
  75. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/serde.py +0 -0
  76. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/sqlite.py +0 -0
  77. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/store/url.py +0 -0
  78. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/__init__.py +0 -0
  79. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/base.py +0 -0
  80. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/cache.py +0 -0
  81. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/context.py +0 -0
  82. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/decorators.py +0 -0
  83. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/errors.py +0 -0
  84. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/graph_query.py +0 -0
  85. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/recorded.py +0 -0
  86. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/tools/web_fetch.py +0 -0
  87. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/trace/__init__.py +0 -0
  88. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/trace/causal.py +0 -0
  89. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph/trace/printer.py +0 -0
  90. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph.egg-info/dependency_links.txt +0 -0
  91. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph.egg-info/entry_points.txt +0 -0
  92. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph.egg-info/requires.txt +0 -0
  93. {activegraph-1.0.1 → activegraph-1.0.2}/activegraph.egg-info/top_level.txt +0 -0
  94. {activegraph-1.0.1 → activegraph-1.0.2}/setup.cfg +0 -0
  95. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_activate_after.py +0 -0
  96. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_causal_cross_tool.py +0 -0
  97. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_cli.py +0 -0
  98. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_clock.py +0 -0
  99. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_diff.py +0 -0
  100. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_diligence_pack.py +0 -0
  101. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_diligence_with_tools.py +0 -0
  102. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_doc_links.py +0 -0
  103. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_doc_site_reachable.py +0 -0
  104. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_errors_format.py +0 -0
  105. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_event.py +0 -0
  106. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_fork.py +0 -0
  107. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_graph.py +0 -0
  108. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_ids.py +0 -0
  109. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_anthropic.py +0 -0
  110. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_behavior.py +0 -0
  111. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_budget.py +0 -0
  112. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_causal.py +0 -0
  113. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_claim_extraction.py +0 -0
  114. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_determinism.py +0 -0
  115. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_failure.py +0 -0
  116. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_openai.py +0 -0
  117. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_prompt.py +0 -0
  118. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_provider_fixtures.py +0 -0
  119. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_replay.py +0 -0
  120. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_tool_loop.py +0 -0
  121. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_trace.py +0 -0
  122. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_trace_snapshot.py +0 -0
  123. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_llm_types.py +0 -0
  124. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_migration.py +0 -0
  125. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_observability_logging.py +0 -0
  126. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_observability_metrics.py +0 -0
  127. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_operate_example.py +0 -0
  128. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_pack_scaffold.py +0 -0
  129. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_packs.py +0 -0
  130. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_patch.py +0 -0
  131. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_pattern_matcher.py +0 -0
  132. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_pattern_parser.py +0 -0
  133. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_pattern_subscriptions.py +0 -0
  134. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_persistence.py +0 -0
  135. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_postgres_store.py +0 -0
  136. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_quickstart.py +0 -0
  137. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_quickstart_snapshot.py +0 -0
  138. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_replay.py +0 -0
  139. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_replay_trace_snapshot.py +0 -0
  140. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_requeue_unfired.py +0 -0
  141. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_resume_example.py +0 -0
  142. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_runtime.py +0 -0
  143. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_runtime_status.py +0 -0
  144. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_serde.py +0 -0
  145. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_store_conformance.py +0 -0
  146. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_store_url.py +0 -0
  147. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_tool_replay.py +0 -0
  148. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_tool_trace_snapshot.py +0 -0
  149. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_tools.py +0 -0
  150. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_trace.py +0 -0
  151. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_tutorial_snippets.py +0 -0
  152. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_v1_0_1_patches.py +0 -0
  153. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_version_sync.py +0 -0
  154. {activegraph-1.0.1 → activegraph-1.0.2}/tests/test_view.py +0 -0
  155. {activegraph-1.0.1 → activegraph-1.0.2}/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
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"
@@ -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,
@@ -147,7 +147,7 @@ def llm_behavior(
147
147
  on: Optional[list[str]] = None,
148
148
  where: Optional[dict[str, Any]] = None,
149
149
  description: str = "",
150
- model: str = "claude-sonnet-4-5",
150
+ model: Optional[str] = None,
151
151
  output_schema: Optional[type] = None,
152
152
  view: Optional[dict[str, Any]] = None,
153
153
  creates: Optional[list[str]] = None,
@@ -177,6 +177,19 @@ def llm_behavior(
177
177
  Keyword-only on purpose — `@llm_behavior` carries enough
178
178
  parameters that positional binding would be a footgun.
179
179
 
180
+ ``model=`` is optional (v1.0.2 #1). Omitted, the runtime resolves
181
+ it to the configured provider's ``default_model`` at registration
182
+ time — ``"claude-sonnet-4-5"`` for ``AnthropicProvider``,
183
+ ``"gpt-4o-mini"`` for ``OpenAIProvider``. Passing an explicit
184
+ model string still works byte-identically; the runtime additionally
185
+ validates the name against the configured provider's
186
+ ``recognizes_model()`` and raises
187
+ :class:`InvalidRuntimeConfiguration` at registration time if the
188
+ name belongs to a different shipped provider's family (e.g.
189
+ ``model="gpt-4o-mini"`` on a runtime configured with
190
+ ``AnthropicProvider``). Names no shipped provider recognizes
191
+ (custom or fine-tuned models) pass through silently.
192
+
180
193
  `prompt_template=` is the only escape hatch from the
181
194
  runtime-assembled prompt. It is a `str.format`-style template that
182
195
  receives four placeholders:
@@ -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
  *,
@@ -376,6 +376,11 @@ class Runtime:
376
376
  for b in source:
377
377
  if isinstance(b, LLMBehavior):
378
378
  raise MissingProviderError(behavior_name=b.name)
379
+ else:
380
+ # CONTRACT v1.0.2 #1: resolve the provider's default model for
381
+ # behaviors that didn't pin one, and validate explicit model
382
+ # names against cross-provider mismatches.
383
+ _resolve_and_validate_llm_models(source, self.llm_provider)
379
384
  self.registry = Registry(source)
380
385
 
381
386
  # v0.7: assemble the tool registry. Explicit tools= override the
@@ -2180,6 +2185,121 @@ _BUDGET_REASON_MAP = {
2180
2185
  }
2181
2186
 
2182
2187
 
2188
+ def _resolve_and_validate_llm_models(source, provider) -> None:
2189
+ """Stamp provider defaults onto LLMBehaviors with model=None, and
2190
+ raise InvalidRuntimeConfiguration for explicit names that belong to
2191
+ a different shipped provider. CONTRACT v1.0.2 #1.
2192
+
2193
+ The validation is permissive: a name no shipped provider recognizes
2194
+ (custom or fine-tuned model) passes silently — only *recognized*
2195
+ cross-provider mismatches fire the diagnostic.
2196
+ """
2197
+ from activegraph.runtime.config_errors import InvalidRuntimeConfiguration
2198
+
2199
+ # Custom providers that pre-date v1.0.2 don't declare `default_model`.
2200
+ # Fall back to the v1.0.1 hardcoded default — keeps every pre-v1.0.2
2201
+ # call site (which silently inherited "claude-sonnet-4-5") working
2202
+ # byte-identically without code changes. The runtime only swaps in a
2203
+ # different default when the provider opts in by declaring one.
2204
+ provider_default = getattr(provider, "default_model", None) or "claude-sonnet-4-5"
2205
+ provider_recognizes = getattr(provider, "recognizes_model", None)
2206
+ provider_class = type(provider).__name__
2207
+
2208
+ for b in source:
2209
+ if not isinstance(b, LLMBehavior):
2210
+ continue
2211
+ if b.model is None:
2212
+ # v1.0.2 #1 (a): resolve to the provider's default_model
2213
+ # (or the v1.0.1 fallback for custom providers that don't
2214
+ # declare one).
2215
+ b.model = provider_default
2216
+ continue
2217
+
2218
+ # b.model is an explicit string. Cross-provider validation per
2219
+ # v1.0.2 #1 (b): if the configured provider doesn't recognize the
2220
+ # name, ask each *other* shipped provider class whether it does.
2221
+ # Exactly one other-provider claim ⇒ raise; otherwise pass through.
2222
+ if provider_recognizes is None or provider_recognizes(b.model):
2223
+ continue
2224
+ claimed_by = _which_shipped_provider_claims(b.model, exclude=type(provider))
2225
+ if claimed_by is None:
2226
+ continue
2227
+ # The model name belongs to a different shipped provider's family.
2228
+ # This is the v1.0.1-user-test finding the v1.0.2 patch addresses:
2229
+ # silently sending an Anthropic-family name to OpenAI 404s with no
2230
+ # helpful diagnostic. Fire at registration time, not call time.
2231
+ claimed_by_default = getattr(claimed_by, "default_model", "")
2232
+ provider_default_hint = (
2233
+ f"or remove the model= argument to use {provider_class}'s "
2234
+ f"default ({provider_default!r})"
2235
+ if provider_default
2236
+ else f"or set a {provider_class}-compatible model name"
2237
+ )
2238
+ raise InvalidRuntimeConfiguration(
2239
+ (
2240
+ f"@llm_behavior(name={b.name!r}, model={b.model!r}) "
2241
+ f"names a {claimed_by.__name__}-family model, but the "
2242
+ f"runtime is configured with {provider_class}"
2243
+ ),
2244
+ what_failed=(
2245
+ f"The behavior {b.name!r} pinned model={b.model!r}. That "
2246
+ f"name belongs to {claimed_by.__name__}'s model family, "
2247
+ f"but this Runtime was constructed with a "
2248
+ f"{provider_class} instance. Sending the name to the "
2249
+ f"wrong provider produces an HTTP 404 (or equivalent "
2250
+ f"'unknown model' response) at first LLM call, with no "
2251
+ f"hint that the mismatch is the cause."
2252
+ ),
2253
+ why=(
2254
+ "v1.0.2 #1 validates explicit model names at registration "
2255
+ "time against each shipped provider's recognizes_model() "
2256
+ "method. The configured provider doesn't claim this "
2257
+ "name, but another shipped provider does — that's a "
2258
+ "configuration mismatch worth surfacing before the "
2259
+ "first network call rather than after."
2260
+ ),
2261
+ how_to_fix=(
2262
+ f"Either swap the provider — Runtime(graph, "
2263
+ f"llm_provider={claimed_by.__name__}()) — "
2264
+ f"{provider_default_hint}, or pass an explicit name "
2265
+ f"from {provider_class}'s model families."
2266
+ ),
2267
+ context={
2268
+ "behavior": b.name,
2269
+ "model": b.model,
2270
+ "configured_provider": provider_class,
2271
+ "claimed_by_provider": claimed_by.__name__,
2272
+ "claimed_by_default_model": claimed_by_default,
2273
+ },
2274
+ )
2275
+
2276
+
2277
+ def _which_shipped_provider_claims(name: str, *, exclude: type):
2278
+ """Return the first shipped provider class that recognizes `name`,
2279
+ excluding `exclude`. Returns None when no other shipped provider
2280
+ claims the name (permissive default per v1.0.2 #1 (b))."""
2281
+ from activegraph.llm.anthropic import AnthropicProvider
2282
+ from activegraph.llm.openai import OpenAIProvider
2283
+
2284
+ candidates = [AnthropicProvider, OpenAIProvider]
2285
+ for cls in candidates:
2286
+ if cls is exclude:
2287
+ continue
2288
+ recognizes = getattr(cls, "recognizes_model", None)
2289
+ if recognizes is None:
2290
+ continue
2291
+ # Instantiate cheaply: the providers' __init__ doesn't open the SDK.
2292
+ try:
2293
+ inst = cls()
2294
+ except Exception:
2295
+ # Defensive: if a provider's no-arg constructor ever requires
2296
+ # real credentials, skip it for the validation lookup.
2297
+ continue
2298
+ if inst.recognizes_model(name):
2299
+ return cls
2300
+ return None
2301
+
2302
+
2183
2303
  def _budget_reason(name: Optional[str]) -> str:
2184
2304
  if name is None:
2185
2305
  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
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
@@ -108,6 +108,7 @@ tests/test_llm_behavior.py
108
108
  tests/test_llm_budget.py
109
109
  tests/test_llm_causal.py
110
110
  tests/test_llm_claim_extraction.py
111
+ tests/test_llm_default_model.py
111
112
  tests/test_llm_determinism.py
112
113
  tests/test_llm_failure.py
113
114
  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"
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"