agentinputlab 0.1.0a1__py3-none-any.whl
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.
- agentinputlab/__init__.py +28 -0
- agentinputlab/algorithms/__init__.py +6 -0
- agentinputlab/algorithms/acquire/__init__.py +145 -0
- agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
- agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
- agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
- agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
- agentinputlab/algorithms/acquire/hyde.py +296 -0
- agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
- agentinputlab/algorithms/acquire/hyde_external.py +148 -0
- agentinputlab/algorithms/acquire/ircot.py +28 -0
- agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
- agentinputlab/algorithms/acquire/ircot_native.py +355 -0
- agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
- agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
- agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
- agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
- agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
- agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
- agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
- agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
- agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
- agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
- agentinputlab/algorithms/clarify/__init__.py +61 -0
- agentinputlab/algorithms/clarify/_structured.py +64 -0
- agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
- agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
- agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
- agentinputlab/algorithms/clarify/future_turns.py +379 -0
- agentinputlab/algorithms/clarify/udcs.py +255 -0
- agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
- agentinputlab/algorithms/curate/__init__.py +65 -0
- agentinputlab/algorithms/curate/acon/__init__.py +27 -0
- agentinputlab/algorithms/curate/acon/common.py +284 -0
- agentinputlab/algorithms/curate/acon/common_support.py +136 -0
- agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
- agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
- agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
- agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
- agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
- agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
- agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
- agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
- agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
- agentinputlab/algorithms/curate/recomp/external.py +1129 -0
- agentinputlab/algorithms/curate/recomp/selective.py +304 -0
- agentinputlab/algorithms/decompose/__init__.py +107 -0
- agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
- agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
- agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
- agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
- agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
- agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
- agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
- agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
- agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
- agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
- agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
- agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
- agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
- agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
- agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
- agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
- agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
- agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
- agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
- agentinputlab/algorithms/reformulate/__init__.py +85 -0
- agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
- agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
- agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
- agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
- agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
- agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
- agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
- agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
- agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
- agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
- agentinputlab/algorithms/reformulate/rrr.py +696 -0
- agentinputlab/annotation/__init__.py +5 -0
- agentinputlab/annotation/independent.py +428 -0
- agentinputlab/annotation/server.py +233 -0
- agentinputlab/annotation/static/app.js +121 -0
- agentinputlab/annotation/static/index.html +60 -0
- agentinputlab/annotation/static/review.css +2 -0
- agentinputlab/annotation/static/review.html +10 -0
- agentinputlab/annotation/static/review.js +11 -0
- agentinputlab/annotation/static/styles.css +55 -0
- agentinputlab/annotation/store.py +444 -0
- agentinputlab/api.py +368 -0
- agentinputlab/artifacts/__init__.py +15 -0
- agentinputlab/artifacts/json_file.py +104 -0
- agentinputlab/artifacts/manifest.py +98 -0
- agentinputlab/artifacts/store.py +157 -0
- agentinputlab/backends/__init__.py +98 -0
- agentinputlab/backends/embedding/__init__.py +25 -0
- agentinputlab/backends/embedding/base.py +145 -0
- agentinputlab/backends/embedding/callable.py +75 -0
- agentinputlab/backends/embedding/replay.py +129 -0
- agentinputlab/backends/llm/__init__.py +31 -0
- agentinputlab/backends/llm/base.py +149 -0
- agentinputlab/backends/llm/callable.py +108 -0
- agentinputlab/backends/llm/decomp_completion.py +88 -0
- agentinputlab/backends/llm/final_content_stop.py +72 -0
- agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
- agentinputlab/backends/llm/openai_compatible.py +332 -0
- agentinputlab/backends/llm/transformers.py +171 -0
- agentinputlab/backends/retrieval/__init__.py +35 -0
- agentinputlab/backends/retrieval/base.py +50 -0
- agentinputlab/backends/retrieval/dense.py +777 -0
- agentinputlab/backends/retrieval/memory.py +119 -0
- agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
- agentinputlab/backends/user/__init__.py +15 -0
- agentinputlab/backends/user/base.py +66 -0
- agentinputlab/backends/user/callable.py +54 -0
- agentinputlab/backends/user/replay.py +89 -0
- agentinputlab/backends/vector.py +38 -0
- agentinputlab/benchmarks/__init__.py +246 -0
- agentinputlab/benchmarks/acquisition.py +106 -0
- agentinputlab/benchmarks/ale.py +540 -0
- agentinputlab/benchmarks/appworld_ul.py +669 -0
- agentinputlab/benchmarks/assets.py +226 -0
- agentinputlab/benchmarks/base.py +68 -0
- agentinputlab/benchmarks/beir_archive.py +235 -0
- agentinputlab/benchmarks/beir_sample.py +269 -0
- agentinputlab/benchmarks/bfcl_native.py +245 -0
- agentinputlab/benchmarks/bfcl_v3.py +607 -0
- agentinputlab/benchmarks/cache.py +218 -0
- agentinputlab/benchmarks/clariq.py +203 -0
- agentinputlab/benchmarks/data/registry.json +321 -0
- agentinputlab/benchmarks/data/smoke_assets.json +511 -0
- agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
- agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
- agentinputlab/benchmarks/execution.py +1111 -0
- agentinputlab/benchmarks/interactive.py +257 -0
- agentinputlab/benchmarks/ir_query.py +204 -0
- agentinputlab/benchmarks/longbench.py +203 -0
- agentinputlab/benchmarks/paired_replay.py +479 -0
- agentinputlab/benchmarks/preprocess.py +2313 -0
- agentinputlab/benchmarks/production.py +236 -0
- agentinputlab/benchmarks/qa.py +963 -0
- agentinputlab/benchmarks/registry.py +110 -0
- agentinputlab/benchmarks/retrieval.py +459 -0
- agentinputlab/benchmarks/runtime.py +509 -0
- agentinputlab/benchmarks/statebench.py +938 -0
- agentinputlab/benchmarks/tau3.py +725 -0
- agentinputlab/benchmarks/toolsandbox.py +626 -0
- agentinputlab/benchmarks/trec_dl_sample.py +263 -0
- agentinputlab/campaign.py +654 -0
- agentinputlab/campaign_runner.py +953 -0
- agentinputlab/cli/__init__.py +6 -0
- agentinputlab/cli/main.py +933 -0
- agentinputlab/connectors/__init__.py +46 -0
- agentinputlab/connectors/knowledge/__init__.py +4 -0
- agentinputlab/connectors/tools/__init__.py +30 -0
- agentinputlab/connectors/tools/base.py +57 -0
- agentinputlab/connectors/tools/mcp.py +222 -0
- agentinputlab/connectors/tools/memory.py +41 -0
- agentinputlab/connectors/tools/openapi.py +695 -0
- agentinputlab/connectors/tools/schema.py +102 -0
- agentinputlab/contracts/__init__.py +91 -0
- agentinputlab/contracts/availability.py +24 -0
- agentinputlab/contracts/dependency.py +58 -0
- agentinputlab/contracts/enums.py +82 -0
- agentinputlab/contracts/errors.py +63 -0
- agentinputlab/contracts/evidence.py +43 -0
- agentinputlab/contracts/intent.py +110 -0
- agentinputlab/contracts/lifecycle.py +164 -0
- agentinputlab/contracts/patch.py +167 -0
- agentinputlab/contracts/serialization.py +93 -0
- agentinputlab/contracts/state.py +127 -0
- agentinputlab/contracts/trace.py +59 -0
- agentinputlab/contracts/worker.py +42 -0
- agentinputlab/core/__init__.py +7 -0
- agentinputlab/data/__init__.py +32 -0
- agentinputlab/data/checksums.py +9 -0
- agentinputlab/data/downloader.py +235 -0
- agentinputlab/data/http.py +171 -0
- agentinputlab/data/index.py +21 -0
- agentinputlab/data/overlap.py +28 -0
- agentinputlab/data/preprocess.py +32 -0
- agentinputlab/data/storage.py +57 -0
- agentinputlab/data/trec_sample.py +427 -0
- agentinputlab/evaluation/__init__.py +5 -0
- agentinputlab/evaluation/outcomes.py +32 -0
- agentinputlab/experiments/__init__.py +278 -0
- agentinputlab/experiments/analysis.py +981 -0
- agentinputlab/experiments/artifacts.py +687 -0
- agentinputlab/experiments/automatic_measurement.py +678 -0
- agentinputlab/experiments/auxiliary_matrix.py +203 -0
- agentinputlab/experiments/bindings.py +430 -0
- agentinputlab/experiments/blueprints.py +147 -0
- agentinputlab/experiments/builders/__init__.py +45 -0
- agentinputlab/experiments/builders/executable_external_payload.py +423 -0
- agentinputlab/experiments/builders/external_process_arena.py +1018 -0
- agentinputlab/experiments/builders/registry_intervention.py +463 -0
- agentinputlab/experiments/cli.py +158 -0
- agentinputlab/experiments/controls.py +340 -0
- agentinputlab/experiments/driver_guard.py +205 -0
- agentinputlab/experiments/drivers/__init__.py +89 -0
- agentinputlab/experiments/drivers/external_arena.py +2908 -0
- agentinputlab/experiments/drivers/method_process.py +662 -0
- agentinputlab/experiments/drivers/parity_process.py +1484 -0
- agentinputlab/experiments/drivers/static_production.py +1574 -0
- agentinputlab/experiments/executor.py +448 -0
- agentinputlab/experiments/loader.py +605 -0
- agentinputlab/experiments/measurement.py +675 -0
- agentinputlab/experiments/paper_analysis.py +1818 -0
- agentinputlab/experiments/parity_identity.py +605 -0
- agentinputlab/experiments/plan.py +277 -0
- agentinputlab/experiments/production_builders.py +3739 -0
- agentinputlab/experiments/readiness.py +432 -0
- agentinputlab/experiments/schema.py +514 -0
- agentinputlab/experiments/secrets.py +177 -0
- agentinputlab/experiments/stateful_analysis.py +1738 -0
- agentinputlab/experiments/transfer_analysis.py +1316 -0
- agentinputlab/experiments/unseen_analysis.py +1025 -0
- agentinputlab/integrations/__init__.py +1 -0
- agentinputlab/integrations/action_space/__init__.py +1 -0
- agentinputlab/integrations/external_workers/__init__.py +76 -0
- agentinputlab/integrations/external_workers/ace.py +431 -0
- agentinputlab/integrations/external_workers/base.py +181 -0
- agentinputlab/integrations/external_workers/decomp_live.py +249 -0
- agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
- agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
- agentinputlab/integrations/external_workers/ircot.py +576 -0
- agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
- agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
- agentinputlab/integrations/external_workers/method.py +314 -0
- agentinputlab/integrations/external_workers/process.py +678 -0
- agentinputlab/integrations/external_workers/supervisor.py +1071 -0
- agentinputlab/integrations/full_architecture/__init__.py +1 -0
- agentinputlab/integrations/full_architecture/hyde.py +212 -0
- agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
- agentinputlab/integrations/live_history.py +222 -0
- agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
- agentinputlab/integrations/training/__init__.py +1 -0
- agentinputlab/metrics/__init__.py +50 -0
- agentinputlab/metrics/catalog.py +313 -0
- agentinputlab/metrics/harm.py +95 -0
- agentinputlab/metrics/paired.py +40 -0
- agentinputlab/metrics/qa.py +76 -0
- agentinputlab/metrics/text.py +33 -0
- agentinputlab/metrics/third_party.py +161 -0
- agentinputlab/models/__init__.py +4 -0
- agentinputlab/observability/__init__.py +56 -0
- agentinputlab/observability/budget.py +316 -0
- agentinputlab/observability/instrumentation.py +754 -0
- agentinputlab/observability/models.py +403 -0
- agentinputlab/observability/monitor.py +380 -0
- agentinputlab/optimizers/__init__.py +1 -0
- agentinputlab/optimizers/clarify/__init__.py +12 -0
- agentinputlab/optimizers/context/__init__.py +22 -0
- agentinputlab/optimizers/decompose/__init__.py +64 -0
- agentinputlab/optimizers/evidence/__init__.py +22 -0
- agentinputlab/optimizers/rewrite/__init__.py +30 -0
- agentinputlab/parity/__init__.py +5 -0
- agentinputlab/parity/base.py +206 -0
- agentinputlab/production_campaign.py +763 -0
- agentinputlab/py.typed +1 -0
- agentinputlab/recipes/__init__.py +333 -0
- agentinputlab/recipes/ace.py +418 -0
- agentinputlab/recipes/ace_bfcl.py +273 -0
- agentinputlab/recipes/ace_common_support.py +169 -0
- agentinputlab/recipes/ace_native_plan.py +322 -0
- agentinputlab/recipes/ace_online.py +711 -0
- agentinputlab/recipes/ace_retrieval.py +330 -0
- agentinputlab/recipes/ace_statebench.py +382 -0
- agentinputlab/recipes/ace_tau.py +463 -0
- agentinputlab/recipes/ace_toolsandbox.py +432 -0
- agentinputlab/recipes/acon_guideline.py +22 -0
- agentinputlab/recipes/adaptive_rag.py +822 -0
- agentinputlab/recipes/llmlingua2_inference.py +146 -0
- agentinputlab/recipes/llmlingua2_training.py +796 -0
- agentinputlab/recipes/mce.py +1486 -0
- agentinputlab/recipes/mce_final.py +335 -0
- agentinputlab/recipes/mce_text_fit.py +499 -0
- agentinputlab/recipes/mce_text_only.py +298 -0
- agentinputlab/recipes/qrag.py +729 -0
- agentinputlab/recipes/qrag_inference.py +364 -0
- agentinputlab/recipes/r2ag_features.py +236 -0
- agentinputlab/recipes/r2ag_inference.py +503 -0
- agentinputlab/recipes/r2ag_training.py +948 -0
- agentinputlab/recipes/recomp_abstractive.py +592 -0
- agentinputlab/recipes/recomp_extractive.py +525 -0
- agentinputlab/recipes/reformer.py +17 -0
- agentinputlab/recipes/rqrag.py +1516 -0
- agentinputlab/recipes/rqrag_common_support.py +193 -0
- agentinputlab/recipes/rqrag_completion.py +147 -0
- agentinputlab/recipes/skillopt.py +462 -0
- agentinputlab/recipes/skillopt_common_support.py +154 -0
- agentinputlab/recipes/skillopt_environment.py +165 -0
- agentinputlab/recipes/skillopt_environment_cli.py +5 -0
- agentinputlab/recipes/skillopt_retrieval.py +188 -0
- agentinputlab/recipes/smartsearch.py +897 -0
- agentinputlab/recipes/smartsearch_curriculum.py +283 -0
- agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
- agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
- agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
- agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
- agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
- agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
- agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
- agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
- agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
- agentinputlab/recipes/smartsearch_inference.py +524 -0
- agentinputlab/recipes/smartsearch_native_data.py +172 -0
- agentinputlab/recipes/smartsearch_policy.py +616 -0
- agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
- agentinputlab/recipes/smartsearch_sampling.py +232 -0
- agentinputlab/recipes/smartsearch_teacher.py +203 -0
- agentinputlab/recipes/smartsearch_training.py +265 -0
- agentinputlab/registry/__init__.py +46 -0
- agentinputlab/registry/beta.py +727 -0
- agentinputlab/registry/data/algorithms.json +1959 -0
- agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
- agentinputlab/registry/loader.py +100 -0
- agentinputlab/registry/result_manifest.py +184 -0
- agentinputlab/registry/runtime.py +195 -0
- agentinputlab/registry/schema.py +449 -0
- agentinputlab/runner/__init__.py +30 -0
- agentinputlab/runner/balance.py +248 -0
- agentinputlab/runner/cache.py +135 -0
- agentinputlab/runner/composite.py +130 -0
- agentinputlab/runner/engine.py +285 -0
- agentinputlab/runner/runtime_guard.py +268 -0
- agentinputlab/runner/worker.py +80 -0
- agentinputlab/runtime_bootstrap.py +288 -0
- agentinputlab/selectors/__init__.py +196 -0
- agentinputlab/selectors/advanced.py +1514 -0
- agentinputlab/selectors/artifacts.py +1087 -0
- agentinputlab/selectors/base.py +112 -0
- agentinputlab/selectors/campaigns.py +1893 -0
- agentinputlab/selectors/catalog.py +86 -0
- agentinputlab/selectors/discovery.py +1860 -0
- agentinputlab/selectors/evaluation.py +1348 -0
- agentinputlab/selectors/features.py +383 -0
- agentinputlab/selectors/learned.py +502 -0
- agentinputlab/selectors/no_op.py +28 -0
- agentinputlab/selectors/outcome_table.py +475 -0
- agentinputlab/selectors/producer.py +853 -0
- agentinputlab/selectors/rules.py +109 -0
- agentinputlab/selectors/runtime.py +420 -0
- agentinputlab/studies/__init__.py +16 -0
- agentinputlab/studies/design.py +165 -0
- agentinputlab/studies/paired.py +322 -0
- agentinputlab/studio/__init__.py +6 -0
- agentinputlab/studio/campaigns.py +819 -0
- agentinputlab/studio/engine.py +1408 -0
- agentinputlab/studio/model_backends.py +425 -0
- agentinputlab/studio/prepared.py +410 -0
- agentinputlab/studio/server.py +613 -0
- agentinputlab/studio/static/app.js +397 -0
- agentinputlab/studio/static/campaigns.html +175 -0
- agentinputlab/studio/static/campaigns.js +426 -0
- agentinputlab/studio/static/index.html +192 -0
- agentinputlab/studio/static/result.html +103 -0
- agentinputlab/studio/static/result.js +273 -0
- agentinputlab/studio/static/styles.css +467 -0
- agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
- agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
- agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
- agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
- agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
- agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
- agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
- workers/README.md +94 -0
- workers/ace/README.md +261 -0
- workers/ace/__init__.py +1 -0
- workers/ace/compatibility_manifest.json +39 -0
- workers/ace/driver.py +1682 -0
- workers/acon/README.md +215 -0
- workers/acon/__init__.py +5 -0
- workers/acon/compatibility_manifest.json +93 -0
- workers/acon/driver.py +1859 -0
- workers/acon/fitted.py +395 -0
- workers/acon/guideline.py +470 -0
- workers/acon/guideline_source_manifest.json +20 -0
- workers/adaptive_rag/README.md +96 -0
- workers/adaptive_rag/__init__.py +15 -0
- workers/adaptive_rag/compatibility_manifest.json +61 -0
- workers/adaptive_rag/driver.py +1470 -0
- workers/adaptive_rag/fixture_runtime.py +163 -0
- workers/adaptive_rag/runtime.py +252 -0
- workers/ask_or_assume/README.md +124 -0
- workers/ask_or_assume/__init__.py +1 -0
- workers/ask_or_assume/compatibility_manifest.json +36 -0
- workers/ask_or_assume/driver.py +1227 -0
- workers/decomp/README.md +133 -0
- workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
- workers/decomp/__init__.py +5 -0
- workers/decomp/compatibility_manifest.json +205 -0
- workers/decomp/driver.py +1015 -0
- workers/decomp/live.py +137 -0
- workers/decomp/runtime.py +231 -0
- workers/infocqr/README.md +106 -0
- workers/infocqr/__init__.py +1 -0
- workers/infocqr/compatibility_manifest.json +84 -0
- workers/infocqr/driver.py +1766 -0
- workers/ircot/README.md +167 -0
- workers/ircot/__init__.py +7 -0
- workers/ircot/compatibility_manifest.json +121 -0
- workers/ircot/controlled.py +457 -0
- workers/ircot/driver.py +1373 -0
- workers/ircot/fixture_bindings.json +39 -0
- workers/ircot/fixture_config.json +48 -0
- workers/ircot/fixture_prompt.txt +2 -0
- workers/ircot/native_qa.py +398 -0
- workers/ircot/qa_compatibility_manifest.json +57 -0
- workers/ircot/runtime.py +97 -0
- workers/llmcompiler/README.md +160 -0
- workers/llmcompiler/__init__.py +1 -0
- workers/llmcompiler/compatibility_manifest.json +45 -0
- workers/llmcompiler/driver.py +1514 -0
- workers/llmcompiler/export_prompts.py +74 -0
- workers/llmcompiler/live.py +174 -0
- workers/llmcompiler/patch_manifest.json +147 -0
- workers/mce/README.md +198 -0
- workers/mce/__init__.py +29 -0
- workers/mce/compatibility_manifest.json +115 -0
- workers/mce/driver.py +2585 -0
- workers/mce/final_eval.py +173 -0
- workers/official_arenas/README.md +25 -0
- workers/official_arenas/__init__.py +1 -0
- workers/official_arenas/capture_records.py +110 -0
- workers/official_arenas/driver.py +619 -0
- workers/official_arenas/protocol.py +334 -0
- workers/official_arenas/providers/__init__.py +1 -0
- workers/official_arenas/providers/_live_history.py +228 -0
- workers/official_arenas/providers/_message_compat.py +77 -0
- workers/official_arenas/providers/appworld_ul.py +196 -0
- workers/official_arenas/providers/fixture.py +112 -0
- workers/official_arenas/providers/statebench.py +960 -0
- workers/official_arenas/providers/tau3.py +1672 -0
- workers/official_arenas/providers/tau_retail.py +586 -0
- workers/official_arenas/providers/toolsandbox.py +1321 -0
- workers/official_arenas/providers/tooltalk.py +699 -0
- workers/official_arenas/retail_capture.py +188 -0
- workers/official_arenas/retail_driver.py +275 -0
- workers/qrag/README.md +172 -0
- workers/qrag/__init__.py +1 -0
- workers/qrag/compatibility_manifest.json +64 -0
- workers/qrag/driver.py +673 -0
- workers/r2ag/README.md +154 -0
- workers/r2ag/__init__.py +5 -0
- workers/r2ag/compatibility_manifest.json +55 -0
- workers/r2ag/driver.py +1287 -0
- workers/r2ag/feature_schema.json +8 -0
- workers/r2ag/runtime.py +589 -0
- workers/recomp/README.md +120 -0
- workers/recomp/__init__.py +1 -0
- workers/recomp/compatibility_manifest.json +43 -0
- workers/recomp/driver.py +1653 -0
- workers/reformer/README.md +125 -0
- workers/reformer/__init__.py +1 -0
- workers/reformer/compatibility_manifest.json +68 -0
- workers/reformer/driver.py +1459 -0
- workers/reformer/induction.py +457 -0
- workers/rrr/README.md +126 -0
- workers/rrr/__init__.py +1 -0
- workers/rrr/compatibility_manifest.json +57 -0
- workers/rrr/driver.py +1133 -0
- workers/smartsearch/README.md +331 -0
- workers/smartsearch/__init__.py +1 -0
- workers/smartsearch/compatibility_manifest.json +41 -0
- workers/smartsearch/driver.py +1643 -0
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
"""Controlled benchmark execution across interventions and downstream predictors.
|
|
2
|
+
|
|
3
|
+
The lower-level benchmark runner accepts an arbitrary prediction callback. This
|
|
4
|
+
module owns the missing research boundary: it applies one intervention to an
|
|
5
|
+
evaluator-blind state, sends only the transformed state to a typed downstream
|
|
6
|
+
predictor, and lets the benchmark adapter see evaluator gold only while
|
|
7
|
+
scoring. It provides both the historical two-arm split runner and a per-unit
|
|
8
|
+
multi-arm block runner used by production experiment cells.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Callable, Mapping
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
from agentinputlab.contracts.enums import PatchOp, SemanticFamily
|
|
20
|
+
from agentinputlab.contracts.errors import (
|
|
21
|
+
ArmExecutionError,
|
|
22
|
+
BalanceError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
from agentinputlab.contracts.lifecycle import Intervention, InterventionContext
|
|
26
|
+
from agentinputlab.contracts.serialization import content_hash, freeze_mapping
|
|
27
|
+
from agentinputlab.contracts.state import ExecutionBudget, FrozenAgentState
|
|
28
|
+
from agentinputlab.observability import (
|
|
29
|
+
BudgetLedger,
|
|
30
|
+
ResourceReceipt,
|
|
31
|
+
ResourceUsage,
|
|
32
|
+
RunMonitor,
|
|
33
|
+
)
|
|
34
|
+
from agentinputlab.runner import (
|
|
35
|
+
BalanceAuditor,
|
|
36
|
+
BalanceReport,
|
|
37
|
+
ComparisonArm,
|
|
38
|
+
ComparisonProfile,
|
|
39
|
+
InterventionRunner,
|
|
40
|
+
RunRecord,
|
|
41
|
+
context_dependency_fingerprints,
|
|
42
|
+
)
|
|
43
|
+
from agentinputlab.runner.runtime_guard import inspect_object_graph, is_runtime_dependency
|
|
44
|
+
from agentinputlab.studies.paired import ArmFailure
|
|
45
|
+
|
|
46
|
+
from .base import BenchmarkAdapter, BenchmarkExample
|
|
47
|
+
from .runtime import (
|
|
48
|
+
BenchmarkExampleRecord,
|
|
49
|
+
PairedBenchmarkExampleRecord,
|
|
50
|
+
PairedBenchmarkRecord,
|
|
51
|
+
build_benchmark_example_record,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_PREDICTOR_FINGERPRINT_SLOT = "downstream_predictor"
|
|
55
|
+
_CONTEXT_RUNTIME_SLOTS = (
|
|
56
|
+
"backend",
|
|
57
|
+
"dense_embedder",
|
|
58
|
+
"retriever",
|
|
59
|
+
"vector_searcher",
|
|
60
|
+
"tools",
|
|
61
|
+
"external_runtime",
|
|
62
|
+
"artifact_manager",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class DownstreamPrediction:
|
|
68
|
+
"""Typed downstream output, usage, and per-call supervisor receipts."""
|
|
69
|
+
|
|
70
|
+
prediction: Any = field(repr=False)
|
|
71
|
+
usage: ResourceUsage = field(default_factory=ResourceUsage)
|
|
72
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
73
|
+
resource_receipts: tuple[ResourceReceipt, ...] = ()
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
if not isinstance(self.usage, ResourceUsage):
|
|
77
|
+
raise TypeError("downstream prediction usage must be ResourceUsage")
|
|
78
|
+
receipts = _validated_resource_receipts(
|
|
79
|
+
self.resource_receipts,
|
|
80
|
+
label="downstream prediction",
|
|
81
|
+
)
|
|
82
|
+
object.__setattr__(self, "metadata", freeze_mapping(self.metadata))
|
|
83
|
+
object.__setattr__(self, "resource_receipts", receipts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@runtime_checkable
|
|
87
|
+
class DownstreamPredictor(Protocol):
|
|
88
|
+
"""Agent/retriever boundary used after an input intervention is applied."""
|
|
89
|
+
|
|
90
|
+
def predict(self, state: FrozenAgentState) -> DownstreamPrediction: ...
|
|
91
|
+
|
|
92
|
+
def fingerprint(self) -> Mapping[str, Any]: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class BenchmarkInterventionArm:
|
|
97
|
+
"""One intervention and an independently constructed downstream runtime."""
|
|
98
|
+
|
|
99
|
+
role: str
|
|
100
|
+
comparison_arm: ComparisonArm
|
|
101
|
+
intervention: Intervention
|
|
102
|
+
context: InterventionContext
|
|
103
|
+
predictor: DownstreamPredictor
|
|
104
|
+
|
|
105
|
+
def __post_init__(self) -> None:
|
|
106
|
+
if self.role not in {"baseline", "treatment"}:
|
|
107
|
+
raise ValidationError("benchmark intervention arm role must be baseline or treatment")
|
|
108
|
+
if self.comparison_arm.release_id != self.intervention.release_id:
|
|
109
|
+
raise ValidationError("comparison arm and intervention release IDs differ")
|
|
110
|
+
if not isinstance(self.context, InterventionContext):
|
|
111
|
+
raise TypeError("benchmark arm context must be InterventionContext")
|
|
112
|
+
if not isinstance(self.predictor, DownstreamPredictor):
|
|
113
|
+
raise TypeError("benchmark arm predictor must implement predict() and fingerprint()")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class BenchmarkArmExecutionRecord:
|
|
118
|
+
"""One arm's intervention, prediction, score, usage, or typed failure."""
|
|
119
|
+
|
|
120
|
+
role: str
|
|
121
|
+
arm_id: str
|
|
122
|
+
status: str
|
|
123
|
+
intervention_record: RunRecord | None = None
|
|
124
|
+
benchmark_record: BenchmarkExampleRecord | None = None
|
|
125
|
+
downstream_usage: ResourceUsage = field(default_factory=ResourceUsage)
|
|
126
|
+
downstream_duration_ms: float = 0.0
|
|
127
|
+
downstream_metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
128
|
+
downstream_resource_receipts: tuple[ResourceReceipt, ...] = ()
|
|
129
|
+
failure: ArmFailure | None = None
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
if self.role not in {"baseline", "treatment"}:
|
|
133
|
+
raise ValidationError("benchmark arm execution role must be baseline or treatment")
|
|
134
|
+
if not isinstance(self.arm_id, str) or not self.arm_id.strip():
|
|
135
|
+
raise ValidationError("benchmark arm execution requires a non-empty arm_id")
|
|
136
|
+
if self.status not in {"succeeded", "failed"}:
|
|
137
|
+
raise ValidationError("benchmark arm status must be succeeded or failed")
|
|
138
|
+
if not isinstance(self.downstream_usage, ResourceUsage):
|
|
139
|
+
raise TypeError("benchmark arm downstream_usage must be ResourceUsage")
|
|
140
|
+
receipts = _validated_resource_receipts(
|
|
141
|
+
self.downstream_resource_receipts,
|
|
142
|
+
label="benchmark arm downstream",
|
|
143
|
+
)
|
|
144
|
+
object.__setattr__(
|
|
145
|
+
self,
|
|
146
|
+
"downstream_metadata",
|
|
147
|
+
freeze_mapping(self.downstream_metadata),
|
|
148
|
+
)
|
|
149
|
+
if (
|
|
150
|
+
isinstance(self.downstream_duration_ms, bool)
|
|
151
|
+
or not isinstance(self.downstream_duration_ms, (int, float))
|
|
152
|
+
or not math.isfinite(self.downstream_duration_ms)
|
|
153
|
+
or self.downstream_duration_ms < 0
|
|
154
|
+
):
|
|
155
|
+
raise ValidationError(
|
|
156
|
+
"benchmark arm downstream_duration_ms must be finite and non-negative"
|
|
157
|
+
)
|
|
158
|
+
object.__setattr__(self, "downstream_duration_ms", float(self.downstream_duration_ms))
|
|
159
|
+
object.__setattr__(self, "downstream_resource_receipts", receipts)
|
|
160
|
+
if self.status == "succeeded":
|
|
161
|
+
if (
|
|
162
|
+
self.intervention_record is None
|
|
163
|
+
or self.benchmark_record is None
|
|
164
|
+
or self.failure is not None
|
|
165
|
+
):
|
|
166
|
+
raise ValidationError(
|
|
167
|
+
"successful benchmark arm requires intervention and benchmark records"
|
|
168
|
+
)
|
|
169
|
+
elif self.benchmark_record is not None or self.failure is None:
|
|
170
|
+
raise ValidationError(
|
|
171
|
+
"failed benchmark arm requires a failure and cannot contain a benchmark score"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def total_usage(self) -> ResourceUsage:
|
|
176
|
+
intervention_usage = (
|
|
177
|
+
self.intervention_record.usage
|
|
178
|
+
if self.intervention_record is not None
|
|
179
|
+
else ResourceUsage()
|
|
180
|
+
)
|
|
181
|
+
return intervention_usage + self.downstream_usage
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class PairedBenchmarkExecutionExample:
|
|
186
|
+
"""Both arm attempts for one shared evaluator-blind input state."""
|
|
187
|
+
|
|
188
|
+
example_id: str
|
|
189
|
+
state_hash: str
|
|
190
|
+
arms: tuple[BenchmarkArmExecutionRecord, ...]
|
|
191
|
+
|
|
192
|
+
def __post_init__(self) -> None:
|
|
193
|
+
arms = tuple(self.arms)
|
|
194
|
+
if len(arms) != 2 or len({arm.arm_id for arm in arms}) != 2:
|
|
195
|
+
raise ValidationError("paired benchmark execution requires two unique arms")
|
|
196
|
+
if {arm.role for arm in arms} != {"baseline", "treatment"}:
|
|
197
|
+
raise ValidationError(
|
|
198
|
+
"paired benchmark execution requires baseline and treatment roles"
|
|
199
|
+
)
|
|
200
|
+
for arm in arms:
|
|
201
|
+
record = arm.benchmark_record
|
|
202
|
+
if record is not None and (
|
|
203
|
+
record.example_id != self.example_id or record.state_hash != self.state_hash
|
|
204
|
+
):
|
|
205
|
+
raise ValidationError(
|
|
206
|
+
"benchmark arm score does not match the paired example identity"
|
|
207
|
+
)
|
|
208
|
+
object.__setattr__(self, "arms", arms)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def complete(self) -> bool:
|
|
212
|
+
return all(arm.status == "succeeded" for arm in self.arms)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(frozen=True, slots=True)
|
|
216
|
+
class PairedBenchmarkExecution:
|
|
217
|
+
"""Auditable high-level result; aggregates exist only for a complete run."""
|
|
218
|
+
|
|
219
|
+
benchmark_id: str
|
|
220
|
+
split: str
|
|
221
|
+
profile_id: str
|
|
222
|
+
balance: BalanceReport
|
|
223
|
+
examples: tuple[PairedBenchmarkExecutionExample, ...]
|
|
224
|
+
observed_dependency_hashes: Mapping[str, Mapping[str, str]]
|
|
225
|
+
paired_record: PairedBenchmarkRecord | None = None
|
|
226
|
+
|
|
227
|
+
def __post_init__(self) -> None:
|
|
228
|
+
examples = tuple(self.examples)
|
|
229
|
+
if not examples:
|
|
230
|
+
raise ValidationError("paired benchmark execution requires at least one example")
|
|
231
|
+
if len({example.example_id for example in examples}) != len(examples):
|
|
232
|
+
raise ValidationError("paired benchmark example IDs must be unique")
|
|
233
|
+
complete = all(example.complete for example in examples)
|
|
234
|
+
if complete != (self.paired_record is not None):
|
|
235
|
+
raise ValidationError(
|
|
236
|
+
"paired aggregate must exist exactly when every example has two successful arms"
|
|
237
|
+
)
|
|
238
|
+
if self.paired_record is not None and (
|
|
239
|
+
self.paired_record.benchmark_id != self.benchmark_id
|
|
240
|
+
or self.paired_record.split != self.split
|
|
241
|
+
):
|
|
242
|
+
raise ValidationError("paired aggregate identity differs from execution identity")
|
|
243
|
+
object.__setattr__(self, "examples", examples)
|
|
244
|
+
object.__setattr__(
|
|
245
|
+
self,
|
|
246
|
+
"observed_dependency_hashes",
|
|
247
|
+
freeze_mapping(self.observed_dependency_hashes),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def complete(self) -> bool:
|
|
252
|
+
return self.paired_record is not None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass(frozen=True, slots=True)
|
|
256
|
+
class BenchmarkBlockArm:
|
|
257
|
+
"""One planned arm in a per-example controlled comparison block."""
|
|
258
|
+
|
|
259
|
+
comparison_arm: ComparisonArm
|
|
260
|
+
intervention: Intervention
|
|
261
|
+
context: InterventionContext
|
|
262
|
+
predictor: DownstreamPredictor
|
|
263
|
+
|
|
264
|
+
def __post_init__(self) -> None:
|
|
265
|
+
if self.comparison_arm.release_id != self.intervention.release_id:
|
|
266
|
+
raise ValidationError("comparison arm and intervention release IDs differ")
|
|
267
|
+
if not isinstance(self.context, InterventionContext):
|
|
268
|
+
raise TypeError("benchmark block arm context must be InterventionContext")
|
|
269
|
+
if not isinstance(self.predictor, DownstreamPredictor):
|
|
270
|
+
raise TypeError(
|
|
271
|
+
"benchmark block arm predictor must implement predict() and fingerprint()"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(frozen=True, slots=True)
|
|
276
|
+
class BenchmarkBlockArmExecutionRecord:
|
|
277
|
+
"""One exact planned arm outcome for a single benchmark example."""
|
|
278
|
+
|
|
279
|
+
arm_id: str
|
|
280
|
+
release_id: str
|
|
281
|
+
status: str
|
|
282
|
+
intervention_record: RunRecord | None = None
|
|
283
|
+
benchmark_record: BenchmarkExampleRecord | None = None
|
|
284
|
+
downstream_usage: ResourceUsage = field(default_factory=ResourceUsage)
|
|
285
|
+
downstream_duration_ms: float = 0.0
|
|
286
|
+
downstream_metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
287
|
+
downstream_resource_receipts: tuple[ResourceReceipt, ...] = ()
|
|
288
|
+
failure: ArmFailure | None = None
|
|
289
|
+
|
|
290
|
+
def __post_init__(self) -> None:
|
|
291
|
+
for label, value in (("arm_id", self.arm_id), ("release_id", self.release_id)):
|
|
292
|
+
if not isinstance(value, str) or not value.strip():
|
|
293
|
+
raise ValidationError(f"benchmark block arm execution requires a non-empty {label}")
|
|
294
|
+
if self.status not in {"succeeded", "failed"}:
|
|
295
|
+
raise ValidationError("benchmark block arm status must be succeeded or failed")
|
|
296
|
+
if not isinstance(self.downstream_usage, ResourceUsage):
|
|
297
|
+
raise TypeError("benchmark block downstream_usage must be ResourceUsage")
|
|
298
|
+
receipts = _validated_resource_receipts(
|
|
299
|
+
self.downstream_resource_receipts,
|
|
300
|
+
label="benchmark block downstream",
|
|
301
|
+
)
|
|
302
|
+
object.__setattr__(
|
|
303
|
+
self,
|
|
304
|
+
"downstream_metadata",
|
|
305
|
+
freeze_mapping(self.downstream_metadata),
|
|
306
|
+
)
|
|
307
|
+
if (
|
|
308
|
+
isinstance(self.downstream_duration_ms, bool)
|
|
309
|
+
or not isinstance(self.downstream_duration_ms, (int, float))
|
|
310
|
+
or not math.isfinite(self.downstream_duration_ms)
|
|
311
|
+
or self.downstream_duration_ms < 0
|
|
312
|
+
):
|
|
313
|
+
raise ValidationError(
|
|
314
|
+
"benchmark block downstream_duration_ms must be finite and non-negative"
|
|
315
|
+
)
|
|
316
|
+
object.__setattr__(self, "downstream_duration_ms", float(self.downstream_duration_ms))
|
|
317
|
+
object.__setattr__(self, "downstream_resource_receipts", receipts)
|
|
318
|
+
if self.status == "succeeded":
|
|
319
|
+
if (
|
|
320
|
+
self.intervention_record is None
|
|
321
|
+
or self.benchmark_record is None
|
|
322
|
+
or self.failure is not None
|
|
323
|
+
):
|
|
324
|
+
raise ValidationError(
|
|
325
|
+
"successful benchmark block arm requires intervention and benchmark records"
|
|
326
|
+
)
|
|
327
|
+
elif self.benchmark_record is not None or self.failure is None:
|
|
328
|
+
raise ValidationError(
|
|
329
|
+
"failed benchmark block arm requires a failure and cannot contain a score"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def total_usage(self) -> ResourceUsage:
|
|
334
|
+
intervention_usage = (
|
|
335
|
+
self.intervention_record.usage
|
|
336
|
+
if self.intervention_record is not None
|
|
337
|
+
else ResourceUsage()
|
|
338
|
+
)
|
|
339
|
+
return intervention_usage + self.downstream_usage
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(frozen=True, slots=True)
|
|
343
|
+
class ControlledMultiArmBlockResult:
|
|
344
|
+
"""One complete attempt of the exact planned arm set on one shared state."""
|
|
345
|
+
|
|
346
|
+
benchmark_id: str
|
|
347
|
+
split: str
|
|
348
|
+
example_id: str
|
|
349
|
+
state_hash: str
|
|
350
|
+
profile_id: str
|
|
351
|
+
baseline_arm_id: str
|
|
352
|
+
planned_arm_ids: tuple[str, ...]
|
|
353
|
+
balance: BalanceReport
|
|
354
|
+
arms: tuple[BenchmarkBlockArmExecutionRecord, ...]
|
|
355
|
+
observed_dependency_hashes: Mapping[str, Mapping[str, str]]
|
|
356
|
+
pre_action: Mapping[str, Any] = field(default_factory=dict)
|
|
357
|
+
|
|
358
|
+
def __post_init__(self) -> None:
|
|
359
|
+
for label, value in (
|
|
360
|
+
("benchmark_id", self.benchmark_id),
|
|
361
|
+
("split", self.split),
|
|
362
|
+
("example_id", self.example_id),
|
|
363
|
+
("state_hash", self.state_hash),
|
|
364
|
+
("profile_id", self.profile_id),
|
|
365
|
+
("baseline_arm_id", self.baseline_arm_id),
|
|
366
|
+
):
|
|
367
|
+
if not isinstance(value, str) or not value.strip():
|
|
368
|
+
raise ValidationError(f"controlled block {label} must be non-empty")
|
|
369
|
+
planned = tuple(self.planned_arm_ids)
|
|
370
|
+
arms = tuple(self.arms)
|
|
371
|
+
if len(planned) < 2 or len(planned) != len(set(planned)):
|
|
372
|
+
raise ValidationError("controlled block requires at least two unique planned arms")
|
|
373
|
+
if self.baseline_arm_id not in planned:
|
|
374
|
+
raise ValidationError("controlled block baseline must belong to the planned arm set")
|
|
375
|
+
if tuple(arm.arm_id for arm in arms) != planned:
|
|
376
|
+
raise ValidationError(
|
|
377
|
+
"controlled block outcomes must exactly match planned arm IDs and order"
|
|
378
|
+
)
|
|
379
|
+
if not isinstance(self.balance, BalanceReport) or not self.balance.eligible:
|
|
380
|
+
raise ValidationError("controlled block requires an eligible balance report")
|
|
381
|
+
successful_metric_keys: tuple[str, ...] | None = None
|
|
382
|
+
for arm in arms:
|
|
383
|
+
record = arm.benchmark_record
|
|
384
|
+
if record is not None:
|
|
385
|
+
if record.example_id != self.example_id or record.state_hash != self.state_hash:
|
|
386
|
+
raise ValidationError(
|
|
387
|
+
"controlled block arm score differs from the shared example identity"
|
|
388
|
+
)
|
|
389
|
+
keys = tuple(sorted(record.metrics))
|
|
390
|
+
if successful_metric_keys is None:
|
|
391
|
+
successful_metric_keys = keys
|
|
392
|
+
elif keys != successful_metric_keys:
|
|
393
|
+
raise ValidationError("controlled block successful arm metric schemas differ")
|
|
394
|
+
object.__setattr__(self, "planned_arm_ids", planned)
|
|
395
|
+
object.__setattr__(self, "arms", arms)
|
|
396
|
+
object.__setattr__(
|
|
397
|
+
self,
|
|
398
|
+
"observed_dependency_hashes",
|
|
399
|
+
freeze_mapping(self.observed_dependency_hashes),
|
|
400
|
+
)
|
|
401
|
+
object.__setattr__(self, "pre_action", freeze_mapping(self.pre_action))
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def all_arms_succeeded(self) -> bool:
|
|
405
|
+
return all(arm.status == "succeeded" for arm in self.arms)
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def metric_deltas(self) -> Mapping[str, Mapping[str, float]]:
|
|
409
|
+
"""Return successful-arm effects against the one shared NO_OP outcome."""
|
|
410
|
+
|
|
411
|
+
by_id = {arm.arm_id: arm for arm in self.arms}
|
|
412
|
+
baseline = by_id[self.baseline_arm_id].benchmark_record
|
|
413
|
+
if baseline is None:
|
|
414
|
+
return freeze_mapping({})
|
|
415
|
+
deltas: dict[str, Mapping[str, float]] = {}
|
|
416
|
+
for arm in self.arms:
|
|
417
|
+
if arm.arm_id == self.baseline_arm_id or arm.benchmark_record is None:
|
|
418
|
+
continue
|
|
419
|
+
deltas[arm.arm_id] = {
|
|
420
|
+
key: arm.benchmark_record.metrics[key] - baseline.metrics[key]
|
|
421
|
+
for key in baseline.metrics
|
|
422
|
+
}
|
|
423
|
+
return freeze_mapping(deltas)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class ControlledMultiArmBlockRunner:
|
|
427
|
+
"""Execute one exact multi-arm block without replaying the full split.
|
|
428
|
+
|
|
429
|
+
The baseline is one explicitly declared ``SemanticFamily.NO_OP`` arm. It is
|
|
430
|
+
constructed and executed once, alongside each treatment on the same frozen
|
|
431
|
+
pre-action state. This prevents an N-arm experiment from silently becoming
|
|
432
|
+
N-1 independently repeated two-arm studies.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def __init__(self, *, intervention_runner: InterventionRunner | None = None) -> None:
|
|
436
|
+
self._intervention_runner = intervention_runner or InterventionRunner()
|
|
437
|
+
self._balance = BalanceAuditor()
|
|
438
|
+
|
|
439
|
+
def run_one(
|
|
440
|
+
self,
|
|
441
|
+
adapter: BenchmarkAdapter,
|
|
442
|
+
split: str,
|
|
443
|
+
example_id: str,
|
|
444
|
+
profile: ComparisonProfile,
|
|
445
|
+
arms: tuple[BenchmarkBlockArm, ...],
|
|
446
|
+
*,
|
|
447
|
+
planned_arm_ids: tuple[str, ...],
|
|
448
|
+
baseline_arm_id: str,
|
|
449
|
+
budget: ExecutionBudget | None = None,
|
|
450
|
+
pre_action_observer: Callable[[FrozenAgentState], Mapping[str, Any]] | None = None,
|
|
451
|
+
) -> ControlledMultiArmBlockResult:
|
|
452
|
+
normalized_arms = tuple(arms)
|
|
453
|
+
planned = tuple(planned_arm_ids)
|
|
454
|
+
_validate_block_inputs(
|
|
455
|
+
adapter,
|
|
456
|
+
split,
|
|
457
|
+
example_id,
|
|
458
|
+
profile,
|
|
459
|
+
normalized_arms,
|
|
460
|
+
planned,
|
|
461
|
+
baseline_arm_id,
|
|
462
|
+
budget,
|
|
463
|
+
)
|
|
464
|
+
_reject_shared_execution_instances(normalized_arms)
|
|
465
|
+
before = _observed_fingerprints(normalized_arms)
|
|
466
|
+
_require_equal_predictor_fingerprints(before)
|
|
467
|
+
balance = self._audit(profile, normalized_arms, before)
|
|
468
|
+
|
|
469
|
+
example = _resolve_example(adapter, split, example_id)
|
|
470
|
+
state = example.visible_state(budget=budget)
|
|
471
|
+
pre_action: Mapping[str, Any] = {}
|
|
472
|
+
if pre_action_observer is not None:
|
|
473
|
+
observed = pre_action_observer(state)
|
|
474
|
+
if not isinstance(observed, Mapping) or not observed:
|
|
475
|
+
raise ValidationError(
|
|
476
|
+
"pre_action_observer must return a non-empty canonical mapping"
|
|
477
|
+
)
|
|
478
|
+
try:
|
|
479
|
+
pre_action = freeze_mapping(observed)
|
|
480
|
+
except (TypeError, ValueError) as exc:
|
|
481
|
+
raise ValidationError(
|
|
482
|
+
f"pre_action_observer returned a non-canonical mapping: {exc}"
|
|
483
|
+
) from exc
|
|
484
|
+
records = tuple(
|
|
485
|
+
self._run_arm(
|
|
486
|
+
adapter,
|
|
487
|
+
example,
|
|
488
|
+
state,
|
|
489
|
+
arm,
|
|
490
|
+
require_no_op=arm.comparison_arm.arm_id == baseline_arm_id,
|
|
491
|
+
)
|
|
492
|
+
for arm in normalized_arms
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
current = _observed_fingerprints(normalized_arms)
|
|
496
|
+
if current != before:
|
|
497
|
+
raise BalanceError(
|
|
498
|
+
"runtime or downstream predictor identity changed during controlled block"
|
|
499
|
+
)
|
|
500
|
+
balance = self._audit(profile, normalized_arms, current)
|
|
501
|
+
return ControlledMultiArmBlockResult(
|
|
502
|
+
benchmark_id=str(adapter.benchmark_id),
|
|
503
|
+
split=split,
|
|
504
|
+
example_id=example.example_id,
|
|
505
|
+
state_hash=state.state_hash,
|
|
506
|
+
profile_id=profile.profile_id,
|
|
507
|
+
baseline_arm_id=baseline_arm_id,
|
|
508
|
+
planned_arm_ids=planned,
|
|
509
|
+
balance=balance,
|
|
510
|
+
arms=records,
|
|
511
|
+
observed_dependency_hashes=current,
|
|
512
|
+
pre_action=pre_action,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _audit(
|
|
516
|
+
self,
|
|
517
|
+
profile: ComparisonProfile,
|
|
518
|
+
arms: tuple[BenchmarkBlockArm, ...],
|
|
519
|
+
fingerprints: Mapping[str, Mapping[str, str]],
|
|
520
|
+
) -> BalanceReport:
|
|
521
|
+
report = self._balance.audit(
|
|
522
|
+
profile,
|
|
523
|
+
tuple(arm.comparison_arm for arm in arms),
|
|
524
|
+
runtime_fingerprints=fingerprints,
|
|
525
|
+
)
|
|
526
|
+
report.require_eligible()
|
|
527
|
+
return report
|
|
528
|
+
|
|
529
|
+
def _run_arm(
|
|
530
|
+
self,
|
|
531
|
+
adapter: BenchmarkAdapter,
|
|
532
|
+
example: BenchmarkExample,
|
|
533
|
+
state: FrozenAgentState,
|
|
534
|
+
arm: BenchmarkBlockArm,
|
|
535
|
+
*,
|
|
536
|
+
require_no_op: bool,
|
|
537
|
+
) -> BenchmarkBlockArmExecutionRecord:
|
|
538
|
+
intervention_record: RunRecord | None = None
|
|
539
|
+
downstream_usage = ResourceUsage()
|
|
540
|
+
downstream_duration_ms = 0.0
|
|
541
|
+
downstream_metadata: Mapping[str, Any] = {}
|
|
542
|
+
downstream_resource_receipts: tuple[ResourceReceipt, ...] = ()
|
|
543
|
+
monitor = arm.context.monitor or self._intervention_runner.monitor
|
|
544
|
+
if monitor is not None and not isinstance(monitor, RunMonitor):
|
|
545
|
+
raise TypeError("context.monitor must be a RunMonitor")
|
|
546
|
+
prior_run_ids = (
|
|
547
|
+
frozenset(snapshot.run_id for snapshot in monitor.list_snapshots())
|
|
548
|
+
if monitor is not None
|
|
549
|
+
else frozenset()
|
|
550
|
+
)
|
|
551
|
+
try:
|
|
552
|
+
intervention_record = self._intervention_runner.run(
|
|
553
|
+
arm.intervention,
|
|
554
|
+
state,
|
|
555
|
+
arm.context,
|
|
556
|
+
)
|
|
557
|
+
if require_no_op and any(
|
|
558
|
+
operation.op is not PatchOp.NO_OP
|
|
559
|
+
for operation in intervention_record.patch.operations
|
|
560
|
+
):
|
|
561
|
+
raise BalanceError("declared controlled-block baseline did not produce NO_OP")
|
|
562
|
+
transformed = intervention_record.patch.apply(state)
|
|
563
|
+
started = time.perf_counter()
|
|
564
|
+
downstream = arm.predictor.predict(transformed)
|
|
565
|
+
downstream_duration_ms = (time.perf_counter() - started) * 1_000.0
|
|
566
|
+
if not isinstance(downstream, DownstreamPrediction):
|
|
567
|
+
raise TypeError(
|
|
568
|
+
"downstream predictor must return DownstreamPrediction, "
|
|
569
|
+
f"got {type(downstream).__name__}"
|
|
570
|
+
)
|
|
571
|
+
downstream_usage = downstream.usage
|
|
572
|
+
downstream_metadata = downstream.metadata
|
|
573
|
+
downstream_resource_receipts = downstream.resource_receipts
|
|
574
|
+
_require_total_usage_within_budget(
|
|
575
|
+
state,
|
|
576
|
+
intervention_record=intervention_record,
|
|
577
|
+
downstream_usage=downstream_usage,
|
|
578
|
+
)
|
|
579
|
+
benchmark_record = build_benchmark_example_record(
|
|
580
|
+
adapter,
|
|
581
|
+
example,
|
|
582
|
+
state_hash=state.state_hash,
|
|
583
|
+
prediction=downstream.prediction,
|
|
584
|
+
)
|
|
585
|
+
except ArmExecutionError as exc:
|
|
586
|
+
downstream_resource_receipts = _failure_resource_receipts(
|
|
587
|
+
exc,
|
|
588
|
+
downstream_resource_receipts,
|
|
589
|
+
)
|
|
590
|
+
monitor_run_id = (
|
|
591
|
+
intervention_record.monitor_run_id
|
|
592
|
+
if intervention_record is not None
|
|
593
|
+
else _find_new_monitor_run_id(
|
|
594
|
+
monitor,
|
|
595
|
+
prior_run_ids,
|
|
596
|
+
release_id=arm.intervention.release_id,
|
|
597
|
+
state_hash=state.state_hash,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
return BenchmarkBlockArmExecutionRecord(
|
|
601
|
+
arm_id=arm.comparison_arm.arm_id,
|
|
602
|
+
release_id=arm.intervention.release_id,
|
|
603
|
+
status="failed",
|
|
604
|
+
intervention_record=intervention_record,
|
|
605
|
+
downstream_usage=downstream_usage,
|
|
606
|
+
downstream_duration_ms=downstream_duration_ms,
|
|
607
|
+
downstream_metadata=downstream_metadata,
|
|
608
|
+
downstream_resource_receipts=downstream_resource_receipts,
|
|
609
|
+
failure=ArmFailure(
|
|
610
|
+
category=type(exc).__name__,
|
|
611
|
+
message=str(exc),
|
|
612
|
+
monitor_run_id=monitor_run_id,
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
return BenchmarkBlockArmExecutionRecord(
|
|
616
|
+
arm_id=arm.comparison_arm.arm_id,
|
|
617
|
+
release_id=arm.intervention.release_id,
|
|
618
|
+
status="succeeded",
|
|
619
|
+
intervention_record=intervention_record,
|
|
620
|
+
benchmark_record=benchmark_record,
|
|
621
|
+
downstream_usage=downstream_usage,
|
|
622
|
+
downstream_duration_ms=downstream_duration_ms,
|
|
623
|
+
downstream_metadata=downstream_metadata,
|
|
624
|
+
downstream_resource_receipts=downstream_resource_receipts,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class PairedInterventionBenchmarkRunner:
|
|
629
|
+
"""Run two balanced intervention arms and score their downstream outcomes."""
|
|
630
|
+
|
|
631
|
+
def __init__(self, *, intervention_runner: InterventionRunner | None = None) -> None:
|
|
632
|
+
self._intervention_runner = intervention_runner or InterventionRunner()
|
|
633
|
+
self._balance = BalanceAuditor()
|
|
634
|
+
|
|
635
|
+
def run(
|
|
636
|
+
self,
|
|
637
|
+
adapter: BenchmarkAdapter,
|
|
638
|
+
split: str,
|
|
639
|
+
profile: ComparisonProfile,
|
|
640
|
+
arms: tuple[BenchmarkInterventionArm, ...],
|
|
641
|
+
*,
|
|
642
|
+
budget: ExecutionBudget | None = None,
|
|
643
|
+
) -> PairedBenchmarkExecution:
|
|
644
|
+
candidate_arms = tuple(arms)
|
|
645
|
+
_validate_run_inputs(adapter, split, profile, candidate_arms, budget)
|
|
646
|
+
by_role = {arm.role: arm for arm in candidate_arms}
|
|
647
|
+
normalized_arms = (by_role["baseline"], by_role["treatment"])
|
|
648
|
+
_reject_shared_execution_instances(normalized_arms)
|
|
649
|
+
before = _observed_fingerprints(normalized_arms)
|
|
650
|
+
_require_equal_predictor_fingerprints(before)
|
|
651
|
+
balance = self._audit(profile, normalized_arms, before)
|
|
652
|
+
|
|
653
|
+
examples = tuple(adapter.examples(split))
|
|
654
|
+
_validate_examples(examples)
|
|
655
|
+
execution_examples: list[PairedBenchmarkExecutionExample] = []
|
|
656
|
+
paired_examples: list[PairedBenchmarkExampleRecord] = []
|
|
657
|
+
|
|
658
|
+
for example in examples:
|
|
659
|
+
state = example.visible_state(budget=budget)
|
|
660
|
+
arm_records = tuple(
|
|
661
|
+
self._run_arm(adapter, example, state, arm) for arm in normalized_arms
|
|
662
|
+
)
|
|
663
|
+
execution_example = PairedBenchmarkExecutionExample(
|
|
664
|
+
example_id=example.example_id,
|
|
665
|
+
state_hash=state.state_hash,
|
|
666
|
+
arms=arm_records,
|
|
667
|
+
)
|
|
668
|
+
execution_examples.append(execution_example)
|
|
669
|
+
if execution_example.complete:
|
|
670
|
+
records_by_role = {record.role: record for record in arm_records}
|
|
671
|
+
baseline = records_by_role["baseline"].benchmark_record
|
|
672
|
+
treatment = records_by_role["treatment"].benchmark_record
|
|
673
|
+
if baseline is None or treatment is None: # pragma: no cover - dataclass invariant
|
|
674
|
+
raise RuntimeError("complete paired execution lost one benchmark record")
|
|
675
|
+
paired_examples.append(
|
|
676
|
+
PairedBenchmarkExampleRecord(
|
|
677
|
+
example_id=example.example_id,
|
|
678
|
+
state_hash=state.state_hash,
|
|
679
|
+
baseline=baseline,
|
|
680
|
+
treatment=treatment,
|
|
681
|
+
metric_deltas={
|
|
682
|
+
key: treatment.metrics[key] - baseline.metrics[key]
|
|
683
|
+
for key in baseline.metrics
|
|
684
|
+
},
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
current = _observed_fingerprints(normalized_arms)
|
|
689
|
+
if current != before:
|
|
690
|
+
raise BalanceError(
|
|
691
|
+
"runtime or downstream predictor identity changed during benchmark execution"
|
|
692
|
+
)
|
|
693
|
+
balance = self._audit(profile, normalized_arms, current)
|
|
694
|
+
|
|
695
|
+
paired_record = None
|
|
696
|
+
if len(paired_examples) == len(execution_examples):
|
|
697
|
+
baseline_metrics = _aggregate_metrics(
|
|
698
|
+
tuple(record.baseline.metrics for record in paired_examples)
|
|
699
|
+
)
|
|
700
|
+
treatment_metrics = _aggregate_metrics(
|
|
701
|
+
tuple(record.treatment.metrics for record in paired_examples)
|
|
702
|
+
)
|
|
703
|
+
paired_record = PairedBenchmarkRecord(
|
|
704
|
+
benchmark_id=str(adapter.benchmark_id),
|
|
705
|
+
split=split,
|
|
706
|
+
examples=tuple(paired_examples),
|
|
707
|
+
baseline_metrics=baseline_metrics,
|
|
708
|
+
treatment_metrics=treatment_metrics,
|
|
709
|
+
metric_deltas={
|
|
710
|
+
key: treatment_metrics[key] - baseline_metrics[key] for key in baseline_metrics
|
|
711
|
+
},
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
return PairedBenchmarkExecution(
|
|
715
|
+
benchmark_id=str(adapter.benchmark_id),
|
|
716
|
+
split=split,
|
|
717
|
+
profile_id=profile.profile_id,
|
|
718
|
+
balance=balance,
|
|
719
|
+
examples=tuple(execution_examples),
|
|
720
|
+
observed_dependency_hashes=before,
|
|
721
|
+
paired_record=paired_record,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def _audit(
|
|
725
|
+
self,
|
|
726
|
+
profile: ComparisonProfile,
|
|
727
|
+
arms: tuple[BenchmarkInterventionArm, ...],
|
|
728
|
+
fingerprints: Mapping[str, Mapping[str, str]],
|
|
729
|
+
) -> BalanceReport:
|
|
730
|
+
report = self._balance.audit(
|
|
731
|
+
profile,
|
|
732
|
+
tuple(arm.comparison_arm for arm in arms),
|
|
733
|
+
runtime_fingerprints=fingerprints,
|
|
734
|
+
)
|
|
735
|
+
report.require_eligible()
|
|
736
|
+
return report
|
|
737
|
+
|
|
738
|
+
def _run_arm(
|
|
739
|
+
self,
|
|
740
|
+
adapter: BenchmarkAdapter,
|
|
741
|
+
example: BenchmarkExample,
|
|
742
|
+
state: FrozenAgentState,
|
|
743
|
+
arm: BenchmarkInterventionArm,
|
|
744
|
+
) -> BenchmarkArmExecutionRecord:
|
|
745
|
+
intervention_record: RunRecord | None = None
|
|
746
|
+
downstream_usage = ResourceUsage()
|
|
747
|
+
downstream_duration_ms = 0.0
|
|
748
|
+
downstream_metadata: Mapping[str, Any] = {}
|
|
749
|
+
downstream_resource_receipts: tuple[ResourceReceipt, ...] = ()
|
|
750
|
+
monitor = arm.context.monitor or self._intervention_runner.monitor
|
|
751
|
+
if monitor is not None and not isinstance(monitor, RunMonitor):
|
|
752
|
+
raise TypeError("context.monitor must be a RunMonitor")
|
|
753
|
+
prior_run_ids = (
|
|
754
|
+
frozenset(snapshot.run_id for snapshot in monitor.list_snapshots())
|
|
755
|
+
if monitor is not None
|
|
756
|
+
else frozenset()
|
|
757
|
+
)
|
|
758
|
+
try:
|
|
759
|
+
intervention_record = self._intervention_runner.run(
|
|
760
|
+
arm.intervention,
|
|
761
|
+
state,
|
|
762
|
+
arm.context,
|
|
763
|
+
)
|
|
764
|
+
transformed = intervention_record.patch.apply(state)
|
|
765
|
+
started = time.perf_counter()
|
|
766
|
+
downstream = arm.predictor.predict(transformed)
|
|
767
|
+
downstream_duration_ms = (time.perf_counter() - started) * 1_000.0
|
|
768
|
+
if not isinstance(downstream, DownstreamPrediction):
|
|
769
|
+
raise TypeError(
|
|
770
|
+
"downstream predictor must return DownstreamPrediction, "
|
|
771
|
+
f"got {type(downstream).__name__}"
|
|
772
|
+
)
|
|
773
|
+
downstream_usage = downstream.usage
|
|
774
|
+
downstream_metadata = downstream.metadata
|
|
775
|
+
downstream_resource_receipts = downstream.resource_receipts
|
|
776
|
+
_require_total_usage_within_budget(
|
|
777
|
+
state,
|
|
778
|
+
intervention_record=intervention_record,
|
|
779
|
+
downstream_usage=downstream_usage,
|
|
780
|
+
)
|
|
781
|
+
benchmark_record = build_benchmark_example_record(
|
|
782
|
+
adapter,
|
|
783
|
+
example,
|
|
784
|
+
state_hash=state.state_hash,
|
|
785
|
+
prediction=downstream.prediction,
|
|
786
|
+
)
|
|
787
|
+
except ArmExecutionError as exc:
|
|
788
|
+
downstream_resource_receipts = _failure_resource_receipts(
|
|
789
|
+
exc,
|
|
790
|
+
downstream_resource_receipts,
|
|
791
|
+
)
|
|
792
|
+
monitor_run_id = (
|
|
793
|
+
intervention_record.monitor_run_id
|
|
794
|
+
if intervention_record is not None
|
|
795
|
+
else _find_new_monitor_run_id(
|
|
796
|
+
monitor,
|
|
797
|
+
prior_run_ids,
|
|
798
|
+
release_id=arm.intervention.release_id,
|
|
799
|
+
state_hash=state.state_hash,
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
return BenchmarkArmExecutionRecord(
|
|
803
|
+
role=arm.role,
|
|
804
|
+
arm_id=arm.comparison_arm.arm_id,
|
|
805
|
+
status="failed",
|
|
806
|
+
intervention_record=intervention_record,
|
|
807
|
+
downstream_usage=downstream_usage,
|
|
808
|
+
downstream_duration_ms=downstream_duration_ms,
|
|
809
|
+
downstream_metadata=downstream_metadata,
|
|
810
|
+
downstream_resource_receipts=downstream_resource_receipts,
|
|
811
|
+
failure=ArmFailure(
|
|
812
|
+
category=type(exc).__name__,
|
|
813
|
+
message=str(exc),
|
|
814
|
+
monitor_run_id=monitor_run_id,
|
|
815
|
+
),
|
|
816
|
+
)
|
|
817
|
+
return BenchmarkArmExecutionRecord(
|
|
818
|
+
role=arm.role,
|
|
819
|
+
arm_id=arm.comparison_arm.arm_id,
|
|
820
|
+
status="succeeded",
|
|
821
|
+
intervention_record=intervention_record,
|
|
822
|
+
benchmark_record=benchmark_record,
|
|
823
|
+
downstream_usage=downstream_usage,
|
|
824
|
+
downstream_duration_ms=downstream_duration_ms,
|
|
825
|
+
downstream_metadata=downstream_metadata,
|
|
826
|
+
downstream_resource_receipts=downstream_resource_receipts,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _require_total_usage_within_budget(
|
|
831
|
+
state: FrozenAgentState,
|
|
832
|
+
*,
|
|
833
|
+
intervention_record: RunRecord,
|
|
834
|
+
downstream_usage: ResourceUsage,
|
|
835
|
+
) -> None:
|
|
836
|
+
"""Enforce one budget across intervention and downstream agent execution.
|
|
837
|
+
|
|
838
|
+
The intervention runtime owns its own reservation ledger, while a
|
|
839
|
+
downstream predictor reports usage only after its side effects have
|
|
840
|
+
occurred. Replaying both actual usage records through a fresh ledger makes
|
|
841
|
+
the experiment-level budget cover the complete arm without double-charging
|
|
842
|
+
either runtime. Any overage remains an infrastructure-level cell failure,
|
|
843
|
+
not an algorithm-quality outcome.
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
ledger = BudgetLedger(state.budget)
|
|
847
|
+
ledger.charge_actual(intervention_record.usage + downstream_usage)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _validated_resource_receipts(
|
|
851
|
+
values: tuple[ResourceReceipt, ...],
|
|
852
|
+
*,
|
|
853
|
+
label: str,
|
|
854
|
+
) -> tuple[ResourceReceipt, ...]:
|
|
855
|
+
receipts = tuple(values)
|
|
856
|
+
if any(not isinstance(receipt, ResourceReceipt) for receipt in receipts):
|
|
857
|
+
raise TypeError(f"{label} resource receipts must contain ResourceReceipt values")
|
|
858
|
+
return receipts
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _failure_resource_receipts(
|
|
862
|
+
error: ArmExecutionError,
|
|
863
|
+
current: tuple[ResourceReceipt, ...],
|
|
864
|
+
) -> tuple[ResourceReceipt, ...]:
|
|
865
|
+
receipt = getattr(error, "resource_receipt", None)
|
|
866
|
+
if receipt is None:
|
|
867
|
+
return current
|
|
868
|
+
if not isinstance(receipt, ResourceReceipt):
|
|
869
|
+
raise TypeError("arm failure resource_receipt must be a ResourceReceipt")
|
|
870
|
+
return current if receipt in current else (*current, receipt)
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _validate_run_inputs(
|
|
874
|
+
adapter: BenchmarkAdapter,
|
|
875
|
+
split: str,
|
|
876
|
+
profile: ComparisonProfile,
|
|
877
|
+
arms: tuple[BenchmarkInterventionArm, ...],
|
|
878
|
+
budget: ExecutionBudget | None,
|
|
879
|
+
) -> None:
|
|
880
|
+
benchmark_id = getattr(adapter, "benchmark_id", None)
|
|
881
|
+
if not isinstance(benchmark_id, str) or not benchmark_id.strip():
|
|
882
|
+
raise ValidationError("adapter.benchmark_id must be a non-empty string")
|
|
883
|
+
if getattr(adapter, "pairing_mode", None) != "stateless_score_twice":
|
|
884
|
+
raise ValidationError(
|
|
885
|
+
"paired intervention benchmark execution requires pairing_mode="
|
|
886
|
+
"'stateless_score_twice'; use the atomic external replay runner for "
|
|
887
|
+
"stateful arenas"
|
|
888
|
+
)
|
|
889
|
+
if not isinstance(split, str) or not split.strip():
|
|
890
|
+
raise ValidationError("benchmark split must be a non-empty string")
|
|
891
|
+
if not isinstance(profile, ComparisonProfile):
|
|
892
|
+
raise TypeError("profile must be a ComparisonProfile")
|
|
893
|
+
if not all(isinstance(arm, BenchmarkInterventionArm) for arm in arms):
|
|
894
|
+
raise TypeError("arms must be BenchmarkInterventionArm instances")
|
|
895
|
+
if len(arms) != 2 or len({arm.comparison_arm.arm_id for arm in arms}) != 2:
|
|
896
|
+
raise ValidationError("paired intervention benchmark runner requires two unique arms")
|
|
897
|
+
if {arm.role for arm in arms} != {"baseline", "treatment"}:
|
|
898
|
+
raise ValidationError(
|
|
899
|
+
"paired intervention benchmark runner requires baseline and treatment roles"
|
|
900
|
+
)
|
|
901
|
+
if budget is not None and not isinstance(budget, ExecutionBudget):
|
|
902
|
+
raise TypeError("budget must be an ExecutionBudget or None")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _validate_block_inputs(
|
|
906
|
+
adapter: BenchmarkAdapter,
|
|
907
|
+
split: str,
|
|
908
|
+
example_id: str,
|
|
909
|
+
profile: ComparisonProfile,
|
|
910
|
+
arms: tuple[BenchmarkBlockArm, ...],
|
|
911
|
+
planned_arm_ids: tuple[str, ...],
|
|
912
|
+
baseline_arm_id: str,
|
|
913
|
+
budget: ExecutionBudget | None,
|
|
914
|
+
) -> None:
|
|
915
|
+
benchmark_id = getattr(adapter, "benchmark_id", None)
|
|
916
|
+
if not isinstance(benchmark_id, str) or not benchmark_id.strip():
|
|
917
|
+
raise ValidationError("adapter.benchmark_id must be a non-empty string")
|
|
918
|
+
if getattr(adapter, "pairing_mode", None) != "stateless_score_twice":
|
|
919
|
+
raise ValidationError(
|
|
920
|
+
"controlled multi-arm blocks require pairing_mode='stateless_score_twice'; "
|
|
921
|
+
"stateful arenas require an atomic external replay bridge"
|
|
922
|
+
)
|
|
923
|
+
if not isinstance(split, str) or not split.strip():
|
|
924
|
+
raise ValidationError("controlled block split must be non-empty")
|
|
925
|
+
if not isinstance(example_id, str) or not example_id.strip():
|
|
926
|
+
raise ValidationError("controlled block example_id must be non-empty")
|
|
927
|
+
if not isinstance(profile, ComparisonProfile):
|
|
928
|
+
raise TypeError("profile must be a ComparisonProfile")
|
|
929
|
+
if not all(isinstance(arm, BenchmarkBlockArm) for arm in arms):
|
|
930
|
+
raise TypeError("controlled block arms must be BenchmarkBlockArm instances")
|
|
931
|
+
if len(planned_arm_ids) < 2 or len(planned_arm_ids) != len(set(planned_arm_ids)):
|
|
932
|
+
raise ValidationError("planned_arm_ids must contain at least two unique arms")
|
|
933
|
+
observed_arm_ids = tuple(arm.comparison_arm.arm_id for arm in arms)
|
|
934
|
+
if observed_arm_ids != planned_arm_ids:
|
|
935
|
+
raise ValidationError(
|
|
936
|
+
"controlled block arm IDs and order must exactly match planned_arm_ids"
|
|
937
|
+
)
|
|
938
|
+
if baseline_arm_id not in planned_arm_ids:
|
|
939
|
+
raise ValidationError("controlled block baseline_arm_id is outside planned_arm_ids")
|
|
940
|
+
no_op_ids = tuple(
|
|
941
|
+
arm.comparison_arm.arm_id for arm in arms if arm.intervention.family is SemanticFamily.NO_OP
|
|
942
|
+
)
|
|
943
|
+
if no_op_ids != (baseline_arm_id,):
|
|
944
|
+
raise ValidationError(
|
|
945
|
+
"controlled block requires exactly one declared NO_OP arm matching baseline_arm_id"
|
|
946
|
+
)
|
|
947
|
+
if budget is not None and not isinstance(budget, ExecutionBudget):
|
|
948
|
+
raise TypeError("budget must be an ExecutionBudget or None")
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _resolve_example(
|
|
952
|
+
adapter: BenchmarkAdapter,
|
|
953
|
+
split: str,
|
|
954
|
+
example_id: str,
|
|
955
|
+
) -> BenchmarkExample:
|
|
956
|
+
resolver = getattr(adapter, "example", None)
|
|
957
|
+
if callable(resolver):
|
|
958
|
+
example = resolver(split, example_id)
|
|
959
|
+
if not isinstance(example, BenchmarkExample):
|
|
960
|
+
raise ValidationError("adapter.example() must return a BenchmarkExample")
|
|
961
|
+
if example.example_id != example_id:
|
|
962
|
+
raise ValidationError("adapter.example() returned another example identity")
|
|
963
|
+
return example
|
|
964
|
+
examples = tuple(adapter.examples(split))
|
|
965
|
+
_validate_examples(examples)
|
|
966
|
+
matches = tuple(example for example in examples if example.example_id == example_id)
|
|
967
|
+
if len(matches) != 1:
|
|
968
|
+
raise ValidationError(
|
|
969
|
+
f"benchmark split must contain exactly one example with ID {example_id!r}"
|
|
970
|
+
)
|
|
971
|
+
return matches[0]
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _validate_examples(examples: tuple[BenchmarkExample, ...]) -> None:
|
|
975
|
+
if not examples:
|
|
976
|
+
raise ValidationError("benchmark split must contain at least one example")
|
|
977
|
+
if not all(isinstance(example, BenchmarkExample) for example in examples):
|
|
978
|
+
raise ValidationError("adapter.examples() must return BenchmarkExample instances")
|
|
979
|
+
identifiers = [example.example_id for example in examples]
|
|
980
|
+
if any(not isinstance(identifier, str) or not identifier.strip() for identifier in identifiers):
|
|
981
|
+
raise ValidationError("benchmark example IDs must be non-empty strings")
|
|
982
|
+
if len(identifiers) != len(set(identifiers)):
|
|
983
|
+
raise ValidationError("benchmark example IDs must be unique")
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _observed_fingerprints(
|
|
987
|
+
arms: tuple[BenchmarkInterventionArm | BenchmarkBlockArm, ...],
|
|
988
|
+
) -> dict[str, dict[str, str]]:
|
|
989
|
+
observed: dict[str, dict[str, str]] = {}
|
|
990
|
+
for arm in arms:
|
|
991
|
+
arm_id = arm.comparison_arm.arm_id
|
|
992
|
+
fingerprints = context_dependency_fingerprints(arm.context)
|
|
993
|
+
fingerprint = arm.predictor.fingerprint()
|
|
994
|
+
if not isinstance(fingerprint, Mapping) or not fingerprint:
|
|
995
|
+
raise ValidationError("downstream predictor fingerprint must be a non-empty mapping")
|
|
996
|
+
try:
|
|
997
|
+
fingerprints[_PREDICTOR_FINGERPRINT_SLOT] = content_hash(freeze_mapping(fingerprint))
|
|
998
|
+
except (TypeError, ValueError) as exc:
|
|
999
|
+
raise ValidationError(
|
|
1000
|
+
f"downstream predictor fingerprint must be canonical: {exc}"
|
|
1001
|
+
) from exc
|
|
1002
|
+
observed[arm_id] = fingerprints
|
|
1003
|
+
return observed
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def _require_equal_predictor_fingerprints(
|
|
1007
|
+
observed: Mapping[str, Mapping[str, str]],
|
|
1008
|
+
) -> None:
|
|
1009
|
+
hashes = {fingerprints[_PREDICTOR_FINGERPRINT_SLOT] for fingerprints in observed.values()}
|
|
1010
|
+
if len(hashes) != 1:
|
|
1011
|
+
raise BalanceError("downstream predictor fingerprint differs across comparison arms")
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _reject_shared_execution_instances(
|
|
1015
|
+
arms: tuple[BenchmarkInterventionArm | BenchmarkBlockArm, ...],
|
|
1016
|
+
) -> None:
|
|
1017
|
+
if len({id(arm.context) for arm in arms}) != len(arms):
|
|
1018
|
+
raise BalanceError("comparison arms require independently constructed contexts")
|
|
1019
|
+
if len({id(arm.predictor) for arm in arms}) != len(arms):
|
|
1020
|
+
raise BalanceError("comparison arms require independently constructed predictors")
|
|
1021
|
+
|
|
1022
|
+
owners: dict[int, set[str]] = {}
|
|
1023
|
+
ledger_owners: dict[int, set[str]] = {}
|
|
1024
|
+
for arm in arms:
|
|
1025
|
+
arm_id = arm.comparison_arm.arm_id
|
|
1026
|
+
if isinstance(arm.context.budget_ledger, BudgetLedger):
|
|
1027
|
+
ledger_owners.setdefault(id(arm.context.budget_ledger), set()).add(arm_id)
|
|
1028
|
+
roots = [
|
|
1029
|
+
*(getattr(arm.context, slot) for slot in _CONTEXT_RUNTIME_SLOTS),
|
|
1030
|
+
arm.predictor,
|
|
1031
|
+
]
|
|
1032
|
+
for index, root in enumerate(roots):
|
|
1033
|
+
if root is None:
|
|
1034
|
+
continue
|
|
1035
|
+
graph = inspect_object_graph(
|
|
1036
|
+
root,
|
|
1037
|
+
root_path=f"arm[{arm_id}].runtime[{index}]",
|
|
1038
|
+
terminal_types=(BudgetLedger,),
|
|
1039
|
+
traverse_runtime_delegates=True,
|
|
1040
|
+
)
|
|
1041
|
+
for node in graph:
|
|
1042
|
+
if is_runtime_dependency(node.value):
|
|
1043
|
+
owners.setdefault(id(node.value), set()).add(arm_id)
|
|
1044
|
+
if isinstance(node.value, BudgetLedger):
|
|
1045
|
+
ledger_owners.setdefault(id(node.value), set()).add(arm_id)
|
|
1046
|
+
|
|
1047
|
+
shared = [sorted(arm_ids) for arm_ids in owners.values() if len(arm_ids) > 1]
|
|
1048
|
+
if shared:
|
|
1049
|
+
details = ", ".join("/".join(arm_ids) for arm_ids in shared)
|
|
1050
|
+
raise BalanceError(
|
|
1051
|
+
f"runtime dependency objects must be independently constructed across arms: {details}"
|
|
1052
|
+
)
|
|
1053
|
+
shared_ledgers = [sorted(arm_ids) for arm_ids in ledger_owners.values() if len(arm_ids) > 1]
|
|
1054
|
+
if shared_ledgers:
|
|
1055
|
+
details = ", ".join("/".join(arm_ids) for arm_ids in shared_ledgers)
|
|
1056
|
+
raise BalanceError(
|
|
1057
|
+
"runtime BudgetLedger instances must be independently constructed across arms: "
|
|
1058
|
+
f"{details}"
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _find_new_monitor_run_id(
|
|
1063
|
+
monitor: RunMonitor | None,
|
|
1064
|
+
prior_run_ids: frozenset[str],
|
|
1065
|
+
*,
|
|
1066
|
+
release_id: str,
|
|
1067
|
+
state_hash: str,
|
|
1068
|
+
) -> str:
|
|
1069
|
+
if monitor is None:
|
|
1070
|
+
return ""
|
|
1071
|
+
matches = [
|
|
1072
|
+
snapshot.run_id
|
|
1073
|
+
for snapshot in monitor.list_snapshots()
|
|
1074
|
+
if snapshot.run_id not in prior_run_ids
|
|
1075
|
+
and snapshot.release_id == release_id
|
|
1076
|
+
and snapshot.state_hash == state_hash
|
|
1077
|
+
]
|
|
1078
|
+
return matches[-1] if matches else ""
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _aggregate_metrics(
|
|
1082
|
+
rows: tuple[Mapping[str, float], ...],
|
|
1083
|
+
) -> Mapping[str, float]:
|
|
1084
|
+
if not rows:
|
|
1085
|
+
raise ValidationError("cannot aggregate an empty metric collection")
|
|
1086
|
+
keys = tuple(sorted(rows[0]))
|
|
1087
|
+
if any(tuple(sorted(row)) != keys for row in rows[1:]):
|
|
1088
|
+
raise ValidationError("metric keys differ across paired benchmark examples")
|
|
1089
|
+
count = len(rows)
|
|
1090
|
+
aggregate: dict[str, float] = {}
|
|
1091
|
+
for key in keys:
|
|
1092
|
+
value = math.fsum(float(row[key]) / count for row in rows)
|
|
1093
|
+
if not math.isfinite(value):
|
|
1094
|
+
raise ValidationError(f"aggregate metric {key!r} is not finite")
|
|
1095
|
+
aggregate[key] = value
|
|
1096
|
+
return freeze_mapping(aggregate)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
__all__ = [
|
|
1100
|
+
"BenchmarkArmExecutionRecord",
|
|
1101
|
+
"BenchmarkBlockArm",
|
|
1102
|
+
"BenchmarkBlockArmExecutionRecord",
|
|
1103
|
+
"BenchmarkInterventionArm",
|
|
1104
|
+
"ControlledMultiArmBlockResult",
|
|
1105
|
+
"ControlledMultiArmBlockRunner",
|
|
1106
|
+
"DownstreamPrediction",
|
|
1107
|
+
"DownstreamPredictor",
|
|
1108
|
+
"PairedBenchmarkExecution",
|
|
1109
|
+
"PairedBenchmarkExecutionExample",
|
|
1110
|
+
"PairedInterventionBenchmarkRunner",
|
|
1111
|
+
]
|