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,132 @@
|
|
|
1
|
+
"""Arm a deployed env gateway's virtual clock, then sync its backing servers to it.
|
|
2
|
+
|
|
3
|
+
Arms the clock (set_time) and reads the gateway's server-reachable env_get_time_url (state), both as the
|
|
4
|
+
stored env card declares clock/v1, then invokes sync_time on each backing server whose child env card
|
|
5
|
+
advertises it. Non-advertisers are skipped when tolerate_missing_sync_time (default True). Place right
|
|
6
|
+
before prompt_agent so the virtual timeline starts at agent-start.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import ClassVar, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from agentenv_protocol import client as protocol_v1
|
|
16
|
+
|
|
17
|
+
from agent_env.env.gateway.constants import EXT_CLOCK_URI
|
|
18
|
+
from agent_env.task_step.context import TaskStepContext
|
|
19
|
+
from agent_env.entity_refs import EntityRef
|
|
20
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SyncEnvClockTaskStep(TaskStep):
|
|
26
|
+
type: ClassVar[str] = "sync_env_clock"
|
|
27
|
+
entity_refs = (EntityRef.env("env_id"),)
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
id: str,
|
|
32
|
+
version: Optional[int],
|
|
33
|
+
env_id: str,
|
|
34
|
+
virtual_time: str,
|
|
35
|
+
virtual_seconds_per_real_second: float = 1.0,
|
|
36
|
+
tolerate_missing_sync_time: bool = True,
|
|
37
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
38
|
+
fail_task_on_error: bool = True,
|
|
39
|
+
timeout_seconds: int = 30,
|
|
40
|
+
):
|
|
41
|
+
"""virtual_seconds_per_real_second = clock speed (1.0 real time, 0 frozen, 86400 cap =
|
|
42
|
+
1 real s -> 1 virtual day). For ~V virtual seconds over an ~R-real-second task, pass V/R."""
|
|
43
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
44
|
+
if not isinstance(virtual_time, str):
|
|
45
|
+
raise ValueError("virtual_time must be an RFC3339 string")
|
|
46
|
+
self.env_id = env_id
|
|
47
|
+
self.virtual_time = virtual_time
|
|
48
|
+
self.virtual_seconds_per_real_second = virtual_seconds_per_real_second
|
|
49
|
+
self.tolerate_missing_sync_time = tolerate_missing_sync_time
|
|
50
|
+
self.timeout_seconds = timeout_seconds
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict:
|
|
53
|
+
base = super().to_dict()
|
|
54
|
+
base["env_id"] = self.env_id
|
|
55
|
+
base["virtual_time"] = self.virtual_time
|
|
56
|
+
base["virtual_seconds_per_real_second"] = self.virtual_seconds_per_real_second
|
|
57
|
+
base["tolerate_missing_sync_time"] = self.tolerate_missing_sync_time
|
|
58
|
+
base["timeout_seconds"] = self.timeout_seconds
|
|
59
|
+
return base
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, data: dict) -> SyncEnvClockTaskStep:
|
|
63
|
+
return cls(
|
|
64
|
+
**cls._base_from_dict(data),
|
|
65
|
+
env_id=data["env_id"],
|
|
66
|
+
virtual_time=data["virtual_time"],
|
|
67
|
+
virtual_seconds_per_real_second=data.get("virtual_seconds_per_real_second", 1.0),
|
|
68
|
+
tolerate_missing_sync_time=data.get("tolerate_missing_sync_time", True),
|
|
69
|
+
timeout_seconds=data.get("timeout_seconds", 30),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _service_names(self, deployed) -> list[str]:
|
|
73
|
+
"""Backing MCP server names for the env (mirrors apply_server_config's enumeration)."""
|
|
74
|
+
from agent_env.env.env import Env
|
|
75
|
+
env = Env.get(deployed.env_id, deployed.env_version)
|
|
76
|
+
names = [e.environment_name for e in (getattr(env, "mcp_server_envs", None) or [])]
|
|
77
|
+
if not names and getattr(env, "environment_name", None):
|
|
78
|
+
names = [env.environment_name]
|
|
79
|
+
return names
|
|
80
|
+
|
|
81
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
82
|
+
from agent_env.env.env import gateway_url_of
|
|
83
|
+
|
|
84
|
+
deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
|
|
85
|
+
if deployed is None:
|
|
86
|
+
raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
|
|
87
|
+
try:
|
|
88
|
+
await deployed.invoke(EXT_CLOCK_URI, "set_time",
|
|
89
|
+
{"virtual_time": self.virtual_time, "virtual_seconds_per_real_second": self.virtual_seconds_per_real_second},
|
|
90
|
+
timeout=self.timeout_seconds)
|
|
91
|
+
except httpx.HTTPStatusError as e:
|
|
92
|
+
raise RuntimeError(f"clock arm failed (HTTP {e.response.status_code}): {e.response.text}") from e
|
|
93
|
+
state = await deployed.invoke(EXT_CLOCK_URI, "state", timeout=self.timeout_seconds)
|
|
94
|
+
env_get_time_url = state.get("env_get_time_url")
|
|
95
|
+
|
|
96
|
+
synced, skipped = [], []
|
|
97
|
+
for service in self._service_names(deployed):
|
|
98
|
+
outcome = await self._sync_one(deployed, gateway_url_of(deployed), service, env_get_time_url)
|
|
99
|
+
(synced if outcome.get("synced") else skipped).append({"service": service, "environment": service, **outcome})
|
|
100
|
+
|
|
101
|
+
context.metadata.setdefault("clock_configurations", []).append({
|
|
102
|
+
"step_id": self.id, "env_id": self.env_id,
|
|
103
|
+
"virtual_time": self.virtual_time, "virtual_seconds_per_real_second": self.virtual_seconds_per_real_second,
|
|
104
|
+
"env_get_time_url": env_get_time_url, "synced": synced, "skipped": skipped,
|
|
105
|
+
})
|
|
106
|
+
logger.info(f"sync_env_clock env={self.env_id}: armed rate={self.virtual_seconds_per_real_second}; "
|
|
107
|
+
f"synced={[s['service'] for s in synced]} skipped={[s['service'] for s in skipped]}")
|
|
108
|
+
if not synced:
|
|
109
|
+
logger.warning(f"sync_env_clock env={self.env_id}: clock armed but NO server synced "
|
|
110
|
+
f"({[(s['service'], s.get('reason')) for s in skipped]}) — every tool still answers wall time")
|
|
111
|
+
return context
|
|
112
|
+
|
|
113
|
+
async def _sync_one(self, deployed, gateway_url: str, service: str, env_get_time_url: Optional[str]) -> dict:
|
|
114
|
+
"""Invoke sync_time on one server if its card advertises clock/v1; skip (or raise) per the flag."""
|
|
115
|
+
from agent_env.env import legacy_protocol
|
|
116
|
+
|
|
117
|
+
base_url, card = await legacy_protocol.child_env_card(deployed, gateway_url, service, timeout=self.timeout_seconds)
|
|
118
|
+
if card is None:
|
|
119
|
+
if self.tolerate_missing_sync_time:
|
|
120
|
+
return {"synced": False, "reason": "no_env_card"}
|
|
121
|
+
raise RuntimeError(f"{service} has no env card: it serves none, its card isn't named {service!r}, or the deploy couldn't read it")
|
|
122
|
+
if protocol_v1.find_extension(card, EXT_CLOCK_URI) is None:
|
|
123
|
+
if self.tolerate_missing_sync_time:
|
|
124
|
+
logger.info(f"sync_env_clock: {service} does not advertise {EXT_CLOCK_URI}; skipping")
|
|
125
|
+
return {"synced": False, "reason": "not_advertised"}
|
|
126
|
+
raise RuntimeError(f"{service} does not advertise {EXT_CLOCK_URI}")
|
|
127
|
+
if not env_get_time_url:
|
|
128
|
+
raise RuntimeError(f"gateway did not expose env_get_time_url; cannot sync {service}")
|
|
129
|
+
result = await protocol_v1.invoke_extension(base_url, card, EXT_CLOCK_URI,
|
|
130
|
+
params={"env_get_time_url": env_get_time_url},
|
|
131
|
+
timeout=self.timeout_seconds)
|
|
132
|
+
return {"synced": True, "result": result}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Terminate the sandboxes behind named agents, envs and bare sandboxes, mid-run.
|
|
2
|
+
|
|
3
|
+
A long pipeline keeps every sandbox it deployed until the run ends or its TTL expires. Place this
|
|
4
|
+
step after the last step that uses a target (via ``depends_on``) to stop paying for it early.
|
|
5
|
+
Strictly best-effort: a sandbox that is already gone or fails to terminate is logged and the run
|
|
6
|
+
continues. The step never raises on a terminate, so a retry never re-runs it over partly torn-down
|
|
7
|
+
state, and ``fail_task_on_error`` must stay false. The ids it terminated are appended to ``metadata["torn_down_sandbox_ids"]``; the
|
|
8
|
+
``deployed_*`` records stay, since context list fields persist additions only.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from agent_env.entity_refs import EntityRef
|
|
18
|
+
from agent_env.env.env import DeployedSandboxEnv
|
|
19
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import (
|
|
20
|
+
SandboxProvider,
|
|
21
|
+
build_sandbox_provider,
|
|
22
|
+
get_agent_sandbox_provider,
|
|
23
|
+
get_env_sandbox_provider,
|
|
24
|
+
get_sandbox_provider,
|
|
25
|
+
)
|
|
26
|
+
from agent_env.task_step.context import TaskStepContext
|
|
27
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
TORN_DOWN_KEY = "torn_down_sandbox_ids"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TeardownSandboxesTaskStep(TaskStep):
|
|
35
|
+
"""Terminate every sandbox behind the named deployed agents, envs and bare sandboxes."""
|
|
36
|
+
|
|
37
|
+
type = "teardown_sandboxes"
|
|
38
|
+
entity_refs = (EntityRef.env("env_ids[]"),)
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
id: str,
|
|
43
|
+
version: Optional[int],
|
|
44
|
+
agent_names: Optional[list[str]] = None,
|
|
45
|
+
env_ids: Optional[list[str]] = None,
|
|
46
|
+
sandbox_names: Optional[list[str]] = None,
|
|
47
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
48
|
+
fail_task_on_error: bool = False,
|
|
49
|
+
):
|
|
50
|
+
if fail_task_on_error:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"teardown_sandboxes is best-effort (a failed terminate is logged, never raised); "
|
|
53
|
+
"fail_task_on_error must be false"
|
|
54
|
+
)
|
|
55
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
56
|
+
self.agent_names = list(agent_names or [])
|
|
57
|
+
self.env_ids = list(env_ids or [])
|
|
58
|
+
self.sandbox_names = list(sandbox_names or [])
|
|
59
|
+
if not (self.agent_names or self.env_ids or self.sandbox_names):
|
|
60
|
+
raise ValueError("teardown_sandboxes needs at least one of agent_names, env_ids, sandbox_names")
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
base = super().to_dict()
|
|
64
|
+
base["agent_names"] = self.agent_names
|
|
65
|
+
base["env_ids"] = self.env_ids
|
|
66
|
+
base["sandbox_names"] = self.sandbox_names
|
|
67
|
+
return base
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: dict) -> TeardownSandboxesTaskStep:
|
|
71
|
+
base = cls._base_from_dict(data)
|
|
72
|
+
# Stay best-effort when unset: _base_from_dict would otherwise default it to True.
|
|
73
|
+
base["fail_task_on_error"] = data.get("fail_task_on_error", False)
|
|
74
|
+
return cls(
|
|
75
|
+
**base,
|
|
76
|
+
agent_names=data.get("agent_names"),
|
|
77
|
+
env_ids=data.get("env_ids"),
|
|
78
|
+
sandbox_names=data.get("sandbox_names"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
82
|
+
targets, missing = self._resolve(context)
|
|
83
|
+
if missing:
|
|
84
|
+
logger.warning(f"teardown_sandboxes: not deployed in this run, skipping: {', '.join(missing)}")
|
|
85
|
+
already = set(context.metadata.get(TORN_DOWN_KEY) or [])
|
|
86
|
+
pending = {sid: (stype, default) for sid, (stype, default) in targets.items() if sid not in already}
|
|
87
|
+
results = await asyncio.gather(
|
|
88
|
+
*(_terminate(sid, stype, default) for sid, (stype, default) in pending.items()),
|
|
89
|
+
return_exceptions=True,
|
|
90
|
+
)
|
|
91
|
+
terminated, failed = [], []
|
|
92
|
+
for sandbox_id, result in zip(pending, results):
|
|
93
|
+
if isinstance(result, BaseException):
|
|
94
|
+
failed.append(sandbox_id)
|
|
95
|
+
logger.warning(f"teardown_sandboxes: terminate {sandbox_id} failed: {result!r}"[:300])
|
|
96
|
+
else:
|
|
97
|
+
terminated.append(sandbox_id)
|
|
98
|
+
if terminated:
|
|
99
|
+
context.metadata[TORN_DOWN_KEY] = [*context.metadata.get(TORN_DOWN_KEY, []), *terminated]
|
|
100
|
+
logger.info(
|
|
101
|
+
f"teardown_sandboxes: terminated={len(terminated)} failed={len(failed)} "
|
|
102
|
+
f"already_torn_down={len(targets) - len(pending)} missing={len(missing)}"
|
|
103
|
+
)
|
|
104
|
+
return context
|
|
105
|
+
|
|
106
|
+
def _resolve(
|
|
107
|
+
self, context: TaskStepContext
|
|
108
|
+
) -> tuple[dict[str, tuple[Optional[str], str]], list[str]]:
|
|
109
|
+
"""``{sandbox_id: (sandbox_type, default_slot)}`` for every target, plus unmatched names."""
|
|
110
|
+
targets: dict[str, tuple[Optional[str], str]] = {}
|
|
111
|
+
missing: list[str] = []
|
|
112
|
+
for name in self.agent_names:
|
|
113
|
+
agents = [a for a in context.deployed_agents if a.agent_name == name]
|
|
114
|
+
if not agents:
|
|
115
|
+
missing.append(f"agent {name!r}")
|
|
116
|
+
for agent in agents:
|
|
117
|
+
if agent.sandbox_id:
|
|
118
|
+
targets[agent.sandbox_id] = (agent.sandbox_type, "agent")
|
|
119
|
+
for name in self.sandbox_names:
|
|
120
|
+
sandboxes = [s for s in context.deployed_sandboxes if s.sandbox_name == name]
|
|
121
|
+
if not sandboxes:
|
|
122
|
+
missing.append(f"sandbox {name!r}")
|
|
123
|
+
for sandbox in sandboxes:
|
|
124
|
+
targets[sandbox.sandbox_id] = (sandbox.sandbox_type, "sandbox")
|
|
125
|
+
for env_id in self.env_ids:
|
|
126
|
+
envs = [e for e in context.deployed_envs if e.env_id == env_id]
|
|
127
|
+
if not envs:
|
|
128
|
+
missing.append(f"env {env_id!r}")
|
|
129
|
+
for env in envs:
|
|
130
|
+
targets.update({sid: (stype, "env") for sid, stype in _env_sandbox_ids(env)})
|
|
131
|
+
return targets, missing
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _env_sandbox_ids(env) -> list[tuple[str, Optional[str]]]:
|
|
135
|
+
"""Every (sandbox_id, sandbox_type) of a deployed env: its primary sandbox plus ``sandbox_ids``.
|
|
136
|
+
|
|
137
|
+
``sandbox_ids`` values are an id or a ``{service: id}`` dict and inherit the env's type, unless
|
|
138
|
+
the key itself names a registered backend (a sandbox on another backend than the gateway's).
|
|
139
|
+
"""
|
|
140
|
+
if not isinstance(env, DeployedSandboxEnv): # an env outside our sandboxes owns its own lifetime
|
|
141
|
+
return []
|
|
142
|
+
ids = [(env.sandbox_id, env.sandbox_type)] if env.sandbox_id else []
|
|
143
|
+
for key, value in (env.sandbox_ids or {}).items():
|
|
144
|
+
entry_type = key if _is_backend(key) else env.sandbox_type
|
|
145
|
+
values = [value] if isinstance(value, str) else [v for v in value.values() if isinstance(v, str)]
|
|
146
|
+
ids.extend((v, entry_type) for v in values)
|
|
147
|
+
return ids
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_backend(name: str) -> bool:
|
|
151
|
+
try:
|
|
152
|
+
build_sandbox_provider(name)
|
|
153
|
+
except Exception:
|
|
154
|
+
return False
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_DEFAULT_PROVIDERS = {
|
|
159
|
+
"agent": get_agent_sandbox_provider,
|
|
160
|
+
"env": get_env_sandbox_provider,
|
|
161
|
+
"sandbox": get_sandbox_provider,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def _terminate(sandbox_id: str, sandbox_type: Optional[str], default_slot: str) -> None:
|
|
166
|
+
provider: SandboxProvider = (
|
|
167
|
+
build_sandbox_provider(sandbox_type) if sandbox_type else _DEFAULT_PROVIDERS[default_slot]()
|
|
168
|
+
)
|
|
169
|
+
sandbox = await provider.get_sandbox(sandbox_id)
|
|
170
|
+
await sandbox.terminate()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Verifier task steps: the steps that score a run and record a row under
|
|
2
|
+
``context.metadata["verifications"]``, plus the helpers only they use (score
|
|
3
|
+
aggregation, judge output formats, trajectory compaction and frame selection for
|
|
4
|
+
the LLM judge)."""
|
|
5
|
+
|
|
6
|
+
from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
|
|
7
|
+
from agent_env.task_step.task_steps.verifiers.env_outcome_verifier import EnvOutcomeVerifierTaskStep
|
|
8
|
+
from agent_env.task_step.task_steps.verifiers.rubrics_verifier import RubricsVerifierTaskStep
|
|
9
|
+
from agent_env.task_step.task_steps.verifiers.agent_prompt_response_verifier import AgentPromptResponseVerifierTaskStep
|
|
10
|
+
from agent_env.task_step.task_steps.verifiers.run_container_unit_tests_verifier import RunContainerUnitTestsVerifierTaskStep
|
|
11
|
+
from agent_env.task_step.task_steps.verifiers.verify_sandbox import VerifySandboxTaskStep
|
|
12
|
+
from agent_env.task_step.task_steps.verifiers.aggregate_verifiers import AggregateVerifiersTaskStep
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentPromptResponseVerifierTaskStep",
|
|
16
|
+
"AggregateVerifiersTaskStep",
|
|
17
|
+
"EnvOutcomeVerifierTaskStep",
|
|
18
|
+
"RubricsVerifierTaskStep",
|
|
19
|
+
"RunContainerUnitTestsVerifierTaskStep",
|
|
20
|
+
"ScoreAggregator",
|
|
21
|
+
"VerifySandboxTaskStep",
|
|
22
|
+
"aggregate_score",
|
|
23
|
+
]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Verify response-side rubric criteria against an agent's prompt response text.
|
|
2
|
+
|
|
3
|
+
Deterministic checks that operate on the agent's final text response — no
|
|
4
|
+
sandbox connection, no LLM judge:
|
|
5
|
+
- response_contains — every needle in `needles` appears in the response
|
|
6
|
+
- response_regex_present — `pattern` (regex) matches the response (re.search)
|
|
7
|
+
|
|
8
|
+
Strict: unknown criterion types and criteria missing their required field
|
|
9
|
+
(empty `needles` for response_contains, empty `pattern` for
|
|
10
|
+
response_regex_present) raise ValueError. A verifier that can't actually
|
|
11
|
+
verify anything is a misconfiguration, not a runtime decision.
|
|
12
|
+
|
|
13
|
+
Result rows merge `{**criterion, **outcome}` and aggregated score is written
|
|
14
|
+
to `context.metadata["verifications"][verifier_id]`, matching the shape used
|
|
15
|
+
by `VerifySandboxTaskStep` and `RubricsVerifierTaskStep`.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
import uuid
|
|
22
|
+
from typing import ClassVar, Optional
|
|
23
|
+
|
|
24
|
+
from agent_env.task_step.context import TaskStepContext
|
|
25
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
26
|
+
from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_HANDLED_TYPES = {
|
|
31
|
+
"response_contains",
|
|
32
|
+
"response_regex_present",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentPromptResponseVerifierTaskStep(TaskStep):
|
|
37
|
+
type: ClassVar[str] = "agent_prompt_response_verifier"
|
|
38
|
+
entity_refs = ()
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
id: str,
|
|
43
|
+
version: Optional[int],
|
|
44
|
+
prompt_id: str,
|
|
45
|
+
criteria: Optional[list[dict]] = None,
|
|
46
|
+
score_aggregator: Optional[ScoreAggregator] = None,
|
|
47
|
+
verifier_id: Optional[str] = None,
|
|
48
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
49
|
+
fail_task_on_error: bool = True,
|
|
50
|
+
):
|
|
51
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
52
|
+
self.prompt_id = prompt_id
|
|
53
|
+
self.criteria = list(criteria or [])
|
|
54
|
+
if isinstance(score_aggregator, str):
|
|
55
|
+
score_aggregator = ScoreAggregator(score_aggregator)
|
|
56
|
+
self.score_aggregator = score_aggregator or ScoreAggregator.ALL_PASS
|
|
57
|
+
self.verifier_id = verifier_id or uuid.uuid4().hex
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
base = super().to_dict()
|
|
61
|
+
base["prompt_id"] = self.prompt_id
|
|
62
|
+
base["criteria"] = self.criteria
|
|
63
|
+
base["score_aggregator"] = self.score_aggregator.value
|
|
64
|
+
base["verifier_id"] = self.verifier_id
|
|
65
|
+
return base
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: dict) -> "AgentPromptResponseVerifierTaskStep":
|
|
69
|
+
raw_agg = data.get("score_aggregator")
|
|
70
|
+
return cls(
|
|
71
|
+
**cls._base_from_dict(data),
|
|
72
|
+
prompt_id=data["prompt_id"],
|
|
73
|
+
criteria=data.get("criteria"),
|
|
74
|
+
score_aggregator=ScoreAggregator(raw_agg) if raw_agg else None,
|
|
75
|
+
verifier_id=data.get("verifier_id"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
79
|
+
prompt_response = next(
|
|
80
|
+
(pr for pr in context.prompt_responses if pr.prompt_id == self.prompt_id),
|
|
81
|
+
None,
|
|
82
|
+
)
|
|
83
|
+
if prompt_response is None:
|
|
84
|
+
raise RuntimeError(f"PromptResponse with prompt_id='{self.prompt_id}' not found in context")
|
|
85
|
+
|
|
86
|
+
if prompt_response.error_type:
|
|
87
|
+
logger.warning(
|
|
88
|
+
f"Skipping verification '{self.verifier_id}': prompt had error_type={prompt_response.error_type}"
|
|
89
|
+
)
|
|
90
|
+
context.metadata.setdefault("verifications", {})[self.verifier_id] = {
|
|
91
|
+
"results": [{
|
|
92
|
+
"id": "prompt_error",
|
|
93
|
+
"score": 0,
|
|
94
|
+
"result": False,
|
|
95
|
+
"message": f"Skipped: prompt had error_type={prompt_response.error_type}",
|
|
96
|
+
}],
|
|
97
|
+
"score": 0,
|
|
98
|
+
}
|
|
99
|
+
return context
|
|
100
|
+
|
|
101
|
+
response = prompt_response.response or ""
|
|
102
|
+
|
|
103
|
+
results: list[dict] = []
|
|
104
|
+
for idx, criterion in enumerate(self.criteria):
|
|
105
|
+
rtype = criterion.get("type")
|
|
106
|
+
if rtype not in _HANDLED_TYPES:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"criterion #{idx} has unknown type {rtype!r}; "
|
|
109
|
+
f"handled types: {sorted(_HANDLED_TYPES)}"
|
|
110
|
+
)
|
|
111
|
+
outcome = self._eval_criterion(idx, response, criterion)
|
|
112
|
+
results.append({
|
|
113
|
+
**criterion,
|
|
114
|
+
"criterion_index": idx,
|
|
115
|
+
"score": float(outcome["score"]),
|
|
116
|
+
"result": bool(outcome["passed"]),
|
|
117
|
+
"justification": outcome["justification"],
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
score = aggregate_score(results, self.score_aggregator)
|
|
121
|
+
|
|
122
|
+
context.metadata.setdefault("verifications", {})[self.verifier_id] = {
|
|
123
|
+
"results": results,
|
|
124
|
+
"score": score,
|
|
125
|
+
}
|
|
126
|
+
logger.info(
|
|
127
|
+
f"Verification '{self.verifier_id}': "
|
|
128
|
+
f"{len(results)} criteria evaluated, score={score}"
|
|
129
|
+
)
|
|
130
|
+
return context
|
|
131
|
+
|
|
132
|
+
def _eval_criterion(self, idx: int, response: str, criterion: dict) -> dict:
|
|
133
|
+
rtype = criterion["type"]
|
|
134
|
+
if rtype == "response_contains":
|
|
135
|
+
needles = criterion.get("needles") or []
|
|
136
|
+
if not needles:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"criterion #{idx} (response_contains) requires non-empty `needles`"
|
|
139
|
+
)
|
|
140
|
+
missing = [n for n in needles if n not in response]
|
|
141
|
+
passed = not missing
|
|
142
|
+
return {
|
|
143
|
+
"score": 1.0 if passed else 0.0,
|
|
144
|
+
"passed": passed,
|
|
145
|
+
"justification": "all needles present" if passed else f"missing: {missing}",
|
|
146
|
+
}
|
|
147
|
+
if rtype == "response_regex_present":
|
|
148
|
+
pattern = criterion.get("pattern")
|
|
149
|
+
if not pattern:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"criterion #{idx} (response_regex_present) requires non-empty `pattern`"
|
|
152
|
+
)
|
|
153
|
+
flags_raw = criterion.get("flags")
|
|
154
|
+
if flags_raw is None:
|
|
155
|
+
flags = re.DOTALL
|
|
156
|
+
else:
|
|
157
|
+
flags = 0
|
|
158
|
+
for name in flags_raw:
|
|
159
|
+
flags |= getattr(re, name)
|
|
160
|
+
passed = bool(re.search(pattern, response, flags))
|
|
161
|
+
return {
|
|
162
|
+
"score": 1.0 if passed else 0.0,
|
|
163
|
+
"passed": passed,
|
|
164
|
+
"justification": "pattern matched" if passed else f"no match for pattern {pattern!r}",
|
|
165
|
+
}
|
|
166
|
+
raise RuntimeError(f"unhandled criterion type {rtype}")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Aggregate result rows across multiple upstream verifier steps into one unified score.
|
|
2
|
+
|
|
3
|
+
Reads `context.metadata["verifications"][<vid>]["results"]` for each id in
|
|
4
|
+
`verifier_ids`, concatenates the rows, drops skipped ones, and runs
|
|
5
|
+
`aggregate_score` once on the union — same math as the upstream steps, but
|
|
6
|
+
applied across their combined criterion set.
|
|
7
|
+
|
|
8
|
+
Use this to match Harbor's unified-aggregation behavior when filesystem
|
|
9
|
+
criteria (verify_sandbox) and response criteria (rubrics_verifier) live in
|
|
10
|
+
separate steps.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import uuid
|
|
16
|
+
from typing import ClassVar, Optional
|
|
17
|
+
|
|
18
|
+
from agent_env.task_step.context import TaskStepContext
|
|
19
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
20
|
+
from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AggregateVerifiersTaskStep(TaskStep):
|
|
26
|
+
type: ClassVar[str] = "aggregate_verifiers"
|
|
27
|
+
entity_refs = ()
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
id: str,
|
|
32
|
+
version: Optional[int],
|
|
33
|
+
verifier_ids: Optional[list[str]] = None,
|
|
34
|
+
score_aggregator: Optional[ScoreAggregator] = None,
|
|
35
|
+
verifier_id: Optional[str] = None,
|
|
36
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
37
|
+
fail_task_on_error: bool = True,
|
|
38
|
+
):
|
|
39
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
40
|
+
self.verifier_ids = list(verifier_ids or [])
|
|
41
|
+
if isinstance(score_aggregator, str):
|
|
42
|
+
score_aggregator = ScoreAggregator(score_aggregator)
|
|
43
|
+
self.score_aggregator = score_aggregator or ScoreAggregator.WEIGHTED_AVERAGE
|
|
44
|
+
self.verifier_id = verifier_id or uuid.uuid4().hex
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
base = super().to_dict()
|
|
48
|
+
base["verifier_ids"] = self.verifier_ids
|
|
49
|
+
base["score_aggregator"] = self.score_aggregator.value
|
|
50
|
+
base["verifier_id"] = self.verifier_id
|
|
51
|
+
return base
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict) -> "AggregateVerifiersTaskStep":
|
|
55
|
+
raw_agg = data.get("score_aggregator")
|
|
56
|
+
return cls(
|
|
57
|
+
**cls._base_from_dict(data),
|
|
58
|
+
verifier_ids=data.get("verifier_ids"),
|
|
59
|
+
score_aggregator=ScoreAggregator(raw_agg) if raw_agg else None,
|
|
60
|
+
verifier_id=data.get("verifier_id"),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
64
|
+
all_verifications = context.metadata.get("verifications", {})
|
|
65
|
+
missing = [vid for vid in self.verifier_ids if vid not in all_verifications]
|
|
66
|
+
if missing:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"AggregateVerifiers: verifier_id(s) not found in "
|
|
69
|
+
f"context.metadata.verifications: {missing}. "
|
|
70
|
+
f"Available: {list(all_verifications.keys())}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
merged_results: list[dict] = []
|
|
74
|
+
for vid in self.verifier_ids:
|
|
75
|
+
merged_results.extend(all_verifications[vid].get("results", []))
|
|
76
|
+
|
|
77
|
+
non_skipped = [r for r in merged_results if not r.get("skipped")]
|
|
78
|
+
score = aggregate_score(non_skipped, self.score_aggregator)
|
|
79
|
+
|
|
80
|
+
if "verifications" not in context.metadata:
|
|
81
|
+
context.metadata["verifications"] = {}
|
|
82
|
+
context.metadata["verifications"][self.verifier_id] = {
|
|
83
|
+
"score": score,
|
|
84
|
+
"source_verifier_ids": list(self.verifier_ids),
|
|
85
|
+
}
|
|
86
|
+
logger.info(
|
|
87
|
+
f"AggregateVerifiers '{self.verifier_id}': merged {len(self.verifier_ids)} verifier(s), "
|
|
88
|
+
f"{len(non_skipped)}/{len(merged_results)} criteria evaluated, score={score}"
|
|
89
|
+
)
|
|
90
|
+
return context
|