activegraph 1.0.2.post1__tar.gz → 1.0.4__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.post1 → activegraph-1.0.4}/PKG-INFO +1 -1
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/__init__.py +3 -2
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/behaviors/decorators.py +54 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/graph.py +50 -3
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/anthropic.py +21 -1
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/recorded.py +17 -1
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/types.py +12 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/observability/logging.py +4 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/runtime.py +136 -39
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/graph_query.py +1 -1
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph.egg-info/PKG-INFO +1 -1
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph.egg-info/SOURCES.txt +2 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/pyproject.toml +1 -1
- activegraph-1.0.4/tests/test_graph.py +269 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_behavior.py +103 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_observability_logging.py +2 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_requeue_unfired.py +64 -0
- activegraph-1.0.4/tests/test_v1_0_3_behavior_failed_ux.py +262 -0
- activegraph-1.0.4/tests/test_v1_0_3_tool_multiturn.py +371 -0
- activegraph-1.0.2.post1/tests/test_graph.py +0 -110
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/README.md +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/__main__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/behaviors/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/behaviors/base.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/cli/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/cli/main.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/cli/quickstart.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/clock.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/event.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/ids.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/patch.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/core/view.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/frame.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/cache.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/openai.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/parsing.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/prompt.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/llm/provider.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/observability/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/observability/metrics.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/observability/migration.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/observability/prometheus.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/observability/status.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/behaviors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/fixtures/companies.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/object_types.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/settings.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/diligence/tools.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/loader.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/packs/scaffold.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/policy.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/_live.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/behavior_graph.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/budget.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/config_errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/diff.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/exec_errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/patterns.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/queue.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/registration_errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/registry.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/scheduler.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/runtime/view_builder.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/base.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/conformance.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/memory.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/postgres.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/serde.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/sqlite.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/store/url.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/base.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/cache.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/context.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/decorators.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/errors.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/recorded.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/tools/web_fetch.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/trace/__init__.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/trace/causal.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph/trace/printer.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph.egg-info/dependency_links.txt +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph.egg-info/entry_points.txt +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph.egg-info/requires.txt +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/activegraph.egg-info/top_level.txt +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/setup.cfg +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_activate_after.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_causal_cross_tool.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_cli.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_clock.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_diff.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_diligence_pack.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_diligence_with_tools.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_doc_links.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_doc_site_reachable.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_errors_format.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_event.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_fork.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_ids.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_anthropic.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_budget.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_causal.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_claim_extraction.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_default_model.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_determinism.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_failure.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_openai.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_prompt.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_provider_fixtures.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_replay.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_tool_loop.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_trace.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_trace_snapshot.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_llm_types.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_migration.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_observability_metrics.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_operate_example.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_pack_scaffold.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_packs.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_patch.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_pattern_matcher.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_pattern_parser.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_pattern_subscriptions.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_persistence.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_postgres_store.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_quickstart.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_quickstart_snapshot.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_replay.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_replay_trace_snapshot.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_resume_example.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_runtime.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_runtime_status.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_serde.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_store_conformance.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_store_url.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_tool_replay.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_tool_trace_snapshot.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_tools.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_trace.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_tutorial_snippets.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_v1_0_1_patches.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_version_sync.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/tests/test_view.py +0 -0
- {activegraph-1.0.2.post1 → activegraph-1.0.4}/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.4"
|
|
@@ -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
|
|
|
@@ -227,6 +275,12 @@ def llm_behavior(
|
|
|
227
275
|
if activate_after is not None:
|
|
228
276
|
delay_n = _parse_aa(activate_after)
|
|
229
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
|
+
|
|
230
284
|
def wrap(fn: Callable) -> LLMBehavior:
|
|
231
285
|
b = LLMBehavior(
|
|
232
286
|
name=name or fn.__name__,
|
|
@@ -181,6 +181,33 @@ class Graph:
|
|
|
181
181
|
out.append(r)
|
|
182
182
|
return out
|
|
183
183
|
|
|
184
|
+
def relations(
|
|
185
|
+
self,
|
|
186
|
+
source: Optional[str] = None,
|
|
187
|
+
target: Optional[str] = None,
|
|
188
|
+
type: Optional[str] = None,
|
|
189
|
+
) -> list[Relation]:
|
|
190
|
+
"""Return relations filtered by ``source``, ``target``, and/or ``type``.
|
|
191
|
+
|
|
192
|
+
v1.0.4 #1: the canonical filter API on ``Graph``. Decomposes the
|
|
193
|
+
v0 ``get_relations(object_id=, direction=)`` axis into separate
|
|
194
|
+
``source`` and ``target`` slots so the call reads the way users
|
|
195
|
+
already write it (matches ``docs/concepts/graph.md``). Filter
|
|
196
|
+
kwargs compose by AND; calling with no kwargs returns every
|
|
197
|
+
relation. ``Graph.get_relations(object_id=, type=, direction=)``
|
|
198
|
+
stays as a backward-compatible alias.
|
|
199
|
+
"""
|
|
200
|
+
out: list[Relation] = []
|
|
201
|
+
for r in self._relations.values():
|
|
202
|
+
if source is not None and r.source != source:
|
|
203
|
+
continue
|
|
204
|
+
if target is not None and r.target != target:
|
|
205
|
+
continue
|
|
206
|
+
if type is not None and r.type != type:
|
|
207
|
+
continue
|
|
208
|
+
out.append(r)
|
|
209
|
+
return out
|
|
210
|
+
|
|
184
211
|
def neighborhood(self, object_id: str, depth: int = 1) -> tuple[list[Object], list[Relation]]:
|
|
185
212
|
if object_id not in self._objects:
|
|
186
213
|
return ([], [])
|
|
@@ -205,20 +232,40 @@ class Graph:
|
|
|
205
232
|
[self._relations[i] for i in seen_rels if i in self._relations],
|
|
206
233
|
)
|
|
207
234
|
|
|
208
|
-
def
|
|
235
|
+
def objects(
|
|
209
236
|
self,
|
|
210
|
-
|
|
237
|
+
type: Optional[str] = None,
|
|
211
238
|
where: Optional[dict[str, Any]] = None,
|
|
212
239
|
) -> list[Object]:
|
|
240
|
+
"""Return objects matching `type` and/or `where`.
|
|
241
|
+
|
|
242
|
+
v1.0.3 #1: the canonical query API on `Graph`, mirroring
|
|
243
|
+
`View.objects(type=...)` so call sites read the same inside
|
|
244
|
+
and outside behaviors. `Graph.query(object_type=...)` is kept
|
|
245
|
+
as a backward-compatible alias.
|
|
246
|
+
"""
|
|
213
247
|
out: list[Object] = []
|
|
214
248
|
for o in self._objects.values():
|
|
215
|
-
if
|
|
249
|
+
if type is not None and o.type != type:
|
|
216
250
|
continue
|
|
217
251
|
if where and not _eval_where_on_object(where, o):
|
|
218
252
|
continue
|
|
219
253
|
out.append(o)
|
|
220
254
|
return out
|
|
221
255
|
|
|
256
|
+
def query(
|
|
257
|
+
self,
|
|
258
|
+
object_type: Optional[str] = None,
|
|
259
|
+
where: Optional[dict[str, Any]] = None,
|
|
260
|
+
) -> list[Object]:
|
|
261
|
+
"""Backward-compatible alias for :meth:`objects`. v1.0.3 #1.
|
|
262
|
+
|
|
263
|
+
New code should use ``graph.objects(type=...)`` — the kwarg
|
|
264
|
+
``type`` matches :meth:`View.objects` so the call reads the
|
|
265
|
+
same in and out of behaviors.
|
|
266
|
+
"""
|
|
267
|
+
return self.objects(type=object_type, where=where)
|
|
268
|
+
|
|
222
269
|
def has_object_of_type(self, type_: str) -> bool:
|
|
223
270
|
return any(o.type == type_ for o in self._objects.values())
|
|
224
271
|
|
|
@@ -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
|
|
@@ -65,7 +65,7 @@ def _monotonic() -> float:
|
|
|
65
65
|
from dataclasses import dataclass, field
|
|
66
66
|
from datetime import datetime, timezone
|
|
67
67
|
from decimal import Decimal
|
|
68
|
-
from typing import Any, Callable, Iterable, Optional, Union
|
|
68
|
+
from typing import Any, Callable, Iterable, NamedTuple, Optional, Union
|
|
69
69
|
|
|
70
70
|
from activegraph.behaviors.base import Behavior, LLMBehavior, RelationBehavior
|
|
71
71
|
from activegraph.behaviors.decorators import get_registry
|
|
@@ -170,6 +170,58 @@ class Context:
|
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
class BehaviorFailure(NamedTuple):
|
|
174
|
+
"""Structured view of a ``behavior.failed`` event for
|
|
175
|
+
:attr:`Runtime.errors`. CONTRACT v1.0.3 #3.
|
|
176
|
+
|
|
177
|
+
Five fields capture the operationally important parts of a
|
|
178
|
+
failure: which behavior, which event triggered it, the v0.6 #11
|
|
179
|
+
reason code (when present), the Python exception class name, and
|
|
180
|
+
the exception message. ``failed_event_id`` ties the structured
|
|
181
|
+
view back to the underlying ``behavior.failed`` event so callers
|
|
182
|
+
that want the full payload (traceback, payload extras) can
|
|
183
|
+
re-read it from ``runtime.graph._events``.
|
|
184
|
+
|
|
185
|
+
Not exposed as a class users construct — it's a projection of
|
|
186
|
+
the events the runtime emits, and the runtime is the only writer.
|
|
187
|
+
Distinct from Python's builtin ``RuntimeError``; named
|
|
188
|
+
``BehaviorFailure`` to avoid the shadow.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
behavior: str
|
|
192
|
+
event_id: str
|
|
193
|
+
reason: Optional[str]
|
|
194
|
+
exception_type: str
|
|
195
|
+
message: str
|
|
196
|
+
failed_event_id: str
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Map v0.6 #11 reason-code prefixes to the framework error class's
|
|
200
|
+
# doc-page slug. Used by the WARNING log emitted from
|
|
201
|
+
# `_emit_behavior_failed` so the log line carries a More: URL the
|
|
202
|
+
# operator can open. The slugs are class-level (the v1.0 #4 More:
|
|
203
|
+
# URL convention) — a per-reason slug doesn't exist today.
|
|
204
|
+
_REASON_PREFIX_TO_DOC_SLUG: tuple[tuple[str, str], ...] = (
|
|
205
|
+
("llm.", "llm-behavior-error"),
|
|
206
|
+
("tool.", "tool-error"),
|
|
207
|
+
("budget.", "budget-exhausted"),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _doc_url_for_reason(reason: str) -> str:
|
|
212
|
+
"""Return the More: doc-page URL for a v0.6 #11 reason code.
|
|
213
|
+
|
|
214
|
+
Defaults to the generic execution-error page when no prefix
|
|
215
|
+
matches (covers ``exception.*`` reasons from generic catches).
|
|
216
|
+
"""
|
|
217
|
+
from activegraph.errors import DOCS_BASE_URL
|
|
218
|
+
|
|
219
|
+
for prefix, slug in _REASON_PREFIX_TO_DOC_SLUG:
|
|
220
|
+
if reason.startswith(prefix):
|
|
221
|
+
return f"{DOCS_BASE_URL}/errors/{slug}"
|
|
222
|
+
return f"{DOCS_BASE_URL}/errors/execution-error"
|
|
223
|
+
|
|
224
|
+
|
|
173
225
|
class Runtime:
|
|
174
226
|
def __init__(
|
|
175
227
|
self,
|
|
@@ -326,6 +378,38 @@ class Runtime:
|
|
|
326
378
|
def run_id(self) -> str:
|
|
327
379
|
return self.graph.run_id
|
|
328
380
|
|
|
381
|
+
@property
|
|
382
|
+
def errors(self) -> list[BehaviorFailure]:
|
|
383
|
+
"""Accumulated ``behavior.failed`` events as structured tuples.
|
|
384
|
+
|
|
385
|
+
v1.0.3 #3. Reads from ``self.graph._events`` on each access —
|
|
386
|
+
the events are the source of truth and this property is a
|
|
387
|
+
projection. No caching, no listener registration, no new
|
|
388
|
+
state. Callers can inspect failures programmatically without
|
|
389
|
+
reaching into ``graph._events`` or parsing payload dicts.
|
|
390
|
+
|
|
391
|
+
Each :class:`BehaviorFailure` carries five operationally
|
|
392
|
+
useful fields plus the underlying ``behavior.failed`` event
|
|
393
|
+
id for callers that want to re-read the full payload (e.g.,
|
|
394
|
+
traceback, LLM payload extras).
|
|
395
|
+
"""
|
|
396
|
+
out: list[BehaviorFailure] = []
|
|
397
|
+
for e in self.graph._events:
|
|
398
|
+
if e.type != "behavior.failed":
|
|
399
|
+
continue
|
|
400
|
+
p = e.payload
|
|
401
|
+
out.append(
|
|
402
|
+
BehaviorFailure(
|
|
403
|
+
behavior=p.get("behavior", ""),
|
|
404
|
+
event_id=p.get("event_id", ""),
|
|
405
|
+
reason=p.get("reason"),
|
|
406
|
+
exception_type=p.get("exception_type", ""),
|
|
407
|
+
message=p.get("message", ""),
|
|
408
|
+
failed_event_id=e.id,
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
return out
|
|
412
|
+
|
|
329
413
|
# ---------- listener ----------
|
|
330
414
|
|
|
331
415
|
def _on_event(self, event: Event) -> None:
|
|
@@ -661,27 +745,12 @@ class Runtime:
|
|
|
661
745
|
"activegraph_behaviors_failed_total",
|
|
662
746
|
{"behavior": b.name, "reason": f"exception.{type(e).__name__}"},
|
|
663
747
|
)
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
reason=f"exception.{type(e).__name__}",
|
|
671
|
-
error_type=type(e).__name__,
|
|
672
|
-
error_message=str(e),
|
|
673
|
-
),
|
|
674
|
-
)
|
|
675
|
-
self._emit_lifecycle(
|
|
676
|
-
"behavior.failed",
|
|
677
|
-
{
|
|
678
|
-
"behavior": b.name,
|
|
679
|
-
"event_id": event.id,
|
|
680
|
-
"exception_type": type(e).__name__,
|
|
681
|
-
"message": str(e),
|
|
682
|
-
"traceback": traceback.format_exc(),
|
|
683
|
-
},
|
|
684
|
-
)
|
|
748
|
+
# v1.0.3 #3: route through _emit_behavior_failed so the
|
|
749
|
+
# WARNING log line and the event emission stay in one
|
|
750
|
+
# place. The previous ERROR log is removed — the
|
|
751
|
+
# centralized emitter handles logging at WARNING for
|
|
752
|
+
# every behavior.failed.
|
|
753
|
+
self._emit_behavior_failed(b.name, event.id, e)
|
|
685
754
|
return
|
|
686
755
|
|
|
687
756
|
self.metrics.histogram(
|
|
@@ -974,14 +1043,22 @@ class Runtime:
|
|
|
974
1043
|
break
|
|
975
1044
|
|
|
976
1045
|
# Tool calls. Append the assistant turn to messages, then
|
|
977
|
-
# dispatch each tool.
|
|
978
|
-
#
|
|
979
|
-
#
|
|
980
|
-
#
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1046
|
+
# dispatch each tool. v1.0.3 #4: the assistant turn must
|
|
1047
|
+
# carry both the text and the tool_use blocks. Anthropic
|
|
1048
|
+
# requires every following tool_result block to reference
|
|
1049
|
+
# a tool_use_id from the preceding assistant message; the
|
|
1050
|
+
# Vertex AI proxy enforces this strictly (HTTP 400 without
|
|
1051
|
+
# matching blocks), and the direct API tolerates the raw-
|
|
1052
|
+
# text-only form for now but is expected to tighten. The
|
|
1053
|
+
# provider adapter reconstructs the wire-format content
|
|
1054
|
+
# blocks from `tool_calls`.
|
|
1055
|
+
running_messages.append(
|
|
1056
|
+
LLMMessage(
|
|
1057
|
+
role="assistant",
|
|
1058
|
+
content=turn_response.raw_text or "",
|
|
1059
|
+
tool_calls=tuple(response_tool_calls),
|
|
984
1060
|
)
|
|
1061
|
+
)
|
|
985
1062
|
for call in response_tool_calls:
|
|
986
1063
|
# CONTRACT v0.7 #6: budget enforcement BEFORE invocation
|
|
987
1064
|
# so an exhausted budget fails the behavior, doesn't
|
|
@@ -1411,6 +1488,14 @@ class Runtime:
|
|
|
1411
1488
|
) -> None:
|
|
1412
1489
|
"""Centralized behavior.failed emission. CONTRACT #13 plus
|
|
1413
1490
|
v0.6 #11 (optional `reason` + LLM-specific extras).
|
|
1491
|
+
|
|
1492
|
+
v1.0.3 #3: emits a WARNING log line so callers running
|
|
1493
|
+
``runtime.run_goal()`` see failures without subscribing to
|
|
1494
|
+
``behavior.failed`` or inspecting ``graph._events``. Users
|
|
1495
|
+
opt out via standard ``logging.getLogger('activegraph.runtime')``
|
|
1496
|
+
configuration. Every ``behavior.failed`` emission routes
|
|
1497
|
+
through this method so exactly one log line is produced per
|
|
1498
|
+
failure at one consistent level.
|
|
1414
1499
|
"""
|
|
1415
1500
|
|
|
1416
1501
|
tb_str = "".join(
|
|
@@ -1429,6 +1514,24 @@ class Runtime:
|
|
|
1429
1514
|
for k, v in extras.items():
|
|
1430
1515
|
if k not in payload:
|
|
1431
1516
|
payload[k] = v
|
|
1517
|
+
# v1.0.3 #3: WARNING log with doc_url pointing at the reason's
|
|
1518
|
+
# documentation page. Slug is class-level (the v1.0 #4 More:
|
|
1519
|
+
# URL convention) — LLMBehaviorError for llm.*, ToolError for
|
|
1520
|
+
# tool.*, ConfigurationError for budget.*, else the generic
|
|
1521
|
+
# execution-error page.
|
|
1522
|
+
log_reason = reason or f"exception.{type(exc).__name__}"
|
|
1523
|
+
self._log.warning(
|
|
1524
|
+
f"behavior failed: {behavior_name} (reason={log_reason})",
|
|
1525
|
+
extra=runtime_log_extra(
|
|
1526
|
+
run_id=self.graph.run_id,
|
|
1527
|
+
event_id=event_id,
|
|
1528
|
+
behavior=behavior_name,
|
|
1529
|
+
reason=log_reason,
|
|
1530
|
+
error_type=type(exc).__name__,
|
|
1531
|
+
error_message=str(exc),
|
|
1532
|
+
doc_url=_doc_url_for_reason(log_reason),
|
|
1533
|
+
),
|
|
1534
|
+
)
|
|
1432
1535
|
self._emit_lifecycle("behavior.failed", payload)
|
|
1433
1536
|
|
|
1434
1537
|
def _invoke_relation(
|
|
@@ -1470,16 +1573,10 @@ class Runtime:
|
|
|
1470
1573
|
try:
|
|
1471
1574
|
b.run(relation, event, bgraph, ctx)
|
|
1472
1575
|
except Exception as e:
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
"event_id": event.id,
|
|
1478
|
-
"exception_type": type(e).__name__,
|
|
1479
|
-
"message": str(e),
|
|
1480
|
-
"traceback": traceback.format_exc(),
|
|
1481
|
-
},
|
|
1482
|
-
)
|
|
1576
|
+
# v1.0.3 #3: route through _emit_behavior_failed so
|
|
1577
|
+
# relation-behavior failures go through the same WARNING
|
|
1578
|
+
# log + event emission path as function and LLM behaviors.
|
|
1579
|
+
self._emit_behavior_failed(b.name, event.id, e)
|
|
1483
1580
|
return
|
|
1484
1581
|
|
|
1485
1582
|
self._emit_lifecycle(
|
|
@@ -68,7 +68,7 @@ def make_graph_query_tool(graph) -> Tool:
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
def fn(args: GraphQueryInput, ctx: ToolContext) -> GraphQueryOutput:
|
|
71
|
-
results = graph.
|
|
71
|
+
results = graph.objects(type=args.object_type, where=args.where)
|
|
72
72
|
truncated = len(results) > args.limit
|
|
73
73
|
results = results[: args.limit]
|
|
74
74
|
return GraphQueryOutput(
|
|
@@ -149,6 +149,8 @@ tests/test_tools.py
|
|
|
149
149
|
tests/test_trace.py
|
|
150
150
|
tests/test_tutorial_snippets.py
|
|
151
151
|
tests/test_v1_0_1_patches.py
|
|
152
|
+
tests/test_v1_0_3_behavior_failed_ux.py
|
|
153
|
+
tests/test_v1_0_3_tool_multiturn.py
|
|
152
154
|
tests/test_version_sync.py
|
|
153
155
|
tests/test_view.py
|
|
154
156
|
tests/test_wheel_completeness.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "activegraph"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
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"
|