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,475 @@
1
+ """Run dispatch + progress — the Task Runner surface.
2
+
3
+ Every route goes through the configured ``[runner]``. Responses use ``workflow_id`` /
4
+ ``instance_id`` for the UI's poll loop; under the local runner ``workflow_id`` is a
5
+ locally-minted run id.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import uuid
14
+ from typing import Optional
15
+
16
+ from fastapi import APIRouter, HTTPException, Query
17
+ from pydantic import BaseModel, ConfigDict
18
+
19
+ from agent_env.config import get_config, get_runner
20
+ from agent_env.explorer.routers.common import PaginatedResponse, docs
21
+ from agent_env.runner.runner import RunStatus
22
+ from agent_env.store import Filter, Sort
23
+ from agent_env.task.store import TaskStepStatus
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ router = APIRouter(prefix="/api/v1/tasks", tags=["runs"])
28
+
29
+ TASK_INSTANCES_COLLECTION = "task_instances"
30
+
31
+ MAX_RUNS_PER_REQUEST = 100 # cap a single start-runs fan-out
32
+
33
+ RUN_GROUP_STREAM_POLL_SECONDS = 2 # gap between run-group snapshots on the SSE stream
34
+ RUN_GROUP_STREAM_MAX_SNAPSHOTS = 600 # ceiling before a `timeout` event (~20 min at the poll interval)
35
+
36
+
37
+ class RunRequest(BaseModel):
38
+ """One run. ``extra="forbid"`` rejects unknown keys so an unsupported field returns a
39
+ 422 rather than being silently dropped."""
40
+
41
+ model_config = ConfigDict(extra="forbid")
42
+
43
+ version: Optional[int] = None
44
+ agent_model: Optional[str] = None
45
+ agent_artifact_id: Optional[str] = None
46
+ metadata: Optional[dict] = None
47
+ # Resume / override controls, threaded into context.metadata for the task step.
48
+ start_step: Optional[int] = None
49
+ context_from_instance_id: Optional[str] = None
50
+ context_json: Optional[dict] = None
51
+ step_overrides: Optional[dict] = None
52
+ overrides: Optional[dict] = None
53
+ project_id: Optional[str] = None
54
+ priority: Optional[int] = None
55
+ # Accepted but ignored: the local explorer gets models from the [model] config, not a
56
+ # per-request key; declared so a client that always sends them isn't rejected.
57
+ litellm_api_key: Optional[str] = None
58
+ litellm_key_id: Optional[str] = None
59
+
60
+
61
+ class RunResponse(BaseModel):
62
+ workflow_id: str
63
+ instance_id: str
64
+ status: RunStatus
65
+
66
+
67
+ # Keys the runner acts on that a caller could also smuggle through the untyped
68
+ # `metadata` passthrough. Validating only the typed RunRequest fields is not enough:
69
+ # `_run_metadata` copies `body.metadata` verbatim, so every one of these has to be
70
+ # checked on the merged dict, whatever route it arrived by.
71
+ _UNSUPPORTED_METADATA_KEYS = ("context_from_instance_id", "context_json")
72
+
73
+
74
+ def _validate_run_metadata(metadata: dict) -> None:
75
+ """Reject metadata the runner cannot honour, before a run id is handed back.
76
+
77
+ Anything accepted here and then ignored produces the worst outcome available: a run
78
+ that reports its skipped steps as SUCCESS with no data, or one that raises inside
79
+ the worker after submit already returned 200 and so never registers an instance.
80
+ """
81
+ for key in _UNSUPPORTED_METADATA_KEYS:
82
+ if metadata.get(key) is not None:
83
+ raise HTTPException(
84
+ status_code=501,
85
+ detail=f"{key} is not supported by this runner: a resumed run would "
86
+ "report skipped steps as successful with no data",
87
+ )
88
+
89
+ start_step = metadata.get("start_step")
90
+ if start_step is not None:
91
+ if isinstance(start_step, bool) or not isinstance(start_step, int):
92
+ raise HTTPException(
93
+ status_code=422,
94
+ detail=f"start_step must be an integer, got {type(start_step).__name__}",
95
+ )
96
+ if start_step < 0:
97
+ raise HTTPException(status_code=422, detail="start_step must be >= 0")
98
+
99
+ overrides = metadata.get("user_overrides")
100
+ if overrides is not None and not isinstance(overrides, dict):
101
+ raise HTTPException(
102
+ status_code=422,
103
+ detail=f"metadata.user_overrides must be an object, got {type(overrides).__name__}",
104
+ )
105
+
106
+
107
+ def _run_metadata(body: "RunRequest") -> dict:
108
+ """Fold the override fields into the metadata Task.run() carries."""
109
+ metadata = dict(body.metadata or {})
110
+ _validate_run_metadata(metadata) # what the caller smuggled in
111
+
112
+ # Seed from any user_overrides already in metadata — a re-run inherits the prior
113
+ # instance's — so the explicit fields merge into them rather than replacing the lot.
114
+ user_overrides = {**(metadata.get("user_overrides") or {}), **(body.overrides or {})}
115
+ for key in ("project_id", "priority"):
116
+ value = getattr(body, key)
117
+ if value is not None:
118
+ user_overrides[key] = value
119
+ # The steps read `step_params` (TaskStep.step_param_overrides); the request field is
120
+ # named differently. Writing the request name through dropped every override.
121
+ if body.step_overrides is not None:
122
+ user_overrides["step_params"] = body.step_overrides
123
+ if user_overrides:
124
+ metadata["user_overrides"] = user_overrides
125
+
126
+ for key in ("start_step", *_UNSUPPORTED_METADATA_KEYS):
127
+ value = getattr(body, key)
128
+ if value is not None:
129
+ metadata[key] = value
130
+ _validate_run_metadata(metadata) # and what the typed fields added
131
+ return metadata
132
+
133
+
134
+ def _resolve_task_version(task_id: str, version: Optional[int]) -> int:
135
+ """The concrete version to run; 404 if the task (at ``version``, else latest) is absent."""
136
+ from agent_env.task import Task
137
+
138
+ task = Task.get(task_id, version)
139
+ if task is None:
140
+ label = f"v{version}" if version is not None else "latest"
141
+ raise HTTPException(status_code=404, detail=f"Task {task_id} {label} not found")
142
+ return task.version
143
+
144
+
145
+ @router.post("/{task_id}/run", response_model=RunResponse)
146
+ async def start_run(task_id: str, body: RunRequest | None = None) -> RunResponse:
147
+ body = body or RunRequest()
148
+ version = _resolve_task_version(task_id, body.version)
149
+ handle = await get_runner().submit(
150
+ task_id, version,
151
+ agent_model=body.agent_model,
152
+ agent_artifact_id=body.agent_artifact_id,
153
+ metadata=_run_metadata(body),
154
+ )
155
+ return RunResponse(workflow_id=handle.run_id, instance_id=handle.instance_id, status=RunStatus.QUEUED)
156
+
157
+
158
+ class StartRunsRequest(RunRequest):
159
+ """N runs of one task, started as a group (the UI's "Start N Runs")."""
160
+
161
+ count: Optional[int] = None
162
+ seeds: Optional[list[dict]] = None
163
+ # Accepted but ignored, like litellm_api_key above. Runs are submitted
164
+ # sequentially and LocalRunner bounds execution with a semaphore sized once at
165
+ # construction from `[runner.config] workers`, so there is no per-request knob to
166
+ # honour. Declared so the inherited Start-Runs panel isn't rejected by
167
+ # `extra="forbid"`; the panel labels it as ignored.
168
+ concurrency: Optional[int] = None
169
+
170
+
171
+ class RunInfo(BaseModel):
172
+ index: int
173
+ workflow_id: Optional[str] = None
174
+ seed: Optional[dict] = None
175
+ error: Optional[str] = None
176
+
177
+
178
+ class StartRunsResponse(BaseModel):
179
+ run_group_id: str
180
+ task_id: str
181
+ task_version: Optional[int] = None
182
+ total: int
183
+ runs: list[RunInfo]
184
+
185
+
186
+ @router.post("/{task_id}/runs", response_model=StartRunsResponse)
187
+ async def start_runs(task_id: str, body: StartRunsRequest | None = None) -> StartRunsResponse:
188
+ """Start a group of runs, returning their handles immediately. ``seeds`` wins over
189
+ ``count`` when both are given (one run per seed)."""
190
+ body = body or StartRunsRequest()
191
+ version = _resolve_task_version(task_id, body.version) # 404 the whole group if the task is absent
192
+ requested = len(body.seeds) if body.seeds is not None else max(body.count or 1, 1)
193
+ if requested > MAX_RUNS_PER_REQUEST:
194
+ raise HTTPException(status_code=422,
195
+ detail=f"at most {MAX_RUNS_PER_REQUEST} runs per request (got {requested})")
196
+ seeds = body.seeds if body.seeds is not None else [None] * requested
197
+ run_group_id = f"rg-{uuid.uuid4().hex}"
198
+ base_metadata = _run_metadata(body)
199
+
200
+ runs: list[RunInfo] = []
201
+ for index, seed in enumerate(seeds):
202
+ metadata = dict(base_metadata)
203
+ metadata["run_group_id"] = run_group_id
204
+ if seed:
205
+ metadata["seed"] = seed
206
+ try:
207
+ handle = await get_runner().submit(
208
+ task_id, version,
209
+ agent_model=body.agent_model,
210
+ agent_artifact_id=body.agent_artifact_id,
211
+ metadata=metadata,
212
+ )
213
+ runs.append(RunInfo(index=index, workflow_id=handle.run_id, seed=seed))
214
+ except Exception as e: # one bad run must not sink the group
215
+ logger.exception("Run %d of group %s failed to start", index, run_group_id)
216
+ runs.append(RunInfo(index=index, seed=seed, error=f"{type(e).__name__}: {e}"))
217
+
218
+ return StartRunsResponse(
219
+ run_group_id=run_group_id, task_id=task_id, task_version=version,
220
+ total=len(runs), runs=runs,
221
+ )
222
+
223
+
224
+ @router.post("/{task_id}/cancel-run")
225
+ async def cancel_run(task_id: str, workflow_id: str = Query(...)) -> dict:
226
+ canceled = await get_runner().cancel(workflow_id)
227
+ return {"workflow_id": workflow_id, "canceled": canceled}
228
+
229
+
230
+ @router.get("/{task_id}/runs", response_model=PaginatedResponse)
231
+ def list_runs(
232
+ task_id: str,
233
+ limit: int = Query(50, ge=1, le=500),
234
+ offset: int = Query(0, ge=0),
235
+ ) -> PaginatedResponse:
236
+ from agent_env.runner import store as run_store
237
+
238
+ records = run_store.list_runs(task_id=task_id, limit=limit, offset=offset)
239
+ total = docs().count("runs", Filter.of(task_id=task_id))
240
+ return PaginatedResponse(
241
+ items=[r.to_dict() for r in records], total=total, limit=limit, offset=offset,
242
+ has_more=offset + len(records) < total,
243
+ )
244
+
245
+
246
+ @router.get("/{task_id}/instances", response_model=PaginatedResponse)
247
+ def list_instances(
248
+ task_id: str,
249
+ task_version: Optional[int] = None,
250
+ limit: int = Query(50, ge=1, le=500),
251
+ offset: int = Query(0, ge=0),
252
+ ) -> PaginatedResponse:
253
+ conditions = {"task_id": task_id}
254
+ if task_version is not None:
255
+ conditions["task_version"] = task_version
256
+ filt = Filter.of(**conditions)
257
+ store = docs()
258
+ items = store.query(TASK_INSTANCES_COLLECTION, filt,
259
+ sort=Sort.by("created_at_utc", descending=True),
260
+ limit=limit, offset=offset)
261
+ total = store.count(TASK_INSTANCES_COLLECTION, filt)
262
+ return PaginatedResponse(items=items, total=total, limit=limit, offset=offset,
263
+ has_more=offset + len(items) < total)
264
+
265
+
266
+ @router.get("/{task_id}/instances/{instance_id}")
267
+ def get_instance(task_id: str, instance_id: str) -> dict:
268
+ doc = docs().find_one(TASK_INSTANCES_COLLECTION, Filter.of(instance_id=instance_id))
269
+ if doc is None:
270
+ raise HTTPException(status_code=404, detail=f"instance {instance_id} not found")
271
+ return doc
272
+
273
+
274
+ # --- run groups -------------------------------------------------------------
275
+ #
276
+ # A run group is the set of runs sharing a run_group_id (stamped into each run's
277
+ # metadata) — a view over the runs collection, not a stored entity.
278
+
279
+
280
+ def _all_task_runs(task_id: str) -> list:
281
+ """Every run of a task, paged so no fixed limit truncates the group aggregation."""
282
+ from agent_env.runner import store as run_store
283
+
284
+ out, offset = [], 0
285
+ while True:
286
+ page = run_store.list_runs(task_id=task_id, limit=500, offset=offset)
287
+ out.extend(page)
288
+ if len(page) < 500:
289
+ return out
290
+ offset += 500
291
+
292
+
293
+ def _group_id_of(record) -> str:
294
+ """The group a run belongs to.
295
+
296
+ A batch (``POST /runs``) stamps ``metadata.run_group_id`` on every run it starts.
297
+ A single run (``POST /run``, the "Start 1 Run" button) stamps nothing, so it is its
298
+ own group, keyed by its run id. ``list_run_groups`` and ``_instances_for_group``
299
+ MUST agree on this — when only the former applied the fallback, every single run
300
+ produced a group row that the join could never match, so the Rollouts table showed
301
+ the group with "0 runs" and no instance, for every non-batch run.
302
+ """
303
+ return (record.overrides.get("metadata") or {}).get("run_group_id") or record.run_id
304
+
305
+
306
+ def _instances_for_group(run_group_id: str, task_id: str) -> tuple[list[dict], dict]:
307
+ """Every run in the group, joined to its task instance for live status, plus the
308
+ per-step ``{"done", "failed"}`` tally the Batch progress strip draws."""
309
+ out: list[dict] = []
310
+ step_counts: dict[str, dict[str, int]] = {}
311
+ for record in _all_task_runs(task_id):
312
+ if _group_id_of(record) != run_group_id:
313
+ continue
314
+ instance = docs().find_one(
315
+ TASK_INSTANCES_COLLECTION, Filter.of(instance_id=record.instance_id)
316
+ ) or {}
317
+ # One outcome per step per run. A completion callback that raises records a
318
+ # failure beside the step's success, and the failure is the final verdict.
319
+ outcome: dict[str, str] = {}
320
+ for step in instance.get("completed_steps") or []:
321
+ step_id, status = step.get("step_id"), step.get("status")
322
+ if status not in (TaskStepStatus.SUCCESS, TaskStepStatus.FAILURE):
323
+ continue
324
+ if outcome.get(step_id) != TaskStepStatus.FAILURE:
325
+ outcome[step_id] = status
326
+ for step_id, status in outcome.items():
327
+ counts = step_counts.setdefault(step_id, {"done": 0, "failed": 0})
328
+ counts["done" if status == TaskStepStatus.SUCCESS else "failed"] += 1
329
+ out.append({
330
+ "instance_id": record.instance_id,
331
+ "workflow_id": record.run_id,
332
+ "seed": (record.overrides.get("metadata") or {}).get("seed"),
333
+ # The run record is authoritative for lifecycle; the instance may lag or be absent.
334
+ "status": str(record.status).lower(),
335
+ "created_at_utc": record.created_at_utc,
336
+ "completed_at_utc": record.finished_at_utc,
337
+ "current_step": instance.get("current_step"),
338
+ "total_steps": instance.get("total_steps"),
339
+ })
340
+ return out, step_counts
341
+
342
+
343
+ # Run-group funnel buckets keyed by RunStatus, so the tally tracks the enum — adding a
344
+ # status is a deliberate edit here, not a silent fall-through. CANCELED folds into
345
+ # "failed"; QUEUED is "provisioning" (accepted, not yet picked up by a worker).
346
+ _FUNNEL_BUCKET: dict[RunStatus, str] = {
347
+ RunStatus.COMPLETED: "completed",
348
+ RunStatus.FAILED: "failed",
349
+ RunStatus.CANCELED: "failed",
350
+ RunStatus.RUNNING: "running",
351
+ RunStatus.QUEUED: "provisioning",
352
+ }
353
+
354
+
355
+ def _group_status(task_id: str, run_group_id: str) -> dict:
356
+ instances, step_counts = _instances_for_group(run_group_id, task_id)
357
+ tally = {"completed": 0, "failed": 0, "running": 0, "provisioning": 0}
358
+ for inst in instances:
359
+ try:
360
+ run_status: Optional[RunStatus] = RunStatus(str(inst["status"]).upper())
361
+ except ValueError:
362
+ run_status = None
363
+ tally[_FUNNEL_BUCKET.get(run_status, "provisioning")] += 1
364
+ return {
365
+ "run_group_id": run_group_id,
366
+ "task_id": task_id,
367
+ "total": len(instances),
368
+ **tally,
369
+ "step_counts": step_counts,
370
+ "instances": instances,
371
+ }
372
+
373
+
374
+ @router.get("/{task_id}/run-groups", response_model=PaginatedResponse)
375
+ def list_run_groups(
376
+ task_id: str,
377
+ task_version: Optional[int] = None,
378
+ limit: int = Query(20, ge=1, le=200),
379
+ offset: int = Query(0, ge=0),
380
+ ) -> PaginatedResponse:
381
+ """Run groups for a task, newest first — the Rollouts table."""
382
+ groups: dict[str, dict] = {}
383
+ for record in _all_task_runs(task_id):
384
+ if task_version is not None and record.task_version != task_version:
385
+ continue
386
+ gid = _group_id_of(record)
387
+ group = groups.setdefault(gid, {
388
+ "run_group_id": gid,
389
+ "task_id": task_id,
390
+ "task_version": record.task_version,
391
+ "created_at_utc": record.created_at_utc,
392
+ "total": 0,
393
+ })
394
+ group["total"] += 1
395
+ group["created_at_utc"] = record.created_at_utc # newest-first input: ends on the oldest
396
+
397
+ ordered = sorted(groups.values(), key=lambda g: g["created_at_utc"] or "", reverse=True)
398
+ page = ordered[offset: offset + limit]
399
+ items = []
400
+ for group in page:
401
+ status = _group_status(task_id, group["run_group_id"])
402
+ # The list row nests the tally under `counts` and names its timestamp
403
+ # `earliest_created_at_utc` — unlike the flat shape /run-groups/{id} returns.
404
+ items.append({
405
+ "run_group_id": group["run_group_id"],
406
+ "task_id": task_id,
407
+ "task_version": group["task_version"],
408
+ "earliest_created_at_utc": group["created_at_utc"],
409
+ "total": status["total"],
410
+ "counts": {
411
+ "completed": status["completed"],
412
+ "failed": status["failed"],
413
+ "running": status["running"],
414
+ "provisioning": status["provisioning"],
415
+ "waiting": 0,
416
+ },
417
+ "step_counts": status["step_counts"],
418
+ "instances": status["instances"],
419
+ })
420
+ return PaginatedResponse(items=items, total=len(ordered), limit=limit, offset=offset,
421
+ has_more=offset + len(items) < len(ordered))
422
+
423
+
424
+ @router.get("/{task_id}/run-groups/{run_group_id}")
425
+ def get_run_group(task_id: str, run_group_id: str) -> dict:
426
+ return _group_status(task_id, run_group_id)
427
+
428
+
429
+ @router.get("/{task_id}/run-groups/{run_group_id}/stream")
430
+ async def stream_run_group(task_id: str, run_group_id: str):
431
+ """Server-sent snapshots until every run in the group is terminal, ending with a
432
+ ``complete`` event."""
433
+ from fastapi.responses import StreamingResponse
434
+
435
+ async def events():
436
+ for _ in range(RUN_GROUP_STREAM_MAX_SNAPSHOTS):
437
+ snapshot = _group_status(task_id, run_group_id)
438
+ if snapshot["total"] == 0:
439
+ yield "event: not_found\ndata: {}\n\n"
440
+ return
441
+ yield f"event: snapshot\ndata: {json.dumps(snapshot)}\n\n"
442
+ if snapshot["completed"] + snapshot["failed"] == snapshot["total"]:
443
+ yield "event: complete\ndata: {}\n\n"
444
+ return
445
+ await asyncio.sleep(RUN_GROUP_STREAM_POLL_SECONDS)
446
+ yield "event: timeout\ndata: {}\n\n"
447
+
448
+ return StreamingResponse(events(), media_type="text/event-stream")
449
+
450
+
451
+ @router.get("/{task_id}/instances/{instance_id}/progress")
452
+ def instance_progress(task_id: str, instance_id: str) -> dict:
453
+ """Step progress for one instance. Polled while a run is in flight."""
454
+ doc = docs().find_one(TASK_INSTANCES_COLLECTION, Filter.of(instance_id=instance_id))
455
+ if doc is None:
456
+ raise HTTPException(status_code=404, detail=f"instance {instance_id} not found")
457
+ # The poller stops on `done`. It also reads `live_step_index`, deliberately not
458
+ # sent: `current_step` counts completed steps rather than indexing them, and a
459
+ # `depends_on` DAG completes out of order, so the count would mark un-started
460
+ # steps done. The component's firstPending fallback handles that correctly.
461
+ status = doc.get("status")
462
+ current_step = doc.get("current_step")
463
+ try:
464
+ # The instance store spells it "cancelled"; RunStatus spells it CANCELED.
465
+ terminal = RunStatus("CANCELED" if status == "cancelled" else str(status).upper()).is_terminal
466
+ except ValueError:
467
+ terminal = False # an unrecognised status keeps polling rather than stalling the view
468
+ return {
469
+ "instance_id": instance_id,
470
+ "status": status,
471
+ "current_step": current_step,
472
+ "total_steps": doc.get("total_steps"),
473
+ "completed_steps": doc.get("completed_steps") or [],
474
+ "done": terminal,
475
+ }
@@ -0,0 +1,59 @@
1
+ """Entry-point plugins: register types from an installed package.
2
+
3
+ An installed distribution adds a class to a registry by declaring an entry point. The group
4
+ says what kind of thing it is, the entry-point name is the registry key, and the value is the
5
+ class::
6
+
7
+ [project.entry-points."agent_env.envs"]
8
+ browser = "agentenv_browser.env:BrowserEnv"
9
+
10
+ A plugin's own settings live in the config file under ``[plugins.<its distribution name>]``, and
11
+ ``settings`` reads them.
12
+
13
+ This package's top level is the public surface: the group names, ``load_failures``, ``settings``,
14
+ and ``inventory`` with the types it returns; its underscored modules are internal. The full contract
15
+ is in the README ("Register types from an installed package", "Plugin settings"), and the codes the
16
+ inventory reports are in "Plugin report format": codes are stable, reason text is not.
17
+ """
18
+
19
+ from agent_env.config.plugin_tables import settings
20
+ from agent_env.plugins._inventory import (
21
+ Claimant,
22
+ Contribution,
23
+ Diagnostic,
24
+ Distribution,
25
+ Inventory,
26
+ Replacement,
27
+ Status,
28
+ inventory,
29
+ )
30
+ from agent_env.plugins._registration import (
31
+ ARTIFACTS,
32
+ ENV_PROVIDERS,
33
+ ENVS,
34
+ EXPLORER_PLUGINS,
35
+ SANDBOX_PROVIDERS,
36
+ STATE_PROVIDERS,
37
+ TASK_STEPS,
38
+ load_failures,
39
+ )
40
+
41
+ __all__ = [
42
+ "ARTIFACTS",
43
+ "ENV_PROVIDERS",
44
+ "ENVS",
45
+ "EXPLORER_PLUGINS",
46
+ "SANDBOX_PROVIDERS",
47
+ "STATE_PROVIDERS",
48
+ "TASK_STEPS",
49
+ "Claimant",
50
+ "Contribution",
51
+ "Diagnostic",
52
+ "Distribution",
53
+ "Inventory",
54
+ "Replacement",
55
+ "Status",
56
+ "inventory",
57
+ "load_failures",
58
+ "settings",
59
+ ]