activegraph 1.2.0__tar.gz → 1.4.0__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.2.0/activegraph.egg-info → activegraph-1.4.0}/PKG-INFO +1 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/__init__.py +13 -1
- activegraph-1.4.0/activegraph/_signature.py +222 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/behaviors/base.py +35 -6
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/behaviors/decorators.py +43 -8
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/cli/main.py +121 -2
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/clock.py +21 -4
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/event.py +10 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/graph.py +52 -13
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/ids.py +9 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/patch.py +11 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/errors.py +22 -3
- activegraph-1.4.0/activegraph/frame.py +24 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/__init__.py +8 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/anthropic.py +86 -13
- activegraph-1.4.0/activegraph/llm/embedding.py +94 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/errors.py +38 -0
- activegraph-1.4.0/activegraph/llm/native.py +173 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/openai.py +114 -20
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/prompt.py +24 -2
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/provider.py +16 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/recorded.py +46 -2
- activegraph-1.4.0/activegraph/llm/wire.py +140 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/logging.py +4 -4
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/migration.py +26 -7
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/otel.py +2 -2
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/prometheus.py +4 -4
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/status.py +11 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/__init__.py +148 -26
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/settings.py +1 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/loader.py +21 -0
- activegraph-1.4.0/activegraph/packs/manifest.py +593 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/policy.py +11 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/diff.py +32 -4
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/exec_errors.py +117 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/patterns.py +20 -10
- activegraph-1.4.0/activegraph/runtime/promote.py +411 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/registration_errors.py +4 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/runtime.py +841 -60
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/base.py +23 -3
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/falkordb.py +2 -2
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/postgres.py +3 -3
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/serde.py +2 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/sqlite.py +10 -3
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/url.py +8 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/base.py +14 -2
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/context.py +11 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/decorators.py +24 -3
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/trace/printer.py +85 -0
- {activegraph-1.2.0 → activegraph-1.4.0/activegraph.egg-info}/PKG-INFO +1 -1
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph.egg-info/SOURCES.txt +14 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/pyproject.toml +45 -6
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_cli.py +118 -0
- activegraph-1.4.0/tests/test_disable_pack.py +155 -0
- activegraph-1.4.0/tests/test_embedding_provider.py +97 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_anthropic.py +100 -0
- activegraph-1.4.0/tests/test_llm_native_structured_output.py +466 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_openai.py +183 -4
- activegraph-1.4.0/tests/test_llm_wire.py +186 -0
- activegraph-1.4.0/tests/test_pack_manifest.py +408 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_persistence.py +75 -0
- activegraph-1.4.0/tests/test_promote.py +821 -0
- activegraph-1.4.0/tests/test_registration_validation.py +301 -0
- activegraph-1.4.0/tests/test_trace_accessors.py +75 -0
- activegraph-1.2.0/activegraph/frame.py +0 -15
- {activegraph-1.2.0 → activegraph-1.4.0}/LICENSE +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/NOTICE +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/README.md +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/__main__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/behaviors/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/cli/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/cli/quickstart.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/cli/renderers.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/graph_store.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/core/view.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/cache.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/parsing.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/llm/types.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/observability/metrics.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/behaviors.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/object_types.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/diligence/tools.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/packs/scaffold.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/_live.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/behavior_graph.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/budget.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/config_errors.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/errors.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/queue.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/registry.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/scheduler.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/runtime/view_builder.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/conformance.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/errors.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/graph_conformance.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/store/memory.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/cache.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/errors.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/graph_query.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/recorded.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/tools/web_fetch.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/trace/__init__.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph/trace/causal.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph.egg-info/dependency_links.txt +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph.egg-info/entry_points.txt +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph.egg-info/requires.txt +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/activegraph.egg-info/top_level.txt +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/setup.cfg +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_activate_after.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_causal_cross_tool.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_cli_docs_flags.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_clock.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_diff.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_diligence_pack.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_diligence_with_tools.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_doc_links.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_doc_python_snippets.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_doc_site_reachable.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_errors_format.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_event.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_falkordb_store.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_fork.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_graph.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_graph_store.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_ids.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_license.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_behavior.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_budget.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_causal.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_claim_extraction.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_default_model.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_determinism.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_failure.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_prompt.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_provider_fixtures.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_replay.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_tool_loop.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_trace.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_trace_snapshot.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llm_types.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_llms_txt.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_migration.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_observability_logging.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_observability_metrics.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_operate_example.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_pack_scaffold.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_packs.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_patch.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_pattern_matcher.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_pattern_parser.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_pattern_subscriptions.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_postgres_store.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_quickstart.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_quickstart_snapshot.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_reason_codes_docs.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_replay.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_replay_trace_snapshot.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_requeue_unfired.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_resume_example.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_runtime.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_runtime_status.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_serde.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_store_conformance.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_store_url.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_tool_replay.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_tool_trace_snapshot.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_tools.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_trace.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_tutorial_snippets.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_v1_0_1_patches.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_version_sync.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_view.py +0 -0
- {activegraph-1.2.0 → activegraph-1.4.0}/tests/test_wheel_completeness.py +0 -0
|
@@ -42,6 +42,11 @@ from activegraph.llm.errors import LLMBehaviorError, MissingProviderError
|
|
|
42
42
|
from activegraph.policy import Policy
|
|
43
43
|
from activegraph.runtime.budget import Budget
|
|
44
44
|
from activegraph.runtime.diff import Diff, DivergentObject, DivergentRelation
|
|
45
|
+
from activegraph.runtime.promote import (
|
|
46
|
+
PromoteConflict,
|
|
47
|
+
PromotePlan,
|
|
48
|
+
PromoteResult,
|
|
49
|
+
)
|
|
45
50
|
from activegraph.runtime.config_errors import (
|
|
46
51
|
IncompatibleRuntimeState,
|
|
47
52
|
InvalidArgumentType,
|
|
@@ -52,6 +57,8 @@ from activegraph.runtime.exec_errors import (
|
|
|
52
57
|
ApprovalNotFoundError,
|
|
53
58
|
InternalEvaluatorError,
|
|
54
59
|
InvalidPatchLifecycleState,
|
|
60
|
+
PromoteConflictError,
|
|
61
|
+
PromoteLineageError,
|
|
55
62
|
RuntimeContextRequiredError,
|
|
56
63
|
)
|
|
57
64
|
from activegraph.runtime.patterns import UnsupportedPatternError
|
|
@@ -190,6 +197,11 @@ __all__ = [
|
|
|
190
197
|
"PendingApproval",
|
|
191
198
|
"Policy",
|
|
192
199
|
"PrometheusMetrics",
|
|
200
|
+
"PromoteConflict",
|
|
201
|
+
"PromoteConflictError",
|
|
202
|
+
"PromoteLineageError",
|
|
203
|
+
"PromotePlan",
|
|
204
|
+
"PromoteResult",
|
|
193
205
|
"Relation",
|
|
194
206
|
"RelationBehavior",
|
|
195
207
|
"RegistrationError",
|
|
@@ -230,4 +242,4 @@ __all__ = [
|
|
|
230
242
|
"tool",
|
|
231
243
|
]
|
|
232
244
|
|
|
233
|
-
__version__ = "1.
|
|
245
|
+
__version__ = "1.4.0"
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Registration-time handler-signature validation.
|
|
2
|
+
|
|
3
|
+
Shared by the ``@behavior`` / ``@relation_behavior`` / ``@llm_behavior``
|
|
4
|
+
decorators (both the global registry in
|
|
5
|
+
:mod:`activegraph.behaviors.decorators` and the pack-scoped variants in
|
|
6
|
+
:mod:`activegraph.packs`) and the ``@tool`` decorators.
|
|
7
|
+
|
|
8
|
+
Rationale (July 2026 agent-readiness review): a handler with the wrong
|
|
9
|
+
arity — ``def h(ctx)``, ``def h(event, ctx)`` — previously registered
|
|
10
|
+
fine and failed at first invocation with a ``TypeError`` swallowed into
|
|
11
|
+
a ``behavior.failed`` event, painful to notice during pack development.
|
|
12
|
+
Validating at decoration time names the mistake at the line that made
|
|
13
|
+
it, matching the precedent set by ``@llm_behavior``'s
|
|
14
|
+
``output_schema=`` strict validation (CONTRACT v1.0.3 #2).
|
|
15
|
+
|
|
16
|
+
The check is deliberately permissive:
|
|
17
|
+
|
|
18
|
+
* Objects without an inspectable signature (builtins, some callables,
|
|
19
|
+
mocks with ``(*args, **kwargs)``) pass through unvalidated — the
|
|
20
|
+
goal is catching the common authoring mistake, not gatekeeping
|
|
21
|
+
every callable Python can construct.
|
|
22
|
+
* ``*args`` satisfies any positional arity.
|
|
23
|
+
* Extra parameters beyond the contract are allowed when the call can
|
|
24
|
+
still succeed: they need a default, or — for behaviors, where the
|
|
25
|
+
pack loader injects typed settings by keyword
|
|
26
|
+
(``packs/loader.py:_wrap_with_injection``) — a type annotation
|
|
27
|
+
that marks them as injection candidates.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import inspect
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_handler_signature(
|
|
37
|
+
fn: Any,
|
|
38
|
+
*,
|
|
39
|
+
expected_params: tuple[str, ...],
|
|
40
|
+
decorator: str,
|
|
41
|
+
allow_annotated_extras: bool,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Raise ``TypeError`` when ``fn`` cannot be invoked with the
|
|
44
|
+
decorator's positional calling convention.
|
|
45
|
+
|
|
46
|
+
``expected_params`` names the contract's positional parameters
|
|
47
|
+
(documentation for the error message; only arity is checked, not
|
|
48
|
+
names). ``decorator`` is the user-facing decorator name for the
|
|
49
|
+
message (e.g. ``"@tool"``). ``allow_annotated_extras=True`` treats
|
|
50
|
+
a type annotation on an extra parameter as evidence it will be
|
|
51
|
+
keyword-injected at call time (the pack settings pattern
|
|
52
|
+
``*, settings: MyPackSettings``); ``False`` (tools) requires extras
|
|
53
|
+
to carry defaults, because tools are always invoked as exactly
|
|
54
|
+
``fn(args, ctx)``.
|
|
55
|
+
|
|
56
|
+
Callables whose signature cannot be inspected are skipped, and
|
|
57
|
+
``*args`` / ``**kwargs`` satisfy the positional / extra checks
|
|
58
|
+
respectively — the validation is a loud guard for the common
|
|
59
|
+
mistake, not an exhaustive gate.
|
|
60
|
+
"""
|
|
61
|
+
if not callable(fn):
|
|
62
|
+
raise TypeError(
|
|
63
|
+
f"{decorator} must decorate a callable, got "
|
|
64
|
+
f"{type(fn).__name__}."
|
|
65
|
+
)
|
|
66
|
+
try:
|
|
67
|
+
sig = inspect.signature(fn)
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
params = list(sig.parameters.values())
|
|
72
|
+
has_var_positional = any(
|
|
73
|
+
p.kind is inspect.Parameter.VAR_POSITIONAL for p in params
|
|
74
|
+
)
|
|
75
|
+
positional = [
|
|
76
|
+
p
|
|
77
|
+
for p in params
|
|
78
|
+
if p.kind
|
|
79
|
+
in (
|
|
80
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
81
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
82
|
+
)
|
|
83
|
+
]
|
|
84
|
+
n_expected = len(expected_params)
|
|
85
|
+
expected_sig = f"({', '.join(expected_params)})"
|
|
86
|
+
|
|
87
|
+
if len(positional) < n_expected and not has_var_positional:
|
|
88
|
+
found = (
|
|
89
|
+
f"({', '.join(p.name for p in positional)})"
|
|
90
|
+
if positional
|
|
91
|
+
else "()"
|
|
92
|
+
)
|
|
93
|
+
raise TypeError(
|
|
94
|
+
f"{decorator} function {_name_of(fn)!r} must accept "
|
|
95
|
+
f"{n_expected} positional parameters {expected_sig}, but its "
|
|
96
|
+
f"signature is {found}.\n"
|
|
97
|
+
f"\n"
|
|
98
|
+
f"The runtime invokes it as fn{expected_sig}. "
|
|
99
|
+
f"{_extra_hint(decorator)}\n"
|
|
100
|
+
f"\n"
|
|
101
|
+
f"Example:\n"
|
|
102
|
+
f" {decorator}(...)\n"
|
|
103
|
+
f" def {_example_name(fn)}{expected_sig}: ..."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
problems: list[str] = []
|
|
107
|
+
for p in positional[n_expected:]:
|
|
108
|
+
if _extra_is_satisfiable(p, allow_annotated_extras):
|
|
109
|
+
continue
|
|
110
|
+
problems.append(p.name)
|
|
111
|
+
for p in params:
|
|
112
|
+
if p.kind is not inspect.Parameter.KEYWORD_ONLY:
|
|
113
|
+
continue
|
|
114
|
+
if _extra_is_satisfiable(p, allow_annotated_extras):
|
|
115
|
+
continue
|
|
116
|
+
problems.append(p.name)
|
|
117
|
+
|
|
118
|
+
if problems:
|
|
119
|
+
if allow_annotated_extras:
|
|
120
|
+
fix = (
|
|
121
|
+
"give it a default value, or annotate it with your "
|
|
122
|
+
"pack's settings class so the pack loader injects it "
|
|
123
|
+
"(e.g. `*, settings: MyPackSettings`)"
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
fix = "give it a default value or remove it"
|
|
127
|
+
raise TypeError(
|
|
128
|
+
f"{decorator} function {_name_of(fn)!r} has required "
|
|
129
|
+
f"parameter(s) beyond the {expected_sig} contract that the "
|
|
130
|
+
f"runtime will never pass: {', '.join(problems)}.\n"
|
|
131
|
+
f"\n"
|
|
132
|
+
f"The runtime invokes it as fn{expected_sig}, so every "
|
|
133
|
+
f"extra parameter must be optional at call time — {fix}."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def infer_tool_input_schema(fn: Any) -> Any:
|
|
138
|
+
"""Infer a tool's ``input_schema`` from its first parameter's
|
|
139
|
+
type annotation, or return ``None``.
|
|
140
|
+
|
|
141
|
+
v1.3: with ``input_schema=None``, ``@tool`` previously sent the
|
|
142
|
+
model an empty parameters schema (``{"type": "object",
|
|
143
|
+
"properties": {}}``) even when the function body plainly declared
|
|
144
|
+
its shape via ``def fetch(args: FetchArgs, ctx)``. Inference
|
|
145
|
+
closes that gap: when the first positional parameter is annotated
|
|
146
|
+
with a Pydantic ``BaseModel`` subclass, that class becomes the
|
|
147
|
+
tool's ``input_schema`` — the model sees real parameters and the
|
|
148
|
+
runtime validates arguments before invocation, exactly as if
|
|
149
|
+
``input_schema=FetchArgs`` had been passed explicitly.
|
|
150
|
+
|
|
151
|
+
Anything else — no annotation, a non-model annotation
|
|
152
|
+
(``dict``, ``Any``), an uninspectable callable — returns ``None``
|
|
153
|
+
and the tool behaves exactly as before. Explicit ``input_schema=``
|
|
154
|
+
always wins; the decorators only call this when it was omitted.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
sig = inspect.signature(fn)
|
|
158
|
+
except (TypeError, ValueError):
|
|
159
|
+
return None
|
|
160
|
+
positional = [
|
|
161
|
+
p
|
|
162
|
+
for p in sig.parameters.values()
|
|
163
|
+
if p.kind
|
|
164
|
+
in (
|
|
165
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
166
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
167
|
+
)
|
|
168
|
+
]
|
|
169
|
+
if not positional:
|
|
170
|
+
return None
|
|
171
|
+
annotation = positional[0].annotation
|
|
172
|
+
if annotation is inspect.Parameter.empty:
|
|
173
|
+
return None
|
|
174
|
+
# PEP 563 / `from __future__ import annotations` leaves string
|
|
175
|
+
# annotations; resolve through get_type_hints (module-global
|
|
176
|
+
# visibility), the same tolerance the pack loader's settings
|
|
177
|
+
# injection applies.
|
|
178
|
+
if isinstance(annotation, str):
|
|
179
|
+
import typing
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
hints = typing.get_type_hints(fn)
|
|
183
|
+
except Exception:
|
|
184
|
+
return None
|
|
185
|
+
annotation = hints.get(positional[0].name)
|
|
186
|
+
if annotation is None:
|
|
187
|
+
return None
|
|
188
|
+
try:
|
|
189
|
+
from pydantic import BaseModel
|
|
190
|
+
except ImportError: # pragma: no cover — Pydantic is a hard dep
|
|
191
|
+
return None
|
|
192
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
193
|
+
return annotation
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _extra_is_satisfiable(
|
|
198
|
+
p: inspect.Parameter, allow_annotated_extras: bool
|
|
199
|
+
) -> bool:
|
|
200
|
+
if p.default is not inspect.Parameter.empty:
|
|
201
|
+
return True
|
|
202
|
+
if allow_annotated_extras and p.annotation is not inspect.Parameter.empty:
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _name_of(fn: Any) -> str:
|
|
208
|
+
return getattr(fn, "__name__", repr(fn))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _example_name(fn: Any) -> str:
|
|
212
|
+
name = getattr(fn, "__name__", "my_handler")
|
|
213
|
+
return name if name.isidentifier() else "my_handler"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _extra_hint(decorator: str) -> str:
|
|
217
|
+
if decorator == "@tool":
|
|
218
|
+
return (
|
|
219
|
+
"`args` is the validated input model (or a plain dict when "
|
|
220
|
+
"no input_schema is set) and `ctx` is the ToolContext."
|
|
221
|
+
)
|
|
222
|
+
return "See the behavior-handler contract in docs/concepts/behaviors."
|
|
@@ -20,12 +20,15 @@ from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
from activegraph.core.event import Event
|
|
23
|
-
from activegraph.core.graph import Graph
|
|
23
|
+
from activegraph.core.graph import Graph, Relation
|
|
24
24
|
from activegraph.frame import Frame
|
|
25
25
|
from activegraph.llm.prompt import AssembledPrompt
|
|
26
|
+
from activegraph.runtime.runtime import Context
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def _llm_behavior_fn_placeholder(
|
|
29
|
+
def _llm_behavior_fn_placeholder(
|
|
30
|
+
event: "Event", graph: "Graph", ctx: "Context"
|
|
31
|
+
) -> None: # pragma: no cover
|
|
29
32
|
raise RuntimeError(
|
|
30
33
|
"LLMBehavior.fn invoked directly. The runtime owns LLM behavior "
|
|
31
34
|
"invocation via _invoke_llm; calling .run() bypasses prompt "
|
|
@@ -35,6 +38,18 @@ def _llm_behavior_fn_placeholder(event, graph, ctx) -> None: # pragma: no cover
|
|
|
35
38
|
|
|
36
39
|
@dataclass
|
|
37
40
|
class Behavior:
|
|
41
|
+
"""Metadata plus the callable for an event-driven behavior.
|
|
42
|
+
|
|
43
|
+
A Behavior is data, not magic: ``on`` (event types), ``where``
|
|
44
|
+
(payload filter), ``pattern`` (Cypher-subset subscription),
|
|
45
|
+
``activate_after`` (event-count delay), and ``view_spec`` (what
|
|
46
|
+
the runtime builds into ``ctx.view``) describe *when* it fires;
|
|
47
|
+
the 3-arg ``fn`` — invoked as ``fn(event, graph, ctx)`` — is
|
|
48
|
+
*what* runs. Instances come from ``@behavior`` or a pack; the
|
|
49
|
+
runtime introspects the metadata to match events, and nothing
|
|
50
|
+
here executes on its own.
|
|
51
|
+
"""
|
|
52
|
+
|
|
38
53
|
name: str
|
|
39
54
|
fn: Callable[..., None]
|
|
40
55
|
on: list[str] = field(default_factory=list)
|
|
@@ -51,12 +66,22 @@ class Behavior:
|
|
|
51
66
|
# CONTRACT v0.7 #13. Event-count delay. None = fire immediately.
|
|
52
67
|
activate_after: Optional[int] = None
|
|
53
68
|
|
|
54
|
-
def run(self, event, graph, ctx) -> None:
|
|
69
|
+
def run(self, event: "Event", graph: "Graph", ctx: "Context") -> None:
|
|
55
70
|
self.fn(event, graph, ctx)
|
|
56
71
|
|
|
57
72
|
|
|
58
73
|
@dataclass
|
|
59
74
|
class RelationBehavior:
|
|
75
|
+
"""A behavior that fires once per matching relation edge.
|
|
76
|
+
|
|
77
|
+
Same metadata surface as :class:`Behavior` plus
|
|
78
|
+
``relation_type``: when a triggering event touches a relation of
|
|
79
|
+
that type, the 4-arg ``fn`` runs as
|
|
80
|
+
``fn(relation, event, graph, ctx)`` — once per (event, relation)
|
|
81
|
+
pair. Deliberately not a subclass of ``Behavior``; the runtime
|
|
82
|
+
dispatches the two kinds separately.
|
|
83
|
+
"""
|
|
84
|
+
|
|
60
85
|
name: str
|
|
61
86
|
fn: Callable[..., None]
|
|
62
87
|
relation_type: str
|
|
@@ -72,7 +97,9 @@ class RelationBehavior:
|
|
|
72
97
|
pattern_matcher: Any = None
|
|
73
98
|
activate_after: Optional[int] = None
|
|
74
99
|
|
|
75
|
-
def run(
|
|
100
|
+
def run(
|
|
101
|
+
self, relation: "Relation", event: "Event", graph: "Graph", ctx: "Context"
|
|
102
|
+
) -> None:
|
|
76
103
|
self.fn(relation, event, graph, ctx)
|
|
77
104
|
|
|
78
105
|
|
|
@@ -98,7 +125,7 @@ class LLMBehavior(Behavior):
|
|
|
98
125
|
# at registration time. Existing call sites that pass an explicit
|
|
99
126
|
# string keep working unchanged.
|
|
100
127
|
model: Optional[str] = None
|
|
101
|
-
output_schema: Optional[type] = None
|
|
128
|
+
output_schema: Optional[type[Any]] = None
|
|
102
129
|
deterministic: bool = False
|
|
103
130
|
max_tokens: int = 4096
|
|
104
131
|
temperature: float = 0.7
|
|
@@ -106,7 +133,7 @@ class LLMBehavior(Behavior):
|
|
|
106
133
|
timeout_seconds: float = 60.0
|
|
107
134
|
prompt_template: Optional[str] = None
|
|
108
135
|
# v0.7
|
|
109
|
-
tools: list = field(default_factory=list)
|
|
136
|
+
tools: list[Any] = field(default_factory=list) # Tool | str names
|
|
110
137
|
max_tool_turns: int = 6
|
|
111
138
|
|
|
112
139
|
def build_prompt(
|
|
@@ -115,6 +142,7 @@ class LLMBehavior(Behavior):
|
|
|
115
142
|
graph: "Graph",
|
|
116
143
|
*,
|
|
117
144
|
frame: Optional["Frame"] = None,
|
|
145
|
+
structured_output_mode: str = "prompt",
|
|
118
146
|
) -> "AssembledPrompt":
|
|
119
147
|
"""Assemble the prompt that would be sent for this event.
|
|
120
148
|
|
|
@@ -161,4 +189,5 @@ class LLMBehavior(Behavior):
|
|
|
161
189
|
top_p=self.top_p,
|
|
162
190
|
deterministic=self.deterministic,
|
|
163
191
|
prompt_template=self.prompt_template,
|
|
192
|
+
structured_output_mode=structured_output_mode,
|
|
164
193
|
)
|
|
@@ -94,7 +94,15 @@ def clear_registry() -> list[Union[Behavior, RelationBehavior]]:
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def get_registry() -> list[Union[Behavior, RelationBehavior]]:
|
|
97
|
-
"""Snapshot of the global behavior registry (a shallow copy).
|
|
97
|
+
"""Snapshot of the global behavior registry (a shallow copy).
|
|
98
|
+
|
|
99
|
+
Decoration with ``@behavior`` / ``@relation_behavior`` /
|
|
100
|
+
``@llm_behavior`` appends to a module-level list; this returns a
|
|
101
|
+
copy, so callers can filter or iterate without mutating
|
|
102
|
+
registration state. ``Runtime`` snapshots it at construction when
|
|
103
|
+
``behaviors=`` is omitted — later registrations don't leak into
|
|
104
|
+
already-built runtimes.
|
|
105
|
+
"""
|
|
98
106
|
return list(_REGISTRY)
|
|
99
107
|
|
|
100
108
|
|
|
@@ -152,7 +160,7 @@ def behavior(
|
|
|
152
160
|
*,
|
|
153
161
|
pattern: Optional[str] = None,
|
|
154
162
|
activate_after: Any = None,
|
|
155
|
-
) -> Callable[[Callable], Behavior]:
|
|
163
|
+
) -> Callable[[Callable[..., None]], Behavior]:
|
|
156
164
|
"""Decorate a function as an event-driven behavior.
|
|
157
165
|
|
|
158
166
|
v0.7 additions (both keyword-only):
|
|
@@ -175,7 +183,16 @@ def behavior(
|
|
|
175
183
|
if activate_after is not None:
|
|
176
184
|
delay_n = _parse_aa(activate_after)
|
|
177
185
|
|
|
178
|
-
def wrap(fn: Callable) -> Behavior:
|
|
186
|
+
def wrap(fn: Callable[..., None]) -> Behavior:
|
|
187
|
+
# v1.3: arity check at decoration time (see activegraph/_signature.py).
|
|
188
|
+
from activegraph._signature import validate_handler_signature
|
|
189
|
+
|
|
190
|
+
validate_handler_signature(
|
|
191
|
+
fn,
|
|
192
|
+
expected_params=("event", "graph", "ctx"),
|
|
193
|
+
decorator="@behavior",
|
|
194
|
+
allow_annotated_extras=True,
|
|
195
|
+
)
|
|
179
196
|
b = Behavior(
|
|
180
197
|
name=name or fn.__name__,
|
|
181
198
|
fn=fn,
|
|
@@ -215,9 +232,9 @@ def llm_behavior(
|
|
|
215
232
|
priority: int = 0,
|
|
216
233
|
pattern: Optional[str] = None,
|
|
217
234
|
activate_after: Any = None,
|
|
218
|
-
tools: Optional[list] = None,
|
|
235
|
+
tools: Optional[list[Any]] = None,
|
|
219
236
|
max_tool_turns: int = 6,
|
|
220
|
-
) -> Callable[[Callable], LLMBehavior]:
|
|
237
|
+
) -> Callable[[Callable[..., None]], LLMBehavior]:
|
|
221
238
|
"""Decorate a function as an LLM-driven behavior.
|
|
222
239
|
|
|
223
240
|
The decorated function's signature is
|
|
@@ -281,7 +298,16 @@ def llm_behavior(
|
|
|
281
298
|
# call; failing here names the cause at the @llm_behavior line.
|
|
282
299
|
_validate_output_schema(output_schema)
|
|
283
300
|
|
|
284
|
-
def wrap(fn: Callable) -> LLMBehavior:
|
|
301
|
+
def wrap(fn: Callable[..., None]) -> LLMBehavior:
|
|
302
|
+
# v1.3: arity check at decoration time (see activegraph/_signature.py).
|
|
303
|
+
from activegraph._signature import validate_handler_signature
|
|
304
|
+
|
|
305
|
+
validate_handler_signature(
|
|
306
|
+
fn,
|
|
307
|
+
expected_params=("event", "graph", "ctx", "llm_output"),
|
|
308
|
+
decorator="@llm_behavior",
|
|
309
|
+
allow_annotated_extras=True,
|
|
310
|
+
)
|
|
285
311
|
b = LLMBehavior(
|
|
286
312
|
name=name or fn.__name__,
|
|
287
313
|
fn=_llm_behavior_fn_placeholder,
|
|
@@ -331,7 +357,7 @@ def relation_behavior(
|
|
|
331
357
|
*,
|
|
332
358
|
pattern: Optional[str] = None,
|
|
333
359
|
activate_after: Any = None,
|
|
334
|
-
) -> Callable[[Callable], RelationBehavior]:
|
|
360
|
+
) -> Callable[[Callable[..., None]], RelationBehavior]:
|
|
335
361
|
"""Decorate a function as a relation behavior — fires once per matching edge.
|
|
336
362
|
|
|
337
363
|
v0.7: also accepts `pattern=` and `activate_after=` per CONTRACT
|
|
@@ -348,7 +374,16 @@ def relation_behavior(
|
|
|
348
374
|
if activate_after is not None:
|
|
349
375
|
delay_n = _parse_aa(activate_after)
|
|
350
376
|
|
|
351
|
-
def wrap(fn: Callable) -> RelationBehavior:
|
|
377
|
+
def wrap(fn: Callable[..., None]) -> RelationBehavior:
|
|
378
|
+
# v1.3: arity check at decoration time (see activegraph/_signature.py).
|
|
379
|
+
from activegraph._signature import validate_handler_signature
|
|
380
|
+
|
|
381
|
+
validate_handler_signature(
|
|
382
|
+
fn,
|
|
383
|
+
expected_params=("relation", "event", "graph", "ctx"),
|
|
384
|
+
decorator="@relation_behavior",
|
|
385
|
+
allow_annotated_extras=True,
|
|
386
|
+
)
|
|
352
387
|
rb = RelationBehavior(
|
|
353
388
|
name=name or fn.__name__,
|
|
354
389
|
fn=fn,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""activegraph CLI entry point. CONTRACT v0.8 #12–#13.
|
|
2
2
|
|
|
3
|
-
Subcommands: inspect, replay, fork, diff, export-trace, migrate.
|
|
3
|
+
Subcommands: inspect, replay, fork, diff, promote, export-trace, migrate.
|
|
4
4
|
|
|
5
5
|
Each one is a thin wrapper around a library API. The CLI does no
|
|
6
6
|
business logic — it parses arguments, calls into Python, and formats
|
|
@@ -129,7 +129,7 @@ def _list_runs_or_die(url: str):
|
|
|
129
129
|
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
130
130
|
@click.version_option(message="activegraph %(version)s")
|
|
131
131
|
def cli() -> None:
|
|
132
|
-
"""Inspect, replay, fork, diff, export, and migrate activegraph runs."""
|
|
132
|
+
"""Inspect, replay, fork, diff, promote, export, and migrate activegraph runs."""
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
# ---- quickstart ---------------------------------------------------------
|
|
@@ -866,6 +866,125 @@ def cmd_diff(url: str, run_a: str, run_b: str, as_json: bool) -> None:
|
|
|
866
866
|
click.echo(f" - {r.summary()}")
|
|
867
867
|
|
|
868
868
|
|
|
869
|
+
# ---- promote ------------------------------------------------------------
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
@cli.command("promote")
|
|
873
|
+
@click.argument("url")
|
|
874
|
+
@click.option("--run-id", required=True, help="Destination (parent) run.")
|
|
875
|
+
@click.option("--from-run", required=True, help="Source (fork) run.")
|
|
876
|
+
@click.option(
|
|
877
|
+
"--dry-run",
|
|
878
|
+
is_flag=True,
|
|
879
|
+
help="Compute and print the plan without applying anything.",
|
|
880
|
+
)
|
|
881
|
+
@click.option("--json", "as_json", is_flag=True, help="Machine-readable output.")
|
|
882
|
+
def cmd_promote(
|
|
883
|
+
url: str, run_id: str, from_run: str, dry_run: bool, as_json: bool
|
|
884
|
+
) -> None:
|
|
885
|
+
"""Apply a fork's net structural delta to its parent (CONTRACT v1.3 #4).
|
|
886
|
+
|
|
887
|
+
Fail-closed and atomic: any conflict between the fork's delta and
|
|
888
|
+
the parent's own post-fork changes aborts with nothing applied
|
|
889
|
+
(exit 5, the divergence code) — a conflicted --dry-run also exits
|
|
890
|
+
5 so scripts can gate on it. Fork lineage is verified from the
|
|
891
|
+
store's runs table.
|
|
892
|
+
"""
|
|
893
|
+
from activegraph.runtime.exec_errors import (
|
|
894
|
+
PromoteConflictError,
|
|
895
|
+
PromoteLineageError,
|
|
896
|
+
)
|
|
897
|
+
from activegraph.runtime.runtime import Runtime
|
|
898
|
+
from activegraph.store.url import InvalidStoreURL, parse_store_url
|
|
899
|
+
|
|
900
|
+
try:
|
|
901
|
+
parse_store_url(url)
|
|
902
|
+
except InvalidStoreURL as e:
|
|
903
|
+
click.echo(str(e), err=True)
|
|
904
|
+
raise SystemExit(EXIT_USAGE_ERROR)
|
|
905
|
+
|
|
906
|
+
# Validate both run ids against the runs table BEFORE Runtime.load:
|
|
907
|
+
# load upserts a run row for whatever id it's given, so loading a
|
|
908
|
+
# mistyped id would insert a phantom empty run and then fail with
|
|
909
|
+
# a misleading lineage error.
|
|
910
|
+
known_runs = {r.run_id for r in _list_runs_or_die(url)}
|
|
911
|
+
for label_, rid in (("--run-id", run_id), ("--from-run", from_run)):
|
|
912
|
+
if rid not in known_runs:
|
|
913
|
+
click.echo(
|
|
914
|
+
f"{label_} {rid!r}: no such run in {url} "
|
|
915
|
+
f"(activegraph inspect {url} --runs lists them)",
|
|
916
|
+
err=True,
|
|
917
|
+
)
|
|
918
|
+
raise SystemExit(EXIT_NOT_FOUND)
|
|
919
|
+
|
|
920
|
+
try:
|
|
921
|
+
parent = Runtime.load(url, run_id=run_id)
|
|
922
|
+
fork = Runtime.load(url, run_id=from_run)
|
|
923
|
+
except FileNotFoundError as e:
|
|
924
|
+
click.echo(str(e), err=True)
|
|
925
|
+
raise SystemExit(EXIT_NOT_FOUND)
|
|
926
|
+
except __import__("sqlite3").OperationalError as e:
|
|
927
|
+
click.echo(f"{url}: {e}", err=True)
|
|
928
|
+
raise SystemExit(EXIT_NOT_FOUND)
|
|
929
|
+
|
|
930
|
+
try:
|
|
931
|
+
outcome = parent.promote(fork, dry_run=dry_run)
|
|
932
|
+
except PromoteLineageError as e:
|
|
933
|
+
click.echo(str(e), err=True)
|
|
934
|
+
raise SystemExit(EXIT_NOT_FOUND)
|
|
935
|
+
except PromoteConflictError as e:
|
|
936
|
+
click.echo(str(e), err=True)
|
|
937
|
+
raise SystemExit(EXIT_DIVERGENCE)
|
|
938
|
+
|
|
939
|
+
plan = outcome if dry_run else outcome.plan
|
|
940
|
+
summary = {
|
|
941
|
+
"from_run": plan.from_run,
|
|
942
|
+
"into_run": plan.into_run,
|
|
943
|
+
"forked_at_event": plan.forked_at_event,
|
|
944
|
+
"computed_against": plan.computed_against,
|
|
945
|
+
"dry_run": dry_run,
|
|
946
|
+
"objects_created": [o["id"] for o in plan.object_creates],
|
|
947
|
+
"objects_patched": [o["id"] for o in plan.object_patches],
|
|
948
|
+
"objects_removed": list(plan.object_removes),
|
|
949
|
+
"relations_created": [r["id"] for r in plan.relation_creates],
|
|
950
|
+
"relations_removed": list(plan.relation_removes),
|
|
951
|
+
"conflicts": [
|
|
952
|
+
{"kind": c.kind, "entity": c.entity, "id": c.id, "detail": c.detail}
|
|
953
|
+
for c in plan.conflicts
|
|
954
|
+
],
|
|
955
|
+
"warnings": list(plan.warnings),
|
|
956
|
+
}
|
|
957
|
+
if not dry_run:
|
|
958
|
+
summary["marker_event_id"] = outcome.marker_event_id
|
|
959
|
+
summary["applied_event_ids"] = list(outcome.applied_event_ids)
|
|
960
|
+
if as_json:
|
|
961
|
+
click.echo(_json.dumps(summary))
|
|
962
|
+
if dry_run and plan.conflicts:
|
|
963
|
+
raise SystemExit(EXIT_DIVERGENCE)
|
|
964
|
+
return
|
|
965
|
+
verb = "would promote" if dry_run else "promoted"
|
|
966
|
+
click.echo(f"{verb} {plan.from_run} -> {plan.into_run}:")
|
|
967
|
+
for key in (
|
|
968
|
+
"objects_created",
|
|
969
|
+
"objects_patched",
|
|
970
|
+
"objects_removed",
|
|
971
|
+
"relations_created",
|
|
972
|
+
"relations_removed",
|
|
973
|
+
):
|
|
974
|
+
ids = summary[key]
|
|
975
|
+
if ids:
|
|
976
|
+
click.echo(f" {key:20s} {', '.join(ids)}")
|
|
977
|
+
for w in plan.warnings:
|
|
978
|
+
click.echo(f" warning: {w}")
|
|
979
|
+
if dry_run and plan.conflicts:
|
|
980
|
+
click.echo("conflicts (promote would fail):")
|
|
981
|
+
for c in plan.conflicts:
|
|
982
|
+
click.echo(f" - [{c.kind}] {c.entity} {c.id}: {c.detail}")
|
|
983
|
+
# Scripts gate on the exit code: a conflicted dry run is the
|
|
984
|
+
# same signal as a conflicted apply, minus the mutation.
|
|
985
|
+
raise SystemExit(EXIT_DIVERGENCE)
|
|
986
|
+
|
|
987
|
+
|
|
869
988
|
# ---- export-trace -------------------------------------------------------
|
|
870
989
|
|
|
871
990
|
|
|
@@ -6,7 +6,13 @@ from datetime import datetime, timezone
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Clock:
|
|
9
|
-
"""Real wall-clock UTC. ISO 8601 second precision, Z suffix.
|
|
9
|
+
"""Real wall-clock UTC. ISO 8601 second precision, Z suffix.
|
|
10
|
+
|
|
11
|
+
The default time source for event timestamps. The runtime reads
|
|
12
|
+
time only through this interface, so deterministic runs swap in
|
|
13
|
+
:class:`FrozenClock` or :class:`TickingClock` and replay never
|
|
14
|
+
depends on the machine clock.
|
|
15
|
+
"""
|
|
10
16
|
|
|
11
17
|
def now(self) -> str:
|
|
12
18
|
return (
|
|
@@ -17,7 +23,13 @@ class Clock:
|
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
class FrozenClock(Clock):
|
|
20
|
-
"""Always returns the same timestamp. For tests and snapshots.
|
|
26
|
+
"""Always returns the same timestamp. For tests and snapshots.
|
|
27
|
+
|
|
28
|
+
Every ``now()`` call yields the constructor's ``t`` unchanged, so
|
|
29
|
+
an event log written under a FrozenClock is byte-for-byte
|
|
30
|
+
reproducible. Use :class:`TickingClock` when a test needs ordering
|
|
31
|
+
across timestamps rather than strict equality.
|
|
32
|
+
"""
|
|
21
33
|
|
|
22
34
|
def __init__(self, t: str = "2026-05-15T10:32:01Z") -> None:
|
|
23
35
|
self._t = t
|
|
@@ -27,8 +39,13 @@ class FrozenClock(Clock):
|
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
class TickingClock(Clock):
|
|
30
|
-
"""Monotonically advances by
|
|
31
|
-
|
|
42
|
+
"""Monotonically advances by ``step`` seconds on every call.
|
|
43
|
+
|
|
44
|
+
For tests that care about ordering but don't want wall-clock
|
|
45
|
+
noise: timestamps increase deterministically from the start value,
|
|
46
|
+
so before/after assertions hold without sleeping or freezing time
|
|
47
|
+
entirely.
|
|
48
|
+
"""
|
|
32
49
|
|
|
33
50
|
def __init__(
|
|
34
51
|
self,
|
|
@@ -13,6 +13,16 @@ from typing import Any, Optional
|
|
|
13
13
|
|
|
14
14
|
@dataclass(frozen=True)
|
|
15
15
|
class Event:
|
|
16
|
+
"""One immutable record in the append-only log. CONTRACT #3.
|
|
17
|
+
|
|
18
|
+
An event is a fact: ``type`` names what happened, ``payload``
|
|
19
|
+
carries the data, ``actor`` says who caused it, ``caused_by``
|
|
20
|
+
links the causal parent event, and ``frame_id`` scopes it to a
|
|
21
|
+
mission frame. Events are never modified after ``emit`` —
|
|
22
|
+
objects, relations, patches, and views are all projections
|
|
23
|
+
derived from the sequence of these records (CONTRACT #2).
|
|
24
|
+
"""
|
|
25
|
+
|
|
16
26
|
id: str
|
|
17
27
|
type: str
|
|
18
28
|
payload: dict[str, Any] = field(default_factory=dict)
|