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,132 @@
1
+ """Arm a deployed env gateway's virtual clock, then sync its backing servers to it.
2
+
3
+ Arms the clock (set_time) and reads the gateway's server-reachable env_get_time_url (state), both as the
4
+ stored env card declares clock/v1, then invokes sync_time on each backing server whose child env card
5
+ advertises it. Non-advertisers are skipped when tolerate_missing_sync_time (default True). Place right
6
+ before prompt_agent so the virtual timeline starts at agent-start.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import ClassVar, Optional
13
+
14
+ import httpx
15
+ from agentenv_protocol import client as protocol_v1
16
+
17
+ from agent_env.env.gateway.constants import EXT_CLOCK_URI
18
+ from agent_env.task_step.context import TaskStepContext
19
+ from agent_env.entity_refs import EntityRef
20
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class SyncEnvClockTaskStep(TaskStep):
26
+ type: ClassVar[str] = "sync_env_clock"
27
+ entity_refs = (EntityRef.env("env_id"),)
28
+
29
+ def __init__(
30
+ self,
31
+ id: str,
32
+ version: Optional[int],
33
+ env_id: str,
34
+ virtual_time: str,
35
+ virtual_seconds_per_real_second: float = 1.0,
36
+ tolerate_missing_sync_time: bool = True,
37
+ depends_on: Optional[list[TaskStepDependency]] = None,
38
+ fail_task_on_error: bool = True,
39
+ timeout_seconds: int = 30,
40
+ ):
41
+ """virtual_seconds_per_real_second = clock speed (1.0 real time, 0 frozen, 86400 cap =
42
+ 1 real s -> 1 virtual day). For ~V virtual seconds over an ~R-real-second task, pass V/R."""
43
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
44
+ if not isinstance(virtual_time, str):
45
+ raise ValueError("virtual_time must be an RFC3339 string")
46
+ self.env_id = env_id
47
+ self.virtual_time = virtual_time
48
+ self.virtual_seconds_per_real_second = virtual_seconds_per_real_second
49
+ self.tolerate_missing_sync_time = tolerate_missing_sync_time
50
+ self.timeout_seconds = timeout_seconds
51
+
52
+ def to_dict(self) -> dict:
53
+ base = super().to_dict()
54
+ base["env_id"] = self.env_id
55
+ base["virtual_time"] = self.virtual_time
56
+ base["virtual_seconds_per_real_second"] = self.virtual_seconds_per_real_second
57
+ base["tolerate_missing_sync_time"] = self.tolerate_missing_sync_time
58
+ base["timeout_seconds"] = self.timeout_seconds
59
+ return base
60
+
61
+ @classmethod
62
+ def from_dict(cls, data: dict) -> SyncEnvClockTaskStep:
63
+ return cls(
64
+ **cls._base_from_dict(data),
65
+ env_id=data["env_id"],
66
+ virtual_time=data["virtual_time"],
67
+ virtual_seconds_per_real_second=data.get("virtual_seconds_per_real_second", 1.0),
68
+ tolerate_missing_sync_time=data.get("tolerate_missing_sync_time", True),
69
+ timeout_seconds=data.get("timeout_seconds", 30),
70
+ )
71
+
72
+ def _service_names(self, deployed) -> list[str]:
73
+ """Backing MCP server names for the env (mirrors apply_server_config's enumeration)."""
74
+ from agent_env.env.env import Env
75
+ env = Env.get(deployed.env_id, deployed.env_version)
76
+ names = [e.environment_name for e in (getattr(env, "mcp_server_envs", None) or [])]
77
+ if not names and getattr(env, "environment_name", None):
78
+ names = [env.environment_name]
79
+ return names
80
+
81
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
82
+ from agent_env.env.env import gateway_url_of
83
+
84
+ deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
85
+ if deployed is None:
86
+ raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
87
+ try:
88
+ await deployed.invoke(EXT_CLOCK_URI, "set_time",
89
+ {"virtual_time": self.virtual_time, "virtual_seconds_per_real_second": self.virtual_seconds_per_real_second},
90
+ timeout=self.timeout_seconds)
91
+ except httpx.HTTPStatusError as e:
92
+ raise RuntimeError(f"clock arm failed (HTTP {e.response.status_code}): {e.response.text}") from e
93
+ state = await deployed.invoke(EXT_CLOCK_URI, "state", timeout=self.timeout_seconds)
94
+ env_get_time_url = state.get("env_get_time_url")
95
+
96
+ synced, skipped = [], []
97
+ for service in self._service_names(deployed):
98
+ outcome = await self._sync_one(deployed, gateway_url_of(deployed), service, env_get_time_url)
99
+ (synced if outcome.get("synced") else skipped).append({"service": service, "environment": service, **outcome})
100
+
101
+ context.metadata.setdefault("clock_configurations", []).append({
102
+ "step_id": self.id, "env_id": self.env_id,
103
+ "virtual_time": self.virtual_time, "virtual_seconds_per_real_second": self.virtual_seconds_per_real_second,
104
+ "env_get_time_url": env_get_time_url, "synced": synced, "skipped": skipped,
105
+ })
106
+ logger.info(f"sync_env_clock env={self.env_id}: armed rate={self.virtual_seconds_per_real_second}; "
107
+ f"synced={[s['service'] for s in synced]} skipped={[s['service'] for s in skipped]}")
108
+ if not synced:
109
+ logger.warning(f"sync_env_clock env={self.env_id}: clock armed but NO server synced "
110
+ f"({[(s['service'], s.get('reason')) for s in skipped]}) — every tool still answers wall time")
111
+ return context
112
+
113
+ async def _sync_one(self, deployed, gateway_url: str, service: str, env_get_time_url: Optional[str]) -> dict:
114
+ """Invoke sync_time on one server if its card advertises clock/v1; skip (or raise) per the flag."""
115
+ from agent_env.env import legacy_protocol
116
+
117
+ base_url, card = await legacy_protocol.child_env_card(deployed, gateway_url, service, timeout=self.timeout_seconds)
118
+ if card is None:
119
+ if self.tolerate_missing_sync_time:
120
+ return {"synced": False, "reason": "no_env_card"}
121
+ raise RuntimeError(f"{service} has no env card: it serves none, its card isn't named {service!r}, or the deploy couldn't read it")
122
+ if protocol_v1.find_extension(card, EXT_CLOCK_URI) is None:
123
+ if self.tolerate_missing_sync_time:
124
+ logger.info(f"sync_env_clock: {service} does not advertise {EXT_CLOCK_URI}; skipping")
125
+ return {"synced": False, "reason": "not_advertised"}
126
+ raise RuntimeError(f"{service} does not advertise {EXT_CLOCK_URI}")
127
+ if not env_get_time_url:
128
+ raise RuntimeError(f"gateway did not expose env_get_time_url; cannot sync {service}")
129
+ result = await protocol_v1.invoke_extension(base_url, card, EXT_CLOCK_URI,
130
+ params={"env_get_time_url": env_get_time_url},
131
+ timeout=self.timeout_seconds)
132
+ return {"synced": True, "result": result}
@@ -0,0 +1,170 @@
1
+ """Terminate the sandboxes behind named agents, envs and bare sandboxes, mid-run.
2
+
3
+ A long pipeline keeps every sandbox it deployed until the run ends or its TTL expires. Place this
4
+ step after the last step that uses a target (via ``depends_on``) to stop paying for it early.
5
+ Strictly best-effort: a sandbox that is already gone or fails to terminate is logged and the run
6
+ continues. The step never raises on a terminate, so a retry never re-runs it over partly torn-down
7
+ state, and ``fail_task_on_error`` must stay false. The ids it terminated are appended to ``metadata["torn_down_sandbox_ids"]``; the
8
+ ``deployed_*`` records stay, since context list fields persist additions only.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ from typing import Optional
16
+
17
+ from agent_env.entity_refs import EntityRef
18
+ from agent_env.env.env import DeployedSandboxEnv
19
+ from agent_env.providers.sandbox_providers.sandbox_provider import (
20
+ SandboxProvider,
21
+ build_sandbox_provider,
22
+ get_agent_sandbox_provider,
23
+ get_env_sandbox_provider,
24
+ get_sandbox_provider,
25
+ )
26
+ from agent_env.task_step.context import TaskStepContext
27
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ TORN_DOWN_KEY = "torn_down_sandbox_ids"
32
+
33
+
34
+ class TeardownSandboxesTaskStep(TaskStep):
35
+ """Terminate every sandbox behind the named deployed agents, envs and bare sandboxes."""
36
+
37
+ type = "teardown_sandboxes"
38
+ entity_refs = (EntityRef.env("env_ids[]"),)
39
+
40
+ def __init__(
41
+ self,
42
+ id: str,
43
+ version: Optional[int],
44
+ agent_names: Optional[list[str]] = None,
45
+ env_ids: Optional[list[str]] = None,
46
+ sandbox_names: Optional[list[str]] = None,
47
+ depends_on: Optional[list[TaskStepDependency]] = None,
48
+ fail_task_on_error: bool = False,
49
+ ):
50
+ if fail_task_on_error:
51
+ raise ValueError(
52
+ "teardown_sandboxes is best-effort (a failed terminate is logged, never raised); "
53
+ "fail_task_on_error must be false"
54
+ )
55
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
56
+ self.agent_names = list(agent_names or [])
57
+ self.env_ids = list(env_ids or [])
58
+ self.sandbox_names = list(sandbox_names or [])
59
+ if not (self.agent_names or self.env_ids or self.sandbox_names):
60
+ raise ValueError("teardown_sandboxes needs at least one of agent_names, env_ids, sandbox_names")
61
+
62
+ def to_dict(self) -> dict:
63
+ base = super().to_dict()
64
+ base["agent_names"] = self.agent_names
65
+ base["env_ids"] = self.env_ids
66
+ base["sandbox_names"] = self.sandbox_names
67
+ return base
68
+
69
+ @classmethod
70
+ def from_dict(cls, data: dict) -> TeardownSandboxesTaskStep:
71
+ base = cls._base_from_dict(data)
72
+ # Stay best-effort when unset: _base_from_dict would otherwise default it to True.
73
+ base["fail_task_on_error"] = data.get("fail_task_on_error", False)
74
+ return cls(
75
+ **base,
76
+ agent_names=data.get("agent_names"),
77
+ env_ids=data.get("env_ids"),
78
+ sandbox_names=data.get("sandbox_names"),
79
+ )
80
+
81
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
82
+ targets, missing = self._resolve(context)
83
+ if missing:
84
+ logger.warning(f"teardown_sandboxes: not deployed in this run, skipping: {', '.join(missing)}")
85
+ already = set(context.metadata.get(TORN_DOWN_KEY) or [])
86
+ pending = {sid: (stype, default) for sid, (stype, default) in targets.items() if sid not in already}
87
+ results = await asyncio.gather(
88
+ *(_terminate(sid, stype, default) for sid, (stype, default) in pending.items()),
89
+ return_exceptions=True,
90
+ )
91
+ terminated, failed = [], []
92
+ for sandbox_id, result in zip(pending, results):
93
+ if isinstance(result, BaseException):
94
+ failed.append(sandbox_id)
95
+ logger.warning(f"teardown_sandboxes: terminate {sandbox_id} failed: {result!r}"[:300])
96
+ else:
97
+ terminated.append(sandbox_id)
98
+ if terminated:
99
+ context.metadata[TORN_DOWN_KEY] = [*context.metadata.get(TORN_DOWN_KEY, []), *terminated]
100
+ logger.info(
101
+ f"teardown_sandboxes: terminated={len(terminated)} failed={len(failed)} "
102
+ f"already_torn_down={len(targets) - len(pending)} missing={len(missing)}"
103
+ )
104
+ return context
105
+
106
+ def _resolve(
107
+ self, context: TaskStepContext
108
+ ) -> tuple[dict[str, tuple[Optional[str], str]], list[str]]:
109
+ """``{sandbox_id: (sandbox_type, default_slot)}`` for every target, plus unmatched names."""
110
+ targets: dict[str, tuple[Optional[str], str]] = {}
111
+ missing: list[str] = []
112
+ for name in self.agent_names:
113
+ agents = [a for a in context.deployed_agents if a.agent_name == name]
114
+ if not agents:
115
+ missing.append(f"agent {name!r}")
116
+ for agent in agents:
117
+ if agent.sandbox_id:
118
+ targets[agent.sandbox_id] = (agent.sandbox_type, "agent")
119
+ for name in self.sandbox_names:
120
+ sandboxes = [s for s in context.deployed_sandboxes if s.sandbox_name == name]
121
+ if not sandboxes:
122
+ missing.append(f"sandbox {name!r}")
123
+ for sandbox in sandboxes:
124
+ targets[sandbox.sandbox_id] = (sandbox.sandbox_type, "sandbox")
125
+ for env_id in self.env_ids:
126
+ envs = [e for e in context.deployed_envs if e.env_id == env_id]
127
+ if not envs:
128
+ missing.append(f"env {env_id!r}")
129
+ for env in envs:
130
+ targets.update({sid: (stype, "env") for sid, stype in _env_sandbox_ids(env)})
131
+ return targets, missing
132
+
133
+
134
+ def _env_sandbox_ids(env) -> list[tuple[str, Optional[str]]]:
135
+ """Every (sandbox_id, sandbox_type) of a deployed env: its primary sandbox plus ``sandbox_ids``.
136
+
137
+ ``sandbox_ids`` values are an id or a ``{service: id}`` dict and inherit the env's type, unless
138
+ the key itself names a registered backend (a sandbox on another backend than the gateway's).
139
+ """
140
+ if not isinstance(env, DeployedSandboxEnv): # an env outside our sandboxes owns its own lifetime
141
+ return []
142
+ ids = [(env.sandbox_id, env.sandbox_type)] if env.sandbox_id else []
143
+ for key, value in (env.sandbox_ids or {}).items():
144
+ entry_type = key if _is_backend(key) else env.sandbox_type
145
+ values = [value] if isinstance(value, str) else [v for v in value.values() if isinstance(v, str)]
146
+ ids.extend((v, entry_type) for v in values)
147
+ return ids
148
+
149
+
150
+ def _is_backend(name: str) -> bool:
151
+ try:
152
+ build_sandbox_provider(name)
153
+ except Exception:
154
+ return False
155
+ return True
156
+
157
+
158
+ _DEFAULT_PROVIDERS = {
159
+ "agent": get_agent_sandbox_provider,
160
+ "env": get_env_sandbox_provider,
161
+ "sandbox": get_sandbox_provider,
162
+ }
163
+
164
+
165
+ async def _terminate(sandbox_id: str, sandbox_type: Optional[str], default_slot: str) -> None:
166
+ provider: SandboxProvider = (
167
+ build_sandbox_provider(sandbox_type) if sandbox_type else _DEFAULT_PROVIDERS[default_slot]()
168
+ )
169
+ sandbox = await provider.get_sandbox(sandbox_id)
170
+ await sandbox.terminate()
@@ -0,0 +1,23 @@
1
+ """Verifier task steps: the steps that score a run and record a row under
2
+ ``context.metadata["verifications"]``, plus the helpers only they use (score
3
+ aggregation, judge output formats, trajectory compaction and frame selection for
4
+ the LLM judge)."""
5
+
6
+ from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
7
+ from agent_env.task_step.task_steps.verifiers.env_outcome_verifier import EnvOutcomeVerifierTaskStep
8
+ from agent_env.task_step.task_steps.verifiers.rubrics_verifier import RubricsVerifierTaskStep
9
+ from agent_env.task_step.task_steps.verifiers.agent_prompt_response_verifier import AgentPromptResponseVerifierTaskStep
10
+ from agent_env.task_step.task_steps.verifiers.run_container_unit_tests_verifier import RunContainerUnitTestsVerifierTaskStep
11
+ from agent_env.task_step.task_steps.verifiers.verify_sandbox import VerifySandboxTaskStep
12
+ from agent_env.task_step.task_steps.verifiers.aggregate_verifiers import AggregateVerifiersTaskStep
13
+
14
+ __all__ = [
15
+ "AgentPromptResponseVerifierTaskStep",
16
+ "AggregateVerifiersTaskStep",
17
+ "EnvOutcomeVerifierTaskStep",
18
+ "RubricsVerifierTaskStep",
19
+ "RunContainerUnitTestsVerifierTaskStep",
20
+ "ScoreAggregator",
21
+ "VerifySandboxTaskStep",
22
+ "aggregate_score",
23
+ ]
@@ -0,0 +1,166 @@
1
+ """Verify response-side rubric criteria against an agent's prompt response text.
2
+
3
+ Deterministic checks that operate on the agent's final text response — no
4
+ sandbox connection, no LLM judge:
5
+ - response_contains — every needle in `needles` appears in the response
6
+ - response_regex_present — `pattern` (regex) matches the response (re.search)
7
+
8
+ Strict: unknown criterion types and criteria missing their required field
9
+ (empty `needles` for response_contains, empty `pattern` for
10
+ response_regex_present) raise ValueError. A verifier that can't actually
11
+ verify anything is a misconfiguration, not a runtime decision.
12
+
13
+ Result rows merge `{**criterion, **outcome}` and aggregated score is written
14
+ to `context.metadata["verifications"][verifier_id]`, matching the shape used
15
+ by `VerifySandboxTaskStep` and `RubricsVerifierTaskStep`.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import re
21
+ import uuid
22
+ from typing import ClassVar, Optional
23
+
24
+ from agent_env.task_step.context import TaskStepContext
25
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
26
+ from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ _HANDLED_TYPES = {
31
+ "response_contains",
32
+ "response_regex_present",
33
+ }
34
+
35
+
36
+ class AgentPromptResponseVerifierTaskStep(TaskStep):
37
+ type: ClassVar[str] = "agent_prompt_response_verifier"
38
+ entity_refs = ()
39
+
40
+ def __init__(
41
+ self,
42
+ id: str,
43
+ version: Optional[int],
44
+ prompt_id: str,
45
+ criteria: Optional[list[dict]] = None,
46
+ score_aggregator: Optional[ScoreAggregator] = None,
47
+ verifier_id: Optional[str] = None,
48
+ depends_on: Optional[list[TaskStepDependency]] = None,
49
+ fail_task_on_error: bool = True,
50
+ ):
51
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
52
+ self.prompt_id = prompt_id
53
+ self.criteria = list(criteria or [])
54
+ if isinstance(score_aggregator, str):
55
+ score_aggregator = ScoreAggregator(score_aggregator)
56
+ self.score_aggregator = score_aggregator or ScoreAggregator.ALL_PASS
57
+ self.verifier_id = verifier_id or uuid.uuid4().hex
58
+
59
+ def to_dict(self) -> dict:
60
+ base = super().to_dict()
61
+ base["prompt_id"] = self.prompt_id
62
+ base["criteria"] = self.criteria
63
+ base["score_aggregator"] = self.score_aggregator.value
64
+ base["verifier_id"] = self.verifier_id
65
+ return base
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: dict) -> "AgentPromptResponseVerifierTaskStep":
69
+ raw_agg = data.get("score_aggregator")
70
+ return cls(
71
+ **cls._base_from_dict(data),
72
+ prompt_id=data["prompt_id"],
73
+ criteria=data.get("criteria"),
74
+ score_aggregator=ScoreAggregator(raw_agg) if raw_agg else None,
75
+ verifier_id=data.get("verifier_id"),
76
+ )
77
+
78
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
79
+ prompt_response = next(
80
+ (pr for pr in context.prompt_responses if pr.prompt_id == self.prompt_id),
81
+ None,
82
+ )
83
+ if prompt_response is None:
84
+ raise RuntimeError(f"PromptResponse with prompt_id='{self.prompt_id}' not found in context")
85
+
86
+ if prompt_response.error_type:
87
+ logger.warning(
88
+ f"Skipping verification '{self.verifier_id}': prompt had error_type={prompt_response.error_type}"
89
+ )
90
+ context.metadata.setdefault("verifications", {})[self.verifier_id] = {
91
+ "results": [{
92
+ "id": "prompt_error",
93
+ "score": 0,
94
+ "result": False,
95
+ "message": f"Skipped: prompt had error_type={prompt_response.error_type}",
96
+ }],
97
+ "score": 0,
98
+ }
99
+ return context
100
+
101
+ response = prompt_response.response or ""
102
+
103
+ results: list[dict] = []
104
+ for idx, criterion in enumerate(self.criteria):
105
+ rtype = criterion.get("type")
106
+ if rtype not in _HANDLED_TYPES:
107
+ raise ValueError(
108
+ f"criterion #{idx} has unknown type {rtype!r}; "
109
+ f"handled types: {sorted(_HANDLED_TYPES)}"
110
+ )
111
+ outcome = self._eval_criterion(idx, response, criterion)
112
+ results.append({
113
+ **criterion,
114
+ "criterion_index": idx,
115
+ "score": float(outcome["score"]),
116
+ "result": bool(outcome["passed"]),
117
+ "justification": outcome["justification"],
118
+ })
119
+
120
+ score = aggregate_score(results, self.score_aggregator)
121
+
122
+ context.metadata.setdefault("verifications", {})[self.verifier_id] = {
123
+ "results": results,
124
+ "score": score,
125
+ }
126
+ logger.info(
127
+ f"Verification '{self.verifier_id}': "
128
+ f"{len(results)} criteria evaluated, score={score}"
129
+ )
130
+ return context
131
+
132
+ def _eval_criterion(self, idx: int, response: str, criterion: dict) -> dict:
133
+ rtype = criterion["type"]
134
+ if rtype == "response_contains":
135
+ needles = criterion.get("needles") or []
136
+ if not needles:
137
+ raise ValueError(
138
+ f"criterion #{idx} (response_contains) requires non-empty `needles`"
139
+ )
140
+ missing = [n for n in needles if n not in response]
141
+ passed = not missing
142
+ return {
143
+ "score": 1.0 if passed else 0.0,
144
+ "passed": passed,
145
+ "justification": "all needles present" if passed else f"missing: {missing}",
146
+ }
147
+ if rtype == "response_regex_present":
148
+ pattern = criterion.get("pattern")
149
+ if not pattern:
150
+ raise ValueError(
151
+ f"criterion #{idx} (response_regex_present) requires non-empty `pattern`"
152
+ )
153
+ flags_raw = criterion.get("flags")
154
+ if flags_raw is None:
155
+ flags = re.DOTALL
156
+ else:
157
+ flags = 0
158
+ for name in flags_raw:
159
+ flags |= getattr(re, name)
160
+ passed = bool(re.search(pattern, response, flags))
161
+ return {
162
+ "score": 1.0 if passed else 0.0,
163
+ "passed": passed,
164
+ "justification": "pattern matched" if passed else f"no match for pattern {pattern!r}",
165
+ }
166
+ raise RuntimeError(f"unhandled criterion type {rtype}")
@@ -0,0 +1,90 @@
1
+ """Aggregate result rows across multiple upstream verifier steps into one unified score.
2
+
3
+ Reads `context.metadata["verifications"][<vid>]["results"]` for each id in
4
+ `verifier_ids`, concatenates the rows, drops skipped ones, and runs
5
+ `aggregate_score` once on the union — same math as the upstream steps, but
6
+ applied across their combined criterion set.
7
+
8
+ Use this to match Harbor's unified-aggregation behavior when filesystem
9
+ criteria (verify_sandbox) and response criteria (rubrics_verifier) live in
10
+ separate steps.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import uuid
16
+ from typing import ClassVar, Optional
17
+
18
+ from agent_env.task_step.context import TaskStepContext
19
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
20
+ from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AggregateVerifiersTaskStep(TaskStep):
26
+ type: ClassVar[str] = "aggregate_verifiers"
27
+ entity_refs = ()
28
+
29
+ def __init__(
30
+ self,
31
+ id: str,
32
+ version: Optional[int],
33
+ verifier_ids: Optional[list[str]] = None,
34
+ score_aggregator: Optional[ScoreAggregator] = None,
35
+ verifier_id: Optional[str] = None,
36
+ depends_on: Optional[list[TaskStepDependency]] = None,
37
+ fail_task_on_error: bool = True,
38
+ ):
39
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
40
+ self.verifier_ids = list(verifier_ids or [])
41
+ if isinstance(score_aggregator, str):
42
+ score_aggregator = ScoreAggregator(score_aggregator)
43
+ self.score_aggregator = score_aggregator or ScoreAggregator.WEIGHTED_AVERAGE
44
+ self.verifier_id = verifier_id or uuid.uuid4().hex
45
+
46
+ def to_dict(self) -> dict:
47
+ base = super().to_dict()
48
+ base["verifier_ids"] = self.verifier_ids
49
+ base["score_aggregator"] = self.score_aggregator.value
50
+ base["verifier_id"] = self.verifier_id
51
+ return base
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: dict) -> "AggregateVerifiersTaskStep":
55
+ raw_agg = data.get("score_aggregator")
56
+ return cls(
57
+ **cls._base_from_dict(data),
58
+ verifier_ids=data.get("verifier_ids"),
59
+ score_aggregator=ScoreAggregator(raw_agg) if raw_agg else None,
60
+ verifier_id=data.get("verifier_id"),
61
+ )
62
+
63
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
64
+ all_verifications = context.metadata.get("verifications", {})
65
+ missing = [vid for vid in self.verifier_ids if vid not in all_verifications]
66
+ if missing:
67
+ raise RuntimeError(
68
+ f"AggregateVerifiers: verifier_id(s) not found in "
69
+ f"context.metadata.verifications: {missing}. "
70
+ f"Available: {list(all_verifications.keys())}"
71
+ )
72
+
73
+ merged_results: list[dict] = []
74
+ for vid in self.verifier_ids:
75
+ merged_results.extend(all_verifications[vid].get("results", []))
76
+
77
+ non_skipped = [r for r in merged_results if not r.get("skipped")]
78
+ score = aggregate_score(non_skipped, self.score_aggregator)
79
+
80
+ if "verifications" not in context.metadata:
81
+ context.metadata["verifications"] = {}
82
+ context.metadata["verifications"][self.verifier_id] = {
83
+ "score": score,
84
+ "source_verifier_ids": list(self.verifier_ids),
85
+ }
86
+ logger.info(
87
+ f"AggregateVerifiers '{self.verifier_id}': merged {len(self.verifier_ids)} verifier(s), "
88
+ f"{len(non_skipped)}/{len(merged_results)} criteria evaluated, score={score}"
89
+ )
90
+ return context