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.
Files changed (469) hide show
  1. agentinputlab/__init__.py +28 -0
  2. agentinputlab/algorithms/__init__.py +6 -0
  3. agentinputlab/algorithms/acquire/__init__.py +145 -0
  4. agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
  5. agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
  6. agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
  7. agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
  8. agentinputlab/algorithms/acquire/hyde.py +296 -0
  9. agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
  10. agentinputlab/algorithms/acquire/hyde_external.py +148 -0
  11. agentinputlab/algorithms/acquire/ircot.py +28 -0
  12. agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
  13. agentinputlab/algorithms/acquire/ircot_native.py +355 -0
  14. agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
  15. agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
  16. agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
  17. agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
  18. agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
  19. agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
  20. agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
  21. agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
  22. agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
  23. agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
  24. agentinputlab/algorithms/clarify/__init__.py +61 -0
  25. agentinputlab/algorithms/clarify/_structured.py +64 -0
  26. agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
  27. agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
  28. agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
  29. agentinputlab/algorithms/clarify/future_turns.py +379 -0
  30. agentinputlab/algorithms/clarify/udcs.py +255 -0
  31. agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
  32. agentinputlab/algorithms/curate/__init__.py +65 -0
  33. agentinputlab/algorithms/curate/acon/__init__.py +27 -0
  34. agentinputlab/algorithms/curate/acon/common.py +284 -0
  35. agentinputlab/algorithms/curate/acon/common_support.py +136 -0
  36. agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
  37. agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
  38. agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
  39. agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
  40. agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
  41. agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
  42. agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
  43. agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
  44. agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
  45. agentinputlab/algorithms/curate/recomp/external.py +1129 -0
  46. agentinputlab/algorithms/curate/recomp/selective.py +304 -0
  47. agentinputlab/algorithms/decompose/__init__.py +107 -0
  48. agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
  49. agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
  50. agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
  51. agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
  52. agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
  53. agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
  54. agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
  55. agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
  56. agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
  57. agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
  58. agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
  59. agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
  60. agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
  61. agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
  62. agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
  63. agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
  64. agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
  65. agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
  66. agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
  67. agentinputlab/algorithms/reformulate/__init__.py +85 -0
  68. agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
  69. agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
  70. agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
  71. agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
  72. agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
  73. agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
  74. agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
  75. agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
  76. agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
  77. agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
  78. agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
  79. agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
  80. agentinputlab/algorithms/reformulate/rrr.py +696 -0
  81. agentinputlab/annotation/__init__.py +5 -0
  82. agentinputlab/annotation/independent.py +428 -0
  83. agentinputlab/annotation/server.py +233 -0
  84. agentinputlab/annotation/static/app.js +121 -0
  85. agentinputlab/annotation/static/index.html +60 -0
  86. agentinputlab/annotation/static/review.css +2 -0
  87. agentinputlab/annotation/static/review.html +10 -0
  88. agentinputlab/annotation/static/review.js +11 -0
  89. agentinputlab/annotation/static/styles.css +55 -0
  90. agentinputlab/annotation/store.py +444 -0
  91. agentinputlab/api.py +368 -0
  92. agentinputlab/artifacts/__init__.py +15 -0
  93. agentinputlab/artifacts/json_file.py +104 -0
  94. agentinputlab/artifacts/manifest.py +98 -0
  95. agentinputlab/artifacts/store.py +157 -0
  96. agentinputlab/backends/__init__.py +98 -0
  97. agentinputlab/backends/embedding/__init__.py +25 -0
  98. agentinputlab/backends/embedding/base.py +145 -0
  99. agentinputlab/backends/embedding/callable.py +75 -0
  100. agentinputlab/backends/embedding/replay.py +129 -0
  101. agentinputlab/backends/llm/__init__.py +31 -0
  102. agentinputlab/backends/llm/base.py +149 -0
  103. agentinputlab/backends/llm/callable.py +108 -0
  104. agentinputlab/backends/llm/decomp_completion.py +88 -0
  105. agentinputlab/backends/llm/final_content_stop.py +72 -0
  106. agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
  107. agentinputlab/backends/llm/openai_compatible.py +332 -0
  108. agentinputlab/backends/llm/transformers.py +171 -0
  109. agentinputlab/backends/retrieval/__init__.py +35 -0
  110. agentinputlab/backends/retrieval/base.py +50 -0
  111. agentinputlab/backends/retrieval/dense.py +777 -0
  112. agentinputlab/backends/retrieval/memory.py +119 -0
  113. agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
  114. agentinputlab/backends/user/__init__.py +15 -0
  115. agentinputlab/backends/user/base.py +66 -0
  116. agentinputlab/backends/user/callable.py +54 -0
  117. agentinputlab/backends/user/replay.py +89 -0
  118. agentinputlab/backends/vector.py +38 -0
  119. agentinputlab/benchmarks/__init__.py +246 -0
  120. agentinputlab/benchmarks/acquisition.py +106 -0
  121. agentinputlab/benchmarks/ale.py +540 -0
  122. agentinputlab/benchmarks/appworld_ul.py +669 -0
  123. agentinputlab/benchmarks/assets.py +226 -0
  124. agentinputlab/benchmarks/base.py +68 -0
  125. agentinputlab/benchmarks/beir_archive.py +235 -0
  126. agentinputlab/benchmarks/beir_sample.py +269 -0
  127. agentinputlab/benchmarks/bfcl_native.py +245 -0
  128. agentinputlab/benchmarks/bfcl_v3.py +607 -0
  129. agentinputlab/benchmarks/cache.py +218 -0
  130. agentinputlab/benchmarks/clariq.py +203 -0
  131. agentinputlab/benchmarks/data/registry.json +321 -0
  132. agentinputlab/benchmarks/data/smoke_assets.json +511 -0
  133. agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
  134. agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
  135. agentinputlab/benchmarks/execution.py +1111 -0
  136. agentinputlab/benchmarks/interactive.py +257 -0
  137. agentinputlab/benchmarks/ir_query.py +204 -0
  138. agentinputlab/benchmarks/longbench.py +203 -0
  139. agentinputlab/benchmarks/paired_replay.py +479 -0
  140. agentinputlab/benchmarks/preprocess.py +2313 -0
  141. agentinputlab/benchmarks/production.py +236 -0
  142. agentinputlab/benchmarks/qa.py +963 -0
  143. agentinputlab/benchmarks/registry.py +110 -0
  144. agentinputlab/benchmarks/retrieval.py +459 -0
  145. agentinputlab/benchmarks/runtime.py +509 -0
  146. agentinputlab/benchmarks/statebench.py +938 -0
  147. agentinputlab/benchmarks/tau3.py +725 -0
  148. agentinputlab/benchmarks/toolsandbox.py +626 -0
  149. agentinputlab/benchmarks/trec_dl_sample.py +263 -0
  150. agentinputlab/campaign.py +654 -0
  151. agentinputlab/campaign_runner.py +953 -0
  152. agentinputlab/cli/__init__.py +6 -0
  153. agentinputlab/cli/main.py +933 -0
  154. agentinputlab/connectors/__init__.py +46 -0
  155. agentinputlab/connectors/knowledge/__init__.py +4 -0
  156. agentinputlab/connectors/tools/__init__.py +30 -0
  157. agentinputlab/connectors/tools/base.py +57 -0
  158. agentinputlab/connectors/tools/mcp.py +222 -0
  159. agentinputlab/connectors/tools/memory.py +41 -0
  160. agentinputlab/connectors/tools/openapi.py +695 -0
  161. agentinputlab/connectors/tools/schema.py +102 -0
  162. agentinputlab/contracts/__init__.py +91 -0
  163. agentinputlab/contracts/availability.py +24 -0
  164. agentinputlab/contracts/dependency.py +58 -0
  165. agentinputlab/contracts/enums.py +82 -0
  166. agentinputlab/contracts/errors.py +63 -0
  167. agentinputlab/contracts/evidence.py +43 -0
  168. agentinputlab/contracts/intent.py +110 -0
  169. agentinputlab/contracts/lifecycle.py +164 -0
  170. agentinputlab/contracts/patch.py +167 -0
  171. agentinputlab/contracts/serialization.py +93 -0
  172. agentinputlab/contracts/state.py +127 -0
  173. agentinputlab/contracts/trace.py +59 -0
  174. agentinputlab/contracts/worker.py +42 -0
  175. agentinputlab/core/__init__.py +7 -0
  176. agentinputlab/data/__init__.py +32 -0
  177. agentinputlab/data/checksums.py +9 -0
  178. agentinputlab/data/downloader.py +235 -0
  179. agentinputlab/data/http.py +171 -0
  180. agentinputlab/data/index.py +21 -0
  181. agentinputlab/data/overlap.py +28 -0
  182. agentinputlab/data/preprocess.py +32 -0
  183. agentinputlab/data/storage.py +57 -0
  184. agentinputlab/data/trec_sample.py +427 -0
  185. agentinputlab/evaluation/__init__.py +5 -0
  186. agentinputlab/evaluation/outcomes.py +32 -0
  187. agentinputlab/experiments/__init__.py +278 -0
  188. agentinputlab/experiments/analysis.py +981 -0
  189. agentinputlab/experiments/artifacts.py +687 -0
  190. agentinputlab/experiments/automatic_measurement.py +678 -0
  191. agentinputlab/experiments/auxiliary_matrix.py +203 -0
  192. agentinputlab/experiments/bindings.py +430 -0
  193. agentinputlab/experiments/blueprints.py +147 -0
  194. agentinputlab/experiments/builders/__init__.py +45 -0
  195. agentinputlab/experiments/builders/executable_external_payload.py +423 -0
  196. agentinputlab/experiments/builders/external_process_arena.py +1018 -0
  197. agentinputlab/experiments/builders/registry_intervention.py +463 -0
  198. agentinputlab/experiments/cli.py +158 -0
  199. agentinputlab/experiments/controls.py +340 -0
  200. agentinputlab/experiments/driver_guard.py +205 -0
  201. agentinputlab/experiments/drivers/__init__.py +89 -0
  202. agentinputlab/experiments/drivers/external_arena.py +2908 -0
  203. agentinputlab/experiments/drivers/method_process.py +662 -0
  204. agentinputlab/experiments/drivers/parity_process.py +1484 -0
  205. agentinputlab/experiments/drivers/static_production.py +1574 -0
  206. agentinputlab/experiments/executor.py +448 -0
  207. agentinputlab/experiments/loader.py +605 -0
  208. agentinputlab/experiments/measurement.py +675 -0
  209. agentinputlab/experiments/paper_analysis.py +1818 -0
  210. agentinputlab/experiments/parity_identity.py +605 -0
  211. agentinputlab/experiments/plan.py +277 -0
  212. agentinputlab/experiments/production_builders.py +3739 -0
  213. agentinputlab/experiments/readiness.py +432 -0
  214. agentinputlab/experiments/schema.py +514 -0
  215. agentinputlab/experiments/secrets.py +177 -0
  216. agentinputlab/experiments/stateful_analysis.py +1738 -0
  217. agentinputlab/experiments/transfer_analysis.py +1316 -0
  218. agentinputlab/experiments/unseen_analysis.py +1025 -0
  219. agentinputlab/integrations/__init__.py +1 -0
  220. agentinputlab/integrations/action_space/__init__.py +1 -0
  221. agentinputlab/integrations/external_workers/__init__.py +76 -0
  222. agentinputlab/integrations/external_workers/ace.py +431 -0
  223. agentinputlab/integrations/external_workers/base.py +181 -0
  224. agentinputlab/integrations/external_workers/decomp_live.py +249 -0
  225. agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
  226. agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
  227. agentinputlab/integrations/external_workers/ircot.py +576 -0
  228. agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
  229. agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
  230. agentinputlab/integrations/external_workers/method.py +314 -0
  231. agentinputlab/integrations/external_workers/process.py +678 -0
  232. agentinputlab/integrations/external_workers/supervisor.py +1071 -0
  233. agentinputlab/integrations/full_architecture/__init__.py +1 -0
  234. agentinputlab/integrations/full_architecture/hyde.py +212 -0
  235. agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
  236. agentinputlab/integrations/live_history.py +222 -0
  237. agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
  238. agentinputlab/integrations/training/__init__.py +1 -0
  239. agentinputlab/metrics/__init__.py +50 -0
  240. agentinputlab/metrics/catalog.py +313 -0
  241. agentinputlab/metrics/harm.py +95 -0
  242. agentinputlab/metrics/paired.py +40 -0
  243. agentinputlab/metrics/qa.py +76 -0
  244. agentinputlab/metrics/text.py +33 -0
  245. agentinputlab/metrics/third_party.py +161 -0
  246. agentinputlab/models/__init__.py +4 -0
  247. agentinputlab/observability/__init__.py +56 -0
  248. agentinputlab/observability/budget.py +316 -0
  249. agentinputlab/observability/instrumentation.py +754 -0
  250. agentinputlab/observability/models.py +403 -0
  251. agentinputlab/observability/monitor.py +380 -0
  252. agentinputlab/optimizers/__init__.py +1 -0
  253. agentinputlab/optimizers/clarify/__init__.py +12 -0
  254. agentinputlab/optimizers/context/__init__.py +22 -0
  255. agentinputlab/optimizers/decompose/__init__.py +64 -0
  256. agentinputlab/optimizers/evidence/__init__.py +22 -0
  257. agentinputlab/optimizers/rewrite/__init__.py +30 -0
  258. agentinputlab/parity/__init__.py +5 -0
  259. agentinputlab/parity/base.py +206 -0
  260. agentinputlab/production_campaign.py +763 -0
  261. agentinputlab/py.typed +1 -0
  262. agentinputlab/recipes/__init__.py +333 -0
  263. agentinputlab/recipes/ace.py +418 -0
  264. agentinputlab/recipes/ace_bfcl.py +273 -0
  265. agentinputlab/recipes/ace_common_support.py +169 -0
  266. agentinputlab/recipes/ace_native_plan.py +322 -0
  267. agentinputlab/recipes/ace_online.py +711 -0
  268. agentinputlab/recipes/ace_retrieval.py +330 -0
  269. agentinputlab/recipes/ace_statebench.py +382 -0
  270. agentinputlab/recipes/ace_tau.py +463 -0
  271. agentinputlab/recipes/ace_toolsandbox.py +432 -0
  272. agentinputlab/recipes/acon_guideline.py +22 -0
  273. agentinputlab/recipes/adaptive_rag.py +822 -0
  274. agentinputlab/recipes/llmlingua2_inference.py +146 -0
  275. agentinputlab/recipes/llmlingua2_training.py +796 -0
  276. agentinputlab/recipes/mce.py +1486 -0
  277. agentinputlab/recipes/mce_final.py +335 -0
  278. agentinputlab/recipes/mce_text_fit.py +499 -0
  279. agentinputlab/recipes/mce_text_only.py +298 -0
  280. agentinputlab/recipes/qrag.py +729 -0
  281. agentinputlab/recipes/qrag_inference.py +364 -0
  282. agentinputlab/recipes/r2ag_features.py +236 -0
  283. agentinputlab/recipes/r2ag_inference.py +503 -0
  284. agentinputlab/recipes/r2ag_training.py +948 -0
  285. agentinputlab/recipes/recomp_abstractive.py +592 -0
  286. agentinputlab/recipes/recomp_extractive.py +525 -0
  287. agentinputlab/recipes/reformer.py +17 -0
  288. agentinputlab/recipes/rqrag.py +1516 -0
  289. agentinputlab/recipes/rqrag_common_support.py +193 -0
  290. agentinputlab/recipes/rqrag_completion.py +147 -0
  291. agentinputlab/recipes/skillopt.py +462 -0
  292. agentinputlab/recipes/skillopt_common_support.py +154 -0
  293. agentinputlab/recipes/skillopt_environment.py +165 -0
  294. agentinputlab/recipes/skillopt_environment_cli.py +5 -0
  295. agentinputlab/recipes/skillopt_retrieval.py +188 -0
  296. agentinputlab/recipes/smartsearch.py +897 -0
  297. agentinputlab/recipes/smartsearch_curriculum.py +283 -0
  298. agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
  299. agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
  300. agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
  301. agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
  302. agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
  303. agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
  304. agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
  305. agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
  306. agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
  307. agentinputlab/recipes/smartsearch_inference.py +524 -0
  308. agentinputlab/recipes/smartsearch_native_data.py +172 -0
  309. agentinputlab/recipes/smartsearch_policy.py +616 -0
  310. agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
  311. agentinputlab/recipes/smartsearch_sampling.py +232 -0
  312. agentinputlab/recipes/smartsearch_teacher.py +203 -0
  313. agentinputlab/recipes/smartsearch_training.py +265 -0
  314. agentinputlab/registry/__init__.py +46 -0
  315. agentinputlab/registry/beta.py +727 -0
  316. agentinputlab/registry/data/algorithms.json +1959 -0
  317. agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
  318. agentinputlab/registry/loader.py +100 -0
  319. agentinputlab/registry/result_manifest.py +184 -0
  320. agentinputlab/registry/runtime.py +195 -0
  321. agentinputlab/registry/schema.py +449 -0
  322. agentinputlab/runner/__init__.py +30 -0
  323. agentinputlab/runner/balance.py +248 -0
  324. agentinputlab/runner/cache.py +135 -0
  325. agentinputlab/runner/composite.py +130 -0
  326. agentinputlab/runner/engine.py +285 -0
  327. agentinputlab/runner/runtime_guard.py +268 -0
  328. agentinputlab/runner/worker.py +80 -0
  329. agentinputlab/runtime_bootstrap.py +288 -0
  330. agentinputlab/selectors/__init__.py +196 -0
  331. agentinputlab/selectors/advanced.py +1514 -0
  332. agentinputlab/selectors/artifacts.py +1087 -0
  333. agentinputlab/selectors/base.py +112 -0
  334. agentinputlab/selectors/campaigns.py +1893 -0
  335. agentinputlab/selectors/catalog.py +86 -0
  336. agentinputlab/selectors/discovery.py +1860 -0
  337. agentinputlab/selectors/evaluation.py +1348 -0
  338. agentinputlab/selectors/features.py +383 -0
  339. agentinputlab/selectors/learned.py +502 -0
  340. agentinputlab/selectors/no_op.py +28 -0
  341. agentinputlab/selectors/outcome_table.py +475 -0
  342. agentinputlab/selectors/producer.py +853 -0
  343. agentinputlab/selectors/rules.py +109 -0
  344. agentinputlab/selectors/runtime.py +420 -0
  345. agentinputlab/studies/__init__.py +16 -0
  346. agentinputlab/studies/design.py +165 -0
  347. agentinputlab/studies/paired.py +322 -0
  348. agentinputlab/studio/__init__.py +6 -0
  349. agentinputlab/studio/campaigns.py +819 -0
  350. agentinputlab/studio/engine.py +1408 -0
  351. agentinputlab/studio/model_backends.py +425 -0
  352. agentinputlab/studio/prepared.py +410 -0
  353. agentinputlab/studio/server.py +613 -0
  354. agentinputlab/studio/static/app.js +397 -0
  355. agentinputlab/studio/static/campaigns.html +175 -0
  356. agentinputlab/studio/static/campaigns.js +426 -0
  357. agentinputlab/studio/static/index.html +192 -0
  358. agentinputlab/studio/static/result.html +103 -0
  359. agentinputlab/studio/static/result.js +273 -0
  360. agentinputlab/studio/static/styles.css +467 -0
  361. agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
  362. agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
  363. agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
  364. agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
  365. agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
  366. agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  367. agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
  368. agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
  369. agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
  370. workers/README.md +94 -0
  371. workers/ace/README.md +261 -0
  372. workers/ace/__init__.py +1 -0
  373. workers/ace/compatibility_manifest.json +39 -0
  374. workers/ace/driver.py +1682 -0
  375. workers/acon/README.md +215 -0
  376. workers/acon/__init__.py +5 -0
  377. workers/acon/compatibility_manifest.json +93 -0
  378. workers/acon/driver.py +1859 -0
  379. workers/acon/fitted.py +395 -0
  380. workers/acon/guideline.py +470 -0
  381. workers/acon/guideline_source_manifest.json +20 -0
  382. workers/adaptive_rag/README.md +96 -0
  383. workers/adaptive_rag/__init__.py +15 -0
  384. workers/adaptive_rag/compatibility_manifest.json +61 -0
  385. workers/adaptive_rag/driver.py +1470 -0
  386. workers/adaptive_rag/fixture_runtime.py +163 -0
  387. workers/adaptive_rag/runtime.py +252 -0
  388. workers/ask_or_assume/README.md +124 -0
  389. workers/ask_or_assume/__init__.py +1 -0
  390. workers/ask_or_assume/compatibility_manifest.json +36 -0
  391. workers/ask_or_assume/driver.py +1227 -0
  392. workers/decomp/README.md +133 -0
  393. workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
  394. workers/decomp/__init__.py +5 -0
  395. workers/decomp/compatibility_manifest.json +205 -0
  396. workers/decomp/driver.py +1015 -0
  397. workers/decomp/live.py +137 -0
  398. workers/decomp/runtime.py +231 -0
  399. workers/infocqr/README.md +106 -0
  400. workers/infocqr/__init__.py +1 -0
  401. workers/infocqr/compatibility_manifest.json +84 -0
  402. workers/infocqr/driver.py +1766 -0
  403. workers/ircot/README.md +167 -0
  404. workers/ircot/__init__.py +7 -0
  405. workers/ircot/compatibility_manifest.json +121 -0
  406. workers/ircot/controlled.py +457 -0
  407. workers/ircot/driver.py +1373 -0
  408. workers/ircot/fixture_bindings.json +39 -0
  409. workers/ircot/fixture_config.json +48 -0
  410. workers/ircot/fixture_prompt.txt +2 -0
  411. workers/ircot/native_qa.py +398 -0
  412. workers/ircot/qa_compatibility_manifest.json +57 -0
  413. workers/ircot/runtime.py +97 -0
  414. workers/llmcompiler/README.md +160 -0
  415. workers/llmcompiler/__init__.py +1 -0
  416. workers/llmcompiler/compatibility_manifest.json +45 -0
  417. workers/llmcompiler/driver.py +1514 -0
  418. workers/llmcompiler/export_prompts.py +74 -0
  419. workers/llmcompiler/live.py +174 -0
  420. workers/llmcompiler/patch_manifest.json +147 -0
  421. workers/mce/README.md +198 -0
  422. workers/mce/__init__.py +29 -0
  423. workers/mce/compatibility_manifest.json +115 -0
  424. workers/mce/driver.py +2585 -0
  425. workers/mce/final_eval.py +173 -0
  426. workers/official_arenas/README.md +25 -0
  427. workers/official_arenas/__init__.py +1 -0
  428. workers/official_arenas/capture_records.py +110 -0
  429. workers/official_arenas/driver.py +619 -0
  430. workers/official_arenas/protocol.py +334 -0
  431. workers/official_arenas/providers/__init__.py +1 -0
  432. workers/official_arenas/providers/_live_history.py +228 -0
  433. workers/official_arenas/providers/_message_compat.py +77 -0
  434. workers/official_arenas/providers/appworld_ul.py +196 -0
  435. workers/official_arenas/providers/fixture.py +112 -0
  436. workers/official_arenas/providers/statebench.py +960 -0
  437. workers/official_arenas/providers/tau3.py +1672 -0
  438. workers/official_arenas/providers/tau_retail.py +586 -0
  439. workers/official_arenas/providers/toolsandbox.py +1321 -0
  440. workers/official_arenas/providers/tooltalk.py +699 -0
  441. workers/official_arenas/retail_capture.py +188 -0
  442. workers/official_arenas/retail_driver.py +275 -0
  443. workers/qrag/README.md +172 -0
  444. workers/qrag/__init__.py +1 -0
  445. workers/qrag/compatibility_manifest.json +64 -0
  446. workers/qrag/driver.py +673 -0
  447. workers/r2ag/README.md +154 -0
  448. workers/r2ag/__init__.py +5 -0
  449. workers/r2ag/compatibility_manifest.json +55 -0
  450. workers/r2ag/driver.py +1287 -0
  451. workers/r2ag/feature_schema.json +8 -0
  452. workers/r2ag/runtime.py +589 -0
  453. workers/recomp/README.md +120 -0
  454. workers/recomp/__init__.py +1 -0
  455. workers/recomp/compatibility_manifest.json +43 -0
  456. workers/recomp/driver.py +1653 -0
  457. workers/reformer/README.md +125 -0
  458. workers/reformer/__init__.py +1 -0
  459. workers/reformer/compatibility_manifest.json +68 -0
  460. workers/reformer/driver.py +1459 -0
  461. workers/reformer/induction.py +457 -0
  462. workers/rrr/README.md +126 -0
  463. workers/rrr/__init__.py +1 -0
  464. workers/rrr/compatibility_manifest.json +57 -0
  465. workers/rrr/driver.py +1133 -0
  466. workers/smartsearch/README.md +331 -0
  467. workers/smartsearch/__init__.py +1 -0
  468. workers/smartsearch/compatibility_manifest.json +41 -0
  469. 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
+ ]