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,626 @@
1
+ """Deploy A2A agent task step."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import logging
6
+ from typing import ClassVar, Optional
7
+
8
+ import httpx
9
+ from agentenv_protocol.a2a_agent import (
10
+ NamespaceChangelogEnableResponse,
11
+ ObjectChangelogApplyResponse,
12
+ ObjectSnapshotLoadResponse,
13
+ )
14
+
15
+ from agent_env.a2a_agent import protocol
16
+ from agent_env.a2a_agent.object_transfer import (
17
+ REPLY_TIMEOUT_SECONDS,
18
+ TRANSFER_TIMEOUT_SECONDS,
19
+ bounded_echo,
20
+ changelog_apply_call,
21
+ changelog_enable_call,
22
+ check_changelog_applied,
23
+ invoke_transfer,
24
+ snapshot_load_call,
25
+ )
26
+ from agent_env.config import get_config
27
+ from agent_env.env.env import DeployedEnv, DeployedSandboxEnv, Env
28
+ from agent_env.providers.sandbox_providers.sandbox_provider import reachable_url, sandbox_request_headers_for_url
29
+ from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy
30
+ from agent_env.task_step.context import DeployedAgent, TaskStepContext
31
+ from agent_env.entity_refs import EntityRef
32
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
33
+ from agent_env.attribution import deploy_attribution
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ def _mcp_add_body(mcp_ext: dict, url: str, headers: dict | None, card_name: str | None) -> dict:
39
+ """Body for the agent's mcp-config `add`; the card name is relayed verbatim when the agent's card lists `name` as optional."""
40
+ body: dict = {"url": url}
41
+ if headers:
42
+ body["headers"] = headers
43
+ add_request = mcp_ext.get("params", {}).get("methods", {}).get("add", {}).get("request", {})
44
+ if card_name and "name" in (add_request.get("optional") or []):
45
+ body["name"] = card_name
46
+ return body
47
+
48
+
49
+ def _choose_mcp_url(agent, env) -> str:
50
+ """The env's MCP URL, as reachable from wherever the agent runs; an env outside our sandboxes gives its own."""
51
+ if not isinstance(env, DeployedSandboxEnv):
52
+ return env.mcp_url
53
+ return reachable_url(env.mcp_url, from_sandbox_type=env.sandbox_type, to_sandbox_type=agent.sandbox_type)
54
+
55
+
56
+ def _live_mcp_url(env_id: str) -> str:
57
+ """The MCP URL of an env that is referenced by id but never deployed here.
58
+
59
+ Such an env is a pointer at a live endpoint: it exposes ``mcp_url`` and has no
60
+ deployment to bind to.
61
+ """
62
+ url = getattr(Env.get(env_id), "mcp_url", None)
63
+ if not isinstance(url, str) or not url.strip().startswith(("http://", "https://")):
64
+ raise RuntimeError(
65
+ f"Env '{env_id}' is not in context.deployed_envs; add a DeployEnvTaskStep "
66
+ "for it, or reference an env that exposes a live http(s) 'mcp_url'."
67
+ )
68
+ return url.strip()
69
+
70
+
71
+ def _skill_fields(skill: dict) -> dict:
72
+ """A ``skills`` entry as ``A2AAgent.register_skill`` arguments. ``content`` and ``s3_uri`` are
73
+ older names for ``skill_md`` and ``skill_s3_url``; ``skill_md`` is sent verbatim."""
74
+ skill_md = skill.get("skill_md", skill.get("content"))
75
+ return {
76
+ "name": skill.get("name", ""),
77
+ "description": skill.get("description", ""),
78
+ "skill_md": skill_md,
79
+ "object_url": (
80
+ skill.get("skill_s3_url", skill.get("s3_uri")) if skill_md is None else None
81
+ ),
82
+ }
83
+
84
+
85
+ class DeployAgentTaskStep(TaskStep):
86
+ type: ClassVar[str] = "deploy_agent"
87
+ entity_refs = (
88
+ EntityRef.env("env_ids[]"),
89
+ EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),
90
+ EntityRef.artifact(
91
+ "agent_snapshot_files_artifact_id",
92
+ version_field="agent_snapshot_files_artifact_version",
93
+ artifact_type="file_artifact_universe",
94
+ ),
95
+ )
96
+
97
+ def __init__(
98
+ self,
99
+ id: str,
100
+ version: Optional[int],
101
+ env_ids: Optional[list[str]] = None,
102
+ env_step_id: Optional[str] = None,
103
+ a2a_agent_id: Optional[str] = None,
104
+ a2a_agent_version: Optional[int] = None,
105
+ agent_name: Optional[str] = None,
106
+ agent_description: Optional[str] = None,
107
+ env_vars: Optional[dict[str, str]] = None,
108
+ skills: Optional[list[dict]] = None,
109
+ system_prompt: Optional[str] = None,
110
+ disk_size_gb: Optional[float] = None,
111
+ litellm_base_url: Optional[str] = None,
112
+ sandbox_type: Optional[str] = None,
113
+ ttl_seconds: Optional[int] = None,
114
+ cpu: Optional[float] = None,
115
+ memory_mb: Optional[int] = None,
116
+ agent_snapshot_files_artifact_id: Optional[str] = None,
117
+ agent_snapshot_files_artifact_version: Optional[int] = None,
118
+ agent_snapshot_target_context_id: Optional[str] = None,
119
+ priority: Optional[int] = None,
120
+ network_policy: Optional[dict] = None,
121
+ role: Optional[str] = None,
122
+ enable_agent_changelog: bool = False,
123
+ agent_changelog_s3_prefix: Optional[str] = None,
124
+ agent_changelog_toolcall_position_exclusive: Optional[int] = None,
125
+ sandbox_name: Optional[str] = None,
126
+ enable_docker: bool = False,
127
+ depends_on: Optional[list[TaskStepDependency]] = None,
128
+ fail_task_on_error: bool = True,
129
+ metadata: Optional[dict] = None,
130
+ agent_changelog_object_url: Optional[str] = None,
131
+ ):
132
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
133
+ self.env_ids = env_ids or []
134
+ self.env_step_id = env_step_id
135
+ self.a2a_agent_id = a2a_agent_id
136
+ self.a2a_agent_version = a2a_agent_version
137
+ self.agent_name = agent_name or TaskStep.DEFAULT_AGENT_NAME
138
+ self.agent_description = agent_description
139
+ self.env_vars = env_vars or {}
140
+ self.skills = skills or []
141
+ self.system_prompt = system_prompt
142
+ self.disk_size_gb = disk_size_gb
143
+ self.litellm_base_url = litellm_base_url
144
+ self.sandbox_type = sandbox_type
145
+ self.ttl_seconds = ttl_seconds
146
+ self.cpu = cpu
147
+ self.memory_mb = memory_mb
148
+ self.agent_snapshot_files_artifact_id = agent_snapshot_files_artifact_id
149
+ self.agent_snapshot_files_artifact_version = agent_snapshot_files_artifact_version
150
+ self.agent_snapshot_target_context_id = agent_snapshot_target_context_id
151
+ self.priority = priority
152
+ self.metadata = metadata or {}
153
+ self.network_policy = NetworkPolicy.from_dict(network_policy).to_dict() if network_policy else None
154
+ self.role = role
155
+ self.enable_agent_changelog = enable_agent_changelog
156
+ self.agent_changelog_object_url = agent_changelog_object_url
157
+ self.agent_changelog_s3_prefix = agent_changelog_s3_prefix
158
+ self.agent_changelog_toolcall_position_exclusive = agent_changelog_toolcall_position_exclusive
159
+ if self.agent_changelog_object_url and self.agent_changelog_s3_prefix:
160
+ raise ValueError(
161
+ "set only one of agent_changelog_object_url and "
162
+ "agent_changelog_s3_prefix"
163
+ )
164
+ if self.agent_changelog_toolcall_position_exclusive is not None and not (
165
+ self.agent_changelog_object_url or self.agent_changelog_s3_prefix
166
+ ):
167
+ raise ValueError(
168
+ "agent_changelog_toolcall_position_exclusive requires a changelog source"
169
+ )
170
+ self.sandbox_name = sandbox_name
171
+ # Rootless Docker-in-Docker for the agent (not the VM socket); VM mode only.
172
+ self.enable_docker = enable_docker
173
+
174
+ def to_dict(self) -> dict:
175
+ base = super().to_dict()
176
+ base["env_ids"] = self.env_ids
177
+ base["env_step_id"] = self.env_step_id
178
+ base["a2a_agent_id"] = self.a2a_agent_id
179
+ base["a2a_agent_version"] = self.a2a_agent_version
180
+ base["agent_name"] = self.agent_name
181
+ base["agent_description"] = self.agent_description
182
+ base["env_vars"] = self.env_vars
183
+ base["skills"] = self.skills
184
+ base["system_prompt"] = self.system_prompt
185
+ base["disk_size_gb"] = self.disk_size_gb
186
+ base["litellm_base_url"] = self.litellm_base_url
187
+ base["sandbox_type"] = self.sandbox_type
188
+ base["ttl_seconds"] = self.ttl_seconds
189
+ base["cpu"] = self.cpu
190
+ base["memory_mb"] = self.memory_mb
191
+ base["agent_snapshot_files_artifact_id"] = self.agent_snapshot_files_artifact_id
192
+ base["agent_snapshot_files_artifact_version"] = self.agent_snapshot_files_artifact_version
193
+ base["agent_snapshot_target_context_id"] = self.agent_snapshot_target_context_id
194
+ base["priority"] = self.priority
195
+ base["metadata"] = self.metadata
196
+ base["network_policy"] = self.network_policy
197
+ base["role"] = self.role
198
+ base["enable_agent_changelog"] = self.enable_agent_changelog
199
+ base["agent_changelog_object_url"] = self.agent_changelog_object_url
200
+ base["agent_changelog_s3_prefix"] = self.agent_changelog_s3_prefix
201
+ base["agent_changelog_toolcall_position_exclusive"] = self.agent_changelog_toolcall_position_exclusive
202
+ base["sandbox_name"] = self.sandbox_name
203
+ base["enable_docker"] = self.enable_docker
204
+ return base
205
+
206
+ @classmethod
207
+ def from_dict(cls, data: dict) -> DeployAgentTaskStep:
208
+ return cls(
209
+ **cls._base_from_dict(data),
210
+ env_ids=data.get("env_ids"),
211
+ env_step_id=data.get("env_step_id"),
212
+ a2a_agent_id=data.get("a2a_agent_id"),
213
+ a2a_agent_version=data.get("a2a_agent_version"),
214
+ agent_name=data.get("agent_name"),
215
+ agent_description=data.get("agent_description"),
216
+ env_vars=data.get("env_vars"),
217
+ skills=data.get("skills"),
218
+ system_prompt=data.get("system_prompt"),
219
+ disk_size_gb=data.get("disk_size_gb"),
220
+ litellm_base_url=data.get("litellm_base_url"),
221
+ sandbox_type=data.get("sandbox_type"),
222
+ ttl_seconds=data.get("ttl_seconds"),
223
+ cpu=data.get("cpu"),
224
+ memory_mb=data.get("memory_mb"),
225
+ agent_snapshot_files_artifact_id=data.get("agent_snapshot_files_artifact_id"),
226
+ agent_snapshot_files_artifact_version=data.get("agent_snapshot_files_artifact_version"),
227
+ agent_snapshot_target_context_id=data.get("agent_snapshot_target_context_id"),
228
+ priority=data.get("priority"),
229
+ metadata=dict(data.get("metadata") or {}),
230
+ network_policy=data.get("network_policy"),
231
+ role=data.get("role"),
232
+ enable_agent_changelog=data.get("enable_agent_changelog", False),
233
+ agent_changelog_object_url=data.get("agent_changelog_object_url"),
234
+ agent_changelog_s3_prefix=data.get("agent_changelog_s3_prefix"),
235
+ agent_changelog_toolcall_position_exclusive=data.get("agent_changelog_toolcall_position_exclusive"),
236
+ sandbox_name=data.get("sandbox_name"),
237
+ enable_docker=data.get("enable_docker", False),
238
+ )
239
+
240
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
241
+ from agent_env.a2a_agent import A2AAgent
242
+ from agent_env.a2a_agent.a2a_agent import DEFAULT_A2A_PORT
243
+ from agent_env.providers.sandbox_providers.sandbox_provider import (
244
+ SANDBOX_MODE_VM,
245
+ build_sandbox_provider,
246
+ get_sandbox_provider,
247
+ )
248
+
249
+ # Resolved before anything is provisioned so an unbound env fails before a sandbox exists.
250
+ env_deployments = {env_id: self._deployed_env(context, env_id) for env_id in self.env_ids}
251
+
252
+ existing = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
253
+ if existing is not None:
254
+ raise RuntimeError(f"Agent with name '{self.agent_name}' is already deployed")
255
+
256
+ linked_sandbox = None
257
+ if self.sandbox_name:
258
+ ds = next((s for s in context.deployed_sandboxes if s.sandbox_name == self.sandbox_name), None)
259
+ if ds is None:
260
+ raise RuntimeError(
261
+ f"Sandbox '{self.sandbox_name}' not found in context.deployed_sandboxes; "
262
+ f"deploy it with DeploySandboxTaskStep before this step"
263
+ )
264
+ if ds.sandbox_mode != SANDBOX_MODE_VM:
265
+ raise RuntimeError(
266
+ f"Cannot link agent to sandbox '{self.sandbox_name}' (mode={ds.sandbox_mode!r}); "
267
+ f"only VM-mode sandboxes can host an additional agent container"
268
+ )
269
+ if not ds.tunnel_urls or str(DEFAULT_A2A_PORT) not in ds.tunnel_urls:
270
+ raise RuntimeError(
271
+ f"Sandbox '{self.sandbox_name}' does not expose port {DEFAULT_A2A_PORT}; "
272
+ f"re-deploy DeploySandboxTaskStep with exposed_ports=[{DEFAULT_A2A_PORT}]"
273
+ )
274
+ provider = (
275
+ build_sandbox_provider(ds.sandbox_type) if ds.sandbox_type else get_sandbox_provider()
276
+ )
277
+ linked_sandbox = await provider.get_sandbox(ds.sandbox_id)
278
+ logger.info(
279
+ f"Linking agent '{self.agent_name}' to pre-existing sandbox "
280
+ f"'{self.sandbox_name}' (sandbox_id={ds.sandbox_id}, type={ds.sandbox_type})"
281
+ )
282
+
283
+ config = get_config()
284
+ user_overrides = context.metadata.get("user_overrides", {})
285
+ a2a_agent_id = user_overrides.get("a2a_agent_id") or self.a2a_agent_id or config.get_default_a2a_agent_id()
286
+ a2a_agent_version = (
287
+ user_overrides["a2a_agent_version"]
288
+ if "a2a_agent_version" in user_overrides
289
+ else self.a2a_agent_version
290
+ )
291
+ agent = A2AAgent.get(a2a_agent_id, a2a_agent_version)
292
+ requested_disk = user_overrides.get("agent_disk_size_gb") or self.disk_size_gb
293
+ min_disk = agent.metadata.get("min_disk_size_gb")
294
+ agent_disk_size_gb = max(requested_disk or 0, min_disk or 0) or None
295
+ env_vars = dict(self.env_vars) if self.env_vars else {}
296
+ override_litellm_key = user_overrides.get("litellm_api_key")
297
+ if override_litellm_key:
298
+ env_vars["LITELLM_API_KEY"] = override_litellm_key
299
+ resolved_litellm_base_url = user_overrides.get("litellm_base_url") or self.litellm_base_url
300
+ if resolved_litellm_base_url:
301
+ env_vars["LITELLM_BASE_URL"] = resolved_litellm_base_url
302
+
303
+ # Cost-attribution (project_id / task_id) is delivered per-prompt
304
+ # via the A2A `/ext/agent-config` extension from prompt_agent.py —
305
+ # not baked as container env vars at deploy time. See
306
+ # the openai_agents_sdk A2A agent in the universe-generation pipeline
307
+ # for the reference implementation that reads agent-config and threads
308
+ # `user` / `metadata.tags` into each LiteLLM call.
309
+
310
+ resolved_sandbox_type = user_overrides.get("agent_sandbox") or self.sandbox_type
311
+ deploy_kwargs = {"env_vars": env_vars if env_vars else None}
312
+ if agent_disk_size_gb is not None:
313
+ deploy_kwargs["disk_size_gb"] = agent_disk_size_gb
314
+ if resolved_sandbox_type is not None:
315
+ deploy_kwargs["sandbox_type"] = resolved_sandbox_type
316
+ resolved_ttl = self._resolved_ttl_seconds(context)
317
+ deploy_kwargs["ttl_seconds"] = resolved_ttl
318
+ if self.cpu is not None:
319
+ deploy_kwargs["cpu"] = self.cpu
320
+ if self.memory_mb is not None:
321
+ deploy_kwargs["memory"] = self.memory_mb
322
+ deploy_kwargs["attribution"] = deploy_attribution(self, context)
323
+ _priority_override = user_overrides.get("priority")
324
+ resolved_priority = _priority_override if _priority_override is not None else self.priority
325
+ if resolved_priority is not None:
326
+ deploy_kwargs["priority"] = resolved_priority
327
+ _policy_override = user_overrides.get("network_policy")
328
+ resolved_policy = _policy_override if _policy_override is not None else self.network_policy
329
+ if resolved_policy is not None:
330
+ deploy_kwargs["network_policy"] = NetworkPolicy.from_dict(resolved_policy)
331
+ if self.enable_docker:
332
+ deploy_kwargs["enable_docker"] = True
333
+ if linked_sandbox is not None:
334
+ deploy_kwargs["sandbox"] = linked_sandbox
335
+ deployed = await agent.deploy(**deploy_kwargs)
336
+ a2a_url = deployed.a2a_url
337
+ card = deployed.agent_card
338
+ logger.info(f"A2A agent '{a2a_agent_id}' deployed at {a2a_url}")
339
+
340
+ env_mcp_urls: list[tuple[str, str, dict | None, str | None]] = [] # (env_id, url, headers, card_name)
341
+ for env_id in self.env_ids:
342
+ deployed_env = env_deployments[env_id]
343
+ if deployed_env is not None:
344
+ url = _choose_mcp_url(deployed, deployed_env)
345
+ card_name = (deployed_env.environment_card or {}).get("name")
346
+ if not card_name:
347
+ logger.warning(f"Env {env_id} has no env card name; the agent will pick its own MCP alias")
348
+ else:
349
+ url = _live_mcp_url(env_id)
350
+ card_name = None
351
+ env_mcp_urls.append((env_id, url, sandbox_request_headers_for_url(url) or None, card_name))
352
+
353
+ mcp_ext = A2AAgent.find_extension(card, A2AAgent.EXT_MCP_CONFIG)
354
+ if mcp_ext and env_mcp_urls:
355
+ endpoint = a2a_url + mcp_ext["params"]["endpoint"]
356
+ async with httpx.AsyncClient() as client:
357
+ for env_id, mcp_url, headers, card_name in env_mcp_urls:
358
+ body = _mcp_add_body(mcp_ext, mcp_url, headers, card_name)
359
+ logger.info(f"Registering MCP for env {env_id} via {mcp_url}")
360
+ resp = await client.post(endpoint, json=body, timeout=180)
361
+ if resp.status_code >= 400:
362
+ logger.error(f"Failed to register MCP {env_id}: {resp.status_code} {resp.text}")
363
+ resp.raise_for_status()
364
+ logger.info(f"Registered MCP {env_id}: {resp.json()}")
365
+
366
+ skill_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SKILL_CONFIG)
367
+ if skill_ext and self.skills:
368
+ for skill in self.skills:
369
+ result = await A2AAgent.register_skill(deployed, **_skill_fields(skill))
370
+ logger.info(f"Registered skill: {result}")
371
+
372
+ if self.system_prompt is not None:
373
+ config_ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
374
+ if config_ext is None:
375
+ logger.warning(f"Agent '{self.agent_name}' does not advertise {A2AAgent.EXT_AGENT_CONFIG} — skipping system_prompt")
376
+ else:
377
+ system_prompt = self.system_prompt
378
+ seed = context.metadata.get("seed", {})
379
+ if seed:
380
+ for key, value in seed.items():
381
+ system_prompt = system_prompt.replace(f"<{key}>", str(value))
382
+ supported = (config_ext.get("params") or {}).get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
383
+ if "system_prompt" not in supported:
384
+ logger.warning(f"Agent '{self.agent_name}' does not list 'system_prompt' in supported agent-config fields — skipping")
385
+ else:
386
+ endpoint = a2a_url + (config_ext.get("params") or {}).get("endpoint", "/ext/agent-config")
387
+ await protocol.post_agent_config(endpoint, {"system_prompt": system_prompt})
388
+ logger.info(f"Set system_prompt on agent '{self.agent_name}'")
389
+
390
+ identity_payload: dict[str, str] = {"name": self.agent_name}
391
+ if self.agent_description is not None:
392
+ identity_payload["description"] = self.agent_description
393
+ if self.role is not None:
394
+ identity_payload["role"] = self.role
395
+ config_ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
396
+ if config_ext is not None:
397
+ supported = (config_ext.get("params") or {}).get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
398
+ identity_payload = {k: v for k, v in identity_payload.items() if k in supported}
399
+ if identity_payload:
400
+ endpoint = a2a_url + (config_ext.get("params") or {}).get("endpoint", "/ext/agent-config")
401
+ await protocol.post_agent_config(endpoint, identity_payload)
402
+ card.update(identity_payload)
403
+ logger.info(f"Stamped card identity on '{self.agent_name}': {sorted(identity_payload)}")
404
+
405
+ default_model = config.get_model_for_role("agent")
406
+ if default_model and not context.default_agent_model:
407
+ context.default_agent_model = default_model
408
+ logger.info(f"Set context.default_agent_model to '{default_model}'")
409
+
410
+ if self.agent_snapshot_files_artifact_id:
411
+ await self._load_snapshot(a2a_url, card, a2a_agent_id, context)
412
+
413
+ if self.agent_changelog_object_url or self.agent_changelog_s3_prefix:
414
+ await self._apply_agent_changelog(a2a_url, card, context)
415
+
416
+ if self.enable_agent_changelog:
417
+ await self._configure_agent_changelog(
418
+ a2a_url, card, context, expires_in=resolved_ttl
419
+ )
420
+
421
+ context.deployed_agents.append(DeployedAgent(
422
+ agent_name=self.agent_name,
423
+ api_url=a2a_url,
424
+ sandbox_id=deployed.sandbox_id,
425
+ sandbox_type=deployed.sandbox_type,
426
+ a2a_url=a2a_url,
427
+ a2a_card=card,
428
+ instance_id=deployed.instance_id,
429
+ role=self.role,
430
+ network_policy=deployed.network_policy,
431
+ ))
432
+ return context
433
+
434
+ @staticmethod
435
+ def _deploy_step_of(deployed_env: DeployedEnv) -> Optional[str]:
436
+ return (deployed_env.metadata or {}).get("deploy_step_id")
437
+
438
+ def _deployed_env(self, context: TaskStepContext, env_id: str) -> Optional[DeployedEnv]:
439
+ """The deployment of `env_id` this step is bound to, or None if it isn't deployed here."""
440
+ matches = [d for d in context.deployed_envs if d.env_id == env_id]
441
+ if self.env_step_id is not None:
442
+ # A filter, not a hint: an unmatched id is an error, never a substitute.
443
+ # Envs with no deployments fall through to the caller's live-endpoint path.
444
+ named = [d for d in matches if self._deploy_step_of(d) == self.env_step_id]
445
+ if matches and not named:
446
+ raise RuntimeError(
447
+ f"deploy_agent '{self.id}': env_step_id '{self.env_step_id}' names no deployment of "
448
+ f"env '{env_id}' (deployed by: {[self._deploy_step_of(d) for d in matches]})"
449
+ )
450
+ matches = named
451
+ if len(matches) > 1:
452
+ raise RuntimeError(
453
+ f"deploy_agent '{self.id}': {len(matches)} deployments of env '{env_id}' "
454
+ f"(deployed by: {[self._deploy_step_of(d) for d in matches]}). Set env_step_id "
455
+ "to the deploy_env step this agent should bind to."
456
+ )
457
+ return matches[0] if matches else None
458
+
459
+ def _resolved_ttl_seconds(self, context: TaskStepContext) -> int:
460
+ override = context.metadata.get("user_overrides", {}).get("ttl_seconds")
461
+ if override is not None:
462
+ return override
463
+ if self.ttl_seconds is not None:
464
+ return self.ttl_seconds
465
+ return TaskStep.DEFAULT_TTL_SECONDS
466
+
467
+ async def _configure_agent_changelog(
468
+ self,
469
+ a2a_url: str,
470
+ card: dict,
471
+ context: TaskStepContext,
472
+ *,
473
+ expires_in: int,
474
+ ) -> None:
475
+ """Enable whole-writable-layer changelog capture via the snapshot enable-changelog method; records the derived prefix in context.metadata['agent_changelog']."""
476
+ from agent_env.a2a_agent import A2AAgent
477
+
478
+ snapshot_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SNAPSHOT)
479
+ method, path = A2AAgent.operation(snapshot_ext, A2AAgent.SNAPSHOT_METHOD_ENABLE_CHANGELOG)
480
+ if method is None:
481
+ raise RuntimeError(
482
+ f"Agent '{self.agent_name}' does not support the snapshot "
483
+ f"'{A2AAgent.SNAPSHOT_METHOD_ENABLE_CHANGELOG}' method; cannot enable_agent_changelog"
484
+ )
485
+ instance_id = (context.instance_id or "noinstance").replace("/", "_")
486
+ agent_name = self.agent_name.replace("/", "_")
487
+ config = get_config()
488
+ store = config.get_object_store()
489
+ namespace_url = store.object_url(f"{config.get_artifact_key_prefix()}agent_changelog/{instance_id}/{agent_name}")
490
+ call = await asyncio.to_thread(
491
+ changelog_enable_call,
492
+ method,
493
+ store,
494
+ agent_name=self.agent_name,
495
+ namespace_url=namespace_url,
496
+ expires_in=expires_in,
497
+ )
498
+ answer = await invoke_transfer(
499
+ a2a_url + path,
500
+ call,
501
+ verb="POST",
502
+ operation="changelog enable",
503
+ timeout=REPLY_TIMEOUT_SECONDS,
504
+ response_model=NamespaceChangelogEnableResponse,
505
+ )
506
+ if call.mode == "objects":
507
+ roots, object_url = answer.roots, namespace_url
508
+ else:
509
+ roots = answer.get("roots")
510
+ object_url = bounded_echo(namespace_url, answer.get("s3_prefix") or namespace_url)
511
+ context.metadata.setdefault("agent_changelog", []).append({
512
+ "agent_name": self.agent_name,
513
+ "roots": roots,
514
+ "transfer_mode": call.mode,
515
+ "object_url": object_url,
516
+ })
517
+ logger.info(f"agent-changelog capture enabled on '{self.agent_name}': {object_url}")
518
+
519
+ async def _apply_agent_changelog(self, a2a_url: str, card: dict, context: TaskStepContext) -> None:
520
+ """Rewind this fresh agent to a point in a source changelog (fs + conversation) via the snapshot apply-changelog method and resume."""
521
+ from agent_env.a2a_agent import A2AAgent
522
+
523
+ snapshot_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SNAPSHOT)
524
+ method, path = A2AAgent.operation(snapshot_ext, A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG)
525
+ if method is None:
526
+ raise RuntimeError(
527
+ f"Agent '{self.agent_name}' does not support the snapshot "
528
+ f"'{A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG}' method; cannot apply agent changelog"
529
+ )
530
+ store = get_config().get_object_store()
531
+ source_url = self.agent_changelog_object_url or self.agent_changelog_s3_prefix
532
+ call = await asyncio.to_thread(
533
+ changelog_apply_call,
534
+ method,
535
+ store,
536
+ agent_name=self.agent_name,
537
+ source_url=source_url,
538
+ portable=bool(self.agent_changelog_object_url),
539
+ up_to_tool_call_exclusive=self.agent_changelog_toolcall_position_exclusive,
540
+ resume_conversation=True,
541
+ target_context_id=self.agent_snapshot_target_context_id,
542
+ )
543
+ answer = await invoke_transfer(
544
+ a2a_url + path,
545
+ call,
546
+ verb="PUT",
547
+ operation="changelog apply",
548
+ timeout=TRANSFER_TIMEOUT_SECONDS,
549
+ response_model=ObjectChangelogApplyResponse,
550
+ )
551
+ if call.mode == "objects":
552
+ check_changelog_applied(answer, call, agent_name=self.agent_name)
553
+ context_id = answer.context_id
554
+ else:
555
+ context_id = answer.get("context_id")
556
+ context.metadata.setdefault("agent_changelog_rewinds", []).append({
557
+ "agent_name": self.agent_name,
558
+ "context_id": context_id,
559
+ "up_to_tool_call_exclusive": self.agent_changelog_toolcall_position_exclusive,
560
+ "transfer_mode": call.mode,
561
+ "object_url": source_url,
562
+ })
563
+ logger.info(
564
+ f"agent-changelog applied onto '{self.agent_name}' from {source_url} "
565
+ f"(position_exclusive={self.agent_changelog_toolcall_position_exclusive}, context_id={context_id})"
566
+ )
567
+
568
+ async def _load_snapshot(
569
+ self,
570
+ a2a_url: str,
571
+ card: dict,
572
+ a2a_agent_id: str,
573
+ context: TaskStepContext,
574
+ ) -> None:
575
+ """Restore a snapshot into the freshly-deployed agent via /ext/snapshot."""
576
+ from agent_env.a2a_agent import A2AAgent
577
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
578
+
579
+ snapshot_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SNAPSHOT)
580
+ if not snapshot_ext:
581
+ raise RuntimeError(
582
+ f"Cannot load snapshot: agent '{a2a_agent_id}' does not advertise "
583
+ f"the snapshot extension ({A2AAgent.EXT_SNAPSHOT})"
584
+ )
585
+ universe = FileArtifactUniverse.get(
586
+ self.agent_snapshot_files_artifact_id,
587
+ self.agent_snapshot_files_artifact_version,
588
+ )
589
+ if not universe.bundle_object_url:
590
+ raise RuntimeError(
591
+ f"FileArtifactUniverse '{universe.id}' v{universe.version} has no bundle_s3_url; "
592
+ "snapshot universes must be created via put_existing or put_bundled"
593
+ )
594
+ load_method, load_path = A2AAgent.operation(snapshot_ext, "load")
595
+ call = await asyncio.to_thread(
596
+ snapshot_load_call,
597
+ load_method,
598
+ get_config().get_object_store(),
599
+ agent_name=self.agent_name,
600
+ bundle_url=universe.bundle_object_url,
601
+ file_names=set((universe.file_artifact_refs or universe.file_artifact_ids or {}).keys()),
602
+ target_context_id=self.agent_snapshot_target_context_id,
603
+ )
604
+ answer = await invoke_transfer(
605
+ a2a_url + load_path,
606
+ call,
607
+ verb="PUT",
608
+ operation="snapshot load",
609
+ timeout=TRANSFER_TIMEOUT_SECONDS,
610
+ response_model=ObjectSnapshotLoadResponse,
611
+ )
612
+ loaded_context_id = (
613
+ answer.context_id if call.mode == "objects" else answer.get("context_id")
614
+ )
615
+ logger.info(
616
+ f"Loaded snapshot universe={universe.id} v{universe.version} "
617
+ f"into agent '{self.agent_name}' as context_id={loaded_context_id}"
618
+ )
619
+ if loaded_context_id:
620
+ loaded = context.metadata.setdefault("agent_loaded_snapshots", [])
621
+ loaded.append({
622
+ "agent_name": self.agent_name,
623
+ "context_id": loaded_context_id,
624
+ "source_artifact_id": universe.id,
625
+ "source_artifact_version": universe.version,
626
+ })