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,129 @@
1
+ """TaskStep store for MongoDB persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import datetime, timezone
7
+ from typing import TYPE_CHECKING, Optional, Self, TypeVar
8
+
9
+ from agent_env.plugins import _registration
10
+ from agent_env.store.base import NotFoundError
11
+ from agent_env.config import get_config
12
+ from agent_env.store.document_store import VersionedEntityStore, VersionedEntityStoreCache
13
+ from agent_env.store.query import QueryBuilder, to_document_query
14
+
15
+ if TYPE_CHECKING:
16
+ from agent_env.task_step.task_step import TaskStep
17
+
18
+ _S = TypeVar("_S", bound="TaskStep")
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ TASK_STEPS_COLLECTION = "task_steps"
23
+
24
+
25
+ class TaskStepQuery(QueryBuilder["TaskStep"]):
26
+ def __init__(self, store: Optional["TaskStepStore"] = None) -> None:
27
+ super().__init__()
28
+ self._store = store
29
+
30
+ def _clone(self) -> Self:
31
+ clone = TaskStepQuery(self._store)
32
+ clone._filters = self._filters.copy()
33
+ clone._sort_field = self._sort_field
34
+ clone._sort_desc = self._sort_desc
35
+ clone._limit_value = self._limit_value
36
+ clone._offset_value = self._offset_value
37
+ return clone
38
+
39
+ def type(self, task_step_type: str) -> Self:
40
+ return self._add_filter("type", task_step_type)
41
+
42
+ def execute(self) -> list[TaskStep]:
43
+ if self._store is None:
44
+ self._store = get_task_step_store()
45
+ return self._store.execute_query(self)
46
+
47
+ def _execute_count(self) -> int:
48
+ if self._store is None:
49
+ self._store = get_task_step_store()
50
+ return self._store.execute_count(self)
51
+
52
+
53
+ class TaskStepStore:
54
+ def __init__(self) -> None:
55
+ self._versioned_cache: VersionedEntityStoreCache[TaskStep] = VersionedEntityStoreCache(
56
+ TASK_STEPS_COLLECTION, self._serialize, self._deserialize, secondary_indexes=[["type"]],
57
+ )
58
+
59
+ @property
60
+ def _doc_store(self):
61
+ return get_config().get_document_store()
62
+
63
+ @property
64
+ def _versioned(self) -> VersionedEntityStore[TaskStep]:
65
+ return self._versioned_cache.for_store(self._doc_store)
66
+
67
+ def _serialize(self, task_step: TaskStep) -> dict:
68
+ doc = task_step.to_dict()
69
+ doc["created_at_utc"] = datetime.now(timezone.utc)
70
+ return doc
71
+
72
+ def get(self, id: str, version: Optional[int] = None) -> TaskStep:
73
+ task_step = self._versioned.get(id, version)
74
+ if task_step is None:
75
+ version_str = f" version={version}" if version is not None else ""
76
+ raise NotFoundError(f"TaskStep {id}{version_str} not found")
77
+ return task_step
78
+
79
+ def next_version(self, id: str) -> int:
80
+ return self._versioned.next_version(id)
81
+
82
+ def put_document(self, task_step: _S) -> _S:
83
+ task_step.version = self._versioned.put(task_step)
84
+ return task_step
85
+
86
+ def _deserialize(self, doc: dict) -> TaskStep:
87
+ from agent_env.task_step.registry import get_task_step_registry
88
+
89
+ doc.pop("_id", None)
90
+ doc.pop("created_at_utc", None)
91
+ task_step_type = doc["type"]
92
+ registry = get_task_step_registry()
93
+ cls = registry.get(task_step_type)
94
+ if cls is None:
95
+ raise ValueError(
96
+ f"Unknown task step type: {task_step_type}"
97
+ f"{_registration.failure_note(_registration.TASK_STEPS, task_step_type)}"
98
+ )
99
+ from agent_env.task_step.task_step import attach_retry_config
100
+
101
+ return attach_retry_config(cls.from_dict(doc), doc)
102
+
103
+ def execute_query(self, query: TaskStepQuery) -> list[TaskStep]:
104
+ filt, sort = to_document_query(query)
105
+ return self._versioned.query(filt, sort, query._limit_value, query._offset_value)
106
+
107
+ def execute_count(self, query: TaskStepQuery) -> int:
108
+ filt, _ = to_document_query(query)
109
+ return self._versioned.count(filt)
110
+
111
+
112
+ _task_step_store: Optional[TaskStepStore] = None
113
+
114
+
115
+ def get_task_step_store() -> TaskStepStore:
116
+ global _task_step_store
117
+ if _task_step_store is None:
118
+ _task_step_store = TaskStepStore()
119
+ return _task_step_store
120
+
121
+
122
+ def set_task_step_store(store: TaskStepStore) -> None:
123
+ global _task_step_store
124
+ _task_step_store = store
125
+
126
+
127
+ def reset_task_step_store() -> None:
128
+ global _task_step_store
129
+ _task_step_store = None
@@ -0,0 +1,173 @@
1
+ """Base TaskStep model for AgentEnv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Self
9
+
10
+ from agent_env.task_step.context import TaskStepContext
11
+ from agent_env.entity_refs import EntityRef
12
+
13
+ if TYPE_CHECKING:
14
+ from agent_env.task_step.store import TaskStepQuery
15
+
16
+
17
+ @dataclass
18
+ class TaskStepDependency:
19
+ task_step_id: str
20
+
21
+ @classmethod
22
+ def from_dict(cls, data: dict[str, Any]) -> "TaskStepDependency":
23
+ return cls(task_step_id=data["task_step_id"])
24
+
25
+
26
+ def attach_retry_config(step: "TaskStep", data: dict[str, Any]) -> "TaskStep":
27
+ """Hydrate ``retry_config`` after ``step_cls.from_dict`` at the generic
28
+ parse sites, so the field survives round-trips on step types whose
29
+ ``__init__`` doesn't accept it. No-op if already set or absent."""
30
+ raw = data.get("retry_config")
31
+ if raw is not None and getattr(step, "retry_config", None) is None:
32
+ step.retry_config = RetryConfig.from_dict(raw)
33
+ return step
34
+
35
+
36
+ @dataclass
37
+ class RetryConfig:
38
+ """Where a step resumes from on a retry, and how many retries to allow.
39
+
40
+ ``retry_from_step_id`` is required. Set to the step itself it re-runs only
41
+ this step. Point it at an earlier dependency ancestor to roll the world back
42
+ to there and re-dispatch the span (``retry_from_step_id`` .. failed step) as
43
+ ordinary steps on a freshly redeployed world (see ``Task._drive_dag``). The
44
+ task author owns the resume point; the platform does not decide which steps
45
+ are safe to re-run.
46
+
47
+ ``max_retries`` (default 1) bounds how many times a step may trigger this
48
+ rollback-and-re-dispatch before the failure is treated as terminal."""
49
+
50
+ retry_from_step_id: str
51
+ max_retries: int = 1
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: dict[str, Any]) -> "RetryConfig":
55
+ # go_to_step_id = wire alias; retry_from_step_id canonical (only spelling emitted).
56
+ step_id = data.get("retry_from_step_id", data.get("go_to_step_id"))
57
+ if step_id is None:
58
+ raise ValueError("retry_config requires 'retry_from_step_id' (or 'go_to_step_id')")
59
+ max_retries = data.get("max_retries", 1)
60
+ # bool is an int subclass; reject it and non-ints so a bad value fails at
61
+ # parse time, not mid-run inside the scheduler's retry-budget comparison.
62
+ if isinstance(max_retries, bool) or not isinstance(max_retries, int):
63
+ raise ValueError(f"retry_config.max_retries must be an int, got {max_retries!r}")
64
+ if max_retries < 0:
65
+ raise ValueError(f"retry_config.max_retries must be >= 0, got {max_retries}")
66
+ return cls(retry_from_step_id=step_id, max_retries=max_retries)
67
+
68
+
69
+ class TaskStep(ABC):
70
+ """Base class for all task steps. Immutable - each modification creates a new version."""
71
+
72
+ type: ClassVar[str] = "task_step"
73
+ DEFAULT_TTL_SECONDS: ClassVar[int] = 7200
74
+ DEFAULT_AGENT_NAME: ClassVar[str] = "default-agent"
75
+ # Fields holding env, A2A agent or artifact ids; None means undeclared.
76
+ entity_refs: ClassVar[tuple[EntityRef, ...] | None] = None
77
+
78
+ def __init_subclass__(cls, **kwargs) -> None:
79
+ super().__init_subclass__(**kwargs)
80
+ # Never inherited: a subclass may add id fields its parent's declaration misses.
81
+ if "entity_refs" not in vars(cls):
82
+ cls.entity_refs = None
83
+
84
+ def __init__(
85
+ self,
86
+ id: str,
87
+ version: Optional[int],
88
+ depends_on: Optional[list] = None,
89
+ fail_task_on_error: bool = True,
90
+ retry_config: Optional["RetryConfig | dict"] = None,
91
+ ):
92
+ self.id = id
93
+ self.version = version
94
+ if depends_on is not None:
95
+ depends_on = [
96
+ d if isinstance(d, TaskStepDependency) else TaskStepDependency.from_dict(d)
97
+ for d in depends_on
98
+ ]
99
+ self.depends_on = depends_on
100
+ self.fail_task_on_error = fail_task_on_error
101
+ if retry_config is not None and not isinstance(retry_config, RetryConfig):
102
+ retry_config = RetryConfig.from_dict(retry_config)
103
+ self.retry_config = retry_config
104
+
105
+ @abstractmethod
106
+ async def execute(self, context: TaskStepContext) -> TaskStepContext | None:
107
+ """Execute this task step."""
108
+ pass
109
+
110
+ def preflight(self) -> list[str]:
111
+ """Config problems detectable before a run, one message each; empty when fine.
112
+
113
+ Overrides must not need a sandbox or a deployed env.
114
+ """
115
+ return []
116
+
117
+ def step_param_overrides(self, context: TaskStepContext) -> dict[str, Any]:
118
+ """Per-run overrides for this step's params, from a run's ``step_overrides``.
119
+
120
+ Generic seam: a step opts in by reading the params it supports; an
121
+ override wins over the stored value. Empty dict when none set.
122
+ """
123
+ return (context.metadata.get("user_overrides") or {}).get("step_params", {}).get(self.id) or {}
124
+
125
+ def to_dict(self) -> dict[str, Any]:
126
+ base: dict[str, Any] = {"id": self.id, "type": self.type, "version": self.version, "fail_task_on_error": self.fail_task_on_error}
127
+ if self.depends_on is not None:
128
+ base["depends_on"] = [dataclasses.asdict(d) for d in self.depends_on]
129
+ if self.retry_config is not None:
130
+ base["retry_config"] = dataclasses.asdict(self.retry_config)
131
+ return base
132
+
133
+ @classmethod
134
+ def _base_from_dict(cls, data: dict) -> dict:
135
+ raw = data.get("depends_on")
136
+ depends_on = (
137
+ [TaskStepDependency.from_dict(d) for d in raw] if raw is not None else None
138
+ )
139
+ return {
140
+ "id": data["id"], "version": data.get("version"),
141
+ "depends_on": depends_on,
142
+ "fail_task_on_error": data.get("fail_task_on_error", True),
143
+ }
144
+
145
+ @classmethod
146
+ def from_dict(cls, data: dict[str, Any]) -> "TaskStep":
147
+ raise NotImplementedError(f"{cls.__name__} must implement from_dict")
148
+
149
+ @classmethod
150
+ def get(cls, id: str, version: Optional[int] = None) -> "TaskStep":
151
+ from .store import get_task_step_store
152
+
153
+ return get_task_step_store().get(id, version)
154
+
155
+ @classmethod
156
+ def put(cls, **kwargs: Any) -> Self:
157
+ from .store import get_task_step_store
158
+
159
+ kwargs.setdefault("version", None)
160
+ # Most step __init__s don't accept retry_config (it lives on the base but
161
+ # subclasses don't forward it); construct without it, then hydrate so a
162
+ # retry_config on any step type round-trips through the step store.
163
+ retry_config_raw = kwargs.pop("retry_config", None)
164
+ instance = cls(**kwargs)
165
+ if retry_config_raw is not None:
166
+ attach_retry_config(instance, {"retry_config": retry_config_raw})
167
+ return get_task_step_store().put_document(instance)
168
+
169
+ @classmethod
170
+ def query(cls) -> "TaskStepQuery":
171
+ from .store import TaskStepQuery, get_task_step_store
172
+
173
+ return TaskStepQuery(get_task_step_store())
@@ -0,0 +1,27 @@
1
+ from .mcp_cli_builder.build_mcp_cli import BuildMcpCliTaskStep
2
+ from .deploy_agent import DeployAgentTaskStep
3
+ from .deploy_env import DeployEnvTaskStep
4
+ from .verifiers.env_outcome_verifier import EnvOutcomeVerifierTaskStep, ScoreAggregator
5
+ from .load_artifact import LoadArtifactTaskStep
6
+ from .modify_env_tool_access import ModifyEnvToolAccessStep
7
+ from .prompt_agent import PromptAgentTaskStep
8
+ from .verifiers.rubrics_verifier import RubricsVerifierTaskStep
9
+ from .mcp_env_validator.verify_mcp_env_assessment import VerifyMCPEnvAssessmentStep
10
+ from .mcp_env_validator.verify_mcp_tool_schema import VerifyMCPToolSchemaTaskStep
11
+ from .mcp_env_validator.verify_spec_conformance import VerifySpecConformanceTaskStep
12
+ from .mcp_env_validator.validation_gate_aggregator import ValidationGateAggregatorStep
13
+ from .multienv_validator.verify_universe_roundtrip import VerifyUniverseLoadExportRoundtripStep
14
+ from .multienv_validator.combine_universe_verdicts import CombineUniverseVerdictsStep
15
+ from .env_card_validator.verify_env_card import VerifyEnvironmentCardStep
16
+ from .env_card_validator.verify_env_core_protocol import VerifyCoreEnvironmentProtocolStep
17
+ from .a2a_agent_validator.verify_a2a_agent_card import VerifyA2AAgentCardStep
18
+ from .a2a_agent_validator.verify_a2a_agent_mcp import VerifyA2AAgentMCPStep
19
+ from .a2a_agent_validator.verify_a2a_core_protocol import VerifyCoreA2AProtocolStep
20
+ from .a2a_agent_validator.verify_a2a_skill_config import VerifyA2ASkillConfigStep
21
+ from .a2a_agent_validator.verify_a2a_trajectory import VerifyA2ATrajectoryStep
22
+ from .add_skills import AddSkillsTaskStep
23
+ from .apply_server_config import ApplyServerConfigStep, ConfigDirective
24
+ from .verifiers.agent_prompt_response_verifier import AgentPromptResponseVerifierTaskStep
25
+
26
+ __all__ = ["AddSkillsTaskStep", "ApplyServerConfigStep", "ConfigDirective", "AgentPromptResponseVerifierTaskStep", "BuildMcpCliTaskStep",
27
+ "DeployAgentTaskStep", "DeployEnvTaskStep", "EnvOutcomeVerifierTaskStep", "LoadArtifactTaskStep", "ModifyEnvToolAccessStep", "PromptAgentTaskStep", "RubricsVerifierTaskStep", "VerifyMCPEnvAssessmentStep", "VerifyMCPToolSchemaTaskStep", "VerifySpecConformanceTaskStep", "ValidationGateAggregatorStep", "VerifyUniverseLoadExportRoundtripStep", "CombineUniverseVerdictsStep", "VerifyEnvironmentCardStep", "VerifyCoreEnvironmentProtocolStep", "VerifyA2AAgentCardStep", "VerifyA2AAgentMCPStep", "VerifyCoreA2AProtocolStep", "VerifyA2ASkillConfigStep", "VerifyA2ATrajectoryStep", "ScoreAggregator"]
@@ -0,0 +1,31 @@
1
+ from .verify_a2a_agent_card import VerifyA2AAgentCardStep
2
+ from .verify_a2a_agent_config_identity import VerifyA2AAgentConfigIdentityStep
3
+ from .verify_a2a_agent_mcp import VerifyA2AAgentMCPStep
4
+ from .verify_a2a_core_protocol import VerifyCoreA2AProtocolStep
5
+ from .verify_a2a_install import VerifyA2AInstallStep
6
+ from .verify_a2a_litellm_attribution import VerifyA2ALitellmAttributionStep
7
+ from .verify_a2a_litellm_attribution_runtime import VerifyA2ALitellmAttributionRuntimeStep
8
+ from .verify_a2a_modalities import VerifyA2AModalitiesStep
9
+ from .verify_a2a_peer_agents import VerifyA2APeerAgentsStep
10
+ from .verify_a2a_role import VerifyA2ARoleStep
11
+ from .verify_a2a_skill_config import VerifyA2ASkillConfigStep
12
+ from .verify_a2a_snapshot import VerifyA2ASnapshotStep
13
+ from .verify_a2a_system_prompt import VerifyA2ASystemPromptStep
14
+ from .verify_a2a_trajectory import VerifyA2ATrajectoryStep
15
+
16
+ __all__ = [
17
+ "VerifyA2AAgentCardStep",
18
+ "VerifyA2AAgentConfigIdentityStep",
19
+ "VerifyA2AAgentMCPStep",
20
+ "VerifyCoreA2AProtocolStep",
21
+ "VerifyA2AInstallStep",
22
+ "VerifyA2ALitellmAttributionStep",
23
+ "VerifyA2ALitellmAttributionRuntimeStep",
24
+ "VerifyA2AModalitiesStep",
25
+ "VerifyA2APeerAgentsStep",
26
+ "VerifyA2ARoleStep",
27
+ "VerifyA2ASkillConfigStep",
28
+ "VerifyA2ASnapshotStep",
29
+ "VerifyA2ASystemPromptStep",
30
+ "VerifyA2ATrajectoryStep",
31
+ ]
@@ -0,0 +1,158 @@
1
+ # Modality probe fixtures
2
+
3
+ Binary fixtures used by the A2A agent validator's modality probes. Each
4
+ file is loaded at import time via `_b64(name)` in `verify_a2a_modalities.py`
5
+ and exposed as a `*_B64` constant. The same constants get assembled into
6
+ `*_PROBE_PARTS` lists for the inline-bytes path, and the raw bytes are also
7
+ read directly for the URI-based S3 upload path.
8
+
9
+ | File | Probe modality | What it contains | Why |
10
+ |---|---|---|---|
11
+ | `red.png` | `image/png` | 8×8 solid red square | smallest valid red png (75 B) |
12
+ | `red.jpg` | `image/jpeg` | 8×8 solid red square | tests separate JPEG decode path |
13
+ | `red.gif` | `image/gif` | 8×8 solid red square | tests separate GIF decode path |
14
+ | `clip.wav` | `audio/wav` | spoken `"Say the word: banana"` | tests PCM decode + transcription|
15
+ | `clip.mp3` | `audio/mpeg` | spoken `"Say the word: telephone"` | tests MP3 decode + transcription|
16
+ | `clip.m4a` | `audio/mp4` | spoken `"Say the word: bicycle"` | tests AAC decode + transcription|
17
+ | `clip.ogg` | `audio/ogg` | spoken `"Say the word: rainbow"` | tests Opus decode + transcription|
18
+ | `document.pdf` | `application/pdf` | one-page instructional doc, target `ELEPHANT` | tests PDF text extraction |
19
+ | `clip.mp4` | `video/mp4` | 3 s clip showing `GIRAFFE` on white | tests video-frame OCR; URI-delivered |
20
+
21
+ ## Design choices that surface as gotchas
22
+
23
+ ### Distinct target words per audio format
24
+ Each audio file says a *different* word — the validator uses
25
+ `AUDIO_WAV_EXPECTED = "banana"`, `AUDIO_MP3_EXPECTED = "telephone"`, etc.
26
+ If every probe expected the same word, a model could pass by outputting
27
+ that word from any cue (filename, bias, the prompt). Distinct expected
28
+ values per format means the model has to actually decode each clip.
29
+
30
+ ### Neutral filenames
31
+ All fixtures are named `clip.<ext>` / `red.<ext>` / `document.pdf` rather
32
+ than `banana.<ext>` / `red-square.png` etc. A leaky filename lets the model
33
+ answer from the path the wrapper materializes (`@/app/inputs/<task>/X`)
34
+ without ever decoding the bytes. We caught this when an early probe with
35
+ `banana.mp3` was "passing" via `ls banana.mp3` + filename inference.
36
+
37
+ ### Audio encoding details
38
+ - **16 kHz mono is the format Gemini accepts reliably.** An earlier 8 kHz
39
+ WAV failed even though `audio/wav` is in the gemini-cli MIME table.
40
+ - **`Alex` voice transcribes more reliably than the default `say` voice.**
41
+ With the default voice the model occasionally heard "banana" as "apple".
42
+ - **3× volume boost + 200 ms silence padding raise transcription
43
+ accuracy from ~70% to ~100%.** Without them the model sometimes guessed
44
+ unrelated words ("visual", "red") when it couldn't lock onto the
45
+ utterance.
46
+
47
+ ### PDF embeds the target word inside an instructional paragraph
48
+ `document.pdf` doesn't just show `ELEPHANT` alone — it shows a short
49
+ paragraph naming `ELEPHANT` as the target word. Bare word alone tripped
50
+ letter-truncation edge cases (the model occasionally reading "ELEPHANT"
51
+ as "ANT" or "PHANT", probably from letter-spacing artifacts in the PDF
52
+ text extractor).
53
+
54
+ ### Video shows a static frame, not motion
55
+ `clip.mp4` is a static frame on a white background (480×360 @ 15 fps, h.264
56
+ CRF 28, no audio track). It tests video-frame OCR, not temporal
57
+ understanding. The static design plus on-screen instructional context plus
58
+ the larger 480×360 frame size were each needed: smaller / barer clips
59
+ truncated to "FFE" probabilistically.
60
+
61
+ ### Why video is URI-only
62
+ The video fixture is delivered via `FileWithUri` (`s3://`), not inline
63
+ `FileWithBytes` like the others. At 8.9 KB it would fit inline, but the
64
+ validator deliberately exercises the wrapper's URI-materialization code
65
+ path with this probe — it's the only fixture that does so for video, and
66
+ one of three (alongside the two URI-delivered PNG probes) that exercise
67
+ URI delivery at all.
68
+
69
+ ## Regenerating fixtures
70
+
71
+ These are *not* runtime dependencies. They're only needed if you want to
72
+ re-create or modify a fixture. Install with `pip install Pillow fpdf2`.
73
+ You'll also need `ffmpeg` and the `flac` CLI on PATH (`brew install
74
+ ffmpeg flac` on macOS).
75
+
76
+ ### Audio (`clip.wav`, `clip.mp3`, `clip.m4a`, `clip.ogg`)
77
+
78
+ ```bash
79
+ FF_FILTER="silenceremove=start_periods=1:start_duration=0:start_threshold=-50dB:detection=peak,areverse,silenceremove=start_periods=1:start_duration=0:start_threshold=-50dB:detection=peak,areverse,volume=3.0,adelay=200|200,apad=pad_dur=0.2"
80
+
81
+ # Per-format: substitute the target word, re-encode in native format.
82
+ say -v Alex -r 150 "Say the word: Banana" -o /tmp/x.aiff
83
+ ffmpeg -y -i /tmp/x.aiff -ac 1 -ar 16000 -map_metadata -1 -fflags +bitexact \
84
+ -af "$FF_FILTER" -acodec pcm_s16le fixtures/clip.wav
85
+
86
+ # MP3 / M4A / OGG: encode from the freshly-trimmed WAV
87
+ ffmpeg -y -i fixtures/clip.wav -ac 1 -ar 16000 -b:a 64k -map_metadata -1 \
88
+ -fflags +bitexact -write_xing 1 fixtures/clip.mp3
89
+ ffmpeg -y -i fixtures/clip.wav -c:a aac -b:a 64k -ac 1 -ar 16000 \
90
+ -map_metadata -1 -fflags +bitexact fixtures/clip.m4a
91
+ ffmpeg -y -i fixtures/clip.wav -c:a libopus -b:a 32k -ac 1 -ar 16000 \
92
+ -map_metadata -1 -fflags +bitexact fixtures/clip.ogg
93
+ ```
94
+
95
+ If updating the target word for a format, also update the corresponding
96
+ `AUDIO_<FMT>_EXPECTED` constant in `verify_a2a_modalities.py`.
97
+
98
+ ### PDF (`document.pdf`)
99
+
100
+ ```python
101
+ # requires: pip install fpdf2
102
+ from fpdf import FPDF
103
+ pdf = FPDF(orientation='P', unit='pt', format='A4')
104
+ pdf.set_compression(True); pdf.set_creator(''); pdf.set_producer('')
105
+ pdf.add_page()
106
+ pdf.set_font('Helvetica', '', 36)
107
+ pdf.set_xy(50, 100)
108
+ pdf.multi_cell(500, 50,
109
+ 'A2A Modality Probe Document\n\n'
110
+ 'This page contains exactly one target word that the reader should report.\n\n'
111
+ 'The target word is: ELEPHANT\n\n'
112
+ 'Reply with only the word above.',
113
+ align='L')
114
+ pdf.output('fixtures/document.pdf')
115
+ ```
116
+
117
+ ### Video (`clip.mp4`)
118
+
119
+ ```python
120
+ # requires: pip install Pillow; brew install ffmpeg
121
+ from PIL import Image, ImageDraw, ImageFont
122
+ img = Image.new('RGB', (480, 360), 'white')
123
+ draw = ImageDraw.Draw(img)
124
+ big = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 80)
125
+ small = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 22)
126
+ draw.text((30, 30), "A2A Video Probe", font=small, fill='black')
127
+ draw.text((30, 60), "Target word:", font=small, fill='black')
128
+ bbox = draw.textbbox((0, 0), "GIRAFFE", font=big); w = bbox[2] - bbox[0]
129
+ draw.text(((480 - w) / 2, 150), "GIRAFFE", font=big, fill='black')
130
+ draw.text((30, 280), "Reply with only the target word.", font=small, fill='black')
131
+ img.save('/tmp/frame.png')
132
+ ```
133
+ ```bash
134
+ ffmpeg -y -loop 1 -i /tmp/frame.png -t 3 -r 15 \
135
+ -c:v libx264 -preset slow -crf 28 -pix_fmt yuv420p -movflags +faststart \
136
+ fixtures/clip.mp4
137
+ ```
138
+
139
+ ### Images (`red.png`, `red.jpg`, `red.gif`)
140
+
141
+ These are static and committed once. To regenerate, encode an 8×8 solid
142
+ red square via Pillow or any image tool — the existing files are about
143
+ as small as their formats allow.
144
+
145
+ ## Listening / previewing fixtures
146
+
147
+ ```bash
148
+ # Audio (afplay handles wav/mp3/m4a; ffplay for ogg)
149
+ afplay fixtures/clip.wav
150
+ ffplay -nodisp -autoexit fixtures/clip.ogg
151
+
152
+ # PDF / video
153
+ qlmanage -p fixtures/document.pdf
154
+ open fixtures/clip.mp4
155
+
156
+ # Or open the whole directory in Finder and hit space for QuickLook
157
+ open fixtures/
158
+ ```
@@ -0,0 +1,103 @@
1
+ """Verify A2A agent card structure and persist to agent metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import ClassVar, Optional
7
+
8
+ from agent_env.task_step.context import TaskStepContext
9
+ from agent_env.entity_refs import EntityRef
10
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ REQUIRED_CARD_FIELDS = ["name", "description", "url", "version", "capabilities", "defaultInputModes", "defaultOutputModes", "skills"]
15
+ OPTIONAL_CARD_FIELDS = ["authentication", "provider", "documentationUrl"]
16
+
17
+
18
+ class VerifyA2AAgentCardStep(TaskStep):
19
+ type: ClassVar[str] = "verify_a2a_agent_card"
20
+ entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
21
+
22
+ def __init__(
23
+ self,
24
+ id: str,
25
+ version: Optional[int],
26
+ a2a_agent_id: str,
27
+ a2a_agent_version: Optional[int] = None,
28
+ depends_on: Optional[list[TaskStepDependency]] = None,
29
+ fail_task_on_error: bool = True,
30
+ ):
31
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
32
+ self.a2a_agent_id = a2a_agent_id
33
+ self.a2a_agent_version = a2a_agent_version
34
+
35
+ def to_dict(self) -> dict:
36
+ base = super().to_dict()
37
+ base["a2a_agent_id"] = self.a2a_agent_id
38
+ base["a2a_agent_version"] = self.a2a_agent_version
39
+ return base
40
+
41
+ @classmethod
42
+ def from_dict(cls, data: dict) -> VerifyA2AAgentCardStep:
43
+ return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], a2a_agent_version=data.get("a2a_agent_version"))
44
+
45
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
46
+ from agent_env.a2a_agent import A2AAgent
47
+
48
+ deployed = next((a for a in context.deployed_agents), None)
49
+ if deployed is None:
50
+ raise RuntimeError("No deployed agent found in context")
51
+
52
+ card = deployed.a2a_card
53
+ agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
54
+
55
+ # Validate card is accessible
56
+ if not card:
57
+ logger.error("Agent card is inaccessible or empty")
58
+ validated = {"accessible": False, "error": "Inaccessible Agent Card"}
59
+ agent.update_metadata({**agent.metadata, "validated_agent_card": validated})
60
+ raise RuntimeError("Inaccessible Agent Card: agent did not return a valid agent card at /.well-known/agent.json")
61
+
62
+ logger.info(f"Agent card for '{self.a2a_agent_id}': name={card.get('name')}")
63
+
64
+ # Check required fields
65
+ required_fields = {}
66
+ missing_required = []
67
+ for field in REQUIRED_CARD_FIELDS:
68
+ value = card.get(field)
69
+ present = field in card and (
70
+ bool(value) or (field == "skills" and value == [])
71
+ )
72
+ required_fields[field] = {"present": present}
73
+ if not present:
74
+ missing_required.append(field)
75
+
76
+ if missing_required:
77
+ logger.warning(f"Agent card missing required fields: {missing_required}")
78
+ else:
79
+ logger.info("Agent card has all required fields")
80
+
81
+ # Check optional fields
82
+ optional_fields = {}
83
+ missing_optional = []
84
+ for field in OPTIONAL_CARD_FIELDS:
85
+ present = field in card and bool(card[field])
86
+ optional_fields[field] = {"present": present}
87
+ if not present:
88
+ missing_optional.append(field)
89
+
90
+ if missing_optional:
91
+ logger.info(f"Agent card missing optional fields: {missing_optional}")
92
+
93
+ # Log which extensions the card advertises
94
+ for ext_uri in [A2AAgent.EXT_MCP_CONFIG, A2AAgent.EXT_SKILL_CONFIG, A2AAgent.EXT_TRAJECTORY, A2AAgent.EXT_AGENT_CONFIG, A2AAgent.EXT_TOOLS]:
95
+ ext = A2AAgent.find_extension(card, ext_uri)
96
+ logger.info(f" {ext_uri}: {'advertised' if ext else 'not advertised'}")
97
+
98
+ # Persist agent_card (raw) and validated_agent_card (our validation)
99
+ validated = {"accessible": True, "required_fields": required_fields, "optional_fields": optional_fields}
100
+ agent.update_metadata({**agent.metadata, "agent_card": card, "validated_agent_card": validated})
101
+
102
+ context.metadata.setdefault("verifications", {})["a2a_agent_card"] = {"agent_card": card, "validated_agent_card": validated}
103
+ return context