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.
- {activegraph-1.0.2 → activegraph-1.0.3}/PKG-INFO +1 -1
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/__init__.py +3 -2
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/behaviors/decorators.py +66 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/graph.py +23 -3
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/anthropic.py +21 -1
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/recorded.py +17 -1
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/types.py +12 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/logging.py +4 -0
- activegraph-1.0.3/activegraph/runtime/_live.py +173 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/runtime.py +160 -129
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/graph_query.py +1 -1
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/PKG-INFO +1 -1
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/SOURCES.txt +3 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/pyproject.toml +1 -1
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_graph.py +61 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_behavior.py +103 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_default_model.py +243 -17
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_observability_logging.py +2 -0
- activegraph-1.0.3/tests/test_v1_0_3_behavior_failed_ux.py +262 -0
- activegraph-1.0.3/tests/test_v1_0_3_tool_multiturn.py +371 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/README.md +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/__main__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/behaviors/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/behaviors/base.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/cli/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/cli/main.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/cli/quickstart.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/clock.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/event.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/ids.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/patch.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/core/view.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/frame.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/cache.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/openai.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/parsing.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/prompt.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/llm/provider.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/metrics.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/migration.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/prometheus.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/observability/status.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/behaviors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/fixtures/companies.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/object_types.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/settings.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/diligence/tools.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/loader.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/packs/scaffold.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/policy.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/behavior_graph.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/budget.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/config_errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/diff.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/exec_errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/patterns.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/queue.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/registration_errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/registry.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/scheduler.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/runtime/view_builder.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/base.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/conformance.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/memory.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/postgres.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/serde.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/sqlite.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/store/url.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/base.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/cache.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/context.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/decorators.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/errors.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/recorded.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/tools/web_fetch.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/trace/__init__.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/trace/causal.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph/trace/printer.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/dependency_links.txt +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/entry_points.txt +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/requires.txt +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/activegraph.egg-info/top_level.txt +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/setup.cfg +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_activate_after.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_causal_cross_tool.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_cli.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_clock.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_diff.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_diligence_pack.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_diligence_with_tools.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_doc_links.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_doc_site_reachable.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_errors_format.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_event.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_fork.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_ids.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_anthropic.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_budget.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_causal.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_claim_extraction.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_determinism.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_failure.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_openai.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_prompt.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_provider_fixtures.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_replay.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_tool_loop.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_trace.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_trace_snapshot.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_llm_types.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_migration.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_observability_metrics.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_operate_example.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pack_scaffold.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_packs.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_patch.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pattern_matcher.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pattern_parser.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_pattern_subscriptions.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_persistence.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_postgres_store.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_quickstart.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_quickstart_snapshot.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_replay.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_replay_trace_snapshot.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_requeue_unfired.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_resume_example.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_runtime.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_runtime_status.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_serde.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_store_conformance.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_store_url.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tool_replay.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tool_trace_snapshot.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tools.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_trace.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_tutorial_snippets.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_v1_0_1_patches.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_version_sync.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_view.py +0 -0
- {activegraph-1.0.2 → activegraph-1.0.3}/tests/test_wheel_completeness.py +0 -0
|
@@ -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.
|
|
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
|
|
208
|
+
def objects(
|
|
209
209
|
self,
|
|
210
|
-
|
|
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
|
|
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
|