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,1227 @@
1
+ #!/usr/bin/env python3
2
+ """Pinned standalone component worker for Ask or Assume?.
3
+
4
+ The official ``IntentAgent`` is tightly coupled to OpenHands State,
5
+ ConversationMemory, Condenser, LLMRegistry, and controller transitions. This
6
+ worker therefore does not claim to run UA-Multi. It loads the exact pinned
7
+ IntentAgent prompt and ``ClarifyDecisionTool``/``ClarifyTool`` schemas without
8
+ executing checkout code, then runs the two separable model paths through one
9
+ operator-configured OpenAI-compatible backend.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import ast
16
+ import hashlib
17
+ import json
18
+ import math
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ import urllib.error
24
+ import urllib.parse
25
+ import urllib.request
26
+ from collections.abc import Mapping, Sequence
27
+ from dataclasses import dataclass
28
+ from pathlib import Path, PurePosixPath
29
+ from typing import Any, Protocol, TextIO
30
+
31
+ UPSTREAM_COMMIT = "728464a755f15fd448aff61a5cb32dce97fd9c1d"
32
+ METHOD_ID = "ask_or_assume"
33
+ RELEASE_ID = "clarify.ask_or_assume.intent_monitor_component"
34
+ PROTOCOL_VERSION = "agentinputlab.ask-or-assume.monitor.v1"
35
+ MANIFEST_SCHEMA = "agentinputlab.ask-or-assume.compatibility.v1"
36
+ DEFAULT_MANIFEST = Path(__file__).with_name("compatibility_manifest.json")
37
+ INTENT_PROMPT_PATH = "openhands/agenthub/intent_agent/prompts/system_prompt.j2"
38
+ INTENT_TOOL_PATH = "openhands/agenthub/intent_agent/tools/clarify_decision.py"
39
+ CLARIFY_TOOL_PATH = "openhands/agenthub/clarify_agent/tools/clarify.py"
40
+ TOOL_NAMES_PATH = "openhands/llm/tool_names.py"
41
+ MAX_REQUEST_BYTES = 2 * 1024 * 1024
42
+ MAX_HISTORY_ITEMS = 2048
43
+ MAX_TEXT_CHARS = 500_000
44
+ _HASH_RE = re.compile(r"[0-9a-f]{64}")
45
+ _COMMIT_RE = re.compile(r"[0-9a-f]{40}")
46
+ _REF_RE = re.compile(r"refs/[A-Za-z0-9._/-]+")
47
+ _ENV_NAME_RE = re.compile(r"[A-Z_][A-Z0-9_]*")
48
+ _ROOT_FIELDS = frozenset(
49
+ {
50
+ "method_id",
51
+ "release_id",
52
+ "upstream_commit",
53
+ "state_hash",
54
+ "payload",
55
+ "dependency_fingerprints",
56
+ "minimum_reservation",
57
+ "protocol_version",
58
+ "request_id",
59
+ }
60
+ )
61
+ _PAYLOAD_FIELDS = frozenset(
62
+ {
63
+ "task_id",
64
+ "task_view",
65
+ "history",
66
+ "turn_index",
67
+ "clarification_turns",
68
+ "controls",
69
+ "intent_prompt_hash",
70
+ "seed",
71
+ }
72
+ )
73
+ _CONTROL_FIELDS = frozenset(
74
+ {
75
+ "skip_first_turn",
76
+ "max_coding_turns",
77
+ "max_user_turns",
78
+ "max_model_calls",
79
+ "max_tokens",
80
+ "max_cost_usd",
81
+ }
82
+ )
83
+ _ROLE_NAMES = ("intent", "coding")
84
+
85
+
86
+ def _requires_qwen38_initial_system_message(model: Any) -> bool:
87
+ if not isinstance(model, str):
88
+ return False
89
+ alias = model.strip().casefold().replace("_", "-")
90
+ return alias == "qwen3.8" or alias.startswith("qwen3.8-")
91
+
92
+
93
+ def _coalesce_chat_system_messages(messages: Sequence[Any]) -> list[Any]:
94
+ copied = [dict(item) if isinstance(item, Mapping) else item for item in messages]
95
+ system_items: list[dict[str, Any]] = []
96
+ non_system_items: list[Any] = []
97
+ for item in copied:
98
+ if isinstance(item, Mapping) and item.get("role") == "system":
99
+ content = item.get("content")
100
+ if not isinstance(content, str):
101
+ raise DriverError("INVALID_SCHEMA", "Qwen3.8 system message content must be text")
102
+ system_items.append(dict(item))
103
+ else:
104
+ non_system_items.append(item)
105
+ if not system_items:
106
+ return copied
107
+ merged = dict(system_items[0])
108
+ merged["content"] = "\n\n".join(item["content"] for item in system_items)
109
+ return [merged, *non_system_items]
110
+
111
+
112
+ class DriverError(RuntimeError):
113
+ """Expected fail-closed worker error with a stable public code."""
114
+
115
+ def __init__(self, code: str, message: str) -> None:
116
+ super().__init__(message)
117
+ self.code = code
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class CompatibilityPolicy:
122
+ commit: str
123
+ license_path: str
124
+ license_spdx: str
125
+ license_sha256: str
126
+ require_clean_checkout: bool
127
+ excluded_prefixes: tuple[str, ...]
128
+ source_hashes: Mapping[str, str]
129
+
130
+ @classmethod
131
+ def from_mapping(cls, value: Mapping[str, Any]) -> CompatibilityPolicy:
132
+ if value.get("schema_version") != MANIFEST_SCHEMA:
133
+ raise DriverError("INVALID_MANIFEST", "Ask-or-Assume manifest schema mismatch")
134
+ upstream = _mapping(value.get("upstream"), "manifest upstream")
135
+ sources = _mapping(value.get("source_files"), "manifest source_files")
136
+ commit = _text(upstream.get("commit"), "manifest commit")
137
+ license_spdx = _text(upstream.get("license_spdx"), "manifest license_spdx")
138
+ if commit != UPSTREAM_COMMIT or license_spdx != "MIT":
139
+ raise DriverError(
140
+ "INVALID_MANIFEST",
141
+ "Ask-or-Assume manifest must pin the approved commit and MIT root license",
142
+ )
143
+ clean = upstream.get("require_clean_checkout")
144
+ if not isinstance(clean, bool):
145
+ raise DriverError("INVALID_MANIFEST", "manifest clean-checkout flag must be boolean")
146
+ raw_excluded = upstream.get("excluded_prefixes")
147
+ if not isinstance(raw_excluded, Sequence) or isinstance(raw_excluded, (str, bytes)):
148
+ raise DriverError("INVALID_MANIFEST", "manifest excluded_prefixes must be a sequence")
149
+ excluded = tuple(_safe_prefix(item) for item in raw_excluded)
150
+ if "enterprise/" not in excluded:
151
+ raise DriverError("INVALID_MANIFEST", "manifest must exclude enterprise/")
152
+ source_hashes: dict[str, str] = {}
153
+ for raw_path, raw_hash in sources.items():
154
+ path = _safe_relative_path(raw_path, "manifest source path")
155
+ _reject_excluded_path(path, excluded)
156
+ source_hashes[path] = _hash(raw_hash, f"manifest hash for {path}")
157
+ required = {INTENT_PROMPT_PATH, INTENT_TOOL_PATH, CLARIFY_TOOL_PATH, TOOL_NAMES_PATH}
158
+ if not required <= set(source_hashes):
159
+ raise DriverError("INVALID_MANIFEST", "manifest omits an official prompt/tool source")
160
+ return cls(
161
+ commit=commit,
162
+ license_path=_safe_relative_path(
163
+ _text(upstream.get("license_path"), "manifest license_path"),
164
+ "manifest license_path",
165
+ ),
166
+ license_spdx=license_spdx,
167
+ license_sha256=_hash(upstream.get("license_sha256"), "manifest license hash"),
168
+ require_clean_checkout=clean,
169
+ excluded_prefixes=excluded,
170
+ source_hashes=source_hashes,
171
+ )
172
+
173
+
174
+ @dataclass(frozen=True, slots=True)
175
+ class RoleConfig:
176
+ model: str
177
+ fingerprint: Mapping[str, Any]
178
+ max_output_tokens: int
179
+ input_cost_per_million: float
180
+ output_cost_per_million: float
181
+
182
+
183
+ def _fingerprint_model(value: Mapping[str, Any]) -> str | None:
184
+ """Resolve model identity from native or AgentInputLab backend fingerprints."""
185
+
186
+ for key in ("model", "model_id"):
187
+ candidate = value.get(key)
188
+ if isinstance(candidate, str) and candidate.strip():
189
+ return candidate
190
+ nested = value.get("backend")
191
+ if isinstance(nested, Mapping):
192
+ return _fingerprint_model(nested)
193
+ return None
194
+
195
+
196
+ @dataclass(frozen=True, slots=True)
197
+ class BackendConfig:
198
+ base_url: str
199
+ api_key_env: str
200
+ timeout_seconds: float
201
+ roles: Mapping[str, RoleConfig]
202
+
203
+ @classmethod
204
+ def from_json(cls, raw: str) -> BackendConfig:
205
+ root = _mapping(_loads_strict_json(raw), "backend configuration")
206
+ _exact_keys(root, {"base_url", "api_key_env", "timeout_seconds", "roles"}, "backend")
207
+ base_url = _validate_base_url(_text(root["base_url"], "backend base_url"))
208
+ api_key_env = _text(root["api_key_env"], "backend api_key_env")
209
+ if _ENV_NAME_RE.fullmatch(api_key_env) is None:
210
+ raise DriverError("INVALID_BACKEND_CONFIG", "backend api_key_env is invalid")
211
+ timeout = _positive_float(root["timeout_seconds"], "backend timeout_seconds")
212
+ raw_roles = _mapping(root["roles"], "backend roles")
213
+ if set(raw_roles) != set(_ROLE_NAMES):
214
+ raise DriverError("INVALID_BACKEND_CONFIG", "backend must bind intent and coding roles")
215
+ roles: dict[str, RoleConfig] = {}
216
+ for role in _ROLE_NAMES:
217
+ item = _mapping(raw_roles[role], f"backend {role} role")
218
+ _exact_keys(
219
+ item,
220
+ {
221
+ "model",
222
+ "fingerprint",
223
+ "max_output_tokens",
224
+ "input_cost_per_million",
225
+ "output_cost_per_million",
226
+ },
227
+ f"backend {role} role",
228
+ )
229
+ model = _text(item["model"], f"backend {role} model")
230
+ fingerprint = dict(_mapping(item["fingerprint"], f"backend {role} fingerprint"))
231
+ if not fingerprint or _fingerprint_model(fingerprint) != model:
232
+ raise DriverError(
233
+ "INVALID_BACKEND_CONFIG",
234
+ f"backend {role} fingerprint must bind its exact model",
235
+ )
236
+ _canonical_json(fingerprint)
237
+ roles[role] = RoleConfig(
238
+ model=model,
239
+ fingerprint=fingerprint,
240
+ max_output_tokens=_positive_int(
241
+ item["max_output_tokens"], f"backend {role} max_output_tokens"
242
+ ),
243
+ input_cost_per_million=_nonnegative_float(
244
+ item["input_cost_per_million"], f"backend {role} input price"
245
+ ),
246
+ output_cost_per_million=_nonnegative_float(
247
+ item["output_cost_per_million"], f"backend {role} output price"
248
+ ),
249
+ )
250
+ return cls(base_url=base_url, api_key_env=api_key_env, timeout_seconds=timeout, roles=roles)
251
+
252
+
253
+ @dataclass(frozen=True, slots=True)
254
+ class CompletionResult:
255
+ arguments: Mapping[str, Any]
256
+ input_tokens: int
257
+ output_tokens: int
258
+
259
+
260
+ class CompletionBackend(Protocol):
261
+ def fingerprint(self, role: str) -> Mapping[str, Any]: ...
262
+
263
+ def complete(
264
+ self,
265
+ *,
266
+ role: str,
267
+ messages: Sequence[Mapping[str, Any]],
268
+ tool: Mapping[str, Any],
269
+ seed: int,
270
+ ) -> CompletionResult: ...
271
+
272
+
273
+ class OpenAICompatibleBackend:
274
+ """Small startup-configured OpenAI-compatible chat-completions client."""
275
+
276
+ def __init__(self, config: BackendConfig) -> None:
277
+ self.config = config
278
+
279
+ def fingerprint(self, role: str) -> Mapping[str, Any]:
280
+ return self.config.roles[role].fingerprint
281
+
282
+ def complete(
283
+ self,
284
+ *,
285
+ role: str,
286
+ messages: Sequence[Mapping[str, Any]],
287
+ tool: Mapping[str, Any],
288
+ seed: int,
289
+ ) -> CompletionResult:
290
+ role_config = self.config.roles[role]
291
+ api_key = os.environ.get(self.config.api_key_env)
292
+ if not api_key:
293
+ raise DriverError("BACKEND_CREDENTIAL_UNAVAILABLE", "backend credential is unavailable")
294
+ tool_name = _tool_name(tool)
295
+ request_messages = list(messages)
296
+ if _requires_qwen38_initial_system_message(role_config.model):
297
+ request_messages = _coalesce_chat_system_messages(request_messages)
298
+ body = {
299
+ "model": role_config.model,
300
+ "messages": request_messages,
301
+ "tools": [tool],
302
+ "tool_choice": {"type": "function", "function": {"name": tool_name}},
303
+ "temperature": 0,
304
+ "seed": seed,
305
+ "max_tokens": role_config.max_output_tokens,
306
+ }
307
+ encoded = _canonical_json(body).encode("utf-8")
308
+ request = urllib.request.Request(
309
+ f"{self.config.base_url}/chat/completions",
310
+ data=encoded,
311
+ headers={
312
+ "Authorization": f"Bearer {api_key}",
313
+ "Content-Type": "application/json",
314
+ },
315
+ method="POST",
316
+ )
317
+ try:
318
+ with urllib.request.urlopen(request, timeout=self.config.timeout_seconds) as response:
319
+ raw = response.read(MAX_REQUEST_BYTES + 1)
320
+ except (OSError, urllib.error.HTTPError, urllib.error.URLError) as exc:
321
+ raise DriverError("BACKEND_CALL_FAILED", "backend completion request failed") from exc
322
+ if len(raw) > MAX_REQUEST_BYTES:
323
+ raise DriverError("BACKEND_RESPONSE_TOO_LARGE", "backend response exceeded its limit")
324
+ root = _mapping(_loads_strict_json(raw.decode("utf-8")), "backend response")
325
+ choices = root.get("choices")
326
+ if (
327
+ not isinstance(choices, Sequence)
328
+ or isinstance(choices, (str, bytes))
329
+ or len(choices) != 1
330
+ ):
331
+ raise DriverError("BACKEND_OUTPUT_INVALID", "backend must return exactly one choice")
332
+ message = _mapping(_mapping(choices[0], "backend choice").get("message"), "backend message")
333
+ calls = message.get("tool_calls")
334
+ if not isinstance(calls, Sequence) or isinstance(calls, (str, bytes)) or len(calls) != 1:
335
+ raise DriverError("BACKEND_OUTPUT_INVALID", "backend must return exactly one tool call")
336
+ function = _mapping(
337
+ _mapping(calls[0], "backend tool call").get("function"), "tool function"
338
+ )
339
+ if function.get("name") != tool_name:
340
+ raise DriverError("BACKEND_OUTPUT_INVALID", "backend called a non-forced tool")
341
+ arguments_raw = function.get("arguments")
342
+ if not isinstance(arguments_raw, str):
343
+ raise DriverError("BACKEND_OUTPUT_INVALID", "backend tool arguments must be JSON text")
344
+ arguments = _mapping(_loads_strict_json(arguments_raw), "backend tool arguments")
345
+ usage = _mapping(root.get("usage"), "backend usage")
346
+ return CompletionResult(
347
+ arguments=dict(arguments),
348
+ input_tokens=_nonnegative_int(usage.get("prompt_tokens"), "backend prompt_tokens"),
349
+ output_tokens=_nonnegative_int(
350
+ usage.get("completion_tokens"), "backend completion_tokens"
351
+ ),
352
+ )
353
+
354
+
355
+ @dataclass(frozen=True, slots=True)
356
+ class Controls:
357
+ skip_first_turn: bool
358
+ max_coding_turns: int
359
+ max_user_turns: int
360
+ max_model_calls: int
361
+ max_tokens: int
362
+ max_cost_usd: float
363
+
364
+
365
+ @dataclass(frozen=True, slots=True)
366
+ class ValidatedRequest:
367
+ request_id: str
368
+ state_hash: str
369
+ task_id: str
370
+ task_view: Mapping[str, Any]
371
+ history: tuple[Mapping[str, Any], ...]
372
+ turn_index: int
373
+ clarification_turns: int
374
+ controls: Controls
375
+ seed: int
376
+ dependencies: Mapping[str, Mapping[str, Any]]
377
+
378
+
379
+ @dataclass(frozen=True, slots=True)
380
+ class OfficialAssets:
381
+ intent_prompt: str
382
+ intent_prompt_sha256: str
383
+ intent_tool: Mapping[str, Any]
384
+ clarify_tool: Mapping[str, Any]
385
+ intent_tool_schema_hash: str
386
+ clarify_tool_schema_hash: str
387
+
388
+
389
+ class UsageTracker:
390
+ def __init__(self, controls: Controls, config: BackendConfig) -> None:
391
+ self.controls = controls
392
+ self.config = config
393
+ self.model_calls = 0
394
+ self.input_tokens = 0
395
+ self.output_tokens = 0
396
+ self.cost_usd = 0.0
397
+
398
+ def complete(
399
+ self,
400
+ backend: CompletionBackend,
401
+ *,
402
+ role: str,
403
+ messages: Sequence[Mapping[str, Any]],
404
+ tool: Mapping[str, Any],
405
+ seed: int,
406
+ ) -> Mapping[str, Any]:
407
+ if self.model_calls >= self.controls.max_model_calls:
408
+ raise DriverError(
409
+ "MODEL_CALL_BUDGET_EXCEEDED",
410
+ "Ask-or-Assume reached max_model_calls before the next provider attempt",
411
+ )
412
+ self.model_calls += 1
413
+ result = backend.complete(role=role, messages=messages, tool=tool, seed=seed)
414
+ if not isinstance(result, CompletionResult):
415
+ raise DriverError("BACKEND_OUTPUT_INVALID", "backend returned an invalid completion")
416
+ self.input_tokens += _nonnegative_int(result.input_tokens, "completion input_tokens")
417
+ self.output_tokens += _nonnegative_int(result.output_tokens, "completion output_tokens")
418
+ pricing = self.config.roles[role]
419
+ self.cost_usd += (
420
+ result.input_tokens * pricing.input_cost_per_million
421
+ + result.output_tokens * pricing.output_cost_per_million
422
+ ) / 1_000_000
423
+ if self.input_tokens + self.output_tokens > self.controls.max_tokens:
424
+ raise DriverError("TOKEN_BUDGET_EXCEEDED", "Ask-or-Assume exceeded max_tokens")
425
+ if self.cost_usd > self.controls.max_cost_usd:
426
+ raise DriverError("COST_BUDGET_EXCEEDED", "Ask-or-Assume exceeded max_cost_usd")
427
+ return dict(result.arguments)
428
+
429
+ def as_dict(self) -> dict[str, int | float]:
430
+ return {
431
+ "model_calls": self.model_calls,
432
+ "retrieval_calls": 0,
433
+ "input_tokens": self.input_tokens,
434
+ "output_tokens": self.output_tokens,
435
+ "cost_usd": self.cost_usd,
436
+ }
437
+
438
+
439
+ class AskOrAssumeRuntime:
440
+ """Execute the controlled component harness against one verified checkout."""
441
+
442
+ def __init__(
443
+ self,
444
+ checkout: Path,
445
+ backend_config: BackendConfig,
446
+ backend: CompletionBackend,
447
+ *,
448
+ policy: CompatibilityPolicy | None = None,
449
+ ) -> None:
450
+ self.policy = policy or load_compatibility_policy()
451
+ self.checkout = verify_checkout(checkout, self.policy)
452
+ self.assets = load_official_assets(self.checkout)
453
+ self.backend_config = backend_config
454
+ self.backend = backend
455
+ for role in _ROLE_NAMES:
456
+ if _canonical_json(backend.fingerprint(role)) != _canonical_json(
457
+ backend_config.roles[role].fingerprint
458
+ ):
459
+ raise DriverError(
460
+ "BACKEND_IDENTITY_MISMATCH",
461
+ f"runtime {role} backend differs from startup configuration",
462
+ )
463
+
464
+ def run(self, payload: Mapping[str, Any]) -> dict[str, Any]:
465
+ request = validate_request(payload, self.backend_config, self.assets)
466
+ controls = request.controls
467
+ if request.turn_index >= controls.max_coding_turns:
468
+ raise DriverError(
469
+ "CODING_TURN_BUDGET_EXCEEDED",
470
+ "Ask-or-Assume effective coding-turn budget is exhausted",
471
+ )
472
+ if controls.skip_first_turn and request.turn_index == 0:
473
+ raise DriverError(
474
+ "FIRST_TURN_NOT_DELEGATED",
475
+ "Ask-or-Assume IntentAgent must not run before the first coding turn",
476
+ )
477
+ tracker = UsageTracker(controls, self.backend_config)
478
+ history_document = _canonical_json(
479
+ {
480
+ "task_id": request.task_id,
481
+ "task_view": request.task_view,
482
+ "conversation_history": request.history,
483
+ "effective_coding_turns": request.turn_index,
484
+ "effective_user_turns": request.clarification_turns,
485
+ }
486
+ )
487
+ intent_arguments = tracker.complete(
488
+ self.backend,
489
+ role="intent",
490
+ messages=(
491
+ {"role": "system", "content": self.assets.intent_prompt},
492
+ {"role": "user", "content": history_document},
493
+ ),
494
+ tool=self.assets.intent_tool,
495
+ seed=request.seed,
496
+ )
497
+ decision = _validate_intent_decision(intent_arguments)
498
+ needs_clarification = decision["needs_clarification"]
499
+ reasons = decision["reasons"]
500
+ message = decision["message"]
501
+ questions: tuple[Mapping[str, Any], ...] = ()
502
+ clarification_message = ""
503
+ trace: list[dict[str, Any]] = [
504
+ {
505
+ "index": 0,
506
+ "path": "intent_decision",
507
+ "role": "intent",
508
+ "tool_name": _tool_name(self.assets.intent_tool),
509
+ "decision": needs_clarification,
510
+ "reasons": reasons,
511
+ "message": message,
512
+ }
513
+ ]
514
+ if needs_clarification:
515
+ if request.clarification_turns >= controls.max_user_turns:
516
+ raise DriverError(
517
+ "USER_TURN_BUDGET_EXCEEDED",
518
+ "Ask-or-Assume effective user-turn budget is exhausted",
519
+ )
520
+ reminder = "Before proceeding further, ask a clarifying question."
521
+ if reasons:
522
+ reminder += f"\nReason: {reasons.strip()}"
523
+ clarify_arguments = tracker.complete(
524
+ self.backend,
525
+ role="coding",
526
+ messages=(
527
+ {"role": "user", "content": history_document},
528
+ {"role": "system", "content": reminder},
529
+ ),
530
+ tool=self.assets.clarify_tool,
531
+ seed=request.seed + 1,
532
+ )
533
+ clarify = _validate_clarify_call(clarify_arguments)
534
+ questions = clarify["questions"]
535
+ clarification_message = _render_official_clarify_message(clarify["message"], questions)
536
+ trace.append(
537
+ {
538
+ "index": 1,
539
+ "path": "forced_clarify",
540
+ "role": "coding",
541
+ "tool_name": _tool_name(self.assets.clarify_tool),
542
+ "message": clarify["message"],
543
+ "questions": questions,
544
+ "wait_for_response": clarify["wait"],
545
+ "rendered_message": clarification_message,
546
+ }
547
+ )
548
+ return {
549
+ "status": "ok",
550
+ "request_id": request.request_id,
551
+ "upstream_commit": UPSTREAM_COMMIT,
552
+ "protocol_version": PROTOCOL_VERSION,
553
+ "payload": {
554
+ "needs_clarification": needs_clarification,
555
+ "reasons": [reasons] if reasons else [],
556
+ "message": message,
557
+ "clarification_question": clarification_message,
558
+ "clarification_questions": list(questions),
559
+ "intent_summary": message,
560
+ "transition": (
561
+ "await"
562
+ if needs_clarification and clarify["wait"]
563
+ else "force"
564
+ if needs_clarification
565
+ else "resume"
566
+ ),
567
+ "delegated": True,
568
+ "forced": needs_clarification,
569
+ "first_turn_skipped": False,
570
+ "decision_trace": trace,
571
+ "execution_mode": "controlled_component_harness",
572
+ "component_only": True,
573
+ "openhands_full_system": False,
574
+ "ua_multi_parity_claimed": False,
575
+ "intent_prompt_sha256": self.assets.intent_prompt_sha256,
576
+ "intent_tool_schema_hash": self.assets.intent_tool_schema_hash,
577
+ "clarify_tool_schema_hash": self.assets.clarify_tool_schema_hash,
578
+ "effective_coding_turns": request.turn_index,
579
+ "effective_user_turns": request.clarification_turns,
580
+ "model_fingerprints": {
581
+ role: dict(self.backend_config.roles[role].fingerprint) for role in _ROLE_NAMES
582
+ },
583
+ },
584
+ "usage": tracker.as_dict(),
585
+ }
586
+
587
+
588
+ def load_compatibility_policy(path: Path = DEFAULT_MANIFEST) -> CompatibilityPolicy:
589
+ try:
590
+ value = _loads_strict_json(path.read_text(encoding="utf-8"))
591
+ except OSError as exc:
592
+ raise DriverError("MANIFEST_UNAVAILABLE", "compatibility manifest is unavailable") from exc
593
+ return CompatibilityPolicy.from_mapping(_mapping(value, "compatibility manifest"))
594
+
595
+
596
+ def verify_checkout(checkout: Path, policy: CompatibilityPolicy) -> Path:
597
+ try:
598
+ root = checkout.expanduser().resolve(strict=True)
599
+ except OSError as exc:
600
+ raise DriverError("CHECKOUT_UNAVAILABLE", "Ask-or-Assume checkout is unavailable") from exc
601
+ if not root.is_dir():
602
+ raise DriverError("CHECKOUT_UNAVAILABLE", "Ask-or-Assume checkout must be a directory")
603
+ if read_checkout_head(root) != policy.commit:
604
+ raise DriverError("UPSTREAM_REVISION_MISMATCH", "checkout is not at the pinned commit")
605
+ license_path = _checkout_file(root, policy.license_path)
606
+ if _file_sha256(license_path) != policy.license_sha256:
607
+ raise DriverError("UPSTREAM_LICENSE_MISMATCH", "root license hash does not match")
608
+ license_text = license_path.read_text(encoding="utf-8", errors="strict")
609
+ if "MIT License" not in license_text or "enterprise/" not in license_text:
610
+ raise DriverError(
611
+ "UPSTREAM_LICENSE_MISMATCH",
612
+ "root license must identify MIT content and the separate enterprise subtree",
613
+ )
614
+ verified_sources: list[Path] = []
615
+ for relative, expected_hash in policy.source_hashes.items():
616
+ _reject_excluded_path(relative, policy.excluded_prefixes)
617
+ source = _checkout_file(root, relative)
618
+ if _file_sha256(source) != expected_hash:
619
+ raise DriverError("UPSTREAM_SOURCE_MISMATCH", f"source hash mismatch: {relative}")
620
+ verified_sources.append(source)
621
+ _reject_enterprise_imports(verified_sources)
622
+ if policy.require_clean_checkout:
623
+ _require_clean_checkout(root)
624
+ return root
625
+
626
+
627
+ def load_official_assets(checkout: Path) -> OfficialAssets:
628
+ root = checkout.resolve(strict=True)
629
+ prompt_path = _checkout_file(root, INTENT_PROMPT_PATH)
630
+ prompt = prompt_path.read_text(encoding="utf-8", errors="strict")
631
+ names = _literal_assignments(_checkout_file(root, TOOL_NAMES_PATH), {})
632
+ intent_values = _literal_assignments(_checkout_file(root, INTENT_TOOL_PATH), names)
633
+ clarify_values = _literal_assignments(_checkout_file(root, CLARIFY_TOOL_PATH), names)
634
+ intent_tool = _mapping(intent_values.get("ClarifyDecisionTool"), "ClarifyDecisionTool")
635
+ clarify_tool = _mapping(clarify_values.get("ClarifyTool"), "ClarifyTool")
636
+ if _tool_name(intent_tool) != "clarify_decision" or _tool_name(clarify_tool) != "clarify":
637
+ raise DriverError("UPSTREAM_SCHEMA_INVALID", "official tool names are invalid")
638
+ _validate_tool_schema(intent_tool)
639
+ _validate_tool_schema(clarify_tool)
640
+ return OfficialAssets(
641
+ intent_prompt=prompt,
642
+ intent_prompt_sha256=hashlib.sha256(prompt.encode("utf-8")).hexdigest(),
643
+ intent_tool=dict(intent_tool),
644
+ clarify_tool=dict(clarify_tool),
645
+ intent_tool_schema_hash=_canonical_hash(intent_tool),
646
+ clarify_tool_schema_hash=_canonical_hash(clarify_tool),
647
+ )
648
+
649
+
650
+ def validate_request(
651
+ value: Mapping[str, Any], config: BackendConfig, assets: OfficialAssets
652
+ ) -> ValidatedRequest:
653
+ _exact_keys(value, set(_ROOT_FIELDS), "worker request")
654
+ if value["method_id"] != METHOD_ID or value["release_id"] != RELEASE_ID:
655
+ raise DriverError("PROFILE_IDENTITY_MISMATCH", "worker request targets another profile")
656
+ if value["upstream_commit"] != UPSTREAM_COMMIT:
657
+ raise DriverError("UPSTREAM_REVISION_MISMATCH", "worker request targets another revision")
658
+ if value["protocol_version"] != PROTOCOL_VERSION:
659
+ raise DriverError("PROTOCOL_MISMATCH", "worker protocol version mismatch")
660
+ request_id = _hash(value["request_id"], "worker request_id")
661
+ state_hash = _hash(value["state_hash"], "worker state_hash")
662
+ identity = {key: value[key] for key in _ROOT_FIELDS if key != "request_id"}
663
+ if request_id != _canonical_hash(identity):
664
+ raise DriverError("REQUEST_ID_MISMATCH", "worker request_id is invalid")
665
+ reservation = _mapping(value["minimum_reservation"], "minimum_reservation")
666
+ if reservation != {"model_calls": 1}:
667
+ raise DriverError("INVALID_RESERVATION", "worker must reserve one IntentAgent model call")
668
+ dependencies = _mapping(value["dependency_fingerprints"], "dependency_fingerprints")
669
+ if set(dependencies) != {"intent_model", "coding_model"}:
670
+ raise DriverError("BACKEND_IDENTITY_MISMATCH", "request must bind both model roles")
671
+ normalized_dependencies: dict[str, Mapping[str, Any]] = {}
672
+ for role, key in (("intent", "intent_model"), ("coding", "coding_model")):
673
+ fingerprint = _mapping(dependencies[key], f"{key} fingerprint")
674
+ if _canonical_json(fingerprint) != _canonical_json(config.roles[role].fingerprint):
675
+ raise DriverError(
676
+ "BACKEND_IDENTITY_MISMATCH", f"request {key} differs from worker startup"
677
+ )
678
+ normalized_dependencies[key] = dict(fingerprint)
679
+ payload = _mapping(value["payload"], "worker payload")
680
+ _exact_keys(payload, set(_PAYLOAD_FIELDS), "worker payload")
681
+ if payload["intent_prompt_hash"] != assets.intent_prompt_sha256:
682
+ raise DriverError("PROMPT_IDENTITY_MISMATCH", "request intent prompt hash is not pinned")
683
+ task_id = payload["task_id"]
684
+ if not isinstance(task_id, str):
685
+ raise DriverError("INVALID_SCHEMA", "task_id must be text")
686
+ task_view = _validate_task_view(payload["task_view"])
687
+ history = _validate_history(payload["history"])
688
+ turn_index = _nonnegative_int(payload["turn_index"], "effective coding turns")
689
+ clarification_turns = _nonnegative_int(payload["clarification_turns"], "effective user turns")
690
+ controls = _validate_controls(payload["controls"])
691
+ if clarification_turns > controls.max_user_turns:
692
+ raise DriverError("USER_TURN_BUDGET_EXCEEDED", "effective user turns exceed the budget")
693
+ return ValidatedRequest(
694
+ request_id=request_id,
695
+ state_hash=state_hash,
696
+ task_id=task_id,
697
+ task_view=task_view,
698
+ history=history,
699
+ turn_index=turn_index,
700
+ clarification_turns=clarification_turns,
701
+ controls=controls,
702
+ seed=_nonnegative_int(payload["seed"], "worker seed"),
703
+ dependencies=normalized_dependencies,
704
+ )
705
+
706
+
707
+ def _validate_task_view(value: Any) -> Mapping[str, Any]:
708
+ task = _mapping(value, "task_view")
709
+ _exact_keys(task, {"query", "instruction", "context", "tool_names"}, "task_view")
710
+ query = _plain_text(task["query"], "task query")
711
+ instruction = _plain_text(task["instruction"], "task instruction")
712
+ if not query.strip() and not instruction.strip():
713
+ raise DriverError("INVALID_SCHEMA", "task view requires query or instruction")
714
+ context = task["context"]
715
+ tool_names = task["tool_names"]
716
+ _canonical_json(context)
717
+ if not isinstance(tool_names, Sequence) or isinstance(tool_names, (str, bytes)):
718
+ raise DriverError("INVALID_SCHEMA", "task tool_names must be a sequence")
719
+ if not all(isinstance(item, str) and item for item in tool_names):
720
+ raise DriverError("INVALID_SCHEMA", "task tool_names must contain non-empty text")
721
+ normalized = {
722
+ "query": query,
723
+ "instruction": instruction,
724
+ "context": context,
725
+ "tool_names": list(tool_names),
726
+ }
727
+ if len(_canonical_json(normalized)) > MAX_TEXT_CHARS:
728
+ raise DriverError("REQUEST_TOO_LARGE", "task view exceeds its text limit")
729
+ return normalized
730
+
731
+
732
+ def _validate_history(value: Any) -> tuple[Mapping[str, Any], ...]:
733
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
734
+ raise DriverError("INVALID_SCHEMA", "history must be a sequence")
735
+ if len(value) > MAX_HISTORY_ITEMS:
736
+ raise DriverError("REQUEST_TOO_LARGE", "history contains too many messages")
737
+ result: list[Mapping[str, Any]] = []
738
+ for index, raw in enumerate(value):
739
+ item = _mapping(raw, f"history[{index}]")
740
+ _exact_keys(item, {"role", "content", "name"}, f"history[{index}]")
741
+ role = _text(item["role"], f"history[{index}] role")
742
+ content = _plain_text(item["content"], f"history[{index}] content")
743
+ name = item["name"]
744
+ if name is not None and not isinstance(name, str):
745
+ raise DriverError("INVALID_SCHEMA", f"history[{index}] name must be text or null")
746
+ result.append({"role": role, "content": content, "name": name})
747
+ return tuple(result)
748
+
749
+
750
+ def _validate_controls(value: Any) -> Controls:
751
+ item = _mapping(value, "controls")
752
+ _exact_keys(item, set(_CONTROL_FIELDS), "controls")
753
+ skip = item["skip_first_turn"]
754
+ if not isinstance(skip, bool):
755
+ raise DriverError("INVALID_SCHEMA", "skip_first_turn must be boolean")
756
+ return Controls(
757
+ skip_first_turn=skip,
758
+ max_coding_turns=_positive_int(item["max_coding_turns"], "max_coding_turns"),
759
+ max_user_turns=_positive_int(item["max_user_turns"], "max_user_turns"),
760
+ max_model_calls=_positive_int(item["max_model_calls"], "max_model_calls"),
761
+ max_tokens=_positive_int(item["max_tokens"], "max_tokens"),
762
+ max_cost_usd=_nonnegative_float(item["max_cost_usd"], "max_cost_usd"),
763
+ )
764
+
765
+
766
+ def _validate_intent_decision(value: Mapping[str, Any]) -> dict[str, Any]:
767
+ if set(value) - {"needs_clarification", "message", "reasons"}:
768
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify_decision returned unknown fields")
769
+ decision = value.get("needs_clarification")
770
+ if not isinstance(decision, bool):
771
+ raise DriverError("BACKEND_OUTPUT_INVALID", "needs_clarification must be boolean")
772
+ message = value.get("message", "")
773
+ reasons = value.get("reasons", "")
774
+ if not isinstance(message, str) or not isinstance(reasons, str):
775
+ raise DriverError("BACKEND_OUTPUT_INVALID", "decision message and reasons must be text")
776
+ if decision and not reasons.strip():
777
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarification decision requires reasons")
778
+ return {
779
+ "needs_clarification": decision,
780
+ "message": message.strip(),
781
+ "reasons": reasons.strip(),
782
+ }
783
+
784
+
785
+ def _validate_clarify_call(value: Mapping[str, Any]) -> dict[str, Any]:
786
+ if set(value) - {"message", "questions", "wait_for_response"}:
787
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify call returned unknown fields")
788
+ message = value.get("message", "")
789
+ if not isinstance(message, str):
790
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify message must be text")
791
+ wait = value.get("wait_for_response", True)
792
+ if not isinstance(wait, bool):
793
+ raise DriverError("BACKEND_OUTPUT_INVALID", "wait_for_response must be boolean")
794
+ raw_questions = value.get("questions")
795
+ if not isinstance(raw_questions, Sequence) or isinstance(raw_questions, (str, bytes)):
796
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify questions must be a sequence")
797
+ if not raw_questions:
798
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify requires at least one question")
799
+ questions: list[Mapping[str, Any]] = []
800
+ for index, raw in enumerate(raw_questions):
801
+ item = _mapping(raw, f"clarify question {index}")
802
+ if set(item) - {"id", "text", "options", "default"}:
803
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify question has unknown fields")
804
+ question_id = _text(item.get("id"), f"clarify question {index} id")
805
+ text = _text(item.get("text"), f"clarify question {index} text")
806
+ options = item.get("options", [])
807
+ if not isinstance(options, Sequence) or isinstance(options, (str, bytes)):
808
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify options must be a sequence")
809
+ if not all(isinstance(option, str) and option.strip() for option in options):
810
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify options must be non-empty text")
811
+ default = item.get("default", "")
812
+ if not isinstance(default, str):
813
+ raise DriverError("BACKEND_OUTPUT_INVALID", "clarify default must be text")
814
+ questions.append(
815
+ {"id": question_id, "text": text, "options": list(options), "default": default}
816
+ )
817
+ return {"message": message.strip(), "questions": tuple(questions), "wait": wait}
818
+
819
+
820
+ def _render_official_clarify_message(preamble: str, questions: Sequence[Mapping[str, Any]]) -> str:
821
+ """Mirror the pinned MIT ``clarify_agent.function_calling`` renderer."""
822
+
823
+ lines = [f"{preamble}\n" if preamble else "I need some clarification before proceeding:\n"]
824
+ if questions:
825
+ lines.append("**Please answer the following:**\n")
826
+ for index, question in enumerate(questions, 1):
827
+ lines.append(f"Q{index}. {question['text']}")
828
+ lines.append("")
829
+ return "\n".join(lines)
830
+
831
+
832
+ def _literal_assignments(path: Path, initial: Mapping[str, Any]) -> dict[str, Any]:
833
+ try:
834
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.name)
835
+ except (OSError, SyntaxError, UnicodeError) as exc:
836
+ raise DriverError("UPSTREAM_SCHEMA_INVALID", "official schema source is invalid") from exc
837
+ environment = dict(initial)
838
+ for node in tree.body:
839
+ if isinstance(node, ast.Assign) and len(node.targets) == 1:
840
+ target = node.targets[0]
841
+ if not isinstance(target, ast.Name):
842
+ continue
843
+ try:
844
+ environment[target.id] = _safe_ast_value(node.value, environment)
845
+ except DriverError:
846
+ if target.id in {"ClarifyDecisionTool", "ClarifyTool"}:
847
+ raise
848
+ return environment
849
+
850
+
851
+ def _safe_ast_value(node: ast.AST, environment: Mapping[str, Any]) -> Any:
852
+ if isinstance(node, ast.Constant):
853
+ return node.value
854
+ if isinstance(node, ast.Name) and node.id in environment:
855
+ return environment[node.id]
856
+ if isinstance(node, ast.Dict):
857
+ return {
858
+ _safe_ast_value(key, environment): _safe_ast_value(value, environment)
859
+ for key, value in zip(node.keys, node.values, strict=True)
860
+ }
861
+ if isinstance(node, (ast.List, ast.Tuple)):
862
+ return [_safe_ast_value(item, environment) for item in node.elts]
863
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
864
+ if node.func.id not in {"ChatCompletionToolParam", "ChatCompletionToolParamFunctionChunk"}:
865
+ raise DriverError("UPSTREAM_SCHEMA_INVALID", "official schema uses an unsafe call")
866
+ if node.args or any(keyword.arg is None for keyword in node.keywords):
867
+ raise DriverError(
868
+ "UPSTREAM_SCHEMA_INVALID", "official schema call shape is unsupported"
869
+ )
870
+ return {
871
+ str(keyword.arg): _safe_ast_value(keyword.value, environment)
872
+ for keyword in node.keywords
873
+ }
874
+ raise DriverError("UPSTREAM_SCHEMA_INVALID", "official schema contains unsupported syntax")
875
+
876
+
877
+ def _validate_tool_schema(value: Mapping[str, Any]) -> None:
878
+ _exact_keys(value, {"type", "function"}, "official tool")
879
+ if value["type"] != "function":
880
+ raise DriverError("UPSTREAM_SCHEMA_INVALID", "official tool type is invalid")
881
+ function = _mapping(value["function"], "official tool function")
882
+ _exact_keys(function, {"name", "description", "parameters"}, "official tool function")
883
+ parameters = _mapping(function["parameters"], "official tool parameters")
884
+ if parameters.get("type") != "object" or parameters.get("additionalProperties") is not False:
885
+ raise DriverError("UPSTREAM_SCHEMA_INVALID", "official tool schema is not closed")
886
+
887
+
888
+ def _tool_name(value: Mapping[str, Any]) -> str:
889
+ function = _mapping(value.get("function"), "tool function")
890
+ return _text(function.get("name"), "tool name")
891
+
892
+
893
+ def read_checkout_head(root: Path) -> str:
894
+ git_entry = root / ".git"
895
+ if git_entry.is_dir():
896
+ git_dir = git_entry.resolve(strict=True)
897
+ elif git_entry.is_file():
898
+ raw = git_entry.read_text(encoding="utf-8").strip()
899
+ if not raw.startswith("gitdir: "):
900
+ raise DriverError("GIT_METADATA_INVALID", "checkout .git file is malformed")
901
+ candidate = Path(raw.removeprefix("gitdir: "))
902
+ git_dir = (
903
+ (root / candidate).resolve(strict=True)
904
+ if not candidate.is_absolute()
905
+ else candidate.resolve(strict=True)
906
+ )
907
+ else:
908
+ raise DriverError("GIT_METADATA_INVALID", "checkout lacks Git metadata")
909
+ head = (git_dir / "HEAD").read_text(encoding="utf-8").strip()
910
+ if _COMMIT_RE.fullmatch(head):
911
+ return head
912
+ if not head.startswith("ref: "):
913
+ raise DriverError("GIT_METADATA_INVALID", "checkout HEAD is malformed")
914
+ ref = head.removeprefix("ref: ")
915
+ if _REF_RE.fullmatch(ref) is None or ".." in PurePosixPath(ref).parts:
916
+ raise DriverError("GIT_METADATA_INVALID", "checkout HEAD reference is unsafe")
917
+ common_dir = git_dir
918
+ common_file = git_dir / "commondir"
919
+ if common_file.is_file():
920
+ candidate = Path(common_file.read_text(encoding="utf-8").strip())
921
+ common_dir = (
922
+ (git_dir / candidate).resolve(strict=True)
923
+ if not candidate.is_absolute()
924
+ else candidate.resolve(strict=True)
925
+ )
926
+ for base in (git_dir, common_dir):
927
+ loose = _safe_child(base, ref)
928
+ if loose.is_file():
929
+ value = loose.read_text(encoding="utf-8").strip()
930
+ if _COMMIT_RE.fullmatch(value):
931
+ return value
932
+ packed = base / "packed-refs"
933
+ if packed.is_file():
934
+ for line in packed.read_text(encoding="utf-8").splitlines():
935
+ if not line or line.startswith(("#", "^")):
936
+ continue
937
+ commit, separator, name = line.partition(" ")
938
+ if separator and name == ref and _COMMIT_RE.fullmatch(commit):
939
+ return commit
940
+ raise DriverError("GIT_METADATA_INVALID", "checkout HEAD commit cannot be resolved")
941
+
942
+
943
+ def _require_clean_checkout(root: Path) -> None:
944
+ environment = {
945
+ "PATH": os.environ.get("PATH", ""),
946
+ "LC_ALL": "C",
947
+ "GIT_CONFIG_NOSYSTEM": "1",
948
+ "GIT_CONFIG_GLOBAL": os.devnull,
949
+ "GIT_OPTIONAL_LOCKS": "0",
950
+ }
951
+ try:
952
+ result = subprocess.run(
953
+ (
954
+ "git",
955
+ "-c",
956
+ "core.fsmonitor=false",
957
+ "-C",
958
+ str(root),
959
+ "status",
960
+ "--porcelain=v1",
961
+ "--untracked-files=all",
962
+ ),
963
+ check=False,
964
+ capture_output=True,
965
+ text=True,
966
+ encoding="utf-8",
967
+ errors="replace",
968
+ env=environment,
969
+ timeout=15,
970
+ )
971
+ except (OSError, subprocess.SubprocessError) as exc:
972
+ raise DriverError("GIT_STATUS_FAILED", "clean-checkout verification could not run") from exc
973
+ if result.returncode != 0:
974
+ raise DriverError("GIT_STATUS_FAILED", "clean-checkout verification failed")
975
+ if result.stdout:
976
+ raise DriverError("DIRTY_CHECKOUT", "checkout contains tracked or untracked changes")
977
+
978
+
979
+ def _reject_enterprise_imports(paths: Sequence[Path]) -> None:
980
+ for path in paths:
981
+ if path.suffix != ".py":
982
+ continue
983
+ try:
984
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.name)
985
+ except (OSError, SyntaxError, UnicodeError) as exc:
986
+ raise DriverError("UPSTREAM_SOURCE_INVALID", "pinned source cannot be parsed") from exc
987
+ for node in ast.walk(tree):
988
+ names: tuple[str, ...] = ()
989
+ if isinstance(node, ast.Import):
990
+ names = tuple(alias.name for alias in node.names)
991
+ elif isinstance(node, ast.ImportFrom) and node.module:
992
+ names = (node.module,)
993
+ if any(name == "enterprise" or name.startswith("enterprise.") for name in names):
994
+ raise DriverError("ENTERPRISE_EXCLUDED", "worker refuses enterprise imports")
995
+
996
+
997
+ def serve(runtime: AskOrAssumeRuntime, input_stream: TextIO, output_stream: TextIO) -> None:
998
+ for raw_line in input_stream:
999
+ if not raw_line.strip():
1000
+ continue
1001
+ request_id = ""
1002
+ try:
1003
+ if len(raw_line.encode("utf-8")) > MAX_REQUEST_BYTES:
1004
+ raise DriverError("REQUEST_TOO_LARGE", "worker request exceeds its byte limit")
1005
+ value = _mapping(_loads_strict_json(raw_line), "JSONL request")
1006
+ if isinstance(value.get("request_id"), str):
1007
+ request_id = value["request_id"][:128]
1008
+ response = runtime.run(value)
1009
+ except DriverError as exc:
1010
+ response = _failure_response(request_id, exc.code, str(exc))
1011
+ except Exception as exc: # pragma: no cover - final process boundary
1012
+ response = _failure_response(
1013
+ request_id,
1014
+ "INTERNAL_WORKER_ERROR",
1015
+ f"Ask-or-Assume worker failed with {type(exc).__name__}",
1016
+ )
1017
+ output_stream.write(_canonical_json(response) + "\n")
1018
+ output_stream.flush()
1019
+
1020
+
1021
+ def _failure_response(request_id: str, code: str, message: str) -> dict[str, Any]:
1022
+ return {
1023
+ "status": "error",
1024
+ "request_id": request_id,
1025
+ "upstream_commit": UPSTREAM_COMMIT,
1026
+ "protocol_version": PROTOCOL_VERSION,
1027
+ "failure_code": code,
1028
+ "message": message,
1029
+ }
1030
+
1031
+
1032
+ def _checkout_file(root: Path, relative: str) -> Path:
1033
+ path = _safe_child(root, relative)
1034
+ if not path.is_file() or path.is_symlink():
1035
+ raise DriverError("UPSTREAM_FILE_UNAVAILABLE", f"pinned file is unavailable: {relative}")
1036
+ return path
1037
+
1038
+
1039
+ def _safe_child(root: Path, relative: str) -> Path:
1040
+ candidate = (root / relative).resolve(strict=False)
1041
+ if candidate != root and root not in candidate.parents:
1042
+ raise DriverError("UNSAFE_PATH", "path escapes its fixed root")
1043
+ return candidate
1044
+
1045
+
1046
+ def _safe_relative_path(value: Any, label: str) -> str:
1047
+ if not isinstance(value, str) or not value or "\\" in value:
1048
+ raise DriverError("INVALID_MANIFEST", f"{label} is invalid")
1049
+ path = PurePosixPath(value)
1050
+ if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
1051
+ raise DriverError("INVALID_MANIFEST", f"{label} is unsafe")
1052
+ return path.as_posix()
1053
+
1054
+
1055
+ def _safe_prefix(value: Any) -> str:
1056
+ result = _safe_relative_path(value, "excluded prefix")
1057
+ return f"{result.rstrip('/')}/"
1058
+
1059
+
1060
+ def _reject_excluded_path(relative: str, excluded: Sequence[str]) -> None:
1061
+ normalized = f"{relative.rstrip('/')}/" if relative.endswith("/") else relative
1062
+ if any(normalized.startswith(prefix) for prefix in excluded):
1063
+ raise DriverError(
1064
+ "ENTERPRISE_EXCLUDED", "enterprise/ is outside the worker license boundary"
1065
+ )
1066
+
1067
+
1068
+ def _file_sha256(path: Path) -> str:
1069
+ digest = hashlib.sha256()
1070
+ with path.open("rb") as handle:
1071
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
1072
+ digest.update(chunk)
1073
+ return digest.hexdigest()
1074
+
1075
+
1076
+ def _validate_base_url(value: str) -> str:
1077
+ parsed = urllib.parse.urlparse(value)
1078
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
1079
+ raise DriverError("INVALID_BACKEND_CONFIG", "backend base_url must be HTTP(S)")
1080
+ if parsed.username or parsed.password or parsed.query or parsed.fragment:
1081
+ raise DriverError(
1082
+ "INVALID_BACKEND_CONFIG", "backend base_url must not contain secrets/query"
1083
+ )
1084
+ return value.rstrip("/")
1085
+
1086
+
1087
+ def _canonical_hash(value: Any) -> str:
1088
+ return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
1089
+
1090
+
1091
+ def _canonical_json(value: Any) -> str:
1092
+ try:
1093
+ return json.dumps(
1094
+ value,
1095
+ ensure_ascii=False,
1096
+ sort_keys=True,
1097
+ separators=(",", ":"),
1098
+ allow_nan=False,
1099
+ )
1100
+ except (TypeError, ValueError) as exc:
1101
+ raise DriverError("NON_CANONICAL_JSON", "value is not canonical JSON") from exc
1102
+
1103
+
1104
+ def _loads_strict_json(raw: str) -> Any:
1105
+ def reject_constant(value: str) -> None:
1106
+ raise ValueError(f"non-finite constant: {value}")
1107
+
1108
+ def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
1109
+ result: dict[str, Any] = {}
1110
+ for key, value in pairs:
1111
+ if key in result:
1112
+ raise ValueError(f"duplicate key: {key}")
1113
+ result[key] = value
1114
+ return result
1115
+
1116
+ try:
1117
+ return json.loads(raw, parse_constant=reject_constant, object_pairs_hook=reject_duplicates)
1118
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1119
+ raise DriverError("INVALID_JSON", "input is not strict JSON") from exc
1120
+
1121
+
1122
+ def _mapping(value: Any, label: str) -> Mapping[str, Any]:
1123
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
1124
+ raise DriverError("INVALID_SCHEMA", f"{label} must be a string-keyed mapping")
1125
+ return value
1126
+
1127
+
1128
+ def _exact_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
1129
+ if set(value) != expected:
1130
+ raise DriverError("INVALID_SCHEMA", f"{label} fields do not match the protocol")
1131
+
1132
+
1133
+ def _text(value: Any, label: str) -> str:
1134
+ if not isinstance(value, str) or not value.strip():
1135
+ raise DriverError("INVALID_SCHEMA", f"{label} must be non-empty text")
1136
+ return value
1137
+
1138
+
1139
+ def _plain_text(value: Any, label: str) -> str:
1140
+ if not isinstance(value, str):
1141
+ raise DriverError("INVALID_SCHEMA", f"{label} must be text")
1142
+ return value
1143
+
1144
+
1145
+ def _hash(value: Any, label: str) -> str:
1146
+ if not isinstance(value, str) or _HASH_RE.fullmatch(value) is None:
1147
+ raise DriverError("INVALID_SCHEMA", f"{label} must be a SHA-256 hash")
1148
+ return value
1149
+
1150
+
1151
+ def _positive_int(value: Any, label: str) -> int:
1152
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
1153
+ raise DriverError("INVALID_SCHEMA", f"{label} must be a positive integer")
1154
+ return value
1155
+
1156
+
1157
+ def _nonnegative_int(value: Any, label: str) -> int:
1158
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
1159
+ raise DriverError("INVALID_SCHEMA", f"{label} must be a non-negative integer")
1160
+ return value
1161
+
1162
+
1163
+ def _positive_float(value: Any, label: str) -> float:
1164
+ result = _nonnegative_float(value, label)
1165
+ if result <= 0:
1166
+ raise DriverError("INVALID_SCHEMA", f"{label} must be positive")
1167
+ return result
1168
+
1169
+
1170
+ def _nonnegative_float(value: Any, label: str) -> float:
1171
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
1172
+ raise DriverError("INVALID_SCHEMA", f"{label} must be numeric")
1173
+ result = float(value)
1174
+ if not math.isfinite(result) or result < 0:
1175
+ raise DriverError("INVALID_SCHEMA", f"{label} must be finite and non-negative")
1176
+ return result
1177
+
1178
+
1179
+ def build_argument_parser() -> argparse.ArgumentParser:
1180
+ parser = argparse.ArgumentParser(description=__doc__)
1181
+ parser.add_argument("--checkout", required=True, type=Path)
1182
+ parser.add_argument("--backend-config-json")
1183
+ parser.add_argument("--verify-only", action="store_true")
1184
+ return parser
1185
+
1186
+
1187
+ def main(argv: Sequence[str] | None = None) -> int:
1188
+ args = build_argument_parser().parse_args(argv)
1189
+ policy = load_compatibility_policy()
1190
+ checkout = verify_checkout(args.checkout, policy)
1191
+ assets = load_official_assets(checkout)
1192
+ if args.verify_only:
1193
+ print(
1194
+ _canonical_json(
1195
+ {
1196
+ "status": "ok",
1197
+ "upstream_commit": UPSTREAM_COMMIT,
1198
+ "release_id": RELEASE_ID,
1199
+ "execution_mode": "controlled_component_harness",
1200
+ "component_only": True,
1201
+ "openhands_full_system": False,
1202
+ "intent_prompt_sha256": assets.intent_prompt_sha256,
1203
+ "intent_tool_schema_hash": assets.intent_tool_schema_hash,
1204
+ "clarify_tool_schema_hash": assets.clarify_tool_schema_hash,
1205
+ }
1206
+ )
1207
+ )
1208
+ return 0
1209
+ if args.backend_config_json is None:
1210
+ raise DriverError("INVALID_BACKEND_CONFIG", "serve mode requires backend configuration")
1211
+ config = BackendConfig.from_json(args.backend_config_json)
1212
+ runtime = AskOrAssumeRuntime(
1213
+ checkout,
1214
+ config,
1215
+ OpenAICompatibleBackend(config),
1216
+ policy=policy,
1217
+ )
1218
+ serve(runtime, sys.stdin, sys.stdout)
1219
+ return 0
1220
+
1221
+
1222
+ if __name__ == "__main__": # pragma: no cover - exercised through subprocess deployment
1223
+ try:
1224
+ raise SystemExit(main())
1225
+ except DriverError as exc:
1226
+ print(_canonical_json(_failure_response("", exc.code, str(exc))))
1227
+ raise SystemExit(2) from None