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.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- 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
|
+
)
|