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,609 @@
|
|
|
1
|
+
"""How agent-env and an A2A agent move objects: the form each extension call takes, the grants it
|
|
2
|
+
carries, the call itself, and the limits and time budget that bound it."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable, Collection, Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Literal, TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from agentenv_protocol.a2a_agent import (
|
|
14
|
+
BundleSkillRequest,
|
|
15
|
+
ChangelogIncrement,
|
|
16
|
+
ContextObjectTrajectoryRequest,
|
|
17
|
+
ContextTrajectoryRequest,
|
|
18
|
+
InlineSkillRequest,
|
|
19
|
+
NamespaceChangelogEnableRequest,
|
|
20
|
+
ObjectChangelogApplyRequest,
|
|
21
|
+
ObjectChangelogApplyResponse,
|
|
22
|
+
ObjectSnapshotLoadRequest,
|
|
23
|
+
ObjectSnapshotSaveRequest,
|
|
24
|
+
SkillBundle,
|
|
25
|
+
SkillBundleFile,
|
|
26
|
+
SnapshotReadObjects,
|
|
27
|
+
SnapshotWriteObjects,
|
|
28
|
+
TaskObjectTrajectoryRequest,
|
|
29
|
+
TaskTrajectoryRequest,
|
|
30
|
+
TrajectoryObjectsResponse,
|
|
31
|
+
TrajectoryWriteObjects,
|
|
32
|
+
card_request_accepts,
|
|
33
|
+
request_fields,
|
|
34
|
+
)
|
|
35
|
+
from agentenv_protocol.transfers import ReadObject, WriteNamespaceGrant, WriteObject
|
|
36
|
+
from pydantic import BaseModel, ValidationError
|
|
37
|
+
|
|
38
|
+
from agent_env.a2a_agent.protocol import raise_for_extension_status
|
|
39
|
+
from agent_env.store.base import GrantUnavailableError
|
|
40
|
+
from agent_env.store.object_store import DEFAULT_CONTENT_TYPE, ObjectStore
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
# "objects": the call carries grants and the agent moves the bytes. "legacy": every form that
|
|
45
|
+
# carries none, the inline ones and the older S3 ones.
|
|
46
|
+
TransferMode = Literal["objects", "legacy"]
|
|
47
|
+
_Response = TypeVar("_Response", bound=BaseModel)
|
|
48
|
+
_MEDIA_TYPE = re.compile(r"[!#$&^_.+\-|~0-9a-z]+/[!#$&^_.+\-|~0-9a-z]+")
|
|
49
|
+
_INCREMENT_NAME = re.compile(r"^(?P<sequence>[0-9]{6})(?:\.[A-Za-z0-9][A-Za-z0-9._-]*)?$")
|
|
50
|
+
|
|
51
|
+
# The time budget of one transfer, outermost first: a grant outlives agent-env's wait for the
|
|
52
|
+
# agent's answer, which outlasts the SDK's retries of stalled connections
|
|
53
|
+
# (agentenv_protocol.transfers.TRANSFER_STALL_BUDGET_SECONDS).
|
|
54
|
+
GRANT_LIFETIME_SECONDS = 3600
|
|
55
|
+
TRANSFER_TIMEOUT_SECONDS = 600 # a call during which the agent moves objects
|
|
56
|
+
REPLY_TIMEOUT_SECONDS = 120 # a call during which it moves none
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ObjectLimits:
|
|
61
|
+
max_objects: int
|
|
62
|
+
max_object_bytes: int
|
|
63
|
+
max_total_bytes: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_GIB = 1024 * 1024 * 1024
|
|
67
|
+
DEFAULT_TRAJECTORY_MAX_BYTES = _GIB
|
|
68
|
+
DEFAULT_SNAPSHOT_TRAJECTORY_MAX_BYTES = _GIB
|
|
69
|
+
DEFAULT_SNAPSHOT_WORKSPACE_MAX_BYTES = 5 * _GIB
|
|
70
|
+
SKILL_BUNDLE_LIMITS = ObjectLimits(max_objects=1_000, max_object_bytes=_GIB, max_total_bytes=_GIB)
|
|
71
|
+
CHANGELOG_LIMITS = ObjectLimits(
|
|
72
|
+
max_objects=10_000, max_object_bytes=_GIB, max_total_bytes=10 * _GIB
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
SNAPSHOT_TRAJECTORY_OBJECT_NAME = "trajectory"
|
|
76
|
+
SNAPSHOT_WORKSPACE_OBJECT_NAME = "workspace"
|
|
77
|
+
# A portable snapshot's objects, each opaque bytes, and the most each may hold.
|
|
78
|
+
_SNAPSHOT_OBJECTS = {
|
|
79
|
+
SNAPSHOT_TRAJECTORY_OBJECT_NAME: DEFAULT_SNAPSHOT_TRAJECTORY_MAX_BYTES,
|
|
80
|
+
SNAPSHOT_WORKSPACE_OBJECT_NAME: DEFAULT_SNAPSHOT_WORKSPACE_MAX_BYTES,
|
|
81
|
+
}
|
|
82
|
+
_OPAQUE = "application/octet-stream"
|
|
83
|
+
|
|
84
|
+
# The S3 forms the SDK retired; older and hand-written Agent Cards still take them.
|
|
85
|
+
_S3_PREFIX = ("s3_prefix",)
|
|
86
|
+
_SKILL_S3_URL = ("name", "description", "skill_s3_url")
|
|
87
|
+
_SNAPSHOT_SAVE_S3_PREFIX = ("context_id", "s3_prefix")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _fields(model: type[BaseModel]) -> tuple[str, ...]:
|
|
91
|
+
return request_fields(model).required
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_portable_snapshot(file_names: Collection[str]) -> bool:
|
|
95
|
+
"""Whether a snapshot was captured through the object form, which names its trajectory
|
|
96
|
+
object ``trajectory``; an older capture holds the runtime's own files."""
|
|
97
|
+
return SNAPSHOT_TRAJECTORY_OBJECT_NAME in file_names
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def choose_transfer(
|
|
101
|
+
method: Mapping[str, Any] | None,
|
|
102
|
+
*,
|
|
103
|
+
objects: Collection[str] | None = None,
|
|
104
|
+
legacy: Collection[str] | None = None,
|
|
105
|
+
store: ObjectStore,
|
|
106
|
+
) -> TransferMode | None:
|
|
107
|
+
"""How one extension call moves its objects, given the fields each form it can take sends.
|
|
108
|
+
|
|
109
|
+
Objects need a store that issues grants. A method without a declared request predates
|
|
110
|
+
variant negotiation and takes the legacy form. None: the agent takes neither form.
|
|
111
|
+
"""
|
|
112
|
+
if objects is not None and _accepts(method, objects) and store.supports_transfer_grants:
|
|
113
|
+
return "objects"
|
|
114
|
+
if legacy is not None and (
|
|
115
|
+
method is None or "request" not in method or _accepts(method, legacy)
|
|
116
|
+
):
|
|
117
|
+
return "legacy"
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _accepts(method: Mapping[str, Any] | None, fields: Collection[str]) -> bool:
|
|
122
|
+
request = method.get("request") if method is not None else None
|
|
123
|
+
return isinstance(request, Mapping) and card_request_accepts(request, fields)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class TransferCall:
|
|
128
|
+
"""One extension call ready to send: the form it takes and its body."""
|
|
129
|
+
|
|
130
|
+
mode: TransferMode
|
|
131
|
+
payload: dict[str, Any]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def invoke_transfer(
|
|
135
|
+
url: str,
|
|
136
|
+
call: TransferCall,
|
|
137
|
+
*,
|
|
138
|
+
verb: Literal["POST", "PUT"],
|
|
139
|
+
operation: str,
|
|
140
|
+
timeout: float,
|
|
141
|
+
response_model: type[_Response] | None = None,
|
|
142
|
+
) -> Any:
|
|
143
|
+
"""Send ``call`` and return the agent's answer, validated against ``response_model`` when
|
|
144
|
+
the call carried grants. An error never quotes the answer to such a call, which may echo a
|
|
145
|
+
grant."""
|
|
146
|
+
async with httpx.AsyncClient() as client:
|
|
147
|
+
send = client.post if verb == "POST" else client.put
|
|
148
|
+
resp = await send(url, json=call.payload, timeout=timeout)
|
|
149
|
+
raise_for_extension_status(resp, operation=operation, include_body=call.mode == "legacy")
|
|
150
|
+
body = resp.json()
|
|
151
|
+
if call.mode == "objects" and response_model is not None:
|
|
152
|
+
return parse_response(response_model, body, operation=operation)
|
|
153
|
+
return body
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_response(model: type[_Response], body: Any, *, operation: str) -> _Response:
|
|
157
|
+
"""Validate an agent's ``operation`` response, ignoring fields this release does not know."""
|
|
158
|
+
try:
|
|
159
|
+
return model.model_validate(body, extra="ignore")
|
|
160
|
+
except ValidationError as exc:
|
|
161
|
+
raise RuntimeError(f"{operation} returned an invalid object response") from exc
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def write_object(store: ObjectStore, url: str, *, media_type: str, max_bytes: int) -> WriteObject:
|
|
165
|
+
"""A write grant for one object, promising no more than one upload to the store can create."""
|
|
166
|
+
if store.max_single_upload_bytes is not None:
|
|
167
|
+
max_bytes = min(max_bytes, store.max_single_upload_bytes)
|
|
168
|
+
return WriteObject(
|
|
169
|
+
media_type=media_type,
|
|
170
|
+
max_bytes=max_bytes,
|
|
171
|
+
write=store.issue_write_grant(
|
|
172
|
+
url, media_type=media_type, max_bytes=max_bytes, expires_in=GRANT_LIFETIME_SECONDS
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def read_object(
|
|
178
|
+
store: ObjectStore, url: str, *, media_type: str | None = None, max_bytes: int | None = None
|
|
179
|
+
) -> ReadObject:
|
|
180
|
+
"""A read grant for the stored object at ``url``, bounded by its size unless ``max_bytes``
|
|
181
|
+
is given. ``media_type`` defaults to the stored content type."""
|
|
182
|
+
metadata = store.get_object_metadata_at(url)
|
|
183
|
+
if metadata is None or metadata.size is None:
|
|
184
|
+
raise ValueError(f"object metadata is unavailable for {url}")
|
|
185
|
+
stored_type = (metadata.content_type or "").partition(";")[0].strip().lower()
|
|
186
|
+
if not _MEDIA_TYPE.fullmatch(stored_type):
|
|
187
|
+
stored_type = DEFAULT_CONTENT_TYPE
|
|
188
|
+
return ReadObject(
|
|
189
|
+
media_type=media_type or stored_type,
|
|
190
|
+
max_bytes=max_bytes or max(metadata.size, 1),
|
|
191
|
+
size_bytes=metadata.size,
|
|
192
|
+
read=store.issue_read_grant(url, expires_in=GRANT_LIFETIME_SECONDS),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def read_objects_under(
|
|
197
|
+
store: ObjectStore,
|
|
198
|
+
prefix: str,
|
|
199
|
+
*,
|
|
200
|
+
limits: ObjectLimits,
|
|
201
|
+
media_type: str | None = None,
|
|
202
|
+
select: Callable[[str], bool] | None = None,
|
|
203
|
+
) -> list[tuple[str, ReadObject]]:
|
|
204
|
+
"""Read grants for the objects below ``prefix`` that ``select`` keeps, keyed and ordered by
|
|
205
|
+
their path relative to it. The limits count stored bytes, as a namespace uploader does."""
|
|
206
|
+
prefix = prefix.rstrip("/") + "/"
|
|
207
|
+
prefix_key = store.get_object_key(prefix).rstrip("/") + "/"
|
|
208
|
+
listed = sorted(
|
|
209
|
+
(store.get_object_key(url)[len(prefix_key):], url) for url in store.list_at(prefix)
|
|
210
|
+
)
|
|
211
|
+
if select is not None:
|
|
212
|
+
listed = [(path, url) for path, url in listed if select(path)]
|
|
213
|
+
if len(listed) > limits.max_objects:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"{len(listed)} objects under {prefix}; the limit is {limits.max_objects}"
|
|
216
|
+
)
|
|
217
|
+
described: list[tuple[str, ReadObject]] = []
|
|
218
|
+
total_bytes = 0
|
|
219
|
+
for path, url in listed:
|
|
220
|
+
descriptor = read_object(store, url, media_type=media_type)
|
|
221
|
+
if descriptor.size_bytes > limits.max_object_bytes:
|
|
222
|
+
raise ValueError(f"{url} exceeds the {limits.max_object_bytes}-byte object limit")
|
|
223
|
+
total_bytes += descriptor.size_bytes
|
|
224
|
+
if total_bytes > limits.max_total_bytes:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"objects under {prefix} exceed the {limits.max_total_bytes}-byte limit"
|
|
227
|
+
)
|
|
228
|
+
described.append((path, descriptor))
|
|
229
|
+
return described
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def namespace_grant(
|
|
233
|
+
store: ObjectStore, namespace_url: str, *, limits: ObjectLimits, expires_in: int
|
|
234
|
+
) -> WriteNamespaceGrant:
|
|
235
|
+
"""A grant for uploads below ``namespace_url``: the store signs the upload policy, and the
|
|
236
|
+
uploader holds to the object count and total size."""
|
|
237
|
+
policy = store.issue_upload_policy(
|
|
238
|
+
namespace_url.rstrip("/") + "/",
|
|
239
|
+
max_object_bytes=limits.max_object_bytes,
|
|
240
|
+
expires_in=expires_in,
|
|
241
|
+
)
|
|
242
|
+
return WriteNamespaceGrant(
|
|
243
|
+
root_path=store.get_object_key(namespace_url).rstrip("/"),
|
|
244
|
+
expires_at=policy.expires_at,
|
|
245
|
+
max_objects=limits.max_objects,
|
|
246
|
+
max_object_bytes=limits.max_object_bytes,
|
|
247
|
+
max_total_bytes=limits.max_total_bytes,
|
|
248
|
+
write=policy.write,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def bounded_echo(issued: str, echoed: str) -> str:
|
|
253
|
+
"""Where a legacy agent says it wrote, clamped to the prefix agent-env issued.
|
|
254
|
+
|
|
255
|
+
Sidecars uploading with their own client nest under it and echo that; registering the
|
|
256
|
+
issued prefix instead points the artifact a level too high, which `put_existing`'s
|
|
257
|
+
recursive list hides until a restore cannot find its files.
|
|
258
|
+
|
|
259
|
+
Escaping is still refused — `put_existing` registers everything under what it is handed,
|
|
260
|
+
with the worker's credentials. Nesting is free to allow: the presigned POST's
|
|
261
|
+
`starts-with $key` already admits any depth under the issued prefix.
|
|
262
|
+
"""
|
|
263
|
+
issued_norm, echoed_norm = issued.rstrip("/"), echoed.rstrip("/")
|
|
264
|
+
if echoed_norm == issued_norm:
|
|
265
|
+
return issued
|
|
266
|
+
if echoed_norm.startswith(issued_norm + "/"):
|
|
267
|
+
return echoed
|
|
268
|
+
logger.warning(
|
|
269
|
+
"the agent echoed a prefix outside the one issued; registering the "
|
|
270
|
+
"issued one (issued=%s returned=%s)", issued, echoed,
|
|
271
|
+
)
|
|
272
|
+
return issued
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def skill_bundle_request(
|
|
276
|
+
store: ObjectStore, *, name: str, description: str, object_url: str
|
|
277
|
+
) -> BundleSkillRequest:
|
|
278
|
+
"""A portable skill bundle of every object under ``object_url``."""
|
|
279
|
+
described = read_objects_under(store, object_url, limits=SKILL_BUNDLE_LIMITS)
|
|
280
|
+
if not described:
|
|
281
|
+
raise ValueError(f"no objects under {object_url} to send as a skill bundle")
|
|
282
|
+
files = [SkillBundleFile(path=path, object=descriptor) for path, descriptor in described]
|
|
283
|
+
return BundleSkillRequest(
|
|
284
|
+
name=name,
|
|
285
|
+
description=description,
|
|
286
|
+
skill_bundle=SkillBundle(
|
|
287
|
+
max_total_bytes=sum(file.object.max_bytes for file in files), files=files
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def skill_add_call(
|
|
293
|
+
method: Mapping[str, Any] | None,
|
|
294
|
+
store: ObjectStore,
|
|
295
|
+
*,
|
|
296
|
+
name: str,
|
|
297
|
+
description: str,
|
|
298
|
+
skill_md: str | None = None,
|
|
299
|
+
object_url: str | None = None,
|
|
300
|
+
forms: Collection[TransferMode] = ("objects", "legacy"),
|
|
301
|
+
) -> TransferCall:
|
|
302
|
+
"""The skill ``add`` call for a skill given as SKILL.md text or as the objects under
|
|
303
|
+
``object_url``, in a form the agent and the store allow; ``forms`` narrows the choice."""
|
|
304
|
+
base = {"name": name, "description": description}
|
|
305
|
+
if object_url is not None:
|
|
306
|
+
mode = choose_transfer(
|
|
307
|
+
method,
|
|
308
|
+
objects=_fields(BundleSkillRequest) if "objects" in forms else None,
|
|
309
|
+
legacy=_SKILL_S3_URL if "legacy" in forms else None,
|
|
310
|
+
store=store,
|
|
311
|
+
)
|
|
312
|
+
if mode is None:
|
|
313
|
+
raise RuntimeError(
|
|
314
|
+
"Agent advertises no object-backed skill form this object store can serve"
|
|
315
|
+
)
|
|
316
|
+
if mode == "legacy":
|
|
317
|
+
return TransferCall("legacy", {**base, "skill_s3_url": object_url})
|
|
318
|
+
request = skill_bundle_request(
|
|
319
|
+
store, name=name, description=description, object_url=object_url
|
|
320
|
+
)
|
|
321
|
+
return TransferCall("objects", request.model_dump(mode="json"))
|
|
322
|
+
if skill_md is not None:
|
|
323
|
+
if choose_transfer(method, legacy=_fields(InlineSkillRequest), store=store) is None:
|
|
324
|
+
raise RuntimeError("Agent does not advertise the inline skill variant")
|
|
325
|
+
return TransferCall("legacy", {**base, "skill_md": skill_md})
|
|
326
|
+
return TransferCall("legacy", base)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def snapshot_save_call(
|
|
330
|
+
method: Mapping[str, Any] | None,
|
|
331
|
+
store: ObjectStore,
|
|
332
|
+
*,
|
|
333
|
+
agent_name: str,
|
|
334
|
+
context_id: str,
|
|
335
|
+
capture_prefix: str,
|
|
336
|
+
) -> TransferCall:
|
|
337
|
+
"""The snapshot ``save`` call that writes one capture below ``capture_prefix``."""
|
|
338
|
+
mode = choose_transfer(
|
|
339
|
+
method,
|
|
340
|
+
objects=_fields(ObjectSnapshotSaveRequest),
|
|
341
|
+
legacy=_SNAPSHOT_SAVE_S3_PREFIX,
|
|
342
|
+
store=store,
|
|
343
|
+
)
|
|
344
|
+
if mode is None:
|
|
345
|
+
raise RuntimeError(
|
|
346
|
+
f"Agent '{agent_name}' advertises no snapshot save form this object store can serve"
|
|
347
|
+
)
|
|
348
|
+
if mode == "legacy":
|
|
349
|
+
# Signed with the step's fresh credentials: the sidecar's are the deployer's STS
|
|
350
|
+
# session, frozen at deploy and expired on a long run.
|
|
351
|
+
presigned_post = store.signed_post(capture_prefix)
|
|
352
|
+
return TransferCall(
|
|
353
|
+
"legacy",
|
|
354
|
+
{
|
|
355
|
+
"context_id": context_id,
|
|
356
|
+
"s3_prefix": capture_prefix,
|
|
357
|
+
# Absent, not null, when the backend cannot sign: the sidecar then uploads
|
|
358
|
+
# with its own client.
|
|
359
|
+
**({"presigned_post": presigned_post} if presigned_post else {}),
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
objects = {
|
|
363
|
+
name: write_object(store, url, media_type=_OPAQUE, max_bytes=max_bytes)
|
|
364
|
+
for name, url, max_bytes in _snapshot_objects(store, capture_prefix)
|
|
365
|
+
}
|
|
366
|
+
request = ObjectSnapshotSaveRequest(
|
|
367
|
+
context_id=context_id, objects=SnapshotWriteObjects(**objects)
|
|
368
|
+
)
|
|
369
|
+
return TransferCall("objects", request.model_dump(mode="json"))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def snapshot_load_call(
|
|
373
|
+
method: Mapping[str, Any] | None,
|
|
374
|
+
store: ObjectStore,
|
|
375
|
+
*,
|
|
376
|
+
agent_name: str,
|
|
377
|
+
bundle_url: str,
|
|
378
|
+
file_names: Collection[str],
|
|
379
|
+
target_context_id: str | None,
|
|
380
|
+
) -> TransferCall:
|
|
381
|
+
"""The snapshot ``load`` call that restores the capture at ``bundle_url`` in the form it was
|
|
382
|
+
captured in. An older capture is sent as ``bundle_url`` itself, whatever form it is stored in."""
|
|
383
|
+
portable = is_portable_snapshot(file_names)
|
|
384
|
+
mode = (
|
|
385
|
+
choose_transfer(method, objects=_fields(ObjectSnapshotLoadRequest), store=store)
|
|
386
|
+
if portable
|
|
387
|
+
else choose_transfer(method, legacy=_S3_PREFIX, store=store)
|
|
388
|
+
)
|
|
389
|
+
if mode is None:
|
|
390
|
+
raise RuntimeError(
|
|
391
|
+
f"Agent '{agent_name}' cannot load the configured "
|
|
392
|
+
f"{'portable' if portable else 'legacy'} snapshot"
|
|
393
|
+
)
|
|
394
|
+
if mode == "legacy":
|
|
395
|
+
payload: dict[str, Any] = {"s3_prefix": bundle_url}
|
|
396
|
+
if target_context_id:
|
|
397
|
+
payload["target_context_id"] = target_context_id
|
|
398
|
+
return TransferCall("legacy", payload)
|
|
399
|
+
objects = {
|
|
400
|
+
name: read_object(store, url, media_type=_OPAQUE, max_bytes=max_bytes)
|
|
401
|
+
for name, url, max_bytes in _snapshot_objects(store, bundle_url)
|
|
402
|
+
if name in file_names
|
|
403
|
+
}
|
|
404
|
+
request = ObjectSnapshotLoadRequest(
|
|
405
|
+
objects=SnapshotReadObjects(**objects), target_context_id=target_context_id
|
|
406
|
+
)
|
|
407
|
+
return TransferCall("objects", request.model_dump(mode="json", exclude_none=True))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _snapshot_objects(store: ObjectStore, prefix_url: str) -> list[tuple[str, str, int]]:
|
|
411
|
+
prefix_key = store.get_object_key(prefix_url).rstrip("/") + "/"
|
|
412
|
+
return [
|
|
413
|
+
(name, store.object_url(prefix_key + name), max_bytes)
|
|
414
|
+
for name, max_bytes in _SNAPSHOT_OBJECTS.items()
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def changelog_enable_call(
|
|
419
|
+
method: Mapping[str, Any] | None,
|
|
420
|
+
store: ObjectStore,
|
|
421
|
+
*,
|
|
422
|
+
agent_name: str,
|
|
423
|
+
namespace_url: str,
|
|
424
|
+
expires_in: int,
|
|
425
|
+
) -> TransferCall:
|
|
426
|
+
"""The ``enable-changelog`` call that captures below ``namespace_url``. When the store cannot
|
|
427
|
+
sign a namespace grant that lasts ``expires_in``, an agent that also takes ``s3_prefix``
|
|
428
|
+
gets that form; one that does not fails with GrantUnavailableError."""
|
|
429
|
+
mode = choose_transfer(
|
|
430
|
+
method, objects=_fields(NamespaceChangelogEnableRequest), legacy=_S3_PREFIX, store=store
|
|
431
|
+
)
|
|
432
|
+
if mode is None:
|
|
433
|
+
raise RuntimeError(
|
|
434
|
+
f"Agent '{agent_name}' advertises no changelog enable form this object store can serve"
|
|
435
|
+
)
|
|
436
|
+
if mode == "objects":
|
|
437
|
+
try:
|
|
438
|
+
grant = namespace_grant(
|
|
439
|
+
store, namespace_url, limits=CHANGELOG_LIMITS, expires_in=expires_in
|
|
440
|
+
)
|
|
441
|
+
except GrantUnavailableError as exc:
|
|
442
|
+
if choose_transfer(method, legacy=_S3_PREFIX, store=store) is None:
|
|
443
|
+
raise GrantUnavailableError(
|
|
444
|
+
f"Agent '{agent_name}' takes only the object changelog enable form, and "
|
|
445
|
+
f"the object store cannot issue its namespace grant: {exc}"
|
|
446
|
+
) from exc
|
|
447
|
+
logger.warning(
|
|
448
|
+
"Portable changelog grant is unavailable for agent '%s' (%s); "
|
|
449
|
+
"using its legacy s3_prefix variant",
|
|
450
|
+
agent_name,
|
|
451
|
+
exc,
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
request = NamespaceChangelogEnableRequest(write_namespace=grant)
|
|
455
|
+
return TransferCall("objects", request.model_dump(mode="json", exclude_none=True))
|
|
456
|
+
return TransferCall("legacy", {"s3_prefix": namespace_url})
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def changelog_apply_call(
|
|
460
|
+
method: Mapping[str, Any] | None,
|
|
461
|
+
store: ObjectStore,
|
|
462
|
+
*,
|
|
463
|
+
agent_name: str,
|
|
464
|
+
source_url: str,
|
|
465
|
+
portable: bool,
|
|
466
|
+
up_to_tool_call_exclusive: int | None = None,
|
|
467
|
+
resume_conversation: bool = False,
|
|
468
|
+
target_context_id: str | None = None,
|
|
469
|
+
) -> TransferCall:
|
|
470
|
+
"""The ``apply-changelog`` call that replays the capture at ``source_url`` in the form it was
|
|
471
|
+
captured in: a portable one sends read grants for its increments before the cutoff, in
|
|
472
|
+
sequence order, and none when the cutoff precedes the first tool call."""
|
|
473
|
+
mode = (
|
|
474
|
+
choose_transfer(method, objects=_fields(ObjectChangelogApplyRequest), store=store)
|
|
475
|
+
if portable
|
|
476
|
+
else choose_transfer(method, legacy=_S3_PREFIX, store=store)
|
|
477
|
+
)
|
|
478
|
+
if mode is None:
|
|
479
|
+
raise RuntimeError(
|
|
480
|
+
f"Agent '{agent_name}' cannot apply the configured "
|
|
481
|
+
f"{'portable' if portable else 'legacy'} changelog"
|
|
482
|
+
)
|
|
483
|
+
if mode == "legacy":
|
|
484
|
+
payload: dict[str, Any] = {"s3_prefix": source_url}
|
|
485
|
+
if resume_conversation:
|
|
486
|
+
payload["resume_conversation"] = True
|
|
487
|
+
if up_to_tool_call_exclusive is not None:
|
|
488
|
+
payload["up_to_tool_call_exclusive"] = up_to_tool_call_exclusive
|
|
489
|
+
if target_context_id:
|
|
490
|
+
payload["target_context_id"] = target_context_id
|
|
491
|
+
return TransferCall("legacy", payload)
|
|
492
|
+
cutoff = math.inf if up_to_tool_call_exclusive is None else up_to_tool_call_exclusive
|
|
493
|
+
described = read_objects_under(
|
|
494
|
+
store,
|
|
495
|
+
source_url,
|
|
496
|
+
limits=CHANGELOG_LIMITS,
|
|
497
|
+
media_type=_OPAQUE,
|
|
498
|
+
select=lambda path: _increment_sequence(path) < cutoff,
|
|
499
|
+
)
|
|
500
|
+
request = ObjectChangelogApplyRequest(
|
|
501
|
+
increments=[
|
|
502
|
+
ChangelogIncrement(sequence=_increment_sequence(path), object=descriptor)
|
|
503
|
+
for path, descriptor in described
|
|
504
|
+
],
|
|
505
|
+
resume_conversation=resume_conversation,
|
|
506
|
+
target_context_id=target_context_id,
|
|
507
|
+
)
|
|
508
|
+
return TransferCall("objects", request.model_dump(mode="json", exclude_none=True))
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def check_changelog_applied(
|
|
512
|
+
result: ObjectChangelogApplyResponse, call: TransferCall, *, agent_name: str
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Refuse an ``apply-changelog`` answer that does not account for every increment sent."""
|
|
515
|
+
sent = len(call.payload["increments"])
|
|
516
|
+
if result.count != sent:
|
|
517
|
+
raise RuntimeError(
|
|
518
|
+
f"changelog apply on agent '{agent_name}' reported {result.count} increments "
|
|
519
|
+
f"applied; {sent} were sent"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _increment_sequence(path: str) -> int:
|
|
524
|
+
match = _INCREMENT_NAME.fullmatch(path)
|
|
525
|
+
if match is None:
|
|
526
|
+
raise ValueError("portable changelog objects must use zero-padded sequence names")
|
|
527
|
+
return int(match["sequence"])
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
_TRAJECTORY_FORMS: dict[str, tuple[type[BaseModel], type[BaseModel]]] = {
|
|
531
|
+
"task_id": (TaskObjectTrajectoryRequest, TaskTrajectoryRequest),
|
|
532
|
+
"context_id": (ContextObjectTrajectoryRequest, ContextTrajectoryRequest),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def trajectory_mode(
|
|
537
|
+
method: Mapping[str, Any] | None,
|
|
538
|
+
store: ObjectStore,
|
|
539
|
+
*,
|
|
540
|
+
by: Literal["task_id", "context_id"],
|
|
541
|
+
) -> TransferMode | None:
|
|
542
|
+
"""How a trajectory ``get`` selecting by ``by`` moves the trajectory: uploaded by the agent
|
|
543
|
+
through a grant, or returned inline."""
|
|
544
|
+
objects_model, inline_model = _TRAJECTORY_FORMS[by]
|
|
545
|
+
return choose_transfer(
|
|
546
|
+
method, objects=_fields(objects_model), legacy=_fields(inline_model), store=store
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@dataclass(frozen=True)
|
|
551
|
+
class TrajectoryUpload:
|
|
552
|
+
"""Where the agent uploads a trajectory, and the grant it uploads with."""
|
|
553
|
+
|
|
554
|
+
object_url: str
|
|
555
|
+
write: WriteObject
|
|
556
|
+
|
|
557
|
+
@classmethod
|
|
558
|
+
def to(cls, store: ObjectStore, object_url: str) -> TrajectoryUpload:
|
|
559
|
+
write = write_object(
|
|
560
|
+
store,
|
|
561
|
+
object_url,
|
|
562
|
+
media_type="application/json",
|
|
563
|
+
max_bytes=DEFAULT_TRAJECTORY_MAX_BYTES,
|
|
564
|
+
)
|
|
565
|
+
return cls(object_url, write)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@dataclass(frozen=True)
|
|
569
|
+
class FetchedTrajectory:
|
|
570
|
+
"""Where a trajectory ``get`` left the trajectory: uploaded by the agent through the grant
|
|
571
|
+
(``object_url``), below a prefix a legacy agent uploaded to itself (``legacy_prefix``), or in
|
|
572
|
+
the answer (``inline``). All None: the agent returned none."""
|
|
573
|
+
|
|
574
|
+
object_url: str | None = None
|
|
575
|
+
legacy_prefix: str | None = None
|
|
576
|
+
inline: Any = None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
async def fetch_trajectory(
|
|
580
|
+
endpoint: str,
|
|
581
|
+
selector: Mapping[str, str],
|
|
582
|
+
*,
|
|
583
|
+
upload: TrajectoryUpload | None = None,
|
|
584
|
+
timeout: float | None = None,
|
|
585
|
+
) -> FetchedTrajectory:
|
|
586
|
+
"""POST one trajectory ``get``. With ``upload`` the agent uploads the trajectory through its
|
|
587
|
+
grant and the answer is checked. The default wait covers that upload."""
|
|
588
|
+
payload: dict[str, Any] = dict(selector)
|
|
589
|
+
if upload is not None:
|
|
590
|
+
objects = TrajectoryWriteObjects(trajectory=upload.write)
|
|
591
|
+
payload["objects"] = objects.model_dump(mode="json")
|
|
592
|
+
call = TransferCall("objects" if upload is not None else "legacy", payload)
|
|
593
|
+
if timeout is None:
|
|
594
|
+
timeout = TRANSFER_TIMEOUT_SECONDS if upload is not None else REPLY_TIMEOUT_SECONDS
|
|
595
|
+
body = await invoke_transfer(
|
|
596
|
+
endpoint,
|
|
597
|
+
call,
|
|
598
|
+
verb="POST",
|
|
599
|
+
operation="trajectory get",
|
|
600
|
+
timeout=timeout,
|
|
601
|
+
response_model=TrajectoryObjectsResponse,
|
|
602
|
+
)
|
|
603
|
+
if upload is not None:
|
|
604
|
+
return FetchedTrajectory(object_url=upload.object_url)
|
|
605
|
+
if not isinstance(body, Mapping):
|
|
606
|
+
return FetchedTrajectory()
|
|
607
|
+
return FetchedTrajectory(
|
|
608
|
+
legacy_prefix=body.get("trajectory_s3_prefix") or None, inline=body.get("trajectory")
|
|
609
|
+
)
|