activegraph 1.4.0__tar.gz → 1.5.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.4.0/activegraph.egg-info → activegraph-1.5.0}/PKG-INFO +1 -1
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/__init__.py +1 -1
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/runtime.py +80 -0
- activegraph-1.5.0/activegraph/sandbox/__init__.py +257 -0
- activegraph-1.5.0/activegraph/sandbox/_child.py +234 -0
- activegraph-1.5.0/activegraph/store/retention.py +316 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/sqlite.py +126 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/trace/printer.py +12 -0
- {activegraph-1.4.0 → activegraph-1.5.0/activegraph.egg-info}/PKG-INFO +1 -1
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph.egg-info/SOURCES.txt +5 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/pyproject.toml +1 -1
- activegraph-1.5.0/tests/test_compaction.py +249 -0
- activegraph-1.5.0/tests/test_sandbox_trial.py +260 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/LICENSE +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/NOTICE +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/README.md +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/__main__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/_signature.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/behaviors/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/behaviors/base.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/behaviors/decorators.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/cli/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/cli/main.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/cli/quickstart.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/cli/renderers.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/clock.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/event.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/graph.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/graph_store.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/ids.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/patch.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/core/view.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/frame.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/anthropic.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/cache.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/embedding.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/native.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/openai.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/parsing.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/prompt.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/provider.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/recorded.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/types.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/llm/wire.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/logging.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/metrics.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/migration.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/otel.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/prometheus.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/observability/status.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/behaviors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/fixtures/companies.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/object_types.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/settings.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/diligence/tools.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/loader.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/manifest.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/packs/scaffold.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/policy.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/_live.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/behavior_graph.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/budget.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/config_errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/diff.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/exec_errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/patterns.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/promote.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/queue.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/registration_errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/registry.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/scheduler.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/runtime/view_builder.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/base.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/conformance.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/falkordb.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/graph_conformance.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/memory.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/postgres.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/serde.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/store/url.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/base.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/cache.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/context.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/decorators.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/errors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/graph_query.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/recorded.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/tools/web_fetch.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/trace/__init__.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph/trace/causal.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph.egg-info/dependency_links.txt +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph.egg-info/entry_points.txt +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph.egg-info/requires.txt +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/activegraph.egg-info/top_level.txt +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/setup.cfg +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_activate_after.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_causal_cross_tool.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_cli.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_cli_docs_flags.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_clock.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_diff.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_diligence_pack.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_diligence_with_tools.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_disable_pack.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_doc_links.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_doc_python_snippets.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_doc_site_reachable.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_embedding_provider.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_errors_format.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_event.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_falkordb_store.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_fork.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_graph.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_graph_store.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_ids.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_license.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_anthropic.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_behavior.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_budget.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_causal.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_claim_extraction.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_default_model.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_determinism.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_failure.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_native_structured_output.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_openai.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_prompt.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_provider_fixtures.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_replay.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_tool_loop.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_trace.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_trace_snapshot.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_types.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llm_wire.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_llms_txt.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_migration.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_observability_logging.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_observability_metrics.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_operate_example.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_pack_manifest.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_pack_scaffold.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_packs.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_patch.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_pattern_matcher.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_pattern_parser.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_pattern_subscriptions.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_persistence.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_postgres_store.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_promote.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_quickstart.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_quickstart_snapshot.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_reason_codes_docs.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_registration_validation.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_replay.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_replay_trace_snapshot.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_requeue_unfired.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_resume_example.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_runtime.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_runtime_status.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_serde.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_store_conformance.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_store_url.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_tool_replay.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_tool_trace_snapshot.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_tools.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_trace.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_trace_accessors.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_tutorial_snippets.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_v1_0_1_patches.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_v1_0_3_tool_multiturn.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_version_sync.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_view.py +0 -0
- {activegraph-1.4.0 → activegraph-1.5.0}/tests/test_wheel_completeness.py +0 -0
|
@@ -2550,6 +2550,11 @@ class Runtime:
|
|
|
2550
2550
|
store = _open_sqlite_store(path, run_id=chosen)
|
|
2551
2551
|
graph = Graph(ids=IDGen(), run_id=chosen, graph_store=graph_store)
|
|
2552
2552
|
events = list(store.iter_events())
|
|
2553
|
+
# CONTRACT v1.5 #2: a compacted run's hot log begins with its
|
|
2554
|
+
# runtime.snapshot event; project the snapshot state first so
|
|
2555
|
+
# the suffix replays against the base it was recorded on.
|
|
2556
|
+
if events and events[0].type == "runtime.snapshot":
|
|
2557
|
+
_materialize_snapshot(graph, store, events[0])
|
|
2553
2558
|
for ev in events:
|
|
2554
2559
|
graph._replay_event(ev) # noqa: SLF001 — internal seam
|
|
2555
2560
|
graph.ids.reseed_from_events(events)
|
|
@@ -2707,6 +2712,11 @@ class Runtime:
|
|
|
2707
2712
|
fork_store = SQLiteEventStore(store.path, run_id=new_run_id)
|
|
2708
2713
|
fork_graph = Graph(ids=IDGen(), run_id=new_run_id, graph_store=graph_store)
|
|
2709
2714
|
events = list(fork_store.iter_events())
|
|
2715
|
+
# CONTRACT v1.5 #2: forking a compacted parent copies its
|
|
2716
|
+
# runtime.snapshot event; the fork's base state comes from the
|
|
2717
|
+
# shared snapshot sidecar exactly as a load would.
|
|
2718
|
+
if events and events[0].type == "runtime.snapshot":
|
|
2719
|
+
_materialize_snapshot(fork_graph, fork_store, events[0])
|
|
2710
2720
|
for ev in events:
|
|
2711
2721
|
fork_graph._replay_event(ev) # noqa: SLF001
|
|
2712
2722
|
fork_graph.ids.reseed_from_events(events)
|
|
@@ -3416,6 +3426,13 @@ def _verify_replay(
|
|
|
3416
3426
|
cache = LLMCache.from_events(recorded_events)
|
|
3417
3427
|
|
|
3418
3428
|
fresh = Graph(ids=IDGen(), run_id="verify_" + loaded_graph.run_id)
|
|
3429
|
+
# CONTRACT v1.5 #2: compacted runs verify their post-snapshot
|
|
3430
|
+
# suffix against the snapshot base; the archived prefix is
|
|
3431
|
+
# verified separately by store.retention.verify_snapshot.
|
|
3432
|
+
if recorded_events and recorded_events[0].type == "runtime.snapshot":
|
|
3433
|
+
_materialize_snapshot(
|
|
3434
|
+
fresh, loaded_graph.store, recorded_events[0]
|
|
3435
|
+
)
|
|
3419
3436
|
fresh_rt = Runtime(
|
|
3420
3437
|
fresh,
|
|
3421
3438
|
behaviors=behaviors,
|
|
@@ -3538,6 +3555,69 @@ def _is_lifecycle(e: Event) -> bool:
|
|
|
3538
3555
|
)
|
|
3539
3556
|
|
|
3540
3557
|
|
|
3558
|
+
def _materialize_snapshot(graph: Graph, store: Any, snapshot_event: Event) -> None:
|
|
3559
|
+
"""Rebuild a compacted run's base state from its snapshot blob.
|
|
3560
|
+
|
|
3561
|
+
CONTRACT v1.5 #2: the blob is fetched from the store's snapshot
|
|
3562
|
+
sidecar by the hash recorded in the ``runtime.snapshot`` event,
|
|
3563
|
+
verified against that hash (fail-loud on mismatch — a corrupted
|
|
3564
|
+
sidecar must never silently produce wrong state), and projected
|
|
3565
|
+
into the graph before the post-snapshot suffix replays. Id
|
|
3566
|
+
counters recorded at compact time prime the generators so fresh
|
|
3567
|
+
mints can't collide with archived history.
|
|
3568
|
+
"""
|
|
3569
|
+
from activegraph.core.graph import Object, Relation
|
|
3570
|
+
from activegraph.store.retention import (
|
|
3571
|
+
SnapshotIntegrityError,
|
|
3572
|
+
state_hash_of,
|
|
3573
|
+
)
|
|
3574
|
+
|
|
3575
|
+
payload = snapshot_event.payload or {}
|
|
3576
|
+
expected = str(payload.get("state_hash", ""))
|
|
3577
|
+
blob = store.get_snapshot(expected) if store is not None else None
|
|
3578
|
+
if blob is None:
|
|
3579
|
+
raise SnapshotIntegrityError(
|
|
3580
|
+
run_id=graph.run_id,
|
|
3581
|
+
expected=expected,
|
|
3582
|
+
detail="the snapshot blob is missing from the sidecar table.",
|
|
3583
|
+
)
|
|
3584
|
+
if state_hash_of(blob) != expected:
|
|
3585
|
+
raise SnapshotIntegrityError(
|
|
3586
|
+
run_id=graph.run_id,
|
|
3587
|
+
expected=expected,
|
|
3588
|
+
detail="the stored blob does not hash to the recorded state hash.",
|
|
3589
|
+
)
|
|
3590
|
+
state = json.loads(blob)
|
|
3591
|
+
for o in state.get("objects", []):
|
|
3592
|
+
graph._state.put_object( # noqa: SLF001 — the replay seam
|
|
3593
|
+
Object(
|
|
3594
|
+
id=o["id"],
|
|
3595
|
+
type=o["type"],
|
|
3596
|
+
data=o["data"],
|
|
3597
|
+
version=o.get("version", 1),
|
|
3598
|
+
provenance=o.get("provenance", {}),
|
|
3599
|
+
)
|
|
3600
|
+
)
|
|
3601
|
+
for r in state.get("relations", []):
|
|
3602
|
+
graph._state.put_relation( # noqa: SLF001
|
|
3603
|
+
Relation(
|
|
3604
|
+
id=r["id"],
|
|
3605
|
+
source=r["source"],
|
|
3606
|
+
target=r["target"],
|
|
3607
|
+
type=r["type"],
|
|
3608
|
+
data=r.get("data", {}),
|
|
3609
|
+
provenance=r.get("provenance", {}),
|
|
3610
|
+
)
|
|
3611
|
+
)
|
|
3612
|
+
counters = payload.get("id_counters", {})
|
|
3613
|
+
ids = graph.ids
|
|
3614
|
+
ids._object_counter = max(ids._object_counter, int(counters.get("object", 0))) # noqa: SLF001
|
|
3615
|
+
ids._event_counter = max(ids._event_counter, int(counters.get("event", 0))) # noqa: SLF001
|
|
3616
|
+
ids._relation_counter = max(ids._relation_counter, int(counters.get("relation", 0))) # noqa: SLF001
|
|
3617
|
+
ids._patch_counter = max(ids._patch_counter, int(counters.get("patch", 0))) # noqa: SLF001
|
|
3618
|
+
ids._frame_counter = max(ids._frame_counter, int(counters.get("frame", 0))) # noqa: SLF001
|
|
3619
|
+
|
|
3620
|
+
|
|
3541
3621
|
def _rebuild_pending_approvals(rt: "Runtime", events: list[Event]) -> None:
|
|
3542
3622
|
"""Reconstruct the pending-approval queue from the event log. v1.4.
|
|
3543
3623
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Subprocess fork-trial isolation. CONTRACT v1.5 #1.
|
|
2
|
+
|
|
3
|
+
Implements ``trial-isolation-design.md``: candidate pack code trials
|
|
4
|
+
run in a **fresh interpreter**, against a fork of the parent run,
|
|
5
|
+
materialized from artifacts pinned by the bundle hash — so the parent
|
|
6
|
+
process is out of the blast radius of the T1 accident class (runaway
|
|
7
|
+
memory/CPU, corrupted in-process state) and the bytes trialed are the
|
|
8
|
+
bytes the proposal recorded.
|
|
9
|
+
|
|
10
|
+
The division of authority is the design's:
|
|
11
|
+
|
|
12
|
+
* **The parent forks.** The child receives only the fork's run id —
|
|
13
|
+
it never gets fork authority, and it appends only to that run.
|
|
14
|
+
* **The child is a fresh interpreter** (``sys.executable -m
|
|
15
|
+
activegraph.sandbox._child``), not ``os.fork()`` — no inherited
|
|
16
|
+
Python state, no shared clients. Environment is allow-list only.
|
|
17
|
+
* **Three independent nets**: ``resource.setrlimit`` (address
|
|
18
|
+
space + CPU, POSIX; degrade to the other two nets on Windows),
|
|
19
|
+
parent-side wall-clock kill, and the runtime's own event/LLM
|
|
20
|
+
budgets inside the child.
|
|
21
|
+
* **The store is the record.** The child's stdout tail is a signal;
|
|
22
|
+
the parent re-reads the fork run from the store and the store's
|
|
23
|
+
numbers win any disagreement.
|
|
24
|
+
|
|
25
|
+
HONEST LIMITS (the design's §5, restated where users will read it):
|
|
26
|
+
this is crash/state isolation, **not a security sandbox**. A fresh
|
|
27
|
+
interpreter with rlimits does not stop a malicious candidate from
|
|
28
|
+
opening sockets or reading the filesystem — syscall and network
|
|
29
|
+
confinement are host territory (containers, seccomp), and this module
|
|
30
|
+
does not imitate them. The child can also, in principle, open the
|
|
31
|
+
shared SQLite file directly and touch other runs; per-run
|
|
32
|
+
authorization inside one SQLite file is not something SQLite gives us
|
|
33
|
+
honestly, so that caveat is stated rather than solved. The evolution
|
|
34
|
+
pack's static gates remain the pre-execution filter.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import subprocess
|
|
42
|
+
import sys
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Optional
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
#: The closed outcome set. ``crashed`` is the implementation's one
|
|
49
|
+
#: addition over the design draft: a child that dies without a
|
|
50
|
+
#: parseable tail (segfault, os._exit, kill) is reported as what it
|
|
51
|
+
#: is, never guessed into a neater bucket.
|
|
52
|
+
TRIAL_OUTCOMES = (
|
|
53
|
+
"completed",
|
|
54
|
+
"scenario_failed",
|
|
55
|
+
"limits_exceeded",
|
|
56
|
+
"materialization_failed",
|
|
57
|
+
"crashed",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Child exit codes, mirrored in _child.py. Anything else → crashed.
|
|
61
|
+
_EXIT_TO_OUTCOME = {
|
|
62
|
+
0: "completed",
|
|
63
|
+
30: "scenario_failed",
|
|
64
|
+
40: "limits_exceeded",
|
|
65
|
+
50: "materialization_failed",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class PackSource:
|
|
71
|
+
"""Where the child materializes the candidate pack from.
|
|
72
|
+
|
|
73
|
+
``expected_bundle_hash`` is the external pin (the §4 walk WITH
|
|
74
|
+
``manifest.toml``, per the v1.4 bundle-hash amendment) — the child
|
|
75
|
+
verifies it via ``activegraph.packs.manifest.verify_bundle_hash``
|
|
76
|
+
before importing anything, so the bytes trialed are the bytes the
|
|
77
|
+
proposal recorded. ``manifest_required=True`` additionally runs
|
|
78
|
+
``load_manifest`` + ``verify_surface`` against the live ``Pack``,
|
|
79
|
+
making the trial child the first consumer of the full manifest
|
|
80
|
+
chain end-to-end.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
root_dir: str
|
|
84
|
+
expected_bundle_hash: str = ""
|
|
85
|
+
manifest_required: bool = True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class TrialLimits:
|
|
90
|
+
"""The trial's resource nets. Zero/None disables a given net.
|
|
91
|
+
|
|
92
|
+
``max_llm_calls=0`` (the default) means key-freedom is
|
|
93
|
+
STRUCTURAL: the child configures no LLM provider at all, so a
|
|
94
|
+
candidate with LLM behaviors fails loud at registration
|
|
95
|
+
(``MissingProviderError``) rather than reaching a network. A
|
|
96
|
+
positive cap is accepted and recorded for a future
|
|
97
|
+
provider-wiring seam; the v1 child never configures a live
|
|
98
|
+
provider either way. ``env_passthrough`` is the ONLY parent environment
|
|
99
|
+
forwarded beyond interpreter mechanics (PATH/PYTHONPATH) — the
|
|
100
|
+
helper never forwards the environment wholesale, so parent API
|
|
101
|
+
keys don't leak into candidate code by default.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
wall_clock_seconds: float = 120.0
|
|
105
|
+
max_rss_bytes: Optional[int] = None
|
|
106
|
+
max_events: Optional[int] = 2_000
|
|
107
|
+
max_llm_calls: Optional[int] = 0
|
|
108
|
+
env_passthrough: tuple[str, ...] = ()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class TrialReport:
|
|
113
|
+
"""What a trial produced. Store-derived numbers, child-signaled shape.
|
|
114
|
+
|
|
115
|
+
``events_appended`` and ``behavior_failures`` are re-read from the
|
|
116
|
+
fork's run in the store by the parent AFTER the child exits — the
|
|
117
|
+
stdout tail is a signal only, and any disagreement resolves in
|
|
118
|
+
favor of the store. Richer evidence (tracebacks, diffs) is read
|
|
119
|
+
from the fork's log directly: ``Runtime.load(store, run_id=
|
|
120
|
+
report.fork_run_id)`` then ``trace.failures()`` / ``diff()``.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
outcome: str
|
|
124
|
+
fork_run_id: str
|
|
125
|
+
events_appended: int
|
|
126
|
+
behavior_failures: int
|
|
127
|
+
detail: str
|
|
128
|
+
exit_code: Optional[int]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def run_forked_trial(
|
|
132
|
+
store_path: str,
|
|
133
|
+
*,
|
|
134
|
+
parent_run_id: str,
|
|
135
|
+
at_event: str,
|
|
136
|
+
pack_source: PackSource,
|
|
137
|
+
scenario: str = "",
|
|
138
|
+
limits: TrialLimits = TrialLimits(),
|
|
139
|
+
label: str = "trial",
|
|
140
|
+
) -> TrialReport:
|
|
141
|
+
"""Fork the parent at ``at_event`` and trial the candidate pack in
|
|
142
|
+
a fresh subprocess. CONTRACT v1.5 #1.
|
|
143
|
+
|
|
144
|
+
The fork is created HERE, in the parent process, with full
|
|
145
|
+
``fork()`` semantics (lineage recorded, promote-block cut guard);
|
|
146
|
+
the child receives only the fork's run id. ``scenario`` is
|
|
147
|
+
``"relative/path.py"`` or ``"relative/path.py::func"`` inside the
|
|
148
|
+
pack root — the function (default ``main``) is called with the
|
|
149
|
+
fork's ``Runtime`` and drives the trial; empty means just
|
|
150
|
+
``run_until_idle()``. The scenario contract is
|
|
151
|
+
``def main(rt): ...``.
|
|
152
|
+
|
|
153
|
+
Deterministic and key-free by default (``max_llm_calls=0``, empty
|
|
154
|
+
environment pass-through). Returns a :class:`TrialReport`; never
|
|
155
|
+
raises for in-trial failures — those are outcomes. Raises only
|
|
156
|
+
for parent-side setup problems (bad store, bad fork point), with
|
|
157
|
+
the same errors ``Runtime.load`` / ``fork()`` raise.
|
|
158
|
+
"""
|
|
159
|
+
from activegraph.runtime.runtime import Runtime
|
|
160
|
+
|
|
161
|
+
parent_rt = Runtime.load(store_path, run_id=parent_run_id, behaviors=[])
|
|
162
|
+
fork_rt = parent_rt.fork(at_event=at_event, label=label, behaviors=[])
|
|
163
|
+
fork_run_id = fork_rt.run_id
|
|
164
|
+
initial_events = len(fork_rt.graph.events)
|
|
165
|
+
del fork_rt # the child owns the fork from here
|
|
166
|
+
|
|
167
|
+
job = {
|
|
168
|
+
"store_path": store_path,
|
|
169
|
+
"fork_run_id": fork_run_id,
|
|
170
|
+
"initial_events": initial_events,
|
|
171
|
+
"pack_root": str(Path(pack_source.root_dir).resolve()),
|
|
172
|
+
"expected_bundle_hash": pack_source.expected_bundle_hash,
|
|
173
|
+
"manifest_required": pack_source.manifest_required,
|
|
174
|
+
"scenario": scenario,
|
|
175
|
+
"limits": {
|
|
176
|
+
"max_rss_bytes": limits.max_rss_bytes,
|
|
177
|
+
"max_events": limits.max_events,
|
|
178
|
+
"max_llm_calls": limits.max_llm_calls,
|
|
179
|
+
"cpu_seconds": int(limits.wall_clock_seconds) + 5,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
env = {
|
|
183
|
+
key: os.environ[key]
|
|
184
|
+
for key in ("PATH", "PYTHONPATH", "HOME", "LANG")
|
|
185
|
+
if key in os.environ
|
|
186
|
+
}
|
|
187
|
+
for key in limits.env_passthrough:
|
|
188
|
+
if key in os.environ:
|
|
189
|
+
env[key] = os.environ[key]
|
|
190
|
+
|
|
191
|
+
proc = subprocess.Popen(
|
|
192
|
+
[sys.executable, "-m", "activegraph.sandbox._child"],
|
|
193
|
+
stdin=subprocess.PIPE,
|
|
194
|
+
stdout=subprocess.PIPE,
|
|
195
|
+
stderr=subprocess.DEVNULL,
|
|
196
|
+
env=env,
|
|
197
|
+
text=True,
|
|
198
|
+
)
|
|
199
|
+
try:
|
|
200
|
+
stdout, _ = proc.communicate(
|
|
201
|
+
input=json.dumps(job), timeout=limits.wall_clock_seconds
|
|
202
|
+
)
|
|
203
|
+
exit_code: Optional[int] = proc.returncode
|
|
204
|
+
timed_out = False
|
|
205
|
+
except subprocess.TimeoutExpired:
|
|
206
|
+
proc.kill()
|
|
207
|
+
stdout, _ = proc.communicate()
|
|
208
|
+
exit_code = proc.returncode
|
|
209
|
+
timed_out = True
|
|
210
|
+
|
|
211
|
+
tail: dict[str, object] = {}
|
|
212
|
+
for line in reversed((stdout or "").strip().splitlines()):
|
|
213
|
+
try:
|
|
214
|
+
parsed = json.loads(line)
|
|
215
|
+
if isinstance(parsed, dict) and "outcome" in parsed:
|
|
216
|
+
tail = parsed
|
|
217
|
+
break
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
if timed_out:
|
|
222
|
+
outcome = "limits_exceeded"
|
|
223
|
+
detail = (
|
|
224
|
+
f"wall clock exceeded {limits.wall_clock_seconds}s; child killed"
|
|
225
|
+
)
|
|
226
|
+
elif tail:
|
|
227
|
+
outcome = str(tail.get("outcome", "crashed"))
|
|
228
|
+
if outcome not in TRIAL_OUTCOMES:
|
|
229
|
+
outcome = "crashed"
|
|
230
|
+
detail = str(tail.get("detail", ""))
|
|
231
|
+
else:
|
|
232
|
+
outcome = _EXIT_TO_OUTCOME.get(exit_code or -1, "crashed")
|
|
233
|
+
if outcome == "completed":
|
|
234
|
+
# Exit 0 with no tail is itself suspicious; say so.
|
|
235
|
+
outcome = "crashed"
|
|
236
|
+
detail = f"child exited {exit_code} with no report tail"
|
|
237
|
+
|
|
238
|
+
# The store is the record: re-read the fork run for the numbers.
|
|
239
|
+
events_appended = 0
|
|
240
|
+
behavior_failures = 0
|
|
241
|
+
try:
|
|
242
|
+
fork_view = Runtime.load(store_path, run_id=fork_run_id, behaviors=[])
|
|
243
|
+
events_appended = max(
|
|
244
|
+
0, len(fork_view.graph.events) - initial_events
|
|
245
|
+
)
|
|
246
|
+
behavior_failures = len(fork_view.trace.failures())
|
|
247
|
+
except Exception as e: # noqa: BLE001 — report, never mask the trial
|
|
248
|
+
detail = f"{detail} (store re-read failed: {e})".strip()
|
|
249
|
+
|
|
250
|
+
return TrialReport(
|
|
251
|
+
outcome=outcome,
|
|
252
|
+
fork_run_id=fork_run_id,
|
|
253
|
+
events_appended=events_appended,
|
|
254
|
+
behavior_failures=behavior_failures,
|
|
255
|
+
detail=detail,
|
|
256
|
+
exit_code=exit_code,
|
|
257
|
+
)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""The trial child: fresh-interpreter half of the sandbox. v1.5 #1.
|
|
2
|
+
|
|
3
|
+
Reads one JSON job spec from stdin, applies rlimits, materializes the
|
|
4
|
+
candidate pack from artifacts (bundle-hash-pinned, manifest-verified),
|
|
5
|
+
loads the fork run, runs the scenario under budgets, and prints one
|
|
6
|
+
JSON report line to stdout. Everything richer than the report tail is
|
|
7
|
+
already in the store — the child's trial activity is ordinary events
|
|
8
|
+
in the fork's run.
|
|
9
|
+
|
|
10
|
+
Exit codes (mirrored in ``activegraph.sandbox._EXIT_TO_OUTCOME``):
|
|
11
|
+
0 completed, 30 scenario_failed, 40 limits_exceeded,
|
|
12
|
+
50 materialization_failed. Anything else reads as ``crashed``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.util
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
import traceback
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Callable, Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _report(
|
|
26
|
+
outcome: str,
|
|
27
|
+
*,
|
|
28
|
+
fork_run_id: str,
|
|
29
|
+
events_appended: int,
|
|
30
|
+
behavior_failures: int,
|
|
31
|
+
detail: str,
|
|
32
|
+
exit_code: int,
|
|
33
|
+
) -> None:
|
|
34
|
+
print(
|
|
35
|
+
json.dumps(
|
|
36
|
+
{
|
|
37
|
+
"outcome": outcome,
|
|
38
|
+
"fork_run_id": fork_run_id,
|
|
39
|
+
"events_appended": events_appended,
|
|
40
|
+
"behavior_failures": behavior_failures,
|
|
41
|
+
"detail": detail[:500],
|
|
42
|
+
}
|
|
43
|
+
),
|
|
44
|
+
flush=True,
|
|
45
|
+
)
|
|
46
|
+
raise SystemExit(exit_code)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _apply_rlimits(limits: dict[str, Any]) -> None:
|
|
50
|
+
"""POSIX resource caps; on platforms without ``resource`` the
|
|
51
|
+
wall-clock and budget nets still hold (stated in the design)."""
|
|
52
|
+
try:
|
|
53
|
+
import resource
|
|
54
|
+
except ImportError: # pragma: no cover — Windows
|
|
55
|
+
return
|
|
56
|
+
max_rss = limits.get("max_rss_bytes")
|
|
57
|
+
if max_rss:
|
|
58
|
+
resource.setrlimit(resource.RLIMIT_AS, (max_rss, max_rss))
|
|
59
|
+
cpu = limits.get("cpu_seconds")
|
|
60
|
+
if cpu:
|
|
61
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _materialize_pack(job: dict[str, Any]) -> Any:
|
|
65
|
+
"""Verify pins, then import the candidate and return its Pack.
|
|
66
|
+
|
|
67
|
+
Order is the design's §3: bundle hash BEFORE any import (the pin
|
|
68
|
+
covers manifest.toml per the v1.4 amendment), then manifest schema,
|
|
69
|
+
then import, then the two-way surface check against the live Pack.
|
|
70
|
+
"""
|
|
71
|
+
from activegraph.packs import Pack
|
|
72
|
+
from activegraph.packs.manifest import (
|
|
73
|
+
load_manifest,
|
|
74
|
+
verify_bundle_hash,
|
|
75
|
+
verify_surface,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
root = Path(job["pack_root"])
|
|
79
|
+
manifest = None
|
|
80
|
+
if job.get("expected_bundle_hash"):
|
|
81
|
+
verify_bundle_hash(job["expected_bundle_hash"], root)
|
|
82
|
+
if job.get("manifest_required", True):
|
|
83
|
+
manifest = load_manifest(root)
|
|
84
|
+
|
|
85
|
+
spec = importlib.util.spec_from_file_location(
|
|
86
|
+
manifest.name if manifest else root.name,
|
|
87
|
+
root / "__init__.py",
|
|
88
|
+
submodule_search_locations=[str(root)],
|
|
89
|
+
)
|
|
90
|
+
if spec is None or spec.loader is None:
|
|
91
|
+
raise RuntimeError(f"cannot import pack module at {root}")
|
|
92
|
+
module = importlib.util.module_from_spec(spec)
|
|
93
|
+
sys.modules[spec.name] = module
|
|
94
|
+
spec.loader.exec_module(module)
|
|
95
|
+
|
|
96
|
+
packs = [v for v in vars(module).values() if isinstance(v, Pack)]
|
|
97
|
+
if manifest is not None:
|
|
98
|
+
packs = [p for p in packs if p.name == manifest.name]
|
|
99
|
+
if len(packs) != 1:
|
|
100
|
+
raise RuntimeError(
|
|
101
|
+
f"pack module must expose exactly one module-level Pack"
|
|
102
|
+
f"{' named ' + manifest.name if manifest else ''}; "
|
|
103
|
+
f"found {len(packs)}"
|
|
104
|
+
)
|
|
105
|
+
pack = packs[0]
|
|
106
|
+
if manifest is not None:
|
|
107
|
+
verify_surface(manifest, pack)
|
|
108
|
+
return pack
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_scenario(
|
|
112
|
+
root: Path, scenario: str
|
|
113
|
+
) -> Optional[Callable[[Any], None]]:
|
|
114
|
+
if not scenario:
|
|
115
|
+
return None
|
|
116
|
+
path_part, _, func_name = scenario.partition("::")
|
|
117
|
+
func_name = func_name or "main"
|
|
118
|
+
scenario_path = (root / path_part).resolve()
|
|
119
|
+
spec = importlib.util.spec_from_file_location(
|
|
120
|
+
"_trial_scenario", scenario_path
|
|
121
|
+
)
|
|
122
|
+
if spec is None or spec.loader is None:
|
|
123
|
+
raise RuntimeError(f"cannot import scenario at {scenario_path}")
|
|
124
|
+
module = importlib.util.module_from_spec(spec)
|
|
125
|
+
spec.loader.exec_module(module)
|
|
126
|
+
fn = getattr(module, func_name, None)
|
|
127
|
+
if not callable(fn):
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
f"scenario {scenario!r} has no callable {func_name!r}"
|
|
130
|
+
)
|
|
131
|
+
return fn
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main() -> None:
|
|
135
|
+
job = json.loads(sys.stdin.read())
|
|
136
|
+
fork_run_id = str(job["fork_run_id"])
|
|
137
|
+
initial = int(job.get("initial_events", 0))
|
|
138
|
+
_apply_rlimits(job.get("limits", {}))
|
|
139
|
+
|
|
140
|
+
def counts(rt: Any) -> tuple[int, int]:
|
|
141
|
+
return (
|
|
142
|
+
max(0, len(rt.graph.events) - initial),
|
|
143
|
+
len(rt.trace.failures()),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
pack = _materialize_pack(job)
|
|
148
|
+
except Exception as e: # noqa: BLE001 — outcome, not crash
|
|
149
|
+
_report(
|
|
150
|
+
"materialization_failed",
|
|
151
|
+
fork_run_id=fork_run_id,
|
|
152
|
+
events_appended=0,
|
|
153
|
+
behavior_failures=0,
|
|
154
|
+
detail=f"{type(e).__name__}: {e}",
|
|
155
|
+
exit_code=50,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
from activegraph.runtime.runtime import Runtime
|
|
159
|
+
|
|
160
|
+
budget: dict[str, Any] = {}
|
|
161
|
+
if job["limits"].get("max_events"):
|
|
162
|
+
budget["max_events"] = job["limits"]["max_events"]
|
|
163
|
+
# max_llm_calls=0 (the default) needs no budget dimension: the
|
|
164
|
+
# child configures NO llm_provider, so key-freedom is structural —
|
|
165
|
+
# an LLM behavior in the candidate fails loud at registration
|
|
166
|
+
# (MissingProviderError), it cannot silently reach a network. A
|
|
167
|
+
# positive cap is recorded for a future provider-wiring seam.
|
|
168
|
+
if job["limits"].get("max_llm_calls"):
|
|
169
|
+
budget["max_llm_calls"] = job["limits"]["max_llm_calls"]
|
|
170
|
+
|
|
171
|
+
rt = Runtime.load(
|
|
172
|
+
job["store_path"],
|
|
173
|
+
run_id=fork_run_id,
|
|
174
|
+
behaviors=[],
|
|
175
|
+
budget=budget or None,
|
|
176
|
+
)
|
|
177
|
+
rt.load_pack(pack)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
scenario_fn = _resolve_scenario(
|
|
181
|
+
Path(job["pack_root"]), job.get("scenario", "")
|
|
182
|
+
)
|
|
183
|
+
if scenario_fn is not None:
|
|
184
|
+
scenario_fn(rt)
|
|
185
|
+
else:
|
|
186
|
+
rt.run_until_idle()
|
|
187
|
+
except MemoryError:
|
|
188
|
+
appended, failures = counts(rt)
|
|
189
|
+
_report(
|
|
190
|
+
"limits_exceeded",
|
|
191
|
+
fork_run_id=fork_run_id,
|
|
192
|
+
events_appended=appended,
|
|
193
|
+
behavior_failures=failures,
|
|
194
|
+
detail="MemoryError under RLIMIT_AS",
|
|
195
|
+
exit_code=40,
|
|
196
|
+
)
|
|
197
|
+
except Exception as e: # noqa: BLE001 — outcome, not crash
|
|
198
|
+
appended, failures = counts(rt)
|
|
199
|
+
_report(
|
|
200
|
+
"scenario_failed",
|
|
201
|
+
fork_run_id=fork_run_id,
|
|
202
|
+
events_appended=appended,
|
|
203
|
+
behavior_failures=failures,
|
|
204
|
+
detail=f"{type(e).__name__}: {e}\n"
|
|
205
|
+
+ traceback.format_exc(limit=3),
|
|
206
|
+
exit_code=30,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
appended, failures = counts(rt)
|
|
210
|
+
exhausted = any(
|
|
211
|
+
e.type == "runtime.budget_exhausted"
|
|
212
|
+
for e in rt.graph.events[initial:]
|
|
213
|
+
)
|
|
214
|
+
if exhausted:
|
|
215
|
+
_report(
|
|
216
|
+
"limits_exceeded",
|
|
217
|
+
fork_run_id=fork_run_id,
|
|
218
|
+
events_appended=appended,
|
|
219
|
+
behavior_failures=failures,
|
|
220
|
+
detail="trial budget exhausted (see runtime.budget_exhausted)",
|
|
221
|
+
exit_code=40,
|
|
222
|
+
)
|
|
223
|
+
_report(
|
|
224
|
+
"completed",
|
|
225
|
+
fork_run_id=fork_run_id,
|
|
226
|
+
events_appended=appended,
|
|
227
|
+
behavior_failures=failures,
|
|
228
|
+
detail="",
|
|
229
|
+
exit_code=0,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
main()
|