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,1321 @@
1
+ """Production provider for the pinned ToolSandbox official runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import random
9
+ import subprocess
10
+ from collections.abc import Mapping
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from agentinputlab.contracts.serialization import to_jsonable
15
+
16
+ from ..protocol import (
17
+ OfficialArenaError,
18
+ ProviderArmResult,
19
+ content_hash,
20
+ intervention_usage,
21
+ materialized_state,
22
+ require_materialized_budget,
23
+ resolve_shared_budget_config,
24
+ validate_dependency_hashes,
25
+ visible_shared_budget,
26
+ )
27
+ from ._message_compat import (
28
+ coalesce_chat_system_messages,
29
+ requires_qwen38_initial_system_message,
30
+ )
31
+
32
+
33
+ class _EvaluatedAgentOutputInvalid(RuntimeError):
34
+ """A recognized malformed model generation from the evaluated agent."""
35
+
36
+ def __init__(self, error: Exception) -> None:
37
+ super().__init__(f"{type(error).__name__}: evaluated agent output is invalid")
38
+ self.error = error
39
+
40
+
41
+ def _model_output_invalid_result(
42
+ *,
43
+ replay_record: Mapping[str, Any],
44
+ role: str,
45
+ seed: int,
46
+ work_dir: Path,
47
+ error: Exception,
48
+ measured_usage: Mapping[str, int | float],
49
+ compatibility_repairs: list[Mapping[str, Any]],
50
+ budget_enforcements: list[Mapping[str, int]],
51
+ user_simulator_usage: Mapping[str, int | float],
52
+ ) -> ProviderArmResult:
53
+ """Materialize a malformed evaluated-agent response as an official zero score."""
54
+
55
+ trace = {
56
+ "provider": "toolsandbox",
57
+ "source_revision": ToolSandboxProvider.source_revision,
58
+ "scenario_name": replay_record["protected"]["scenario_name"],
59
+ "role": role,
60
+ "seed": seed,
61
+ "termination_reason": "generated_agent_response_invalid",
62
+ "generation_controls": {
63
+ "hard_stops": 1,
64
+ "failure_type": type(error).__name__,
65
+ "failed_generation_scope": "evaluated_agent",
66
+ },
67
+ "compatibility_repairs": list(compatibility_repairs),
68
+ "budget_enforcements": list(budget_enforcements),
69
+ "user_simulator_usage": dict(user_simulator_usage),
70
+ "score_provenance": "paper_model_output_invalid_zero_action",
71
+ }
72
+ work_dir.joinpath("model-output-invalid.json").write_text(
73
+ json.dumps(trace, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
74
+ encoding="utf-8",
75
+ )
76
+ return ProviderArmResult(
77
+ metrics={
78
+ "official_similarity": 0.0,
79
+ "milestone_similarity": 0.0,
80
+ "minefield_similarity": 0.0,
81
+ "turn_count": 0.0,
82
+ "adapter_compatibility_repairs": float(len(compatibility_repairs)),
83
+ "adapter_hard_stops": 1.0,
84
+ },
85
+ trace=trace,
86
+ usage=dict(measured_usage),
87
+ runtime_instance_id=content_hash(
88
+ {
89
+ "provider": "toolsandbox",
90
+ "checkpoint_id": replay_record["checkpoint_id"],
91
+ "role": role,
92
+ "seed": seed,
93
+ "termination_reason": trace["termination_reason"],
94
+ "work_dir": str(work_dir),
95
+ }
96
+ ),
97
+ )
98
+
99
+
100
+ class ToolSandboxProvider:
101
+ """Capture, restore, execute, and officially score ToolSandbox scenarios."""
102
+
103
+ benchmark_id = "toolsandbox.common_support"
104
+ source_revision = "165848b9a78cead7ca7fe7c89c688b58e6501219"
105
+ scorer_id = "toolsandbox.official_similarity_v1"
106
+ scorer_revision = "a81814306ce7aa229079fc530f6d026b423542cecc342a7677ac8dc3dd167481"
107
+
108
+ _CONFIG_FIELDS = frozenset(
109
+ {
110
+ "scenario_names",
111
+ "limit",
112
+ "preferred_tool_backend",
113
+ "agent_model",
114
+ "agent_base_url",
115
+ "agent_key_env",
116
+ "user_model",
117
+ "user_base_url",
118
+ "user_key_env",
119
+ "locked_dependency_hashes",
120
+ }
121
+ )
122
+ _OPTIONAL_CONFIG_FIELDS = frozenset({"budget_override"})
123
+
124
+ def __init__(self, *, history_transform: Any = None) -> None:
125
+ if history_transform is not None and not all(
126
+ callable(getattr(history_transform, name, None)) for name in ("transform", "fingerprint")
127
+ ):
128
+ raise OfficialArenaError("ToolSandbox history transform requires transform/fingerprint")
129
+ self.history_transform = history_transform
130
+
131
+ def doctor(self, config: Mapping[str, Any]) -> tuple[str, ...]:
132
+ try:
133
+ self._validate_config(config)
134
+ head = subprocess.check_output(
135
+ ["git", "rev-parse", "HEAD"],
136
+ text=True,
137
+ stderr=subprocess.DEVNULL,
138
+ ).strip()
139
+ if head != self.source_revision:
140
+ raise OfficialArenaError("ToolSandbox checkout revision differs")
141
+ from tool_sandbox.common.scenario import Scenario
142
+ from tool_sandbox.roles.execution_environment import ExecutionEnvironment
143
+ from tool_sandbox.scenarios import named_scenarios
144
+
145
+ del ExecutionEnvironment, Scenario, named_scenarios
146
+ except Exception as exc:
147
+ return (f"{type(exc).__name__}: {exc}",)
148
+ return ()
149
+
150
+ def capture(
151
+ self,
152
+ config: Mapping[str, Any],
153
+ *,
154
+ output_root: Path,
155
+ ) -> tuple[Mapping[str, Any], ...]:
156
+ del output_root
157
+ parsed = self._validate_config(config)
158
+ scenarios = self._scenarios(parsed)
159
+ rows: list[Mapping[str, Any]] = []
160
+ for scenario_name, scenario in scenarios.items():
161
+ serialized_context = scenario.starting_context.to_dict(serialize_console=False)
162
+ sandbox_rows = _sandbox_rows(serialized_context)
163
+ query = _opening_user_query(sandbox_rows)
164
+ instruction = _agent_system_instruction(sandbox_rows)
165
+ available_tools = _agent_available_tools(scenario.starting_context)
166
+ tools = list(available_tools.keys())
167
+ from tool_sandbox.common.tool_conversion import convert_to_openai_tools
168
+
169
+ tool_schemas = convert_to_openai_tools(dict(available_tools))
170
+ if not tool_schemas or any(not isinstance(item, Mapping) for item in tool_schemas):
171
+ raise OfficialArenaError("ToolSandbox tool schemas are unavailable")
172
+ visible_state = {
173
+ "query": query,
174
+ "task_id": scenario_name,
175
+ "instruction": instruction,
176
+ "context": [
177
+ {
178
+ "text": json.dumps(
179
+ {
180
+ "available_tools": tools,
181
+ "tool_schemas": tool_schemas,
182
+ "categories": [str(item) for item in scenario.categories],
183
+ },
184
+ ensure_ascii=False,
185
+ sort_keys=True,
186
+ ),
187
+ "block_id": "official_tool_catalog",
188
+ "source": "toolsandbox.starting_context",
189
+ "score": None,
190
+ "metadata": {"scenario_name": scenario_name},
191
+ }
192
+ ],
193
+ "messages": [{"role": "user", "content": query, "name": None}],
194
+ "tool_names": tools,
195
+ "metadata": {
196
+ "benchmark_id": self.benchmark_id,
197
+ "checkpoint_phase": "turn_zero_before_agent_response",
198
+ "clarification_roundtrip_available": True,
199
+ "state_restore_available": True,
200
+ "scenario_categories": [str(item) for item in scenario.categories],
201
+ },
202
+ "budget": visible_shared_budget(
203
+ parsed,
204
+ {
205
+ "max_model_calls": scenario.max_messages,
206
+ "max_retrieval_calls": scenario.max_messages,
207
+ "max_tool_calls": scenario.max_messages,
208
+ "max_output_tokens": 65536,
209
+ "max_context_tokens": None,
210
+ "max_cost_usd": None,
211
+ },
212
+ ),
213
+ }
214
+ rows.append(
215
+ {
216
+ "checkpoint_id": scenario_name,
217
+ "group_key": scenario_name,
218
+ "visible_state": visible_state,
219
+ "eligible_families": [
220
+ "reformulate",
221
+ "decompose",
222
+ "acquire",
223
+ "clarify",
224
+ "curate",
225
+ ],
226
+ "protected": {
227
+ "scenario_name": scenario_name,
228
+ "starting_context": serialized_context,
229
+ "starting_context_hash": content_hash(serialized_context),
230
+ "locked_dependency_hashes": parsed["locked_dependency_hashes"],
231
+ },
232
+ }
233
+ )
234
+ if not rows:
235
+ raise OfficialArenaError("ToolSandbox capture found no scenarios")
236
+ return tuple(rows)
237
+
238
+ def run_arm(
239
+ self,
240
+ config: Mapping[str, Any],
241
+ *,
242
+ replay_record: Mapping[str, Any],
243
+ arm_payload: Mapping[str, Any],
244
+ seed: int,
245
+ role: str,
246
+ work_dir: Path,
247
+ ) -> ProviderArmResult:
248
+ parsed = self._validate_config(config)
249
+ protected = replay_record["protected"]
250
+ state = materialized_state(arm_payload)
251
+ budget = require_materialized_budget(state, parsed)
252
+ optimizer_usage = intervention_usage(arm_payload)
253
+ budget_override = parsed.get("budget_override")
254
+ scenario_name = protected["scenario_name"]
255
+ scenarios = self._scenarios(
256
+ {
257
+ **parsed,
258
+ "scenario_names": [scenario_name],
259
+ "limit": 1,
260
+ }
261
+ )
262
+ try:
263
+ scenario = scenarios[scenario_name]
264
+ except KeyError as exc:
265
+ raise OfficialArenaError(
266
+ "ToolSandbox protected scenario identity is unavailable"
267
+ ) from exc
268
+
269
+ from tool_sandbox.common.execution_context import ExecutionContext, RoleType
270
+ from tool_sandbox.roles.execution_environment import ExecutionEnvironment
271
+
272
+ serialized_context = protected["starting_context"]
273
+ if content_hash(serialized_context) != protected["starting_context_hash"]:
274
+ raise OfficialArenaError("ToolSandbox protected starting state changed")
275
+ scenario.starting_context = ExecutionContext.from_dict(dict(serialized_context))
276
+ agent_budget, simulator_budget = _role_budgets(
277
+ scenario_max_messages=scenario.max_messages,
278
+ budget=budget,
279
+ optimizer_usage=optimizer_usage,
280
+ budget_override=budget_override,
281
+ )
282
+ agent = _intervened_agent(state, parsed, seed=seed, budget=agent_budget,
283
+ history_transform=self.history_transform)
284
+ user = _tracked_user(parsed, seed=seed + 1_000_000, budget=simulator_budget)
285
+ try:
286
+ result = scenario.play_and_evaluate(
287
+ roles={
288
+ RoleType.USER: user,
289
+ RoleType.EXECUTION_ENVIRONMENT: ExecutionEnvironment(),
290
+ RoleType.AGENT: agent,
291
+ },
292
+ output_directory=work_dir,
293
+ scenario_name=scenario_name,
294
+ )
295
+ except _EvaluatedAgentOutputInvalid as failure:
296
+ return _model_output_invalid_result(
297
+ replay_record=replay_record,
298
+ role=role,
299
+ seed=seed,
300
+ work_dir=work_dir,
301
+ error=failure.error,
302
+ measured_usage=agent.usage,
303
+ compatibility_repairs=[
304
+ *agent.compatibility_repairs,
305
+ *user.compatibility_repairs,
306
+ ],
307
+ budget_enforcements=agent.budget_enforcements,
308
+ user_simulator_usage=user.usage,
309
+ )
310
+ evaluation = result.evaluation_result
311
+ ending = result.ending_context.to_dict(serialize_console=False)
312
+ sandbox_rows = _sandbox_rows(ending)
313
+ tool_calls = sum(
314
+ 1
315
+ for row in sandbox_rows
316
+ if _role_name(row.get("sender")) == "AGENT"
317
+ and _role_name(row.get("recipient")) == "EXECUTION_ENVIRONMENT"
318
+ )
319
+ # The simulated user is benchmark infrastructure, not the evaluated
320
+ # agent. Its calls must neither consume the intervention/agent budget
321
+ # nor enter paper usage and cost accounting. Keep its usage in the
322
+ # protected provider trace so the execution remains auditable.
323
+ usage = dict(agent.usage)
324
+ usage["tool_calls"] = tool_calls
325
+ trace = {
326
+ "provider": "toolsandbox",
327
+ "live_history_transform": (
328
+ to_jsonable(self.history_transform.fingerprint()) if self.history_transform is not None else None
329
+ ),
330
+ "live_history_records": (
331
+ to_jsonable(list(getattr(self.history_transform, "records", ()))) if self.history_transform is not None else []
332
+ ),
333
+ "compatibility_repairs": [
334
+ *agent.compatibility_repairs,
335
+ *user.compatibility_repairs,
336
+ ],
337
+ "source_revision": self.source_revision,
338
+ "scenario_name": scenario_name,
339
+ "role": role,
340
+ "seed": seed,
341
+ "generation_controls": {
342
+ "temperature": 0.0,
343
+ "seed_policy": "role_base_seed_plus_call_index",
344
+ "agent_base_seed": seed,
345
+ "user_base_seed": seed + 1_000_000,
346
+ "enable_thinking": False,
347
+ "max_agent_model_calls": agent_budget["max_model_calls"],
348
+ "max_agent_tool_calls": agent_budget["max_tool_calls"],
349
+ "user_simulator_budget_separate": True,
350
+ "user_simulator_model_calls": simulator_budget["model_calls"],
351
+ "user_simulator_output_tokens": simulator_budget["output_tokens"],
352
+ "user_simulator_hard_stops": simulator_budget["hard_stops"],
353
+ "hard_stops": agent_budget["hard_stops"],
354
+ },
355
+ "budget_enforcements": list(agent.budget_enforcements),
356
+ "user_simulator_usage": dict(user.usage),
357
+ "ending_context": ending,
358
+ "evaluation": {
359
+ "similarity": evaluation.similarity,
360
+ "milestone_similarity": evaluation.milestone_similarity,
361
+ "minefield_similarity": evaluation.minefield_similarity,
362
+ "turn_count": evaluation.turn_count,
363
+ "milestone_mapping": dict(evaluation.milestone_mapping),
364
+ "minefield_mapping": dict(evaluation.minefield_mapping),
365
+ },
366
+ }
367
+ work_dir.joinpath("official_result.json").write_text(
368
+ json.dumps(trace, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
369
+ encoding="utf-8",
370
+ )
371
+ return ProviderArmResult(
372
+ metrics={
373
+ "official_similarity": float(evaluation.similarity),
374
+ "milestone_similarity": float(evaluation.milestone_similarity),
375
+ "minefield_similarity": float(evaluation.minefield_similarity),
376
+ "turn_count": float(evaluation.turn_count),
377
+ "adapter_compatibility_repairs": float(
378
+ len(agent.compatibility_repairs) + len(user.compatibility_repairs)
379
+ ),
380
+ "adapter_hard_stops": float(agent_budget["hard_stops"]),
381
+ },
382
+ trace=trace,
383
+ usage=usage,
384
+ runtime_instance_id=content_hash(
385
+ {
386
+ "provider": "toolsandbox",
387
+ "checkpoint_id": replay_record["checkpoint_id"],
388
+ "role": role,
389
+ "seed": seed,
390
+ "work_dir": str(work_dir),
391
+ }
392
+ ),
393
+ )
394
+
395
+ def _scenarios(self, config: Mapping[str, Any]) -> Mapping[str, Any]:
396
+ from tool_sandbox.common.tool_discovery import ToolBackend
397
+ from tool_sandbox.scenarios import named_scenarios
398
+
399
+ try:
400
+ backend = ToolBackend(config["preferred_tool_backend"])
401
+ except ValueError as exc:
402
+ try:
403
+ backend = ToolBackend[config["preferred_tool_backend"]]
404
+ except KeyError:
405
+ raise OfficialArenaError(
406
+ "ToolSandbox preferred_tool_backend is unsupported"
407
+ ) from exc
408
+ all_scenarios = _deterministic_scenarios(
409
+ lambda: named_scenarios(preferred_tool_backend=backend),
410
+ seed_material={
411
+ "source_revision": self.source_revision,
412
+ "preferred_tool_backend": config["preferred_tool_backend"],
413
+ },
414
+ )
415
+ requested = config["scenario_names"]
416
+ names = list(all_scenarios) if requested is None else list(requested)
417
+ missing = set(names).difference(all_scenarios)
418
+ if missing:
419
+ raise OfficialArenaError(
420
+ "ToolSandbox scenarios are unavailable: " + ", ".join(sorted(missing))
421
+ )
422
+ return {name: all_scenarios[name] for name in names[: config["limit"]]}
423
+
424
+ def _validate_config(self, config: Mapping[str, Any]) -> Mapping[str, Any]:
425
+ config = resolve_shared_budget_config(config)
426
+ if not isinstance(config, Mapping) or not (
427
+ set(config) >= self._CONFIG_FIELDS
428
+ and set(config) <= self._CONFIG_FIELDS | self._OPTIONAL_CONFIG_FIELDS
429
+ ):
430
+ raise OfficialArenaError("ToolSandbox provider config fields are invalid")
431
+ names = config["scenario_names"]
432
+ if names is not None and (
433
+ isinstance(names, (str, bytes))
434
+ or not isinstance(names, list)
435
+ or not names
436
+ or any(not isinstance(item, str) or not item for item in names)
437
+ ):
438
+ raise OfficialArenaError("ToolSandbox scenario_names is invalid")
439
+ limit = config["limit"]
440
+ if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1:
441
+ raise OfficialArenaError("ToolSandbox limit must be positive")
442
+ for name in (
443
+ "preferred_tool_backend",
444
+ "agent_model",
445
+ "agent_base_url",
446
+ "agent_key_env",
447
+ "user_model",
448
+ "user_base_url",
449
+ "user_key_env",
450
+ ):
451
+ value = config[name]
452
+ if not isinstance(value, str) or not value.strip():
453
+ raise OfficialArenaError(f"ToolSandbox {name} is invalid")
454
+ for name in ("agent_key_env", "user_key_env"):
455
+ environment_name = config[name]
456
+ if not environment_name.replace("_", "").isalnum():
457
+ raise OfficialArenaError(f"ToolSandbox {name} is invalid")
458
+ if not os.environ.get(environment_name):
459
+ raise OfficialArenaError(f"ToolSandbox environment {environment_name} is missing")
460
+ validate_dependency_hashes(
461
+ config["locked_dependency_hashes"],
462
+ label="ToolSandbox",
463
+ )
464
+ return config
465
+
466
+
467
+ def _role_budgets(
468
+ *,
469
+ scenario_max_messages: int,
470
+ budget: Mapping[str, Any],
471
+ optimizer_usage: Mapping[str, Any],
472
+ budget_override: Mapping[str, Any] | None,
473
+ ) -> tuple[dict[str, int], dict[str, int]]:
474
+ """Separate the evaluated agent budget from benchmark-simulator usage."""
475
+
476
+ per_call = int(
477
+ budget_override["max_output_tokens_per_call"]
478
+ if isinstance(budget_override, Mapping)
479
+ else 512
480
+ )
481
+ agent = {
482
+ "max_model_calls": max(
483
+ 0,
484
+ min(
485
+ scenario_max_messages,
486
+ int(budget["max_model_calls"]) - int(optimizer_usage.get("model_calls", 0)),
487
+ ),
488
+ ),
489
+ "model_calls": 0,
490
+ "max_tool_calls": max(
491
+ 0,
492
+ int(budget["max_tool_calls"]) - int(optimizer_usage.get("tool_calls", 0)),
493
+ ),
494
+ "tool_calls": 0,
495
+ "max_output_tokens": max(
496
+ 0,
497
+ int(budget["max_output_tokens"]) - int(optimizer_usage.get("output_tokens", 0)),
498
+ ),
499
+ "output_tokens": 0,
500
+ "max_output_tokens_per_call": per_call,
501
+ "hard_stops": 0,
502
+ }
503
+ simulator = {
504
+ "max_model_calls": scenario_max_messages,
505
+ "model_calls": 0,
506
+ "max_output_tokens": scenario_max_messages * per_call,
507
+ "output_tokens": 0,
508
+ "max_output_tokens_per_call": per_call,
509
+ "hard_stops": 0,
510
+ }
511
+ return agent, simulator
512
+
513
+
514
+ def _sandbox_rows(serialized_context: Mapping[str, Any]) -> list[Mapping[str, Any]]:
515
+ databases = serialized_context.get("_dbs")
516
+ if not isinstance(databases, Mapping):
517
+ raise OfficialArenaError("ToolSandbox serialized context has no databases")
518
+ rows = databases.get("SANDBOX")
519
+ if not isinstance(rows, list):
520
+ raise OfficialArenaError("ToolSandbox serialized context has no SANDBOX rows")
521
+ return [row for row in rows if isinstance(row, Mapping)]
522
+
523
+
524
+ def _role_name(value: Any) -> str:
525
+ text = str(value)
526
+ return text.rsplit(".", 1)[-1].upper()
527
+
528
+
529
+ def _opening_user_query(rows: list[Mapping[str, Any]]) -> str:
530
+ # ToolSandbox prepends user-simulator few-shot conversations to the live
531
+ # scenario. The actual turn-zero task is the final USER -> AGENT message,
532
+ # not the first demonstration message.
533
+ for row in reversed(rows):
534
+ if (
535
+ _role_name(row.get("sender")) == "USER"
536
+ and _role_name(row.get("recipient")) == "AGENT"
537
+ and isinstance(row.get("content"), str)
538
+ and row["content"].strip()
539
+ ):
540
+ return row["content"]
541
+ raise OfficialArenaError("ToolSandbox scenario has no visible opening user query")
542
+
543
+
544
+ def _agent_system_instruction(rows: list[Mapping[str, Any]]) -> str:
545
+ values = [
546
+ row["content"]
547
+ for row in rows
548
+ if _role_name(row.get("sender")) == "SYSTEM"
549
+ and _role_name(row.get("recipient")) == "AGENT"
550
+ and isinstance(row.get("content"), str)
551
+ and row["content"].strip()
552
+ ]
553
+ return "\n\n".join(values)
554
+
555
+
556
+ def _intervened_agent(
557
+ state: Mapping[str, Any],
558
+ config: Mapping[str, Any],
559
+ *,
560
+ seed: int,
561
+ budget: dict[str, int],
562
+ history_transform: Any = None,
563
+ ) -> Any:
564
+ from openai import OpenAI
565
+ from tool_sandbox.common.execution_context import RoleType
566
+ from tool_sandbox.common.message_conversion import Message
567
+ from tool_sandbox.roles.openai_api_agent import OpenAIAPIAgent
568
+
569
+ class IntervenedAgent(OpenAIAPIAgent):
570
+ model_name = config["agent_model"]
571
+
572
+ def __init__(self) -> None:
573
+ # The pinned upstream constructor only creates an unused public
574
+ # OpenAI client. Construct the bound client directly so local-only
575
+ # execution never requires an unrelated OPENAI_API_KEY.
576
+ self.openai_client = OpenAI(
577
+ api_key=os.environ[config["agent_key_env"]],
578
+ base_url=config["agent_base_url"],
579
+ max_retries=0,
580
+ )
581
+ self.usage = _empty_usage()
582
+ self._first_response = True
583
+ self._first_inference = True
584
+ self._last_response: Any = None
585
+ self._last_advertised_tool_names: set[str] = set()
586
+ self.compatibility_repairs: list[dict[str, Any]] = []
587
+ self.budget_enforcements: list[dict[str, int]] = []
588
+ self._base_seed = seed
589
+ self._inference_calls = 0
590
+
591
+ def respond(self, ending_index: int | None = None) -> None:
592
+ if (
593
+ budget["model_calls"] >= budget["max_model_calls"]
594
+ or budget["tool_calls"] >= budget["max_tool_calls"]
595
+ or budget["output_tokens"] >= budget["max_output_tokens"]
596
+ ):
597
+ if not budget["hard_stops"]:
598
+ budget["hard_stops"] = 1
599
+ self.add_messages(
600
+ [
601
+ Message(
602
+ sender=RoleType.AGENT,
603
+ recipient=RoleType.USER,
604
+ content="I could not complete the task within the run budget.",
605
+ conversation_active=False,
606
+ )
607
+ ]
608
+ )
609
+ return
610
+ metadata = state.get("metadata", {})
611
+ clarification = (
612
+ metadata.get("clarification_request") if isinstance(metadata, Mapping) else None
613
+ )
614
+ if self._first_response and isinstance(clarification, str) and clarification.strip():
615
+ self._first_response = False
616
+ self.add_messages(
617
+ [
618
+ Message(
619
+ sender=RoleType.AGENT,
620
+ recipient=RoleType.USER,
621
+ content=clarification,
622
+ )
623
+ ]
624
+ )
625
+ return
626
+ self._first_response = False
627
+ self._last_response = None
628
+ self._last_advertised_tool_names = set()
629
+ try:
630
+ super().respond(ending_index=ending_index)
631
+ except AssertionError as error:
632
+ if not _is_empty_length_response(self._last_response):
633
+ if _is_generated_agent_content_invalid(self._last_response):
634
+ raise _EvaluatedAgentOutputInvalid(error) from error
635
+ raise
636
+ budget["hard_stops"] = 1
637
+ replacement = "I could not complete the task within the run budget."
638
+ self.compatibility_repairs.append(
639
+ {
640
+ "kind": "empty_length_response_to_agent_budget_hard_stop",
641
+ "replacement": replacement,
642
+ }
643
+ )
644
+ self.add_messages(
645
+ [
646
+ Message(
647
+ sender=RoleType.AGENT,
648
+ recipient=RoleType.USER,
649
+ content=replacement,
650
+ conversation_active=False,
651
+ )
652
+ ]
653
+ )
654
+ except KeyError as error:
655
+ repair = _repair_agent_end_conversation(
656
+ self._last_response,
657
+ advertised_tool_names=self._last_advertised_tool_names,
658
+ )
659
+ if repair is None:
660
+ if _has_unadvertised_agent_tool_call(
661
+ self._last_response,
662
+ advertised_tool_names=self._last_advertised_tool_names,
663
+ ):
664
+ raise _EvaluatedAgentOutputInvalid(error) from error
665
+ raise
666
+ self.compatibility_repairs.append(
667
+ {
668
+ "kind": "unadvertised_agent_end_conversation_to_user_message",
669
+ "replacement": repair,
670
+ }
671
+ )
672
+ self.add_messages(
673
+ [
674
+ Message(
675
+ sender=RoleType.AGENT,
676
+ recipient=RoleType.USER,
677
+ content=repair,
678
+ )
679
+ ]
680
+ )
681
+ except json.JSONDecodeError as error:
682
+ raise _EvaluatedAgentOutputInvalid(error) from error
683
+ except Exception as error:
684
+ if not _is_gpt_oss_harmony_parse_failure(error, model_name=self.model_name):
685
+ raise
686
+ # The local vLLM server completed generation but its Harmony
687
+ # response parser rejected malformed channel/header tokens.
688
+ # This is evaluated model output, so preserve the cell as the
689
+ # same official zero-action outcome used for other recognized
690
+ # malformed generations instead of losing the observation.
691
+ budget["model_calls"] += 1
692
+ self.usage["model_calls"] += 1
693
+ self.usage["unpriced_model_calls"] += 1
694
+ self.compatibility_repairs.append(
695
+ {
696
+ "kind": "gpt_oss_harmony_parse_failure_to_agent_invalid_output",
697
+ }
698
+ )
699
+ raise _EvaluatedAgentOutputInvalid(error) from None
700
+
701
+ def model_inference(self, openai_messages: Any, openai_tools: Any) -> Any:
702
+ messages = [dict(item) for item in openai_messages]
703
+ if history_transform is not None:
704
+ transformed = history_transform.transform(messages, state=state, remaining_budget={
705
+ "max_model_calls": budget["max_model_calls"] - budget["model_calls"],
706
+ "max_output_tokens": budget["max_output_tokens"] - budget["output_tokens"],
707
+ })
708
+ # Native rendering inserts its own system preamble below;
709
+ # never mutate the callback's private cross-turn history.
710
+ messages = [dict(message) for message in transformed["messages"]]
711
+ usage = transformed["usage"]
712
+ for key in ("model_calls", "output_tokens"):
713
+ if type(usage.get(key, 0)) is not int or usage.get(key, 0) < 0:
714
+ raise OfficialArenaError("ToolSandbox history transform usage is invalid")
715
+ budget[key] += usage.get(key, 0)
716
+ if (budget["model_calls"] >= budget["max_model_calls"] or
717
+ budget["output_tokens"] >= budget["max_output_tokens"]):
718
+ raise OfficialArenaError("history transform failed to reserve the downstream budget")
719
+ self.usage = _merge_tracked_usage(self.usage, usage)
720
+ additions = [state.get("instruction", "")]
721
+ additions.extend(
722
+ block.get("text", "")
723
+ for block in state.get("context", [])
724
+ if isinstance(block, Mapping)
725
+ )
726
+ optimized_context = "\n\n".join(
727
+ item for item in additions if isinstance(item, str) and item
728
+ )
729
+ messages.insert(
730
+ 0,
731
+ {
732
+ "role": "system",
733
+ "content": (
734
+ "<agentinputlab_visible_input>\n"
735
+ f"Optimized query: {state['query']}\n\n"
736
+ f"{optimized_context}\n"
737
+ "The end_conversation tool is reserved for the simulated user. "
738
+ "Never call it as the agent; when the task is complete, reply "
739
+ "to the user in natural language.\n"
740
+ "</agentinputlab_visible_input>"
741
+ ),
742
+ },
743
+ )
744
+ metadata = state.get("metadata", {})
745
+ clarification = (
746
+ metadata.get("clarification_request") if isinstance(metadata, Mapping) else None
747
+ )
748
+ if self._first_inference and not clarification:
749
+ for message in reversed(messages):
750
+ if message.get("role") == "user":
751
+ message["content"] = state["query"]
752
+ break
753
+ self._first_inference = False
754
+ if requires_qwen38_initial_system_message(self.model_name):
755
+ messages = coalesce_chat_system_messages(messages)
756
+ call_seed = self._base_seed + self._inference_calls
757
+ self._inference_calls += 1
758
+ output_cap = min(
759
+ budget["max_output_tokens_per_call"],
760
+ budget["max_output_tokens"] - budget["output_tokens"],
761
+ )
762
+ if output_cap < 1:
763
+ raise OfficialArenaError("ToolSandbox shared output budget is exhausted")
764
+ response = self.openai_client.chat.completions.create(
765
+ model=self.model_name,
766
+ messages=messages,
767
+ tools=openai_tools,
768
+ temperature=0.0,
769
+ seed=call_seed,
770
+ max_tokens=output_cap,
771
+ extra_body={"chat_template_kwargs": {"enable_thinking": False}},
772
+ )
773
+ budget["model_calls"] += 1
774
+ self._last_response = response
775
+ self._last_advertised_tool_names = _openai_tool_names(openai_tools)
776
+ enforcement = _enforce_agent_tool_budget(response, budget=budget)
777
+ if enforcement is not None:
778
+ self.budget_enforcements.append(enforcement)
779
+ self.compatibility_repairs.extend(_normalize_tool_call_ids(response))
780
+ _record_openai_usage(self.usage, response)
781
+ reported = getattr(response, "usage", None)
782
+ budget["output_tokens"] += int(getattr(reported, "completion_tokens", 0) or 0)
783
+ return response
784
+
785
+ return IntervenedAgent()
786
+
787
+
788
+ def _normalize_tool_call_ids(response: Any) -> list[dict[str, str]]:
789
+ """Encode opaque call IDs for upstream's Python-variable representation.
790
+
791
+ The pinned converter interpolates IDs into ``<id>_parameters`` and
792
+ ``<id>_response``. OpenAI-compatible IDs may contain hyphens or punctuation;
793
+ those are valid correlation IDs, not valid Python identifiers. Change only
794
+ that representation before upstream stores both the call and its result,
795
+ so subsequent tool-result references use the same alias. Keep names,
796
+ arguments, order, call count, text, usage and all generation controls intact.
797
+ Raw HTTP receipts remain unchanged; every alias is recorded separately.
798
+ """
799
+ choices = getattr(response, "choices", None)
800
+ if not isinstance(choices, (list, tuple)) or not choices:
801
+ return []
802
+ message = getattr(choices[0], "message", None)
803
+ calls = getattr(message, "tool_calls", None)
804
+ if not isinstance(calls, (list, tuple)) or not calls:
805
+ return []
806
+ reserved = {
807
+ call.id for call in calls
808
+ if isinstance(getattr(call, "id", None), str) and call.id.isidentifier()
809
+ }
810
+ aliases: dict[str, str] = {}
811
+ repairs: list[dict[str, str]] = []
812
+ for call in calls:
813
+ original = getattr(call, "id", None)
814
+ # Missing/non-string IDs remain upstream validation errors. Do not
815
+ # synthesize a missing model field or repair duplicate logical calls.
816
+ if not isinstance(original, str) or not original or original.isidentifier():
817
+ continue
818
+ if original not in aliases:
819
+ base = "ail_call_" + hashlib.sha256(original.encode()).hexdigest()[:32]
820
+ alias = base
821
+ suffix = 0
822
+ while alias in reserved:
823
+ suffix += 1
824
+ alias = f"{base}_{suffix}"
825
+ aliases[original] = alias
826
+ reserved.add(alias)
827
+ repairs.append({
828
+ "kind": "tool_call_id_to_python_identifier",
829
+ "original_id": original,
830
+ "normalized_id": alias,
831
+ })
832
+ call.id = aliases[original]
833
+ return repairs
834
+
835
+
836
+ def _enforce_agent_tool_budget(
837
+ response: Any,
838
+ *,
839
+ budget: dict[str, int],
840
+ ) -> dict[str, int] | None:
841
+ """Execute at most the remaining agent tool calls from one model response.
842
+
843
+ ToolSandbox permits parallel tool calls, so limiting model turns alone does
844
+ not enforce ``max_tool_calls``. Truncation happens before the upstream role
845
+ materializes calls into executable messages; the partial trajectory is then
846
+ scored as a shared-budget hard stop instead of being discarded after replay.
847
+ """
848
+
849
+ choices = getattr(response, "choices", None)
850
+ if not isinstance(choices, (list, tuple)) or not choices:
851
+ return None
852
+ message = getattr(choices[0], "message", None)
853
+ calls = getattr(message, "tool_calls", None)
854
+ if not isinstance(calls, (list, tuple)) or not calls:
855
+ return None
856
+ remaining = max(0, budget["max_tool_calls"] - budget["tool_calls"])
857
+ kept = min(len(calls), remaining)
858
+ budget["tool_calls"] += kept
859
+ if kept == len(calls):
860
+ return None
861
+ # ``respond`` prevents inference when no tool budget remains, so a generated
862
+ # tool response always retains at least one call here. Preserve upstream call
863
+ # order and remove only the suffix that cannot be executed fairly.
864
+ message.tool_calls = list(calls[:kept])
865
+ budget["hard_stops"] = 1
866
+ return {
867
+ "generated_tool_calls": len(calls),
868
+ "executed_tool_calls": kept,
869
+ "discarded_tool_calls": len(calls) - kept,
870
+ }
871
+
872
+
873
+ def _openai_tool_names(openai_tools: Any) -> set[str]:
874
+ if isinstance(openai_tools, (str, bytes)) or not isinstance(openai_tools, (list, tuple)):
875
+ return set()
876
+ names: set[str] = set()
877
+ for tool in openai_tools:
878
+ if not isinstance(tool, Mapping):
879
+ continue
880
+ function = tool.get("function")
881
+ if isinstance(function, Mapping):
882
+ name = function.get("name")
883
+ if isinstance(name, str) and name:
884
+ names.add(name)
885
+ return names
886
+
887
+
888
+ def _repair_agent_end_conversation(
889
+ response: Any,
890
+ *,
891
+ advertised_tool_names: set[str],
892
+ ) -> str | None:
893
+ """Turn one known model-protocol error into an auditable user-facing turn."""
894
+ choices = getattr(response, "choices", None)
895
+ if not isinstance(choices, (list, tuple)) or not choices:
896
+ return None
897
+ message = getattr(choices[0], "message", None)
898
+ calls = getattr(message, "tool_calls", None)
899
+ if not isinstance(calls, (list, tuple)) or len(calls) != 1:
900
+ return None
901
+ function = getattr(calls[0], "function", None)
902
+ name = getattr(function, "name", None)
903
+ if name != "end_conversation" or name in advertised_tool_names:
904
+ return None
905
+ content = getattr(message, "content", None)
906
+ if isinstance(content, str) and content.strip():
907
+ return content.strip()
908
+ return "I've completed the requested step."
909
+
910
+
911
+ def _repair_user_end_conversation(
912
+ response: Any,
913
+ *,
914
+ tools_were_given: bool,
915
+ ) -> str | None:
916
+ """Recover the one valid ToolSandbox user terminator from a strict client mismatch."""
917
+ if tools_were_given:
918
+ return None
919
+ choices = getattr(response, "choices", None)
920
+ if not isinstance(choices, (list, tuple)) or not choices:
921
+ return None
922
+ message = getattr(choices[0], "message", None)
923
+ calls = getattr(message, "tool_calls", None)
924
+ if not isinstance(calls, (list, tuple)) or len(calls) != 1:
925
+ return None
926
+ function = getattr(calls[0], "function", None)
927
+ if getattr(function, "name", None) != "end_conversation":
928
+ return None
929
+ return "end_conversation()"
930
+
931
+
932
+ def _normalize_empty_user_tool_calls(response: Any, *, tools_were_given: bool) -> bool:
933
+ """Normalize vLLM's empty list to the OpenAI ``null`` no-tool representation."""
934
+ if tools_were_given:
935
+ return False
936
+ choices = getattr(response, "choices", None)
937
+ if not choices:
938
+ return False
939
+ message = getattr(choices[0], "message", None)
940
+ if getattr(message, "tool_calls", None) != []:
941
+ return False
942
+ message.tool_calls = None
943
+ return True
944
+
945
+
946
+ def _tracked_user(
947
+ config: Mapping[str, Any],
948
+ *,
949
+ seed: int,
950
+ budget: dict[str, int],
951
+ ) -> Any:
952
+ from openai import OpenAI
953
+ from tool_sandbox.common.execution_context import RoleType
954
+ from tool_sandbox.common.message_conversion import Message
955
+ from tool_sandbox.roles.openai_api_user import OpenAIAPIUser
956
+
957
+ class TrackedUser(OpenAIAPIUser):
958
+ model_name = config["user_model"]
959
+
960
+ def __init__(self) -> None:
961
+ # OpenAIAPIUser's pinned constructor only creates the default
962
+ # client; no role state is skipped by binding it directly here.
963
+ self.openai_client = OpenAI(
964
+ api_key=os.environ[config["user_key_env"]],
965
+ base_url=config["user_base_url"],
966
+ max_retries=0,
967
+ )
968
+ self.usage = _empty_usage()
969
+ self._base_seed = seed
970
+ self._inference_calls = 0
971
+ self._last_response: Any = None
972
+ self._tools_were_given = False
973
+ self._last_advertised_tool_names: set[str] = set()
974
+ self.compatibility_repairs: list[dict[str, Any]] = []
975
+
976
+ def respond(self, ending_index: int | None = None) -> None:
977
+ if budget["hard_stops"] or budget["model_calls"] >= budget["max_model_calls"]:
978
+ if not budget["hard_stops"]:
979
+ budget["hard_stops"] = 1
980
+ self.add_messages(
981
+ [
982
+ Message(
983
+ sender=RoleType.USER,
984
+ recipient=RoleType.EXECUTION_ENVIRONMENT,
985
+ content="end_conversation()",
986
+ conversation_active=False,
987
+ )
988
+ ]
989
+ )
990
+ return
991
+ self._last_response = None
992
+ self._tools_were_given = False
993
+ try:
994
+ super().respond(ending_index=ending_index)
995
+ except AssertionError:
996
+ if _is_empty_length_response(self._last_response):
997
+ budget["hard_stops"] = 1
998
+ replacement = "end_conversation()"
999
+ self.compatibility_repairs.append(
1000
+ {
1001
+ "kind": "empty_length_response_to_user_budget_hard_stop",
1002
+ "replacement": replacement,
1003
+ }
1004
+ )
1005
+ self.add_messages(
1006
+ [
1007
+ Message(
1008
+ sender=RoleType.USER,
1009
+ recipient=RoleType.EXECUTION_ENVIRONMENT,
1010
+ content=replacement,
1011
+ conversation_active=False,
1012
+ )
1013
+ ]
1014
+ )
1015
+ return
1016
+ if _is_generated_user_content_invalid(self._last_response):
1017
+ # The upstream ToolSandbox user role asserts that a
1018
+ # no-tool response must contain visible text. vLLM may
1019
+ # legally materialize an empty assistant message instead
1020
+ # (most often after reasoning consumed the generation
1021
+ # allowance). This is simulator infrastructure, not the
1022
+ # evaluated agent, so end the simulated conversation and
1023
+ # preserve the official scorer rather than discarding the
1024
+ # experiment cell as a fictitious tool-schema failure.
1025
+ budget["hard_stops"] = 1
1026
+ replacement = "end_conversation()"
1027
+ self.compatibility_repairs.append(
1028
+ {
1029
+ "kind": "empty_user_response_to_simulator_hard_stop",
1030
+ "replacement": replacement,
1031
+ }
1032
+ )
1033
+ self.add_messages(
1034
+ [
1035
+ Message(
1036
+ sender=RoleType.USER,
1037
+ recipient=RoleType.EXECUTION_ENVIRONMENT,
1038
+ content=replacement,
1039
+ conversation_active=False,
1040
+ )
1041
+ ]
1042
+ )
1043
+ return
1044
+ repair = _repair_user_end_conversation(
1045
+ self._last_response,
1046
+ tools_were_given=self._tools_were_given,
1047
+ )
1048
+ if repair is None:
1049
+ raise OfficialArenaError(
1050
+ "ToolSandbox user emitted unsupported tools without an "
1051
+ f"advertised schema: {_response_tool_calls(self._last_response)!r}"
1052
+ ) from None
1053
+ self.compatibility_repairs.append(
1054
+ {
1055
+ "kind": "unadvertised_user_end_conversation_to_official_tool",
1056
+ "replacement": repair,
1057
+ }
1058
+ )
1059
+ self.add_messages(
1060
+ [
1061
+ Message(
1062
+ sender=RoleType.USER,
1063
+ recipient=RoleType.EXECUTION_ENVIRONMENT,
1064
+ content=repair,
1065
+ )
1066
+ ]
1067
+ )
1068
+ except KeyError:
1069
+ if not _has_unadvertised_agent_tool_call(
1070
+ self._last_response,
1071
+ advertised_tool_names=self._last_advertised_tool_names,
1072
+ ):
1073
+ raise
1074
+ # The user role is benchmark infrastructure. Never execute a
1075
+ # hallucinated, unadvertised simulator tool; terminate that
1076
+ # simulator trajectory explicitly so the evaluated agent is
1077
+ # still scored by the official scenario.
1078
+ budget["hard_stops"] = 1
1079
+ replacement = "end_conversation()"
1080
+ self.compatibility_repairs.append(
1081
+ {
1082
+ "kind": "unadvertised_user_tool_to_simulator_hard_stop",
1083
+ "replacement": replacement,
1084
+ }
1085
+ )
1086
+ self.add_messages(
1087
+ [
1088
+ Message(
1089
+ sender=RoleType.USER,
1090
+ recipient=RoleType.EXECUTION_ENVIRONMENT,
1091
+ content=replacement,
1092
+ conversation_active=False,
1093
+ )
1094
+ ]
1095
+ )
1096
+ except Exception as error:
1097
+ if not _is_gpt_oss_harmony_parse_failure(error, model_name=self.model_name):
1098
+ raise
1099
+ # This failure belongs to the simulated-user infrastructure,
1100
+ # not the evaluated agent. End the user trajectory explicitly
1101
+ # and retain the official scenario score.
1102
+ budget["model_calls"] += 1
1103
+ self.usage["model_calls"] += 1
1104
+ self.usage["unpriced_model_calls"] += 1
1105
+ budget["hard_stops"] = 1
1106
+ replacement = "end_conversation()"
1107
+ self.compatibility_repairs.append(
1108
+ {
1109
+ "kind": "gpt_oss_harmony_parse_failure_to_simulator_hard_stop",
1110
+ "replacement": replacement,
1111
+ }
1112
+ )
1113
+ self.add_messages(
1114
+ [
1115
+ Message(
1116
+ sender=RoleType.USER,
1117
+ recipient=RoleType.EXECUTION_ENVIRONMENT,
1118
+ content=replacement,
1119
+ conversation_active=False,
1120
+ )
1121
+ ]
1122
+ )
1123
+
1124
+ def model_inference(self, openai_messages: Any, openai_tools: Any) -> Any:
1125
+ if (
1126
+ budget["model_calls"] >= budget["max_model_calls"]
1127
+ or budget["output_tokens"] >= budget["max_output_tokens"]
1128
+ ):
1129
+ raise OfficialArenaError("ToolSandbox shared user-simulator budget is exhausted")
1130
+ call_seed = self._base_seed + self._inference_calls
1131
+ self._inference_calls += 1
1132
+ tools_were_given = isinstance(openai_tools, (list, tuple))
1133
+ output_cap = min(
1134
+ budget["max_output_tokens_per_call"],
1135
+ budget["max_output_tokens"] - budget["output_tokens"],
1136
+ )
1137
+ messages = [dict(item) for item in openai_messages]
1138
+ if requires_qwen38_initial_system_message(self.model_name):
1139
+ messages = coalesce_chat_system_messages(messages)
1140
+ response = self.openai_client.chat.completions.create(
1141
+ model=self.model_name,
1142
+ messages=messages,
1143
+ tools=openai_tools,
1144
+ tool_choice="auto" if tools_were_given else "none",
1145
+ temperature=0.0,
1146
+ seed=call_seed,
1147
+ max_tokens=output_cap,
1148
+ extra_body={"chat_template_kwargs": {"enable_thinking": False}},
1149
+ )
1150
+ budget["model_calls"] += 1
1151
+ self._last_response = response
1152
+ self._tools_were_given = tools_were_given
1153
+ self._last_advertised_tool_names = _openai_tool_names(openai_tools)
1154
+ if _normalize_empty_user_tool_calls(
1155
+ response,
1156
+ tools_were_given=tools_were_given,
1157
+ ):
1158
+ self.compatibility_repairs.append(
1159
+ {
1160
+ "kind": "empty_user_tool_calls_to_openai_null",
1161
+ "replacement": None,
1162
+ }
1163
+ )
1164
+ self.compatibility_repairs.extend(_normalize_tool_call_ids(response))
1165
+ _record_openai_usage(self.usage, response)
1166
+ reported = getattr(response, "usage", None)
1167
+ budget["output_tokens"] += int(getattr(reported, "completion_tokens", 0) or 0)
1168
+ return response
1169
+
1170
+ return TrackedUser()
1171
+
1172
+
1173
+ def _response_tool_calls(response: Any) -> list[tuple[Any, Any]]:
1174
+ choices = getattr(response, "choices", None)
1175
+ if not choices:
1176
+ return []
1177
+ message = getattr(choices[0], "message", None)
1178
+ calls = getattr(message, "tool_calls", None)
1179
+ if not calls:
1180
+ return []
1181
+ return [
1182
+ (
1183
+ getattr(getattr(call, "function", None), "name", None),
1184
+ getattr(getattr(call, "function", None), "arguments", None),
1185
+ )
1186
+ for call in calls
1187
+ ]
1188
+
1189
+
1190
+ def _is_empty_length_response(response: Any) -> bool:
1191
+ """Recognize a turn that exhausted its fair output cap before acting.
1192
+
1193
+ Reasoning-model APIs may return neither visible content nor tool calls when
1194
+ the shared completion cap is exhausted. ToolSandbox's upstream role asserts
1195
+ on that otherwise valid OpenAI response. Treating it as an explicit hard
1196
+ stop preserves the official failed trajectory and scorer instead of losing
1197
+ the experiment cell to an adapter exception.
1198
+ """
1199
+
1200
+ choices = getattr(response, "choices", None)
1201
+ if not isinstance(choices, (list, tuple)) or len(choices) != 1:
1202
+ return False
1203
+ choice = choices[0]
1204
+ if getattr(choice, "finish_reason", None) != "length":
1205
+ return False
1206
+ message = getattr(choice, "message", None)
1207
+ if message is None:
1208
+ return False
1209
+ content = getattr(message, "content", None)
1210
+ tool_calls = getattr(message, "tool_calls", None)
1211
+ return not (isinstance(content, str) and content.strip()) and not tool_calls
1212
+
1213
+
1214
+ def _is_generated_agent_content_invalid(response: Any) -> bool:
1215
+ """Recognize an otherwise valid response with neither content nor tool calls."""
1216
+
1217
+ choices = getattr(response, "choices", None)
1218
+ if not isinstance(choices, (list, tuple)) or len(choices) != 1:
1219
+ return False
1220
+ message = getattr(choices[0], "message", None)
1221
+ if message is None:
1222
+ return False
1223
+ content = getattr(message, "content", None)
1224
+ tool_calls = getattr(message, "tool_calls", None)
1225
+ return not (isinstance(content, str) and content.strip()) and not tool_calls
1226
+
1227
+
1228
+ def _is_generated_user_content_invalid(response: Any) -> bool:
1229
+ """Recognize the same empty generation at the simulated-user boundary."""
1230
+
1231
+ return _is_generated_agent_content_invalid(response)
1232
+
1233
+
1234
+ def _is_gpt_oss_harmony_parse_failure(error: Exception, *, model_name: str) -> bool:
1235
+ """Recognize only the local GPT-OSS Harmony parser's HTTP-500 failures."""
1236
+
1237
+ if model_name.casefold() != "gpt-oss-20b" or getattr(error, "status_code", None) != 500:
1238
+ return False
1239
+ body = getattr(error, "body", None)
1240
+ message: Any = None
1241
+ if isinstance(body, Mapping):
1242
+ message = body.get("message")
1243
+ nested = body.get("error")
1244
+ if message is None and isinstance(nested, Mapping):
1245
+ message = nested.get("message")
1246
+ if not isinstance(message, str):
1247
+ return False
1248
+ normalized = message.casefold()
1249
+ return normalized.startswith("unexpected tokens remaining in message header") or normalized.startswith(
1250
+ "unexpected token "
1251
+ )
1252
+
1253
+
1254
+ def _has_unadvertised_agent_tool_call(
1255
+ response: Any,
1256
+ *,
1257
+ advertised_tool_names: set[str],
1258
+ ) -> bool:
1259
+ """Recognize a generated tool name that the official turn did not expose."""
1260
+
1261
+ calls = _response_tool_calls(response)
1262
+ return bool(calls) and any(
1263
+ not isinstance(name, str) or name not in advertised_tool_names for name, _ in calls
1264
+ )
1265
+
1266
+
1267
+ def _empty_usage() -> dict[str, int | float]:
1268
+ return {
1269
+ "model_calls": 0,
1270
+ "retrieval_calls": 0,
1271
+ "tool_calls": 0,
1272
+ "embedding_calls": 0,
1273
+ "unpriced_model_calls": 0,
1274
+ "input_tokens": 0,
1275
+ "output_tokens": 0,
1276
+ "cpu_seconds": 0.0,
1277
+ "accelerator_seconds": 0.0,
1278
+ "cost_usd": 0.0,
1279
+ "compute_cost_usd": 0.0,
1280
+ }
1281
+
1282
+
1283
+ def _record_openai_usage(usage: dict[str, int | float], response: Any) -> None:
1284
+ usage["model_calls"] += 1
1285
+ reported = getattr(response, "usage", None)
1286
+ if reported is None:
1287
+ usage["unpriced_model_calls"] += 1
1288
+ return
1289
+ usage["input_tokens"] += int(getattr(reported, "prompt_tokens", 0) or 0)
1290
+ usage["output_tokens"] += int(getattr(reported, "completion_tokens", 0) or 0)
1291
+
1292
+
1293
+ def _merge_tracked_usage(
1294
+ left: Mapping[str, int | float],
1295
+ right: Mapping[str, int | float],
1296
+ ) -> dict[str, int | float]:
1297
+ return {key: left.get(key, 0) + right.get(key, 0) for key in _empty_usage()}
1298
+
1299
+
1300
+ def _agent_available_tools(context: Any) -> Mapping[str, Any]:
1301
+ """Return the exact tool names exposed to ToolSandbox's agent role."""
1302
+ return context.get_available_tools(scrambling_allowed=True)
1303
+
1304
+
1305
+ def _deterministic_scenarios(
1306
+ factory: Any,
1307
+ *,
1308
+ seed_material: Mapping[str, Any],
1309
+ ) -> Any:
1310
+ """Build upstream scenarios deterministically without leaking global RNG state."""
1311
+ previous_state = random.getstate()
1312
+ try:
1313
+ seed = int(content_hash(seed_material), 16)
1314
+ random.seed(seed)
1315
+ return factory()
1316
+ finally:
1317
+ random.setstate(previous_state)
1318
+
1319
+
1320
+ def factory() -> ToolSandboxProvider:
1321
+ return ToolSandboxProvider()