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,626 @@
|
|
|
1
|
+
"""Deploy A2A agent task step."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from typing import ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from agentenv_protocol.a2a_agent import (
|
|
10
|
+
NamespaceChangelogEnableResponse,
|
|
11
|
+
ObjectChangelogApplyResponse,
|
|
12
|
+
ObjectSnapshotLoadResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from agent_env.a2a_agent import protocol
|
|
16
|
+
from agent_env.a2a_agent.object_transfer import (
|
|
17
|
+
REPLY_TIMEOUT_SECONDS,
|
|
18
|
+
TRANSFER_TIMEOUT_SECONDS,
|
|
19
|
+
bounded_echo,
|
|
20
|
+
changelog_apply_call,
|
|
21
|
+
changelog_enable_call,
|
|
22
|
+
check_changelog_applied,
|
|
23
|
+
invoke_transfer,
|
|
24
|
+
snapshot_load_call,
|
|
25
|
+
)
|
|
26
|
+
from agent_env.config import get_config
|
|
27
|
+
from agent_env.env.env import DeployedEnv, DeployedSandboxEnv, Env
|
|
28
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import reachable_url, sandbox_request_headers_for_url
|
|
29
|
+
from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy
|
|
30
|
+
from agent_env.task_step.context import DeployedAgent, TaskStepContext
|
|
31
|
+
from agent_env.entity_refs import EntityRef
|
|
32
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
33
|
+
from agent_env.attribution import deploy_attribution
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _mcp_add_body(mcp_ext: dict, url: str, headers: dict | None, card_name: str | None) -> dict:
|
|
39
|
+
"""Body for the agent's mcp-config `add`; the card name is relayed verbatim when the agent's card lists `name` as optional."""
|
|
40
|
+
body: dict = {"url": url}
|
|
41
|
+
if headers:
|
|
42
|
+
body["headers"] = headers
|
|
43
|
+
add_request = mcp_ext.get("params", {}).get("methods", {}).get("add", {}).get("request", {})
|
|
44
|
+
if card_name and "name" in (add_request.get("optional") or []):
|
|
45
|
+
body["name"] = card_name
|
|
46
|
+
return body
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _choose_mcp_url(agent, env) -> str:
|
|
50
|
+
"""The env's MCP URL, as reachable from wherever the agent runs; an env outside our sandboxes gives its own."""
|
|
51
|
+
if not isinstance(env, DeployedSandboxEnv):
|
|
52
|
+
return env.mcp_url
|
|
53
|
+
return reachable_url(env.mcp_url, from_sandbox_type=env.sandbox_type, to_sandbox_type=agent.sandbox_type)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _live_mcp_url(env_id: str) -> str:
|
|
57
|
+
"""The MCP URL of an env that is referenced by id but never deployed here.
|
|
58
|
+
|
|
59
|
+
Such an env is a pointer at a live endpoint: it exposes ``mcp_url`` and has no
|
|
60
|
+
deployment to bind to.
|
|
61
|
+
"""
|
|
62
|
+
url = getattr(Env.get(env_id), "mcp_url", None)
|
|
63
|
+
if not isinstance(url, str) or not url.strip().startswith(("http://", "https://")):
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
f"Env '{env_id}' is not in context.deployed_envs; add a DeployEnvTaskStep "
|
|
66
|
+
"for it, or reference an env that exposes a live http(s) 'mcp_url'."
|
|
67
|
+
)
|
|
68
|
+
return url.strip()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _skill_fields(skill: dict) -> dict:
|
|
72
|
+
"""A ``skills`` entry as ``A2AAgent.register_skill`` arguments. ``content`` and ``s3_uri`` are
|
|
73
|
+
older names for ``skill_md`` and ``skill_s3_url``; ``skill_md`` is sent verbatim."""
|
|
74
|
+
skill_md = skill.get("skill_md", skill.get("content"))
|
|
75
|
+
return {
|
|
76
|
+
"name": skill.get("name", ""),
|
|
77
|
+
"description": skill.get("description", ""),
|
|
78
|
+
"skill_md": skill_md,
|
|
79
|
+
"object_url": (
|
|
80
|
+
skill.get("skill_s3_url", skill.get("s3_uri")) if skill_md is None else None
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DeployAgentTaskStep(TaskStep):
|
|
86
|
+
type: ClassVar[str] = "deploy_agent"
|
|
87
|
+
entity_refs = (
|
|
88
|
+
EntityRef.env("env_ids[]"),
|
|
89
|
+
EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),
|
|
90
|
+
EntityRef.artifact(
|
|
91
|
+
"agent_snapshot_files_artifact_id",
|
|
92
|
+
version_field="agent_snapshot_files_artifact_version",
|
|
93
|
+
artifact_type="file_artifact_universe",
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
id: str,
|
|
100
|
+
version: Optional[int],
|
|
101
|
+
env_ids: Optional[list[str]] = None,
|
|
102
|
+
env_step_id: Optional[str] = None,
|
|
103
|
+
a2a_agent_id: Optional[str] = None,
|
|
104
|
+
a2a_agent_version: Optional[int] = None,
|
|
105
|
+
agent_name: Optional[str] = None,
|
|
106
|
+
agent_description: Optional[str] = None,
|
|
107
|
+
env_vars: Optional[dict[str, str]] = None,
|
|
108
|
+
skills: Optional[list[dict]] = None,
|
|
109
|
+
system_prompt: Optional[str] = None,
|
|
110
|
+
disk_size_gb: Optional[float] = None,
|
|
111
|
+
litellm_base_url: Optional[str] = None,
|
|
112
|
+
sandbox_type: Optional[str] = None,
|
|
113
|
+
ttl_seconds: Optional[int] = None,
|
|
114
|
+
cpu: Optional[float] = None,
|
|
115
|
+
memory_mb: Optional[int] = None,
|
|
116
|
+
agent_snapshot_files_artifact_id: Optional[str] = None,
|
|
117
|
+
agent_snapshot_files_artifact_version: Optional[int] = None,
|
|
118
|
+
agent_snapshot_target_context_id: Optional[str] = None,
|
|
119
|
+
priority: Optional[int] = None,
|
|
120
|
+
network_policy: Optional[dict] = None,
|
|
121
|
+
role: Optional[str] = None,
|
|
122
|
+
enable_agent_changelog: bool = False,
|
|
123
|
+
agent_changelog_s3_prefix: Optional[str] = None,
|
|
124
|
+
agent_changelog_toolcall_position_exclusive: Optional[int] = None,
|
|
125
|
+
sandbox_name: Optional[str] = None,
|
|
126
|
+
enable_docker: bool = False,
|
|
127
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
128
|
+
fail_task_on_error: bool = True,
|
|
129
|
+
metadata: Optional[dict] = None,
|
|
130
|
+
agent_changelog_object_url: Optional[str] = None,
|
|
131
|
+
):
|
|
132
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
133
|
+
self.env_ids = env_ids or []
|
|
134
|
+
self.env_step_id = env_step_id
|
|
135
|
+
self.a2a_agent_id = a2a_agent_id
|
|
136
|
+
self.a2a_agent_version = a2a_agent_version
|
|
137
|
+
self.agent_name = agent_name or TaskStep.DEFAULT_AGENT_NAME
|
|
138
|
+
self.agent_description = agent_description
|
|
139
|
+
self.env_vars = env_vars or {}
|
|
140
|
+
self.skills = skills or []
|
|
141
|
+
self.system_prompt = system_prompt
|
|
142
|
+
self.disk_size_gb = disk_size_gb
|
|
143
|
+
self.litellm_base_url = litellm_base_url
|
|
144
|
+
self.sandbox_type = sandbox_type
|
|
145
|
+
self.ttl_seconds = ttl_seconds
|
|
146
|
+
self.cpu = cpu
|
|
147
|
+
self.memory_mb = memory_mb
|
|
148
|
+
self.agent_snapshot_files_artifact_id = agent_snapshot_files_artifact_id
|
|
149
|
+
self.agent_snapshot_files_artifact_version = agent_snapshot_files_artifact_version
|
|
150
|
+
self.agent_snapshot_target_context_id = agent_snapshot_target_context_id
|
|
151
|
+
self.priority = priority
|
|
152
|
+
self.metadata = metadata or {}
|
|
153
|
+
self.network_policy = NetworkPolicy.from_dict(network_policy).to_dict() if network_policy else None
|
|
154
|
+
self.role = role
|
|
155
|
+
self.enable_agent_changelog = enable_agent_changelog
|
|
156
|
+
self.agent_changelog_object_url = agent_changelog_object_url
|
|
157
|
+
self.agent_changelog_s3_prefix = agent_changelog_s3_prefix
|
|
158
|
+
self.agent_changelog_toolcall_position_exclusive = agent_changelog_toolcall_position_exclusive
|
|
159
|
+
if self.agent_changelog_object_url and self.agent_changelog_s3_prefix:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"set only one of agent_changelog_object_url and "
|
|
162
|
+
"agent_changelog_s3_prefix"
|
|
163
|
+
)
|
|
164
|
+
if self.agent_changelog_toolcall_position_exclusive is not None and not (
|
|
165
|
+
self.agent_changelog_object_url or self.agent_changelog_s3_prefix
|
|
166
|
+
):
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"agent_changelog_toolcall_position_exclusive requires a changelog source"
|
|
169
|
+
)
|
|
170
|
+
self.sandbox_name = sandbox_name
|
|
171
|
+
# Rootless Docker-in-Docker for the agent (not the VM socket); VM mode only.
|
|
172
|
+
self.enable_docker = enable_docker
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> dict:
|
|
175
|
+
base = super().to_dict()
|
|
176
|
+
base["env_ids"] = self.env_ids
|
|
177
|
+
base["env_step_id"] = self.env_step_id
|
|
178
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
179
|
+
base["a2a_agent_version"] = self.a2a_agent_version
|
|
180
|
+
base["agent_name"] = self.agent_name
|
|
181
|
+
base["agent_description"] = self.agent_description
|
|
182
|
+
base["env_vars"] = self.env_vars
|
|
183
|
+
base["skills"] = self.skills
|
|
184
|
+
base["system_prompt"] = self.system_prompt
|
|
185
|
+
base["disk_size_gb"] = self.disk_size_gb
|
|
186
|
+
base["litellm_base_url"] = self.litellm_base_url
|
|
187
|
+
base["sandbox_type"] = self.sandbox_type
|
|
188
|
+
base["ttl_seconds"] = self.ttl_seconds
|
|
189
|
+
base["cpu"] = self.cpu
|
|
190
|
+
base["memory_mb"] = self.memory_mb
|
|
191
|
+
base["agent_snapshot_files_artifact_id"] = self.agent_snapshot_files_artifact_id
|
|
192
|
+
base["agent_snapshot_files_artifact_version"] = self.agent_snapshot_files_artifact_version
|
|
193
|
+
base["agent_snapshot_target_context_id"] = self.agent_snapshot_target_context_id
|
|
194
|
+
base["priority"] = self.priority
|
|
195
|
+
base["metadata"] = self.metadata
|
|
196
|
+
base["network_policy"] = self.network_policy
|
|
197
|
+
base["role"] = self.role
|
|
198
|
+
base["enable_agent_changelog"] = self.enable_agent_changelog
|
|
199
|
+
base["agent_changelog_object_url"] = self.agent_changelog_object_url
|
|
200
|
+
base["agent_changelog_s3_prefix"] = self.agent_changelog_s3_prefix
|
|
201
|
+
base["agent_changelog_toolcall_position_exclusive"] = self.agent_changelog_toolcall_position_exclusive
|
|
202
|
+
base["sandbox_name"] = self.sandbox_name
|
|
203
|
+
base["enable_docker"] = self.enable_docker
|
|
204
|
+
return base
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_dict(cls, data: dict) -> DeployAgentTaskStep:
|
|
208
|
+
return cls(
|
|
209
|
+
**cls._base_from_dict(data),
|
|
210
|
+
env_ids=data.get("env_ids"),
|
|
211
|
+
env_step_id=data.get("env_step_id"),
|
|
212
|
+
a2a_agent_id=data.get("a2a_agent_id"),
|
|
213
|
+
a2a_agent_version=data.get("a2a_agent_version"),
|
|
214
|
+
agent_name=data.get("agent_name"),
|
|
215
|
+
agent_description=data.get("agent_description"),
|
|
216
|
+
env_vars=data.get("env_vars"),
|
|
217
|
+
skills=data.get("skills"),
|
|
218
|
+
system_prompt=data.get("system_prompt"),
|
|
219
|
+
disk_size_gb=data.get("disk_size_gb"),
|
|
220
|
+
litellm_base_url=data.get("litellm_base_url"),
|
|
221
|
+
sandbox_type=data.get("sandbox_type"),
|
|
222
|
+
ttl_seconds=data.get("ttl_seconds"),
|
|
223
|
+
cpu=data.get("cpu"),
|
|
224
|
+
memory_mb=data.get("memory_mb"),
|
|
225
|
+
agent_snapshot_files_artifact_id=data.get("agent_snapshot_files_artifact_id"),
|
|
226
|
+
agent_snapshot_files_artifact_version=data.get("agent_snapshot_files_artifact_version"),
|
|
227
|
+
agent_snapshot_target_context_id=data.get("agent_snapshot_target_context_id"),
|
|
228
|
+
priority=data.get("priority"),
|
|
229
|
+
metadata=dict(data.get("metadata") or {}),
|
|
230
|
+
network_policy=data.get("network_policy"),
|
|
231
|
+
role=data.get("role"),
|
|
232
|
+
enable_agent_changelog=data.get("enable_agent_changelog", False),
|
|
233
|
+
agent_changelog_object_url=data.get("agent_changelog_object_url"),
|
|
234
|
+
agent_changelog_s3_prefix=data.get("agent_changelog_s3_prefix"),
|
|
235
|
+
agent_changelog_toolcall_position_exclusive=data.get("agent_changelog_toolcall_position_exclusive"),
|
|
236
|
+
sandbox_name=data.get("sandbox_name"),
|
|
237
|
+
enable_docker=data.get("enable_docker", False),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
241
|
+
from agent_env.a2a_agent import A2AAgent
|
|
242
|
+
from agent_env.a2a_agent.a2a_agent import DEFAULT_A2A_PORT
|
|
243
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import (
|
|
244
|
+
SANDBOX_MODE_VM,
|
|
245
|
+
build_sandbox_provider,
|
|
246
|
+
get_sandbox_provider,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Resolved before anything is provisioned so an unbound env fails before a sandbox exists.
|
|
250
|
+
env_deployments = {env_id: self._deployed_env(context, env_id) for env_id in self.env_ids}
|
|
251
|
+
|
|
252
|
+
existing = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
|
|
253
|
+
if existing is not None:
|
|
254
|
+
raise RuntimeError(f"Agent with name '{self.agent_name}' is already deployed")
|
|
255
|
+
|
|
256
|
+
linked_sandbox = None
|
|
257
|
+
if self.sandbox_name:
|
|
258
|
+
ds = next((s for s in context.deployed_sandboxes if s.sandbox_name == self.sandbox_name), None)
|
|
259
|
+
if ds is None:
|
|
260
|
+
raise RuntimeError(
|
|
261
|
+
f"Sandbox '{self.sandbox_name}' not found in context.deployed_sandboxes; "
|
|
262
|
+
f"deploy it with DeploySandboxTaskStep before this step"
|
|
263
|
+
)
|
|
264
|
+
if ds.sandbox_mode != SANDBOX_MODE_VM:
|
|
265
|
+
raise RuntimeError(
|
|
266
|
+
f"Cannot link agent to sandbox '{self.sandbox_name}' (mode={ds.sandbox_mode!r}); "
|
|
267
|
+
f"only VM-mode sandboxes can host an additional agent container"
|
|
268
|
+
)
|
|
269
|
+
if not ds.tunnel_urls or str(DEFAULT_A2A_PORT) not in ds.tunnel_urls:
|
|
270
|
+
raise RuntimeError(
|
|
271
|
+
f"Sandbox '{self.sandbox_name}' does not expose port {DEFAULT_A2A_PORT}; "
|
|
272
|
+
f"re-deploy DeploySandboxTaskStep with exposed_ports=[{DEFAULT_A2A_PORT}]"
|
|
273
|
+
)
|
|
274
|
+
provider = (
|
|
275
|
+
build_sandbox_provider(ds.sandbox_type) if ds.sandbox_type else get_sandbox_provider()
|
|
276
|
+
)
|
|
277
|
+
linked_sandbox = await provider.get_sandbox(ds.sandbox_id)
|
|
278
|
+
logger.info(
|
|
279
|
+
f"Linking agent '{self.agent_name}' to pre-existing sandbox "
|
|
280
|
+
f"'{self.sandbox_name}' (sandbox_id={ds.sandbox_id}, type={ds.sandbox_type})"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
config = get_config()
|
|
284
|
+
user_overrides = context.metadata.get("user_overrides", {})
|
|
285
|
+
a2a_agent_id = user_overrides.get("a2a_agent_id") or self.a2a_agent_id or config.get_default_a2a_agent_id()
|
|
286
|
+
a2a_agent_version = (
|
|
287
|
+
user_overrides["a2a_agent_version"]
|
|
288
|
+
if "a2a_agent_version" in user_overrides
|
|
289
|
+
else self.a2a_agent_version
|
|
290
|
+
)
|
|
291
|
+
agent = A2AAgent.get(a2a_agent_id, a2a_agent_version)
|
|
292
|
+
requested_disk = user_overrides.get("agent_disk_size_gb") or self.disk_size_gb
|
|
293
|
+
min_disk = agent.metadata.get("min_disk_size_gb")
|
|
294
|
+
agent_disk_size_gb = max(requested_disk or 0, min_disk or 0) or None
|
|
295
|
+
env_vars = dict(self.env_vars) if self.env_vars else {}
|
|
296
|
+
override_litellm_key = user_overrides.get("litellm_api_key")
|
|
297
|
+
if override_litellm_key:
|
|
298
|
+
env_vars["LITELLM_API_KEY"] = override_litellm_key
|
|
299
|
+
resolved_litellm_base_url = user_overrides.get("litellm_base_url") or self.litellm_base_url
|
|
300
|
+
if resolved_litellm_base_url:
|
|
301
|
+
env_vars["LITELLM_BASE_URL"] = resolved_litellm_base_url
|
|
302
|
+
|
|
303
|
+
# Cost-attribution (project_id / task_id) is delivered per-prompt
|
|
304
|
+
# via the A2A `/ext/agent-config` extension from prompt_agent.py —
|
|
305
|
+
# not baked as container env vars at deploy time. See
|
|
306
|
+
# the openai_agents_sdk A2A agent in the universe-generation pipeline
|
|
307
|
+
# for the reference implementation that reads agent-config and threads
|
|
308
|
+
# `user` / `metadata.tags` into each LiteLLM call.
|
|
309
|
+
|
|
310
|
+
resolved_sandbox_type = user_overrides.get("agent_sandbox") or self.sandbox_type
|
|
311
|
+
deploy_kwargs = {"env_vars": env_vars if env_vars else None}
|
|
312
|
+
if agent_disk_size_gb is not None:
|
|
313
|
+
deploy_kwargs["disk_size_gb"] = agent_disk_size_gb
|
|
314
|
+
if resolved_sandbox_type is not None:
|
|
315
|
+
deploy_kwargs["sandbox_type"] = resolved_sandbox_type
|
|
316
|
+
resolved_ttl = self._resolved_ttl_seconds(context)
|
|
317
|
+
deploy_kwargs["ttl_seconds"] = resolved_ttl
|
|
318
|
+
if self.cpu is not None:
|
|
319
|
+
deploy_kwargs["cpu"] = self.cpu
|
|
320
|
+
if self.memory_mb is not None:
|
|
321
|
+
deploy_kwargs["memory"] = self.memory_mb
|
|
322
|
+
deploy_kwargs["attribution"] = deploy_attribution(self, context)
|
|
323
|
+
_priority_override = user_overrides.get("priority")
|
|
324
|
+
resolved_priority = _priority_override if _priority_override is not None else self.priority
|
|
325
|
+
if resolved_priority is not None:
|
|
326
|
+
deploy_kwargs["priority"] = resolved_priority
|
|
327
|
+
_policy_override = user_overrides.get("network_policy")
|
|
328
|
+
resolved_policy = _policy_override if _policy_override is not None else self.network_policy
|
|
329
|
+
if resolved_policy is not None:
|
|
330
|
+
deploy_kwargs["network_policy"] = NetworkPolicy.from_dict(resolved_policy)
|
|
331
|
+
if self.enable_docker:
|
|
332
|
+
deploy_kwargs["enable_docker"] = True
|
|
333
|
+
if linked_sandbox is not None:
|
|
334
|
+
deploy_kwargs["sandbox"] = linked_sandbox
|
|
335
|
+
deployed = await agent.deploy(**deploy_kwargs)
|
|
336
|
+
a2a_url = deployed.a2a_url
|
|
337
|
+
card = deployed.agent_card
|
|
338
|
+
logger.info(f"A2A agent '{a2a_agent_id}' deployed at {a2a_url}")
|
|
339
|
+
|
|
340
|
+
env_mcp_urls: list[tuple[str, str, dict | None, str | None]] = [] # (env_id, url, headers, card_name)
|
|
341
|
+
for env_id in self.env_ids:
|
|
342
|
+
deployed_env = env_deployments[env_id]
|
|
343
|
+
if deployed_env is not None:
|
|
344
|
+
url = _choose_mcp_url(deployed, deployed_env)
|
|
345
|
+
card_name = (deployed_env.environment_card or {}).get("name")
|
|
346
|
+
if not card_name:
|
|
347
|
+
logger.warning(f"Env {env_id} has no env card name; the agent will pick its own MCP alias")
|
|
348
|
+
else:
|
|
349
|
+
url = _live_mcp_url(env_id)
|
|
350
|
+
card_name = None
|
|
351
|
+
env_mcp_urls.append((env_id, url, sandbox_request_headers_for_url(url) or None, card_name))
|
|
352
|
+
|
|
353
|
+
mcp_ext = A2AAgent.find_extension(card, A2AAgent.EXT_MCP_CONFIG)
|
|
354
|
+
if mcp_ext and env_mcp_urls:
|
|
355
|
+
endpoint = a2a_url + mcp_ext["params"]["endpoint"]
|
|
356
|
+
async with httpx.AsyncClient() as client:
|
|
357
|
+
for env_id, mcp_url, headers, card_name in env_mcp_urls:
|
|
358
|
+
body = _mcp_add_body(mcp_ext, mcp_url, headers, card_name)
|
|
359
|
+
logger.info(f"Registering MCP for env {env_id} via {mcp_url}")
|
|
360
|
+
resp = await client.post(endpoint, json=body, timeout=180)
|
|
361
|
+
if resp.status_code >= 400:
|
|
362
|
+
logger.error(f"Failed to register MCP {env_id}: {resp.status_code} {resp.text}")
|
|
363
|
+
resp.raise_for_status()
|
|
364
|
+
logger.info(f"Registered MCP {env_id}: {resp.json()}")
|
|
365
|
+
|
|
366
|
+
skill_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SKILL_CONFIG)
|
|
367
|
+
if skill_ext and self.skills:
|
|
368
|
+
for skill in self.skills:
|
|
369
|
+
result = await A2AAgent.register_skill(deployed, **_skill_fields(skill))
|
|
370
|
+
logger.info(f"Registered skill: {result}")
|
|
371
|
+
|
|
372
|
+
if self.system_prompt is not None:
|
|
373
|
+
config_ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
|
|
374
|
+
if config_ext is None:
|
|
375
|
+
logger.warning(f"Agent '{self.agent_name}' does not advertise {A2AAgent.EXT_AGENT_CONFIG} — skipping system_prompt")
|
|
376
|
+
else:
|
|
377
|
+
system_prompt = self.system_prompt
|
|
378
|
+
seed = context.metadata.get("seed", {})
|
|
379
|
+
if seed:
|
|
380
|
+
for key, value in seed.items():
|
|
381
|
+
system_prompt = system_prompt.replace(f"<{key}>", str(value))
|
|
382
|
+
supported = (config_ext.get("params") or {}).get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
|
|
383
|
+
if "system_prompt" not in supported:
|
|
384
|
+
logger.warning(f"Agent '{self.agent_name}' does not list 'system_prompt' in supported agent-config fields — skipping")
|
|
385
|
+
else:
|
|
386
|
+
endpoint = a2a_url + (config_ext.get("params") or {}).get("endpoint", "/ext/agent-config")
|
|
387
|
+
await protocol.post_agent_config(endpoint, {"system_prompt": system_prompt})
|
|
388
|
+
logger.info(f"Set system_prompt on agent '{self.agent_name}'")
|
|
389
|
+
|
|
390
|
+
identity_payload: dict[str, str] = {"name": self.agent_name}
|
|
391
|
+
if self.agent_description is not None:
|
|
392
|
+
identity_payload["description"] = self.agent_description
|
|
393
|
+
if self.role is not None:
|
|
394
|
+
identity_payload["role"] = self.role
|
|
395
|
+
config_ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
|
|
396
|
+
if config_ext is not None:
|
|
397
|
+
supported = (config_ext.get("params") or {}).get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
|
|
398
|
+
identity_payload = {k: v for k, v in identity_payload.items() if k in supported}
|
|
399
|
+
if identity_payload:
|
|
400
|
+
endpoint = a2a_url + (config_ext.get("params") or {}).get("endpoint", "/ext/agent-config")
|
|
401
|
+
await protocol.post_agent_config(endpoint, identity_payload)
|
|
402
|
+
card.update(identity_payload)
|
|
403
|
+
logger.info(f"Stamped card identity on '{self.agent_name}': {sorted(identity_payload)}")
|
|
404
|
+
|
|
405
|
+
default_model = config.get_model_for_role("agent")
|
|
406
|
+
if default_model and not context.default_agent_model:
|
|
407
|
+
context.default_agent_model = default_model
|
|
408
|
+
logger.info(f"Set context.default_agent_model to '{default_model}'")
|
|
409
|
+
|
|
410
|
+
if self.agent_snapshot_files_artifact_id:
|
|
411
|
+
await self._load_snapshot(a2a_url, card, a2a_agent_id, context)
|
|
412
|
+
|
|
413
|
+
if self.agent_changelog_object_url or self.agent_changelog_s3_prefix:
|
|
414
|
+
await self._apply_agent_changelog(a2a_url, card, context)
|
|
415
|
+
|
|
416
|
+
if self.enable_agent_changelog:
|
|
417
|
+
await self._configure_agent_changelog(
|
|
418
|
+
a2a_url, card, context, expires_in=resolved_ttl
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
context.deployed_agents.append(DeployedAgent(
|
|
422
|
+
agent_name=self.agent_name,
|
|
423
|
+
api_url=a2a_url,
|
|
424
|
+
sandbox_id=deployed.sandbox_id,
|
|
425
|
+
sandbox_type=deployed.sandbox_type,
|
|
426
|
+
a2a_url=a2a_url,
|
|
427
|
+
a2a_card=card,
|
|
428
|
+
instance_id=deployed.instance_id,
|
|
429
|
+
role=self.role,
|
|
430
|
+
network_policy=deployed.network_policy,
|
|
431
|
+
))
|
|
432
|
+
return context
|
|
433
|
+
|
|
434
|
+
@staticmethod
|
|
435
|
+
def _deploy_step_of(deployed_env: DeployedEnv) -> Optional[str]:
|
|
436
|
+
return (deployed_env.metadata or {}).get("deploy_step_id")
|
|
437
|
+
|
|
438
|
+
def _deployed_env(self, context: TaskStepContext, env_id: str) -> Optional[DeployedEnv]:
|
|
439
|
+
"""The deployment of `env_id` this step is bound to, or None if it isn't deployed here."""
|
|
440
|
+
matches = [d for d in context.deployed_envs if d.env_id == env_id]
|
|
441
|
+
if self.env_step_id is not None:
|
|
442
|
+
# A filter, not a hint: an unmatched id is an error, never a substitute.
|
|
443
|
+
# Envs with no deployments fall through to the caller's live-endpoint path.
|
|
444
|
+
named = [d for d in matches if self._deploy_step_of(d) == self.env_step_id]
|
|
445
|
+
if matches and not named:
|
|
446
|
+
raise RuntimeError(
|
|
447
|
+
f"deploy_agent '{self.id}': env_step_id '{self.env_step_id}' names no deployment of "
|
|
448
|
+
f"env '{env_id}' (deployed by: {[self._deploy_step_of(d) for d in matches]})"
|
|
449
|
+
)
|
|
450
|
+
matches = named
|
|
451
|
+
if len(matches) > 1:
|
|
452
|
+
raise RuntimeError(
|
|
453
|
+
f"deploy_agent '{self.id}': {len(matches)} deployments of env '{env_id}' "
|
|
454
|
+
f"(deployed by: {[self._deploy_step_of(d) for d in matches]}). Set env_step_id "
|
|
455
|
+
"to the deploy_env step this agent should bind to."
|
|
456
|
+
)
|
|
457
|
+
return matches[0] if matches else None
|
|
458
|
+
|
|
459
|
+
def _resolved_ttl_seconds(self, context: TaskStepContext) -> int:
|
|
460
|
+
override = context.metadata.get("user_overrides", {}).get("ttl_seconds")
|
|
461
|
+
if override is not None:
|
|
462
|
+
return override
|
|
463
|
+
if self.ttl_seconds is not None:
|
|
464
|
+
return self.ttl_seconds
|
|
465
|
+
return TaskStep.DEFAULT_TTL_SECONDS
|
|
466
|
+
|
|
467
|
+
async def _configure_agent_changelog(
|
|
468
|
+
self,
|
|
469
|
+
a2a_url: str,
|
|
470
|
+
card: dict,
|
|
471
|
+
context: TaskStepContext,
|
|
472
|
+
*,
|
|
473
|
+
expires_in: int,
|
|
474
|
+
) -> None:
|
|
475
|
+
"""Enable whole-writable-layer changelog capture via the snapshot enable-changelog method; records the derived prefix in context.metadata['agent_changelog']."""
|
|
476
|
+
from agent_env.a2a_agent import A2AAgent
|
|
477
|
+
|
|
478
|
+
snapshot_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SNAPSHOT)
|
|
479
|
+
method, path = A2AAgent.operation(snapshot_ext, A2AAgent.SNAPSHOT_METHOD_ENABLE_CHANGELOG)
|
|
480
|
+
if method is None:
|
|
481
|
+
raise RuntimeError(
|
|
482
|
+
f"Agent '{self.agent_name}' does not support the snapshot "
|
|
483
|
+
f"'{A2AAgent.SNAPSHOT_METHOD_ENABLE_CHANGELOG}' method; cannot enable_agent_changelog"
|
|
484
|
+
)
|
|
485
|
+
instance_id = (context.instance_id or "noinstance").replace("/", "_")
|
|
486
|
+
agent_name = self.agent_name.replace("/", "_")
|
|
487
|
+
config = get_config()
|
|
488
|
+
store = config.get_object_store()
|
|
489
|
+
namespace_url = store.object_url(f"{config.get_artifact_key_prefix()}agent_changelog/{instance_id}/{agent_name}")
|
|
490
|
+
call = await asyncio.to_thread(
|
|
491
|
+
changelog_enable_call,
|
|
492
|
+
method,
|
|
493
|
+
store,
|
|
494
|
+
agent_name=self.agent_name,
|
|
495
|
+
namespace_url=namespace_url,
|
|
496
|
+
expires_in=expires_in,
|
|
497
|
+
)
|
|
498
|
+
answer = await invoke_transfer(
|
|
499
|
+
a2a_url + path,
|
|
500
|
+
call,
|
|
501
|
+
verb="POST",
|
|
502
|
+
operation="changelog enable",
|
|
503
|
+
timeout=REPLY_TIMEOUT_SECONDS,
|
|
504
|
+
response_model=NamespaceChangelogEnableResponse,
|
|
505
|
+
)
|
|
506
|
+
if call.mode == "objects":
|
|
507
|
+
roots, object_url = answer.roots, namespace_url
|
|
508
|
+
else:
|
|
509
|
+
roots = answer.get("roots")
|
|
510
|
+
object_url = bounded_echo(namespace_url, answer.get("s3_prefix") or namespace_url)
|
|
511
|
+
context.metadata.setdefault("agent_changelog", []).append({
|
|
512
|
+
"agent_name": self.agent_name,
|
|
513
|
+
"roots": roots,
|
|
514
|
+
"transfer_mode": call.mode,
|
|
515
|
+
"object_url": object_url,
|
|
516
|
+
})
|
|
517
|
+
logger.info(f"agent-changelog capture enabled on '{self.agent_name}': {object_url}")
|
|
518
|
+
|
|
519
|
+
async def _apply_agent_changelog(self, a2a_url: str, card: dict, context: TaskStepContext) -> None:
|
|
520
|
+
"""Rewind this fresh agent to a point in a source changelog (fs + conversation) via the snapshot apply-changelog method and resume."""
|
|
521
|
+
from agent_env.a2a_agent import A2AAgent
|
|
522
|
+
|
|
523
|
+
snapshot_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SNAPSHOT)
|
|
524
|
+
method, path = A2AAgent.operation(snapshot_ext, A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG)
|
|
525
|
+
if method is None:
|
|
526
|
+
raise RuntimeError(
|
|
527
|
+
f"Agent '{self.agent_name}' does not support the snapshot "
|
|
528
|
+
f"'{A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG}' method; cannot apply agent changelog"
|
|
529
|
+
)
|
|
530
|
+
store = get_config().get_object_store()
|
|
531
|
+
source_url = self.agent_changelog_object_url or self.agent_changelog_s3_prefix
|
|
532
|
+
call = await asyncio.to_thread(
|
|
533
|
+
changelog_apply_call,
|
|
534
|
+
method,
|
|
535
|
+
store,
|
|
536
|
+
agent_name=self.agent_name,
|
|
537
|
+
source_url=source_url,
|
|
538
|
+
portable=bool(self.agent_changelog_object_url),
|
|
539
|
+
up_to_tool_call_exclusive=self.agent_changelog_toolcall_position_exclusive,
|
|
540
|
+
resume_conversation=True,
|
|
541
|
+
target_context_id=self.agent_snapshot_target_context_id,
|
|
542
|
+
)
|
|
543
|
+
answer = await invoke_transfer(
|
|
544
|
+
a2a_url + path,
|
|
545
|
+
call,
|
|
546
|
+
verb="PUT",
|
|
547
|
+
operation="changelog apply",
|
|
548
|
+
timeout=TRANSFER_TIMEOUT_SECONDS,
|
|
549
|
+
response_model=ObjectChangelogApplyResponse,
|
|
550
|
+
)
|
|
551
|
+
if call.mode == "objects":
|
|
552
|
+
check_changelog_applied(answer, call, agent_name=self.agent_name)
|
|
553
|
+
context_id = answer.context_id
|
|
554
|
+
else:
|
|
555
|
+
context_id = answer.get("context_id")
|
|
556
|
+
context.metadata.setdefault("agent_changelog_rewinds", []).append({
|
|
557
|
+
"agent_name": self.agent_name,
|
|
558
|
+
"context_id": context_id,
|
|
559
|
+
"up_to_tool_call_exclusive": self.agent_changelog_toolcall_position_exclusive,
|
|
560
|
+
"transfer_mode": call.mode,
|
|
561
|
+
"object_url": source_url,
|
|
562
|
+
})
|
|
563
|
+
logger.info(
|
|
564
|
+
f"agent-changelog applied onto '{self.agent_name}' from {source_url} "
|
|
565
|
+
f"(position_exclusive={self.agent_changelog_toolcall_position_exclusive}, context_id={context_id})"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
async def _load_snapshot(
|
|
569
|
+
self,
|
|
570
|
+
a2a_url: str,
|
|
571
|
+
card: dict,
|
|
572
|
+
a2a_agent_id: str,
|
|
573
|
+
context: TaskStepContext,
|
|
574
|
+
) -> None:
|
|
575
|
+
"""Restore a snapshot into the freshly-deployed agent via /ext/snapshot."""
|
|
576
|
+
from agent_env.a2a_agent import A2AAgent
|
|
577
|
+
from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
|
|
578
|
+
|
|
579
|
+
snapshot_ext = A2AAgent.find_extension(card, A2AAgent.EXT_SNAPSHOT)
|
|
580
|
+
if not snapshot_ext:
|
|
581
|
+
raise RuntimeError(
|
|
582
|
+
f"Cannot load snapshot: agent '{a2a_agent_id}' does not advertise "
|
|
583
|
+
f"the snapshot extension ({A2AAgent.EXT_SNAPSHOT})"
|
|
584
|
+
)
|
|
585
|
+
universe = FileArtifactUniverse.get(
|
|
586
|
+
self.agent_snapshot_files_artifact_id,
|
|
587
|
+
self.agent_snapshot_files_artifact_version,
|
|
588
|
+
)
|
|
589
|
+
if not universe.bundle_object_url:
|
|
590
|
+
raise RuntimeError(
|
|
591
|
+
f"FileArtifactUniverse '{universe.id}' v{universe.version} has no bundle_s3_url; "
|
|
592
|
+
"snapshot universes must be created via put_existing or put_bundled"
|
|
593
|
+
)
|
|
594
|
+
load_method, load_path = A2AAgent.operation(snapshot_ext, "load")
|
|
595
|
+
call = await asyncio.to_thread(
|
|
596
|
+
snapshot_load_call,
|
|
597
|
+
load_method,
|
|
598
|
+
get_config().get_object_store(),
|
|
599
|
+
agent_name=self.agent_name,
|
|
600
|
+
bundle_url=universe.bundle_object_url,
|
|
601
|
+
file_names=set((universe.file_artifact_refs or universe.file_artifact_ids or {}).keys()),
|
|
602
|
+
target_context_id=self.agent_snapshot_target_context_id,
|
|
603
|
+
)
|
|
604
|
+
answer = await invoke_transfer(
|
|
605
|
+
a2a_url + load_path,
|
|
606
|
+
call,
|
|
607
|
+
verb="PUT",
|
|
608
|
+
operation="snapshot load",
|
|
609
|
+
timeout=TRANSFER_TIMEOUT_SECONDS,
|
|
610
|
+
response_model=ObjectSnapshotLoadResponse,
|
|
611
|
+
)
|
|
612
|
+
loaded_context_id = (
|
|
613
|
+
answer.context_id if call.mode == "objects" else answer.get("context_id")
|
|
614
|
+
)
|
|
615
|
+
logger.info(
|
|
616
|
+
f"Loaded snapshot universe={universe.id} v{universe.version} "
|
|
617
|
+
f"into agent '{self.agent_name}' as context_id={loaded_context_id}"
|
|
618
|
+
)
|
|
619
|
+
if loaded_context_id:
|
|
620
|
+
loaded = context.metadata.setdefault("agent_loaded_snapshots", [])
|
|
621
|
+
loaded.append({
|
|
622
|
+
"agent_name": self.agent_name,
|
|
623
|
+
"context_id": loaded_context_id,
|
|
624
|
+
"source_artifact_id": universe.id,
|
|
625
|
+
"source_artifact_version": universe.version,
|
|
626
|
+
})
|