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,129 @@
|
|
|
1
|
+
"""TaskStep store for MongoDB persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Self, TypeVar
|
|
8
|
+
|
|
9
|
+
from agent_env.plugins import _registration
|
|
10
|
+
from agent_env.store.base import NotFoundError
|
|
11
|
+
from agent_env.config import get_config
|
|
12
|
+
from agent_env.store.document_store import VersionedEntityStore, VersionedEntityStoreCache
|
|
13
|
+
from agent_env.store.query import QueryBuilder, to_document_query
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from agent_env.task_step.task_step import TaskStep
|
|
17
|
+
|
|
18
|
+
_S = TypeVar("_S", bound="TaskStep")
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
TASK_STEPS_COLLECTION = "task_steps"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskStepQuery(QueryBuilder["TaskStep"]):
|
|
26
|
+
def __init__(self, store: Optional["TaskStepStore"] = None) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._store = store
|
|
29
|
+
|
|
30
|
+
def _clone(self) -> Self:
|
|
31
|
+
clone = TaskStepQuery(self._store)
|
|
32
|
+
clone._filters = self._filters.copy()
|
|
33
|
+
clone._sort_field = self._sort_field
|
|
34
|
+
clone._sort_desc = self._sort_desc
|
|
35
|
+
clone._limit_value = self._limit_value
|
|
36
|
+
clone._offset_value = self._offset_value
|
|
37
|
+
return clone
|
|
38
|
+
|
|
39
|
+
def type(self, task_step_type: str) -> Self:
|
|
40
|
+
return self._add_filter("type", task_step_type)
|
|
41
|
+
|
|
42
|
+
def execute(self) -> list[TaskStep]:
|
|
43
|
+
if self._store is None:
|
|
44
|
+
self._store = get_task_step_store()
|
|
45
|
+
return self._store.execute_query(self)
|
|
46
|
+
|
|
47
|
+
def _execute_count(self) -> int:
|
|
48
|
+
if self._store is None:
|
|
49
|
+
self._store = get_task_step_store()
|
|
50
|
+
return self._store.execute_count(self)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TaskStepStore:
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._versioned_cache: VersionedEntityStoreCache[TaskStep] = VersionedEntityStoreCache(
|
|
56
|
+
TASK_STEPS_COLLECTION, self._serialize, self._deserialize, secondary_indexes=[["type"]],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def _doc_store(self):
|
|
61
|
+
return get_config().get_document_store()
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def _versioned(self) -> VersionedEntityStore[TaskStep]:
|
|
65
|
+
return self._versioned_cache.for_store(self._doc_store)
|
|
66
|
+
|
|
67
|
+
def _serialize(self, task_step: TaskStep) -> dict:
|
|
68
|
+
doc = task_step.to_dict()
|
|
69
|
+
doc["created_at_utc"] = datetime.now(timezone.utc)
|
|
70
|
+
return doc
|
|
71
|
+
|
|
72
|
+
def get(self, id: str, version: Optional[int] = None) -> TaskStep:
|
|
73
|
+
task_step = self._versioned.get(id, version)
|
|
74
|
+
if task_step is None:
|
|
75
|
+
version_str = f" version={version}" if version is not None else ""
|
|
76
|
+
raise NotFoundError(f"TaskStep {id}{version_str} not found")
|
|
77
|
+
return task_step
|
|
78
|
+
|
|
79
|
+
def next_version(self, id: str) -> int:
|
|
80
|
+
return self._versioned.next_version(id)
|
|
81
|
+
|
|
82
|
+
def put_document(self, task_step: _S) -> _S:
|
|
83
|
+
task_step.version = self._versioned.put(task_step)
|
|
84
|
+
return task_step
|
|
85
|
+
|
|
86
|
+
def _deserialize(self, doc: dict) -> TaskStep:
|
|
87
|
+
from agent_env.task_step.registry import get_task_step_registry
|
|
88
|
+
|
|
89
|
+
doc.pop("_id", None)
|
|
90
|
+
doc.pop("created_at_utc", None)
|
|
91
|
+
task_step_type = doc["type"]
|
|
92
|
+
registry = get_task_step_registry()
|
|
93
|
+
cls = registry.get(task_step_type)
|
|
94
|
+
if cls is None:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Unknown task step type: {task_step_type}"
|
|
97
|
+
f"{_registration.failure_note(_registration.TASK_STEPS, task_step_type)}"
|
|
98
|
+
)
|
|
99
|
+
from agent_env.task_step.task_step import attach_retry_config
|
|
100
|
+
|
|
101
|
+
return attach_retry_config(cls.from_dict(doc), doc)
|
|
102
|
+
|
|
103
|
+
def execute_query(self, query: TaskStepQuery) -> list[TaskStep]:
|
|
104
|
+
filt, sort = to_document_query(query)
|
|
105
|
+
return self._versioned.query(filt, sort, query._limit_value, query._offset_value)
|
|
106
|
+
|
|
107
|
+
def execute_count(self, query: TaskStepQuery) -> int:
|
|
108
|
+
filt, _ = to_document_query(query)
|
|
109
|
+
return self._versioned.count(filt)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_task_step_store: Optional[TaskStepStore] = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_task_step_store() -> TaskStepStore:
|
|
116
|
+
global _task_step_store
|
|
117
|
+
if _task_step_store is None:
|
|
118
|
+
_task_step_store = TaskStepStore()
|
|
119
|
+
return _task_step_store
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def set_task_step_store(store: TaskStepStore) -> None:
|
|
123
|
+
global _task_step_store
|
|
124
|
+
_task_step_store = store
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def reset_task_step_store() -> None:
|
|
128
|
+
global _task_step_store
|
|
129
|
+
_task_step_store = None
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Base TaskStep model for AgentEnv."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Self
|
|
9
|
+
|
|
10
|
+
from agent_env.task_step.context import TaskStepContext
|
|
11
|
+
from agent_env.entity_refs import EntityRef
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from agent_env.task_step.store import TaskStepQuery
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TaskStepDependency:
|
|
19
|
+
task_step_id: str
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, data: dict[str, Any]) -> "TaskStepDependency":
|
|
23
|
+
return cls(task_step_id=data["task_step_id"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def attach_retry_config(step: "TaskStep", data: dict[str, Any]) -> "TaskStep":
|
|
27
|
+
"""Hydrate ``retry_config`` after ``step_cls.from_dict`` at the generic
|
|
28
|
+
parse sites, so the field survives round-trips on step types whose
|
|
29
|
+
``__init__`` doesn't accept it. No-op if already set or absent."""
|
|
30
|
+
raw = data.get("retry_config")
|
|
31
|
+
if raw is not None and getattr(step, "retry_config", None) is None:
|
|
32
|
+
step.retry_config = RetryConfig.from_dict(raw)
|
|
33
|
+
return step
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class RetryConfig:
|
|
38
|
+
"""Where a step resumes from on a retry, and how many retries to allow.
|
|
39
|
+
|
|
40
|
+
``retry_from_step_id`` is required. Set to the step itself it re-runs only
|
|
41
|
+
this step. Point it at an earlier dependency ancestor to roll the world back
|
|
42
|
+
to there and re-dispatch the span (``retry_from_step_id`` .. failed step) as
|
|
43
|
+
ordinary steps on a freshly redeployed world (see ``Task._drive_dag``). The
|
|
44
|
+
task author owns the resume point; the platform does not decide which steps
|
|
45
|
+
are safe to re-run.
|
|
46
|
+
|
|
47
|
+
``max_retries`` (default 1) bounds how many times a step may trigger this
|
|
48
|
+
rollback-and-re-dispatch before the failure is treated as terminal."""
|
|
49
|
+
|
|
50
|
+
retry_from_step_id: str
|
|
51
|
+
max_retries: int = 1
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict[str, Any]) -> "RetryConfig":
|
|
55
|
+
# go_to_step_id = wire alias; retry_from_step_id canonical (only spelling emitted).
|
|
56
|
+
step_id = data.get("retry_from_step_id", data.get("go_to_step_id"))
|
|
57
|
+
if step_id is None:
|
|
58
|
+
raise ValueError("retry_config requires 'retry_from_step_id' (or 'go_to_step_id')")
|
|
59
|
+
max_retries = data.get("max_retries", 1)
|
|
60
|
+
# bool is an int subclass; reject it and non-ints so a bad value fails at
|
|
61
|
+
# parse time, not mid-run inside the scheduler's retry-budget comparison.
|
|
62
|
+
if isinstance(max_retries, bool) or not isinstance(max_retries, int):
|
|
63
|
+
raise ValueError(f"retry_config.max_retries must be an int, got {max_retries!r}")
|
|
64
|
+
if max_retries < 0:
|
|
65
|
+
raise ValueError(f"retry_config.max_retries must be >= 0, got {max_retries}")
|
|
66
|
+
return cls(retry_from_step_id=step_id, max_retries=max_retries)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TaskStep(ABC):
|
|
70
|
+
"""Base class for all task steps. Immutable - each modification creates a new version."""
|
|
71
|
+
|
|
72
|
+
type: ClassVar[str] = "task_step"
|
|
73
|
+
DEFAULT_TTL_SECONDS: ClassVar[int] = 7200
|
|
74
|
+
DEFAULT_AGENT_NAME: ClassVar[str] = "default-agent"
|
|
75
|
+
# Fields holding env, A2A agent or artifact ids; None means undeclared.
|
|
76
|
+
entity_refs: ClassVar[tuple[EntityRef, ...] | None] = None
|
|
77
|
+
|
|
78
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
79
|
+
super().__init_subclass__(**kwargs)
|
|
80
|
+
# Never inherited: a subclass may add id fields its parent's declaration misses.
|
|
81
|
+
if "entity_refs" not in vars(cls):
|
|
82
|
+
cls.entity_refs = None
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
id: str,
|
|
87
|
+
version: Optional[int],
|
|
88
|
+
depends_on: Optional[list] = None,
|
|
89
|
+
fail_task_on_error: bool = True,
|
|
90
|
+
retry_config: Optional["RetryConfig | dict"] = None,
|
|
91
|
+
):
|
|
92
|
+
self.id = id
|
|
93
|
+
self.version = version
|
|
94
|
+
if depends_on is not None:
|
|
95
|
+
depends_on = [
|
|
96
|
+
d if isinstance(d, TaskStepDependency) else TaskStepDependency.from_dict(d)
|
|
97
|
+
for d in depends_on
|
|
98
|
+
]
|
|
99
|
+
self.depends_on = depends_on
|
|
100
|
+
self.fail_task_on_error = fail_task_on_error
|
|
101
|
+
if retry_config is not None and not isinstance(retry_config, RetryConfig):
|
|
102
|
+
retry_config = RetryConfig.from_dict(retry_config)
|
|
103
|
+
self.retry_config = retry_config
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext | None:
|
|
107
|
+
"""Execute this task step."""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def preflight(self) -> list[str]:
|
|
111
|
+
"""Config problems detectable before a run, one message each; empty when fine.
|
|
112
|
+
|
|
113
|
+
Overrides must not need a sandbox or a deployed env.
|
|
114
|
+
"""
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
def step_param_overrides(self, context: TaskStepContext) -> dict[str, Any]:
|
|
118
|
+
"""Per-run overrides for this step's params, from a run's ``step_overrides``.
|
|
119
|
+
|
|
120
|
+
Generic seam: a step opts in by reading the params it supports; an
|
|
121
|
+
override wins over the stored value. Empty dict when none set.
|
|
122
|
+
"""
|
|
123
|
+
return (context.metadata.get("user_overrides") or {}).get("step_params", {}).get(self.id) or {}
|
|
124
|
+
|
|
125
|
+
def to_dict(self) -> dict[str, Any]:
|
|
126
|
+
base: dict[str, Any] = {"id": self.id, "type": self.type, "version": self.version, "fail_task_on_error": self.fail_task_on_error}
|
|
127
|
+
if self.depends_on is not None:
|
|
128
|
+
base["depends_on"] = [dataclasses.asdict(d) for d in self.depends_on]
|
|
129
|
+
if self.retry_config is not None:
|
|
130
|
+
base["retry_config"] = dataclasses.asdict(self.retry_config)
|
|
131
|
+
return base
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def _base_from_dict(cls, data: dict) -> dict:
|
|
135
|
+
raw = data.get("depends_on")
|
|
136
|
+
depends_on = (
|
|
137
|
+
[TaskStepDependency.from_dict(d) for d in raw] if raw is not None else None
|
|
138
|
+
)
|
|
139
|
+
return {
|
|
140
|
+
"id": data["id"], "version": data.get("version"),
|
|
141
|
+
"depends_on": depends_on,
|
|
142
|
+
"fail_task_on_error": data.get("fail_task_on_error", True),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_dict(cls, data: dict[str, Any]) -> "TaskStep":
|
|
147
|
+
raise NotImplementedError(f"{cls.__name__} must implement from_dict")
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def get(cls, id: str, version: Optional[int] = None) -> "TaskStep":
|
|
151
|
+
from .store import get_task_step_store
|
|
152
|
+
|
|
153
|
+
return get_task_step_store().get(id, version)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def put(cls, **kwargs: Any) -> Self:
|
|
157
|
+
from .store import get_task_step_store
|
|
158
|
+
|
|
159
|
+
kwargs.setdefault("version", None)
|
|
160
|
+
# Most step __init__s don't accept retry_config (it lives on the base but
|
|
161
|
+
# subclasses don't forward it); construct without it, then hydrate so a
|
|
162
|
+
# retry_config on any step type round-trips through the step store.
|
|
163
|
+
retry_config_raw = kwargs.pop("retry_config", None)
|
|
164
|
+
instance = cls(**kwargs)
|
|
165
|
+
if retry_config_raw is not None:
|
|
166
|
+
attach_retry_config(instance, {"retry_config": retry_config_raw})
|
|
167
|
+
return get_task_step_store().put_document(instance)
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def query(cls) -> "TaskStepQuery":
|
|
171
|
+
from .store import TaskStepQuery, get_task_step_store
|
|
172
|
+
|
|
173
|
+
return TaskStepQuery(get_task_step_store())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .mcp_cli_builder.build_mcp_cli import BuildMcpCliTaskStep
|
|
2
|
+
from .deploy_agent import DeployAgentTaskStep
|
|
3
|
+
from .deploy_env import DeployEnvTaskStep
|
|
4
|
+
from .verifiers.env_outcome_verifier import EnvOutcomeVerifierTaskStep, ScoreAggregator
|
|
5
|
+
from .load_artifact import LoadArtifactTaskStep
|
|
6
|
+
from .modify_env_tool_access import ModifyEnvToolAccessStep
|
|
7
|
+
from .prompt_agent import PromptAgentTaskStep
|
|
8
|
+
from .verifiers.rubrics_verifier import RubricsVerifierTaskStep
|
|
9
|
+
from .mcp_env_validator.verify_mcp_env_assessment import VerifyMCPEnvAssessmentStep
|
|
10
|
+
from .mcp_env_validator.verify_mcp_tool_schema import VerifyMCPToolSchemaTaskStep
|
|
11
|
+
from .mcp_env_validator.verify_spec_conformance import VerifySpecConformanceTaskStep
|
|
12
|
+
from .mcp_env_validator.validation_gate_aggregator import ValidationGateAggregatorStep
|
|
13
|
+
from .multienv_validator.verify_universe_roundtrip import VerifyUniverseLoadExportRoundtripStep
|
|
14
|
+
from .multienv_validator.combine_universe_verdicts import CombineUniverseVerdictsStep
|
|
15
|
+
from .env_card_validator.verify_env_card import VerifyEnvironmentCardStep
|
|
16
|
+
from .env_card_validator.verify_env_core_protocol import VerifyCoreEnvironmentProtocolStep
|
|
17
|
+
from .a2a_agent_validator.verify_a2a_agent_card import VerifyA2AAgentCardStep
|
|
18
|
+
from .a2a_agent_validator.verify_a2a_agent_mcp import VerifyA2AAgentMCPStep
|
|
19
|
+
from .a2a_agent_validator.verify_a2a_core_protocol import VerifyCoreA2AProtocolStep
|
|
20
|
+
from .a2a_agent_validator.verify_a2a_skill_config import VerifyA2ASkillConfigStep
|
|
21
|
+
from .a2a_agent_validator.verify_a2a_trajectory import VerifyA2ATrajectoryStep
|
|
22
|
+
from .add_skills import AddSkillsTaskStep
|
|
23
|
+
from .apply_server_config import ApplyServerConfigStep, ConfigDirective
|
|
24
|
+
from .verifiers.agent_prompt_response_verifier import AgentPromptResponseVerifierTaskStep
|
|
25
|
+
|
|
26
|
+
__all__ = ["AddSkillsTaskStep", "ApplyServerConfigStep", "ConfigDirective", "AgentPromptResponseVerifierTaskStep", "BuildMcpCliTaskStep",
|
|
27
|
+
"DeployAgentTaskStep", "DeployEnvTaskStep", "EnvOutcomeVerifierTaskStep", "LoadArtifactTaskStep", "ModifyEnvToolAccessStep", "PromptAgentTaskStep", "RubricsVerifierTaskStep", "VerifyMCPEnvAssessmentStep", "VerifyMCPToolSchemaTaskStep", "VerifySpecConformanceTaskStep", "ValidationGateAggregatorStep", "VerifyUniverseLoadExportRoundtripStep", "CombineUniverseVerdictsStep", "VerifyEnvironmentCardStep", "VerifyCoreEnvironmentProtocolStep", "VerifyA2AAgentCardStep", "VerifyA2AAgentMCPStep", "VerifyCoreA2AProtocolStep", "VerifyA2ASkillConfigStep", "VerifyA2ATrajectoryStep", "ScoreAggregator"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .verify_a2a_agent_card import VerifyA2AAgentCardStep
|
|
2
|
+
from .verify_a2a_agent_config_identity import VerifyA2AAgentConfigIdentityStep
|
|
3
|
+
from .verify_a2a_agent_mcp import VerifyA2AAgentMCPStep
|
|
4
|
+
from .verify_a2a_core_protocol import VerifyCoreA2AProtocolStep
|
|
5
|
+
from .verify_a2a_install import VerifyA2AInstallStep
|
|
6
|
+
from .verify_a2a_litellm_attribution import VerifyA2ALitellmAttributionStep
|
|
7
|
+
from .verify_a2a_litellm_attribution_runtime import VerifyA2ALitellmAttributionRuntimeStep
|
|
8
|
+
from .verify_a2a_modalities import VerifyA2AModalitiesStep
|
|
9
|
+
from .verify_a2a_peer_agents import VerifyA2APeerAgentsStep
|
|
10
|
+
from .verify_a2a_role import VerifyA2ARoleStep
|
|
11
|
+
from .verify_a2a_skill_config import VerifyA2ASkillConfigStep
|
|
12
|
+
from .verify_a2a_snapshot import VerifyA2ASnapshotStep
|
|
13
|
+
from .verify_a2a_system_prompt import VerifyA2ASystemPromptStep
|
|
14
|
+
from .verify_a2a_trajectory import VerifyA2ATrajectoryStep
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"VerifyA2AAgentCardStep",
|
|
18
|
+
"VerifyA2AAgentConfigIdentityStep",
|
|
19
|
+
"VerifyA2AAgentMCPStep",
|
|
20
|
+
"VerifyCoreA2AProtocolStep",
|
|
21
|
+
"VerifyA2AInstallStep",
|
|
22
|
+
"VerifyA2ALitellmAttributionStep",
|
|
23
|
+
"VerifyA2ALitellmAttributionRuntimeStep",
|
|
24
|
+
"VerifyA2AModalitiesStep",
|
|
25
|
+
"VerifyA2APeerAgentsStep",
|
|
26
|
+
"VerifyA2ARoleStep",
|
|
27
|
+
"VerifyA2ASkillConfigStep",
|
|
28
|
+
"VerifyA2ASnapshotStep",
|
|
29
|
+
"VerifyA2ASystemPromptStep",
|
|
30
|
+
"VerifyA2ATrajectoryStep",
|
|
31
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Modality probe fixtures
|
|
2
|
+
|
|
3
|
+
Binary fixtures used by the A2A agent validator's modality probes. Each
|
|
4
|
+
file is loaded at import time via `_b64(name)` in `verify_a2a_modalities.py`
|
|
5
|
+
and exposed as a `*_B64` constant. The same constants get assembled into
|
|
6
|
+
`*_PROBE_PARTS` lists for the inline-bytes path, and the raw bytes are also
|
|
7
|
+
read directly for the URI-based S3 upload path.
|
|
8
|
+
|
|
9
|
+
| File | Probe modality | What it contains | Why |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `red.png` | `image/png` | 8×8 solid red square | smallest valid red png (75 B) |
|
|
12
|
+
| `red.jpg` | `image/jpeg` | 8×8 solid red square | tests separate JPEG decode path |
|
|
13
|
+
| `red.gif` | `image/gif` | 8×8 solid red square | tests separate GIF decode path |
|
|
14
|
+
| `clip.wav` | `audio/wav` | spoken `"Say the word: banana"` | tests PCM decode + transcription|
|
|
15
|
+
| `clip.mp3` | `audio/mpeg` | spoken `"Say the word: telephone"` | tests MP3 decode + transcription|
|
|
16
|
+
| `clip.m4a` | `audio/mp4` | spoken `"Say the word: bicycle"` | tests AAC decode + transcription|
|
|
17
|
+
| `clip.ogg` | `audio/ogg` | spoken `"Say the word: rainbow"` | tests Opus decode + transcription|
|
|
18
|
+
| `document.pdf` | `application/pdf` | one-page instructional doc, target `ELEPHANT` | tests PDF text extraction |
|
|
19
|
+
| `clip.mp4` | `video/mp4` | 3 s clip showing `GIRAFFE` on white | tests video-frame OCR; URI-delivered |
|
|
20
|
+
|
|
21
|
+
## Design choices that surface as gotchas
|
|
22
|
+
|
|
23
|
+
### Distinct target words per audio format
|
|
24
|
+
Each audio file says a *different* word — the validator uses
|
|
25
|
+
`AUDIO_WAV_EXPECTED = "banana"`, `AUDIO_MP3_EXPECTED = "telephone"`, etc.
|
|
26
|
+
If every probe expected the same word, a model could pass by outputting
|
|
27
|
+
that word from any cue (filename, bias, the prompt). Distinct expected
|
|
28
|
+
values per format means the model has to actually decode each clip.
|
|
29
|
+
|
|
30
|
+
### Neutral filenames
|
|
31
|
+
All fixtures are named `clip.<ext>` / `red.<ext>` / `document.pdf` rather
|
|
32
|
+
than `banana.<ext>` / `red-square.png` etc. A leaky filename lets the model
|
|
33
|
+
answer from the path the wrapper materializes (`@/app/inputs/<task>/X`)
|
|
34
|
+
without ever decoding the bytes. We caught this when an early probe with
|
|
35
|
+
`banana.mp3` was "passing" via `ls banana.mp3` + filename inference.
|
|
36
|
+
|
|
37
|
+
### Audio encoding details
|
|
38
|
+
- **16 kHz mono is the format Gemini accepts reliably.** An earlier 8 kHz
|
|
39
|
+
WAV failed even though `audio/wav` is in the gemini-cli MIME table.
|
|
40
|
+
- **`Alex` voice transcribes more reliably than the default `say` voice.**
|
|
41
|
+
With the default voice the model occasionally heard "banana" as "apple".
|
|
42
|
+
- **3× volume boost + 200 ms silence padding raise transcription
|
|
43
|
+
accuracy from ~70% to ~100%.** Without them the model sometimes guessed
|
|
44
|
+
unrelated words ("visual", "red") when it couldn't lock onto the
|
|
45
|
+
utterance.
|
|
46
|
+
|
|
47
|
+
### PDF embeds the target word inside an instructional paragraph
|
|
48
|
+
`document.pdf` doesn't just show `ELEPHANT` alone — it shows a short
|
|
49
|
+
paragraph naming `ELEPHANT` as the target word. Bare word alone tripped
|
|
50
|
+
letter-truncation edge cases (the model occasionally reading "ELEPHANT"
|
|
51
|
+
as "ANT" or "PHANT", probably from letter-spacing artifacts in the PDF
|
|
52
|
+
text extractor).
|
|
53
|
+
|
|
54
|
+
### Video shows a static frame, not motion
|
|
55
|
+
`clip.mp4` is a static frame on a white background (480×360 @ 15 fps, h.264
|
|
56
|
+
CRF 28, no audio track). It tests video-frame OCR, not temporal
|
|
57
|
+
understanding. The static design plus on-screen instructional context plus
|
|
58
|
+
the larger 480×360 frame size were each needed: smaller / barer clips
|
|
59
|
+
truncated to "FFE" probabilistically.
|
|
60
|
+
|
|
61
|
+
### Why video is URI-only
|
|
62
|
+
The video fixture is delivered via `FileWithUri` (`s3://`), not inline
|
|
63
|
+
`FileWithBytes` like the others. At 8.9 KB it would fit inline, but the
|
|
64
|
+
validator deliberately exercises the wrapper's URI-materialization code
|
|
65
|
+
path with this probe — it's the only fixture that does so for video, and
|
|
66
|
+
one of three (alongside the two URI-delivered PNG probes) that exercise
|
|
67
|
+
URI delivery at all.
|
|
68
|
+
|
|
69
|
+
## Regenerating fixtures
|
|
70
|
+
|
|
71
|
+
These are *not* runtime dependencies. They're only needed if you want to
|
|
72
|
+
re-create or modify a fixture. Install with `pip install Pillow fpdf2`.
|
|
73
|
+
You'll also need `ffmpeg` and the `flac` CLI on PATH (`brew install
|
|
74
|
+
ffmpeg flac` on macOS).
|
|
75
|
+
|
|
76
|
+
### Audio (`clip.wav`, `clip.mp3`, `clip.m4a`, `clip.ogg`)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
FF_FILTER="silenceremove=start_periods=1:start_duration=0:start_threshold=-50dB:detection=peak,areverse,silenceremove=start_periods=1:start_duration=0:start_threshold=-50dB:detection=peak,areverse,volume=3.0,adelay=200|200,apad=pad_dur=0.2"
|
|
80
|
+
|
|
81
|
+
# Per-format: substitute the target word, re-encode in native format.
|
|
82
|
+
say -v Alex -r 150 "Say the word: Banana" -o /tmp/x.aiff
|
|
83
|
+
ffmpeg -y -i /tmp/x.aiff -ac 1 -ar 16000 -map_metadata -1 -fflags +bitexact \
|
|
84
|
+
-af "$FF_FILTER" -acodec pcm_s16le fixtures/clip.wav
|
|
85
|
+
|
|
86
|
+
# MP3 / M4A / OGG: encode from the freshly-trimmed WAV
|
|
87
|
+
ffmpeg -y -i fixtures/clip.wav -ac 1 -ar 16000 -b:a 64k -map_metadata -1 \
|
|
88
|
+
-fflags +bitexact -write_xing 1 fixtures/clip.mp3
|
|
89
|
+
ffmpeg -y -i fixtures/clip.wav -c:a aac -b:a 64k -ac 1 -ar 16000 \
|
|
90
|
+
-map_metadata -1 -fflags +bitexact fixtures/clip.m4a
|
|
91
|
+
ffmpeg -y -i fixtures/clip.wav -c:a libopus -b:a 32k -ac 1 -ar 16000 \
|
|
92
|
+
-map_metadata -1 -fflags +bitexact fixtures/clip.ogg
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If updating the target word for a format, also update the corresponding
|
|
96
|
+
`AUDIO_<FMT>_EXPECTED` constant in `verify_a2a_modalities.py`.
|
|
97
|
+
|
|
98
|
+
### PDF (`document.pdf`)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# requires: pip install fpdf2
|
|
102
|
+
from fpdf import FPDF
|
|
103
|
+
pdf = FPDF(orientation='P', unit='pt', format='A4')
|
|
104
|
+
pdf.set_compression(True); pdf.set_creator(''); pdf.set_producer('')
|
|
105
|
+
pdf.add_page()
|
|
106
|
+
pdf.set_font('Helvetica', '', 36)
|
|
107
|
+
pdf.set_xy(50, 100)
|
|
108
|
+
pdf.multi_cell(500, 50,
|
|
109
|
+
'A2A Modality Probe Document\n\n'
|
|
110
|
+
'This page contains exactly one target word that the reader should report.\n\n'
|
|
111
|
+
'The target word is: ELEPHANT\n\n'
|
|
112
|
+
'Reply with only the word above.',
|
|
113
|
+
align='L')
|
|
114
|
+
pdf.output('fixtures/document.pdf')
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Video (`clip.mp4`)
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# requires: pip install Pillow; brew install ffmpeg
|
|
121
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
122
|
+
img = Image.new('RGB', (480, 360), 'white')
|
|
123
|
+
draw = ImageDraw.Draw(img)
|
|
124
|
+
big = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 80)
|
|
125
|
+
small = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 22)
|
|
126
|
+
draw.text((30, 30), "A2A Video Probe", font=small, fill='black')
|
|
127
|
+
draw.text((30, 60), "Target word:", font=small, fill='black')
|
|
128
|
+
bbox = draw.textbbox((0, 0), "GIRAFFE", font=big); w = bbox[2] - bbox[0]
|
|
129
|
+
draw.text(((480 - w) / 2, 150), "GIRAFFE", font=big, fill='black')
|
|
130
|
+
draw.text((30, 280), "Reply with only the target word.", font=small, fill='black')
|
|
131
|
+
img.save('/tmp/frame.png')
|
|
132
|
+
```
|
|
133
|
+
```bash
|
|
134
|
+
ffmpeg -y -loop 1 -i /tmp/frame.png -t 3 -r 15 \
|
|
135
|
+
-c:v libx264 -preset slow -crf 28 -pix_fmt yuv420p -movflags +faststart \
|
|
136
|
+
fixtures/clip.mp4
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Images (`red.png`, `red.jpg`, `red.gif`)
|
|
140
|
+
|
|
141
|
+
These are static and committed once. To regenerate, encode an 8×8 solid
|
|
142
|
+
red square via Pillow or any image tool — the existing files are about
|
|
143
|
+
as small as their formats allow.
|
|
144
|
+
|
|
145
|
+
## Listening / previewing fixtures
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Audio (afplay handles wav/mp3/m4a; ffplay for ogg)
|
|
149
|
+
afplay fixtures/clip.wav
|
|
150
|
+
ffplay -nodisp -autoexit fixtures/clip.ogg
|
|
151
|
+
|
|
152
|
+
# PDF / video
|
|
153
|
+
qlmanage -p fixtures/document.pdf
|
|
154
|
+
open fixtures/clip.mp4
|
|
155
|
+
|
|
156
|
+
# Or open the whole directory in Finder and hit space for QuickLook
|
|
157
|
+
open fixtures/
|
|
158
|
+
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Verify A2A agent card structure and persist to agent metadata."""
|
|
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
|
+
REQUIRED_CARD_FIELDS = ["name", "description", "url", "version", "capabilities", "defaultInputModes", "defaultOutputModes", "skills"]
|
|
15
|
+
OPTIONAL_CARD_FIELDS = ["authentication", "provider", "documentationUrl"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VerifyA2AAgentCardStep(TaskStep):
|
|
19
|
+
type: ClassVar[str] = "verify_a2a_agent_card"
|
|
20
|
+
entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
id: str,
|
|
25
|
+
version: Optional[int],
|
|
26
|
+
a2a_agent_id: str,
|
|
27
|
+
a2a_agent_version: Optional[int] = None,
|
|
28
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
29
|
+
fail_task_on_error: bool = True,
|
|
30
|
+
):
|
|
31
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
32
|
+
self.a2a_agent_id = a2a_agent_id
|
|
33
|
+
self.a2a_agent_version = a2a_agent_version
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
base = super().to_dict()
|
|
37
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
38
|
+
base["a2a_agent_version"] = self.a2a_agent_version
|
|
39
|
+
return base
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_dict(cls, data: dict) -> VerifyA2AAgentCardStep:
|
|
43
|
+
return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], a2a_agent_version=data.get("a2a_agent_version"))
|
|
44
|
+
|
|
45
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
46
|
+
from agent_env.a2a_agent import A2AAgent
|
|
47
|
+
|
|
48
|
+
deployed = next((a for a in context.deployed_agents), None)
|
|
49
|
+
if deployed is None:
|
|
50
|
+
raise RuntimeError("No deployed agent found in context")
|
|
51
|
+
|
|
52
|
+
card = deployed.a2a_card
|
|
53
|
+
agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
|
|
54
|
+
|
|
55
|
+
# Validate card is accessible
|
|
56
|
+
if not card:
|
|
57
|
+
logger.error("Agent card is inaccessible or empty")
|
|
58
|
+
validated = {"accessible": False, "error": "Inaccessible Agent Card"}
|
|
59
|
+
agent.update_metadata({**agent.metadata, "validated_agent_card": validated})
|
|
60
|
+
raise RuntimeError("Inaccessible Agent Card: agent did not return a valid agent card at /.well-known/agent.json")
|
|
61
|
+
|
|
62
|
+
logger.info(f"Agent card for '{self.a2a_agent_id}': name={card.get('name')}")
|
|
63
|
+
|
|
64
|
+
# Check required fields
|
|
65
|
+
required_fields = {}
|
|
66
|
+
missing_required = []
|
|
67
|
+
for field in REQUIRED_CARD_FIELDS:
|
|
68
|
+
value = card.get(field)
|
|
69
|
+
present = field in card and (
|
|
70
|
+
bool(value) or (field == "skills" and value == [])
|
|
71
|
+
)
|
|
72
|
+
required_fields[field] = {"present": present}
|
|
73
|
+
if not present:
|
|
74
|
+
missing_required.append(field)
|
|
75
|
+
|
|
76
|
+
if missing_required:
|
|
77
|
+
logger.warning(f"Agent card missing required fields: {missing_required}")
|
|
78
|
+
else:
|
|
79
|
+
logger.info("Agent card has all required fields")
|
|
80
|
+
|
|
81
|
+
# Check optional fields
|
|
82
|
+
optional_fields = {}
|
|
83
|
+
missing_optional = []
|
|
84
|
+
for field in OPTIONAL_CARD_FIELDS:
|
|
85
|
+
present = field in card and bool(card[field])
|
|
86
|
+
optional_fields[field] = {"present": present}
|
|
87
|
+
if not present:
|
|
88
|
+
missing_optional.append(field)
|
|
89
|
+
|
|
90
|
+
if missing_optional:
|
|
91
|
+
logger.info(f"Agent card missing optional fields: {missing_optional}")
|
|
92
|
+
|
|
93
|
+
# Log which extensions the card advertises
|
|
94
|
+
for ext_uri in [A2AAgent.EXT_MCP_CONFIG, A2AAgent.EXT_SKILL_CONFIG, A2AAgent.EXT_TRAJECTORY, A2AAgent.EXT_AGENT_CONFIG, A2AAgent.EXT_TOOLS]:
|
|
95
|
+
ext = A2AAgent.find_extension(card, ext_uri)
|
|
96
|
+
logger.info(f" {ext_uri}: {'advertised' if ext else 'not advertised'}")
|
|
97
|
+
|
|
98
|
+
# Persist agent_card (raw) and validated_agent_card (our validation)
|
|
99
|
+
validated = {"accessible": True, "required_fields": required_fields, "optional_fields": optional_fields}
|
|
100
|
+
agent.update_metadata({**agent.metadata, "agent_card": card, "validated_agent_card": validated})
|
|
101
|
+
|
|
102
|
+
context.metadata.setdefault("verifications", {})["a2a_agent_card"] = {"agent_card": card, "validated_agent_card": validated}
|
|
103
|
+
return context
|