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,1071 @@
1
+ """Auditable local and container supervisors for external workers.
2
+
3
+ The module has no Docker or accelerator SDK dependency. Docker is invoked as
4
+ an argv-only client through an injectable bounded runner, and accelerator
5
+ accounting is an optional protocol implemented by the experiment environment.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ import os
13
+ import re
14
+ import time
15
+ import uuid
16
+ from collections.abc import Mapping, Sequence
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path, PurePosixPath
19
+ from typing import Any, Protocol, runtime_checkable
20
+
21
+ from agentinputlab.contracts.errors import ExternalDependencyError, ValidationError
22
+ from agentinputlab.contracts.serialization import content_hash, freeze_mapping
23
+ from agentinputlab.observability.models import (
24
+ AcceleratorSample,
25
+ ResourceExitReason,
26
+ ResourceLimits,
27
+ ResourceReceipt,
28
+ )
29
+
30
+ from .process import (
31
+ BoundedProcessResult,
32
+ BoundedProcessStartupError,
33
+ BoundedProcessTimeoutError,
34
+ ProcessResourceObservation,
35
+ run_bounded_process,
36
+ )
37
+
38
+ _DIGEST_PINNED_IMAGE = re.compile(r"^.+@sha256:([0-9a-f]{64})$")
39
+ _SAFE_CONTAINER_PREFIX = re.compile(r"[^a-z0-9_.-]+")
40
+ WORKER_SUPERVISOR_KIND_ENV = "AILAB_WORKER_SUPERVISOR"
41
+ WORKER_SUPERVISOR_IMAGE_ENV = "AILAB_WORKER_IMAGE"
42
+ WORKER_SUPERVISOR_ASSET_ROOT_ENV = "AILAB_WORKER_ASSET_ROOT"
43
+ _LOCAL_LOCKED_LAUNCHER_PROTOCOL = "agentinputlab.local-locked-python-launcher.v1"
44
+ _DOCKER_LOCKED_LAUNCHER_PROTOCOL = "agentinputlab.docker-locked-python-launcher.v1"
45
+ _LOCKED_PYTHON_LAUNCHER = """\
46
+ import runpy
47
+ import sys
48
+
49
+ upstream, upstream_src, adapter, entrypoint, *arguments = sys.argv[1:]
50
+ trusted_paths = [upstream]
51
+ if upstream_src:
52
+ trusted_paths.append(upstream_src)
53
+ if adapter != upstream:
54
+ trusted_paths.append(adapter)
55
+ sys.path[:0] = trusted_paths
56
+ sys.argv = [entrypoint, *arguments]
57
+ runpy.run_path(entrypoint, run_name="__main__")
58
+ """
59
+
60
+
61
+ @runtime_checkable
62
+ class AcceleratorSampler(Protocol):
63
+ """Optional accelerator evidence for exactly one supervised execution.
64
+
65
+ :meth:`stop` may return device-seconds and sampled peak device memory.
66
+ Implementations must state in ``AcceleratorSample.sampler_identity``
67
+ whether seconds are utilization-integrated or allocation-time accounting.
68
+ Unavailable measurements are ``None``, never a fabricated zero.
69
+ """
70
+
71
+ def start(self) -> object:
72
+ """Start a sample window and return an opaque, per-call token."""
73
+
74
+ def stop(self, token: object) -> AcceleratorSample:
75
+ """Stop the matching window and return immutable accounting."""
76
+
77
+
78
+ @runtime_checkable
79
+ class ProcessRunner(Protocol):
80
+ """Injectable argv-only process runner used by both supervisors."""
81
+
82
+ def run(
83
+ self,
84
+ argv: Sequence[str],
85
+ *,
86
+ input_text: str,
87
+ timeout_seconds: float,
88
+ max_input_bytes: int,
89
+ max_stdout_bytes: int,
90
+ max_stderr_bytes: int,
91
+ cwd: str | None,
92
+ environment: Mapping[str, str] | None,
93
+ process_name: str,
94
+ ) -> BoundedProcessResult: ...
95
+
96
+
97
+ @dataclass(frozen=True, slots=True)
98
+ class DefaultProcessRunner:
99
+ """Adapter around the library's hard wall-time/pipe-bounded transport."""
100
+
101
+ def run(
102
+ self,
103
+ argv: Sequence[str],
104
+ *,
105
+ input_text: str,
106
+ timeout_seconds: float,
107
+ max_input_bytes: int,
108
+ max_stdout_bytes: int,
109
+ max_stderr_bytes: int,
110
+ cwd: str | None,
111
+ environment: Mapping[str, str] | None,
112
+ process_name: str,
113
+ ) -> BoundedProcessResult:
114
+ return run_bounded_process(
115
+ argv,
116
+ input_text=input_text,
117
+ timeout_seconds=timeout_seconds,
118
+ max_input_bytes=max_input_bytes,
119
+ max_stdout_bytes=max_stdout_bytes,
120
+ max_stderr_bytes=max_stderr_bytes,
121
+ cwd=cwd,
122
+ environment=environment,
123
+ process_name=process_name,
124
+ )
125
+
126
+
127
+ @dataclass(frozen=True, slots=True)
128
+ class SupervisedProcessResult:
129
+ returncode: int
130
+ stdout: str
131
+ stderr: str
132
+ resource_receipt: ResourceReceipt
133
+
134
+
135
+ class SupervisedWorkerError(ExternalDependencyError):
136
+ """A supervisor failure retaining the audit receipt produced before exit."""
137
+
138
+ def __init__(self, message: str, *, resource_receipt: ResourceReceipt) -> None:
139
+ super().__init__(message)
140
+ self.resource_receipt = resource_receipt
141
+
142
+
143
+ @runtime_checkable
144
+ class WorkerSupervisor(Protocol):
145
+ """Execute one bounded worker request and return non-additive evidence."""
146
+
147
+ def fingerprint(self) -> Mapping[str, Any]:
148
+ """Return stable supervisor identity, excluding credentials."""
149
+
150
+ def execute(
151
+ self,
152
+ argv: Sequence[str],
153
+ *,
154
+ input_text: str,
155
+ limits: ResourceLimits,
156
+ max_input_bytes: int,
157
+ max_stdout_bytes: int,
158
+ max_stderr_bytes: int,
159
+ cwd: str | None,
160
+ environment: Mapping[str, str] | None,
161
+ process_name: str,
162
+ image_identity: Mapping[str, Any] | None = None,
163
+ accelerator_sampler: AcceleratorSampler | None = None,
164
+ ) -> SupervisedProcessResult: ...
165
+
166
+
167
+ @dataclass(frozen=True, slots=True)
168
+ class LocalSubprocessSupervisor:
169
+ """Host-process supervisor with a real hard wall deadline.
170
+
171
+ The portable transport cannot guarantee a process-tree CPU quota, memory
172
+ cgroup, or PID controller. Requests for any of those limits are rejected
173
+ before process creation rather than being mislabeled as enforced.
174
+ """
175
+
176
+ runner: ProcessRunner = field(default_factory=DefaultProcessRunner, repr=False)
177
+ host_identity: Mapping[str, Any] = field(
178
+ default_factory=lambda: {"kind": "host_process", "pinned": False}
179
+ )
180
+
181
+ def __post_init__(self) -> None:
182
+ object.__setattr__(self, "host_identity", freeze_mapping(self.host_identity))
183
+
184
+ def fingerprint(self) -> Mapping[str, Any]:
185
+ return {
186
+ "kind": "local_subprocess",
187
+ "protocol": "agentinputlab.worker-supervisor.v1",
188
+ "hard_limits": ("wall_timeout_seconds", "pipe_bytes"),
189
+ "optional_measurements": (
190
+ "psutil_process_tree_cpu_rss",
191
+ "pynvml_process_tree_allocation",
192
+ ),
193
+ "locked_python_launcher": {
194
+ "protocol": _LOCAL_LOCKED_LAUNCHER_PROTOCOL,
195
+ "source_sha256": content_hash(_LOCKED_PYTHON_LAUNCHER),
196
+ "isolated_mode": True,
197
+ "bytecode_writes": False,
198
+ "import_order": "upstream_then_upstream_src_then_adapter",
199
+ },
200
+ }
201
+
202
+ def execute_locked_worker(
203
+ self,
204
+ argv: Sequence[str],
205
+ *,
206
+ checkout: str | Path,
207
+ adapter_checkout: str | Path,
208
+ entrypoint_relative_path: str,
209
+ input_text: str,
210
+ limits: ResourceLimits,
211
+ max_input_bytes: int,
212
+ max_stdout_bytes: int,
213
+ max_stderr_bytes: int,
214
+ environment: Mapping[str, str] | None,
215
+ process_name: str,
216
+ image_identity: Mapping[str, Any] | None = None,
217
+ accelerator_sampler: AcceleratorSampler | None = None,
218
+ ) -> SupervisedProcessResult:
219
+ """Execute an attested Python entrypoint without implicit script paths."""
220
+
221
+ checkout_source = _host_bind_directory(checkout, label="worker checkout")
222
+ adapter_source = _host_bind_directory(
223
+ adapter_checkout,
224
+ label="worker adapter checkout",
225
+ )
226
+ if checkout_source != adapter_source and (
227
+ checkout_source.is_relative_to(adapter_source)
228
+ or adapter_source.is_relative_to(checkout_source)
229
+ ):
230
+ raise ValidationError("worker checkout and adapter checkout must not partially overlap")
231
+ relative = PurePosixPath(entrypoint_relative_path)
232
+ if (
233
+ relative.is_absolute()
234
+ or not relative.parts
235
+ or any(part in {"", ".", ".."} for part in relative.parts)
236
+ ):
237
+ raise ValidationError("worker entrypoint_relative_path must stay inside the adapter")
238
+ expected_entrypoint = adapter_source.joinpath(*relative.parts)
239
+ if len(argv) < 2 or Path(argv[1]).resolve() != expected_entrypoint:
240
+ raise ValidationError("worker argv does not match the attested adapter entrypoint")
241
+ upstream_src = checkout_source / "src"
242
+ runtime_argv = (
243
+ argv[0],
244
+ "-I",
245
+ "-B",
246
+ "-c",
247
+ _LOCKED_PYTHON_LAUNCHER,
248
+ str(checkout_source),
249
+ str(upstream_src) if upstream_src.is_dir() else "",
250
+ str(adapter_source),
251
+ str(expected_entrypoint),
252
+ *tuple(argv[2:]),
253
+ )
254
+ return self.execute(
255
+ runtime_argv,
256
+ input_text=input_text,
257
+ limits=limits,
258
+ max_input_bytes=max_input_bytes,
259
+ max_stdout_bytes=max_stdout_bytes,
260
+ max_stderr_bytes=max_stderr_bytes,
261
+ cwd=str(checkout_source),
262
+ environment=environment,
263
+ process_name=process_name,
264
+ image_identity=image_identity,
265
+ accelerator_sampler=accelerator_sampler,
266
+ )
267
+
268
+ def execute(
269
+ self,
270
+ argv: Sequence[str],
271
+ *,
272
+ input_text: str,
273
+ limits: ResourceLimits,
274
+ max_input_bytes: int,
275
+ max_stdout_bytes: int,
276
+ max_stderr_bytes: int,
277
+ cwd: str | None,
278
+ environment: Mapping[str, str] | None,
279
+ process_name: str,
280
+ image_identity: Mapping[str, Any] | None = None,
281
+ accelerator_sampler: AcceleratorSampler | None = None,
282
+ ) -> SupervisedProcessResult:
283
+ if not isinstance(limits, ResourceLimits):
284
+ raise TypeError("limits must be ResourceLimits")
285
+ unsupported = [
286
+ name
287
+ for name, value in (
288
+ ("memory_bytes", limits.memory_bytes),
289
+ ("cpus", limits.cpus),
290
+ ("pids_limit", limits.pids_limit),
291
+ )
292
+ if value is not None
293
+ ]
294
+ if unsupported:
295
+ raise ValidationError(
296
+ "local subprocess supervisor cannot guarantee hard "
297
+ + ", ".join(unsupported)
298
+ + " limits; use a container supervisor"
299
+ )
300
+ started = time.perf_counter()
301
+ sample_token = _start_accelerator_sample(accelerator_sampler)
302
+ try:
303
+ result = self.runner.run(
304
+ argv,
305
+ input_text=input_text,
306
+ timeout_seconds=limits.wall_timeout_seconds,
307
+ max_input_bytes=max_input_bytes,
308
+ max_stdout_bytes=max_stdout_bytes,
309
+ max_stderr_bytes=max_stderr_bytes,
310
+ cwd=cwd,
311
+ environment=environment,
312
+ process_name=process_name,
313
+ )
314
+ except BoundedProcessTimeoutError as exc:
315
+ observation = exc.resource_observation
316
+ sample = _stop_accelerator_after_process_error(
317
+ accelerator_sampler,
318
+ sample_token,
319
+ observation=observation,
320
+ )
321
+ receipt = self._receipt(
322
+ limits=limits,
323
+ wall_seconds=time.perf_counter() - started,
324
+ sample=sample,
325
+ observation=observation,
326
+ image_identity=image_identity,
327
+ timed_out=True,
328
+ exit_reason=ResourceExitReason.TIMED_OUT,
329
+ )
330
+ raise SupervisedWorkerError(
331
+ str(exc),
332
+ resource_receipt=receipt,
333
+ ) from exc
334
+ except BoundedProcessStartupError as exc:
335
+ observation = exc.resource_observation
336
+ sample = _stop_accelerator_after_process_error(
337
+ accelerator_sampler,
338
+ sample_token,
339
+ observation=observation,
340
+ )
341
+ receipt = self._receipt(
342
+ limits=limits,
343
+ wall_seconds=time.perf_counter() - started,
344
+ sample=sample,
345
+ observation=observation,
346
+ image_identity=image_identity,
347
+ exit_reason=ResourceExitReason.STARTUP_FAILED,
348
+ )
349
+ raise SupervisedWorkerError(
350
+ str(exc),
351
+ resource_receipt=receipt,
352
+ ) from exc
353
+ except ExternalDependencyError as exc:
354
+ observation = _error_observation(exc)
355
+ sample = _stop_accelerator_after_process_error(
356
+ accelerator_sampler,
357
+ sample_token,
358
+ observation=observation,
359
+ )
360
+ receipt = self._receipt(
361
+ limits=limits,
362
+ wall_seconds=time.perf_counter() - started,
363
+ sample=sample,
364
+ observation=observation,
365
+ image_identity=image_identity,
366
+ exit_reason=ResourceExitReason.SUPERVISOR_ERROR,
367
+ )
368
+ raise SupervisedWorkerError(
369
+ str(exc),
370
+ resource_receipt=receipt,
371
+ ) from exc
372
+ try:
373
+ sample = _stop_accelerator_sample(
374
+ accelerator_sampler,
375
+ sample_token,
376
+ observation=result.resource_observation,
377
+ )
378
+ except Exception as exc:
379
+ receipt = self._receipt(
380
+ limits=limits,
381
+ wall_seconds=time.perf_counter() - started,
382
+ sample=AcceleratorSample(),
383
+ observation=result.resource_observation,
384
+ image_identity=image_identity,
385
+ exit_code=result.returncode,
386
+ exit_reason=ResourceExitReason.SUPERVISOR_ERROR,
387
+ )
388
+ raise SupervisedWorkerError(
389
+ "accelerator sampler could not finalize",
390
+ resource_receipt=receipt,
391
+ ) from exc
392
+ reason = (
393
+ ResourceExitReason.COMPLETED
394
+ if result.returncode == 0
395
+ else ResourceExitReason.NONZERO_EXIT
396
+ )
397
+ receipt = self._receipt(
398
+ limits=limits,
399
+ wall_seconds=time.perf_counter() - started,
400
+ sample=sample,
401
+ observation=result.resource_observation,
402
+ image_identity=image_identity,
403
+ exit_code=result.returncode,
404
+ exit_reason=reason,
405
+ )
406
+ return SupervisedProcessResult(
407
+ returncode=result.returncode,
408
+ stdout=result.stdout,
409
+ stderr=result.stderr,
410
+ resource_receipt=receipt,
411
+ )
412
+
413
+ def _receipt(
414
+ self,
415
+ *,
416
+ limits: ResourceLimits,
417
+ wall_seconds: float,
418
+ sample: AcceleratorSample,
419
+ observation: ProcessResourceObservation,
420
+ image_identity: Mapping[str, Any] | None,
421
+ exit_reason: ResourceExitReason,
422
+ exit_code: int | None = None,
423
+ timed_out: bool = False,
424
+ ) -> ResourceReceipt:
425
+ return ResourceReceipt(
426
+ limit_hash=limits.limit_hash,
427
+ wall_seconds=wall_seconds,
428
+ cpu_seconds=observation.cpu_seconds,
429
+ peak_rss_bytes=observation.peak_rss_bytes,
430
+ accelerator_seconds=sample.accelerator_seconds,
431
+ accelerator_peak_memory_bytes=(sample.accelerator_peak_memory_bytes),
432
+ timed_out=timed_out,
433
+ exit_reason=exit_reason,
434
+ exit_code=exit_code,
435
+ supervisor_identity={
436
+ **dict(self.fingerprint()),
437
+ "accelerator_sampler": dict(sample.sampler_identity),
438
+ "measurement": dict(observation.measurement_identity),
439
+ },
440
+ image_identity=dict(image_identity or self.host_identity),
441
+ )
442
+
443
+
444
+ @dataclass(frozen=True, slots=True)
445
+ class DockerWorkerSupervisor:
446
+ """Digest-pinned, network-isolated Docker worker supervisor."""
447
+
448
+ image: str
449
+ runner: ProcessRunner = field(default_factory=DefaultProcessRunner, repr=False)
450
+ docker_executable: str = "docker"
451
+ name_prefix: str = "agentinputlab-worker"
452
+ management_timeout_seconds: float = 15.0
453
+ asset_root: str | None = None
454
+ container_python: str = "/usr/local/bin/python"
455
+ container_user: str = "10001:10001"
456
+ tmpfs_size_bytes: int = 512 * 1024 * 1024
457
+
458
+ def __post_init__(self) -> None:
459
+ if not isinstance(self.image, str) or not _DIGEST_PINNED_IMAGE.fullmatch(self.image):
460
+ raise ValidationError("Docker worker image must be pinned as name@sha256:<64 hex>")
461
+ if not isinstance(self.docker_executable, str) or not self.docker_executable:
462
+ raise ValidationError("docker_executable must be a non-empty string")
463
+ if (
464
+ not isinstance(self.container_python, str)
465
+ or not self.container_python.startswith("/")
466
+ or any(character in self.container_python for character in ("\x00", "\n", "\r"))
467
+ ):
468
+ raise ValidationError("container_python must be an absolute argument-safe path")
469
+ if self.container_user != "10001:10001":
470
+ raise ValidationError("Docker worker container_user must be 10001:10001")
471
+ if (
472
+ isinstance(self.tmpfs_size_bytes, bool)
473
+ or not isinstance(self.tmpfs_size_bytes, int)
474
+ or self.tmpfs_size_bytes < 1024 * 1024
475
+ ):
476
+ raise ValidationError("Docker worker tmpfs_size_bytes must be at least 1 MiB")
477
+ if self.asset_root is not None:
478
+ asset_root = _host_bind_directory(self.asset_root, label="Docker worker asset_root")
479
+ object.__setattr__(self, "asset_root", str(asset_root))
480
+ if (
481
+ isinstance(self.management_timeout_seconds, bool)
482
+ or not isinstance(self.management_timeout_seconds, (int, float))
483
+ or not math.isfinite(self.management_timeout_seconds)
484
+ or self.management_timeout_seconds <= 0
485
+ ):
486
+ raise ValidationError("management_timeout_seconds must be finite and positive")
487
+ prefix = _SAFE_CONTAINER_PREFIX.sub("-", self.name_prefix.casefold()).strip("-.")
488
+ if not prefix:
489
+ raise ValidationError("name_prefix must contain a Docker-safe character")
490
+ object.__setattr__(self, "name_prefix", prefix[:40])
491
+
492
+ def fingerprint(self) -> Mapping[str, Any]:
493
+ digest = self.image.rsplit("@sha256:", 1)[1]
494
+ return {
495
+ "kind": "docker",
496
+ "protocol": "agentinputlab.worker-supervisor.v1",
497
+ "docker_executable": self.docker_executable,
498
+ "image_digest": f"sha256:{digest}",
499
+ "network": "none",
500
+ "read_only": True,
501
+ "cap_drop": "ALL",
502
+ "no_new_privileges": True,
503
+ "container_python": self.container_python,
504
+ "container_user": self.container_user,
505
+ "checkout_target": "/opt/upstream",
506
+ "adapter_checkout_target": "/opt/candidate",
507
+ "asset_target": "/opt/assets" if self.asset_root is not None else None,
508
+ "tmpfs_size_bytes": self.tmpfs_size_bytes,
509
+ "container_measurements": "runner_declared_supervised_workload_only",
510
+ "locked_python_launcher": {
511
+ "protocol": _DOCKER_LOCKED_LAUNCHER_PROTOCOL,
512
+ "source_sha256": content_hash(_LOCKED_PYTHON_LAUNCHER),
513
+ "isolated_mode": True,
514
+ "bytecode_writes": False,
515
+ "import_order": "upstream_then_upstream_src_then_adapter",
516
+ },
517
+ }
518
+
519
+ def execute_locked_worker(
520
+ self,
521
+ argv: Sequence[str],
522
+ *,
523
+ checkout: str | Path,
524
+ adapter_checkout: str | Path,
525
+ entrypoint_relative_path: str,
526
+ input_text: str,
527
+ limits: ResourceLimits,
528
+ max_input_bytes: int,
529
+ max_stdout_bytes: int,
530
+ max_stderr_bytes: int,
531
+ environment: Mapping[str, str] | None,
532
+ process_name: str,
533
+ image_identity: Mapping[str, Any] | None = None,
534
+ accelerator_sampler: AcceleratorSampler | None = None,
535
+ ) -> SupervisedProcessResult:
536
+ """Run one host-attested worker through fixed container paths.
537
+
538
+ Host executable/checkouts remain part of the caller's attestation. The
539
+ image digest binds the container Python runtime; only the exact
540
+ entrypoint-relative path and configured worker arguments cross the
541
+ boundary.
542
+ """
543
+
544
+ checkout_source = _host_bind_directory(checkout, label="worker checkout")
545
+ adapter_source = _host_bind_directory(
546
+ adapter_checkout,
547
+ label="worker adapter checkout",
548
+ )
549
+ if checkout_source != adapter_source and (
550
+ checkout_source.is_relative_to(adapter_source)
551
+ or adapter_source.is_relative_to(checkout_source)
552
+ ):
553
+ raise ValidationError("worker checkout and adapter checkout must not partially overlap")
554
+ asset_source = Path(self.asset_root) if self.asset_root is not None else None
555
+ if asset_source is not None:
556
+ for source in (checkout_source, adapter_source):
557
+ if (
558
+ asset_source == source
559
+ or asset_source.is_relative_to(source)
560
+ or source.is_relative_to(asset_source)
561
+ ):
562
+ raise ValidationError("worker asset root must not overlap either checkout")
563
+ relative = PurePosixPath(entrypoint_relative_path)
564
+ if (
565
+ relative.is_absolute()
566
+ or not relative.parts
567
+ or any(part in {"", ".", ".."} for part in relative.parts)
568
+ ):
569
+ raise ValidationError("worker entrypoint_relative_path must stay inside the adapter")
570
+ expected_host_entrypoint = adapter_source.joinpath(*relative.parts)
571
+ if len(argv) < 2 or Path(argv[1]).resolve() != expected_host_entrypoint:
572
+ raise ValidationError("worker argv does not match the attested adapter entrypoint")
573
+ mapped_environment = _container_worker_environment(
574
+ environment or {},
575
+ checkout=checkout_source,
576
+ adapter_checkout=adapter_source,
577
+ asset_root=asset_source,
578
+ )
579
+ mounts: list[tuple[str, str]] = [
580
+ (str(checkout_source), "/opt/upstream"),
581
+ (str(adapter_source), "/opt/candidate"),
582
+ ]
583
+ if self.asset_root is not None:
584
+ mounts.append((self.asset_root, "/opt/assets"))
585
+ container_adapter = (
586
+ PurePosixPath("/opt/upstream")
587
+ if checkout_source == adapter_source
588
+ else PurePosixPath("/opt/candidate")
589
+ )
590
+ container_argv = (
591
+ "-I",
592
+ "-B",
593
+ "-c",
594
+ _LOCKED_PYTHON_LAUNCHER,
595
+ "/opt/upstream",
596
+ "/opt/upstream/src" if (checkout_source / "src").is_dir() else "",
597
+ str(container_adapter),
598
+ str(container_adapter.joinpath(relative)),
599
+ *tuple(argv[2:]),
600
+ )
601
+ return self.execute(
602
+ container_argv,
603
+ input_text=input_text,
604
+ limits=limits,
605
+ max_input_bytes=max_input_bytes,
606
+ max_stdout_bytes=max_stdout_bytes,
607
+ max_stderr_bytes=max_stderr_bytes,
608
+ cwd="/opt/upstream",
609
+ environment=mapped_environment,
610
+ process_name=process_name,
611
+ image_identity=image_identity,
612
+ accelerator_sampler=accelerator_sampler,
613
+ container_mounts=tuple(mounts),
614
+ container_entrypoint=self.container_python,
615
+ )
616
+
617
+ def execute(
618
+ self,
619
+ argv: Sequence[str],
620
+ *,
621
+ input_text: str,
622
+ limits: ResourceLimits,
623
+ max_input_bytes: int,
624
+ max_stdout_bytes: int,
625
+ max_stderr_bytes: int,
626
+ cwd: str | None,
627
+ environment: Mapping[str, str] | None,
628
+ process_name: str,
629
+ image_identity: Mapping[str, Any] | None = None,
630
+ accelerator_sampler: AcceleratorSampler | None = None,
631
+ container_mounts: Sequence[tuple[str, str]] = (),
632
+ container_entrypoint: str | None = None,
633
+ ) -> SupervisedProcessResult:
634
+ if not isinstance(limits, ResourceLimits):
635
+ raise TypeError("limits must be ResourceLimits")
636
+ missing = [
637
+ name
638
+ for name, value in (
639
+ ("memory_bytes", limits.memory_bytes),
640
+ ("cpus", limits.cpus),
641
+ ("pids_limit", limits.pids_limit),
642
+ )
643
+ if value is None
644
+ ]
645
+ if missing:
646
+ raise ValidationError(
647
+ "Docker supervisor requires explicit hard " + ", ".join(missing) + " limits"
648
+ )
649
+ name = f"{self.name_prefix}-{uuid.uuid4().hex}"
650
+ command = self._run_command(
651
+ name=name,
652
+ argv=argv,
653
+ limits=limits,
654
+ cwd=cwd,
655
+ environment=environment,
656
+ mounts=container_mounts,
657
+ entrypoint=container_entrypoint,
658
+ )
659
+ started = time.perf_counter()
660
+ sample_token = _start_accelerator_sample(accelerator_sampler)
661
+ result: BoundedProcessResult | None = None
662
+ run_error: ExternalDependencyError | None = None
663
+ run_error_reason: ResourceExitReason | None = None
664
+ run_observation = ProcessResourceObservation()
665
+ timed_out = False
666
+ state: Mapping[str, Any] = {}
667
+ cleanup_error: ExternalDependencyError | None = None
668
+ try:
669
+ try:
670
+ result = self.runner.run(
671
+ command,
672
+ input_text=input_text,
673
+ timeout_seconds=limits.wall_timeout_seconds,
674
+ max_input_bytes=max_input_bytes,
675
+ max_stdout_bytes=max_stdout_bytes,
676
+ max_stderr_bytes=max_stderr_bytes,
677
+ cwd=None,
678
+ environment=environment,
679
+ process_name=process_name,
680
+ )
681
+ except BoundedProcessTimeoutError as exc:
682
+ timed_out = True
683
+ run_error = exc
684
+ run_error_reason = ResourceExitReason.TIMED_OUT
685
+ run_observation = exc.resource_observation
686
+ except BoundedProcessStartupError as exc:
687
+ run_error = exc
688
+ run_error_reason = ResourceExitReason.STARTUP_FAILED
689
+ run_observation = exc.resource_observation
690
+ except ExternalDependencyError as exc:
691
+ run_error = exc
692
+ run_error_reason = ResourceExitReason.SUPERVISOR_ERROR
693
+ run_observation = _error_observation(exc)
694
+ if result is not None:
695
+ run_observation = result.resource_observation
696
+ run_observation = _docker_workload_observation(run_observation)
697
+ state = self._inspect_state(name)
698
+ finally:
699
+ try:
700
+ self._remove_container(name)
701
+ except ExternalDependencyError as exc:
702
+ cleanup_error = exc
703
+
704
+ try:
705
+ sample = _stop_accelerator_sample(
706
+ accelerator_sampler,
707
+ sample_token,
708
+ observation=run_observation,
709
+ )
710
+ except Exception as exc:
711
+ sample = AcceleratorSample()
712
+ if run_error is None:
713
+ run_error = ExternalDependencyError("accelerator sampler could not finalize")
714
+ run_error.__cause__ = exc
715
+ run_error_reason = ResourceExitReason.SUPERVISOR_ERROR
716
+
717
+ oom_killed = bool(state.get("OOMKilled", False)) and not timed_out
718
+ exit_code = _docker_exit_code(state, result)
719
+ if timed_out:
720
+ reason = ResourceExitReason.TIMED_OUT
721
+ elif oom_killed:
722
+ reason = ResourceExitReason.OOM_KILLED
723
+ elif run_error is not None:
724
+ reason = run_error_reason or ResourceExitReason.SUPERVISOR_ERROR
725
+ elif cleanup_error is not None:
726
+ reason = ResourceExitReason.SUPERVISOR_ERROR
727
+ elif exit_code == 0:
728
+ reason = ResourceExitReason.COMPLETED
729
+ else:
730
+ reason = ResourceExitReason.NONZERO_EXIT
731
+ receipt = ResourceReceipt(
732
+ limit_hash=limits.limit_hash,
733
+ wall_seconds=time.perf_counter() - started,
734
+ cpu_seconds=run_observation.cpu_seconds,
735
+ peak_rss_bytes=run_observation.peak_rss_bytes,
736
+ accelerator_seconds=sample.accelerator_seconds,
737
+ accelerator_peak_memory_bytes=(sample.accelerator_peak_memory_bytes),
738
+ oom_killed=oom_killed,
739
+ timed_out=timed_out,
740
+ exit_reason=reason,
741
+ exit_code=exit_code,
742
+ supervisor_identity={
743
+ **dict(self.fingerprint()),
744
+ "accelerator_sampler": dict(sample.sampler_identity),
745
+ "measurement": dict(run_observation.measurement_identity),
746
+ },
747
+ image_identity={
748
+ "reference": self.image,
749
+ "digest": self.fingerprint()["image_digest"],
750
+ "worker": dict(image_identity or {}),
751
+ },
752
+ )
753
+ if run_error is not None:
754
+ raise SupervisedWorkerError(
755
+ str(run_error),
756
+ resource_receipt=receipt,
757
+ ) from run_error
758
+ if cleanup_error is not None:
759
+ raise SupervisedWorkerError(
760
+ "Docker supervisor could not remove its worker container",
761
+ resource_receipt=receipt,
762
+ ) from cleanup_error
763
+ if result is None: # pragma: no cover - guarded by error branches
764
+ raise AssertionError("Docker runner produced neither a result nor an error")
765
+ return SupervisedProcessResult(
766
+ returncode=result.returncode,
767
+ stdout=result.stdout,
768
+ stderr=result.stderr,
769
+ resource_receipt=receipt,
770
+ )
771
+
772
+ def _run_command(
773
+ self,
774
+ *,
775
+ name: str,
776
+ argv: Sequence[str],
777
+ limits: ResourceLimits,
778
+ cwd: str | None,
779
+ environment: Mapping[str, str] | None,
780
+ mounts: Sequence[tuple[str, str]],
781
+ entrypoint: str | None,
782
+ ) -> tuple[str, ...]:
783
+ command = [
784
+ self.docker_executable,
785
+ "run",
786
+ "--name",
787
+ name,
788
+ "--init",
789
+ "--pull",
790
+ "never",
791
+ "--network",
792
+ "none",
793
+ "--read-only",
794
+ "--cap-drop",
795
+ "ALL",
796
+ "--security-opt",
797
+ "no-new-privileges",
798
+ "--pids-limit",
799
+ str(limits.pids_limit),
800
+ "--memory",
801
+ f"{limits.memory_bytes}b",
802
+ "--memory-swap",
803
+ f"{limits.memory_bytes}b",
804
+ "--cpus",
805
+ _format_number(limits.cpus),
806
+ "--user",
807
+ self.container_user,
808
+ "--tmpfs",
809
+ (f"/tmp:rw,noexec,nosuid,nodev,size={self.tmpfs_size_bytes}"),
810
+ ]
811
+ for source, target in mounts:
812
+ source_path = _host_bind_directory(source, label="Docker worker mount")
813
+ if not target.startswith("/") or any(character in target for character in ",\x00\n\r"):
814
+ raise ValidationError("Docker worker mount target is not argument-safe")
815
+ command.extend(
816
+ (
817
+ "--mount",
818
+ f"type=bind,source={source_path},target={target},readonly",
819
+ )
820
+ )
821
+ if cwd is not None:
822
+ command.extend(("--workdir", cwd))
823
+ if entrypoint is not None:
824
+ command.extend(("--entrypoint", entrypoint))
825
+ for key in sorted((environment or {}).keys()):
826
+ command.extend(("--env", key))
827
+ command.extend(("-i", self.image, *tuple(argv)))
828
+ return tuple(command)
829
+
830
+ def _inspect_state(self, name: str) -> Mapping[str, Any]:
831
+ try:
832
+ result = self.runner.run(
833
+ (
834
+ self.docker_executable,
835
+ "inspect",
836
+ "--format",
837
+ "{{json .State}}",
838
+ name,
839
+ ),
840
+ input_text="",
841
+ timeout_seconds=self.management_timeout_seconds,
842
+ max_input_bytes=1,
843
+ max_stdout_bytes=64 * 1024,
844
+ max_stderr_bytes=64 * 1024,
845
+ cwd=None,
846
+ environment=None,
847
+ process_name="Docker worker state inspection",
848
+ )
849
+ except ExternalDependencyError:
850
+ return {}
851
+ if result.returncode != 0:
852
+ return {}
853
+ try:
854
+ state = json.loads(result.stdout.strip())
855
+ except (json.JSONDecodeError, TypeError):
856
+ return {}
857
+ return state if isinstance(state, dict) else {}
858
+
859
+ def _remove_container(self, name: str) -> None:
860
+ result = self.runner.run(
861
+ (self.docker_executable, "rm", "--force", name),
862
+ input_text="",
863
+ timeout_seconds=self.management_timeout_seconds,
864
+ max_input_bytes=1,
865
+ max_stdout_bytes=64 * 1024,
866
+ max_stderr_bytes=64 * 1024,
867
+ cwd=None,
868
+ environment=None,
869
+ process_name="Docker worker cleanup",
870
+ )
871
+ if result.returncode != 0:
872
+ raise ExternalDependencyError("Docker worker cleanup exited non-zero")
873
+
874
+
875
+ def worker_supervisor_from_environment(
876
+ environment: Mapping[str, str] | None = None,
877
+ ) -> WorkerSupervisor:
878
+ """Resolve an explicit local-or-Docker supervisor without guessing.
879
+
880
+ Local execution remains the compatibility default. Docker execution is
881
+ enabled only when the operator sets ``AILAB_WORKER_SUPERVISOR=docker`` and
882
+ supplies an immutable image identity.
883
+ """
884
+
885
+ values = os.environ if environment is None else environment
886
+ kind = values.get(WORKER_SUPERVISOR_KIND_ENV, "local").strip().casefold()
887
+ image = values.get(WORKER_SUPERVISOR_IMAGE_ENV, "").strip()
888
+ asset_root = values.get(WORKER_SUPERVISOR_ASSET_ROOT_ENV, "").strip()
889
+ if kind == "local":
890
+ if image or asset_root:
891
+ raise ValidationError(
892
+ f"Docker worker image/asset settings require {WORKER_SUPERVISOR_KIND_ENV}=docker"
893
+ )
894
+ return LocalSubprocessSupervisor()
895
+ if kind != "docker":
896
+ raise ValidationError(f"{WORKER_SUPERVISOR_KIND_ENV} must be either local or docker")
897
+ if not image:
898
+ raise ValidationError(f"{WORKER_SUPERVISOR_IMAGE_ENV} is required for Docker supervision")
899
+ return DockerWorkerSupervisor(
900
+ image=image,
901
+ asset_root=asset_root or None,
902
+ )
903
+
904
+
905
+ def _host_bind_directory(value: str | Path, *, label: str) -> Path:
906
+ raw = Path(value).expanduser()
907
+ if not raw.is_absolute():
908
+ raise ValidationError(f"{label} must be absolute")
909
+ if raw == Path(raw.anchor):
910
+ raise ValidationError(f"{label} must not be a filesystem root")
911
+ if raw.is_symlink():
912
+ raise ValidationError(f"{label} must not be a symlink")
913
+ resolved = raw.resolve()
914
+ if resolved != raw or not resolved.is_dir():
915
+ raise ValidationError(f"{label} must be an existing canonical directory")
916
+ if any(character in str(resolved) for character in ",\x00\n\r"):
917
+ raise ValidationError(f"{label} contains a Docker-unsafe character")
918
+ return resolved
919
+
920
+
921
+ def _container_worker_environment(
922
+ environment: Mapping[str, str],
923
+ *,
924
+ checkout: Path,
925
+ adapter_checkout: Path,
926
+ asset_root: Path | None,
927
+ ) -> Mapping[str, str]:
928
+ roots: tuple[tuple[Path, PurePosixPath], ...] = (
929
+ (checkout, PurePosixPath("/opt/upstream")),
930
+ (adapter_checkout, PurePosixPath("/opt/candidate")),
931
+ *(((asset_root, PurePosixPath("/opt/assets")),) if asset_root is not None else ()),
932
+ )
933
+ result: dict[str, str] = {}
934
+ for key, raw_value in environment.items():
935
+ if not isinstance(key, str) or not isinstance(raw_value, str):
936
+ raise ValidationError("Docker worker environment must contain strings")
937
+ if key == "PWD":
938
+ result[key] = "/opt/upstream"
939
+ continue
940
+ if key == "PYTHONPATH":
941
+ # The attested implementation checkout owns algorithm imports.
942
+ # Keep the adapter root available only after the complete upstream
943
+ # search path so a same-named candidate module cannot contaminate
944
+ # an upstream parity arm. ``candidate/src`` is intentionally not
945
+ # exposed: the adapter executes by absolute path and may import
946
+ # support packages from its root, while AgentInputLab itself comes
947
+ # from the pinned worker image.
948
+ paths = ["/opt/upstream"]
949
+ if (checkout / "src").is_dir():
950
+ paths.append("/opt/upstream/src")
951
+ if adapter_checkout != checkout:
952
+ paths.append("/opt/candidate")
953
+ paths.append("/opt/agentinputlab")
954
+ result[key] = ":".join(paths)
955
+ continue
956
+ value_path = Path(raw_value).expanduser()
957
+ if value_path.is_absolute():
958
+ canonical = value_path.resolve()
959
+ mapped: str | None = None
960
+ for host_root, container_root in roots:
961
+ if canonical == host_root or canonical.is_relative_to(host_root):
962
+ relative = canonical.relative_to(host_root)
963
+ mapped = str(container_root.joinpath(*relative.parts))
964
+ break
965
+ if mapped is None:
966
+ raise ValidationError(
967
+ f"Docker worker environment {key!r} contains an unmapped host path"
968
+ )
969
+ result[key] = mapped
970
+ else:
971
+ result[key] = raw_value
972
+ result["HOME"] = "/tmp/worker-home"
973
+ return freeze_mapping(result)
974
+
975
+
976
+ def _start_accelerator_sample(sampler: AcceleratorSampler | None) -> object | None:
977
+ return None if sampler is None else sampler.start()
978
+
979
+
980
+ def _stop_accelerator_sample(
981
+ sampler: AcceleratorSampler | None,
982
+ token: object | None,
983
+ *,
984
+ observation: ProcessResourceObservation,
985
+ ) -> AcceleratorSample:
986
+ if sampler is None:
987
+ identity = observation.measurement_identity.get("accelerator", {})
988
+ if not isinstance(identity, Mapping) or not identity:
989
+ identity = {
990
+ "status": "unavailable",
991
+ "reason": "process_runner_did_not_report",
992
+ }
993
+ return AcceleratorSample(
994
+ accelerator_seconds=observation.accelerator_seconds,
995
+ accelerator_peak_memory_bytes=(observation.accelerator_peak_memory_bytes),
996
+ sampler_identity=identity,
997
+ )
998
+ sample = sampler.stop(token)
999
+ if not isinstance(sample, AcceleratorSample):
1000
+ raise ValidationError("accelerator sampler must return AcceleratorSample")
1001
+ return sample
1002
+
1003
+
1004
+ def _error_observation(error: ExternalDependencyError) -> ProcessResourceObservation:
1005
+ observation = getattr(error, "resource_observation", None)
1006
+ return (
1007
+ observation
1008
+ if isinstance(observation, ProcessResourceObservation)
1009
+ else ProcessResourceObservation()
1010
+ )
1011
+
1012
+
1013
+ def _stop_accelerator_after_process_error(
1014
+ sampler: AcceleratorSampler | None,
1015
+ token: object | None,
1016
+ *,
1017
+ observation: ProcessResourceObservation,
1018
+ ) -> AcceleratorSample:
1019
+ try:
1020
+ return _stop_accelerator_sample(
1021
+ sampler,
1022
+ token,
1023
+ observation=observation,
1024
+ )
1025
+ except Exception as exc:
1026
+ return AcceleratorSample(
1027
+ sampler_identity={
1028
+ "status": "unavailable",
1029
+ "reason": f"finalization_failed:{type(exc).__name__}",
1030
+ }
1031
+ )
1032
+
1033
+
1034
+ def _docker_workload_observation(
1035
+ observation: ProcessResourceObservation,
1036
+ ) -> ProcessResourceObservation:
1037
+ process_identity = observation.measurement_identity.get("process_tree", {})
1038
+ if (
1039
+ isinstance(process_identity, Mapping)
1040
+ and process_identity.get("scope") == "supervised_workload"
1041
+ ):
1042
+ return observation
1043
+ return ProcessResourceObservation(
1044
+ measurement_identity={
1045
+ "process_tree": {
1046
+ "status": "unavailable",
1047
+ "reason": "container_process_is_not_a_child_of_the_docker_client",
1048
+ "launcher_observation": dict(observation.measurement_identity),
1049
+ },
1050
+ "accelerator": {
1051
+ "status": "unavailable",
1052
+ "reason": "container_process_is_not_a_child_of_the_docker_client",
1053
+ },
1054
+ }
1055
+ )
1056
+
1057
+
1058
+ def _docker_exit_code(
1059
+ state: Mapping[str, Any],
1060
+ result: BoundedProcessResult | None,
1061
+ ) -> int | None:
1062
+ value = state.get("ExitCode")
1063
+ if isinstance(value, int) and not isinstance(value, bool):
1064
+ return value
1065
+ return result.returncode if result is not None else None
1066
+
1067
+
1068
+ def _format_number(value: float | None) -> str:
1069
+ if value is None: # pragma: no cover - validated by caller
1070
+ raise AssertionError("required Docker numeric limit is missing")
1071
+ return format(value, ".15g")