agentenv-framework 0.9.1254__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 (312) hide show
  1. agent_env/__init__.py +0 -0
  2. agent_env/a2a_agent/__init__.py +23 -0
  3. agent_env/a2a_agent/a2a_agent.py +600 -0
  4. agent_env/a2a_agent/conversation_store.py +356 -0
  5. agent_env/a2a_agent/object_transfer.py +609 -0
  6. agent_env/a2a_agent/protocol.py +238 -0
  7. agent_env/a2a_agent/store.py +201 -0
  8. agent_env/a2a_agent/validator.py +901 -0
  9. agent_env/artifact/__init__.py +48 -0
  10. agent_env/artifact/artifact.py +74 -0
  11. agent_env/artifact/artifacts/__init__.py +0 -0
  12. agent_env/artifact/artifacts/cli.py +83 -0
  13. agent_env/artifact/artifacts/docker_image.py +457 -0
  14. agent_env/artifact/artifacts/environment.py +89 -0
  15. agent_env/artifact/artifacts/environment_universe.py +156 -0
  16. agent_env/artifact/artifacts/file.py +214 -0
  17. agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
  18. agent_env/artifact/artifacts/skill.py +206 -0
  19. agent_env/artifact/artifacts/vm_image.py +95 -0
  20. agent_env/artifact/ref.py +15 -0
  21. agent_env/artifact/registry.py +251 -0
  22. agent_env/artifact/store.py +295 -0
  23. agent_env/artifact/universe.py +38 -0
  24. agent_env/attribution.py +41 -0
  25. agent_env/bundle/__init__.py +4 -0
  26. agent_env/bundle/_fs.py +128 -0
  27. agent_env/bundle/authoring.py +239 -0
  28. agent_env/bundle/installed.py +175 -0
  29. agent_env/bundle/ledger.py +269 -0
  30. agent_env/bundle/materialize.py +204 -0
  31. agent_env/bundle/parse.py +594 -0
  32. agent_env/bundle/plan.py +414 -0
  33. agent_env/bundle/resolve.py +404 -0
  34. agent_env/bundle/run.py +361 -0
  35. agent_env/cli/__init__.py +69 -0
  36. agent_env/cli/__main__.py +10 -0
  37. agent_env/cli/_installers.py +576 -0
  38. agent_env/cli/_plugin_changes.py +947 -0
  39. agent_env/cli/a2a_agent/__init__.py +22 -0
  40. agent_env/cli/a2a_agent/add_skill.py +97 -0
  41. agent_env/cli/a2a_agent/deploy.py +60 -0
  42. agent_env/cli/a2a_agent/get.py +15 -0
  43. agent_env/cli/a2a_agent/get_instance.py +26 -0
  44. agent_env/cli/a2a_agent/put.py +78 -0
  45. agent_env/cli/a2a_agent/validate.py +56 -0
  46. agent_env/cli/artifact/__init__.py +20 -0
  47. agent_env/cli/artifact/cli.py +83 -0
  48. agent_env/cli/artifact/environment.py +44 -0
  49. agent_env/cli/artifact/environment_universe.py +163 -0
  50. agent_env/cli/artifact/file_artifact_universe.py +281 -0
  51. agent_env/cli/artifact/skill.py +128 -0
  52. agent_env/cli/banner.py +25 -0
  53. agent_env/cli/config.py +263 -0
  54. agent_env/cli/env/__init__.py +31 -0
  55. agent_env/cli/env/deploy.py +112 -0
  56. agent_env/cli/env/gateway.py +57 -0
  57. agent_env/cli/env/get_instance.py +40 -0
  58. agent_env/cli/env/mcp_server.py +410 -0
  59. agent_env/cli/env/multi.py +282 -0
  60. agent_env/cli/env/service_db.py +99 -0
  61. agent_env/cli/env/snapshot.py +35 -0
  62. agent_env/cli/env/state/__init__.py +6 -0
  63. agent_env/cli/env/state/init.py +108 -0
  64. agent_env/cli/env/state/teardown.py +44 -0
  65. agent_env/cli/env/website.py +216 -0
  66. agent_env/cli/env/website_browser.py +67 -0
  67. agent_env/cli/eval/__init__.py +16 -0
  68. agent_env/cli/eval/add_tasks.py +63 -0
  69. agent_env/cli/eval/create.py +49 -0
  70. agent_env/cli/eval/run.py +194 -0
  71. agent_env/cli/identity/__init__.py +5 -0
  72. agent_env/cli/identity/client_identity.py +37 -0
  73. agent_env/cli/plugin.py +438 -0
  74. agent_env/cli/run.py +285 -0
  75. agent_env/cli/task/__init__.py +21 -0
  76. agent_env/cli/task/create.py +109 -0
  77. agent_env/cli/task/get.py +17 -0
  78. agent_env/cli/task/get_instance.py +34 -0
  79. agent_env/cli/task/run.py +615 -0
  80. agent_env/cli/task/validate.py +31 -0
  81. agent_env/cli/up.py +123 -0
  82. agent_env/cli/utils.py +183 -0
  83. agent_env/config/__init__.py +50 -0
  84. agent_env/config/describe.py +1067 -0
  85. agent_env/config/errors.py +13 -0
  86. agent_env/config/loader.py +245 -0
  87. agent_env/config/model.py +154 -0
  88. agent_env/config/paths.py +25 -0
  89. agent_env/config/plugin_tables.py +101 -0
  90. agent_env/config/provenance.py +134 -0
  91. agent_env/config/runtime.py +1064 -0
  92. agent_env/config/snapshot.py +81 -0
  93. agent_env/entity_refs.py +186 -0
  94. agent_env/env/__init__.py +49 -0
  95. agent_env/env/env.py +433 -0
  96. agent_env/env/env_artifact_store.py +87 -0
  97. agent_env/env/envs/__init__.py +7 -0
  98. agent_env/env/envs/_deployment.py +281 -0
  99. agent_env/env/envs/gateway_server.py +32 -0
  100. agent_env/env/envs/mcp_server.py +387 -0
  101. agent_env/env/envs/multi_env.py +780 -0
  102. agent_env/env/envs/service_db/Dockerfile +20 -0
  103. agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
  104. agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
  105. agent_env/env/envs/service_db.py +92 -0
  106. agent_env/env/envs/website.py +294 -0
  107. agent_env/env/envs/website_browser/Dockerfile +40 -0
  108. agent_env/env/envs/website_browser/__init__.py +17 -0
  109. agent_env/env/envs/website_browser/entrypoint.sh +13 -0
  110. agent_env/env/gateway/Dockerfile +14 -0
  111. agent_env/env/gateway/__init__.py +37 -0
  112. agent_env/env/gateway/clock.py +184 -0
  113. agent_env/env/gateway/constants.py +190 -0
  114. agent_env/env/gateway/entrypoint.py +156 -0
  115. agent_env/env/gateway/gateway.py +1095 -0
  116. agent_env/env/gateway/get_time.py +60 -0
  117. agent_env/env/gateway/requirements.txt +4 -0
  118. agent_env/env/gateway/triggers.py +1193 -0
  119. agent_env/env/legacy_protocol.py +146 -0
  120. agent_env/env/registry.py +93 -0
  121. agent_env/env/snapshot_store.py +372 -0
  122. agent_env/env/store.py +411 -0
  123. agent_env/eval/__init__.py +14 -0
  124. agent_env/eval/eval.py +68 -0
  125. agent_env/eval/store.py +116 -0
  126. agent_env/examples/hello/README.md +9 -0
  127. agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
  128. agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
  129. agent_env/examples/hello/tasks/hello.json +10 -0
  130. agent_env/explorer/__init__.py +12 -0
  131. agent_env/explorer/app.py +277 -0
  132. agent_env/explorer/openapi_docs.py +242 -0
  133. agent_env/explorer/plugin.py +95 -0
  134. agent_env/explorer/routers/__init__.py +1 -0
  135. agent_env/explorer/routers/common.py +174 -0
  136. agent_env/explorer/routers/conversations.py +25 -0
  137. agent_env/explorer/routers/objects.py +112 -0
  138. agent_env/explorer/routers/runs.py +475 -0
  139. agent_env/plugins/__init__.py +59 -0
  140. agent_env/plugins/_cli.py +244 -0
  141. agent_env/plugins/_discovery.py +97 -0
  142. agent_env/plugins/_inventory.py +262 -0
  143. agent_env/plugins/_registration.py +268 -0
  144. agent_env/plugins/_report.py +47 -0
  145. agent_env/plugins/_requirements.py +55 -0
  146. agent_env/providers/__init__.py +37 -0
  147. agent_env/providers/env_providers/__init__.py +12 -0
  148. agent_env/providers/env_providers/constants.py +13 -0
  149. agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
  150. agent_env/providers/env_providers/env_provider.py +237 -0
  151. agent_env/providers/env_providers/env_server_provider.py +133 -0
  152. agent_env/providers/env_state/__init__.py +49 -0
  153. agent_env/providers/env_state/env_state_provider.py +554 -0
  154. agent_env/providers/env_state/local_postgres.py +390 -0
  155. agent_env/providers/env_state/store.py +112 -0
  156. agent_env/providers/sandbox_providers/__init__.py +29 -0
  157. agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
  158. agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
  159. agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
  160. agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
  161. agent_env/providers/sandbox_providers/e2b/template.py +158 -0
  162. agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
  163. agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
  164. agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
  165. agent_env/providers/sandbox_providers/sandbox.py +387 -0
  166. agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
  167. agent_env/py.typed +0 -0
  168. agent_env/runner/__init__.py +16 -0
  169. agent_env/runner/local_runner.py +190 -0
  170. agent_env/runner/runner.py +127 -0
  171. agent_env/runner/store.py +94 -0
  172. agent_env/store/__init__.py +126 -0
  173. agent_env/store/_google.py +68 -0
  174. agent_env/store/base.py +33 -0
  175. agent_env/store/document_store/__init__.py +60 -0
  176. agent_env/store/document_store/document_store.py +519 -0
  177. agent_env/store/document_store/dynamodb_document_store.py +306 -0
  178. agent_env/store/document_store/evaluation.py +199 -0
  179. agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
  180. agent_env/store/document_store/mongo_document_store.py +259 -0
  181. agent_env/store/document_store/sqlite_document_store.py +314 -0
  182. agent_env/store/ids.py +123 -0
  183. agent_env/store/image_store/__init__.py +34 -0
  184. agent_env/store/image_store/ecr_image_store.py +131 -0
  185. agent_env/store/image_store/google_credentials.py +113 -0
  186. agent_env/store/image_store/image_store.py +113 -0
  187. agent_env/store/image_store/local_registry_image_store.py +106 -0
  188. agent_env/store/image_store/oci_registry_credentials.py +196 -0
  189. agent_env/store/local_state.py +27 -0
  190. agent_env/store/object_store/__init__.py +24 -0
  191. agent_env/store/object_store/gcs_object_store.py +415 -0
  192. agent_env/store/object_store/local_object_store.py +143 -0
  193. agent_env/store/object_store/object_store.py +198 -0
  194. agent_env/store/object_store/s3_object_store.py +354 -0
  195. agent_env/store/query.py +112 -0
  196. agent_env/store/routing.py +609 -0
  197. agent_env/store/secret_store/__init__.py +19 -0
  198. agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
  199. agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
  200. agent_env/store/secret_store/local_secret_store.py +47 -0
  201. agent_env/store/secret_store/secret_store.py +19 -0
  202. agent_env/task/__init__.py +27 -0
  203. agent_env/task/interrupts.py +109 -0
  204. agent_env/task/registry.py +27 -0
  205. agent_env/task/step_journal.py +99 -0
  206. agent_env/task/store.py +940 -0
  207. agent_env/task/task.py +815 -0
  208. agent_env/task/teardown.py +142 -0
  209. agent_env/task_step/__init__.py +47 -0
  210. agent_env/task_step/context.py +195 -0
  211. agent_env/task_step/context_ops.py +224 -0
  212. agent_env/task_step/registry.py +182 -0
  213. agent_env/task_step/review_store.py +118 -0
  214. agent_env/task_step/snapshot_utils/__init__.py +2 -0
  215. agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
  216. agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
  217. agent_env/task_step/store.py +129 -0
  218. agent_env/task_step/task_step.py +173 -0
  219. agent_env/task_step/task_steps/__init__.py +27 -0
  220. agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
  221. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
  222. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
  223. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
  224. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
  225. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
  226. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
  227. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
  228. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
  229. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
  230. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
  231. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
  232. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
  233. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
  234. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
  235. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
  236. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
  237. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
  238. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
  239. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
  240. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
  241. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
  242. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
  243. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
  244. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
  245. agent_env/task_step/task_steps/add_skills.py +240 -0
  246. agent_env/task_step/task_steps/apply_server_config.py +288 -0
  247. agent_env/task_step/task_steps/collect_artifacts.py +940 -0
  248. agent_env/task_step/task_steps/deploy_agent.py +626 -0
  249. agent_env/task_step/task_steps/deploy_env.py +197 -0
  250. agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
  251. agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
  252. agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
  253. agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
  254. agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
  255. agent_env/task_step/task_steps/install_agent.py +376 -0
  256. agent_env/task_step/task_steps/load_artifact.py +775 -0
  257. agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
  258. agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
  259. agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
  260. agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
  261. agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
  262. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
  263. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
  264. agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
  265. agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
  266. agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
  267. agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
  268. agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
  269. agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
  270. agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
  271. agent_env/task_step/task_steps/peer_agents.py +92 -0
  272. agent_env/task_step/task_steps/prompt_agent.py +900 -0
  273. agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
  274. agent_env/task_step/task_steps/register_env_triggers.py +97 -0
  275. agent_env/task_step/task_steps/reset_env.py +71 -0
  276. agent_env/task_step/task_steps/review.py +152 -0
  277. agent_env/task_step/task_steps/run_code.py +461 -0
  278. agent_env/task_step/task_steps/run_code_runner.py +52 -0
  279. agent_env/task_step/task_steps/run_docker_container.py +380 -0
  280. agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
  281. agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
  282. agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
  283. agent_env/task_step/task_steps/snapshot_env.py +637 -0
  284. agent_env/task_step/task_steps/sync_env_clock.py +132 -0
  285. agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
  286. agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
  287. agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
  288. agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
  289. agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
  290. agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
  291. agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
  292. agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
  293. agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
  294. agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
  295. agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
  296. agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
  297. agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
  298. agent_env/task_step/thread_work.py +61 -0
  299. agent_env/utils/__init__.py +0 -0
  300. agent_env/utils/card_naming.py +108 -0
  301. agent_env/utils/deprecation.py +109 -0
  302. agent_env/utils/docker_build.py +29 -0
  303. agent_env/utils/exec_retry.py +77 -0
  304. agent_env/utils/litellm_attribution.py +59 -0
  305. agent_env/utils/paths.py +20 -0
  306. agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
  307. agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
  308. agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
  309. agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
  310. agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
  311. agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
  312. agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
@@ -0,0 +1,182 @@
1
+ """TaskStep registry for type-based deserialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from agent_env.plugins import _registration
8
+
9
+ if TYPE_CHECKING:
10
+ from agent_env.task_step.task_step import TaskStep
11
+ from agent_env.config.runtime import Config
12
+
13
+ # TaskStep defines this only to raise, so a registered class must override it.
14
+ _MUST_IMPLEMENT = ("from_dict",)
15
+
16
+
17
+ def _get_type(cls: type["TaskStep"]) -> str:
18
+ return cls.type
19
+
20
+
21
+
22
+
23
+ def get_task_step_registry() -> dict[str, type["TaskStep"]]:
24
+ from agent_env.config import runtime
25
+
26
+ return runtime.get_config().task_step_registry()
27
+
28
+
29
+ def _builtin_registry() -> dict[str, type["TaskStep"]]:
30
+ from agent_env.task_step.task_steps.collect_artifacts import CollectArtifactsTaskStep
31
+ from agent_env.task_step.task_steps.mcp_cli_builder.build_mcp_cli import BuildMcpCliTaskStep
32
+ from agent_env.task_step.task_steps.deploy_agent import DeployAgentTaskStep
33
+ from agent_env.task_step.task_steps.deploy_env import DeployEnvTaskStep
34
+ from agent_env.task_step.task_steps.deploy_human_agent import DeployHumanAgentTaskStep
35
+ from agent_env.task_step.task_steps.deploy_sandbox import DeploySandboxTaskStep
36
+ from agent_env.task_step.task_steps.reset_env import ResetEnvTaskStep
37
+ from agent_env.task_step.task_steps.teardown_sandboxes import TeardownSandboxesTaskStep
38
+ from agent_env.task_step.task_steps.verifiers.env_outcome_verifier import EnvOutcomeVerifierTaskStep
39
+ from agent_env.task_step.task_steps.load_artifact import LoadArtifactTaskStep
40
+ from agent_env.task_step.task_steps.modify_env_tool_access import ModifyEnvToolAccessStep
41
+ from agent_env.task_step.task_steps.prompt_agent import PromptAgentTaskStep
42
+ from agent_env.task_step.task_steps.register_env_triggers import RegisterEnvTriggersStep
43
+ from agent_env.task_step.task_steps.register_agent_triggers import RegisterAgentTriggersStep
44
+ from agent_env.task_step.task_steps.sync_env_clock import SyncEnvClockTaskStep
45
+ from agent_env.task_step.task_steps.install_agent import InstallAgentTaskStep
46
+ from agent_env.task_step.task_steps.verifiers.rubrics_verifier import RubricsVerifierTaskStep
47
+ from agent_env.task_step.task_steps.verifiers.run_container_unit_tests_verifier import RunContainerUnitTestsVerifierTaskStep
48
+ from agent_env.task_step.task_steps.run_docker_container import RunDockerContainerTaskStep
49
+ from agent_env.task_step.task_steps.mcp_env_validator.verify_mcp_tool_schema import VerifyMCPToolSchemaTaskStep
50
+ from agent_env.task_step.task_steps.mcp_env_validator.verify_spec_conformance import VerifySpecConformanceTaskStep
51
+ from agent_env.task_step.task_steps.mcp_env_validator.verify_mcp_env_assessment import VerifyMCPEnvAssessmentStep
52
+ from agent_env.task_step.task_steps.mcp_env_validator.validation_gate_aggregator import ValidationGateAggregatorStep
53
+ from agent_env.task_step.task_steps.multienv_validator.verify_universe_roundtrip import VerifyUniverseLoadExportRoundtripStep
54
+ from agent_env.task_step.task_steps.multienv_validator.combine_universe_verdicts import CombineUniverseVerdictsStep
55
+ from agent_env.task_step.task_steps.env_card_validator.verify_env_card import VerifyEnvironmentCardStep
56
+ from agent_env.task_step.task_steps.env_card_validator.verify_env_core_protocol import VerifyCoreEnvironmentProtocolStep
57
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_agent_card import VerifyA2AAgentCardStep
58
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_agent_config_identity import VerifyA2AAgentConfigIdentityStep
59
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_agent_mcp import VerifyA2AAgentMCPStep
60
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_core_protocol import VerifyCoreA2AProtocolStep
61
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_modalities import VerifyA2AModalitiesStep
62
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_peer_agents import VerifyA2APeerAgentsStep
63
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_install import VerifyA2AInstallStep
64
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_skill_config import VerifyA2ASkillConfigStep
65
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_snapshot import VerifyA2ASnapshotStep
66
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_trajectory import VerifyA2ATrajectoryStep
67
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_role import VerifyA2ARoleStep
68
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_system_prompt import VerifyA2ASystemPromptStep
69
+ from agent_env.task_step.task_steps.add_skills import AddSkillsTaskStep
70
+ from agent_env.task_step.task_steps.apply_server_config import ApplyServerConfigStep
71
+ from agent_env.task_step.task_steps.verifiers.agent_prompt_response_verifier import AgentPromptResponseVerifierTaskStep
72
+ from agent_env.task_step.task_steps.snapshot_agent_state import SnapshotAgentStateTaskStep
73
+ from agent_env.task_step.task_steps.snapshot_env import SnapshotEnvTaskStep
74
+ from agent_env.task_step.task_steps.peer_agents import PeerAgentsTaskStep
75
+ from agent_env.task_step.task_steps.verifiers.verify_sandbox import VerifySandboxTaskStep
76
+ from agent_env.task_step.task_steps.verifiers.aggregate_verifiers import AggregateVerifiersTaskStep
77
+ from agent_env.task_step.task_steps.run_code import RunCodeTaskStep
78
+ from agent_env.task_step.task_steps.review import ReviewTaskStep
79
+
80
+ return {
81
+ _get_type(ReviewTaskStep): ReviewTaskStep,
82
+ _get_type(BuildMcpCliTaskStep): BuildMcpCliTaskStep,
83
+ _get_type(CollectArtifactsTaskStep): CollectArtifactsTaskStep,
84
+ _get_type(DeployAgentTaskStep): DeployAgentTaskStep,
85
+ _get_type(DeployEnvTaskStep): DeployEnvTaskStep,
86
+ _get_type(DeployHumanAgentTaskStep): DeployHumanAgentTaskStep,
87
+ _get_type(DeploySandboxTaskStep): DeploySandboxTaskStep,
88
+ _get_type(ResetEnvTaskStep): ResetEnvTaskStep,
89
+ _get_type(TeardownSandboxesTaskStep): TeardownSandboxesTaskStep,
90
+ _get_type(EnvOutcomeVerifierTaskStep): EnvOutcomeVerifierTaskStep,
91
+ _get_type(LoadArtifactTaskStep): LoadArtifactTaskStep,
92
+ _get_type(ModifyEnvToolAccessStep): ModifyEnvToolAccessStep,
93
+ _get_type(PromptAgentTaskStep): PromptAgentTaskStep,
94
+ _get_type(RegisterEnvTriggersStep): RegisterEnvTriggersStep,
95
+ _get_type(RegisterAgentTriggersStep): RegisterAgentTriggersStep,
96
+ _get_type(SyncEnvClockTaskStep): SyncEnvClockTaskStep,
97
+ _get_type(InstallAgentTaskStep): InstallAgentTaskStep,
98
+ _get_type(RubricsVerifierTaskStep): RubricsVerifierTaskStep,
99
+ _get_type(RunContainerUnitTestsVerifierTaskStep): RunContainerUnitTestsVerifierTaskStep,
100
+ _get_type(RunDockerContainerTaskStep): RunDockerContainerTaskStep,
101
+ _get_type(VerifyMCPToolSchemaTaskStep): VerifyMCPToolSchemaTaskStep,
102
+ _get_type(VerifySpecConformanceTaskStep): VerifySpecConformanceTaskStep,
103
+ _get_type(VerifyMCPEnvAssessmentStep): VerifyMCPEnvAssessmentStep,
104
+ _get_type(ValidationGateAggregatorStep): ValidationGateAggregatorStep,
105
+ _get_type(VerifyUniverseLoadExportRoundtripStep): VerifyUniverseLoadExportRoundtripStep,
106
+ _get_type(CombineUniverseVerdictsStep): CombineUniverseVerdictsStep,
107
+ _get_type(VerifyEnvironmentCardStep): VerifyEnvironmentCardStep,
108
+ _get_type(VerifyCoreEnvironmentProtocolStep): VerifyCoreEnvironmentProtocolStep,
109
+ _get_type(VerifyA2AAgentCardStep): VerifyA2AAgentCardStep,
110
+ _get_type(VerifyA2AAgentConfigIdentityStep): VerifyA2AAgentConfigIdentityStep,
111
+ _get_type(VerifyA2AAgentMCPStep): VerifyA2AAgentMCPStep,
112
+ _get_type(VerifyA2ATrajectoryStep): VerifyA2ATrajectoryStep,
113
+ _get_type(VerifyA2AInstallStep): VerifyA2AInstallStep,
114
+ _get_type(VerifyA2ASkillConfigStep): VerifyA2ASkillConfigStep,
115
+ _get_type(AddSkillsTaskStep): AddSkillsTaskStep,
116
+ _get_type(ApplyServerConfigStep): ApplyServerConfigStep,
117
+ _get_type(AgentPromptResponseVerifierTaskStep): AgentPromptResponseVerifierTaskStep,
118
+ _get_type(VerifyCoreA2AProtocolStep): VerifyCoreA2AProtocolStep,
119
+ _get_type(SnapshotAgentStateTaskStep): SnapshotAgentStateTaskStep,
120
+ _get_type(SnapshotEnvTaskStep): SnapshotEnvTaskStep,
121
+ _get_type(RunCodeTaskStep): RunCodeTaskStep,
122
+ _get_type(PeerAgentsTaskStep): PeerAgentsTaskStep,
123
+ _get_type(VerifyA2ASnapshotStep): VerifyA2ASnapshotStep,
124
+ _get_type(VerifyA2APeerAgentsStep): VerifyA2APeerAgentsStep,
125
+ _get_type(VerifyA2AModalitiesStep): VerifyA2AModalitiesStep,
126
+ _get_type(VerifySandboxTaskStep): VerifySandboxTaskStep,
127
+ _get_type(AggregateVerifiersTaskStep): AggregateVerifiersTaskStep,
128
+ _get_type(VerifyA2ARoleStep): VerifyA2ARoleStep,
129
+ _get_type(VerifyA2ASystemPromptStep): VerifyA2ASystemPromptStep,
130
+ }
131
+
132
+
133
+ def _build_registry(source: Config | None = None) -> dict[str, type["TaskStep"]]:
134
+ """The built-in task steps, then ``agent_env.task_steps`` plugins, then ``[task_steps] impls``
135
+ in ``source``."""
136
+ from agent_env.task_step.task_step import TaskStep
137
+
138
+ registry = _builtin_registry()
139
+ validate = _registration.typed_validator(TaskStep, builtins=frozenset(registry), required=_MUST_IMPLEMENT)
140
+ from_plugins = _registration.merge(registry, _registration.TASK_STEPS, validate, source=source)
141
+ _merge_config_toml_steps(registry, source=source, from_plugins=from_plugins)
142
+ return registry
143
+
144
+
145
+ def _merge_config_toml_steps(
146
+ registry: dict[str, type["TaskStep"]],
147
+ *,
148
+ source: Config | None = None,
149
+ from_plugins: _registration.Registrations | None = None,
150
+ ) -> None:
151
+ from agent_env.config import runtime
152
+ from agent_env.config import ConfigError, load_impl
153
+ from agent_env.task_step.task_step import TaskStep
154
+
155
+ section = (source or runtime.get_config()).section("task_steps")
156
+ impls = section.get("impls", [])
157
+ if not isinstance(impls, list):
158
+ raise ConfigError(
159
+ f"[task_steps] impls must be a list of 'module:Class' strings, got {type(impls).__name__}"
160
+ )
161
+ from_plugins = from_plugins if from_plugins is not None else _registration.Registrations.empty()
162
+ for impl in impls:
163
+ if not isinstance(impl, str):
164
+ raise ConfigError(
165
+ f"[task_steps] impl must be a 'module:Class' string, got {type(impl).__name__}: {impl!r}"
166
+ )
167
+ cls = load_impl(impl, TaskStep)
168
+ if cls.type == TaskStep.type:
169
+ raise ConfigError(
170
+ f"[task_steps] impl {impl!r} does not define its own 'type' "
171
+ f"(inherits the base default {TaskStep.type!r}); set a unique 'type' ClassVar"
172
+ )
173
+ if problem := _registration.unimplemented(cls, TaskStep, _MUST_IMPLEMENT):
174
+ raise ConfigError(f"[task_steps] impl {impl!r}: {problem}")
175
+ if from_plugins.refuse(cls.type, f"[task_steps] impl {impl!r}"):
176
+ continue
177
+ if not from_plugins.release(cls.type, "task_steps", impl, cls) and cls.type in registry:
178
+ raise ConfigError(
179
+ f"[task_steps] impl {impl!r} type {cls.type!r} is already registered "
180
+ f"(conflicts with a built-in or another custom step)"
181
+ )
182
+ registry[cls.type] = cls
@@ -0,0 +1,118 @@
1
+ """Store for human-in-the-loop review decisions, keyed by ``(instance_id, step_id)``.
2
+
3
+ Both the worker (``ReviewTaskStep``) and the hub write here, so it lives in
4
+ agent-env and the hub imports it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import datetime, timedelta, timezone
11
+ from typing import Any, Optional
12
+
13
+ from agent_env.store import DuplicateKeyError, Eq, Filter, UpdateSpec, get_config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ REVIEWS_COLLECTION = "task_reviews"
18
+
19
+ AWAITING = "awaiting"
20
+ CONTINUE = "continue"
21
+ ABORT = "abort"
22
+ _DECISIONS = (CONTINUE, ABORT)
23
+
24
+ # Reap old coordination docs; window dwarfs any realistic review timeout, so active reviews never expire.
25
+ _TTL = timedelta(days=7)
26
+
27
+
28
+ class ReviewStore:
29
+ def __init__(self) -> None:
30
+ self._indexed = None
31
+
32
+ @property
33
+ def _doc_store(self):
34
+ # Resolved per call: a cached store outlives reset_config(), so a process that
35
+ # re-pointed would read the new config and write the old backend.
36
+ store = get_config().get_document_store()
37
+ if self._indexed is not store:
38
+ store.ensure_index(
39
+ REVIEWS_COLLECTION, ["instance_id", "step_id"], unique=True
40
+ )
41
+ store.ensure_index(
42
+ REVIEWS_COLLECTION, ["expires_at"], ttl_seconds=0
43
+ )
44
+ self._indexed = store
45
+ return store
46
+
47
+ @staticmethod
48
+ def _now() -> str:
49
+ return datetime.now(timezone.utc).isoformat()
50
+
51
+ def put_awaiting(
52
+ self,
53
+ instance_id: str,
54
+ step_id: str,
55
+ *,
56
+ label: Optional[str] = None,
57
+ upstream: Optional[dict[str, Any]] = None,
58
+ ) -> None:
59
+ """Register a review as awaiting a decision. Idempotent — a retry won't clobber a recorded decision."""
60
+ doc = {
61
+ "instance_id": instance_id,
62
+ "step_id": step_id,
63
+ "state": AWAITING,
64
+ "label": label,
65
+ "upstream": upstream or {},
66
+ "awaiting_at": self._now(),
67
+ "expires_at": datetime.now(timezone.utc) + _TTL,
68
+ }
69
+ try:
70
+ self._doc_store.insert(REVIEWS_COLLECTION, doc)
71
+ except DuplicateKeyError:
72
+ pass
73
+
74
+ def get(self, instance_id: str, step_id: str) -> Optional[dict[str, Any]]:
75
+ return self._doc_store.find_one(
76
+ REVIEWS_COLLECTION, Filter.of(instance_id=instance_id, step_id=step_id)
77
+ )
78
+
79
+ def set_decision(
80
+ self,
81
+ instance_id: str,
82
+ step_id: str,
83
+ decision: str,
84
+ *,
85
+ decided_by: Optional[str] = None,
86
+ ) -> bool:
87
+ """Record ``continue``/``abort`` on an awaiting review; False if none was awaiting."""
88
+ if decision not in _DECISIONS:
89
+ raise ValueError(f"decision must be one of {_DECISIONS}, got {decision!r}")
90
+ matched = self._doc_store.update(
91
+ REVIEWS_COLLECTION,
92
+ Filter.of(instance_id=instance_id, step_id=step_id).where("state", Eq(AWAITING)),
93
+ UpdateSpec(
94
+ set={"state": decision, "decided_by": decided_by, "decided_at": self._now()}
95
+ ),
96
+ )
97
+ return matched > 0
98
+
99
+ def list_awaiting(self, instance_id: Optional[str] = None) -> list[dict[str, Any]]:
100
+ filt = Filter.of(state=AWAITING)
101
+ if instance_id is not None:
102
+ filt = filt.where("instance_id", Eq(instance_id))
103
+ return self._doc_store.query(REVIEWS_COLLECTION, filt)
104
+
105
+
106
+ _store: Optional[ReviewStore] = None
107
+
108
+
109
+ def get_review_store() -> ReviewStore:
110
+ global _store
111
+ if _store is None:
112
+ _store = ReviewStore()
113
+ return _store
114
+
115
+
116
+ def reset_review_store() -> None:
117
+ global _store
118
+ _store = None
@@ -0,0 +1,2 @@
1
+ """Agent-state capture. Separate from ``task_steps`` because none of it is a
2
+ ``TaskStep`` subclass."""
@@ -0,0 +1,261 @@
1
+ """The reads a capture makes against a running agent's sidecar.
2
+
3
+ ``capture_workspace`` raises on any failure — without the tar there is nothing to
4
+ grade — while ``read_partial_trajectory`` never does, because the bundle alone is
5
+ what makes a point gradable.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import uuid
13
+ from dataclasses import dataclass
14
+ from typing import Any, Optional
15
+
16
+ import httpx
17
+ from agentenv_protocol.a2a_agent import ObjectSnapshotSaveResponse
18
+
19
+ from agent_env.a2a_agent import A2AAgent
20
+ from agent_env.a2a_agent.object_transfer import (
21
+ SNAPSHOT_TRAJECTORY_OBJECT_NAME,
22
+ SNAPSHOT_WORKSPACE_OBJECT_NAME,
23
+ FetchedTrajectory,
24
+ TrajectoryUpload,
25
+ bounded_echo,
26
+ fetch_trajectory,
27
+ invoke_transfer,
28
+ snapshot_save_call,
29
+ trajectory_mode,
30
+ )
31
+ from agent_env.config import get_config
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ @dataclass
37
+ class WorkspaceCapture:
38
+ """One ``/ext/snapshot`` call plus the ``FileArtifactUniverse`` wrapping it."""
39
+
40
+ universe_id: str
41
+ universe_version: Optional[int]
42
+ # Where the files landed; what restores.
43
+ bundle_object_url: str
44
+ # What was presigned — not always where the agent wrote. Per-service state,
45
+ # captured alongside, still goes here.
46
+ capture_prefix: str
47
+
48
+
49
+ @dataclass
50
+ class TrajectoryCapture:
51
+ trajectory: Any = None
52
+ object_url: str | None = None
53
+ # Non-None means this read produced no trajectory; the caller records it as a
54
+ # `partial` capture reason.
55
+ reason: Optional[str] = None
56
+
57
+
58
+ async def capture_workspace(
59
+ *,
60
+ a2a_url: str,
61
+ a2a_card: dict,
62
+ agent_name: str,
63
+ a2a_context_id: str,
64
+ artifact_id: str,
65
+ timeout_seconds: float,
66
+ ) -> WorkspaceCapture:
67
+ """Tar the agent's workspace to a fresh S3 prefix and wrap it as a universe.
68
+
69
+ Raises on any failure — without the tar there is nothing to grade."""
70
+ from agent_env.artifact.store import get_artifact_store
71
+
72
+ snapshot_ext = A2AAgent.find_extension(a2a_card or {}, A2AAgent.EXT_SNAPSHOT)
73
+ if not snapshot_ext:
74
+ raise RuntimeError(f"Agent '{agent_name}' does not advertise the snapshot extension")
75
+ save_method, save_path = A2AAgent.operation(snapshot_ext, "save")
76
+
77
+ # The random suffix, not the version, isolates captures: two concurrent ones
78
+ # can peek the same version. to_thread because this loop is shared by every
79
+ # concurrent rollout, so a blocking call here stalls their poll loops.
80
+ snapshot_version = await asyncio.to_thread(get_artifact_store().next_version, artifact_id)
81
+ config = get_config()
82
+ capture_key_prefix = (
83
+ f"{config.get_artifact_key_prefix()}agent_snapshots/{artifact_id}/{snapshot_version}-{uuid.uuid4().hex[:8]}/"
84
+ )
85
+ store = config.get_object_store()
86
+ capture_prefix = store.object_url(capture_key_prefix)
87
+
88
+ call = await asyncio.to_thread(
89
+ snapshot_save_call,
90
+ save_method,
91
+ store,
92
+ agent_name=agent_name,
93
+ context_id=a2a_context_id,
94
+ capture_prefix=capture_prefix,
95
+ )
96
+ save_body = await invoke_transfer(
97
+ a2a_url + save_path,
98
+ call,
99
+ verb="POST",
100
+ operation="snapshot save",
101
+ timeout=timeout_seconds,
102
+ response_model=ObjectSnapshotSaveResponse,
103
+ )
104
+
105
+ if call.mode == "objects":
106
+ # The agent's answer is only a claim: a snapshot missing either object cannot be restored.
107
+ listed = await asyncio.to_thread(store.list_at, capture_prefix)
108
+ stored = {url.rsplit("/", 1)[-1] for url in listed}
109
+ missing = {SNAPSHOT_TRAJECTORY_OBJECT_NAME, SNAPSHOT_WORKSPACE_OBJECT_NAME} - stored
110
+ if missing:
111
+ raise RuntimeError(
112
+ f"snapshot save from agent '{agent_name}' left {sorted(missing)} out of the object store"
113
+ )
114
+ registered_prefix = capture_prefix
115
+ else:
116
+ echoed = save_body.get("s3_prefix")
117
+ if not echoed:
118
+ raise RuntimeError(f"snapshot save response missing 's3_prefix': {save_body}")
119
+ registered_prefix = bounded_echo(capture_prefix, echoed)
120
+
121
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
122
+
123
+ # Unserialized — a concurrent version race on one artifact id is
124
+ # `ArtifactStore.put_document`'s retry to absorb.
125
+ universe = await asyncio.to_thread(
126
+ FileArtifactUniverse.put_existing, id=artifact_id, s3_url=registered_prefix
127
+ )
128
+ # `put_existing` always sets it, but the field is Optional on the artifact, and a
129
+ # row carrying None here would read as an ungradable capture rather than an error.
130
+ if not universe.bundle_object_url:
131
+ raise RuntimeError(
132
+ f"FileArtifactUniverse {universe.id} v{universe.version} registered no "
133
+ f"bundle url for {registered_prefix}"
134
+ )
135
+ logger.info(
136
+ "Snapshot captured: agent=%s context_id=%s -> FileArtifactUniverse %s v%s at %s",
137
+ agent_name, a2a_context_id, universe.id, universe.version,
138
+ universe.bundle_object_url,
139
+ )
140
+ return WorkspaceCapture(
141
+ universe_id=universe.id,
142
+ universe_version=universe.version,
143
+ bundle_object_url=universe.bundle_object_url,
144
+ capture_prefix=capture_prefix,
145
+ )
146
+
147
+
148
+ async def read_partial_trajectory(
149
+ *,
150
+ a2a_url: str,
151
+ a2a_card: dict,
152
+ context_id: str,
153
+ timeout_seconds: float,
154
+ trajectory_output_prefix: str,
155
+ ) -> TrajectoryCapture:
156
+ """Read the trajectory-so-far for an in-progress run.
157
+
158
+ Separate from the workspace bundle because not every harness's snapshot
159
+ carries the transcript, which would leave interior points with no trajectory.
160
+
161
+ Served by ``get`` in its ``context_id`` mode — cumulative, and keyed on the a2a
162
+ context id each image maps to its own session. An agent advertising ``get`` as
163
+ ``task_id``-only cannot answer this: a task id names one finished turn, which
164
+ 404s while the run it belongs to is still going.
165
+
166
+ Never raises: a failure yields a ``reason`` the caller records as ``partial``,
167
+ so an agent serving neither shape still yields a usable curve minus the
168
+ trajectory column.
169
+ """
170
+ traj_ext = A2AAgent.find_extension(a2a_card or {}, A2AAgent.EXT_TRAJECTORY)
171
+ if not traj_ext:
172
+ return TrajectoryCapture(reason="trajectory_ext_unavailable")
173
+ # Presence, not truthiness: a card may advertise a method as a bare `{}`,
174
+ # which is falsy. Testing truth would read such a card as not having it.
175
+ get_method, get_path = A2AAgent.operation(traj_ext, "get")
176
+ if get_method is None:
177
+ return TrajectoryCapture(reason="trajectory_get_unadvertised")
178
+ # Only an explicit request contract offers the context mode: a `get` that declares
179
+ # none may be task_id-only, and would 400 on every tick.
180
+ if "request" not in get_method:
181
+ return TrajectoryCapture(reason="trajectory_context_unsupported")
182
+ store = get_config().get_object_store()
183
+ mode = trajectory_mode(get_method, store, by="context_id")
184
+ if mode is None:
185
+ return TrajectoryCapture(reason="trajectory_context_unsupported")
186
+
187
+ upload = None
188
+ if mode == "objects":
189
+ try:
190
+ object_url = trajectory_object_url(trajectory_output_prefix, store=store)
191
+ upload = await asyncio.to_thread(TrajectoryUpload.to, store, object_url)
192
+ except Exception as exc:
193
+ logger.warning("partial trajectory grant failed for %s: %s", context_id, exc)
194
+ return TrajectoryCapture(reason="trajectory_grant_unavailable")
195
+ try:
196
+ fetched = await fetch_trajectory(
197
+ a2a_url + get_path, {"context_id": context_id}, upload=upload, timeout=timeout_seconds
198
+ )
199
+ except httpx.HTTPStatusError as exc:
200
+ if exc.response.status_code == 404:
201
+ # No transcript yet — the agent hasn't written a turn.
202
+ return TrajectoryCapture(reason="trajectory_session_missing")
203
+ return TrajectoryCapture(reason=f"trajectory_http_{exc.response.status_code}")
204
+ except Exception as exc:
205
+ logger.warning("partial trajectory read failed for %s: %s", context_id, exc)
206
+ return TrajectoryCapture(reason="trajectory_read_failed")
207
+
208
+ if fetched.object_url is not None:
209
+ return TrajectoryCapture(object_url=fetched.object_url)
210
+ if not fetched.inline:
211
+ return TrajectoryCapture(reason="trajectory_empty")
212
+ return TrajectoryCapture(trajectory=fetched.inline)
213
+
214
+
215
+ def upload_trajectory(
216
+ trajectory: Any, trajectory_output_prefix: str, *, name: Optional[str] = None
217
+ ) -> str:
218
+ """Serialize a trajectory payload and store it, returning its object URL.
219
+
220
+ Always uploaded from the worker side, never by the agent: sandbox VMs hold
221
+ static STS env vars with a fixed expiry, and long runs exceed it.
222
+
223
+ ``name`` makes the object addressable by an id the caller already records
224
+ (#731); omit it to get a random one, which is what writing repeatedly under a
225
+ single prefix needs, since a fixed name would overwrite the last upload.
226
+ """
227
+
228
+ store = get_config().get_object_store()
229
+ # Via the store, not urlparse().path: a prefix naming a different bucket
230
+ # must fail loudly, not silently write to the configured one.
231
+ prefix_key = store.get_object_key(trajectory_output_prefix)
232
+ if prefix_key and not prefix_key.endswith("/"):
233
+ prefix_key += "/"
234
+ key = f"{prefix_key}trajectory-{name or uuid.uuid4().hex[:12]}.json"
235
+ body = json.dumps(trajectory, indent=2, default=str).encode()
236
+ return store.put(key, body, content_type="application/json")
237
+
238
+
239
+ def store_trajectory(
240
+ fetched: FetchedTrajectory, trajectory_output_prefix: str, *, name: Optional[str] = None
241
+ ) -> str | None:
242
+ """The URL of a fetched trajectory, storing it first when the agent returned it inline."""
243
+ if fetched.object_url is not None:
244
+ return fetched.object_url
245
+ if fetched.legacy_prefix:
246
+ urls = get_config().get_object_store().list_at(fetched.legacy_prefix)
247
+ return urls[0] if urls else None
248
+ if fetched.inline is not None:
249
+ return upload_trajectory(fetched.inline, trajectory_output_prefix, name=name)
250
+ return None
251
+
252
+
253
+ def trajectory_object_url(
254
+ trajectory_output_prefix: str, *, store, name: Optional[str] = None
255
+ ) -> str:
256
+ """Return the durable URL used for one agent-written trajectory."""
257
+ prefix_key = store.get_object_key(trajectory_output_prefix)
258
+ if prefix_key and not prefix_key.endswith("/"):
259
+ prefix_key += "/"
260
+ key = f"{prefix_key}trajectory-{name or uuid.uuid4().hex[:12]}.json"
261
+ return store.object_url(key)