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,127 @@
1
+ """Verify environment outcome using a dynamically loaded Python verifier script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import importlib.util
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ import uuid
11
+ from typing import ClassVar, Optional
12
+
13
+ from agent_env.task_step.context import TaskStepContext
14
+ from agent_env.entity_refs import EntityRef
15
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
16
+ from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
17
+
18
+ __all__ = ["EnvOutcomeVerifierTaskStep", "ScoreAggregator", "aggregate_score"]
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class EnvOutcomeVerifierTaskStep(TaskStep):
25
+ type: ClassVar[str] = "env_outcome_verifier"
26
+ entity_refs = (
27
+ EntityRef.env("env_id"),
28
+ EntityRef.artifact("file_artifact_id", version_field="file_artifact_version", artifact_type="file"),
29
+ )
30
+
31
+ def __init__(
32
+ self,
33
+ id: str,
34
+ version: Optional[int],
35
+ env_id: str,
36
+ file_artifact_id: str,
37
+ file_artifact_version: Optional[int] = None,
38
+ score_aggregator: Optional[ScoreAggregator] = None,
39
+ verifier_id: Optional[str] = None,
40
+ depends_on: Optional[list[TaskStepDependency]] = None,
41
+ fail_task_on_error: bool = True,
42
+ ):
43
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
44
+ self.verifier_id = verifier_id or uuid.uuid4().hex
45
+ self.env_id = env_id
46
+ self.file_artifact_id = file_artifact_id
47
+ self.file_artifact_version = file_artifact_version
48
+ if isinstance(score_aggregator, str):
49
+ score_aggregator = ScoreAggregator(score_aggregator)
50
+ self.score_aggregator = score_aggregator or ScoreAggregator.ALL_PASS
51
+
52
+ def to_dict(self) -> dict:
53
+ base = super().to_dict()
54
+ base["verifier_id"] = self.verifier_id
55
+ base["env_id"] = self.env_id
56
+ base["file_artifact_id"] = self.file_artifact_id
57
+ base["file_artifact_version"] = self.file_artifact_version
58
+ base["score_aggregator"] = self.score_aggregator.value
59
+ return base
60
+
61
+ @classmethod
62
+ def put(cls, *, verify_script_file_path: Optional[str] = None, **kwargs) -> "EnvOutcomeVerifierTaskStep":
63
+ """Store the step. With ``verify_script_file_path`` the script is uploaded as
64
+ ``<id>-verifier-script`` first; otherwise the kwargs name an existing ``file_artifact_id``
65
+ (the shape ``agent-env task create`` builds from JSON)."""
66
+ from agent_env.task_step.store import get_task_step_store
67
+
68
+ if verify_script_file_path is not None:
69
+ from agent_env.artifact import FileArtifact
70
+
71
+ artifact = FileArtifact.put(
72
+ id=f"{kwargs['id']}-verifier-script",
73
+ description=f"Verifier script for {kwargs['id']}",
74
+ file_path=verify_script_file_path,
75
+ )
76
+ kwargs.update(file_artifact_id=artifact.id, file_artifact_version=artifact.version)
77
+ kwargs.setdefault("version", None)
78
+ return get_task_step_store().put_document(cls(**kwargs))
79
+
80
+ @classmethod
81
+ def from_dict(cls, data: dict) -> EnvOutcomeVerifierTaskStep:
82
+ raw = data.get("score_aggregator")
83
+ return cls(
84
+ **cls._base_from_dict(data),
85
+ env_id=data["env_id"],
86
+ file_artifact_id=data["file_artifact_id"],
87
+ file_artifact_version=data.get("file_artifact_version"),
88
+ score_aggregator=ScoreAggregator(raw) if raw else None,
89
+ verifier_id=data.get("verifier_id"),
90
+ )
91
+
92
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
93
+ from agent_env.artifact import FileArtifact
94
+
95
+ deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
96
+ if deployed is None:
97
+ raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
98
+
99
+ artifact = FileArtifact.get(self.file_artifact_id, self.file_artifact_version)
100
+ verifier_bytes = await asyncio.to_thread(artifact.load)
101
+
102
+ tmp_fd, tmp_path = tempfile.mkstemp(suffix=".py")
103
+ try:
104
+ os.write(tmp_fd, verifier_bytes)
105
+ os.close(tmp_fd)
106
+
107
+ module_name = f"outcome_verifier_{self.id}"
108
+ spec = importlib.util.spec_from_file_location(module_name, tmp_path)
109
+ module = importlib.util.module_from_spec(spec)
110
+ spec.loader.exec_module(module)
111
+
112
+ logger.info(f"Running verify() from artifact '{self.file_artifact_id}' against {deployed.mcp_url}")
113
+ results = await module.verify(deployed.mcp_url)
114
+
115
+ score = aggregate_score(results, self.score_aggregator)
116
+ if "verifications" not in context.metadata:
117
+ context.metadata["verifications"] = {}
118
+ context.metadata["verifications"][self.verifier_id] = {
119
+ "results": results,
120
+ "score": score,
121
+ }
122
+ logger.info(f"Verification '{self.verifier_id}' complete: {len(results)} criteria, score={score}")
123
+ finally:
124
+ if os.path.exists(tmp_path):
125
+ os.unlink(tmp_path)
126
+
127
+ return context
@@ -0,0 +1,2 @@
1
+ """Helpers only the LLM judge uses: judge output formats and prompt scaffolds, OTel
2
+ trajectory compaction, and per-criterion frame selection. Not task steps."""
@@ -0,0 +1,377 @@
1
+ """Per-criterion screenshot selection for a direct multimodal judge. Instead of the last N frames
2
+ (``compact_screenshot_trajectory``), the frame budget goes to the actions that matter: the actions the task
3
+ pins (``TrajectoryFilter.always_show_actions``), the action best matching each criterion plus the one after
4
+ it (an action's effect shows on the following screen), the final frames, and the rest spread over the run.
5
+ A run that fits the budget is sent whole. Every frame carries a label (``"[c3] action 17"``,
6
+ ``"submitted action 4"``, ``"final action 41"``, several tags composed) the judge cites. Everything here is
7
+ trajectory-level and judge-agnostic; the verifier adapts ``LabeledFrame`` to its judge's message shape.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from collections.abc import Iterable, Mapping
14
+ from dataclasses import dataclass
15
+
16
+ from agent_env.task_step.task_steps.verifiers.judge_utils.trajectory_filter import (
17
+ DEFAULT_ACTION_LOG_MAX_LINES,
18
+ DEFAULT_FRAME_BUDGET,
19
+ FINAL_FRAMES_RESERVED,
20
+ ImageFrame,
21
+ media_type_for_b64,
22
+ parse_json_attr,
23
+ strip_base64_images,
24
+ )
25
+
26
+ # Cuts (with an ellipsis) applied to the rendered JSON of tool arguments — the same text the action log,
27
+ # the regex knobs and criterion matching see — tool results and agent messages.
28
+ MAX_ARGS_CHARS = 110
29
+ MAX_RESULT_CHARS = 160
30
+ MAX_AGENT_MESSAGE_CHARS = 200
31
+ # Replaces a base64 image payload in rendered text before any cut, so the cut lands on text.
32
+ _IMAGE_PLACEHOLDER = "<image>"
33
+ # A second candidate scoring at least this fraction of a criterion's best match is kept as a near-tie.
34
+ _NEAR_TIE_RATIO = 0.6
35
+
36
+ # Words that carry no evidence about WHICH action shows a criterion.
37
+ _CRITERION_STOPWORDS = frozenset({
38
+ "the", "and", "for", "that", "this", "with", "was", "were", "been", "being", "has", "have", "are",
39
+ "its", "his", "her", "not", "any", "all", "one", "two", "from", "into", "than", "then", "there",
40
+ "shows", "show", "shown", "screen", "state", "final", "trajectory", "agent", "task", "user", "app",
41
+ "exactly", "visible", "displayed", "confirm", "confirmed", "set", "least", "valid", "correct",
42
+ "matching", "either", "should", "must", "step", "steps", "run",
43
+ })
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class TrajectoryAction:
48
+ """One ``execute_tool`` span of a raw OTel trajectory, in run order (``index`` is 1-based)."""
49
+
50
+ index: int
51
+ tool: str
52
+ args: dict
53
+ screenshot: str | None # raw base64 of the post-action screenshot, if the span had one
54
+ result: object = None # the tool result minus its `screenshot`; None if nothing is left
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class AgentMessage:
59
+ """A ``text`` block of a ``chat`` span, placed after ``after_action`` tool calls (0 = before the first)."""
60
+
61
+ after_action: int
62
+ text: str
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Transcript:
67
+ """What the RAW trajectory records the agent doing (``actions``) and saying (``messages``)."""
68
+
69
+ actions: list[TrajectoryAction]
70
+ messages: list[AgentMessage]
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class LabeledFrame:
75
+ label: str
76
+ frame: ImageFrame
77
+
78
+
79
+ def _drop_screenshots(value: object) -> object:
80
+ """``value`` with every ``screenshot`` key removed, at any depth."""
81
+ if isinstance(value, dict):
82
+ return {k: _drop_screenshots(v) for k, v in value.items() if k != "screenshot"}
83
+ if isinstance(value, list):
84
+ return [_drop_screenshots(v) for v in value]
85
+ return value
86
+
87
+
88
+ def _without_screenshot(completion: object) -> object:
89
+ """The tool result minus its ``screenshot`` keys; None when nothing is left at the TOP level. Nested
90
+ empty values are kept — an empty string inside a list is still what the tool returned."""
91
+ stripped = _drop_screenshots(completion)
92
+ return None if stripped in ("", None, {}) else stripped
93
+
94
+
95
+ def _text_blocks(completion: object) -> list[str]:
96
+ """The non-empty ``text`` blocks of a chat completion — a bare block list, or ``{"content": [...]}``."""
97
+ blocks = completion.get("content") if isinstance(completion, dict) else completion
98
+ if not isinstance(blocks, list):
99
+ return []
100
+ return [b["text"] for b in blocks
101
+ if isinstance(b, dict) and b.get("type") == "text" and isinstance(b.get("text"), str) and b["text"].strip()]
102
+
103
+
104
+ def transcript_from_raw(raw_text: str) -> Transcript:
105
+ """Every ``execute_tool`` span as a ``TrajectoryAction`` and every text block of a ``chat`` span as an
106
+ ``AgentMessage``, in span order. The ``chain`` root is skipped: its completion is the final response the
107
+ judge already gets. Malformed input gives an empty transcript."""
108
+ try:
109
+ spans = json.loads(raw_text)
110
+ except (json.JSONDecodeError, TypeError):
111
+ return Transcript([], [])
112
+ if not isinstance(spans, list):
113
+ return Transcript([], [])
114
+ actions: list[TrajectoryAction] = []
115
+ messages: list[AgentMessage] = []
116
+ for span in spans:
117
+ attrs = span.get("attributes") if isinstance(span, dict) else None
118
+ if not isinstance(attrs, dict):
119
+ continue
120
+ operation = attrs.get("gen_ai.operation.name")
121
+ completion = parse_json_attr(attrs.get("gen_ai.completion", "{}"))
122
+ if operation == "chat":
123
+ messages.extend(AgentMessage(len(actions), text) for text in _text_blocks(completion))
124
+ elif operation == "execute_tool":
125
+ prompt = parse_json_attr(attrs.get("gen_ai.prompt", "{}"))
126
+ args = prompt.get("input", prompt) if isinstance(prompt, dict) else {}
127
+ if not isinstance(args, dict):
128
+ args = {}
129
+ # The tool name: harnesses that name every action span generically (e.g. "execute_tool")
130
+ # carry the real tool in the prompt attribute; prefer it, fall back to the span name.
131
+ tool = prompt.get("tool") if isinstance(prompt, dict) else None
132
+ shot = completion.get("screenshot") if isinstance(completion, dict) else None
133
+ actions.append(TrajectoryAction(
134
+ index=len(actions) + 1, tool=str(tool or span.get("name") or ""), args=args,
135
+ screenshot=shot if isinstance(shot, str) and shot else None,
136
+ result=_without_screenshot(completion),
137
+ ))
138
+ return Transcript(actions, messages)
139
+
140
+
141
+ def _cut(text: str, max_chars: int) -> str:
142
+ return text if len(text) <= max_chars else text[:max_chars] + "…"
143
+
144
+
145
+ def _render_json(value: object, max_chars: int) -> str:
146
+ """``value`` as compact JSON, images replaced by ``_IMAGE_PLACEHOLDER``, cut to ``max_chars``."""
147
+ return _cut(strip_base64_images(json.dumps(value, ensure_ascii=False), _IMAGE_PLACEHOLDER), max_chars)
148
+
149
+
150
+ def rendered_action(a: TrajectoryAction) -> str:
151
+ """``"<tool> <args json>"``, the arguments cut to ``MAX_ARGS_CHARS`` — the text the regex knobs
152
+ (``evidence_exclude_pattern``, ``always_show_actions``) and criterion matching see, and what the action
153
+ log shows, so a value past the cut cannot be matched."""
154
+ return f"{a.tool} {_render_json(a.args, MAX_ARGS_CHARS)}"
155
+
156
+
157
+ def _action_line(a: TrajectoryAction) -> str:
158
+ line = f"{a.index}. {rendered_action(a)}"
159
+ if a.result is not None:
160
+ line += f" -> {_render_json(a.result, MAX_RESULT_CHARS)}"
161
+ return line
162
+
163
+
164
+ def _message_line(m: AgentMessage) -> str:
165
+ # Images first, whitespace second: collapsing whitespace inside a base64 run would break it into
166
+ # pieces too short for the image pattern to catch.
167
+ text = " ".join(strip_base64_images(m.text, _IMAGE_PLACEHOLDER).split())
168
+ return f'agent said: "{_cut(text, MAX_AGENT_MESSAGE_CHARS)}"'
169
+
170
+
171
+ def action_log(actions: Iterable[TrajectoryAction], *, messages: Iterable[AgentMessage] = (),
172
+ max_lines: int = DEFAULT_ACTION_LOG_MAX_LINES) -> str:
173
+ """The run as text: ``"<i>. <tool> <args> -> <result>"`` per tool call and, in run order between them,
174
+ ``'agent said: "..."'`` per agent message — the tool lines are facts, the agent's lines its claims.
175
+ Over ``max_lines`` the head and the tail are kept (half each, the head taking the odd line) around one
176
+ ``"... (N lines not shown) ..."`` marker, so the final actions are always in the log."""
177
+ pending = sorted(messages, key=lambda m: m.after_action)
178
+ lines: list[str] = []
179
+ for a in actions:
180
+ while pending and pending[0].after_action < a.index:
181
+ lines.append(_message_line(pending.pop(0)))
182
+ lines.append(_action_line(a))
183
+ lines.extend(_message_line(m) for m in pending)
184
+ if len(lines) <= max_lines:
185
+ return "\n".join(lines)
186
+ tail = max_lines // 2
187
+ head = max_lines - tail
188
+ hidden = len(lines) - max_lines
189
+ return "\n".join([*lines[:head], f"... ({hidden} lines not shown) ...", *lines[-tail:]])
190
+
191
+
192
+ def _terms(text: object) -> set[str]:
193
+ return {w for w in re.findall(r"[a-z0-9]{3,}", str(text or "").lower()) if w not in _CRITERION_STOPWORDS}
194
+
195
+
196
+ def _criterion_id(criterion: dict) -> str:
197
+ return str(criterion.get("id") or "?")
198
+
199
+
200
+ def criterion_matches(criteria: list[dict], actions: list[TrajectoryAction], *,
201
+ exclude_pattern: re.Pattern | None = None) -> list[tuple[str, TrajectoryAction]]:
202
+ """``[(criterion_id, action), ...]`` in criteria order — each criterion's best-matching action, then a
203
+ near-tie (>= ``_NEAR_TIE_RATIO`` of the best score) if there is one.
204
+
205
+ Terms are weighted by inverse document frequency across this task's criteria. Candidates carrying a
206
+ term unique to the criterion are preferred; when none does, the shared-only matches are used (weaker
207
+ evidence is not a reason to send none). Actions whose rendering matches ``exclude_pattern`` are skipped."""
208
+ docs = [_terms(c.get("description")) | _terms(c.get("id")) for c in criteria]
209
+ df: dict[str, int] = {}
210
+ for d in docs:
211
+ for t in d:
212
+ df[t] = df.get(t, 0) + 1
213
+ out: list[tuple[str, TrajectoryAction]] = []
214
+ for c, terms in zip(criteria, docs, strict=True):
215
+ unique: list[tuple[float, int, TrajectoryAction]] = []
216
+ shared: list[tuple[float, int, TrajectoryAction]] = []
217
+ for a in actions:
218
+ rendered = rendered_action(a)
219
+ if exclude_pattern is not None and exclude_pattern.search(rendered):
220
+ continue
221
+ hit = terms & _terms(rendered)
222
+ if not hit:
223
+ continue
224
+ cand = (sum(1.0 / df.get(t, 1) for t in hit), a.index, a)
225
+ (unique if any(df.get(t, 9) == 1 for t in hit) else shared).append(cand)
226
+ scored = unique or shared
227
+ scored.sort(key=lambda x: (-x[0], -x[1]))
228
+ keep = scored[:1]
229
+ for cand in scored[1:2]:
230
+ if cand[0] >= _NEAR_TIE_RATIO * scored[0][0]:
231
+ keep.append(cand)
232
+ out.extend((_criterion_id(c), a) for _, _, a in keep)
233
+ return out
234
+
235
+
236
+ @dataclass(frozen=True)
237
+ class _Want:
238
+ """One frame a criterion wants attached."""
239
+
240
+ cid: str
241
+ index: int
242
+ rank: int # 0 = the criterion's best match, 1 = its near-tie
243
+ effect: bool # the frame AFTER the matching action, where the action's effect shows
244
+
245
+ @property
246
+ def tier(self) -> int:
247
+ """Drop order under budget pressure: 0 = effect frames, 1 = near-tie matches, 2 = best matches."""
248
+ return 0 if self.effect else (1 if self.rank else 2)
249
+
250
+
251
+ def _wants(criteria: list[dict], actions: list[TrajectoryAction], has_shot: set[int],
252
+ exclude_pattern: re.Pattern | None) -> list[_Want]:
253
+ rank: dict[str, int] = {}
254
+ out: list[_Want] = []
255
+ for cid, a in criterion_matches(criteria, actions, exclude_pattern=exclude_pattern):
256
+ r = rank.get(cid, 0)
257
+ rank[cid] = r + 1
258
+ for j, effect in ((a.index, False), (a.index + 1, True)):
259
+ if j in has_shot:
260
+ out.append(_Want(cid, j, r, effect))
261
+ return out
262
+
263
+
264
+ def _pins(shots: list[TrajectoryAction], always_show: Mapping[str, re.Pattern]) -> dict[int, list[str]]:
265
+ """``{action index: [labels]}`` for every action with a frame whose rendering matches a pin, labels
266
+ in configuration order."""
267
+ out: dict[int, list[str]] = {}
268
+ for a in shots:
269
+ rendered = rendered_action(a)
270
+ labels = [label for label, pattern in always_show.items() if pattern.search(rendered)]
271
+ if labels:
272
+ out[a.index] = labels
273
+ return out
274
+
275
+
276
+ def _spread(items: list[int], k: int) -> list[int]:
277
+ """Up to ``k`` of ``items``, evenly spaced from the first to the last (both always kept); ``k == 1``
278
+ keeps the last."""
279
+ n = len(items)
280
+ if k <= 0:
281
+ return []
282
+ if k >= n:
283
+ return list(items)
284
+ if k == 1:
285
+ return items[-1:]
286
+ return [items[round(i * (n - 1) / (k - 1))] for i in range(k)]
287
+
288
+
289
+ def _trim(picked: set[int], reserved: set[int], wants: list[_Want], cids: list[str],
290
+ max_frames: int) -> None:
291
+ """Drop criterion frames from ``picked`` until it fits ``max_frames``: lower tiers first, one frame
292
+ per criterion per round, so no criterion loses its best match while another keeps an effect frame.
293
+ A frame wanted at several tiers is only droppable at its highest one."""
294
+ over = len(picked) - max_frames
295
+ value: dict[int, int] = {}
296
+ for w in wants:
297
+ value[w.index] = max(value.get(w.index, -1), w.tier)
298
+ for tier in (0, 1, 2):
299
+ queues = {
300
+ cid: [w.index for w in sorted(wants, key=lambda w: -w.rank) if w.cid == cid and w.tier == tier]
301
+ for cid in cids
302
+ }
303
+ while over > 0 and any(queues.values()):
304
+ for cid in cids:
305
+ if over <= 0:
306
+ break
307
+ queue = queues[cid]
308
+ while queue:
309
+ j = queue.pop(0)
310
+ if j in picked and j not in reserved and value[j] == tier:
311
+ picked.remove(j)
312
+ over -= 1
313
+ break
314
+
315
+
316
+ def _pick_within_budget(shots: list[TrajectoryAction], final: set[int], pins: Mapping[int, list[str]],
317
+ wants: list[_Want], cids: list[str], max_frames: int) -> set[int]:
318
+ """The frame indices to keep when the run exceeds ``max_frames``: the final frames and the pinned
319
+ frames (at most half the budget left after the final frames, spread over the run) are reserved, the
320
+ criterion frames are trimmed to fit (see ``_trim``), and any room left is spread over the rest of the run."""
321
+ pin_cap = (max_frames - len(final)) // 2
322
+ reserved = final | set(_spread([p for p in sorted(pins) if p not in final], pin_cap))
323
+ picked = reserved | {w.index for w in wants}
324
+ if len(picked) > max_frames:
325
+ _trim(picked, reserved, wants, cids, max_frames)
326
+ room = max_frames - len(picked)
327
+ cands = [a.index for a in shots if a.index not in picked]
328
+ if room > 0 and cands:
329
+ step = max(1.0, len(cands) / room)
330
+ for k in range(room):
331
+ picked.add(cands[min(int(k * step), len(cands) - 1)])
332
+ return picked
333
+
334
+
335
+ def _labeled_frames(actions: list[TrajectoryAction], picked: set[int], wants: list[_Want], final: set[int],
336
+ pins: Mapping[int, list[str]], label_ids: Mapping[str, str]) -> list[LabeledFrame]:
337
+ """``picked`` in run order, each labelled ``"[cA][cB] final <pin labels> action K"`` (parts present as
338
+ they apply); ``label_ids`` maps a criterion id to the key written in its tag."""
339
+ tags: dict[int, list[str]] = {}
340
+ for w in wants:
341
+ if w.cid not in tags.setdefault(w.index, []):
342
+ tags[w.index].append(w.cid)
343
+ by_index = {a.index: a for a in actions}
344
+ frames = []
345
+ for j in sorted(picked):
346
+ a = by_index[j]
347
+ assert a.screenshot is not None
348
+ parts = ["".join(f"[{label_ids.get(cid, cid)}]" for cid in tags.get(j, ()))]
349
+ if j in final:
350
+ parts.append("final")
351
+ parts.extend(pins.get(j, ()))
352
+ parts.append(f"action {j}")
353
+ frames.append(LabeledFrame(" ".join(part for part in parts if part),
354
+ ImageFrame(media_type=media_type_for_b64(a.screenshot), data=a.screenshot)))
355
+ return frames
356
+
357
+
358
+ def select_frames_per_criterion(actions: list[TrajectoryAction], criteria: list[dict], *,
359
+ max_frames: int = DEFAULT_FRAME_BUDGET,
360
+ exclude_pattern: re.Pattern | None = None,
361
+ always_show: Mapping[str, re.Pattern] | None = None,
362
+ label_ids: Mapping[str, str] | None = None) -> list[LabeledFrame]:
363
+ """The labelled frames to attach for grading ``criteria`` from ``actions``, in run order, never more
364
+ than ``max_frames``. The final-frame reservation is ``FINAL_FRAMES_RESERVED``, shrunk to half the
365
+ budget for tiny budgets. ``always_show`` maps a pin label to a compiled regex over ``rendered_action``;
366
+ ``label_ids`` maps a criterion id to the key written in its frame tags (the verifier passes c1..cN)."""
367
+ shots = [a for a in actions if a.screenshot]
368
+ has_shot = {a.index for a in shots}
369
+ n_final = max(1, min(FINAL_FRAMES_RESERVED, max_frames // 2))
370
+ final = {a.index for a in shots[-n_final:]}
371
+ pins = _pins(shots, always_show or {})
372
+ wants = _wants(criteria, actions, has_shot, exclude_pattern)
373
+ if len(shots) <= max_frames:
374
+ picked = has_shot
375
+ else:
376
+ picked = _pick_within_budget(shots, final, pins, wants, [_criterion_id(c) for c in criteria], max_frames)
377
+ return _labeled_frames(actions, picked, wants, final, pins, label_ids or {})