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.
Files changed (312) hide show
  1. agent_env/__init__.py +0 -0
  2. agent_env/a2a_agent/__init__.py +23 -0
  3. agent_env/a2a_agent/a2a_agent.py +600 -0
  4. agent_env/a2a_agent/conversation_store.py +356 -0
  5. agent_env/a2a_agent/object_transfer.py +609 -0
  6. agent_env/a2a_agent/protocol.py +238 -0
  7. agent_env/a2a_agent/store.py +201 -0
  8. agent_env/a2a_agent/validator.py +901 -0
  9. agent_env/artifact/__init__.py +48 -0
  10. agent_env/artifact/artifact.py +74 -0
  11. agent_env/artifact/artifacts/__init__.py +0 -0
  12. agent_env/artifact/artifacts/cli.py +83 -0
  13. agent_env/artifact/artifacts/docker_image.py +457 -0
  14. agent_env/artifact/artifacts/environment.py +89 -0
  15. agent_env/artifact/artifacts/environment_universe.py +156 -0
  16. agent_env/artifact/artifacts/file.py +214 -0
  17. agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
  18. agent_env/artifact/artifacts/skill.py +206 -0
  19. agent_env/artifact/artifacts/vm_image.py +95 -0
  20. agent_env/artifact/ref.py +15 -0
  21. agent_env/artifact/registry.py +251 -0
  22. agent_env/artifact/store.py +295 -0
  23. agent_env/artifact/universe.py +38 -0
  24. agent_env/attribution.py +41 -0
  25. agent_env/bundle/__init__.py +4 -0
  26. agent_env/bundle/_fs.py +128 -0
  27. agent_env/bundle/authoring.py +239 -0
  28. agent_env/bundle/installed.py +175 -0
  29. agent_env/bundle/ledger.py +269 -0
  30. agent_env/bundle/materialize.py +204 -0
  31. agent_env/bundle/parse.py +594 -0
  32. agent_env/bundle/plan.py +414 -0
  33. agent_env/bundle/resolve.py +404 -0
  34. agent_env/bundle/run.py +361 -0
  35. agent_env/cli/__init__.py +69 -0
  36. agent_env/cli/__main__.py +10 -0
  37. agent_env/cli/_installers.py +576 -0
  38. agent_env/cli/_plugin_changes.py +947 -0
  39. agent_env/cli/a2a_agent/__init__.py +22 -0
  40. agent_env/cli/a2a_agent/add_skill.py +97 -0
  41. agent_env/cli/a2a_agent/deploy.py +60 -0
  42. agent_env/cli/a2a_agent/get.py +15 -0
  43. agent_env/cli/a2a_agent/get_instance.py +26 -0
  44. agent_env/cli/a2a_agent/put.py +78 -0
  45. agent_env/cli/a2a_agent/validate.py +56 -0
  46. agent_env/cli/artifact/__init__.py +20 -0
  47. agent_env/cli/artifact/cli.py +83 -0
  48. agent_env/cli/artifact/environment.py +44 -0
  49. agent_env/cli/artifact/environment_universe.py +163 -0
  50. agent_env/cli/artifact/file_artifact_universe.py +281 -0
  51. agent_env/cli/artifact/skill.py +128 -0
  52. agent_env/cli/banner.py +25 -0
  53. agent_env/cli/config.py +263 -0
  54. agent_env/cli/env/__init__.py +31 -0
  55. agent_env/cli/env/deploy.py +112 -0
  56. agent_env/cli/env/gateway.py +57 -0
  57. agent_env/cli/env/get_instance.py +40 -0
  58. agent_env/cli/env/mcp_server.py +410 -0
  59. agent_env/cli/env/multi.py +282 -0
  60. agent_env/cli/env/service_db.py +99 -0
  61. agent_env/cli/env/snapshot.py +35 -0
  62. agent_env/cli/env/state/__init__.py +6 -0
  63. agent_env/cli/env/state/init.py +108 -0
  64. agent_env/cli/env/state/teardown.py +44 -0
  65. agent_env/cli/env/website.py +216 -0
  66. agent_env/cli/env/website_browser.py +67 -0
  67. agent_env/cli/eval/__init__.py +16 -0
  68. agent_env/cli/eval/add_tasks.py +63 -0
  69. agent_env/cli/eval/create.py +49 -0
  70. agent_env/cli/eval/run.py +194 -0
  71. agent_env/cli/identity/__init__.py +5 -0
  72. agent_env/cli/identity/client_identity.py +37 -0
  73. agent_env/cli/plugin.py +438 -0
  74. agent_env/cli/run.py +285 -0
  75. agent_env/cli/task/__init__.py +21 -0
  76. agent_env/cli/task/create.py +109 -0
  77. agent_env/cli/task/get.py +17 -0
  78. agent_env/cli/task/get_instance.py +34 -0
  79. agent_env/cli/task/run.py +615 -0
  80. agent_env/cli/task/validate.py +31 -0
  81. agent_env/cli/up.py +123 -0
  82. agent_env/cli/utils.py +183 -0
  83. agent_env/config/__init__.py +50 -0
  84. agent_env/config/describe.py +1067 -0
  85. agent_env/config/errors.py +13 -0
  86. agent_env/config/loader.py +245 -0
  87. agent_env/config/model.py +154 -0
  88. agent_env/config/paths.py +25 -0
  89. agent_env/config/plugin_tables.py +101 -0
  90. agent_env/config/provenance.py +134 -0
  91. agent_env/config/runtime.py +1064 -0
  92. agent_env/config/snapshot.py +81 -0
  93. agent_env/entity_refs.py +186 -0
  94. agent_env/env/__init__.py +49 -0
  95. agent_env/env/env.py +433 -0
  96. agent_env/env/env_artifact_store.py +87 -0
  97. agent_env/env/envs/__init__.py +7 -0
  98. agent_env/env/envs/_deployment.py +281 -0
  99. agent_env/env/envs/gateway_server.py +32 -0
  100. agent_env/env/envs/mcp_server.py +387 -0
  101. agent_env/env/envs/multi_env.py +780 -0
  102. agent_env/env/envs/service_db/Dockerfile +20 -0
  103. agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
  104. agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
  105. agent_env/env/envs/service_db.py +92 -0
  106. agent_env/env/envs/website.py +294 -0
  107. agent_env/env/envs/website_browser/Dockerfile +40 -0
  108. agent_env/env/envs/website_browser/__init__.py +17 -0
  109. agent_env/env/envs/website_browser/entrypoint.sh +13 -0
  110. agent_env/env/gateway/Dockerfile +14 -0
  111. agent_env/env/gateway/__init__.py +37 -0
  112. agent_env/env/gateway/clock.py +184 -0
  113. agent_env/env/gateway/constants.py +190 -0
  114. agent_env/env/gateway/entrypoint.py +156 -0
  115. agent_env/env/gateway/gateway.py +1095 -0
  116. agent_env/env/gateway/get_time.py +60 -0
  117. agent_env/env/gateway/requirements.txt +4 -0
  118. agent_env/env/gateway/triggers.py +1193 -0
  119. agent_env/env/legacy_protocol.py +146 -0
  120. agent_env/env/registry.py +93 -0
  121. agent_env/env/snapshot_store.py +372 -0
  122. agent_env/env/store.py +411 -0
  123. agent_env/eval/__init__.py +14 -0
  124. agent_env/eval/eval.py +68 -0
  125. agent_env/eval/store.py +116 -0
  126. agent_env/examples/hello/README.md +9 -0
  127. agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
  128. agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
  129. agent_env/examples/hello/tasks/hello.json +10 -0
  130. agent_env/explorer/__init__.py +12 -0
  131. agent_env/explorer/app.py +277 -0
  132. agent_env/explorer/openapi_docs.py +242 -0
  133. agent_env/explorer/plugin.py +95 -0
  134. agent_env/explorer/routers/__init__.py +1 -0
  135. agent_env/explorer/routers/common.py +174 -0
  136. agent_env/explorer/routers/conversations.py +25 -0
  137. agent_env/explorer/routers/objects.py +112 -0
  138. agent_env/explorer/routers/runs.py +475 -0
  139. agent_env/plugins/__init__.py +59 -0
  140. agent_env/plugins/_cli.py +244 -0
  141. agent_env/plugins/_discovery.py +97 -0
  142. agent_env/plugins/_inventory.py +262 -0
  143. agent_env/plugins/_registration.py +268 -0
  144. agent_env/plugins/_report.py +47 -0
  145. agent_env/plugins/_requirements.py +55 -0
  146. agent_env/providers/__init__.py +37 -0
  147. agent_env/providers/env_providers/__init__.py +12 -0
  148. agent_env/providers/env_providers/constants.py +13 -0
  149. agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
  150. agent_env/providers/env_providers/env_provider.py +237 -0
  151. agent_env/providers/env_providers/env_server_provider.py +133 -0
  152. agent_env/providers/env_state/__init__.py +49 -0
  153. agent_env/providers/env_state/env_state_provider.py +554 -0
  154. agent_env/providers/env_state/local_postgres.py +390 -0
  155. agent_env/providers/env_state/store.py +112 -0
  156. agent_env/providers/sandbox_providers/__init__.py +29 -0
  157. agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
  158. agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
  159. agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
  160. agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
  161. agent_env/providers/sandbox_providers/e2b/template.py +158 -0
  162. agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
  163. agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
  164. agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
  165. agent_env/providers/sandbox_providers/sandbox.py +387 -0
  166. agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
  167. agent_env/py.typed +0 -0
  168. agent_env/runner/__init__.py +16 -0
  169. agent_env/runner/local_runner.py +190 -0
  170. agent_env/runner/runner.py +127 -0
  171. agent_env/runner/store.py +94 -0
  172. agent_env/store/__init__.py +126 -0
  173. agent_env/store/_google.py +68 -0
  174. agent_env/store/base.py +33 -0
  175. agent_env/store/document_store/__init__.py +60 -0
  176. agent_env/store/document_store/document_store.py +519 -0
  177. agent_env/store/document_store/dynamodb_document_store.py +306 -0
  178. agent_env/store/document_store/evaluation.py +199 -0
  179. agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
  180. agent_env/store/document_store/mongo_document_store.py +259 -0
  181. agent_env/store/document_store/sqlite_document_store.py +314 -0
  182. agent_env/store/ids.py +123 -0
  183. agent_env/store/image_store/__init__.py +34 -0
  184. agent_env/store/image_store/ecr_image_store.py +131 -0
  185. agent_env/store/image_store/google_credentials.py +113 -0
  186. agent_env/store/image_store/image_store.py +113 -0
  187. agent_env/store/image_store/local_registry_image_store.py +106 -0
  188. agent_env/store/image_store/oci_registry_credentials.py +196 -0
  189. agent_env/store/local_state.py +27 -0
  190. agent_env/store/object_store/__init__.py +24 -0
  191. agent_env/store/object_store/gcs_object_store.py +415 -0
  192. agent_env/store/object_store/local_object_store.py +143 -0
  193. agent_env/store/object_store/object_store.py +198 -0
  194. agent_env/store/object_store/s3_object_store.py +354 -0
  195. agent_env/store/query.py +112 -0
  196. agent_env/store/routing.py +609 -0
  197. agent_env/store/secret_store/__init__.py +19 -0
  198. agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
  199. agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
  200. agent_env/store/secret_store/local_secret_store.py +47 -0
  201. agent_env/store/secret_store/secret_store.py +19 -0
  202. agent_env/task/__init__.py +27 -0
  203. agent_env/task/interrupts.py +109 -0
  204. agent_env/task/registry.py +27 -0
  205. agent_env/task/step_journal.py +99 -0
  206. agent_env/task/store.py +940 -0
  207. agent_env/task/task.py +815 -0
  208. agent_env/task/teardown.py +142 -0
  209. agent_env/task_step/__init__.py +47 -0
  210. agent_env/task_step/context.py +195 -0
  211. agent_env/task_step/context_ops.py +224 -0
  212. agent_env/task_step/registry.py +182 -0
  213. agent_env/task_step/review_store.py +118 -0
  214. agent_env/task_step/snapshot_utils/__init__.py +2 -0
  215. agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
  216. agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
  217. agent_env/task_step/store.py +129 -0
  218. agent_env/task_step/task_step.py +173 -0
  219. agent_env/task_step/task_steps/__init__.py +27 -0
  220. agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
  221. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
  222. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
  223. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
  224. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
  225. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
  226. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
  227. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
  228. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
  229. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
  230. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
  231. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
  232. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
  233. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
  234. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
  235. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
  236. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
  237. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
  238. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
  239. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
  240. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
  241. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
  242. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
  243. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
  244. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
  245. agent_env/task_step/task_steps/add_skills.py +240 -0
  246. agent_env/task_step/task_steps/apply_server_config.py +288 -0
  247. agent_env/task_step/task_steps/collect_artifacts.py +940 -0
  248. agent_env/task_step/task_steps/deploy_agent.py +626 -0
  249. agent_env/task_step/task_steps/deploy_env.py +197 -0
  250. agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
  251. agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
  252. agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
  253. agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
  254. agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
  255. agent_env/task_step/task_steps/install_agent.py +376 -0
  256. agent_env/task_step/task_steps/load_artifact.py +775 -0
  257. agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
  258. agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
  259. agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
  260. agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
  261. agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
  262. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
  263. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
  264. agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
  265. agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
  266. agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
  267. agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
  268. agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
  269. agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
  270. agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
  271. agent_env/task_step/task_steps/peer_agents.py +92 -0
  272. agent_env/task_step/task_steps/prompt_agent.py +900 -0
  273. agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
  274. agent_env/task_step/task_steps/register_env_triggers.py +97 -0
  275. agent_env/task_step/task_steps/reset_env.py +71 -0
  276. agent_env/task_step/task_steps/review.py +152 -0
  277. agent_env/task_step/task_steps/run_code.py +461 -0
  278. agent_env/task_step/task_steps/run_code_runner.py +52 -0
  279. agent_env/task_step/task_steps/run_docker_container.py +380 -0
  280. agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
  281. agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
  282. agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
  283. agent_env/task_step/task_steps/snapshot_env.py +637 -0
  284. agent_env/task_step/task_steps/sync_env_clock.py +132 -0
  285. agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
  286. agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
  287. agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
  288. agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
  289. agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
  290. agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
  291. agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
  292. agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
  293. agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
  294. agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
  295. agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
  296. agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
  297. agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
  298. agent_env/task_step/thread_work.py +61 -0
  299. agent_env/utils/__init__.py +0 -0
  300. agent_env/utils/card_naming.py +108 -0
  301. agent_env/utils/deprecation.py +109 -0
  302. agent_env/utils/docker_build.py +29 -0
  303. agent_env/utils/exec_retry.py +77 -0
  304. agent_env/utils/litellm_attribution.py +59 -0
  305. agent_env/utils/paths.py +20 -0
  306. agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
  307. agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
  308. agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
  309. agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
  310. agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
  311. agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
  312. 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
+ )