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