activegraph 1.0.2__tar.gz → 1.0.3__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 (158) hide show
  1. {activegraph-1.0.2 → activegraph-1.0.3}/PKG-INFO +1 -1
  2. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/__init__.py +3 -2
  3. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/behaviors/decorators.py +66 -0
  4. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/graph.py +23 -3
  5. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/anthropic.py +21 -1
  6. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/recorded.py +17 -1
  7. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/types.py +12 -0
  8. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/logging.py +4 -0
  9. activegraph-1.0.3/activegraph/runtime/_live.py +173 -0
  10. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/runtime.py +160 -129
  11. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/graph_query.py +1 -1
  12. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/PKG-INFO +1 -1
  13. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/SOURCES.txt +3 -0
  14. {activegraph-1.0.2 → activegraph-1.0.3}/pyproject.toml +1 -1
  15. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_graph.py +61 -0
  16. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_behavior.py +103 -0
  17. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_default_model.py +243 -17
  18. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_observability_logging.py +2 -0
  19. activegraph-1.0.3/tests/test_v1_0_3_behavior_failed_ux.py +262 -0
  20. activegraph-1.0.3/tests/test_v1_0_3_tool_multiturn.py +371 -0
  21. {activegraph-1.0.2 → activegraph-1.0.3}/README.md +0 -0
  22. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/__main__.py +0 -0
  23. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/behaviors/__init__.py +0 -0
  24. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/behaviors/base.py +0 -0
  25. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/cli/__init__.py +0 -0
  26. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/cli/main.py +0 -0
  27. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/cli/quickstart.py +0 -0
  28. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/__init__.py +0 -0
  29. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/clock.py +0 -0
  30. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/event.py +0 -0
  31. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/ids.py +0 -0
  32. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/patch.py +0 -0
  33. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/view.py +0 -0
  34. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/errors.py +0 -0
  35. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/frame.py +0 -0
  36. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/__init__.py +0 -0
  37. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/cache.py +0 -0
  38. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/errors.py +0 -0
  39. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/openai.py +0 -0
  40. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/parsing.py +0 -0
  41. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/prompt.py +0 -0
  42. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/provider.py +0 -0
  43. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/__init__.py +0 -0
  44. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/metrics.py +0 -0
  45. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/migration.py +0 -0
  46. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/prometheus.py +0 -0
  47. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/status.py +0 -0
  48. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/__init__.py +0 -0
  49. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/__init__.py +0 -0
  50. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/behaviors.py +0 -0
  51. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  52. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  53. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/object_types.py +0 -0
  54. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  55. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  56. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  57. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  58. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/settings.py +0 -0
  59. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/tools.py +0 -0
  60. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/loader.py +0 -0
  61. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/scaffold.py +0 -0
  62. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/policy.py +0 -0
  63. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/__init__.py +0 -0
  64. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/behavior_graph.py +0 -0
  65. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/budget.py +0 -0
  66. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/config_errors.py +0 -0
  67. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/diff.py +0 -0
  68. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/errors.py +0 -0
  69. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/exec_errors.py +0 -0
  70. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/patterns.py +0 -0
  71. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/queue.py +0 -0
  72. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/registration_errors.py +0 -0
  73. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/registry.py +0 -0
  74. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/scheduler.py +0 -0
  75. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/view_builder.py +0 -0
  76. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/__init__.py +0 -0
  77. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/base.py +0 -0
  78. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/conformance.py +0 -0
  79. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/errors.py +0 -0
  80. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/memory.py +0 -0
  81. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/postgres.py +0 -0
  82. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/serde.py +0 -0
  83. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/sqlite.py +0 -0
  84. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/url.py +0 -0
  85. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/__init__.py +0 -0
  86. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/base.py +0 -0
  87. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/cache.py +0 -0
  88. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/context.py +0 -0
  89. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/decorators.py +0 -0
  90. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/errors.py +0 -0
  91. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/recorded.py +0 -0
  92. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/web_fetch.py +0 -0
  93. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/trace/__init__.py +0 -0
  94. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/trace/causal.py +0 -0
  95. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/trace/printer.py +0 -0
  96. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/dependency_links.txt +0 -0
  97. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/entry_points.txt +0 -0
  98. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/requires.txt +0 -0
  99. {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/top_level.txt +0 -0
  100. {activegraph-1.0.2 → activegraph-1.0.3}/setup.cfg +0 -0
  101. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_activate_after.py +0 -0
  102. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_causal_cross_tool.py +0 -0
  103. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_cli.py +0 -0
  104. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_clock.py +0 -0
  105. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_diff.py +0 -0
  106. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_diligence_pack.py +0 -0
  107. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_diligence_with_tools.py +0 -0
  108. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_doc_links.py +0 -0
  109. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_doc_site_reachable.py +0 -0
  110. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_errors_format.py +0 -0
  111. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_event.py +0 -0
  112. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_fork.py +0 -0
  113. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_ids.py +0 -0
  114. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_anthropic.py +0 -0
  115. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_budget.py +0 -0
  116. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_causal.py +0 -0
  117. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_claim_extraction.py +0 -0
  118. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_determinism.py +0 -0
  119. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_failure.py +0 -0
  120. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_openai.py +0 -0
  121. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_prompt.py +0 -0
  122. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_provider_fixtures.py +0 -0
  123. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_replay.py +0 -0
  124. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_tool_loop.py +0 -0
  125. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_trace.py +0 -0
  126. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_trace_snapshot.py +0 -0
  127. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_types.py +0 -0
  128. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_migration.py +0 -0
  129. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_observability_metrics.py +0 -0
  130. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_operate_example.py +0 -0
  131. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pack_scaffold.py +0 -0
  132. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_packs.py +0 -0
  133. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_patch.py +0 -0
  134. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pattern_matcher.py +0 -0
  135. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pattern_parser.py +0 -0
  136. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pattern_subscriptions.py +0 -0
  137. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_persistence.py +0 -0
  138. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_postgres_store.py +0 -0
  139. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_quickstart.py +0 -0
  140. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_quickstart_snapshot.py +0 -0
  141. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_replay.py +0 -0
  142. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_replay_trace_snapshot.py +0 -0
  143. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_requeue_unfired.py +0 -0
  144. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_resume_example.py +0 -0
  145. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_runtime.py +0 -0
  146. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_runtime_status.py +0 -0
  147. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_serde.py +0 -0
  148. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_store_conformance.py +0 -0
  149. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_store_url.py +0 -0
  150. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tool_replay.py +0 -0
  151. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tool_trace_snapshot.py +0 -0
  152. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tools.py +0 -0
  153. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_trace.py +0 -0
  154. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tutorial_snippets.py +0 -0
  155. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_v1_0_1_patches.py +0 -0
  156. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_version_sync.py +0 -0
  157. {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_view.py +0 -0
  158. {activegraph-1.0.2 → activegraph-1.0.3}/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.2
3
+ Version: 1.0.3
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,7 +55,7 @@ from activegraph.runtime.exec_errors import (
55
55
  RuntimeContextRequiredError,
56
56
  )
57
57
  from activegraph.runtime.patterns import UnsupportedPatternError
58
- from activegraph.runtime.runtime import Runtime
58
+ from activegraph.runtime.runtime import BehaviorFailure, Runtime
59
59
  from activegraph.store import (
60
60
  CorruptedEventPayloadError,
61
61
  DuplicateEventError,
@@ -125,6 +125,7 @@ __all__ = [
125
125
  "AmbiguousToolError",
126
126
  "ApprovalNotFoundError",
127
127
  "Behavior",
128
+ "BehaviorFailure",
128
129
  "BehaviorNotFoundError",
129
130
  "Budget",
130
131
  "Clock",
@@ -221,4 +222,4 @@ __all__ = [
221
222
  "tool",
222
223
  ]
223
224
 
224
- __version__ = "1.0.2"
225
+ __version__ = "1.0.3"
@@ -27,6 +27,54 @@ from activegraph.behaviors.base import (
27
27
  _REGISTRY: list[Union[Behavior, RelationBehavior]] = []
28
28
 
29
29
 
30
+ def _validate_output_schema(output_schema: Any) -> None:
31
+ """Strict-validate ``output_schema=`` at @llm_behavior time.
32
+
33
+ CONTRACT v1.0.3 #2. Accepts ``None`` (output_schema is optional)
34
+ or a Pydantic ``BaseModel`` subclass. Anything else — a dict, a
35
+ string, an instance instead of a class — raises ``TypeError``
36
+ with a structured message naming what was passed and showing the
37
+ correct form as a code example.
38
+
39
+ Dict-form output_schema support (JSON-schema dict) is a v1.1
40
+ candidate per the CONTRACT amendment; the rationale and the
41
+ fix the user wants live there.
42
+ """
43
+ if output_schema is None:
44
+ return
45
+ # Import lazily so the framework can still be imported in
46
+ # environments without Pydantic (e.g., pack metadata tooling).
47
+ # All real @llm_behavior callsites already require Pydantic
48
+ # transitively via LLMBehavior.output_schema.
49
+ try:
50
+ from pydantic import BaseModel
51
+ except ImportError: # pragma: no cover — Pydantic is a hard dep at runtime
52
+ return
53
+ if isinstance(output_schema, type) and issubclass(output_schema, BaseModel):
54
+ return
55
+ passed = (
56
+ type(output_schema).__name__
57
+ if not isinstance(output_schema, type)
58
+ else f"{output_schema.__name__} (a class, but not a BaseModel subclass)"
59
+ )
60
+ raise TypeError(
61
+ f"output_schema must be a Pydantic BaseModel subclass, not "
62
+ f"{passed}.\n"
63
+ f"\n"
64
+ f"Example:\n"
65
+ f" from pydantic import BaseModel\n"
66
+ f"\n"
67
+ f" class MyOutput(BaseModel):\n"
68
+ f" result: str\n"
69
+ f"\n"
70
+ f" @llm_behavior(output_schema=MyOutput)\n"
71
+ f" def my_behavior(event, graph, ctx, out): ...\n"
72
+ f"\n"
73
+ f"Dict-form output_schema (e.g., JSON Schema as a dict) is\n"
74
+ f"filed as a v1.1 candidate. See CONTRACT v1.0.3 #2."
75
+ )
76
+
77
+
30
78
  def clear_registry() -> list[Union[Behavior, RelationBehavior]]:
31
79
  """Empty the global behavior registry and return what was cleared.
32
80
 
@@ -85,6 +133,12 @@ def register(behavior_obj: Union[Behavior, RelationBehavior]) -> None:
85
133
  f"decorators to construct one."
86
134
  )
87
135
  _REGISTRY.append(behavior_obj)
136
+ # v1.0.2.post1 #1 (b): if any Runtime is already alive, validate the
137
+ # new behavior against each one's configured provider so cross-
138
+ # provider model mismatches fire at the @llm_behavior / register()
139
+ # line rather than at first run_goal.
140
+ from activegraph.runtime._live import validate_behavior_against_live_runtimes
141
+ validate_behavior_against_live_runtimes(behavior_obj)
88
142
 
89
143
 
90
144
  def behavior(
@@ -221,6 +275,12 @@ def llm_behavior(
221
275
  if activate_after is not None:
222
276
  delay_n = _parse_aa(activate_after)
223
277
 
278
+ # v1.0.3 #2: strict-validate output_schema= at decoration time.
279
+ # Users who passed a JSON-schema dict previously hit a silent
280
+ # behavior.failed with reason=llm.schema_violation at first LLM
281
+ # call; failing here names the cause at the @llm_behavior line.
282
+ _validate_output_schema(output_schema)
283
+
224
284
  def wrap(fn: Callable) -> LLMBehavior:
225
285
  b = LLMBehavior(
226
286
  name=name or fn.__name__,
@@ -248,6 +308,12 @@ def llm_behavior(
248
308
  max_tool_turns=max_tool_turns,
249
309
  )
250
310
  _REGISTRY.append(b)
311
+ # v1.0.2.post1 #1 (b): validate against any live Runtime's
312
+ # provider. Mirrors the eager check in register() so decorated
313
+ # behaviors with cross-provider model names fire at the
314
+ # @llm_behavior line instead of at first run_goal.
315
+ from activegraph.runtime._live import validate_behavior_against_live_runtimes
316
+ validate_behavior_against_live_runtimes(b)
251
317
  return b
252
318
 
253
319
  return wrap
@@ -205,20 +205,40 @@ class Graph:
205
205
  [self._relations[i] for i in seen_rels if i in self._relations],
206
206
  )
207
207
 
208
- def query(
208
+ def objects(
209
209
  self,
210
- object_type: Optional[str] = None,
210
+ type: Optional[str] = None,
211
211
  where: Optional[dict[str, Any]] = None,
212
212
  ) -> list[Object]:
213
+ """Return objects matching `type` and/or `where`.
214
+
215
+ v1.0.3 #1: the canonical query API on `Graph`, mirroring
216
+ `View.objects(type=...)` so call sites read the same inside
217
+ and outside behaviors. `Graph.query(object_type=...)` is kept
218
+ as a backward-compatible alias.
219
+ """
213
220
  out: list[Object] = []
214
221
  for o in self._objects.values():
215
- if object_type is not None and o.type != object_type:
222
+ if type is not None and o.type != type:
216
223
  continue
217
224
  if where and not _eval_where_on_object(where, o):
218
225
  continue
219
226
  out.append(o)
220
227
  return out
221
228
 
229
+ def query(
230
+ self,
231
+ object_type: Optional[str] = None,
232
+ where: Optional[dict[str, Any]] = None,
233
+ ) -> list[Object]:
234
+ """Backward-compatible alias for :meth:`objects`. v1.0.3 #1.
235
+
236
+ New code should use ``graph.objects(type=...)`` — the kwarg
237
+ ``type`` matches :meth:`View.objects` so the call reads the
238
+ same in and out of behaviors.
239
+ """
240
+ return self.objects(type=object_type, where=where)
241
+
222
242
  def has_object_of_type(self, type_: str) -> bool:
223
243
  return any(o.type == type_ for o in self._objects.values())
224
244
 
@@ -257,7 +257,13 @@ def _message_to_anthropic(m: LLMMessage) -> dict[str, Any]:
257
257
 
258
258
  For role="tool", Anthropic wants a "user" message with a
259
259
  `tool_result` content block. For role in {"user","assistant"} the
260
- standard {role, content: str} shape works.
260
+ standard {role, content: str} shape works — except for the
261
+ multi-turn tool-use case (v1.0.3 #4), where an assistant message
262
+ that triggered tool_use must echo back its full content blocks
263
+ (text + tool_use) so the subsequent user tool_result blocks
264
+ reference matching tool_use_ids in the preceding assistant turn.
265
+ Direct Anthropic API access tolerated raw_text-only echo; the
266
+ Vertex AI proxy enforces the spec strictly and 400s without it.
261
267
  """
262
268
  if m.role == "tool":
263
269
  return {
@@ -270,6 +276,20 @@ def _message_to_anthropic(m: LLMMessage) -> dict[str, Any]:
270
276
  }
271
277
  ],
272
278
  }
279
+ if m.role == "assistant" and m.tool_calls:
280
+ blocks: list[dict[str, Any]] = []
281
+ if m.content:
282
+ blocks.append({"type": "text", "text": m.content})
283
+ for tc in m.tool_calls:
284
+ blocks.append(
285
+ {
286
+ "type": "tool_use",
287
+ "id": tc.id,
288
+ "name": tc.name,
289
+ "input": dict(tc.args),
290
+ }
291
+ )
292
+ return {"role": "assistant", "content": blocks}
273
293
  return {"role": m.role, "content": m.content}
274
294
 
275
295
 
@@ -51,7 +51,7 @@ from typing import Any, Optional
51
51
 
52
52
  from activegraph.llm.errors import LLMBehaviorError
53
53
  from activegraph.llm.provider import LLMProvider
54
- from activegraph.llm.types import LLMMessage, LLMResponse
54
+ from activegraph.llm.types import LLMMessage, LLMResponse, ToolCall
55
55
 
56
56
 
57
57
  def _now_iso() -> str:
@@ -188,6 +188,21 @@ def _response_from_fixture(
188
188
  cost = rdata.get("cost_usd", "0")
189
189
  if not isinstance(cost, Decimal):
190
190
  cost = Decimal(str(cost))
191
+ # v1.0.3 #4: reconstruct tool_calls from the fixture so multi-turn
192
+ # tool-use exchanges replay correctly. Pre-v1.0.3 fixtures and
193
+ # non-tool-using fixtures have no tool_calls key (or null) and
194
+ # round-trip to None — backward compatible.
195
+ tool_calls_raw = rdata.get("tool_calls")
196
+ tool_calls: Optional[list[ToolCall]] = None
197
+ if tool_calls_raw:
198
+ tool_calls = [
199
+ ToolCall(
200
+ id=tc.get("id", ""),
201
+ name=tc.get("name", ""),
202
+ args=dict(tc.get("args") or {}),
203
+ )
204
+ for tc in tool_calls_raw
205
+ ]
191
206
  return LLMResponse(
192
207
  raw_text=rdata.get("raw_text", ""),
193
208
  parsed=parsed,
@@ -200,6 +215,7 @@ def _response_from_fixture(
200
215
  seed=rdata.get("seed"),
201
216
  cache_hit=False,
202
217
  provider_meta=dict(rdata.get("provider_meta", {}) or {}),
218
+ tool_calls=tool_calls,
203
219
  )
204
220
 
205
221
 
@@ -43,12 +43,20 @@ class LLMMessage:
43
43
  CONTRACT v0.7: a `role="tool"` message echoes a tool result back to
44
44
  the model. `tool_use_id` ties it to the originating tool_use block
45
45
  from the previous assistant turn.
46
+
47
+ CONTRACT v1.0.3 #4: when an assistant message echoes a turn that
48
+ triggered tool_use, the originating ToolCall objects travel
49
+ alongside the raw text via `tool_calls`. The provider adapter
50
+ reconstructs the wire-format content blocks on the way out. None
51
+ on every other message shape — single-turn fixtures and zero-tool
52
+ assistant messages keep their byte-identical serialization.
46
53
  """
47
54
 
48
55
  role: Role
49
56
  content: str
50
57
  tool_use_id: Optional[str] = None
51
58
  tool_name: Optional[str] = None
59
+ tool_calls: Optional[tuple["ToolCall", ...]] = None
52
60
 
53
61
  def to_dict(self) -> dict[str, Any]:
54
62
  out: dict[str, Any] = {"role": self.role, "content": self.content}
@@ -56,6 +64,10 @@ class LLMMessage:
56
64
  out["tool_use_id"] = self.tool_use_id
57
65
  if self.tool_name is not None:
58
66
  out["tool_name"] = self.tool_name
67
+ # v1.0.3 #4: only emit `tool_calls` when present so recorded-
68
+ # fixture hashes for single-turn flows stay byte-identical.
69
+ if self.tool_calls is not None:
70
+ out["tool_calls"] = [tc.to_dict() for tc in self.tool_calls]
59
71
  return out
60
72
 
61
73
 
@@ -41,6 +41,10 @@ LOG_FIELDS: tuple[str, ...] = (
41
41
  "reason",
42
42
  "error_type",
43
43
  "error_message",
44
+ # v1.0.3 #3: behavior.failed WARNING log carries the More: URL
45
+ # for the failure reason's documentation page. Operators tail
46
+ # logs and click through to the reason's doc-page from the URL.
47
+ "doc_url",
44
48
  )
45
49
 
46
50
  # Reserved attributes on every LogRecord (stdlib internals). Any
@@ -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