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,1860 @@
|
|
|
1
|
+
"""Strict conversion from frozen discovery runs to selector training artifacts.
|
|
2
|
+
|
|
3
|
+
This module is a producer boundary, not an experiment runner. It consumes only
|
|
4
|
+
completed, checksum-verified discovery run directories and never invokes a
|
|
5
|
+
driver, model, retriever, tool, or benchmark. Raw state text remains in the
|
|
6
|
+
source experiment artifact; the exported selector discovery freeze contains
|
|
7
|
+
only hashes, visible-only feature observations, evaluator outcomes, and the
|
|
8
|
+
derived training table.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import tempfile
|
|
18
|
+
from collections.abc import Mapping, Sequence
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from types import MappingProxyType
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from agentinputlab.contracts.enums import SemanticFamily
|
|
25
|
+
from agentinputlab.contracts.errors import ArtifactError, ValidationError
|
|
26
|
+
from agentinputlab.contracts.serialization import canonical_json, content_hash
|
|
27
|
+
from agentinputlab.contracts.state import (
|
|
28
|
+
ContextBlock,
|
|
29
|
+
ExecutionBudget,
|
|
30
|
+
FrozenAgentState,
|
|
31
|
+
Message,
|
|
32
|
+
)
|
|
33
|
+
from agentinputlab.experiments import (
|
|
34
|
+
ExperimentManifest,
|
|
35
|
+
ExperimentStage,
|
|
36
|
+
load_experiment_manifest,
|
|
37
|
+
load_verified_cell_results,
|
|
38
|
+
verify_experiment_run_directory,
|
|
39
|
+
)
|
|
40
|
+
from agentinputlab.metrics import assess_hard_harm_from_metrics
|
|
41
|
+
|
|
42
|
+
from .base import SelectionFeatures
|
|
43
|
+
from .features import VisibleFeatureConfig, VisibleStateFeatureExtractor
|
|
44
|
+
from .learned import SelectorExample
|
|
45
|
+
from .outcome_table import (
|
|
46
|
+
EvaluatedArmOutcome,
|
|
47
|
+
EvaluatedArmStatus,
|
|
48
|
+
SelectorTrainingTable,
|
|
49
|
+
UtilityDefinition,
|
|
50
|
+
build_selector_training_table,
|
|
51
|
+
)
|
|
52
|
+
from .runtime import FeatureAccessDomain, FeatureObservation
|
|
53
|
+
|
|
54
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
55
|
+
_SAFE_SEGMENT = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
56
|
+
_CORE_FAMILIES = frozenset(
|
|
57
|
+
{
|
|
58
|
+
SemanticFamily.REFORMULATE,
|
|
59
|
+
SemanticFamily.DECOMPOSE,
|
|
60
|
+
SemanticFamily.ACQUIRE,
|
|
61
|
+
SemanticFamily.CLARIFY,
|
|
62
|
+
SemanticFamily.CURATE,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
_SELECTOR_SPLIT_BY_EXPERIMENT_SPLIT = {
|
|
66
|
+
"discovery_train": "train",
|
|
67
|
+
"discovery_calibration": "calibration",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class SelectorDiscoverySource:
|
|
73
|
+
"""Content identities for one verified source experiment run."""
|
|
74
|
+
|
|
75
|
+
study_id: str
|
|
76
|
+
identity_hash: str
|
|
77
|
+
manifest_hash: str
|
|
78
|
+
plan_hash: str
|
|
79
|
+
result_set_hash: str
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
if not isinstance(self.study_id, str) or not self.study_id.strip():
|
|
83
|
+
raise ValidationError("selector discovery source study_id must be non-empty")
|
|
84
|
+
for label in (
|
|
85
|
+
"identity_hash",
|
|
86
|
+
"manifest_hash",
|
|
87
|
+
"plan_hash",
|
|
88
|
+
"result_set_hash",
|
|
89
|
+
):
|
|
90
|
+
if not isinstance(getattr(self, label), str) or not _SHA256.fullmatch(
|
|
91
|
+
getattr(self, label)
|
|
92
|
+
):
|
|
93
|
+
raise ValidationError(f"selector discovery source {label} must be SHA-256")
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def source_hash(self) -> str:
|
|
97
|
+
return content_hash(self)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class SelectorDiscoveryIdentity:
|
|
102
|
+
revision: str
|
|
103
|
+
sha256: str
|
|
104
|
+
|
|
105
|
+
def __post_init__(self) -> None:
|
|
106
|
+
if (
|
|
107
|
+
not isinstance(self.revision, str)
|
|
108
|
+
or not _SAFE_SEGMENT.fullmatch(self.revision)
|
|
109
|
+
or ".." in self.revision
|
|
110
|
+
):
|
|
111
|
+
raise ValidationError("selector discovery revision must be a safe flat identifier")
|
|
112
|
+
if not isinstance(self.sha256, str) or not _SHA256.fullmatch(self.sha256):
|
|
113
|
+
raise ValidationError("selector discovery identity requires a lowercase SHA-256")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class SelectorDiscoveryFreeze:
|
|
118
|
+
"""Immutable, content-addressed selector producer input."""
|
|
119
|
+
|
|
120
|
+
revision: str
|
|
121
|
+
source_runs: tuple[SelectorDiscoverySource, ...]
|
|
122
|
+
observations: tuple[FeatureObservation, ...]
|
|
123
|
+
outcomes: tuple[EvaluatedArmOutcome, ...]
|
|
124
|
+
training_table: SelectorTrainingTable
|
|
125
|
+
utility_definition: UtilityDefinition
|
|
126
|
+
representative_by_family: Mapping[SemanticFamily, str]
|
|
127
|
+
extractor_id: str
|
|
128
|
+
extractor_revision: str
|
|
129
|
+
extractor_hash: str
|
|
130
|
+
feature_schema_id: str
|
|
131
|
+
feature_schema_hash: str
|
|
132
|
+
comparison_profile_hash: str
|
|
133
|
+
schema_version: int = 1
|
|
134
|
+
|
|
135
|
+
def __post_init__(self) -> None:
|
|
136
|
+
if self.schema_version != 1 or isinstance(self.schema_version, bool):
|
|
137
|
+
raise ValidationError("unsupported selector discovery schema")
|
|
138
|
+
if (
|
|
139
|
+
not isinstance(self.revision, str)
|
|
140
|
+
or not _SAFE_SEGMENT.fullmatch(self.revision)
|
|
141
|
+
or ".." in self.revision
|
|
142
|
+
):
|
|
143
|
+
raise ValidationError("selector discovery revision must be a safe flat identifier")
|
|
144
|
+
sources = tuple(self.source_runs)
|
|
145
|
+
if not sources or any(not isinstance(item, SelectorDiscoverySource) for item in sources):
|
|
146
|
+
raise ValidationError("selector discovery freeze requires source run identities")
|
|
147
|
+
if len({item.identity_hash for item in sources}) != len(sources):
|
|
148
|
+
raise ValidationError("selector discovery source run identities must be unique")
|
|
149
|
+
observations = tuple(self.observations)
|
|
150
|
+
outcomes = tuple(self.outcomes)
|
|
151
|
+
if not observations or any(
|
|
152
|
+
not isinstance(item, FeatureObservation) for item in observations
|
|
153
|
+
):
|
|
154
|
+
raise ValidationError("selector discovery freeze requires feature observations")
|
|
155
|
+
if not outcomes or any(not isinstance(item, EvaluatedArmOutcome) for item in outcomes):
|
|
156
|
+
raise ValidationError("selector discovery freeze requires evaluated arm outcomes")
|
|
157
|
+
if len({item.state_hash for item in observations}) != len(observations):
|
|
158
|
+
raise ValidationError("selector discovery observations require unique state hashes")
|
|
159
|
+
if not isinstance(self.training_table, SelectorTrainingTable):
|
|
160
|
+
raise ValidationError("selector discovery freeze requires SelectorTrainingTable")
|
|
161
|
+
if not isinstance(self.utility_definition, UtilityDefinition):
|
|
162
|
+
raise ValidationError("selector discovery freeze requires UtilityDefinition")
|
|
163
|
+
representatives = _validate_representatives(self.representative_by_family)
|
|
164
|
+
for label in ("extractor_id", "extractor_revision", "feature_schema_id"):
|
|
165
|
+
value = getattr(self, label)
|
|
166
|
+
if not isinstance(value, str) or not value.strip() or value != value.strip():
|
|
167
|
+
raise ValidationError(f"selector discovery {label} must be trimmed and non-empty")
|
|
168
|
+
for label in (
|
|
169
|
+
"extractor_hash",
|
|
170
|
+
"feature_schema_hash",
|
|
171
|
+
"comparison_profile_hash",
|
|
172
|
+
):
|
|
173
|
+
if not _SHA256.fullmatch(getattr(self, label)):
|
|
174
|
+
raise ValidationError(f"selector discovery {label} must be SHA-256")
|
|
175
|
+
if {item.state_hash for item in observations} != {item.state_hash for item in outcomes}:
|
|
176
|
+
raise ValidationError(
|
|
177
|
+
"selector discovery observations and outcomes cover different states"
|
|
178
|
+
)
|
|
179
|
+
for observation in observations:
|
|
180
|
+
if (
|
|
181
|
+
observation.extractor_id != self.extractor_id
|
|
182
|
+
or observation.extractor_revision != self.extractor_revision
|
|
183
|
+
or observation.provenance.get("extractor_hash") != self.extractor_hash
|
|
184
|
+
or observation.provenance.get("schema_id") != self.feature_schema_id
|
|
185
|
+
or observation.provenance.get("schema_hash") != self.feature_schema_hash
|
|
186
|
+
):
|
|
187
|
+
raise ValidationError(
|
|
188
|
+
"selector discovery feature observation differs from frozen extractor/schema"
|
|
189
|
+
)
|
|
190
|
+
if {item.comparison_profile_hash for item in outcomes} != {self.comparison_profile_hash}:
|
|
191
|
+
raise ValidationError("selector discovery outcomes must share one comparison profile")
|
|
192
|
+
rebuilt = build_selector_training_table(
|
|
193
|
+
outcomes,
|
|
194
|
+
{item.state_hash: item for item in observations},
|
|
195
|
+
utility_definition=self.utility_definition,
|
|
196
|
+
representative_by_family=representatives,
|
|
197
|
+
)
|
|
198
|
+
if rebuilt.table_hash != self.training_table.table_hash:
|
|
199
|
+
raise ValidationError(
|
|
200
|
+
"selector discovery exported training table differs from its source rows"
|
|
201
|
+
)
|
|
202
|
+
object.__setattr__(
|
|
203
|
+
self,
|
|
204
|
+
"source_runs",
|
|
205
|
+
tuple(sorted(sources, key=lambda item: item.identity_hash)),
|
|
206
|
+
)
|
|
207
|
+
object.__setattr__(
|
|
208
|
+
self,
|
|
209
|
+
"observations",
|
|
210
|
+
tuple(sorted(observations, key=lambda item: item.state_hash)),
|
|
211
|
+
)
|
|
212
|
+
object.__setattr__(
|
|
213
|
+
self,
|
|
214
|
+
"outcomes",
|
|
215
|
+
tuple(sorted(outcomes, key=_outcome_sort_key)),
|
|
216
|
+
)
|
|
217
|
+
object.__setattr__(
|
|
218
|
+
self,
|
|
219
|
+
"representative_by_family",
|
|
220
|
+
MappingProxyType(representatives),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def split_provenance_hash(self) -> str:
|
|
225
|
+
return content_hash(
|
|
226
|
+
{
|
|
227
|
+
"group_split_digests": self.training_table.group_split_digests,
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def discovery_hash(self) -> str:
|
|
233
|
+
return content_hash(self.to_artifact())
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def identity(self) -> SelectorDiscoveryIdentity:
|
|
237
|
+
return SelectorDiscoveryIdentity(self.revision, self.discovery_hash)
|
|
238
|
+
|
|
239
|
+
def to_artifact(self) -> dict[str, Any]:
|
|
240
|
+
return {
|
|
241
|
+
"schema_version": self.schema_version,
|
|
242
|
+
"revision": self.revision,
|
|
243
|
+
"source_runs": [_source_to_artifact(item) for item in self.source_runs],
|
|
244
|
+
"extractor": {
|
|
245
|
+
"id": self.extractor_id,
|
|
246
|
+
"revision": self.extractor_revision,
|
|
247
|
+
"sha256": self.extractor_hash,
|
|
248
|
+
},
|
|
249
|
+
"feature_schema": {
|
|
250
|
+
"id": self.feature_schema_id,
|
|
251
|
+
"sha256": self.feature_schema_hash,
|
|
252
|
+
},
|
|
253
|
+
"comparison_profile_hash": self.comparison_profile_hash,
|
|
254
|
+
"utility_definition": _utility_to_artifact(self.utility_definition),
|
|
255
|
+
"representative_by_family": _encode_representatives(self.representative_by_family),
|
|
256
|
+
"observations": [_observation_to_artifact(item) for item in self.observations],
|
|
257
|
+
"outcomes": [_outcome_to_artifact(item) for item in self.outcomes],
|
|
258
|
+
"training_table": _training_table_to_artifact(self.training_table),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def from_artifact(cls, value: Mapping[str, Any]) -> SelectorDiscoveryFreeze:
|
|
263
|
+
artifact = _strict_object(
|
|
264
|
+
value,
|
|
265
|
+
allowed={
|
|
266
|
+
"schema_version",
|
|
267
|
+
"revision",
|
|
268
|
+
"source_runs",
|
|
269
|
+
"extractor",
|
|
270
|
+
"feature_schema",
|
|
271
|
+
"comparison_profile_hash",
|
|
272
|
+
"utility_definition",
|
|
273
|
+
"representative_by_family",
|
|
274
|
+
"observations",
|
|
275
|
+
"outcomes",
|
|
276
|
+
"training_table",
|
|
277
|
+
},
|
|
278
|
+
required={
|
|
279
|
+
"schema_version",
|
|
280
|
+
"revision",
|
|
281
|
+
"source_runs",
|
|
282
|
+
"extractor",
|
|
283
|
+
"feature_schema",
|
|
284
|
+
"comparison_profile_hash",
|
|
285
|
+
"utility_definition",
|
|
286
|
+
"representative_by_family",
|
|
287
|
+
"observations",
|
|
288
|
+
"outcomes",
|
|
289
|
+
"training_table",
|
|
290
|
+
},
|
|
291
|
+
label="selector discovery artifact",
|
|
292
|
+
)
|
|
293
|
+
extractor = _strict_object(
|
|
294
|
+
artifact["extractor"],
|
|
295
|
+
allowed={"id", "revision", "sha256"},
|
|
296
|
+
required={"id", "revision", "sha256"},
|
|
297
|
+
label="selector discovery extractor",
|
|
298
|
+
)
|
|
299
|
+
feature_schema = _strict_object(
|
|
300
|
+
artifact["feature_schema"],
|
|
301
|
+
allowed={"id", "sha256"},
|
|
302
|
+
required={"id", "sha256"},
|
|
303
|
+
label="selector discovery feature schema",
|
|
304
|
+
)
|
|
305
|
+
return cls(
|
|
306
|
+
schema_version=_integer(
|
|
307
|
+
artifact["schema_version"],
|
|
308
|
+
label="schema_version",
|
|
309
|
+
),
|
|
310
|
+
revision=_text(artifact["revision"], label="revision"),
|
|
311
|
+
source_runs=tuple(
|
|
312
|
+
_source_from_artifact(item)
|
|
313
|
+
for item in _array(artifact["source_runs"], label="source_runs")
|
|
314
|
+
),
|
|
315
|
+
observations=tuple(
|
|
316
|
+
_observation_from_artifact(item)
|
|
317
|
+
for item in _array(artifact["observations"], label="observations")
|
|
318
|
+
),
|
|
319
|
+
outcomes=tuple(
|
|
320
|
+
_outcome_from_artifact(item)
|
|
321
|
+
for item in _array(artifact["outcomes"], label="outcomes")
|
|
322
|
+
),
|
|
323
|
+
training_table=_training_table_from_artifact(artifact["training_table"]),
|
|
324
|
+
utility_definition=_utility_from_artifact(artifact["utility_definition"]),
|
|
325
|
+
representative_by_family=_decode_representatives(artifact["representative_by_family"]),
|
|
326
|
+
extractor_id=_text(extractor["id"], label="extractor.id"),
|
|
327
|
+
extractor_revision=_text(
|
|
328
|
+
extractor["revision"],
|
|
329
|
+
label="extractor.revision",
|
|
330
|
+
),
|
|
331
|
+
extractor_hash=_sha256(
|
|
332
|
+
extractor["sha256"],
|
|
333
|
+
label="extractor.sha256",
|
|
334
|
+
),
|
|
335
|
+
feature_schema_id=_text(
|
|
336
|
+
feature_schema["id"],
|
|
337
|
+
label="feature_schema.id",
|
|
338
|
+
),
|
|
339
|
+
feature_schema_hash=_sha256(
|
|
340
|
+
feature_schema["sha256"],
|
|
341
|
+
label="feature_schema.sha256",
|
|
342
|
+
),
|
|
343
|
+
comparison_profile_hash=_sha256(
|
|
344
|
+
artifact["comparison_profile_hash"],
|
|
345
|
+
label="comparison_profile_hash",
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class SelectorDiscoveryStore:
|
|
351
|
+
"""Append-only, content-addressed storage for discovery freezes."""
|
|
352
|
+
|
|
353
|
+
def __init__(self, root: str | Path) -> None:
|
|
354
|
+
self.root = Path(root).expanduser().resolve()
|
|
355
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
356
|
+
|
|
357
|
+
def path_for(self, identity: SelectorDiscoveryIdentity) -> Path:
|
|
358
|
+
if not isinstance(identity, SelectorDiscoveryIdentity):
|
|
359
|
+
raise TypeError("selector discovery path requires SelectorDiscoveryIdentity")
|
|
360
|
+
destination = (
|
|
361
|
+
self.root
|
|
362
|
+
/ "selector_discovery"
|
|
363
|
+
/ identity.revision
|
|
364
|
+
/ identity.sha256
|
|
365
|
+
/ "discovery.json"
|
|
366
|
+
).resolve()
|
|
367
|
+
if self.root not in destination.parents:
|
|
368
|
+
raise ArtifactError("selector discovery path escaped store root")
|
|
369
|
+
return destination
|
|
370
|
+
|
|
371
|
+
def save(self, freeze: SelectorDiscoveryFreeze) -> SelectorDiscoveryIdentity:
|
|
372
|
+
if not isinstance(freeze, SelectorDiscoveryFreeze):
|
|
373
|
+
raise TypeError("selector discovery store requires SelectorDiscoveryFreeze")
|
|
374
|
+
identity = freeze.identity
|
|
375
|
+
destination = self.path_for(identity)
|
|
376
|
+
revision_root = destination.parent.parent
|
|
377
|
+
if revision_root.exists():
|
|
378
|
+
existing_hashes = {
|
|
379
|
+
item.name
|
|
380
|
+
for item in revision_root.iterdir()
|
|
381
|
+
if item.is_dir() and _SHA256.fullmatch(item.name)
|
|
382
|
+
}
|
|
383
|
+
if existing_hashes - {identity.sha256}:
|
|
384
|
+
raise ArtifactError(
|
|
385
|
+
"selector discovery revision is already bound to different content"
|
|
386
|
+
)
|
|
387
|
+
payload = (canonical_json(freeze.to_artifact()) + "\n").encode("utf-8")
|
|
388
|
+
if destination.exists():
|
|
389
|
+
if not destination.is_file() or destination.read_bytes() != payload:
|
|
390
|
+
raise ArtifactError("stored selector discovery bytes differ from identity")
|
|
391
|
+
self.load(identity)
|
|
392
|
+
return identity
|
|
393
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
394
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
395
|
+
prefix=".partial-",
|
|
396
|
+
dir=destination.parent,
|
|
397
|
+
)
|
|
398
|
+
try:
|
|
399
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
400
|
+
handle.write(payload)
|
|
401
|
+
handle.flush()
|
|
402
|
+
os.fsync(handle.fileno())
|
|
403
|
+
try:
|
|
404
|
+
os.link(temporary, destination)
|
|
405
|
+
except FileExistsError:
|
|
406
|
+
if destination.read_bytes() != payload:
|
|
407
|
+
raise ArtifactError(
|
|
408
|
+
"concurrent selector discovery materialization differs"
|
|
409
|
+
) from None
|
|
410
|
+
finally:
|
|
411
|
+
if os.path.exists(temporary):
|
|
412
|
+
os.unlink(temporary)
|
|
413
|
+
self.load(identity)
|
|
414
|
+
return identity
|
|
415
|
+
|
|
416
|
+
def load(self, identity: SelectorDiscoveryIdentity) -> SelectorDiscoveryFreeze:
|
|
417
|
+
path = self.path_for(identity)
|
|
418
|
+
if not path.is_file():
|
|
419
|
+
raise ArtifactError(
|
|
420
|
+
f"selector discovery artifact missing: {identity.revision}@{identity.sha256}"
|
|
421
|
+
)
|
|
422
|
+
try:
|
|
423
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
424
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
425
|
+
raise ArtifactError("selector discovery artifact is not valid UTF-8 JSON") from exc
|
|
426
|
+
if not isinstance(raw, Mapping):
|
|
427
|
+
raise ArtifactError("selector discovery artifact root must be an object")
|
|
428
|
+
try:
|
|
429
|
+
freeze = SelectorDiscoveryFreeze.from_artifact(raw)
|
|
430
|
+
except (TypeError, ValueError) as exc:
|
|
431
|
+
raise ArtifactError("selector discovery artifact failed validation") from exc
|
|
432
|
+
if freeze.identity != identity:
|
|
433
|
+
raise ArtifactError("selector discovery identity does not match stored content")
|
|
434
|
+
return freeze
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def build_selector_discovery_freeze(
|
|
438
|
+
run_dirs: Sequence[str | Path],
|
|
439
|
+
*,
|
|
440
|
+
revision: str,
|
|
441
|
+
extractor: VisibleStateFeatureExtractor,
|
|
442
|
+
utility_definition: UtilityDefinition,
|
|
443
|
+
representative_by_family: Mapping[SemanticFamily, str],
|
|
444
|
+
) -> SelectorDiscoveryFreeze:
|
|
445
|
+
"""Convert explicit, verified discovery runs into a frozen training artifact."""
|
|
446
|
+
|
|
447
|
+
roots = tuple(Path(item).expanduser().resolve() for item in run_dirs)
|
|
448
|
+
if not roots:
|
|
449
|
+
raise ValidationError("selector discovery conversion requires run directories")
|
|
450
|
+
if not isinstance(extractor, VisibleStateFeatureExtractor):
|
|
451
|
+
raise TypeError("selector discovery production requires VisibleStateFeatureExtractor")
|
|
452
|
+
if not isinstance(utility_definition, UtilityDefinition):
|
|
453
|
+
raise TypeError("utility_definition must be UtilityDefinition")
|
|
454
|
+
representatives = _validate_representatives(representative_by_family)
|
|
455
|
+
|
|
456
|
+
observations: dict[str, FeatureObservation] = {}
|
|
457
|
+
outcomes: list[EvaluatedArmOutcome] = []
|
|
458
|
+
sources: list[SelectorDiscoverySource] = []
|
|
459
|
+
source_identities: set[str] = set()
|
|
460
|
+
profile_hashes: set[str] = set()
|
|
461
|
+
backend_by_model_slot: dict[str, str] = {}
|
|
462
|
+
|
|
463
|
+
for root in roots:
|
|
464
|
+
verification = verify_experiment_run_directory(root)
|
|
465
|
+
if not verification["complete"] or not verification["all_succeeded"]:
|
|
466
|
+
raise ArtifactError(
|
|
467
|
+
"selector discovery conversion requires a complete, all-succeeded run"
|
|
468
|
+
)
|
|
469
|
+
manifest = load_experiment_manifest(root / "manifest.json")
|
|
470
|
+
_validate_discovery_manifest(manifest)
|
|
471
|
+
if manifest.metadata.get("full_five_family_selector") is True:
|
|
472
|
+
if set(representatives) != _CORE_FAMILIES:
|
|
473
|
+
raise ValidationError(
|
|
474
|
+
"full-five-family selector discovery requires exactly one "
|
|
475
|
+
"representative for every core intervention family"
|
|
476
|
+
)
|
|
477
|
+
manifest_families = {
|
|
478
|
+
arm.family for arm in manifest.arms if arm.family is not SemanticFamily.NO_OP
|
|
479
|
+
}
|
|
480
|
+
if manifest_families != _CORE_FAMILIES:
|
|
481
|
+
raise ValidationError(
|
|
482
|
+
"full-five-family selector discovery manifest must contain "
|
|
483
|
+
"all and only the five core intervention families"
|
|
484
|
+
)
|
|
485
|
+
records = load_verified_cell_results(root)
|
|
486
|
+
source = SelectorDiscoverySource(
|
|
487
|
+
study_id=manifest.study_id,
|
|
488
|
+
identity_hash=_sha256(
|
|
489
|
+
verification["identity_hash"],
|
|
490
|
+
label="source identity_hash",
|
|
491
|
+
),
|
|
492
|
+
manifest_hash=_sha256(
|
|
493
|
+
verification["manifest_hash"],
|
|
494
|
+
label="source manifest_hash",
|
|
495
|
+
),
|
|
496
|
+
plan_hash=_sha256(
|
|
497
|
+
verification["plan_hash"],
|
|
498
|
+
label="source plan_hash",
|
|
499
|
+
),
|
|
500
|
+
result_set_hash=content_hash(records),
|
|
501
|
+
)
|
|
502
|
+
if source.identity_hash in source_identities:
|
|
503
|
+
raise ValidationError("selector discovery source run was supplied more than once")
|
|
504
|
+
source_identities.add(source.identity_hash)
|
|
505
|
+
sources.append(source)
|
|
506
|
+
arm_by_id = {item.arm_id: item for item in manifest.arms}
|
|
507
|
+
arm_by_release: dict[str, list[str]] = {}
|
|
508
|
+
for arm in manifest.arms:
|
|
509
|
+
arm_by_release.setdefault(arm.release_id, []).append(arm.arm_id)
|
|
510
|
+
no_op_arm_id = _unique_arm_for_release(
|
|
511
|
+
arm_by_release,
|
|
512
|
+
"baseline.no_op",
|
|
513
|
+
label="NO_OP",
|
|
514
|
+
)
|
|
515
|
+
representative_arm_ids = {
|
|
516
|
+
family: _unique_arm_for_release(
|
|
517
|
+
arm_by_release,
|
|
518
|
+
release_id,
|
|
519
|
+
label=family.value,
|
|
520
|
+
)
|
|
521
|
+
for family, release_id in representatives.items()
|
|
522
|
+
}
|
|
523
|
+
for record in records:
|
|
524
|
+
converted, observation, profile_hash, model_slot, backend_hash = _convert_result_record(
|
|
525
|
+
record,
|
|
526
|
+
arm_by_id=arm_by_id,
|
|
527
|
+
no_op_arm_id=no_op_arm_id,
|
|
528
|
+
representative_arm_ids=representative_arm_ids,
|
|
529
|
+
representative_by_family=representatives,
|
|
530
|
+
extractor=extractor,
|
|
531
|
+
)
|
|
532
|
+
existing = observations.setdefault(observation.state_hash, observation)
|
|
533
|
+
if existing != observation:
|
|
534
|
+
raise ValidationError("one frozen state produced different feature observations")
|
|
535
|
+
outcomes.extend(converted)
|
|
536
|
+
profile_hashes.add(profile_hash)
|
|
537
|
+
previous_backend = backend_by_model_slot.setdefault(
|
|
538
|
+
model_slot,
|
|
539
|
+
backend_hash,
|
|
540
|
+
)
|
|
541
|
+
if previous_backend != backend_hash:
|
|
542
|
+
raise ValidationError(
|
|
543
|
+
f"model slot {model_slot!r} changed backend identity across discovery runs"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if len(profile_hashes) != 1:
|
|
547
|
+
raise ValidationError("selector discovery inputs must share one comparison profile hash")
|
|
548
|
+
comparison_profile_hash = next(iter(profile_hashes))
|
|
549
|
+
table = build_selector_training_table(
|
|
550
|
+
outcomes,
|
|
551
|
+
observations,
|
|
552
|
+
utility_definition=utility_definition,
|
|
553
|
+
representative_by_family=representatives,
|
|
554
|
+
)
|
|
555
|
+
return SelectorDiscoveryFreeze(
|
|
556
|
+
revision=revision,
|
|
557
|
+
source_runs=tuple(sources),
|
|
558
|
+
observations=tuple(observations.values()),
|
|
559
|
+
outcomes=tuple(outcomes),
|
|
560
|
+
training_table=table,
|
|
561
|
+
utility_definition=utility_definition,
|
|
562
|
+
representative_by_family=representatives,
|
|
563
|
+
extractor_id=extractor.extractor_id,
|
|
564
|
+
extractor_revision=extractor.extractor_revision,
|
|
565
|
+
extractor_hash=extractor.extractor_hash,
|
|
566
|
+
feature_schema_id=extractor.feature_schema_id,
|
|
567
|
+
feature_schema_hash=extractor.feature_schema_hash,
|
|
568
|
+
comparison_profile_hash=comparison_profile_hash,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def load_discovery_export_config(
|
|
573
|
+
path: str | Path,
|
|
574
|
+
) -> tuple[
|
|
575
|
+
str,
|
|
576
|
+
VisibleStateFeatureExtractor,
|
|
577
|
+
UtilityDefinition,
|
|
578
|
+
Mapping[SemanticFamily, str],
|
|
579
|
+
]:
|
|
580
|
+
"""Load the explicit, strict configuration used by the export script."""
|
|
581
|
+
|
|
582
|
+
return parse_discovery_export_config(
|
|
583
|
+
_read_json_object(path, label="selector discovery export config")
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def parse_discovery_export_config(
|
|
588
|
+
raw: Mapping[str, Any],
|
|
589
|
+
) -> tuple[
|
|
590
|
+
str,
|
|
591
|
+
VisibleStateFeatureExtractor,
|
|
592
|
+
UtilityDefinition,
|
|
593
|
+
Mapping[SemanticFamily, str],
|
|
594
|
+
]:
|
|
595
|
+
"""Parse an already authenticated selector discovery export config."""
|
|
596
|
+
|
|
597
|
+
config = _strict_object(
|
|
598
|
+
raw,
|
|
599
|
+
allowed={
|
|
600
|
+
"schema_version",
|
|
601
|
+
"revision",
|
|
602
|
+
"feature_extractor",
|
|
603
|
+
"utility_definition",
|
|
604
|
+
"representative_by_family",
|
|
605
|
+
},
|
|
606
|
+
required={
|
|
607
|
+
"schema_version",
|
|
608
|
+
"revision",
|
|
609
|
+
"feature_extractor",
|
|
610
|
+
"utility_definition",
|
|
611
|
+
"representative_by_family",
|
|
612
|
+
},
|
|
613
|
+
label="selector discovery export config",
|
|
614
|
+
)
|
|
615
|
+
if _integer(config["schema_version"], label="schema_version") != 1:
|
|
616
|
+
raise ValidationError("unsupported selector discovery export config")
|
|
617
|
+
extractor_raw = _strict_object(
|
|
618
|
+
config["feature_extractor"],
|
|
619
|
+
allowed={
|
|
620
|
+
"extractor_id",
|
|
621
|
+
"extractor_revision",
|
|
622
|
+
"schema_id",
|
|
623
|
+
"tokenization_revision",
|
|
624
|
+
"context_soft_limit_tokens",
|
|
625
|
+
},
|
|
626
|
+
required={
|
|
627
|
+
"extractor_id",
|
|
628
|
+
"extractor_revision",
|
|
629
|
+
"schema_id",
|
|
630
|
+
"tokenization_revision",
|
|
631
|
+
"context_soft_limit_tokens",
|
|
632
|
+
},
|
|
633
|
+
label="feature_extractor",
|
|
634
|
+
)
|
|
635
|
+
extractor = VisibleStateFeatureExtractor(
|
|
636
|
+
config=VisibleFeatureConfig(
|
|
637
|
+
schema_id=_text(extractor_raw["schema_id"], label="schema_id"),
|
|
638
|
+
tokenization_revision=_text(
|
|
639
|
+
extractor_raw["tokenization_revision"],
|
|
640
|
+
label="tokenization_revision",
|
|
641
|
+
),
|
|
642
|
+
context_soft_limit_tokens=_integer(
|
|
643
|
+
extractor_raw["context_soft_limit_tokens"],
|
|
644
|
+
label="context_soft_limit_tokens",
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
extractor_id=_text(
|
|
648
|
+
extractor_raw["extractor_id"],
|
|
649
|
+
label="extractor_id",
|
|
650
|
+
),
|
|
651
|
+
extractor_revision=_text(
|
|
652
|
+
extractor_raw["extractor_revision"],
|
|
653
|
+
label="extractor_revision",
|
|
654
|
+
),
|
|
655
|
+
)
|
|
656
|
+
return (
|
|
657
|
+
_text(config["revision"], label="revision"),
|
|
658
|
+
extractor,
|
|
659
|
+
_utility_from_artifact(config["utility_definition"]),
|
|
660
|
+
_decode_representatives(config["representative_by_family"]),
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _validate_discovery_manifest(manifest: ExperimentManifest) -> None:
|
|
665
|
+
if manifest.stage is not ExperimentStage.DISCOVERY:
|
|
666
|
+
raise ValidationError("selector producer accepts discovery-stage runs only")
|
|
667
|
+
if manifest.metadata.get("selector_training_allowed") is not True:
|
|
668
|
+
raise ValidationError(
|
|
669
|
+
"discovery manifest must explicitly set selector_training_allowed=true"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _convert_result_record(
|
|
674
|
+
record: Mapping[str, Any],
|
|
675
|
+
*,
|
|
676
|
+
arm_by_id: Mapping[str, Any],
|
|
677
|
+
no_op_arm_id: str,
|
|
678
|
+
representative_arm_ids: Mapping[SemanticFamily, str],
|
|
679
|
+
representative_by_family: Mapping[SemanticFamily, str],
|
|
680
|
+
extractor: VisibleStateFeatureExtractor,
|
|
681
|
+
selector_split_override: str | None = None,
|
|
682
|
+
) -> tuple[
|
|
683
|
+
tuple[EvaluatedArmOutcome, ...],
|
|
684
|
+
FeatureObservation,
|
|
685
|
+
str,
|
|
686
|
+
str,
|
|
687
|
+
str,
|
|
688
|
+
]:
|
|
689
|
+
cell = _mapping(record.get("cell"), label="result cell")
|
|
690
|
+
result = _mapping(record.get("result"), label="result payload")
|
|
691
|
+
if result.get("status") != "succeeded":
|
|
692
|
+
raise ArtifactError("selector discovery cannot consume a failed experiment cell")
|
|
693
|
+
payload = _mapping(result.get("payload"), label="successful result.payload")
|
|
694
|
+
selector_record = _strict_object(
|
|
695
|
+
payload.get("selector_record"),
|
|
696
|
+
allowed={
|
|
697
|
+
"schema_version",
|
|
698
|
+
"state",
|
|
699
|
+
"group_id",
|
|
700
|
+
"eligible_families",
|
|
701
|
+
},
|
|
702
|
+
required={
|
|
703
|
+
"schema_version",
|
|
704
|
+
"state",
|
|
705
|
+
"group_id",
|
|
706
|
+
"eligible_families",
|
|
707
|
+
},
|
|
708
|
+
label="result.payload.selector_record",
|
|
709
|
+
)
|
|
710
|
+
if _integer(selector_record["schema_version"], label="selector_record.schema_version") != 1:
|
|
711
|
+
raise ArtifactError("unsupported selector_record schema")
|
|
712
|
+
state = _state_from_artifact(selector_record["state"])
|
|
713
|
+
observation = extractor.extract(state)
|
|
714
|
+
_validate_pre_action_receipt(
|
|
715
|
+
payload.get("pre_action"),
|
|
716
|
+
observation=observation,
|
|
717
|
+
extractor=extractor,
|
|
718
|
+
)
|
|
719
|
+
experiment_split = _text(cell.get("split"), label="cell.split")
|
|
720
|
+
if selector_split_override is None:
|
|
721
|
+
try:
|
|
722
|
+
expected_selector_split = _SELECTOR_SPLIT_BY_EXPERIMENT_SPLIT[experiment_split]
|
|
723
|
+
except KeyError as exc:
|
|
724
|
+
allowed = ", ".join(sorted(_SELECTOR_SPLIT_BY_EXPERIMENT_SPLIT))
|
|
725
|
+
raise ValidationError(
|
|
726
|
+
"selector discovery cell.split must freeze the producer partition "
|
|
727
|
+
f"({allowed}); got {experiment_split!r}"
|
|
728
|
+
) from exc
|
|
729
|
+
split = expected_selector_split
|
|
730
|
+
else:
|
|
731
|
+
if selector_split_override != "test":
|
|
732
|
+
raise ValidationError(
|
|
733
|
+
"selector result conversion override is reserved for untouched test"
|
|
734
|
+
)
|
|
735
|
+
split = selector_split_override
|
|
736
|
+
raw_group_id = _text(selector_record["group_id"], label="group_id")
|
|
737
|
+
benchmark_id = _text(cell.get("benchmark_id"), label="cell.benchmark_id")
|
|
738
|
+
unit_id = _text(cell.get("unit_id"), label="cell.unit_id")
|
|
739
|
+
model_slot = _text(cell.get("model_slot"), label="cell.model_slot")
|
|
740
|
+
seed = _integer(cell.get("seed"), label="cell.seed")
|
|
741
|
+
example_id = "example_" + content_hash({"benchmark_id": benchmark_id, "unit_id": unit_id})
|
|
742
|
+
group_id = "group_" + content_hash({"benchmark_id": benchmark_id, "group_id": raw_group_id})
|
|
743
|
+
|
|
744
|
+
cell_arm_ids = tuple(
|
|
745
|
+
_text(item, label="cell.arm_ids item")
|
|
746
|
+
for item in _array(cell.get("arm_ids"), label="cell.arm_ids")
|
|
747
|
+
)
|
|
748
|
+
if len(cell_arm_ids) != len(set(cell_arm_ids)):
|
|
749
|
+
raise ArtifactError("selector discovery cell contains duplicate arm IDs")
|
|
750
|
+
try:
|
|
751
|
+
cell_specs = {arm_id: arm_by_id[arm_id] for arm_id in cell_arm_ids}
|
|
752
|
+
except KeyError as exc:
|
|
753
|
+
raise ArtifactError("selector discovery cell references an unknown manifest arm") from exc
|
|
754
|
+
if no_op_arm_id not in cell_specs:
|
|
755
|
+
raise ValidationError("selector discovery cell requires an explicit NO_OP arm")
|
|
756
|
+
dependencies = _dependency_hashes(
|
|
757
|
+
payload.get("observed_dependency_hashes"),
|
|
758
|
+
label="result.payload.observed_dependency_hashes",
|
|
759
|
+
expected_arm_ids=cell_arm_ids,
|
|
760
|
+
)
|
|
761
|
+
backend_hash = _selector_backend_hash(
|
|
762
|
+
dependencies,
|
|
763
|
+
model_slot=model_slot,
|
|
764
|
+
)
|
|
765
|
+
profile_hash = _selector_comparison_profile_hash(
|
|
766
|
+
cell_arm_ids,
|
|
767
|
+
cell_specs,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
eligible_families = _family_array(
|
|
771
|
+
selector_record["eligible_families"],
|
|
772
|
+
label="eligible_families",
|
|
773
|
+
)
|
|
774
|
+
observed_families = {
|
|
775
|
+
spec.family for spec in cell_specs.values() if spec.family is not SemanticFamily.NO_OP
|
|
776
|
+
}
|
|
777
|
+
if eligible_families != observed_families:
|
|
778
|
+
raise ValidationError(
|
|
779
|
+
"selector_record eligible_families differ from the frozen cell arm families"
|
|
780
|
+
)
|
|
781
|
+
for family, arm_id in representative_arm_ids.items():
|
|
782
|
+
if family in eligible_families and arm_id not in cell_specs:
|
|
783
|
+
raise ValidationError(
|
|
784
|
+
f"eligible family {family.value} lacks its frozen representative arm"
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
raw_arms = _array(payload.get("arms"), label="result.payload.arms")
|
|
788
|
+
arm_results: dict[str, Mapping[str, Any]] = {}
|
|
789
|
+
for index, raw_arm in enumerate(raw_arms):
|
|
790
|
+
arm = _mapping(raw_arm, label=f"result.payload.arms[{index}]")
|
|
791
|
+
arm_id = _text(arm.get("arm_id"), label=f"arms[{index}].arm_id")
|
|
792
|
+
if arm_id in arm_results:
|
|
793
|
+
raise ArtifactError("selector discovery result contains duplicate arms")
|
|
794
|
+
arm_results[arm_id] = arm
|
|
795
|
+
if set(arm_results) != set(cell_arm_ids):
|
|
796
|
+
raise ArtifactError("selector discovery result arms differ from the frozen cell inventory")
|
|
797
|
+
|
|
798
|
+
normalized_arms = {
|
|
799
|
+
arm_id: _normalized_selector_arm(
|
|
800
|
+
raw_arm,
|
|
801
|
+
arm_id=arm_id,
|
|
802
|
+
)
|
|
803
|
+
for arm_id, raw_arm in arm_results.items()
|
|
804
|
+
}
|
|
805
|
+
_baseline_status, _baseline_metrics, baseline_full_cost, _baseline_harm = normalized_arms[
|
|
806
|
+
no_op_arm_id
|
|
807
|
+
]
|
|
808
|
+
|
|
809
|
+
selected: list[tuple[SemanticFamily, str, str, bool]] = [
|
|
810
|
+
(SemanticFamily.NO_OP, "baseline.no_op", no_op_arm_id, False)
|
|
811
|
+
]
|
|
812
|
+
selected.extend(
|
|
813
|
+
(
|
|
814
|
+
family,
|
|
815
|
+
representative_by_family[family],
|
|
816
|
+
arm_id,
|
|
817
|
+
family not in eligible_families,
|
|
818
|
+
)
|
|
819
|
+
for family, arm_id in sorted(
|
|
820
|
+
representative_arm_ids.items(),
|
|
821
|
+
key=lambda item: item[0].value,
|
|
822
|
+
)
|
|
823
|
+
)
|
|
824
|
+
converted: list[EvaluatedArmOutcome] = []
|
|
825
|
+
for family, release_id, arm_id, structural in selected:
|
|
826
|
+
if structural:
|
|
827
|
+
converted.append(
|
|
828
|
+
EvaluatedArmOutcome(
|
|
829
|
+
example_id=example_id,
|
|
830
|
+
group_id=group_id,
|
|
831
|
+
state_hash=state.state_hash,
|
|
832
|
+
split=split,
|
|
833
|
+
benchmark_id=benchmark_id,
|
|
834
|
+
family=family,
|
|
835
|
+
release_id=release_id,
|
|
836
|
+
seed=seed,
|
|
837
|
+
backend_hash=backend_hash,
|
|
838
|
+
comparison_profile_hash=profile_hash,
|
|
839
|
+
status=EvaluatedArmStatus.STRUCTURAL_INELIGIBLE,
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
continue
|
|
843
|
+
converted_status, metrics, full_cost, hard_harm = normalized_arms[arm_id]
|
|
844
|
+
is_no_op = family is SemanticFamily.NO_OP
|
|
845
|
+
cost = 0.0 if is_no_op else max(0.0, full_cost - baseline_full_cost)
|
|
846
|
+
# Strict harm is independent of whether utility falls below NO_OP.
|
|
847
|
+
# Utility regression remains available from the arm utility and NO_OP
|
|
848
|
+
# utility stored together in the selector outcome table.
|
|
849
|
+
risk = 0.0 if is_no_op else float(hard_harm)
|
|
850
|
+
converted.append(
|
|
851
|
+
EvaluatedArmOutcome(
|
|
852
|
+
example_id=example_id,
|
|
853
|
+
group_id=group_id,
|
|
854
|
+
state_hash=state.state_hash,
|
|
855
|
+
split=split,
|
|
856
|
+
benchmark_id=benchmark_id,
|
|
857
|
+
family=family,
|
|
858
|
+
release_id=release_id,
|
|
859
|
+
seed=seed,
|
|
860
|
+
backend_hash=backend_hash,
|
|
861
|
+
comparison_profile_hash=profile_hash,
|
|
862
|
+
status=converted_status,
|
|
863
|
+
metrics=metrics,
|
|
864
|
+
cost=cost,
|
|
865
|
+
risk=risk,
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
return tuple(converted), observation, profile_hash, model_slot, backend_hash
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _validate_pre_action_receipt(
|
|
872
|
+
value: object,
|
|
873
|
+
*,
|
|
874
|
+
observation: FeatureObservation,
|
|
875
|
+
extractor: VisibleStateFeatureExtractor,
|
|
876
|
+
) -> None:
|
|
877
|
+
"""Bind selector features to the state observed before any intervention.
|
|
878
|
+
|
|
879
|
+
The experiment driver creates this receipt at the benchmark/checkpoint
|
|
880
|
+
boundary. The selector producer independently reconstructs the visible
|
|
881
|
+
state and feature observation, then requires every content identity and
|
|
882
|
+
score to match. A driver payload cannot silently relabel a post-action
|
|
883
|
+
state or swap a train/calibration row after execution.
|
|
884
|
+
"""
|
|
885
|
+
|
|
886
|
+
receipt = _strict_object(
|
|
887
|
+
value,
|
|
888
|
+
allowed={
|
|
889
|
+
"state_hash",
|
|
890
|
+
"feature_hash",
|
|
891
|
+
"feature_schema_id",
|
|
892
|
+
"feature_schema_hash",
|
|
893
|
+
"provenance_hash",
|
|
894
|
+
"defect_scores",
|
|
895
|
+
},
|
|
896
|
+
required={
|
|
897
|
+
"state_hash",
|
|
898
|
+
"feature_hash",
|
|
899
|
+
"feature_schema_id",
|
|
900
|
+
"feature_schema_hash",
|
|
901
|
+
"provenance_hash",
|
|
902
|
+
"defect_scores",
|
|
903
|
+
},
|
|
904
|
+
label="result.payload.pre_action",
|
|
905
|
+
)
|
|
906
|
+
expected_text = {
|
|
907
|
+
"state_hash": observation.state_hash,
|
|
908
|
+
"feature_hash": observation.feature_hash,
|
|
909
|
+
"feature_schema_id": extractor.feature_schema_id,
|
|
910
|
+
"feature_schema_hash": extractor.feature_schema_hash,
|
|
911
|
+
"provenance_hash": content_hash(observation.provenance),
|
|
912
|
+
}
|
|
913
|
+
for key, expected in expected_text.items():
|
|
914
|
+
observed = _text(receipt[key], label=f"pre_action.{key}")
|
|
915
|
+
if observed != expected:
|
|
916
|
+
raise ArtifactError(f"pre_action.{key} does not authenticate selector_record.state")
|
|
917
|
+
|
|
918
|
+
features = observation.features
|
|
919
|
+
expected_scores = {
|
|
920
|
+
SemanticFamily.REFORMULATE.value: features.reformulation_need_score,
|
|
921
|
+
SemanticFamily.DECOMPOSE.value: features.multi_step_score,
|
|
922
|
+
SemanticFamily.ACQUIRE.value: features.retrieval_need_score,
|
|
923
|
+
SemanticFamily.CLARIFY.value: features.ambiguity_score,
|
|
924
|
+
SemanticFamily.CURATE.value: features.context_pressure,
|
|
925
|
+
}
|
|
926
|
+
raw_scores = _mapping(receipt["defect_scores"], label="pre_action.defect_scores")
|
|
927
|
+
if set(raw_scores) != set(expected_scores):
|
|
928
|
+
raise ArtifactError("pre_action.defect_scores must contain exactly five families")
|
|
929
|
+
for family, expected in expected_scores.items():
|
|
930
|
+
observed = _number(
|
|
931
|
+
raw_scores[family],
|
|
932
|
+
label=f"pre_action.defect_scores.{family}",
|
|
933
|
+
)
|
|
934
|
+
if observed != expected:
|
|
935
|
+
raise ArtifactError(
|
|
936
|
+
"pre_action defect scores do not authenticate selector_record.state"
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _state_from_artifact(value: object) -> FrozenAgentState:
|
|
941
|
+
state = _strict_object(
|
|
942
|
+
value,
|
|
943
|
+
allowed={
|
|
944
|
+
"query",
|
|
945
|
+
"task_id",
|
|
946
|
+
"instruction",
|
|
947
|
+
"context",
|
|
948
|
+
"messages",
|
|
949
|
+
"tool_names",
|
|
950
|
+
"metadata",
|
|
951
|
+
"budget",
|
|
952
|
+
},
|
|
953
|
+
required={
|
|
954
|
+
"query",
|
|
955
|
+
"task_id",
|
|
956
|
+
"instruction",
|
|
957
|
+
"context",
|
|
958
|
+
"messages",
|
|
959
|
+
"tool_names",
|
|
960
|
+
"metadata",
|
|
961
|
+
"budget",
|
|
962
|
+
},
|
|
963
|
+
label="selector_record.state",
|
|
964
|
+
)
|
|
965
|
+
context = tuple(
|
|
966
|
+
_context_from_artifact(item, index=index)
|
|
967
|
+
for index, item in enumerate(_array(state["context"], label="state.context"))
|
|
968
|
+
)
|
|
969
|
+
messages = tuple(
|
|
970
|
+
_message_from_artifact(item, index=index)
|
|
971
|
+
for index, item in enumerate(_array(state["messages"], label="state.messages"))
|
|
972
|
+
)
|
|
973
|
+
tool_names = tuple(
|
|
974
|
+
_text(item, label="state.tool_names item")
|
|
975
|
+
for item in _array(state["tool_names"], label="state.tool_names")
|
|
976
|
+
)
|
|
977
|
+
metadata = _mapping(state["metadata"], label="state.metadata")
|
|
978
|
+
budget_raw = _strict_object(
|
|
979
|
+
state["budget"],
|
|
980
|
+
allowed={
|
|
981
|
+
"max_model_calls",
|
|
982
|
+
"max_retrieval_calls",
|
|
983
|
+
"max_tool_calls",
|
|
984
|
+
"max_output_tokens",
|
|
985
|
+
"max_context_tokens",
|
|
986
|
+
"max_cost_usd",
|
|
987
|
+
},
|
|
988
|
+
required={
|
|
989
|
+
"max_model_calls",
|
|
990
|
+
"max_retrieval_calls",
|
|
991
|
+
"max_tool_calls",
|
|
992
|
+
"max_output_tokens",
|
|
993
|
+
"max_context_tokens",
|
|
994
|
+
"max_cost_usd",
|
|
995
|
+
},
|
|
996
|
+
label="state.budget",
|
|
997
|
+
)
|
|
998
|
+
return FrozenAgentState(
|
|
999
|
+
query=_string(state["query"], label="state.query"),
|
|
1000
|
+
task_id=_string(state["task_id"], label="state.task_id"),
|
|
1001
|
+
instruction=_string(state["instruction"], label="state.instruction"),
|
|
1002
|
+
context=context,
|
|
1003
|
+
messages=messages,
|
|
1004
|
+
tool_names=tool_names,
|
|
1005
|
+
metadata=metadata,
|
|
1006
|
+
budget=ExecutionBudget(
|
|
1007
|
+
max_model_calls=_integer(
|
|
1008
|
+
budget_raw["max_model_calls"],
|
|
1009
|
+
label="budget.max_model_calls",
|
|
1010
|
+
),
|
|
1011
|
+
max_retrieval_calls=_integer(
|
|
1012
|
+
budget_raw["max_retrieval_calls"],
|
|
1013
|
+
label="budget.max_retrieval_calls",
|
|
1014
|
+
),
|
|
1015
|
+
max_tool_calls=_integer(
|
|
1016
|
+
budget_raw["max_tool_calls"],
|
|
1017
|
+
label="budget.max_tool_calls",
|
|
1018
|
+
),
|
|
1019
|
+
max_output_tokens=_integer(
|
|
1020
|
+
budget_raw["max_output_tokens"],
|
|
1021
|
+
label="budget.max_output_tokens",
|
|
1022
|
+
),
|
|
1023
|
+
max_context_tokens=_optional_integer(
|
|
1024
|
+
budget_raw["max_context_tokens"],
|
|
1025
|
+
label="budget.max_context_tokens",
|
|
1026
|
+
),
|
|
1027
|
+
max_cost_usd=_optional_number(
|
|
1028
|
+
budget_raw["max_cost_usd"],
|
|
1029
|
+
label="budget.max_cost_usd",
|
|
1030
|
+
),
|
|
1031
|
+
),
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _context_from_artifact(value: object, *, index: int) -> ContextBlock:
|
|
1036
|
+
context = _strict_object(
|
|
1037
|
+
value,
|
|
1038
|
+
allowed={"text", "block_id", "source", "score", "metadata"},
|
|
1039
|
+
required={"text", "block_id", "source", "score", "metadata"},
|
|
1040
|
+
label=f"state.context[{index}]",
|
|
1041
|
+
)
|
|
1042
|
+
return ContextBlock(
|
|
1043
|
+
text=_string(context["text"], label=f"context[{index}].text"),
|
|
1044
|
+
block_id=_string(
|
|
1045
|
+
context["block_id"],
|
|
1046
|
+
label=f"context[{index}].block_id",
|
|
1047
|
+
),
|
|
1048
|
+
source=_string(context["source"], label=f"context[{index}].source"),
|
|
1049
|
+
score=_optional_number(
|
|
1050
|
+
context["score"],
|
|
1051
|
+
label=f"context[{index}].score",
|
|
1052
|
+
),
|
|
1053
|
+
metadata=_mapping(
|
|
1054
|
+
context["metadata"],
|
|
1055
|
+
label=f"context[{index}].metadata",
|
|
1056
|
+
),
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _message_from_artifact(value: object, *, index: int) -> Message:
|
|
1061
|
+
message = _strict_object(
|
|
1062
|
+
value,
|
|
1063
|
+
allowed={"role", "content", "name"},
|
|
1064
|
+
required={"role", "content", "name"},
|
|
1065
|
+
label=f"state.messages[{index}]",
|
|
1066
|
+
)
|
|
1067
|
+
name = message["name"]
|
|
1068
|
+
if name is not None and not isinstance(name, str):
|
|
1069
|
+
raise ValidationError(f"state.messages[{index}].name must be a string or null")
|
|
1070
|
+
return Message(
|
|
1071
|
+
role=_text(message["role"], label=f"messages[{index}].role"),
|
|
1072
|
+
content=_string(
|
|
1073
|
+
message["content"],
|
|
1074
|
+
label=f"messages[{index}].content",
|
|
1075
|
+
),
|
|
1076
|
+
name=name,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _source_to_artifact(value: SelectorDiscoverySource) -> dict[str, Any]:
|
|
1081
|
+
return {
|
|
1082
|
+
"study_id": value.study_id,
|
|
1083
|
+
"identity_hash": value.identity_hash,
|
|
1084
|
+
"manifest_hash": value.manifest_hash,
|
|
1085
|
+
"plan_hash": value.plan_hash,
|
|
1086
|
+
"result_set_hash": value.result_set_hash,
|
|
1087
|
+
"source_hash": value.source_hash,
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _source_from_artifact(value: object) -> SelectorDiscoverySource:
|
|
1092
|
+
raw = _strict_object(
|
|
1093
|
+
value,
|
|
1094
|
+
allowed={
|
|
1095
|
+
"study_id",
|
|
1096
|
+
"identity_hash",
|
|
1097
|
+
"manifest_hash",
|
|
1098
|
+
"plan_hash",
|
|
1099
|
+
"result_set_hash",
|
|
1100
|
+
"source_hash",
|
|
1101
|
+
},
|
|
1102
|
+
required={
|
|
1103
|
+
"study_id",
|
|
1104
|
+
"identity_hash",
|
|
1105
|
+
"manifest_hash",
|
|
1106
|
+
"plan_hash",
|
|
1107
|
+
"result_set_hash",
|
|
1108
|
+
"source_hash",
|
|
1109
|
+
},
|
|
1110
|
+
label="selector discovery source",
|
|
1111
|
+
)
|
|
1112
|
+
source = SelectorDiscoverySource(
|
|
1113
|
+
study_id=_text(raw["study_id"], label="source.study_id"),
|
|
1114
|
+
identity_hash=_sha256(
|
|
1115
|
+
raw["identity_hash"],
|
|
1116
|
+
label="source.identity_hash",
|
|
1117
|
+
),
|
|
1118
|
+
manifest_hash=_sha256(
|
|
1119
|
+
raw["manifest_hash"],
|
|
1120
|
+
label="source.manifest_hash",
|
|
1121
|
+
),
|
|
1122
|
+
plan_hash=_sha256(raw["plan_hash"], label="source.plan_hash"),
|
|
1123
|
+
result_set_hash=_sha256(
|
|
1124
|
+
raw["result_set_hash"],
|
|
1125
|
+
label="source.result_set_hash",
|
|
1126
|
+
),
|
|
1127
|
+
)
|
|
1128
|
+
if _sha256(raw["source_hash"], label="source.source_hash") != source.source_hash:
|
|
1129
|
+
raise ValidationError("selector discovery source hash mismatch")
|
|
1130
|
+
return source
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _utility_to_artifact(value: UtilityDefinition) -> dict[str, Any]:
|
|
1134
|
+
return {
|
|
1135
|
+
"utility_metric": value.utility_metric,
|
|
1136
|
+
"algorithm_failure_utility": value.algorithm_failure_utility,
|
|
1137
|
+
"aggregate": value.aggregate,
|
|
1138
|
+
"definition_hash": value.definition_hash,
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _utility_from_artifact(value: object) -> UtilityDefinition:
|
|
1143
|
+
raw = _strict_object(
|
|
1144
|
+
value,
|
|
1145
|
+
allowed={
|
|
1146
|
+
"utility_metric",
|
|
1147
|
+
"algorithm_failure_utility",
|
|
1148
|
+
"aggregate",
|
|
1149
|
+
"definition_hash",
|
|
1150
|
+
},
|
|
1151
|
+
required={
|
|
1152
|
+
"utility_metric",
|
|
1153
|
+
"algorithm_failure_utility",
|
|
1154
|
+
"aggregate",
|
|
1155
|
+
},
|
|
1156
|
+
label="utility_definition",
|
|
1157
|
+
)
|
|
1158
|
+
utility = UtilityDefinition(
|
|
1159
|
+
utility_metric=_text(
|
|
1160
|
+
raw["utility_metric"],
|
|
1161
|
+
label="utility_metric",
|
|
1162
|
+
),
|
|
1163
|
+
algorithm_failure_utility=_number(
|
|
1164
|
+
raw["algorithm_failure_utility"],
|
|
1165
|
+
label="algorithm_failure_utility",
|
|
1166
|
+
),
|
|
1167
|
+
aggregate=_text(raw["aggregate"], label="aggregate"),
|
|
1168
|
+
)
|
|
1169
|
+
if (
|
|
1170
|
+
"definition_hash" in raw
|
|
1171
|
+
and _sha256(
|
|
1172
|
+
raw["definition_hash"],
|
|
1173
|
+
label="definition_hash",
|
|
1174
|
+
)
|
|
1175
|
+
!= utility.definition_hash
|
|
1176
|
+
):
|
|
1177
|
+
raise ValidationError("utility definition hash mismatch")
|
|
1178
|
+
return utility
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def _observation_to_artifact(value: FeatureObservation) -> dict[str, Any]:
|
|
1182
|
+
return {
|
|
1183
|
+
"state_hash": value.state_hash,
|
|
1184
|
+
"extractor_id": value.extractor_id,
|
|
1185
|
+
"extractor_revision": value.extractor_revision,
|
|
1186
|
+
"access_domains": [item.value for item in value.access_domains],
|
|
1187
|
+
"features": _features_to_artifact(value.features),
|
|
1188
|
+
"provenance": value.provenance,
|
|
1189
|
+
"feature_hash": value.feature_hash,
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def _observation_from_artifact(value: object) -> FeatureObservation:
|
|
1194
|
+
raw = _strict_object(
|
|
1195
|
+
value,
|
|
1196
|
+
allowed={
|
|
1197
|
+
"state_hash",
|
|
1198
|
+
"extractor_id",
|
|
1199
|
+
"extractor_revision",
|
|
1200
|
+
"access_domains",
|
|
1201
|
+
"features",
|
|
1202
|
+
"provenance",
|
|
1203
|
+
"feature_hash",
|
|
1204
|
+
},
|
|
1205
|
+
required={
|
|
1206
|
+
"state_hash",
|
|
1207
|
+
"extractor_id",
|
|
1208
|
+
"extractor_revision",
|
|
1209
|
+
"access_domains",
|
|
1210
|
+
"features",
|
|
1211
|
+
"provenance",
|
|
1212
|
+
"feature_hash",
|
|
1213
|
+
},
|
|
1214
|
+
label="feature observation",
|
|
1215
|
+
)
|
|
1216
|
+
try:
|
|
1217
|
+
domains = tuple(
|
|
1218
|
+
FeatureAccessDomain(_text(item, label="access domain"))
|
|
1219
|
+
for item in _array(raw["access_domains"], label="access_domains")
|
|
1220
|
+
)
|
|
1221
|
+
except ValueError as exc:
|
|
1222
|
+
raise ValidationError("unknown feature access domain") from exc
|
|
1223
|
+
observation = FeatureObservation(
|
|
1224
|
+
features=_features_from_artifact(raw["features"]),
|
|
1225
|
+
state_hash=_sha256(raw["state_hash"], label="observation.state_hash"),
|
|
1226
|
+
extractor_id=_text(raw["extractor_id"], label="observation.extractor_id"),
|
|
1227
|
+
extractor_revision=_text(
|
|
1228
|
+
raw["extractor_revision"],
|
|
1229
|
+
label="observation.extractor_revision",
|
|
1230
|
+
),
|
|
1231
|
+
access_domains=domains,
|
|
1232
|
+
provenance=_mapping(raw["provenance"], label="observation.provenance"),
|
|
1233
|
+
)
|
|
1234
|
+
if _sha256(raw["feature_hash"], label="feature_hash") != observation.feature_hash:
|
|
1235
|
+
raise ValidationError("feature observation hash mismatch")
|
|
1236
|
+
return observation
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def _features_to_artifact(value: SelectionFeatures) -> dict[str, float]:
|
|
1240
|
+
return {
|
|
1241
|
+
"ambiguity_score": value.ambiguity_score,
|
|
1242
|
+
"multi_step_score": value.multi_step_score,
|
|
1243
|
+
"retrieval_need_score": value.retrieval_need_score,
|
|
1244
|
+
"context_pressure": value.context_pressure,
|
|
1245
|
+
"reformulation_need_score": value.reformulation_need_score,
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def _features_from_artifact(value: object) -> SelectionFeatures:
|
|
1250
|
+
raw = _strict_object(
|
|
1251
|
+
value,
|
|
1252
|
+
allowed={
|
|
1253
|
+
"ambiguity_score",
|
|
1254
|
+
"multi_step_score",
|
|
1255
|
+
"retrieval_need_score",
|
|
1256
|
+
"context_pressure",
|
|
1257
|
+
"reformulation_need_score",
|
|
1258
|
+
},
|
|
1259
|
+
required={
|
|
1260
|
+
"ambiguity_score",
|
|
1261
|
+
"multi_step_score",
|
|
1262
|
+
"retrieval_need_score",
|
|
1263
|
+
"context_pressure",
|
|
1264
|
+
"reformulation_need_score",
|
|
1265
|
+
},
|
|
1266
|
+
label="selection features",
|
|
1267
|
+
)
|
|
1268
|
+
return SelectionFeatures(
|
|
1269
|
+
ambiguity_score=_number(raw["ambiguity_score"], label="ambiguity_score"),
|
|
1270
|
+
multi_step_score=_number(raw["multi_step_score"], label="multi_step_score"),
|
|
1271
|
+
retrieval_need_score=_number(
|
|
1272
|
+
raw["retrieval_need_score"],
|
|
1273
|
+
label="retrieval_need_score",
|
|
1274
|
+
),
|
|
1275
|
+
context_pressure=_number(
|
|
1276
|
+
raw["context_pressure"],
|
|
1277
|
+
label="context_pressure",
|
|
1278
|
+
),
|
|
1279
|
+
reformulation_need_score=_number(
|
|
1280
|
+
raw["reformulation_need_score"],
|
|
1281
|
+
label="reformulation_need_score",
|
|
1282
|
+
),
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _outcome_to_artifact(value: EvaluatedArmOutcome) -> dict[str, Any]:
|
|
1287
|
+
payload = {
|
|
1288
|
+
"example_id": value.example_id,
|
|
1289
|
+
"group_id": value.group_id,
|
|
1290
|
+
"state_hash": value.state_hash,
|
|
1291
|
+
"split": value.split,
|
|
1292
|
+
"benchmark_id": value.benchmark_id,
|
|
1293
|
+
"family": value.family.value,
|
|
1294
|
+
"release_id": value.release_id,
|
|
1295
|
+
"seed": value.seed,
|
|
1296
|
+
"backend_hash": value.backend_hash,
|
|
1297
|
+
"comparison_profile_hash": value.comparison_profile_hash,
|
|
1298
|
+
"status": value.status.value,
|
|
1299
|
+
"metrics": value.metrics,
|
|
1300
|
+
"cost": value.cost,
|
|
1301
|
+
"risk": value.risk,
|
|
1302
|
+
}
|
|
1303
|
+
payload["outcome_hash"] = content_hash(payload)
|
|
1304
|
+
return payload
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _outcome_from_artifact(value: object) -> EvaluatedArmOutcome:
|
|
1308
|
+
raw = _strict_object(
|
|
1309
|
+
value,
|
|
1310
|
+
allowed={
|
|
1311
|
+
"example_id",
|
|
1312
|
+
"group_id",
|
|
1313
|
+
"state_hash",
|
|
1314
|
+
"split",
|
|
1315
|
+
"benchmark_id",
|
|
1316
|
+
"family",
|
|
1317
|
+
"release_id",
|
|
1318
|
+
"seed",
|
|
1319
|
+
"backend_hash",
|
|
1320
|
+
"comparison_profile_hash",
|
|
1321
|
+
"status",
|
|
1322
|
+
"metrics",
|
|
1323
|
+
"cost",
|
|
1324
|
+
"risk",
|
|
1325
|
+
"outcome_hash",
|
|
1326
|
+
},
|
|
1327
|
+
required={
|
|
1328
|
+
"example_id",
|
|
1329
|
+
"group_id",
|
|
1330
|
+
"state_hash",
|
|
1331
|
+
"split",
|
|
1332
|
+
"benchmark_id",
|
|
1333
|
+
"family",
|
|
1334
|
+
"release_id",
|
|
1335
|
+
"seed",
|
|
1336
|
+
"backend_hash",
|
|
1337
|
+
"comparison_profile_hash",
|
|
1338
|
+
"status",
|
|
1339
|
+
"metrics",
|
|
1340
|
+
"cost",
|
|
1341
|
+
"risk",
|
|
1342
|
+
"outcome_hash",
|
|
1343
|
+
},
|
|
1344
|
+
label="evaluated arm outcome",
|
|
1345
|
+
)
|
|
1346
|
+
without_hash = {key: item for key, item in raw.items() if key != "outcome_hash"}
|
|
1347
|
+
if _sha256(raw["outcome_hash"], label="outcome_hash") != content_hash(without_hash):
|
|
1348
|
+
raise ValidationError("evaluated arm outcome hash mismatch")
|
|
1349
|
+
try:
|
|
1350
|
+
family = SemanticFamily(_text(raw["family"], label="outcome.family"))
|
|
1351
|
+
status = EvaluatedArmStatus(_text(raw["status"], label="outcome.status"))
|
|
1352
|
+
except ValueError as exc:
|
|
1353
|
+
raise ValidationError("unknown outcome family or status") from exc
|
|
1354
|
+
return EvaluatedArmOutcome(
|
|
1355
|
+
example_id=_text(raw["example_id"], label="outcome.example_id"),
|
|
1356
|
+
group_id=_text(raw["group_id"], label="outcome.group_id"),
|
|
1357
|
+
state_hash=_sha256(raw["state_hash"], label="outcome.state_hash"),
|
|
1358
|
+
split=_text(raw["split"], label="outcome.split"),
|
|
1359
|
+
benchmark_id=_text(raw["benchmark_id"], label="outcome.benchmark_id"),
|
|
1360
|
+
family=family,
|
|
1361
|
+
release_id=_text(raw["release_id"], label="outcome.release_id"),
|
|
1362
|
+
seed=_integer(raw["seed"], label="outcome.seed"),
|
|
1363
|
+
backend_hash=_sha256(raw["backend_hash"], label="outcome.backend_hash"),
|
|
1364
|
+
comparison_profile_hash=_sha256(
|
|
1365
|
+
raw["comparison_profile_hash"],
|
|
1366
|
+
label="outcome.comparison_profile_hash",
|
|
1367
|
+
),
|
|
1368
|
+
status=status,
|
|
1369
|
+
metrics=_metrics(raw["metrics"], label="outcome.metrics", allow_empty=True),
|
|
1370
|
+
cost=_number(raw["cost"], label="outcome.cost"),
|
|
1371
|
+
risk=_number(raw["risk"], label="outcome.risk"),
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _training_table_to_artifact(value: SelectorTrainingTable) -> dict[str, Any]:
|
|
1376
|
+
return {
|
|
1377
|
+
"examples": [_example_to_artifact(item) for item in value.examples],
|
|
1378
|
+
"representative_by_family": _encode_representatives(value.representative_by_family),
|
|
1379
|
+
"group_split_digests": value.group_split_digests,
|
|
1380
|
+
"extractor_id": value.extractor_id,
|
|
1381
|
+
"extractor_revision": value.extractor_revision,
|
|
1382
|
+
"feature_table_sha256": value.feature_table_sha256,
|
|
1383
|
+
"outcome_table_sha256": value.outcome_table_sha256,
|
|
1384
|
+
"utility_definition_sha256": value.utility_definition_sha256,
|
|
1385
|
+
"table_hash": value.table_hash,
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _training_table_from_artifact(value: object) -> SelectorTrainingTable:
|
|
1390
|
+
raw = _strict_object(
|
|
1391
|
+
value,
|
|
1392
|
+
allowed={
|
|
1393
|
+
"examples",
|
|
1394
|
+
"representative_by_family",
|
|
1395
|
+
"group_split_digests",
|
|
1396
|
+
"extractor_id",
|
|
1397
|
+
"extractor_revision",
|
|
1398
|
+
"feature_table_sha256",
|
|
1399
|
+
"outcome_table_sha256",
|
|
1400
|
+
"utility_definition_sha256",
|
|
1401
|
+
"table_hash",
|
|
1402
|
+
},
|
|
1403
|
+
required={
|
|
1404
|
+
"examples",
|
|
1405
|
+
"representative_by_family",
|
|
1406
|
+
"group_split_digests",
|
|
1407
|
+
"extractor_id",
|
|
1408
|
+
"extractor_revision",
|
|
1409
|
+
"feature_table_sha256",
|
|
1410
|
+
"outcome_table_sha256",
|
|
1411
|
+
"utility_definition_sha256",
|
|
1412
|
+
"table_hash",
|
|
1413
|
+
},
|
|
1414
|
+
label="selector training table",
|
|
1415
|
+
)
|
|
1416
|
+
digests = _mapping(raw["group_split_digests"], label="group_split_digests")
|
|
1417
|
+
table = SelectorTrainingTable(
|
|
1418
|
+
examples=tuple(
|
|
1419
|
+
_example_from_artifact(item)
|
|
1420
|
+
for item in _array(raw["examples"], label="training examples")
|
|
1421
|
+
),
|
|
1422
|
+
representative_by_family=_decode_representatives(raw["representative_by_family"]),
|
|
1423
|
+
group_split_digests={
|
|
1424
|
+
_text(split, label="group split"): _sha256(
|
|
1425
|
+
digest,
|
|
1426
|
+
label=f"group split digest {split}",
|
|
1427
|
+
)
|
|
1428
|
+
for split, digest in digests.items()
|
|
1429
|
+
},
|
|
1430
|
+
extractor_id=_text(raw["extractor_id"], label="table.extractor_id"),
|
|
1431
|
+
extractor_revision=_text(
|
|
1432
|
+
raw["extractor_revision"],
|
|
1433
|
+
label="table.extractor_revision",
|
|
1434
|
+
),
|
|
1435
|
+
feature_table_sha256=_sha256(
|
|
1436
|
+
raw["feature_table_sha256"],
|
|
1437
|
+
label="feature_table_sha256",
|
|
1438
|
+
),
|
|
1439
|
+
outcome_table_sha256=_sha256(
|
|
1440
|
+
raw["outcome_table_sha256"],
|
|
1441
|
+
label="outcome_table_sha256",
|
|
1442
|
+
),
|
|
1443
|
+
utility_definition_sha256=_sha256(
|
|
1444
|
+
raw["utility_definition_sha256"],
|
|
1445
|
+
label="utility_definition_sha256",
|
|
1446
|
+
),
|
|
1447
|
+
)
|
|
1448
|
+
if _sha256(raw["table_hash"], label="table_hash") != table.table_hash:
|
|
1449
|
+
raise ValidationError("selector training table hash mismatch")
|
|
1450
|
+
return table
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def _example_to_artifact(value: SelectorExample) -> dict[str, Any]:
|
|
1454
|
+
return {
|
|
1455
|
+
"example_id": value.example_id,
|
|
1456
|
+
"features": _features_to_artifact(value.features),
|
|
1457
|
+
"utilities": _encode_family_values(value.utilities),
|
|
1458
|
+
"split": value.split,
|
|
1459
|
+
"costs": _encode_family_values(value.costs),
|
|
1460
|
+
"risks": _encode_family_values(value.risks),
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _example_from_artifact(value: object) -> SelectorExample:
|
|
1465
|
+
raw = _strict_object(
|
|
1466
|
+
value,
|
|
1467
|
+
allowed={"example_id", "features", "utilities", "split", "costs", "risks"},
|
|
1468
|
+
required={"example_id", "features", "utilities", "split", "costs", "risks"},
|
|
1469
|
+
label="selector training example",
|
|
1470
|
+
)
|
|
1471
|
+
return SelectorExample(
|
|
1472
|
+
example_id=_text(raw["example_id"], label="example_id"),
|
|
1473
|
+
features=_features_from_artifact(raw["features"]),
|
|
1474
|
+
utilities=_decode_family_values(
|
|
1475
|
+
raw["utilities"],
|
|
1476
|
+
label="example.utilities",
|
|
1477
|
+
allow_no_op=True,
|
|
1478
|
+
),
|
|
1479
|
+
split=_text(raw["split"], label="example.split"),
|
|
1480
|
+
costs=_decode_family_values(
|
|
1481
|
+
raw["costs"],
|
|
1482
|
+
label="example.costs",
|
|
1483
|
+
allow_no_op=False,
|
|
1484
|
+
),
|
|
1485
|
+
risks=_decode_family_values(
|
|
1486
|
+
raw["risks"],
|
|
1487
|
+
label="example.risks",
|
|
1488
|
+
allow_no_op=False,
|
|
1489
|
+
),
|
|
1490
|
+
)
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
def _normalized_selector_arm(
|
|
1494
|
+
value: Mapping[str, Any],
|
|
1495
|
+
*,
|
|
1496
|
+
arm_id: str,
|
|
1497
|
+
) -> tuple[EvaluatedArmStatus, Mapping[str, float], float, bool]:
|
|
1498
|
+
status = _text(value.get("status"), label=f"arm {arm_id} status")
|
|
1499
|
+
if status == "succeeded":
|
|
1500
|
+
metrics = _metrics(value.get("metrics"), label=f"arm {arm_id} metrics")
|
|
1501
|
+
converted_status = EvaluatedArmStatus.SUCCEEDED
|
|
1502
|
+
elif status == "failed":
|
|
1503
|
+
failure = _mapping(value.get("failure"), label=f"arm {arm_id} failure")
|
|
1504
|
+
if failure.get("scope") != "algorithm":
|
|
1505
|
+
raise ArtifactError(f"failed selector arm {arm_id} must be algorithm-scoped")
|
|
1506
|
+
if value.get("metrics") not in (None, {}):
|
|
1507
|
+
raise ArtifactError(f"failed selector arm {arm_id} cannot carry evaluator metrics")
|
|
1508
|
+
metrics = {}
|
|
1509
|
+
converted_status = EvaluatedArmStatus.ALGORITHM_FAILURE
|
|
1510
|
+
else:
|
|
1511
|
+
raise ArtifactError(f"selector arm {arm_id} status must be succeeded or failed")
|
|
1512
|
+
|
|
1513
|
+
usage = _mapping(value.get("usage"), label=f"arm {arm_id} usage")
|
|
1514
|
+
unpriced_calls = _integer(
|
|
1515
|
+
usage.get("unpriced_model_calls"),
|
|
1516
|
+
label=f"arm {arm_id} usage.unpriced_model_calls",
|
|
1517
|
+
)
|
|
1518
|
+
if unpriced_calls:
|
|
1519
|
+
raise ArtifactError(
|
|
1520
|
+
f"selector arm {arm_id} has unpriced model calls; freeze a pricing or "
|
|
1521
|
+
"self-hosted compute-cost policy before selector production"
|
|
1522
|
+
)
|
|
1523
|
+
total_cost = _number(
|
|
1524
|
+
usage.get("total_cost_usd"),
|
|
1525
|
+
label=f"arm {arm_id} usage.total_cost_usd",
|
|
1526
|
+
)
|
|
1527
|
+
if total_cost < 0:
|
|
1528
|
+
raise ArtifactError(f"selector arm {arm_id} total cost must be non-negative")
|
|
1529
|
+
assessment = assess_hard_harm_from_metrics(
|
|
1530
|
+
{
|
|
1531
|
+
**metrics,
|
|
1532
|
+
# A typed algorithm-scoped failure terminates this intervention arm
|
|
1533
|
+
# before a valid outcome exists and is therefore the generic
|
|
1534
|
+
# adapter-hard-stop component of the strict protocol.
|
|
1535
|
+
"adapter_hard_stops": float(converted_status is EvaluatedArmStatus.ALGORITHM_FAILURE),
|
|
1536
|
+
}
|
|
1537
|
+
)
|
|
1538
|
+
return converted_status, metrics, total_cost, assessment.hard_harm
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
def _dependency_hashes(
|
|
1542
|
+
value: object,
|
|
1543
|
+
*,
|
|
1544
|
+
label: str,
|
|
1545
|
+
expected_arm_ids: Sequence[str],
|
|
1546
|
+
) -> Mapping[str, str]:
|
|
1547
|
+
"""Normalize both supported execution-driver dependency receipts.
|
|
1548
|
+
|
|
1549
|
+
External replay emits one common ``slot -> digest`` map. The static
|
|
1550
|
+
multi-arm runner emits ``arm_id -> {slot -> digest}``; those maps are
|
|
1551
|
+
admissible only when every planned arm is present and all common nuisance
|
|
1552
|
+
dependencies are byte-for-byte identical. The arm IDs are deliberately
|
|
1553
|
+
not folded into the selector backend identity.
|
|
1554
|
+
"""
|
|
1555
|
+
|
|
1556
|
+
raw = _mapping(value, label=label)
|
|
1557
|
+
if not raw:
|
|
1558
|
+
raise ArtifactError(f"{label} must not be empty")
|
|
1559
|
+
value_kinds = {"mapping" if isinstance(item, Mapping) else "scalar" for item in raw.values()}
|
|
1560
|
+
if value_kinds == {"mapping"}:
|
|
1561
|
+
if set(raw) != set(expected_arm_ids):
|
|
1562
|
+
raise ArtifactError(f"{label} arm inventory differs from the frozen experiment cell")
|
|
1563
|
+
by_arm = {
|
|
1564
|
+
_text(arm_id, label=f"{label} arm_id"): _flat_dependency_hashes(
|
|
1565
|
+
dependencies,
|
|
1566
|
+
label=f"{label}.{arm_id}",
|
|
1567
|
+
)
|
|
1568
|
+
for arm_id, dependencies in raw.items()
|
|
1569
|
+
}
|
|
1570
|
+
common = next(iter(by_arm.values()))
|
|
1571
|
+
if any(dependencies != common for dependencies in by_arm.values()):
|
|
1572
|
+
raise ArtifactError(
|
|
1573
|
+
f"{label} differs across arms; selector discovery requires balanced "
|
|
1574
|
+
"nuisance dependencies"
|
|
1575
|
+
)
|
|
1576
|
+
return common
|
|
1577
|
+
if value_kinds != {"scalar"}:
|
|
1578
|
+
raise ArtifactError(f"{label} cannot mix flat and per-arm dependency receipts")
|
|
1579
|
+
return _flat_dependency_hashes(raw, label=label)
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _flat_dependency_hashes(value: object, *, label: str) -> Mapping[str, str]:
|
|
1583
|
+
raw = _mapping(value, label=label)
|
|
1584
|
+
if not raw:
|
|
1585
|
+
raise ArtifactError(f"{label} must not be empty")
|
|
1586
|
+
normalized: dict[str, str] = {}
|
|
1587
|
+
for slot, digest in raw.items():
|
|
1588
|
+
name = _text(slot, label=f"{label} slot")
|
|
1589
|
+
normalized[name] = _sha256(digest, label=f"{label}.{name}")
|
|
1590
|
+
return normalized
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _selector_comparison_profile_hash(
|
|
1594
|
+
arm_ids: Sequence[str],
|
|
1595
|
+
arm_specs: Mapping[str, Any],
|
|
1596
|
+
) -> str:
|
|
1597
|
+
"""Derive the selector's comparison profile from the frozen manifest arms."""
|
|
1598
|
+
|
|
1599
|
+
return content_hash(
|
|
1600
|
+
{
|
|
1601
|
+
"schema": "agentinputlab.selector-comparison-profile.v1",
|
|
1602
|
+
"arms": [
|
|
1603
|
+
{
|
|
1604
|
+
"arm_id": arm_id,
|
|
1605
|
+
"release_id": arm_specs[arm_id].release_id,
|
|
1606
|
+
"family": arm_specs[arm_id].family.value,
|
|
1607
|
+
"profile_kind": arm_specs[arm_id].profile_kind.value,
|
|
1608
|
+
"arm_kind": arm_specs[arm_id].arm_kind.value,
|
|
1609
|
+
"parameters": arm_specs[arm_id].parameters,
|
|
1610
|
+
}
|
|
1611
|
+
for arm_id in arm_ids
|
|
1612
|
+
],
|
|
1613
|
+
}
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def _selector_backend_hash(
|
|
1618
|
+
dependencies: Mapping[str, str],
|
|
1619
|
+
*,
|
|
1620
|
+
model_slot: str,
|
|
1621
|
+
) -> str:
|
|
1622
|
+
"""Bind selector rows to the shared agent model, not arena infrastructure."""
|
|
1623
|
+
|
|
1624
|
+
identity_slots = tuple(slot for slot in ("agent_model", "backend") if slot in dependencies)
|
|
1625
|
+
if len(identity_slots) != 1:
|
|
1626
|
+
raise ArtifactError(
|
|
1627
|
+
"selector dependency receipt must contain exactly one model identity "
|
|
1628
|
+
"slot ('agent_model' for external arenas or 'backend' for static runs)"
|
|
1629
|
+
)
|
|
1630
|
+
slot = identity_slots[0]
|
|
1631
|
+
return content_hash(
|
|
1632
|
+
{
|
|
1633
|
+
"schema": "agentinputlab.selector-backend.v2",
|
|
1634
|
+
"model_slot": model_slot,
|
|
1635
|
+
"identity_slot": slot,
|
|
1636
|
+
"identity_sha256": dependencies[slot],
|
|
1637
|
+
}
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def _metrics(
|
|
1642
|
+
value: object,
|
|
1643
|
+
*,
|
|
1644
|
+
label: str,
|
|
1645
|
+
allow_empty: bool = False,
|
|
1646
|
+
) -> dict[str, float]:
|
|
1647
|
+
raw = _mapping(value, label=label)
|
|
1648
|
+
metrics = {
|
|
1649
|
+
_text(name, label=f"{label} name"): _number(item, label=f"{label}.{name}")
|
|
1650
|
+
for name, item in raw.items()
|
|
1651
|
+
}
|
|
1652
|
+
if not metrics and not allow_empty:
|
|
1653
|
+
raise ArtifactError(f"{label} must not be empty")
|
|
1654
|
+
return metrics
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
def _family_array(value: object, *, label: str) -> frozenset[SemanticFamily]:
|
|
1658
|
+
raw = _array(value, label=label)
|
|
1659
|
+
families: list[SemanticFamily] = []
|
|
1660
|
+
for item in raw:
|
|
1661
|
+
try:
|
|
1662
|
+
family = SemanticFamily(_text(item, label=f"{label} item"))
|
|
1663
|
+
except ValueError as exc:
|
|
1664
|
+
raise ValidationError(f"{label} contains an unknown family") from exc
|
|
1665
|
+
if family not in _CORE_FAMILIES:
|
|
1666
|
+
raise ValidationError(f"{label} may contain core intervention families only")
|
|
1667
|
+
families.append(family)
|
|
1668
|
+
if len(families) != len(set(families)):
|
|
1669
|
+
raise ValidationError(f"{label} contains duplicate families")
|
|
1670
|
+
return frozenset(families)
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def _unique_arm_for_release(
|
|
1674
|
+
arm_by_release: Mapping[str, list[str]],
|
|
1675
|
+
release_id: str,
|
|
1676
|
+
*,
|
|
1677
|
+
label: str,
|
|
1678
|
+
) -> str:
|
|
1679
|
+
matches = arm_by_release.get(release_id, [])
|
|
1680
|
+
if len(matches) != 1:
|
|
1681
|
+
raise ValidationError(
|
|
1682
|
+
f"selector discovery manifest requires exactly one {label} arm for {release_id}"
|
|
1683
|
+
)
|
|
1684
|
+
return matches[0]
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def _validate_representatives(
|
|
1688
|
+
value: Mapping[SemanticFamily, str],
|
|
1689
|
+
) -> dict[SemanticFamily, str]:
|
|
1690
|
+
representatives = dict(value)
|
|
1691
|
+
if not representatives or any(
|
|
1692
|
+
family not in _CORE_FAMILIES
|
|
1693
|
+
or not isinstance(release, str)
|
|
1694
|
+
or not release.strip()
|
|
1695
|
+
or release != release.strip()
|
|
1696
|
+
for family, release in representatives.items()
|
|
1697
|
+
):
|
|
1698
|
+
raise ValidationError(
|
|
1699
|
+
"selector discovery representatives require core families and release IDs"
|
|
1700
|
+
)
|
|
1701
|
+
if len(set(representatives.values())) != len(representatives):
|
|
1702
|
+
raise ValidationError("selector discovery representatives must use unique releases")
|
|
1703
|
+
return representatives
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
def _encode_representatives(
|
|
1707
|
+
value: Mapping[SemanticFamily, str],
|
|
1708
|
+
) -> dict[str, str]:
|
|
1709
|
+
return {
|
|
1710
|
+
family.value: release
|
|
1711
|
+
for family, release in sorted(value.items(), key=lambda item: item[0].value)
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
def _decode_representatives(value: object) -> dict[SemanticFamily, str]:
|
|
1716
|
+
raw = _mapping(value, label="representative_by_family")
|
|
1717
|
+
result: dict[SemanticFamily, str] = {}
|
|
1718
|
+
for name, release in raw.items():
|
|
1719
|
+
try:
|
|
1720
|
+
family = SemanticFamily(_text(name, label="representative family"))
|
|
1721
|
+
except ValueError as exc:
|
|
1722
|
+
raise ValidationError("unknown representative family") from exc
|
|
1723
|
+
result[family] = _text(release, label=f"representative {name}")
|
|
1724
|
+
return _validate_representatives(result)
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
def _encode_family_values(
|
|
1728
|
+
value: Mapping[SemanticFamily, float],
|
|
1729
|
+
) -> dict[str, float]:
|
|
1730
|
+
return {
|
|
1731
|
+
family.value: float(item)
|
|
1732
|
+
for family, item in sorted(value.items(), key=lambda pair: pair[0].value)
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def _decode_family_values(
|
|
1737
|
+
value: object,
|
|
1738
|
+
*,
|
|
1739
|
+
label: str,
|
|
1740
|
+
allow_no_op: bool,
|
|
1741
|
+
) -> dict[SemanticFamily, float]:
|
|
1742
|
+
raw = _mapping(value, label=label)
|
|
1743
|
+
result: dict[SemanticFamily, float] = {}
|
|
1744
|
+
for name, item in raw.items():
|
|
1745
|
+
try:
|
|
1746
|
+
family = SemanticFamily(_text(name, label=f"{label} family"))
|
|
1747
|
+
except ValueError as exc:
|
|
1748
|
+
raise ValidationError(f"{label} contains an unknown family") from exc
|
|
1749
|
+
allowed = _CORE_FAMILIES | ({SemanticFamily.NO_OP} if allow_no_op else set())
|
|
1750
|
+
if family not in allowed:
|
|
1751
|
+
raise ValidationError(f"{label} contains an unsupported family")
|
|
1752
|
+
result[family] = _number(item, label=f"{label}.{name}")
|
|
1753
|
+
return result
|
|
1754
|
+
|
|
1755
|
+
|
|
1756
|
+
def _outcome_sort_key(
|
|
1757
|
+
row: EvaluatedArmOutcome,
|
|
1758
|
+
) -> tuple[str, str, str, str, str, str, int]:
|
|
1759
|
+
return (
|
|
1760
|
+
row.split,
|
|
1761
|
+
row.group_id,
|
|
1762
|
+
row.state_hash,
|
|
1763
|
+
row.backend_hash,
|
|
1764
|
+
row.comparison_profile_hash,
|
|
1765
|
+
row.family.value,
|
|
1766
|
+
row.seed,
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def _read_json_object(path: str | Path, *, label: str) -> Mapping[str, Any]:
|
|
1771
|
+
source = Path(path).expanduser().resolve()
|
|
1772
|
+
try:
|
|
1773
|
+
raw = json.loads(source.read_text(encoding="utf-8"))
|
|
1774
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
1775
|
+
raise ArtifactError(f"{label} is not valid UTF-8 JSON: {source}") from exc
|
|
1776
|
+
if not isinstance(raw, Mapping):
|
|
1777
|
+
raise ArtifactError(f"{label} must be a JSON object")
|
|
1778
|
+
return raw
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _strict_object(
|
|
1782
|
+
value: object,
|
|
1783
|
+
*,
|
|
1784
|
+
allowed: set[str],
|
|
1785
|
+
required: set[str],
|
|
1786
|
+
label: str,
|
|
1787
|
+
) -> dict[str, Any]:
|
|
1788
|
+
raw = _mapping(value, label=label)
|
|
1789
|
+
unknown = set(raw) - allowed
|
|
1790
|
+
missing = required - set(raw)
|
|
1791
|
+
if unknown:
|
|
1792
|
+
raise ValidationError(f"{label} contains unknown fields: {', '.join(sorted(unknown))}")
|
|
1793
|
+
if missing:
|
|
1794
|
+
raise ValidationError(f"{label} is missing fields: {', '.join(sorted(missing))}")
|
|
1795
|
+
return raw
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
def _mapping(value: object, *, label: str) -> dict[str, Any]:
|
|
1799
|
+
if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
|
|
1800
|
+
raise ValidationError(f"{label} must be an object with string keys")
|
|
1801
|
+
return dict(value)
|
|
1802
|
+
|
|
1803
|
+
|
|
1804
|
+
def _array(value: object, *, label: str) -> list[Any]:
|
|
1805
|
+
if not isinstance(value, list):
|
|
1806
|
+
raise ValidationError(f"{label} must be a JSON array")
|
|
1807
|
+
return value
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def _string(value: object, *, label: str) -> str:
|
|
1811
|
+
if not isinstance(value, str):
|
|
1812
|
+
raise ValidationError(f"{label} must be a string")
|
|
1813
|
+
return value
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
def _text(value: object, *, label: str) -> str:
|
|
1817
|
+
if not isinstance(value, str) or not value.strip() or value != value.strip():
|
|
1818
|
+
raise ValidationError(f"{label} must be a trimmed non-empty string")
|
|
1819
|
+
return value
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _integer(value: object, *, label: str) -> int:
|
|
1823
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
1824
|
+
raise ValidationError(f"{label} must be an integer")
|
|
1825
|
+
return value
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def _optional_integer(value: object, *, label: str) -> int | None:
|
|
1829
|
+
if value is None:
|
|
1830
|
+
return None
|
|
1831
|
+
return _integer(value, label=label)
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
def _number(value: object, *, label: str) -> float:
|
|
1835
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
1836
|
+
raise ValidationError(f"{label} must be a finite number")
|
|
1837
|
+
return float(value)
|
|
1838
|
+
|
|
1839
|
+
|
|
1840
|
+
def _optional_number(value: object, *, label: str) -> float | None:
|
|
1841
|
+
if value is None:
|
|
1842
|
+
return None
|
|
1843
|
+
return _number(value, label=label)
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def _sha256(value: object, *, label: str) -> str:
|
|
1847
|
+
if not isinstance(value, str) or not _SHA256.fullmatch(value):
|
|
1848
|
+
raise ValidationError(f"{label} must be a lowercase SHA-256")
|
|
1849
|
+
return value
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
__all__ = [
|
|
1853
|
+
"SelectorDiscoveryFreeze",
|
|
1854
|
+
"SelectorDiscoveryIdentity",
|
|
1855
|
+
"SelectorDiscoverySource",
|
|
1856
|
+
"SelectorDiscoveryStore",
|
|
1857
|
+
"build_selector_discovery_freeze",
|
|
1858
|
+
"load_discovery_export_config",
|
|
1859
|
+
"parse_discovery_export_config",
|
|
1860
|
+
]
|