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,238 @@
|
|
|
1
|
+
"""Helper Functions for interacting with A2A agents (like sending a task to an A2A Agent)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from a2a.types import TaskState
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_TERMINAL_TASK_STATES = frozenset({
|
|
18
|
+
TaskState.completed, TaskState.failed, TaskState.canceled, TaskState.rejected,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_ERROR_CODE = re.compile(r"[a-z][a-z0-9_]{0,63}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def raise_for_extension_status(
|
|
26
|
+
response: httpx.Response, *, operation: str, include_body: bool = False
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Raise ``httpx.HTTPStatusError`` naming the SDK error code, but no other part of a body
|
|
29
|
+
that may echo a grant. ``include_body`` keeps the body, for a request that sent no grant."""
|
|
30
|
+
try:
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
except httpx.HTTPStatusError as exc:
|
|
33
|
+
if include_body:
|
|
34
|
+
detail = response.text[:500]
|
|
35
|
+
else:
|
|
36
|
+
try:
|
|
37
|
+
body = response.json()
|
|
38
|
+
except ValueError:
|
|
39
|
+
body = None
|
|
40
|
+
error = body.get("error") if isinstance(body, Mapping) else None
|
|
41
|
+
code = error.get("code") if isinstance(error, Mapping) else None
|
|
42
|
+
detail = code if isinstance(code, str) and _ERROR_CODE.fullmatch(code) else ""
|
|
43
|
+
message = f"{operation} failed with HTTP {response.status_code}"
|
|
44
|
+
if detail:
|
|
45
|
+
message += f": {detail}"
|
|
46
|
+
raise httpx.HTTPStatusError(message, request=exc.request, response=response) from exc
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def post_agent_config(url: str, payload: dict, timeout_seconds: int = 60) -> None:
|
|
50
|
+
"""POST to an agent's /ext/agent-config endpoint with retry on transient 404/connect errors.
|
|
51
|
+
|
|
52
|
+
`url` should be the full endpoint URL (a2a_url + endpoint path from the agent card).
|
|
53
|
+
"""
|
|
54
|
+
max_attempts = 10
|
|
55
|
+
delay = 2.0
|
|
56
|
+
last_exc: Exception | None = None
|
|
57
|
+
for attempt in range(1, max_attempts + 1):
|
|
58
|
+
try:
|
|
59
|
+
async with httpx.AsyncClient() as client:
|
|
60
|
+
resp = await client.post(url, json=payload, timeout=timeout_seconds)
|
|
61
|
+
if resp.status_code == 404:
|
|
62
|
+
raise httpx.HTTPStatusError(
|
|
63
|
+
f"agent-config endpoint not available (404) at {url} after {max_attempts} "
|
|
64
|
+
f"attempts — the agent gateway isn't serving it (either boot up/initialization "
|
|
65
|
+
f"failed, or the agent gateway was torn down by TTL reasons).",
|
|
66
|
+
request=resp.request, response=resp,
|
|
67
|
+
)
|
|
68
|
+
resp.raise_for_status()
|
|
69
|
+
if attempt > 1:
|
|
70
|
+
logger.info(f"agent-config POST succeeded on attempt {attempt}")
|
|
71
|
+
return
|
|
72
|
+
except (httpx.ConnectError, httpx.ReadTimeout, httpx.RemoteProtocolError) as e:
|
|
73
|
+
last_exc = e
|
|
74
|
+
logger.warning(f"agent-config POST failed (attempt {attempt}/{max_attempts}, will retry): {e}")
|
|
75
|
+
except httpx.HTTPStatusError as e:
|
|
76
|
+
if e.response.status_code != 404:
|
|
77
|
+
raise
|
|
78
|
+
last_exc = e
|
|
79
|
+
logger.warning(f"agent-config POST got 404 (attempt {attempt}/{max_attempts}, will retry): {url}")
|
|
80
|
+
if attempt < max_attempts:
|
|
81
|
+
await asyncio.sleep(delay)
|
|
82
|
+
delay = min(delay * 2, 15.0)
|
|
83
|
+
assert last_exc is not None
|
|
84
|
+
raise last_exc
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def send_a2a_message(
|
|
88
|
+
a2a_url: str,
|
|
89
|
+
parts: list[dict],
|
|
90
|
+
message_id: str,
|
|
91
|
+
context_id: Optional[str],
|
|
92
|
+
timeout_seconds: int,
|
|
93
|
+
) -> tuple[str, Optional[str]]:
|
|
94
|
+
"""POST /a2a message/send. Returns (task_id, resolved_context_id). Retries on connect errors."""
|
|
95
|
+
message: dict[str, Any] = {
|
|
96
|
+
"messageId": message_id, "role": "user",
|
|
97
|
+
"parts": parts,
|
|
98
|
+
}
|
|
99
|
+
if context_id is not None:
|
|
100
|
+
message["contextId"] = context_id
|
|
101
|
+
payload = {
|
|
102
|
+
"jsonrpc": "2.0", "id": "1", "method": "message/send",
|
|
103
|
+
"params": {"message": message, "configuration": {"blocking": False}},
|
|
104
|
+
}
|
|
105
|
+
max_attempts = 5
|
|
106
|
+
delay = 2.0
|
|
107
|
+
last_exc: Exception | None = None
|
|
108
|
+
for attempt in range(1, max_attempts + 1):
|
|
109
|
+
try:
|
|
110
|
+
async with httpx.AsyncClient() as client:
|
|
111
|
+
resp = await client.post(f"{a2a_url}/a2a", json=payload, timeout=timeout_seconds)
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
send_body = resp.json()
|
|
114
|
+
if "error" in send_body:
|
|
115
|
+
raise RuntimeError(f"A2A message/send failed: {send_body['error']}")
|
|
116
|
+
send_result = send_body.get("result") or {}
|
|
117
|
+
task_id = send_result.get("id")
|
|
118
|
+
if not task_id:
|
|
119
|
+
raise RuntimeError(f"A2A message/send succeeded but returned no task id: {send_result}")
|
|
120
|
+
resolved_context_id = context_id or send_result.get("contextId")
|
|
121
|
+
return task_id, resolved_context_id
|
|
122
|
+
except (httpx.ConnectError, httpx.ConnectTimeout) as e:
|
|
123
|
+
last_exc = e
|
|
124
|
+
logger.warning(f"message/send connect error (attempt {attempt}/{max_attempts}, will retry): {e}")
|
|
125
|
+
if attempt < max_attempts:
|
|
126
|
+
await asyncio.sleep(delay)
|
|
127
|
+
delay = min(delay * 2, 15.0)
|
|
128
|
+
assert last_exc is not None
|
|
129
|
+
raise last_exc
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def poll_a2a_task(
|
|
133
|
+
a2a_url: str,
|
|
134
|
+
task_id: str,
|
|
135
|
+
timeout_seconds: int,
|
|
136
|
+
poll_interval_seconds: int = 10,
|
|
137
|
+
) -> dict:
|
|
138
|
+
"""POST /a2a tasks/get until status.state is 'completed' or 'failed'. Returns the result dict."""
|
|
139
|
+
deadline = time.monotonic() + timeout_seconds
|
|
140
|
+
consecutive_failures = 0
|
|
141
|
+
while time.monotonic() < deadline:
|
|
142
|
+
backoff = min(poll_interval_seconds + consecutive_failures * 5, 60)
|
|
143
|
+
await asyncio.sleep(backoff)
|
|
144
|
+
try:
|
|
145
|
+
async with httpx.AsyncClient() as client:
|
|
146
|
+
resp = await client.post(f"{a2a_url}/a2a", json={
|
|
147
|
+
"jsonrpc": "2.0", "id": "poll", "method": "tasks/get",
|
|
148
|
+
"params": {"id": task_id},
|
|
149
|
+
}, timeout=30)
|
|
150
|
+
resp.raise_for_status()
|
|
151
|
+
data = resp.json()
|
|
152
|
+
if "error" in data:
|
|
153
|
+
consecutive_failures += 1
|
|
154
|
+
logger.warning(f"A2A poll returned error (consec={consecutive_failures}, will retry): {data['error']}")
|
|
155
|
+
continue
|
|
156
|
+
result = data["result"]
|
|
157
|
+
except httpx.HTTPStatusError as e:
|
|
158
|
+
if 400 <= e.response.status_code < 500:
|
|
159
|
+
raise
|
|
160
|
+
consecutive_failures += 1
|
|
161
|
+
logger.warning(f"A2A poll got {e.response.status_code} (consec={consecutive_failures}, will retry)")
|
|
162
|
+
continue
|
|
163
|
+
except httpx.HTTPError as e:
|
|
164
|
+
consecutive_failures += 1
|
|
165
|
+
logger.warning(f"A2A poll failed (consec={consecutive_failures}, will retry): {type(e).__name__}: {e}")
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
consecutive_failures = 0
|
|
169
|
+
state = result["status"]["state"]
|
|
170
|
+
if state in _TERMINAL_TASK_STATES:
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
raise TimeoutError(f"A2A task {task_id} did not complete within {timeout_seconds}s")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class TerminalResponse:
|
|
178
|
+
"""What's pulled from an A2A terminal status message: the text reply, the typed
|
|
179
|
+
``structured_output`` (when ``output_format`` was set), and tool/error telemetry."""
|
|
180
|
+
|
|
181
|
+
response_text: str
|
|
182
|
+
tool_call_count: Optional[int] = None
|
|
183
|
+
error_type: Optional[str] = None
|
|
184
|
+
error_code: Optional[str] = None
|
|
185
|
+
error_class: Optional[str] = None
|
|
186
|
+
error_message: Optional[str] = None
|
|
187
|
+
structured_output: Optional[Any] = None
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_message(cls, status_message: dict) -> "TerminalResponse":
|
|
191
|
+
"""Parse an A2A terminal status message. Per A2A spec 0.3 the final agent message
|
|
192
|
+
(TaskUpdater.complete/failed) lives in result.status.message, not result.history."""
|
|
193
|
+
response_text = ""
|
|
194
|
+
tool_call_count: Optional[int] = None
|
|
195
|
+
error_type: Optional[str] = None
|
|
196
|
+
error_code: Optional[str] = None
|
|
197
|
+
error_class: Optional[str] = None
|
|
198
|
+
error_message: Optional[str] = None
|
|
199
|
+
structured_output: Optional[Any] = None
|
|
200
|
+
for part in (status_message or {}).get("parts", []):
|
|
201
|
+
kind = part.get("kind")
|
|
202
|
+
if kind == "text":
|
|
203
|
+
response_text = part["text"]
|
|
204
|
+
elif kind == "data":
|
|
205
|
+
data = part.get("data", {})
|
|
206
|
+
if "tool_call_count" in data:
|
|
207
|
+
tool_call_count = data["tool_call_count"]
|
|
208
|
+
usage = data.get("usage")
|
|
209
|
+
if isinstance(usage, dict) and "tool_call_count" in usage:
|
|
210
|
+
tool_call_count = usage["tool_call_count"]
|
|
211
|
+
if "error_type" in data:
|
|
212
|
+
error_type = data["error_type"]
|
|
213
|
+
if "error_code" in data:
|
|
214
|
+
error_code = data["error_code"]
|
|
215
|
+
if "error_class" in data:
|
|
216
|
+
error_class = data["error_class"]
|
|
217
|
+
if "error_message" in data:
|
|
218
|
+
error_message = data["error_message"]
|
|
219
|
+
if "structured_output" in data:
|
|
220
|
+
structured_output = data["structured_output"]
|
|
221
|
+
return cls(
|
|
222
|
+
response_text=response_text,
|
|
223
|
+
tool_call_count=tool_call_count,
|
|
224
|
+
error_type=error_type,
|
|
225
|
+
error_code=error_code,
|
|
226
|
+
error_class=error_class,
|
|
227
|
+
error_message=error_message,
|
|
228
|
+
structured_output=structured_output,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def extract_terminal_response(
|
|
233
|
+
status_message: dict,
|
|
234
|
+
) -> tuple[str, Optional[int], Optional[str], Optional[str], Optional[str]]:
|
|
235
|
+
"""Back-compat shim over ``TerminalResponse.from_message``; external callers
|
|
236
|
+
(the hub's agents.py) still unpack the 5-tuple. Drop once they migrate."""
|
|
237
|
+
tr = TerminalResponse.from_message(status_message)
|
|
238
|
+
return tr.response_text, tr.tool_call_count, tr.error_type, tr.error_class, tr.error_message
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""A2A Agent store for MongoDB persistence."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import dataclasses
|
|
5
|
+
import logging
|
|
6
|
+
import random
|
|
7
|
+
import string
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Optional, Self
|
|
10
|
+
|
|
11
|
+
from agent_env.store.base import ConcurrentModificationError, NotFoundError
|
|
12
|
+
from agent_env.config import get_config
|
|
13
|
+
from agent_env.store.document_store import (
|
|
14
|
+
AbsentOrNull,
|
|
15
|
+
Filter,
|
|
16
|
+
LteOrAbsent,
|
|
17
|
+
UpdateSpec,
|
|
18
|
+
VersionedEntityStore,
|
|
19
|
+
VersionedEntityStoreCache,
|
|
20
|
+
)
|
|
21
|
+
from agent_env.store.query import QueryBuilder, to_document_query
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from agent_env.a2a_agent.a2a_agent import A2AAgent
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
A2A_AGENTS_COLLECTION = "a2a_agents"
|
|
29
|
+
A2A_AGENT_INSTANCES_COLLECTION = "a2a_agent_instances"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class A2AAgentQuery(QueryBuilder["A2AAgent"]):
|
|
33
|
+
def __init__(self, store: Optional[A2AAgentStore] = None) -> None:
|
|
34
|
+
super().__init__()
|
|
35
|
+
self._store = store
|
|
36
|
+
|
|
37
|
+
def _clone(self) -> Self:
|
|
38
|
+
clone = A2AAgentQuery(self._store)
|
|
39
|
+
clone._filters = self._filters.copy()
|
|
40
|
+
clone._sort_field = self._sort_field
|
|
41
|
+
clone._sort_desc = self._sort_desc
|
|
42
|
+
clone._limit_value = self._limit_value
|
|
43
|
+
clone._offset_value = self._offset_value
|
|
44
|
+
return clone
|
|
45
|
+
|
|
46
|
+
def execute(self) -> list[A2AAgent]:
|
|
47
|
+
if self._store is None:
|
|
48
|
+
self._store = get_a2a_agent_store()
|
|
49
|
+
return self._store.execute_query(self)
|
|
50
|
+
|
|
51
|
+
def _execute_count(self) -> int:
|
|
52
|
+
if self._store is None:
|
|
53
|
+
self._store = get_a2a_agent_store()
|
|
54
|
+
return self._store.execute_count(self)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class A2AAgentStore:
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._versioned_cache: VersionedEntityStoreCache[A2AAgent] = VersionedEntityStoreCache(
|
|
60
|
+
A2A_AGENTS_COLLECTION, self._serialize, self._deserialize, secondary_indexes=[["type"]],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def _doc_store(self):
|
|
65
|
+
return get_config().get_document_store()
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def _versioned(self) -> VersionedEntityStore[A2AAgent]:
|
|
69
|
+
return self._versioned_cache.for_store(self._doc_store)
|
|
70
|
+
|
|
71
|
+
def _serialize(self, agent: A2AAgent) -> dict:
|
|
72
|
+
doc = agent.to_dict()
|
|
73
|
+
doc["created_at_utc"] = datetime.now(timezone.utc)
|
|
74
|
+
return doc
|
|
75
|
+
|
|
76
|
+
def get(self, id: str, version: Optional[int] = None) -> A2AAgent:
|
|
77
|
+
agent = self._versioned.get(id, version)
|
|
78
|
+
if agent is None:
|
|
79
|
+
version_str = f" version={version}" if version is not None else ""
|
|
80
|
+
raise NotFoundError(f"A2AAgent {id}{version_str} not found")
|
|
81
|
+
return agent
|
|
82
|
+
|
|
83
|
+
def next_version(self, id: str) -> int:
|
|
84
|
+
return self._versioned.next_version(id)
|
|
85
|
+
|
|
86
|
+
def put_document(self, agent: A2AAgent) -> A2AAgent:
|
|
87
|
+
agent.version = self._versioned.put(agent)
|
|
88
|
+
return agent
|
|
89
|
+
|
|
90
|
+
def update_metadata(self, id: str, version: int, old_metadata: dict[str, Any], new_metadata: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
"""Replace metadata using timestamp-based CAS (same pattern as EnvStore)."""
|
|
92
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
93
|
+
old_ts = old_metadata.get("updated_at")
|
|
94
|
+
new_metadata = {**new_metadata, "updated_at": now}
|
|
95
|
+
|
|
96
|
+
ts_pred = LteOrAbsent(old_ts) if old_ts is not None else AbsentOrNull()
|
|
97
|
+
store = self._doc_store
|
|
98
|
+
matched = store.update(
|
|
99
|
+
A2A_AGENTS_COLLECTION,
|
|
100
|
+
Filter.of(id=id, version=version).where("metadata.updated_at", ts_pred),
|
|
101
|
+
UpdateSpec(set={"metadata": new_metadata}),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if matched == 0:
|
|
105
|
+
if store.find_one(A2A_AGENTS_COLLECTION, Filter.of(id=id, version=version)) is None:
|
|
106
|
+
raise NotFoundError(f"A2AAgent {id} version={version} not found")
|
|
107
|
+
raise ConcurrentModificationError(f"A2AAgent {id} version={version} metadata was modified concurrently")
|
|
108
|
+
|
|
109
|
+
return new_metadata
|
|
110
|
+
|
|
111
|
+
def _deserialize(self, doc: dict) -> A2AAgent:
|
|
112
|
+
from agent_env.a2a_agent.a2a_agent import A2AAgent
|
|
113
|
+
|
|
114
|
+
doc.pop("_id", None)
|
|
115
|
+
doc.pop("created_at_utc", None)
|
|
116
|
+
return A2AAgent.from_dict(doc)
|
|
117
|
+
|
|
118
|
+
def execute_query(self, query: A2AAgentQuery) -> list[A2AAgent]:
|
|
119
|
+
filt, sort = to_document_query(query)
|
|
120
|
+
return self._versioned.query(filt, sort, query._limit_value, query._offset_value)
|
|
121
|
+
|
|
122
|
+
def execute_count(self, query: A2AAgentQuery) -> int:
|
|
123
|
+
filt, _ = to_document_query(query)
|
|
124
|
+
return self._versioned.count(filt)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_a2a_agent_store: Optional[A2AAgentStore] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_a2a_agent_store() -> A2AAgentStore:
|
|
131
|
+
global _a2a_agent_store
|
|
132
|
+
if _a2a_agent_store is None:
|
|
133
|
+
_a2a_agent_store = A2AAgentStore()
|
|
134
|
+
return _a2a_agent_store
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def set_a2a_agent_store(store: A2AAgentStore) -> None:
|
|
138
|
+
global _a2a_agent_store
|
|
139
|
+
_a2a_agent_store = store
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def reset_a2a_agent_store() -> None:
|
|
143
|
+
global _a2a_agent_store
|
|
144
|
+
_a2a_agent_store = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class A2AAgentInstanceStore:
|
|
148
|
+
def __init__(self) -> None:
|
|
149
|
+
self._indexed = None
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def _doc_store(self):
|
|
153
|
+
# Resolved per call: a cached store outlives reset_config(), so a process that
|
|
154
|
+
# re-pointed would read the new config and write the old backend.
|
|
155
|
+
store = get_config().get_document_store()
|
|
156
|
+
if self._indexed is not store:
|
|
157
|
+
store.ensure_index(A2A_AGENT_INSTANCES_COLLECTION, ["instance_id"], unique=True)
|
|
158
|
+
self._indexed = store
|
|
159
|
+
return store
|
|
160
|
+
|
|
161
|
+
def create_instance(self, deployed: "DeployedA2AAgent", ttl_seconds: int) -> "DeployedA2AAgent":
|
|
162
|
+
from agent_env.a2a_agent.a2a_agent import DeployedA2AAgent
|
|
163
|
+
suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
|
164
|
+
instance_id = f"{deployed.agent_id}-{suffix}"
|
|
165
|
+
now = datetime.now(timezone.utc)
|
|
166
|
+
created_at_utc = now.strftime("%Y-%m-%d %H:%M UTC")
|
|
167
|
+
expires_at_utc = (now + timedelta(seconds=ttl_seconds)).strftime("%Y-%m-%d %H:%M UTC")
|
|
168
|
+
doc = dataclasses.asdict(deployed) | {
|
|
169
|
+
"instance_id": instance_id,
|
|
170
|
+
"created_at_utc": created_at_utc,
|
|
171
|
+
"expires_at_utc": expires_at_utc,
|
|
172
|
+
}
|
|
173
|
+
self._doc_store.insert(A2A_AGENT_INSTANCES_COLLECTION, doc)
|
|
174
|
+
return DeployedA2AAgent.from_dict(doc)
|
|
175
|
+
|
|
176
|
+
def get(self, instance_id: str) -> "DeployedA2AAgent":
|
|
177
|
+
from agent_env.a2a_agent.a2a_agent import DeployedA2AAgent
|
|
178
|
+
doc = self._doc_store.find_one(A2A_AGENT_INSTANCES_COLLECTION, Filter.of(instance_id=instance_id))
|
|
179
|
+
if not doc:
|
|
180
|
+
raise NotFoundError(f"A2AAgentInstance '{instance_id}' not found")
|
|
181
|
+
return DeployedA2AAgent.from_dict(doc)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
_a2a_agent_instance_store: Optional[A2AAgentInstanceStore] = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_a2a_agent_instance_store() -> A2AAgentInstanceStore:
|
|
188
|
+
global _a2a_agent_instance_store
|
|
189
|
+
if _a2a_agent_instance_store is None:
|
|
190
|
+
_a2a_agent_instance_store = A2AAgentInstanceStore()
|
|
191
|
+
return _a2a_agent_instance_store
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def set_a2a_agent_instance_store(store: A2AAgentInstanceStore) -> None:
|
|
195
|
+
global _a2a_agent_instance_store
|
|
196
|
+
_a2a_agent_instance_store = store
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def reset_a2a_agent_instance_store() -> None:
|
|
200
|
+
global _a2a_agent_instance_store
|
|
201
|
+
_a2a_agent_instance_store = None
|