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,238 @@
1
+ """Helper Functions for interacting with A2A agents (like sending a task to an A2A Agent)."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import logging
6
+ import re
7
+ import time
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass
10
+ from typing import Any, Optional
11
+
12
+ import httpx
13
+ from a2a.types import TaskState
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _TERMINAL_TASK_STATES = frozenset({
18
+ TaskState.completed, TaskState.failed, TaskState.canceled, TaskState.rejected,
19
+ })
20
+
21
+
22
+ _ERROR_CODE = re.compile(r"[a-z][a-z0-9_]{0,63}")
23
+
24
+
25
+ def raise_for_extension_status(
26
+ response: httpx.Response, *, operation: str, include_body: bool = False
27
+ ) -> None:
28
+ """Raise ``httpx.HTTPStatusError`` naming the SDK error code, but no other part of a body
29
+ that may echo a grant. ``include_body`` keeps the body, for a request that sent no grant."""
30
+ try:
31
+ response.raise_for_status()
32
+ except httpx.HTTPStatusError as exc:
33
+ if include_body:
34
+ detail = response.text[:500]
35
+ else:
36
+ try:
37
+ body = response.json()
38
+ except ValueError:
39
+ body = None
40
+ error = body.get("error") if isinstance(body, Mapping) else None
41
+ code = error.get("code") if isinstance(error, Mapping) else None
42
+ detail = code if isinstance(code, str) and _ERROR_CODE.fullmatch(code) else ""
43
+ message = f"{operation} failed with HTTP {response.status_code}"
44
+ if detail:
45
+ message += f": {detail}"
46
+ raise httpx.HTTPStatusError(message, request=exc.request, response=response) from exc
47
+
48
+
49
+ async def post_agent_config(url: str, payload: dict, timeout_seconds: int = 60) -> None:
50
+ """POST to an agent's /ext/agent-config endpoint with retry on transient 404/connect errors.
51
+
52
+ `url` should be the full endpoint URL (a2a_url + endpoint path from the agent card).
53
+ """
54
+ max_attempts = 10
55
+ delay = 2.0
56
+ last_exc: Exception | None = None
57
+ for attempt in range(1, max_attempts + 1):
58
+ try:
59
+ async with httpx.AsyncClient() as client:
60
+ resp = await client.post(url, json=payload, timeout=timeout_seconds)
61
+ if resp.status_code == 404:
62
+ raise httpx.HTTPStatusError(
63
+ f"agent-config endpoint not available (404) at {url} after {max_attempts} "
64
+ f"attempts — the agent gateway isn't serving it (either boot up/initialization "
65
+ f"failed, or the agent gateway was torn down by TTL reasons).",
66
+ request=resp.request, response=resp,
67
+ )
68
+ resp.raise_for_status()
69
+ if attempt > 1:
70
+ logger.info(f"agent-config POST succeeded on attempt {attempt}")
71
+ return
72
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.RemoteProtocolError) as e:
73
+ last_exc = e
74
+ logger.warning(f"agent-config POST failed (attempt {attempt}/{max_attempts}, will retry): {e}")
75
+ except httpx.HTTPStatusError as e:
76
+ if e.response.status_code != 404:
77
+ raise
78
+ last_exc = e
79
+ logger.warning(f"agent-config POST got 404 (attempt {attempt}/{max_attempts}, will retry): {url}")
80
+ if attempt < max_attempts:
81
+ await asyncio.sleep(delay)
82
+ delay = min(delay * 2, 15.0)
83
+ assert last_exc is not None
84
+ raise last_exc
85
+
86
+
87
+ async def send_a2a_message(
88
+ a2a_url: str,
89
+ parts: list[dict],
90
+ message_id: str,
91
+ context_id: Optional[str],
92
+ timeout_seconds: int,
93
+ ) -> tuple[str, Optional[str]]:
94
+ """POST /a2a message/send. Returns (task_id, resolved_context_id). Retries on connect errors."""
95
+ message: dict[str, Any] = {
96
+ "messageId": message_id, "role": "user",
97
+ "parts": parts,
98
+ }
99
+ if context_id is not None:
100
+ message["contextId"] = context_id
101
+ payload = {
102
+ "jsonrpc": "2.0", "id": "1", "method": "message/send",
103
+ "params": {"message": message, "configuration": {"blocking": False}},
104
+ }
105
+ max_attempts = 5
106
+ delay = 2.0
107
+ last_exc: Exception | None = None
108
+ for attempt in range(1, max_attempts + 1):
109
+ try:
110
+ async with httpx.AsyncClient() as client:
111
+ resp = await client.post(f"{a2a_url}/a2a", json=payload, timeout=timeout_seconds)
112
+ resp.raise_for_status()
113
+ send_body = resp.json()
114
+ if "error" in send_body:
115
+ raise RuntimeError(f"A2A message/send failed: {send_body['error']}")
116
+ send_result = send_body.get("result") or {}
117
+ task_id = send_result.get("id")
118
+ if not task_id:
119
+ raise RuntimeError(f"A2A message/send succeeded but returned no task id: {send_result}")
120
+ resolved_context_id = context_id or send_result.get("contextId")
121
+ return task_id, resolved_context_id
122
+ except (httpx.ConnectError, httpx.ConnectTimeout) as e:
123
+ last_exc = e
124
+ logger.warning(f"message/send connect error (attempt {attempt}/{max_attempts}, will retry): {e}")
125
+ if attempt < max_attempts:
126
+ await asyncio.sleep(delay)
127
+ delay = min(delay * 2, 15.0)
128
+ assert last_exc is not None
129
+ raise last_exc
130
+
131
+
132
+ async def poll_a2a_task(
133
+ a2a_url: str,
134
+ task_id: str,
135
+ timeout_seconds: int,
136
+ poll_interval_seconds: int = 10,
137
+ ) -> dict:
138
+ """POST /a2a tasks/get until status.state is 'completed' or 'failed'. Returns the result dict."""
139
+ deadline = time.monotonic() + timeout_seconds
140
+ consecutive_failures = 0
141
+ while time.monotonic() < deadline:
142
+ backoff = min(poll_interval_seconds + consecutive_failures * 5, 60)
143
+ await asyncio.sleep(backoff)
144
+ try:
145
+ async with httpx.AsyncClient() as client:
146
+ resp = await client.post(f"{a2a_url}/a2a", json={
147
+ "jsonrpc": "2.0", "id": "poll", "method": "tasks/get",
148
+ "params": {"id": task_id},
149
+ }, timeout=30)
150
+ resp.raise_for_status()
151
+ data = resp.json()
152
+ if "error" in data:
153
+ consecutive_failures += 1
154
+ logger.warning(f"A2A poll returned error (consec={consecutive_failures}, will retry): {data['error']}")
155
+ continue
156
+ result = data["result"]
157
+ except httpx.HTTPStatusError as e:
158
+ if 400 <= e.response.status_code < 500:
159
+ raise
160
+ consecutive_failures += 1
161
+ logger.warning(f"A2A poll got {e.response.status_code} (consec={consecutive_failures}, will retry)")
162
+ continue
163
+ except httpx.HTTPError as e:
164
+ consecutive_failures += 1
165
+ logger.warning(f"A2A poll failed (consec={consecutive_failures}, will retry): {type(e).__name__}: {e}")
166
+ continue
167
+
168
+ consecutive_failures = 0
169
+ state = result["status"]["state"]
170
+ if state in _TERMINAL_TASK_STATES:
171
+ return result
172
+
173
+ raise TimeoutError(f"A2A task {task_id} did not complete within {timeout_seconds}s")
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class TerminalResponse:
178
+ """What's pulled from an A2A terminal status message: the text reply, the typed
179
+ ``structured_output`` (when ``output_format`` was set), and tool/error telemetry."""
180
+
181
+ response_text: str
182
+ tool_call_count: Optional[int] = None
183
+ error_type: Optional[str] = None
184
+ error_code: Optional[str] = None
185
+ error_class: Optional[str] = None
186
+ error_message: Optional[str] = None
187
+ structured_output: Optional[Any] = None
188
+
189
+ @classmethod
190
+ def from_message(cls, status_message: dict) -> "TerminalResponse":
191
+ """Parse an A2A terminal status message. Per A2A spec 0.3 the final agent message
192
+ (TaskUpdater.complete/failed) lives in result.status.message, not result.history."""
193
+ response_text = ""
194
+ tool_call_count: Optional[int] = None
195
+ error_type: Optional[str] = None
196
+ error_code: Optional[str] = None
197
+ error_class: Optional[str] = None
198
+ error_message: Optional[str] = None
199
+ structured_output: Optional[Any] = None
200
+ for part in (status_message or {}).get("parts", []):
201
+ kind = part.get("kind")
202
+ if kind == "text":
203
+ response_text = part["text"]
204
+ elif kind == "data":
205
+ data = part.get("data", {})
206
+ if "tool_call_count" in data:
207
+ tool_call_count = data["tool_call_count"]
208
+ usage = data.get("usage")
209
+ if isinstance(usage, dict) and "tool_call_count" in usage:
210
+ tool_call_count = usage["tool_call_count"]
211
+ if "error_type" in data:
212
+ error_type = data["error_type"]
213
+ if "error_code" in data:
214
+ error_code = data["error_code"]
215
+ if "error_class" in data:
216
+ error_class = data["error_class"]
217
+ if "error_message" in data:
218
+ error_message = data["error_message"]
219
+ if "structured_output" in data:
220
+ structured_output = data["structured_output"]
221
+ return cls(
222
+ response_text=response_text,
223
+ tool_call_count=tool_call_count,
224
+ error_type=error_type,
225
+ error_code=error_code,
226
+ error_class=error_class,
227
+ error_message=error_message,
228
+ structured_output=structured_output,
229
+ )
230
+
231
+
232
+ def extract_terminal_response(
233
+ status_message: dict,
234
+ ) -> tuple[str, Optional[int], Optional[str], Optional[str], Optional[str]]:
235
+ """Back-compat shim over ``TerminalResponse.from_message``; external callers
236
+ (the hub's agents.py) still unpack the 5-tuple. Drop once they migrate."""
237
+ tr = TerminalResponse.from_message(status_message)
238
+ return tr.response_text, tr.tool_call_count, tr.error_type, tr.error_class, tr.error_message
@@ -0,0 +1,201 @@
1
+ """A2A Agent store for MongoDB persistence."""
2
+ from __future__ import annotations
3
+
4
+ import dataclasses
5
+ import logging
6
+ import random
7
+ import string
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import TYPE_CHECKING, Any, Optional, Self
10
+
11
+ from agent_env.store.base import ConcurrentModificationError, NotFoundError
12
+ from agent_env.config import get_config
13
+ from agent_env.store.document_store import (
14
+ AbsentOrNull,
15
+ Filter,
16
+ LteOrAbsent,
17
+ UpdateSpec,
18
+ VersionedEntityStore,
19
+ VersionedEntityStoreCache,
20
+ )
21
+ from agent_env.store.query import QueryBuilder, to_document_query
22
+
23
+ if TYPE_CHECKING:
24
+ from agent_env.a2a_agent.a2a_agent import A2AAgent
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ A2A_AGENTS_COLLECTION = "a2a_agents"
29
+ A2A_AGENT_INSTANCES_COLLECTION = "a2a_agent_instances"
30
+
31
+
32
+ class A2AAgentQuery(QueryBuilder["A2AAgent"]):
33
+ def __init__(self, store: Optional[A2AAgentStore] = None) -> None:
34
+ super().__init__()
35
+ self._store = store
36
+
37
+ def _clone(self) -> Self:
38
+ clone = A2AAgentQuery(self._store)
39
+ clone._filters = self._filters.copy()
40
+ clone._sort_field = self._sort_field
41
+ clone._sort_desc = self._sort_desc
42
+ clone._limit_value = self._limit_value
43
+ clone._offset_value = self._offset_value
44
+ return clone
45
+
46
+ def execute(self) -> list[A2AAgent]:
47
+ if self._store is None:
48
+ self._store = get_a2a_agent_store()
49
+ return self._store.execute_query(self)
50
+
51
+ def _execute_count(self) -> int:
52
+ if self._store is None:
53
+ self._store = get_a2a_agent_store()
54
+ return self._store.execute_count(self)
55
+
56
+
57
+ class A2AAgentStore:
58
+ def __init__(self) -> None:
59
+ self._versioned_cache: VersionedEntityStoreCache[A2AAgent] = VersionedEntityStoreCache(
60
+ A2A_AGENTS_COLLECTION, self._serialize, self._deserialize, secondary_indexes=[["type"]],
61
+ )
62
+
63
+ @property
64
+ def _doc_store(self):
65
+ return get_config().get_document_store()
66
+
67
+ @property
68
+ def _versioned(self) -> VersionedEntityStore[A2AAgent]:
69
+ return self._versioned_cache.for_store(self._doc_store)
70
+
71
+ def _serialize(self, agent: A2AAgent) -> dict:
72
+ doc = agent.to_dict()
73
+ doc["created_at_utc"] = datetime.now(timezone.utc)
74
+ return doc
75
+
76
+ def get(self, id: str, version: Optional[int] = None) -> A2AAgent:
77
+ agent = self._versioned.get(id, version)
78
+ if agent is None:
79
+ version_str = f" version={version}" if version is not None else ""
80
+ raise NotFoundError(f"A2AAgent {id}{version_str} not found")
81
+ return agent
82
+
83
+ def next_version(self, id: str) -> int:
84
+ return self._versioned.next_version(id)
85
+
86
+ def put_document(self, agent: A2AAgent) -> A2AAgent:
87
+ agent.version = self._versioned.put(agent)
88
+ return agent
89
+
90
+ def update_metadata(self, id: str, version: int, old_metadata: dict[str, Any], new_metadata: dict[str, Any]) -> dict[str, Any]:
91
+ """Replace metadata using timestamp-based CAS (same pattern as EnvStore)."""
92
+ now = datetime.now(timezone.utc).isoformat()
93
+ old_ts = old_metadata.get("updated_at")
94
+ new_metadata = {**new_metadata, "updated_at": now}
95
+
96
+ ts_pred = LteOrAbsent(old_ts) if old_ts is not None else AbsentOrNull()
97
+ store = self._doc_store
98
+ matched = store.update(
99
+ A2A_AGENTS_COLLECTION,
100
+ Filter.of(id=id, version=version).where("metadata.updated_at", ts_pred),
101
+ UpdateSpec(set={"metadata": new_metadata}),
102
+ )
103
+
104
+ if matched == 0:
105
+ if store.find_one(A2A_AGENTS_COLLECTION, Filter.of(id=id, version=version)) is None:
106
+ raise NotFoundError(f"A2AAgent {id} version={version} not found")
107
+ raise ConcurrentModificationError(f"A2AAgent {id} version={version} metadata was modified concurrently")
108
+
109
+ return new_metadata
110
+
111
+ def _deserialize(self, doc: dict) -> A2AAgent:
112
+ from agent_env.a2a_agent.a2a_agent import A2AAgent
113
+
114
+ doc.pop("_id", None)
115
+ doc.pop("created_at_utc", None)
116
+ return A2AAgent.from_dict(doc)
117
+
118
+ def execute_query(self, query: A2AAgentQuery) -> list[A2AAgent]:
119
+ filt, sort = to_document_query(query)
120
+ return self._versioned.query(filt, sort, query._limit_value, query._offset_value)
121
+
122
+ def execute_count(self, query: A2AAgentQuery) -> int:
123
+ filt, _ = to_document_query(query)
124
+ return self._versioned.count(filt)
125
+
126
+
127
+ _a2a_agent_store: Optional[A2AAgentStore] = None
128
+
129
+
130
+ def get_a2a_agent_store() -> A2AAgentStore:
131
+ global _a2a_agent_store
132
+ if _a2a_agent_store is None:
133
+ _a2a_agent_store = A2AAgentStore()
134
+ return _a2a_agent_store
135
+
136
+
137
+ def set_a2a_agent_store(store: A2AAgentStore) -> None:
138
+ global _a2a_agent_store
139
+ _a2a_agent_store = store
140
+
141
+
142
+ def reset_a2a_agent_store() -> None:
143
+ global _a2a_agent_store
144
+ _a2a_agent_store = None
145
+
146
+
147
+ class A2AAgentInstanceStore:
148
+ def __init__(self) -> None:
149
+ self._indexed = None
150
+
151
+ @property
152
+ def _doc_store(self):
153
+ # Resolved per call: a cached store outlives reset_config(), so a process that
154
+ # re-pointed would read the new config and write the old backend.
155
+ store = get_config().get_document_store()
156
+ if self._indexed is not store:
157
+ store.ensure_index(A2A_AGENT_INSTANCES_COLLECTION, ["instance_id"], unique=True)
158
+ self._indexed = store
159
+ return store
160
+
161
+ def create_instance(self, deployed: "DeployedA2AAgent", ttl_seconds: int) -> "DeployedA2AAgent":
162
+ from agent_env.a2a_agent.a2a_agent import DeployedA2AAgent
163
+ suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
164
+ instance_id = f"{deployed.agent_id}-{suffix}"
165
+ now = datetime.now(timezone.utc)
166
+ created_at_utc = now.strftime("%Y-%m-%d %H:%M UTC")
167
+ expires_at_utc = (now + timedelta(seconds=ttl_seconds)).strftime("%Y-%m-%d %H:%M UTC")
168
+ doc = dataclasses.asdict(deployed) | {
169
+ "instance_id": instance_id,
170
+ "created_at_utc": created_at_utc,
171
+ "expires_at_utc": expires_at_utc,
172
+ }
173
+ self._doc_store.insert(A2A_AGENT_INSTANCES_COLLECTION, doc)
174
+ return DeployedA2AAgent.from_dict(doc)
175
+
176
+ def get(self, instance_id: str) -> "DeployedA2AAgent":
177
+ from agent_env.a2a_agent.a2a_agent import DeployedA2AAgent
178
+ doc = self._doc_store.find_one(A2A_AGENT_INSTANCES_COLLECTION, Filter.of(instance_id=instance_id))
179
+ if not doc:
180
+ raise NotFoundError(f"A2AAgentInstance '{instance_id}' not found")
181
+ return DeployedA2AAgent.from_dict(doc)
182
+
183
+
184
+ _a2a_agent_instance_store: Optional[A2AAgentInstanceStore] = None
185
+
186
+
187
+ def get_a2a_agent_instance_store() -> A2AAgentInstanceStore:
188
+ global _a2a_agent_instance_store
189
+ if _a2a_agent_instance_store is None:
190
+ _a2a_agent_instance_store = A2AAgentInstanceStore()
191
+ return _a2a_agent_instance_store
192
+
193
+
194
+ def set_a2a_agent_instance_store(store: A2AAgentInstanceStore) -> None:
195
+ global _a2a_agent_instance_store
196
+ _a2a_agent_instance_store = store
197
+
198
+
199
+ def reset_a2a_agent_instance_store() -> None:
200
+ global _a2a_agent_instance_store
201
+ _a2a_agent_instance_store = None