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,69 @@
|
|
|
1
|
+
"""Verify the A2A agent-config extension actually mutates the served card."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from agent_env.a2a_agent import A2AAgent
|
|
11
|
+
from agent_env.task_step.context import TaskStepContext
|
|
12
|
+
from agent_env.entity_refs import EntityRef
|
|
13
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VerifyA2AAgentConfigIdentityStep(TaskStep):
|
|
19
|
+
type: ClassVar[str] = "verify_a2a_agent_config_identity"
|
|
20
|
+
entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
|
|
21
|
+
|
|
22
|
+
def __init__(self, id: str, version: Optional[int], a2a_agent_id: str, a2a_agent_version: Optional[int] = None, depends_on: Optional[list[TaskStepDependency]] = None, fail_task_on_error: bool = True):
|
|
23
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
24
|
+
self.a2a_agent_id = a2a_agent_id
|
|
25
|
+
self.a2a_agent_version = a2a_agent_version
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
return {**super().to_dict(), "a2a_agent_id": self.a2a_agent_id, "a2a_agent_version": self.a2a_agent_version}
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, data: dict) -> "VerifyA2AAgentConfigIdentityStep":
|
|
32
|
+
return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], a2a_agent_version=data.get("a2a_agent_version"))
|
|
33
|
+
|
|
34
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
35
|
+
deployed = next((a for a in context.deployed_agents if a.agent_name == TaskStep.DEFAULT_AGENT_NAME), None)
|
|
36
|
+
if deployed is None:
|
|
37
|
+
raise RuntimeError("VerifyA2AAgentConfigIdentityStep: no deployed default-agent in context")
|
|
38
|
+
a2a_url = deployed.a2a_url or deployed.api_url
|
|
39
|
+
ext = A2AAgent.find_extension(deployed.a2a_card or {}, A2AAgent.EXT_AGENT_CONFIG)
|
|
40
|
+
supported = ((ext or {}).get("params") or {}).get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
|
|
41
|
+
supports_identity = "name" in supported and "description" in supported
|
|
42
|
+
|
|
43
|
+
identity_round_trip_ok = False
|
|
44
|
+
if ext is not None and supports_identity:
|
|
45
|
+
name, desc = f"IdentityName-{uuid.uuid4().hex[:12]}", f"IdentityDesc-{uuid.uuid4().hex[:12]}"
|
|
46
|
+
endpoint = a2a_url + (ext.get("params") or {}).get("endpoint", "/ext/agent-config")
|
|
47
|
+
try:
|
|
48
|
+
async with httpx.AsyncClient() as client:
|
|
49
|
+
(await client.post(endpoint, json={"name": name, "description": desc}, timeout=30)).raise_for_status()
|
|
50
|
+
card_resp = await client.get(f"{a2a_url}/.well-known/agent.json", timeout=30)
|
|
51
|
+
card_resp.raise_for_status()
|
|
52
|
+
card = card_resp.json() or {}
|
|
53
|
+
identity_round_trip_ok = card.get("name") == name and card.get("description") == desc
|
|
54
|
+
except httpx.HTTPError as e:
|
|
55
|
+
logger.warning(f"VerifyA2AAgentConfigIdentityStep: round-trip failed: {e}")
|
|
56
|
+
|
|
57
|
+
entry = {
|
|
58
|
+
"supported": ext is not None and supports_identity and identity_round_trip_ok,
|
|
59
|
+
"extension_advertised": ext is not None,
|
|
60
|
+
"supports_identity": supports_identity,
|
|
61
|
+
"identity_round_trip_ok": identity_round_trip_ok,
|
|
62
|
+
}
|
|
63
|
+
agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
|
|
64
|
+
validated_ext = agent.metadata.get("validated_a2a_extensions", {})
|
|
65
|
+
validated_ext[A2AAgent.EXT_AGENT_CONFIG] = entry
|
|
66
|
+
agent.update_metadata({**agent.metadata, "validated_a2a_extensions": validated_ext})
|
|
67
|
+
context.metadata.setdefault("verifications", {})["a2a_agent_config_identity"] = entry
|
|
68
|
+
logger.info(f"agent-config identity validation: {entry}")
|
|
69
|
+
return context
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Verify A2A agent can invoke MCP tools by sending a test prompt and reading the env trajectory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import ClassVar, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from agent_env.task_step.context import TaskStepContext
|
|
14
|
+
from agent_env.entity_refs import EntityRef
|
|
15
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
TEST_PROMPT = (
|
|
20
|
+
"Use the `search_emails` MCP tool (from the registered MCP server) to search for "
|
|
21
|
+
"emails containing the word 'invoice'. Do not answer from memory and do not call "
|
|
22
|
+
"any other tool — actually invoke `search_emails`. Report the tool's raw output."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VerifyA2AAgentMCPStep(TaskStep):
|
|
27
|
+
type: ClassVar[str] = "verify_a2a_agent_mcp"
|
|
28
|
+
entity_refs = (EntityRef.agent("a2a_agent_id"),)
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
id: str,
|
|
33
|
+
version: Optional[int],
|
|
34
|
+
a2a_agent_id: str,
|
|
35
|
+
timeout_seconds: int = 300,
|
|
36
|
+
poll_interval_seconds: int = 10,
|
|
37
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
38
|
+
fail_task_on_error: bool = True,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
41
|
+
self.a2a_agent_id = a2a_agent_id
|
|
42
|
+
self.timeout_seconds = timeout_seconds
|
|
43
|
+
self.poll_interval_seconds = poll_interval_seconds
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
base = super().to_dict()
|
|
47
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
48
|
+
base["timeout_seconds"] = self.timeout_seconds
|
|
49
|
+
base["poll_interval_seconds"] = self.poll_interval_seconds
|
|
50
|
+
return base
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: dict) -> VerifyA2AAgentMCPStep:
|
|
54
|
+
return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], timeout_seconds=data.get("timeout_seconds", 300), poll_interval_seconds=data.get("poll_interval_seconds", 10))
|
|
55
|
+
|
|
56
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
57
|
+
from agent_env.a2a_agent import A2AAgent, protocol
|
|
58
|
+
from agent_env.env.env import require_gateway_url
|
|
59
|
+
|
|
60
|
+
deployed_agent = next((a for a in context.deployed_agents), None)
|
|
61
|
+
if deployed_agent is None:
|
|
62
|
+
raise RuntimeError("No deployed agent found in context")
|
|
63
|
+
deployed_env = next((e for e in context.deployed_envs), None)
|
|
64
|
+
if deployed_env is None:
|
|
65
|
+
raise RuntimeError("No deployed env found in context")
|
|
66
|
+
# It counts tool calls in the gateway's trajectory, so an env without one can't be checked here.
|
|
67
|
+
require_gateway_url(deployed_env, "verify_a2a_agent_mcp")
|
|
68
|
+
|
|
69
|
+
a2a_url = deployed_agent.a2a_url or deployed_agent.api_url
|
|
70
|
+
message_id = uuid.uuid4().hex
|
|
71
|
+
|
|
72
|
+
# Send test prompt via message/send (A2A spec 0.3)
|
|
73
|
+
logger.info(f"Sending test prompt to agent at {a2a_url}...")
|
|
74
|
+
async with httpx.AsyncClient() as client:
|
|
75
|
+
resp = await client.post(f"{a2a_url}/a2a", json={
|
|
76
|
+
"jsonrpc": "2.0", "id": "1", "method": "message/send",
|
|
77
|
+
"params": {"message": {
|
|
78
|
+
"messageId": message_id,
|
|
79
|
+
"role": "user",
|
|
80
|
+
"parts": [{"kind": "text", "text": TEST_PROMPT}],
|
|
81
|
+
}},
|
|
82
|
+
}, timeout=self.timeout_seconds)
|
|
83
|
+
resp.raise_for_status()
|
|
84
|
+
send_body = resp.json()
|
|
85
|
+
if "error" in send_body:
|
|
86
|
+
raise RuntimeError(f"A2A message/send failed: {send_body['error']}")
|
|
87
|
+
send_result = send_body.get("result") or {}
|
|
88
|
+
task_id = send_result["id"]
|
|
89
|
+
logger.info(f"message/send returned task_id={task_id}")
|
|
90
|
+
|
|
91
|
+
# Poll tasks/get until terminal state
|
|
92
|
+
logger.info("Polling for task completion...")
|
|
93
|
+
a2a_result = await self._poll_task(a2a_url, task_id)
|
|
94
|
+
task_status = a2a_result["status"]["state"]
|
|
95
|
+
|
|
96
|
+
# Parse the terminal message through the shared compatibility layer so
|
|
97
|
+
# both legacy top-level telemetry and typed usage telemetry are handled.
|
|
98
|
+
status_msg = (a2a_result.get("status") or {}).get("message") or {}
|
|
99
|
+
terminal = protocol.TerminalResponse.from_message(status_msg)
|
|
100
|
+
agent_response = terminal.response_text
|
|
101
|
+
tool_call_count_reported = terminal.tool_call_count is not None
|
|
102
|
+
|
|
103
|
+
# Read trajectory from env sandbox
|
|
104
|
+
tools_invoked = await self._count_tool_calls(deployed_env.sandbox_id)
|
|
105
|
+
passed = task_status == "completed" and tools_invoked > 0
|
|
106
|
+
logger.info(f"MCP validation: status={task_status} tools_invoked={tools_invoked} passed={passed}")
|
|
107
|
+
|
|
108
|
+
mcp_validation = {
|
|
109
|
+
"passed": passed,
|
|
110
|
+
"tools_invoked": tools_invoked,
|
|
111
|
+
"task_status": task_status,
|
|
112
|
+
"task_id": task_id,
|
|
113
|
+
"agent_response": agent_response[:500],
|
|
114
|
+
}
|
|
115
|
+
context.metadata.setdefault("verifications", {})["a2a_agent_mcp"] = mcp_validation
|
|
116
|
+
|
|
117
|
+
# Build validated mcp-config entry from what we actually tested
|
|
118
|
+
card = deployed_agent.a2a_card or {}
|
|
119
|
+
mcp_ext = A2AAgent.find_extension(card, A2AAgent.EXT_MCP_CONFIG)
|
|
120
|
+
mcp_entry: dict = {"supported": mcp_ext is not None and passed}
|
|
121
|
+
if mcp_ext:
|
|
122
|
+
ext_config = mcp_ext.get("config") or mcp_ext.get("params") or {}
|
|
123
|
+
methods: dict = {}
|
|
124
|
+
for method_name, method_def in ext_config.get("methods", {}).items():
|
|
125
|
+
method_entry: dict = {"supported": passed}
|
|
126
|
+
request = method_def.get("request", {})
|
|
127
|
+
options = []
|
|
128
|
+
for key in ("required", "optional", "supported"):
|
|
129
|
+
options.extend(request.get(key, []))
|
|
130
|
+
for one_of in request.get("oneOf", []):
|
|
131
|
+
options.extend(one_of.get("required", []))
|
|
132
|
+
if options:
|
|
133
|
+
method_entry["options"] = {name: {"supported": passed} for name in options}
|
|
134
|
+
methods[method_name] = method_entry
|
|
135
|
+
mcp_entry["methods"] = methods
|
|
136
|
+
|
|
137
|
+
agent = A2AAgent.get(self.a2a_agent_id)
|
|
138
|
+
validated_ext = agent.metadata.get("validated_a2a_extensions", {})
|
|
139
|
+
validated_ext[A2AAgent.EXT_MCP_CONFIG] = mcp_entry
|
|
140
|
+
validated_data_ext = agent.metadata.get("validated_data_extensions", {})
|
|
141
|
+
validated_data_ext["tool_call_count"] = {"supported": tool_call_count_reported}
|
|
142
|
+
agent.update_metadata({**agent.metadata, "validated_a2a_extensions": validated_ext, "validated_data_extensions": validated_data_ext})
|
|
143
|
+
|
|
144
|
+
return context
|
|
145
|
+
|
|
146
|
+
async def _poll_task(self, a2a_url: str, task_id: str) -> dict:
|
|
147
|
+
deadline = time.monotonic() + self.timeout_seconds
|
|
148
|
+
while time.monotonic() < deadline:
|
|
149
|
+
await asyncio.sleep(self.poll_interval_seconds)
|
|
150
|
+
try:
|
|
151
|
+
async with httpx.AsyncClient() as client:
|
|
152
|
+
resp = await client.post(f"{a2a_url}/a2a", json={
|
|
153
|
+
"jsonrpc": "2.0", "id": "poll", "method": "tasks/get",
|
|
154
|
+
"params": {"id": task_id},
|
|
155
|
+
}, timeout=30)
|
|
156
|
+
resp.raise_for_status()
|
|
157
|
+
data = resp.json()
|
|
158
|
+
if "error" in data:
|
|
159
|
+
logger.warning(f"A2A poll error (will retry): {data['error']}")
|
|
160
|
+
continue
|
|
161
|
+
result = data["result"]
|
|
162
|
+
except (httpx.ConnectError, httpx.ReadTimeout, httpx.RemoteProtocolError) as e:
|
|
163
|
+
logger.warning(f"A2A poll failed (will retry): {e}")
|
|
164
|
+
continue
|
|
165
|
+
state = result["status"]["state"]
|
|
166
|
+
if state in ("completed", "failed"):
|
|
167
|
+
return result
|
|
168
|
+
raise TimeoutError(f"A2A task {task_id} did not complete within {self.timeout_seconds}s")
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
async def _count_tool_calls(sandbox_id: str) -> int:
|
|
172
|
+
"""Read the env gateway trajectory from the gateway container and count tool_call events."""
|
|
173
|
+
from agent_env.providers import get_env_sandbox_provider
|
|
174
|
+
from agent_env.providers import EnvironmentGatewayProvider
|
|
175
|
+
sandbox = await get_env_sandbox_provider().get_sandbox(sandbox_id)
|
|
176
|
+
events = await EnvironmentGatewayProvider().read_trajectory(sandbox)
|
|
177
|
+
count = sum(1 for e in events if e.get("event_type") == "tool_call")
|
|
178
|
+
return count
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Verify core A2A protocol support (message/send + tasks/get)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
from agent_env.task_step.context import TaskStepContext
|
|
9
|
+
from agent_env.entity_refs import EntityRef
|
|
10
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VerifyCoreA2AProtocolStep(TaskStep):
|
|
16
|
+
type: ClassVar[str] = "verify_a2a_core_protocol"
|
|
17
|
+
entity_refs = (EntityRef.agent("a2a_agent_id"),)
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
id: str,
|
|
22
|
+
version: Optional[int],
|
|
23
|
+
a2a_agent_id: str,
|
|
24
|
+
prompt_id: str,
|
|
25
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
26
|
+
fail_task_on_error: bool = True,
|
|
27
|
+
):
|
|
28
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
29
|
+
self.a2a_agent_id = a2a_agent_id
|
|
30
|
+
self.prompt_id = prompt_id
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
base = super().to_dict()
|
|
34
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
35
|
+
base["prompt_id"] = self.prompt_id
|
|
36
|
+
return base
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, data: dict) -> VerifyCoreA2AProtocolStep:
|
|
40
|
+
return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], prompt_id=data["prompt_id"])
|
|
41
|
+
|
|
42
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
43
|
+
from agent_env.a2a_agent import A2AAgent
|
|
44
|
+
|
|
45
|
+
prompt_response = next((pr for pr in context.prompt_responses if pr.prompt_id == self.prompt_id), None)
|
|
46
|
+
|
|
47
|
+
# message/send succeeded if we have a prompt response at all
|
|
48
|
+
message_send_ok = prompt_response is not None
|
|
49
|
+
# tasks/get succeeded if the response has no error_type
|
|
50
|
+
tasks_get_ok = message_send_ok and prompt_response.error_type is None
|
|
51
|
+
|
|
52
|
+
logger.info(f"Core A2A protocol: message/send={message_send_ok} tasks/get={tasks_get_ok}")
|
|
53
|
+
if not message_send_ok:
|
|
54
|
+
logger.error("message/send failed: no prompt response found")
|
|
55
|
+
elif not tasks_get_ok:
|
|
56
|
+
logger.warning(f"tasks/get returned error: {prompt_response.error_type}")
|
|
57
|
+
|
|
58
|
+
protocol = {
|
|
59
|
+
"message/send": {"supported": message_send_ok},
|
|
60
|
+
"tasks/get": {"supported": tasks_get_ok},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
agent = A2AAgent.get(self.a2a_agent_id)
|
|
64
|
+
agent.update_metadata({**agent.metadata, "validated_a2a_protocol": protocol})
|
|
65
|
+
|
|
66
|
+
context.metadata.setdefault("verifications", {})["a2a_core_protocol"] = protocol
|
|
67
|
+
return context
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Verify A2A agent install/v1 extension end-to-end.
|
|
2
|
+
|
|
3
|
+
Pure recorder step. The actual install exercise (deploy sandbox -> run bare task
|
|
4
|
+
container -> InstallAgentTaskStep -> PromptAgent -> RubricsVerifier) lives in the
|
|
5
|
+
chained steps before this one in the validator DAG. This step reads the rubric
|
|
6
|
+
verdict + checks the installed agent's card, then records the outcome on
|
|
7
|
+
`agent.metadata["validated_a2a_extensions"]` and `context.metadata["verifications"]`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import ClassVar, Optional
|
|
14
|
+
|
|
15
|
+
from agent_env.task_step.context import TaskStepContext
|
|
16
|
+
from agent_env.entity_refs import EntityRef
|
|
17
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VerifyA2AInstallStep(TaskStep):
|
|
23
|
+
type: ClassVar[str] = "verify_a2a_install"
|
|
24
|
+
entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
id: str,
|
|
29
|
+
version: Optional[int],
|
|
30
|
+
a2a_agent_id: str,
|
|
31
|
+
a2a_agent_version: Optional[int] = None,
|
|
32
|
+
agent_name: str = "install-test-agent",
|
|
33
|
+
rubric_verifier_id: Optional[str] = None,
|
|
34
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
35
|
+
fail_task_on_error: bool = True,
|
|
36
|
+
):
|
|
37
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
38
|
+
self.a2a_agent_id = a2a_agent_id
|
|
39
|
+
self.a2a_agent_version = a2a_agent_version
|
|
40
|
+
self.agent_name = agent_name
|
|
41
|
+
self.rubric_verifier_id = rubric_verifier_id
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
base = super().to_dict()
|
|
45
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
46
|
+
base["a2a_agent_version"] = self.a2a_agent_version
|
|
47
|
+
base["agent_name"] = self.agent_name
|
|
48
|
+
base["rubric_verifier_id"] = self.rubric_verifier_id
|
|
49
|
+
return base
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, data: dict) -> VerifyA2AInstallStep:
|
|
53
|
+
return cls(
|
|
54
|
+
**cls._base_from_dict(data),
|
|
55
|
+
a2a_agent_id=data["a2a_agent_id"],
|
|
56
|
+
a2a_agent_version=data.get("a2a_agent_version"),
|
|
57
|
+
agent_name=data.get("agent_name", "install-test-agent"),
|
|
58
|
+
rubric_verifier_id=data.get("rubric_verifier_id"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
62
|
+
from agent_env.a2a_agent import A2AAgent
|
|
63
|
+
|
|
64
|
+
deployed = next(
|
|
65
|
+
(a for a in context.deployed_agents if a.agent_name == self.agent_name),
|
|
66
|
+
None,
|
|
67
|
+
)
|
|
68
|
+
result: dict = {
|
|
69
|
+
"supported": False,
|
|
70
|
+
"advertised": False,
|
|
71
|
+
"installed_successfully": deployed is not None,
|
|
72
|
+
"responded_to_prompt": False,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if deployed is None:
|
|
76
|
+
# Install never produced a DeployedAgent — upstream chain failed.
|
|
77
|
+
result["error"] = (
|
|
78
|
+
"InstallAgentTaskStep did not produce a deployed agent named "
|
|
79
|
+
f"{self.agent_name!r} (upstream chain failure); check earlier validator step logs"
|
|
80
|
+
)
|
|
81
|
+
logger.warning(f"[verify_a2a_install] {result['error']}")
|
|
82
|
+
else:
|
|
83
|
+
card = deployed.a2a_card or {}
|
|
84
|
+
result["advertised"] = A2AAgent.find_extension(card, A2AAgent.EXT_INSTALL) is not None
|
|
85
|
+
|
|
86
|
+
if self.rubric_verifier_id:
|
|
87
|
+
rubric = context.metadata.get("verifications", {}).get(self.rubric_verifier_id, {})
|
|
88
|
+
rubric_results = rubric.get("results") or []
|
|
89
|
+
responded = any(
|
|
90
|
+
r.get("id") == "responded" and r.get("result") for r in rubric_results
|
|
91
|
+
)
|
|
92
|
+
result["responded_to_prompt"] = responded
|
|
93
|
+
result["rubric_score"] = rubric.get("score")
|
|
94
|
+
logger.info(
|
|
95
|
+
f"[verify_a2a_install] rubric {self.rubric_verifier_id!r}: "
|
|
96
|
+
f"responded={responded} score={rubric.get('score')}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
result["supported"] = result["installed_successfully"] and result["responded_to_prompt"]
|
|
100
|
+
|
|
101
|
+
# Persist on the A2AAgent doc so the hub / next consumers can query it.
|
|
102
|
+
# Guard the DB write: this step runs with fail_task_on_error=False, so a
|
|
103
|
+
# raised exception would be swallowed by the task runner and the result
|
|
104
|
+
# lost. Catch it and always fall through to the in-context write.
|
|
105
|
+
try:
|
|
106
|
+
agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
|
|
107
|
+
validated_ext = dict(agent.metadata.get("validated_a2a_extensions", {}))
|
|
108
|
+
validated_ext[A2AAgent.EXT_INSTALL] = result
|
|
109
|
+
agent.update_metadata({**agent.metadata, "validated_a2a_extensions": validated_ext})
|
|
110
|
+
except Exception as e: # noqa: BLE001
|
|
111
|
+
logger.warning(f"[verify_a2a_install] failed to persist validated_a2a_extensions: {e}")
|
|
112
|
+
|
|
113
|
+
context.metadata.setdefault("verifications", {})["a2a_install"] = result
|
|
114
|
+
logger.info(f"[verify_a2a_install] result: {result}")
|
|
115
|
+
return context
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Verify A2A agent declares LiteLLM cost-attribution support on its card.
|
|
2
|
+
|
|
3
|
+
Static check: reads the agent's `/.well-known/agent.json` (already fetched
|
|
4
|
+
at deploy time and stored on `deployed.a2a_card`) and inspects the
|
|
5
|
+
`urn:agentenv:agent-config/v1` extension. An agent "supports LiteLLM
|
|
6
|
+
attribution" when its agent-config extension advertises **both**
|
|
7
|
+
`project_id` and `task_id` in its `params.methods.set.request.supported`
|
|
8
|
+
list.
|
|
9
|
+
|
|
10
|
+
The check tells consumers (e.g. claude-code's auto-integration flow,
|
|
11
|
+
the hub UI) which agents will honor per-prompt attribution delivered
|
|
12
|
+
through `/ext/agent-config` from `PromptAgentTaskStep`. Agents that
|
|
13
|
+
don't advertise it will have those fields filtered out at the worker
|
|
14
|
+
side with a warning, and spend logs from those agents will not carry
|
|
15
|
+
the `projectId:<id>` / `taskId:<id>` tags.
|
|
16
|
+
|
|
17
|
+
This is a card-level static check only. It does NOT verify the agent
|
|
18
|
+
actually threads attribution into its outbound LLM calls — that's an
|
|
19
|
+
agent-implementation concern (e.g. an agent bridge's own
|
|
20
|
+
`_apply_litellm_attribution` helper). A future runtime probe could be added
|
|
21
|
+
if a self-reporting endpoint is exposed; for now, the static check
|
|
22
|
+
mirrors what an external integrator can discover via the AgentCard.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import ClassVar, Optional
|
|
29
|
+
|
|
30
|
+
from agent_env.task_step.context import TaskStepContext
|
|
31
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Body fields the worker (PromptAgentTaskStep) sends via /ext/agent-config
|
|
36
|
+
# for LiteLLM cost attribution. Both must be in the agent-config extension's
|
|
37
|
+
# `supported` list for the agent to be considered attribution-capable.
|
|
38
|
+
ATTRIBUTION_FIELDS = ("project_id", "task_id")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VerifyA2ALitellmAttributionStep(TaskStep):
|
|
42
|
+
type: ClassVar[str] = "verify_a2a_litellm_attribution"
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
id: str,
|
|
47
|
+
version: Optional[int],
|
|
48
|
+
a2a_agent_id: str,
|
|
49
|
+
a2a_agent_version: Optional[int] = None,
|
|
50
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
51
|
+
fail_task_on_error: bool = True,
|
|
52
|
+
):
|
|
53
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
54
|
+
self.a2a_agent_id = a2a_agent_id
|
|
55
|
+
self.a2a_agent_version = a2a_agent_version
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
base = super().to_dict()
|
|
59
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
60
|
+
base["a2a_agent_version"] = self.a2a_agent_version
|
|
61
|
+
return base
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: dict) -> "VerifyA2ALitellmAttributionStep":
|
|
65
|
+
return cls(
|
|
66
|
+
**cls._base_from_dict(data),
|
|
67
|
+
a2a_agent_id=data["a2a_agent_id"],
|
|
68
|
+
a2a_agent_version=data.get("a2a_agent_version"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
72
|
+
from agent_env.a2a_agent import A2AAgent
|
|
73
|
+
|
|
74
|
+
deployed = next((a for a in context.deployed_agents), None)
|
|
75
|
+
if deployed is None:
|
|
76
|
+
raise RuntimeError("No deployed agent found in context")
|
|
77
|
+
|
|
78
|
+
card = deployed.a2a_card or {}
|
|
79
|
+
agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
|
|
80
|
+
|
|
81
|
+
ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
|
|
82
|
+
supported: list[str] = []
|
|
83
|
+
if ext:
|
|
84
|
+
supported = (
|
|
85
|
+
((ext.get("params") or {}).get("methods") or {})
|
|
86
|
+
.get("set", {})
|
|
87
|
+
.get("request", {})
|
|
88
|
+
.get("supported")
|
|
89
|
+
or []
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
advertises = {field: field in supported for field in ATTRIBUTION_FIELDS}
|
|
93
|
+
missing = [field for field, present in advertises.items() if not present]
|
|
94
|
+
passed = ext is not None and not missing
|
|
95
|
+
|
|
96
|
+
if not ext:
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Agent '%s' does not advertise the agent-config extension — "
|
|
99
|
+
"LiteLLM attribution cannot flow.", self.a2a_agent_id,
|
|
100
|
+
)
|
|
101
|
+
elif missing:
|
|
102
|
+
logger.warning(
|
|
103
|
+
"Agent '%s' advertises agent-config but is missing attribution "
|
|
104
|
+
"fields in its `supported` set: %s. Spend logs from this agent "
|
|
105
|
+
"will not carry projectId/taskId tags. See an A2A agent bridge's "
|
|
106
|
+
"`_apply_litellm_attribution` helper for a reference implementation.",
|
|
107
|
+
self.a2a_agent_id, missing,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
logger.info(
|
|
111
|
+
"Agent '%s' supports LiteLLM attribution: advertises %s in "
|
|
112
|
+
"agent-config.supported", self.a2a_agent_id, list(ATTRIBUTION_FIELDS),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
validated = {
|
|
116
|
+
"passed": passed,
|
|
117
|
+
"extension_present": ext is not None,
|
|
118
|
+
"supported_fields": supported,
|
|
119
|
+
"advertises": advertises,
|
|
120
|
+
"missing": missing,
|
|
121
|
+
}
|
|
122
|
+
agent.update_metadata({**agent.metadata, "validated_litellm_attribution": validated})
|
|
123
|
+
context.metadata.setdefault("verifications", {})["a2a_litellm_attribution"] = validated
|
|
124
|
+
return context
|