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,356 @@
1
+ """DocumentStore-backed store for A2A conversations.
2
+
3
+ Each conversation is a multi-turn A2A interaction keyed by `conversation_id`
4
+ (== A2A `contextId`). v1 use case is human-in-the-loop hosted by the
5
+ hub backend; future services that host an A2A endpoint can import
6
+ this module and operate on the same collection.
7
+
8
+ A conversation contains an append-only `messages` array plus a parallel
9
+ `a2a_tasks` array tracking each A2A round-trip (one a2a_task per
10
+ `message/send`). The `a2a_` prefix disambiguates these protocol-level tasks
11
+ from agent-env's own `Task` / `task_instance_id` vocabulary. Each a2a_task
12
+ carries `input_message_idx` / `response_message_idx` pointers into
13
+ `messages` so a `tasks/get` poll can resolve to the right response without
14
+ scanning.
15
+
16
+ All operations are synchronous. Call from `async def` handlers via
17
+ `loop.run_in_executor(...)` to avoid blocking the event loop.
18
+ """
19
+
20
+ import logging
21
+ from datetime import datetime, timezone
22
+ from typing import Optional
23
+
24
+ from a2a.types import TaskState
25
+
26
+ from agent_env.store import (
27
+ Eq,
28
+ Filter,
29
+ Sort,
30
+ UpdateSpec,
31
+ compare_and_swap,
32
+ get_config,
33
+ rev_precondition,
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ _COLLECTION_NAME = "agent_env_a2a_conversations"
39
+ _REV = "rev"
40
+
41
+ _MAX_CAS_ATTEMPTS = 100
42
+
43
+
44
+ def _doc_store():
45
+ return get_config().get_document_store()
46
+
47
+
48
+ def ensure_indexes() -> None:
49
+ """Create indexes for the a2a_conversations collection. Idempotent.
50
+
51
+ No TTL — conversations are retained indefinitely for audit and replay.
52
+ """
53
+ docs = _doc_store()
54
+ docs.ensure_index(_COLLECTION_NAME, ["conversation_id"], unique=True)
55
+ docs.ensure_index(_COLLECTION_NAME, ["task_instance_id"])
56
+ docs.ensure_index(_COLLECTION_NAME, ["a2a_tasks.a2a_task_id"])
57
+ docs.ensure_index(_COLLECTION_NAME, ["pending", "status", "updated_at_utc"])
58
+
59
+
60
+ def create_conversation(
61
+ conversation_id: str,
62
+ task_instance_id: str,
63
+ source_agent_name: str,
64
+ target_agent_name: str,
65
+ ) -> dict:
66
+ """Insert a new conversation in the `active` state with empty messages and a2a_tasks.
67
+
68
+ `source_agent_name` identifies the initiator (the side that calls first).
69
+ `target_agent_name` identifies the responder (the side being called). For
70
+ the canonical PromptAgent multi-turn case: source="human_agent" (or a
71
+ user-sim name like "hana_kim"), target=the solver's in-task DAG name. For
72
+ HITL where a solver consults a human peer: source=the solver's name,
73
+ target="human_agent".
74
+
75
+ Raises `agent_env.store.DuplicateKeyError` if `conversation_id` already
76
+ exists — callers should treat that as "use the existing conversation."
77
+ """
78
+ now = datetime.now(timezone.utc)
79
+ doc = {
80
+ "conversation_id": conversation_id,
81
+ "task_instance_id": task_instance_id,
82
+ "source_agent_name": source_agent_name,
83
+ "target_agent_name": target_agent_name,
84
+ "messages": [],
85
+ "a2a_tasks": [],
86
+ "pending": False,
87
+ "status": "active",
88
+ _REV: 0,
89
+ "created_at_utc": now,
90
+ "updated_at_utc": now,
91
+ }
92
+ _doc_store().insert(_COLLECTION_NAME, doc)
93
+ return doc
94
+
95
+
96
+ def get_conversation(conversation_id: str) -> Optional[dict]:
97
+ """Return the conversation by id, or None if not found."""
98
+ return _doc_store().find_one(_COLLECTION_NAME, Filter.of(conversation_id=conversation_id))
99
+
100
+
101
+ def find_by_a2a_task_id(a2a_task_id: str) -> Optional[dict]:
102
+ """Lookup the conversation containing an a2a_task with the given id.
103
+
104
+ Used by `tasks/get` polls — the multikey index on `a2a_tasks.a2a_task_id`
105
+ makes this a single-key index hit even though `a2a_tasks` is an array.
106
+ """
107
+ return _doc_store().find_one(
108
+ _COLLECTION_NAME, Filter().where("a2a_tasks.a2a_task_id", Eq(a2a_task_id))
109
+ )
110
+
111
+
112
+ def add_a2a_task(
113
+ conversation_id: str,
114
+ parts: list[dict],
115
+ a2a_task_id: str,
116
+ role: str,
117
+ ) -> Optional[dict]:
118
+ """Append a caller-side message and create a new working a2a_task.
119
+
120
+ Called whenever an A2A `message/send` fires. The caller's role depends on
121
+ the conversation direction:
122
+ - PromptAgent multi-turn (canonical): user is the caller → role="user"
123
+ - HITL (solver consulting human peer): solver is the caller → role="agent"
124
+
125
+ Reads the conversation to compute `input_message_idx` (the index the new
126
+ message lands at), then appends both the message and the new a2a_task under
127
+ a `rev` compare-and-swap. A concurrent write that landed first bumps `rev`,
128
+ fails the swap, and we retry with a fresh index, so `input_message_idx`
129
+ always names the message we actually appended. Both arrays are pushed
130
+ (never rewritten), so no entry is dropped.
131
+
132
+ Returns the post-update conversation doc, or None if `conversation_id`
133
+ doesn't exist.
134
+ """
135
+ now = datetime.now(timezone.utc)
136
+ new_message = {"role": role, "parts": parts, "ts": now.isoformat()}
137
+
138
+ def _mutate(doc: dict) -> UpdateSpec:
139
+ idx = len(doc.get("messages", []))
140
+ new_a2a_task = {
141
+ "a2a_task_id": a2a_task_id,
142
+ "state": TaskState.working.value,
143
+ "requested_at": now.isoformat(),
144
+ "input_message_idx": idx,
145
+ }
146
+ return UpdateSpec(
147
+ push={"messages": [new_message], "a2a_tasks": [new_a2a_task]},
148
+ set={"pending": True, "updated_at_utc": now},
149
+ )
150
+
151
+ return compare_and_swap(
152
+ _doc_store(),
153
+ _COLLECTION_NAME,
154
+ Filter.of(conversation_id=conversation_id),
155
+ _mutate,
156
+ counter_field=_REV,
157
+ )
158
+
159
+
160
+ def complete_a2a_task(
161
+ conversation_id: str,
162
+ parts: list[dict],
163
+ role: str,
164
+ ) -> Optional[dict]:
165
+ """Append a responder-side message and complete the oldest working a2a_task.
166
+
167
+ Called when the responder replies. The responder's role depends on the
168
+ conversation direction:
169
+ - PromptAgent multi-turn (canonical): agent is the responder → role="agent"
170
+ - HITL (human replying to solver's consult): human is the responder → role="user"
171
+
172
+ Compare-and-swap on the conversation `rev`, so `response_message_idx` names
173
+ the message we actually appended even if a concurrent write raced us (we
174
+ retry with a fresh index). The target a2a_task is additionally guarded on
175
+ `state == "working"`. A failed swap is disambiguated by a re-read: a
176
+ concurrent write that left the target working → retry; the target no longer
177
+ working → return None (the message is not appended, matching the pre-port
178
+ guarded update).
179
+
180
+ If there is no working a2a_task (the responder is replying out-of-band with
181
+ no in-flight A2A request), the message is appended and no a2a_task is
182
+ touched.
183
+
184
+ Returns the post-update conversation doc, or None if `conversation_id`
185
+ doesn't exist (or the target task lost the state race).
186
+ """
187
+ now = datetime.now(timezone.utc)
188
+ new_message = {"role": role, "parts": parts, "ts": now.isoformat()}
189
+ # One backend for the whole retry loop: a reset between the read and the CAS would
190
+ # decide against one database and write to another.
191
+ docs = _doc_store()
192
+
193
+ for _ in range(_MAX_CAS_ATTEMPTS):
194
+ existing = docs.find_one(
195
+ _COLLECTION_NAME, Filter.of(conversation_id=conversation_id)
196
+ )
197
+ if existing is None:
198
+ return None
199
+
200
+ idx = len(existing.get("messages", []))
201
+ oldest_working_idx: Optional[int] = next(
202
+ (
203
+ i
204
+ for i, t in enumerate(existing.get("a2a_tasks", []))
205
+ if t.get("state") == TaskState.working.value
206
+ ),
207
+ None,
208
+ )
209
+
210
+ spec = UpdateSpec(
211
+ push={"messages": [new_message]},
212
+ set={"pending": False, "updated_at_utc": now},
213
+ inc={_REV: 1},
214
+ )
215
+ filter_ = Filter.of(conversation_id=conversation_id).where(
216
+ _REV, rev_precondition(existing, _REV)
217
+ )
218
+ if oldest_working_idx is not None:
219
+ spec.set[f"a2a_tasks.{oldest_working_idx}.state"] = TaskState.completed.value
220
+ spec.set[f"a2a_tasks.{oldest_working_idx}.response_message_idx"] = idx
221
+ spec.set[f"a2a_tasks.{oldest_working_idx}.ended_at"] = now.isoformat()
222
+ filter_ = filter_.where(
223
+ f"a2a_tasks.{oldest_working_idx}.state", Eq(TaskState.working.value)
224
+ )
225
+
226
+ result = docs.update_one_and_get(_COLLECTION_NAME, filter_, spec)
227
+ if result is not None:
228
+ return result
229
+
230
+ if oldest_working_idx is None:
231
+ continue
232
+
233
+ recheck = docs.find_one(
234
+ _COLLECTION_NAME, Filter.of(conversation_id=conversation_id)
235
+ )
236
+ if recheck is None:
237
+ return None
238
+ tasks = recheck.get("a2a_tasks", [])
239
+ still_working = (
240
+ oldest_working_idx < len(tasks)
241
+ and tasks[oldest_working_idx].get("state") == TaskState.working.value
242
+ )
243
+ if not still_working:
244
+ return None
245
+ raise RuntimeError(
246
+ f"complete_a2a_task: rev CAS did not converge for conversation_id={conversation_id}"
247
+ )
248
+
249
+
250
+ def cancel_a2a_task(
251
+ conversation_id: str,
252
+ a2a_task_id: str,
253
+ ) -> Optional[dict]:
254
+ """Mark a specific a2a_task as canceled. Used by A2A `tasks/cancel`.
255
+
256
+ Does not append a message; only updates the a2a_task entry. Locates the
257
+ target by id in Python, then writes under an `a2a_tasks.<i>.state ==
258
+ "working"` precondition so a task that already terminated (or a racing
259
+ writer) yields a no-op.
260
+
261
+ Returns the post-update conversation doc, or None if the conversation or
262
+ task isn't found / wasn't in `working` state.
263
+ """
264
+ now = datetime.now(timezone.utc)
265
+ docs = _doc_store()
266
+ existing = docs.find_one(_COLLECTION_NAME, Filter.of(conversation_id=conversation_id))
267
+ if existing is None:
268
+ return None
269
+
270
+ target_idx: Optional[int] = next(
271
+ (
272
+ i
273
+ for i, t in enumerate(existing.get("a2a_tasks", []))
274
+ if t.get("a2a_task_id") == a2a_task_id and t.get("state") == TaskState.working.value
275
+ ),
276
+ None,
277
+ )
278
+ if target_idx is None:
279
+ return None
280
+
281
+ return docs.update_one_and_get(
282
+ _COLLECTION_NAME,
283
+ Filter.of(conversation_id=conversation_id).where(
284
+ f"a2a_tasks.{target_idx}.state", Eq(TaskState.working.value)
285
+ ),
286
+ UpdateSpec(
287
+ set={
288
+ f"a2a_tasks.{target_idx}.state": TaskState.canceled.value,
289
+ f"a2a_tasks.{target_idx}.ended_at": now.isoformat(),
290
+ "updated_at_utc": now,
291
+ },
292
+ inc={_REV: 1},
293
+ ),
294
+ )
295
+
296
+
297
+ def mark_closed(conversation_id: str) -> Optional[dict]:
298
+ """Set `status=closed` and cancel any working a2a_tasks.
299
+
300
+ Reads the conversation, cancels working a2a_tasks in Python, and writes the
301
+ transformed array back under a `rev` compare-and-swap so a concurrently
302
+ appended a2a_task (e.g. a `start_a2a_task` push that lands between the read
303
+ and the write) is not clobbered by this whole-array write. On a lost swap we
304
+ re-read and re-transform; a task that landed mid-close is then canceled too,
305
+ which is correct for a terminal close.
306
+
307
+ Returns the post-update conversation doc, or None if not found.
308
+ """
309
+ docs = _doc_store()
310
+ for _ in range(_MAX_CAS_ATTEMPTS):
311
+ now = datetime.now(timezone.utc)
312
+ existing = docs.find_one(
313
+ _COLLECTION_NAME, Filter.of(conversation_id=conversation_id)
314
+ )
315
+ if existing is None:
316
+ return None
317
+
318
+ new_tasks = [
319
+ {**t, "state": TaskState.canceled.value, "ended_at": now.isoformat()}
320
+ if t.get("state") == TaskState.working.value
321
+ else t
322
+ for t in existing.get("a2a_tasks", [])
323
+ ]
324
+ result = docs.update_one_and_get(
325
+ _COLLECTION_NAME,
326
+ Filter.of(conversation_id=conversation_id).where(
327
+ _REV, rev_precondition(existing, _REV)
328
+ ),
329
+ UpdateSpec(
330
+ set={
331
+ "status": "closed",
332
+ "pending": False,
333
+ "updated_at_utc": now,
334
+ "a2a_tasks": new_tasks,
335
+ },
336
+ inc={_REV: 1},
337
+ ),
338
+ )
339
+ if result is not None:
340
+ return result
341
+ raise RuntimeError(
342
+ f"mark_closed: rev CAS did not converge for conversation_id={conversation_id}"
343
+ )
344
+
345
+
346
+ def list_pending(limit: int = 50) -> list[dict]:
347
+ """Return active conversations awaiting a user reply, most-recently-updated first.
348
+
349
+ Backs the hub-frontend Conversations worklist.
350
+ """
351
+ return _doc_store().query(
352
+ _COLLECTION_NAME,
353
+ Filter.of(pending=True, status="active"),
354
+ sort=Sort.by("updated_at_utc", descending=True),
355
+ limit=limit,
356
+ )