agentengine-sdk-python 0.8.0__py3-none-any.whl → 0.8.2__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 (494) hide show
  1. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
  2. agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
  3. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +2 -0
  5. ksadk/a2a/_space_client_events.py +381 -0
  6. ksadk/a2a/bootstrap.py +0 -2
  7. ksadk/a2a/card.py +6 -111
  8. ksadk/a2a/control_plane.py +239 -1
  9. ksadk/a2a/event_adapter.py +156 -64
  10. ksadk/a2a/executor.py +195 -172
  11. ksadk/a2a/routes.py +12 -5
  12. ksadk/a2a/server.py +1 -8
  13. ksadk/a2a/service_env.py +33 -0
  14. ksadk/a2a/space_client.py +43 -349
  15. ksadk/a2a_card.py +101 -0
  16. ksadk/a2ui/core.py +222 -53
  17. ksadk/agui/_agent_helpers.py +243 -0
  18. ksadk/agui/a2ui_projection.py +12 -5
  19. ksadk/agui/agent.py +474 -275
  20. ksadk/agui/config.py +11 -5
  21. ksadk/agui/routes.py +7 -15
  22. ksadk/api/client.py +530 -40
  23. ksadk/api/environment.py +27 -0
  24. ksadk/builders/code_builder.py +352 -53
  25. ksadk/builders/container_builder.py +71 -44
  26. ksadk/builders/framework_requirements.py +2 -1
  27. ksadk/builders/ks3_uploader.py +2 -2
  28. ksadk/builders/managed_runtime_builder.py +60 -28
  29. ksadk/cli/__init__.py +25 -0
  30. ksadk/cli/cmd_a2a.py +18 -31
  31. ksadk/cli/cmd_create.py +13 -26
  32. ksadk/cli/cmd_dashboard.py +15 -1
  33. ksadk/cli/cmd_deploy.py +28 -3
  34. ksadk/cli/cmd_eval.py +443 -0
  35. ksadk/cli/cmd_evalset.py +284 -0
  36. ksadk/cli/cmd_files.py +5 -1
  37. ksadk/cli/cmd_hermes.py +137 -163
  38. ksadk/cli/cmd_invoke.py +28 -38
  39. ksadk/cli/cmd_launch.py +12 -0
  40. ksadk/cli/cmd_managed_runtime.py +95 -0
  41. ksadk/cli/cmd_observe.py +85 -0
  42. ksadk/cli/cmd_openclaw.py +85 -479
  43. ksadk/cli/cmd_replay.py +28 -4
  44. ksadk/cli/cmd_run.py +29 -67
  45. ksadk/cli/cmd_studio.py +201 -0
  46. ksadk/cli/cmd_web.py +15 -6
  47. ksadk/cli/dry_run.py +2 -1
  48. ksadk/cli/env_options.py +83 -0
  49. ksadk/cli/global_options.py +3 -1
  50. ksadk/cli/hermes_env.py +151 -0
  51. ksadk/cli/invoke_payload.py +49 -0
  52. ksadk/cli/network_options.py +7 -5
  53. ksadk/cli/openclaw_env.py +458 -0
  54. ksadk/cli/runtime_bootstrap.py +51 -0
  55. ksadk/cli/ui.py +6 -0
  56. ksadk/cli/workflow_common.py +5 -5
  57. ksadk/codex/__init__.py +3 -3
  58. ksadk/codex/client.py +833 -49
  59. ksadk/codex/phase.py +1 -1
  60. ksadk/codex/runtime.py +720 -127
  61. ksadk/common/kop_client.py +232 -0
  62. ksadk/configs/__init__.py +1 -3
  63. ksadk/configs/env_registry.py +244 -45
  64. ksadk/configs/env_registry_pcm.py +181 -0
  65. ksadk/configs/env_var_spec.py +18 -0
  66. ksadk/configs/global_config.py +6 -1
  67. ksadk/configs/settings.py +5 -108
  68. ksadk/context_engine/__init__.py +98 -0
  69. ksadk/context_engine/assembler.py +176 -0
  70. ksadk/context_engine/baseline.py +419 -0
  71. ksadk/context_engine/cache_observability.py +228 -0
  72. ksadk/context_engine/capabilities.py +443 -0
  73. ksadk/context_engine/contributors.py +344 -0
  74. ksadk/context_engine/hosted_pipeline.py +417 -0
  75. ksadk/context_engine/models.py +117 -0
  76. ksadk/context_engine/planner.py +698 -0
  77. ksadk/context_engine/policies.py +319 -0
  78. ksadk/context_engine/projection.py +31 -0
  79. ksadk/context_engine/shadow_plan.py +266 -0
  80. ksadk/context_engine/tokenizer.py +164 -0
  81. ksadk/conversations/__init__.py +85 -94
  82. ksadk/conversations/context.py +126 -6
  83. ksadk/conversations/message_projection.py +292 -28
  84. ksadk/conversations/model_context.py +49 -3
  85. ksadk/conversations/runtime_compaction.py +207 -5
  86. ksadk/conversations/runtime_input.py +193 -3
  87. ksadk/conversations/runtime_invocation.py +81 -3
  88. ksadk/conversations/runtime_observability.py +157 -4
  89. ksadk/conversations/runtime_payloads.py +42 -0
  90. ksadk/conversations/runtime_preparation.py +280 -2
  91. ksadk/conversations/runtime_resume.py +71 -32
  92. ksadk/conversations/runtime_stream_events.py +94 -9
  93. ksadk/conversations/runtime_streaming.py +175 -22
  94. ksadk/conversations/semantic_summary.py +311 -1
  95. ksadk/conversations/session_lock.py +51 -0
  96. ksadk/deployment/env_forward.py +72 -0
  97. ksadk/deployment/managed_runtime.py +38 -0
  98. ksadk/deployment/manager.py +24 -8
  99. ksadk/deployment/providers/serverless.py +228 -92
  100. ksadk/evaluation/__init__.py +116 -0
  101. ksadk/evaluation/a2a_adapter.py +452 -0
  102. ksadk/evaluation/adapters.py +67 -0
  103. ksadk/evaluation/agent_eval_client.py +395 -0
  104. ksadk/evaluation/cloud_binding.py +80 -0
  105. ksadk/evaluation/cloud_converter.py +194 -0
  106. ksadk/evaluation/cloud_service.py +199 -0
  107. ksadk/evaluation/contracts.py +478 -0
  108. ksadk/evaluation/evalset.py +351 -0
  109. ksadk/evaluation/evaluators.py +627 -0
  110. ksadk/evaluation/evidence.py +258 -0
  111. ksadk/evaluation/executor.py +143 -0
  112. ksadk/evaluation/local_adapter.py +550 -0
  113. ksadk/evaluation/service_env.py +27 -0
  114. ksadk/evaluation/storage.py +68 -0
  115. ksadk/evaluation/studio_build_adapter.py +253 -0
  116. ksadk/evaluation/target.py +82 -0
  117. ksadk/events/__init__.py +13 -13
  118. ksadk/events/_v1_compat/__init__.py +1 -0
  119. ksadk/events/_v1_compat/models.py +299 -0
  120. ksadk/events/_v1_compat/parser.py +247 -0
  121. ksadk/events/_v1_compat/projection.py +753 -0
  122. ksadk/events/adapters/__init__.py +34 -0
  123. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  124. ksadk/events/adapters/_a2a_support.py +275 -0
  125. ksadk/events/adapters/_codex_interactions.py +375 -0
  126. ksadk/events/adapters/_codex_items.py +801 -0
  127. ksadk/events/adapters/_codex_validators.py +311 -0
  128. ksadk/events/adapters/_langgraph_support.py +785 -0
  129. ksadk/events/adapters/a2a.py +841 -0
  130. ksadk/events/adapters/adk.py +503 -0
  131. ksadk/events/adapters/codex.py +717 -0
  132. ksadk/events/adapters/langgraph.py +745 -0
  133. ksadk/events/canonical.py +440 -0
  134. ksadk/events/canonical_replay.py +497 -0
  135. ksadk/events/canonical_store.py +468 -0
  136. ksadk/events/cold_recovery.py +257 -0
  137. ksadk/events/content.py +89 -0
  138. ksadk/events/identity.py +86 -0
  139. ksadk/events/pipeline.py +455 -0
  140. ksadk/events/projections.py +66 -0
  141. ksadk/events/reducer.py +661 -0
  142. ksadk/events/replay.py +3 -36
  143. ksadk/events/runtime_event.py +20 -256
  144. ksadk/events/session_event.py +324 -0
  145. ksadk/events/store.py +3 -316
  146. ksadk/events/v1_compat.py +41 -0
  147. ksadk/harness/__init__.py +3 -2
  148. ksadk/harness/app.py +114 -123
  149. ksadk/harness/config.py +1 -1
  150. ksadk/harness/runtime.py +467 -0
  151. ksadk/hermes_terminal.py +5 -0
  152. ksadk/identity/resolver.py +4 -3
  153. ksadk/interaction/__init__.py +28 -0
  154. ksadk/interaction/contracts.py +136 -0
  155. ksadk/interaction/ledger.py +189 -0
  156. ksadk/interaction/provider.py +122 -0
  157. ksadk/interaction/providers/__init__.py +52 -0
  158. ksadk/interaction/providers/adk.py +40 -0
  159. ksadk/interaction/providers/codex.py +94 -0
  160. ksadk/interaction/providers/langgraph.py +58 -0
  161. ksadk/kernel/__init__.py +117 -0
  162. ksadk/kernel/authorization.py +258 -0
  163. ksadk/kernel/bootstrap.py +1074 -0
  164. ksadk/kernel/contract_fingerprints.py +61 -0
  165. ksadk/kernel/contracts.py +362 -0
  166. ksadk/kernel/control.py +219 -0
  167. ksadk/kernel/errors.py +79 -0
  168. ksadk/kernel/ingress.py +1102 -0
  169. ksadk/kernel/mapping.py +111 -0
  170. ksadk/kernel/memory_store.py +1133 -0
  171. ksadk/kernel/postgres_store.py +1756 -0
  172. ksadk/kernel/recovery.py +452 -0
  173. ksadk/kernel/runtime_identity.py +103 -0
  174. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  175. ksadk/kernel/sqlite_store.py +1410 -0
  176. ksadk/kernel/state.py +145 -0
  177. ksadk/kernel/store.py +232 -0
  178. ksadk/kernel/worker.py +953 -0
  179. ksadk/knowledge_base/client.py +22 -4
  180. ksadk/knowledge_base/service.py +32 -5
  181. ksadk/managed_a2a_card.py +160 -0
  182. ksadk/managed_runtime.py +16 -9
  183. ksadk/memory/__init__.py +61 -0
  184. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  185. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  186. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  187. ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
  188. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  189. ksadk/memory/adk/long_term_memory.py +3 -2
  190. ksadk/memory/coordinator.py +461 -0
  191. ksadk/memory/events.py +202 -0
  192. ksadk/memory/extraction.py +228 -0
  193. ksadk/memory/ltm_backend_factory.py +16 -3
  194. ksadk/memory/models.py +316 -0
  195. ksadk/memory/policy.py +215 -0
  196. ksadk/memory/provider.py +47 -0
  197. ksadk/memory/provider_adapter.py +159 -0
  198. ksadk/memory/provider_resolver.py +71 -0
  199. ksadk/memory/providers/__init__.py +5 -0
  200. ksadk/memory/providers/local_sqlite.py +490 -0
  201. ksadk/memory/resolved_policy.py +145 -0
  202. ksadk/memory/service.py +198 -8
  203. ksadk/memory/tool.py +4 -0
  204. ksadk/model_proxy/bootstrap.py +16 -9
  205. ksadk/model_proxy/config.py +22 -1
  206. ksadk/model_proxy/detect.py +202 -9
  207. ksadk/model_proxy/namespace.py +67 -15
  208. ksadk/model_proxy/server.py +168 -6
  209. ksadk/model_proxy/transform.py +105 -3
  210. ksadk/observability/__init__.py +25 -0
  211. ksadk/observability/session_log.py +420 -0
  212. ksadk/observability/trajectory.py +105 -0
  213. ksadk/prompts/__init__.py +71 -0
  214. ksadk/prompts/compiler.py +215 -0
  215. ksadk/prompts/models.py +82 -0
  216. ksadk/prompts/projection.py +89 -0
  217. ksadk/prompts/resolved.py +137 -0
  218. ksadk/prompts/sources.py +233 -0
  219. ksadk/runners/_langgraph_runner_streams.py +822 -0
  220. ksadk/runners/adk_runner.py +14 -31
  221. ksadk/runners/base_runner.py +44 -3
  222. ksadk/runners/factory.py +3 -3
  223. ksadk/runners/langgraph_runner.py +252 -433
  224. ksadk/runners/utils/__init__.py +0 -10
  225. ksadk/runners/utils/loader.py +16 -0
  226. ksadk/runtime/__init__.py +67 -41
  227. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  228. ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
  229. ksadk/runtime/adapter.py +199 -16
  230. ksadk/runtime/conversation_execution.py +687 -0
  231. ksadk/runtime/executor.py +392 -0
  232. ksadk/runtime/factory.py +342 -0
  233. ksadk/runtime/framework_adapters.py +6 -12
  234. ksadk/runtime/hosted_finalizer.py +268 -0
  235. ksadk/runtime/launch.py +61 -0
  236. ksadk/runtime/preprocessing.py +49 -14
  237. ksadk/runtime/runner_adapter.py +330 -391
  238. ksadk/runtime/runner_loading.py +36 -0
  239. ksadk/runtime/usage.py +33 -0
  240. ksadk/sandbox/backends/e2b.py +36 -3
  241. ksadk/server/__init__.py +39 -4
  242. ksadk/server/app.py +18 -492
  243. ksadk/server/composition.py +36 -2
  244. ksadk/server/factory.py +210 -149
  245. ksadk/server/routes/common.py +15 -46
  246. ksadk/server/routes/control.py +13 -9
  247. ksadk/server/routes/kernel_ingress.py +258 -0
  248. ksadk/server/routes/misc.py +10 -33
  249. ksadk/server/routes/models.py +14 -6
  250. ksadk/server/routes/openai_compat.py +129 -27
  251. ksadk/server/routes/projection.py +84 -16
  252. ksadk/server/routes/run.py +347 -460
  253. ksadk/server/routes/sessions.py +37 -40
  254. ksadk/server/routes/streaming.py +52 -7
  255. ksadk/server/routes/workspace.py +54 -21
  256. ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
  257. ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
  258. ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
  259. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  260. ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
  261. ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
  262. ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
  263. ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
  264. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
  265. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
  266. ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
  267. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
  268. ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
  269. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
  270. ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
  271. ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
  272. ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
  273. ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
  274. ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
  275. ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
  276. ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
  277. ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
  278. ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
  279. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
  280. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
  281. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
  282. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
  283. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
  284. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  285. ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
  286. ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
  287. ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
  288. ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
  289. ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
  290. ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
  291. ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
  292. ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
  293. ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
  294. ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
  295. ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
  296. ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
  297. ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
  298. ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
  299. ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
  300. ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
  301. ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
  302. ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
  303. ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
  304. ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
  305. ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
  306. ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
  307. ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
  308. ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
  309. ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
  310. ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
  311. ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
  312. ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
  313. ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
  314. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
  315. ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
  316. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
  317. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
  318. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
  319. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
  320. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
  321. ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
  322. ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
  323. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
  324. ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
  325. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
  326. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
  327. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
  328. ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
  329. ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
  330. ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
  331. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
  332. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
  333. ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
  334. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
  335. ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
  336. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
  337. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
  338. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
  339. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
  340. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
  341. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
  342. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
  343. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
  344. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
  345. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
  346. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
  347. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
  348. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
  349. ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
  350. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
  351. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
  352. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
  353. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
  354. ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
  355. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
  356. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
  357. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
  358. ksadk/server/static/index.html +2 -2
  359. ksadk/sessions/__init__.py +26 -9
  360. ksadk/sessions/_local_service_sync.py +688 -0
  361. ksadk/sessions/_local_tables.py +21 -0
  362. ksadk/sessions/_postgres_schema.py +305 -0
  363. ksadk/sessions/_postgres_tables.py +15 -0
  364. ksadk/sessions/base.py +80 -1
  365. ksadk/sessions/in_memory.py +53 -2
  366. ksadk/sessions/local_service.py +63 -604
  367. ksadk/sessions/postgres_service.py +58 -148
  368. ksadk/sessions/resilient.py +35 -1
  369. ksadk/skills/runtime/backends/e2b.py +1 -1
  370. ksadk/skills/service_client.py +13 -3
  371. ksadk/studio/__init__.py +5 -0
  372. ksadk/studio/agent_avatar_assets.py +120 -0
  373. ksadk/studio/agent_lifecycle.py +51 -0
  374. ksadk/studio/api.py +1598 -0
  375. ksadk/studio/api_catalog_routes.py +324 -0
  376. ksadk/studio/api_contracts.py +286 -0
  377. ksadk/studio/api_helpers.py +132 -0
  378. ksadk/studio/api_memory_routes.py +82 -0
  379. ksadk/studio/authoring.py +749 -0
  380. ksadk/studio/authoring_coordinator.py +1017 -0
  381. ksadk/studio/builder.py +397 -0
  382. ksadk/studio/capabilities.py +368 -0
  383. ksadk/studio/cloud.py +2099 -0
  384. ksadk/studio/codex_agent_service.py +694 -0
  385. ksadk/studio/codex_authoring.py +469 -0
  386. ksadk/studio/codex_builder.py +437 -0
  387. ksadk/studio/codex_manifest.py +307 -0
  388. ksadk/studio/codex_run.py +395 -0
  389. ksadk/studio/compiler.py +190 -0
  390. ksadk/studio/contracts.py +755 -0
  391. ksadk/studio/errors.py +47 -0
  392. ksadk/studio/event_store.py +214 -0
  393. ksadk/studio/framework_run.py +342 -0
  394. ksadk/studio/hosted_kernel.py +317 -0
  395. ksadk/studio/identifiers.py +33 -0
  396. ksadk/studio/manifest_resolver.py +131 -0
  397. ksadk/studio/mcp_runtime.py +229 -0
  398. ksadk/studio/model_client.py +629 -0
  399. ksadk/studio/model_profile_service.py +214 -0
  400. ksadk/studio/operations.py +275 -0
  401. ksadk/studio/otel_trace.py +1060 -0
  402. ksadk/studio/pagination.py +182 -0
  403. ksadk/studio/pcm_memory.py +85 -0
  404. ksadk/studio/python_tool_inspection.py +183 -0
  405. ksadk/studio/repository.py +327 -0
  406. ksadk/studio/resource_catalog.py +1260 -0
  407. ksadk/studio/run_service.py +1133 -0
  408. ksadk/studio/runtime_catalog.py +74 -0
  409. ksadk/studio/runtime_source.py +365 -0
  410. ksadk/studio/service.py +2249 -0
  411. ksadk/studio/shared_web.py +1004 -0
  412. ksadk/studio/skill_discovery.py +628 -0
  413. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  414. ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
  415. ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
  416. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  417. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  418. ksadk/studio/static/index.html +15 -0
  419. ksadk/studio/templates.py +722 -0
  420. ksadk/studio/validator.py +265 -0
  421. ksadk/studio/workspace.py +135 -0
  422. ksadk/tools/gateway.py +9 -0
  423. ksadk/toolsets/__init__.py +27 -1
  424. ksadk/toolsets/a2a.py +426 -0
  425. ksadk/tracing/__init__.py +19 -21
  426. ksadk/tracing/setup.py +317 -403
  427. ksadk/tracing/span_utils.py +0 -44
  428. ksadk/version.py +1 -1
  429. ksadk_runtime_common/memory_backend/registry.py +56 -56
  430. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  431. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  432. ksadk_runtime_common/workspace_files/router.py +8 -2
  433. agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
  434. ksadk/a2a/card_builder.py +0 -64
  435. ksadk/a2a/client.py +0 -309
  436. ksadk/events/parser.py +0 -191
  437. ksadk/harness/runner.py +0 -211
  438. ksadk/runners/codex_runner.py +0 -132
  439. ksadk/runners/utils/langfuse.py +0 -230
  440. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  441. ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
  442. ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
  443. ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
  444. ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
  445. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
  446. ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
  447. ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
  448. ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
  449. ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
  450. ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
  451. ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
  452. ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
  453. ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
  454. ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
  455. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
  456. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  457. ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
  458. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  459. ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
  460. ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
  461. ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
  462. ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
  463. ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
  464. ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
  465. ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
  466. ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
  467. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
  468. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
  469. ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
  470. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
  471. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  472. ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
  473. ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
  474. ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
  475. ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
  476. ksadk/server/static/assets/katex-DATssjp1.js +0 -265
  477. ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
  478. ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
  479. ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
  480. ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
  481. ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
  482. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
  483. ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
  484. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
  485. ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
  486. ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
  487. ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
  488. ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
  489. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
  490. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
  491. ksadk/tracing/exporters/langfuse_exporter.py +0 -460
  492. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
  493. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
  494. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
ksadk/studio/cloud.py ADDED
@@ -0,0 +1,2099 @@
1
+ """Studio Bundle upload and existing Agent lifecycle gateway contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from collections.abc import AsyncIterator
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Protocol, cast
14
+ from uuid import uuid4
15
+
16
+ from pydantic import ValidationError
17
+
18
+ from ksadk.api import AgentEngineAPIError, AgentEngineClient
19
+ from ksadk.builders.ks3_uploader import KS3Uploader
20
+ from ksadk.studio.contracts import (
21
+ BuildRecord,
22
+ BuildStatus,
23
+ DeploymentRecord,
24
+ DeploymentRequest,
25
+ )
26
+ from ksadk.studio.errors import StudioError
27
+ from ksadk.studio.hosted_kernel import preflight_hosted_kernel_bundle
28
+ from ksadk.studio.repository import BuildRepository
29
+ from ksadk.studio.workspace import Workspace
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ _STUDIO_CODE_COMMAND = (
34
+ "ksadk",
35
+ "web",
36
+ "/app/code/runtime",
37
+ "--port",
38
+ "8080",
39
+ "--host",
40
+ "0.0.0.0",
41
+ "--no-open",
42
+ )
43
+ # A Dashboard access link is both the user/session credential and the Agent
44
+ # binding. Keep the hosted surface in that same link instead of opening the
45
+ # Agent image's legacy `/chat` static bundle after authentication.
46
+ _HOSTED_AGENT_UI_PATH = "/hosted-ui/chat"
47
+ _NATIVE_DASHBOARD_UI_PATH = "/"
48
+ _NATIVE_DASHBOARD_FRAMEWORKS = frozenset({"hermes", "openclaw"})
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class AccountCloudAgentReference:
53
+ agent_id: str
54
+
55
+
56
+ class CloudDeploymentGateway(Protocol):
57
+ async def upload_bundle(
58
+ self,
59
+ *,
60
+ bundle: bytes,
61
+ bundle_digest: str,
62
+ provenance: dict[str, Any],
63
+ ) -> str: ...
64
+
65
+ async def create_version(
66
+ self,
67
+ *,
68
+ agent_id: str,
69
+ bundle_uri: str,
70
+ bundle_digest: str,
71
+ provenance: dict[str, Any],
72
+ ) -> str: ...
73
+
74
+ async def create_deployment(
75
+ self,
76
+ *,
77
+ build_id: str,
78
+ version_id: str,
79
+ bundle_digest: str,
80
+ request: DeploymentRequest,
81
+ ) -> DeploymentRecord: ...
82
+
83
+ async def replace_deployment(
84
+ self,
85
+ deployment: DeploymentRecord,
86
+ *,
87
+ build_id: str,
88
+ version_id: str,
89
+ bundle_digest: str,
90
+ request: DeploymentRequest,
91
+ ) -> DeploymentRecord: ...
92
+
93
+ async def get_deployment_status(self, deployment: DeploymentRecord) -> DeploymentRecord: ...
94
+
95
+ async def get_deployment_dashboard_access(
96
+ self, deployment: DeploymentRecord
97
+ ) -> dict[str, str | None]: ...
98
+
99
+ async def delete_deployment(self, deployment: DeploymentRecord) -> bool: ...
100
+
101
+ async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]: ...
102
+
103
+ async def get_account_agent(self, agent_id: str) -> dict[str, Any]: ...
104
+
105
+ async def list_account_agent_versions(
106
+ self, agent_id: str, *, page: int, size: int
107
+ ) -> dict[str, Any]: ...
108
+
109
+ async def rollback_account_agent_version(
110
+ self, agent_id: str, *, version_id: str
111
+ ) -> dict[str, Any]: ...
112
+
113
+ async def get_account_agent_dashboard_access(
114
+ self, agent_id: str
115
+ ) -> dict[str, str | None]: ...
116
+
117
+ async def delete_account_agent(self, agent_id: str) -> bool: ...
118
+
119
+ async def create_managed_runtime_deployment(
120
+ self,
121
+ *,
122
+ build_id: str,
123
+ agent_name: str,
124
+ manifest: str,
125
+ runtime_name: str,
126
+ runtime_version: str,
127
+ manifest_digest: str,
128
+ request: DeploymentRequest,
129
+ ) -> DeploymentRecord: ...
130
+
131
+ async def replace_managed_runtime_deployment(
132
+ self,
133
+ deployment: DeploymentRecord,
134
+ *,
135
+ build_id: str,
136
+ manifest: str,
137
+ runtime_name: str,
138
+ runtime_version: str,
139
+ manifest_digest: str,
140
+ request: DeploymentRequest,
141
+ ) -> DeploymentRecord: ...
142
+
143
+
144
+ class UnavailableCloudGateway:
145
+ async def upload_bundle(self, **_kwargs) -> str:
146
+ raise StudioError(
147
+ "CLOUD_BUNDLE_DEPLOYMENT_UNAVAILABLE",
148
+ "当前未配置可用的云端签名账号,不能上传 Bundle",
149
+ status_code=501,
150
+ )
151
+
152
+ async def create_version(self, **_kwargs) -> str:
153
+ raise AssertionError("upload_bundle must fail first")
154
+
155
+ async def create_deployment(self, **_kwargs) -> DeploymentRecord:
156
+ raise AssertionError("upload_bundle must fail first")
157
+
158
+ async def replace_deployment(
159
+ self,
160
+ _deployment: DeploymentRecord,
161
+ **_kwargs,
162
+ ) -> DeploymentRecord:
163
+ raise AssertionError("upload_bundle must fail first")
164
+
165
+ async def create_managed_runtime_deployment(self, **_kwargs) -> DeploymentRecord:
166
+ raise StudioError(
167
+ "CLOUD_MANAGED_RUNTIME_DEPLOYMENT_UNAVAILABLE",
168
+ "当前未配置可用的云端签名账号,不能部署声明式 Agent",
169
+ status_code=501,
170
+ )
171
+
172
+ async def replace_managed_runtime_deployment(
173
+ self, _deployment: DeploymentRecord, **_kwargs
174
+ ) -> DeploymentRecord:
175
+ raise AssertionError("managed runtime deployment must fail first")
176
+
177
+ async def get_deployment_dashboard_access(
178
+ self, _deployment: DeploymentRecord
179
+ ) -> dict[str, str | None]:
180
+ raise StudioError(
181
+ "CLOUD_DASHBOARD_UNAVAILABLE",
182
+ "当前未配置可用的云端签名账号,不能打开云端 Agent UI",
183
+ status_code=501,
184
+ )
185
+
186
+ async def delete_deployment(self, _deployment: DeploymentRecord) -> bool:
187
+ raise StudioError(
188
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
189
+ "当前未配置可用的云端签名账号,不能删除云端 Agent",
190
+ status_code=501,
191
+ )
192
+
193
+ async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]:
194
+ # Local-only Studio remains usable without cloud credentials. Detail,
195
+ # chat and mutation still fail closed when explicitly requested.
196
+ return {
197
+ "items": [],
198
+ "total": 0,
199
+ "page": page,
200
+ "size": size,
201
+ "available": False,
202
+ }
203
+
204
+ async def get_account_agent(self, _agent_id: str) -> dict[str, Any]:
205
+ raise StudioError(
206
+ "CLOUD_AGENT_DIRECTORY_UNAVAILABLE",
207
+ "当前未配置可用的云端签名账号,不能读取云端 Agent",
208
+ status_code=501,
209
+ )
210
+
211
+ async def list_account_agent_versions(
212
+ self, _agent_id: str, *, page: int, size: int
213
+ ) -> dict[str, Any]:
214
+ raise StudioError(
215
+ "CLOUD_AGENT_VERSION_DIRECTORY_UNAVAILABLE",
216
+ "当前未配置可用的云端签名账号,不能读取云端版本",
217
+ status_code=501,
218
+ )
219
+
220
+ async def rollback_account_agent_version(
221
+ self, _agent_id: str, *, version_id: str
222
+ ) -> dict[str, Any]:
223
+ raise StudioError(
224
+ "CLOUD_AGENT_VERSION_ROLLBACK_UNAVAILABLE",
225
+ "当前未配置可用的云端签名账号,不能回滚云端版本",
226
+ status_code=501,
227
+ )
228
+
229
+ async def get_account_agent_dashboard_access(
230
+ self, _agent_id: str
231
+ ) -> dict[str, str | None]:
232
+ raise StudioError(
233
+ "CLOUD_DASHBOARD_UNAVAILABLE",
234
+ "当前未配置可用的云端签名账号,不能打开云端 Agent UI",
235
+ status_code=501,
236
+ )
237
+
238
+ async def delete_account_agent(self, _agent_id: str) -> bool:
239
+ raise StudioError(
240
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
241
+ "当前未配置可用的云端签名账号,不能删除云端 Agent",
242
+ status_code=501,
243
+ )
244
+
245
+
246
+ class InMemoryCloudGateway:
247
+ """Contract-test gateway; it records exactly what would cross the cloud boundary."""
248
+
249
+ def __init__(self) -> None:
250
+ self.uploads: list[dict[str, Any]] = []
251
+ self.versions: list[dict[str, Any]] = []
252
+ self.deployments: list[DeploymentRecord] = []
253
+ self.deleted_agent_ids: list[str] = []
254
+
255
+ async def upload_bundle(self, **kwargs) -> str:
256
+ self.uploads.append(kwargs)
257
+ return f"memory://artifacts/{kwargs['bundle_digest']}"
258
+
259
+ async def create_version(self, **kwargs) -> str:
260
+ self.versions.append(kwargs)
261
+ return f"ver_{uuid4().hex}"
262
+
263
+ async def create_deployment(self, **kwargs) -> DeploymentRecord:
264
+ record = DeploymentRecord(
265
+ id=f"dep_{uuid4().hex}",
266
+ build_id=kwargs["build_id"],
267
+ bundle_digest=kwargs["bundle_digest"],
268
+ version_id=kwargs["version_id"],
269
+ status="READY",
270
+ target=kwargs["request"].target,
271
+ )
272
+ self.deployments.append(record)
273
+ return record
274
+
275
+ async def replace_deployment(
276
+ self,
277
+ deployment: DeploymentRecord,
278
+ **kwargs,
279
+ ) -> DeploymentRecord:
280
+ record = DeploymentRecord(
281
+ id=f"dep_{uuid4().hex}",
282
+ build_id=kwargs["build_id"],
283
+ bundle_digest=kwargs["bundle_digest"],
284
+ version_id=kwargs["version_id"],
285
+ status="READY",
286
+ target=kwargs["request"].target,
287
+ agent_id=deployment.agent_id,
288
+ instance_id=deployment.instance_id,
289
+ endpoint=deployment.endpoint,
290
+ )
291
+ self.deployments.append(record)
292
+ return record
293
+
294
+ async def get_deployment_status(self, deployment: DeploymentRecord) -> DeploymentRecord:
295
+ return deployment
296
+
297
+ async def get_deployment_dashboard_access(
298
+ self, deployment: DeploymentRecord
299
+ ) -> dict[str, str | None]:
300
+ if not deployment.agent_id:
301
+ raise StudioError(
302
+ "DEPLOYMENT_DASHBOARD_UNAVAILABLE",
303
+ "Deployment receipt 缺少云端 Agent 标识",
304
+ status_code=409,
305
+ )
306
+ return {
307
+ "access_url": f"memory://dashboard/{deployment.agent_id}",
308
+ "agent_id": deployment.agent_id,
309
+ "instance_id": deployment.instance_id,
310
+ "expires_at": None,
311
+ }
312
+
313
+ async def delete_deployment(self, deployment: DeploymentRecord) -> bool:
314
+ if not deployment.agent_id:
315
+ raise StudioError(
316
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
317
+ "Deployment receipt 缺少云端 Agent 标识",
318
+ status_code=409,
319
+ )
320
+ self.deleted_agent_ids.append(deployment.agent_id)
321
+ return True
322
+
323
+ async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]:
324
+ rows = [
325
+ {
326
+ "agentId": item.agent_id,
327
+ "name": item.agent_id,
328
+ "status": item.status,
329
+ "endpoint": item.endpoint,
330
+ }
331
+ for item in self.deployments
332
+ if item.agent_id
333
+ ]
334
+ start = (page - 1) * size
335
+ return {"items": rows[start : start + size], "total": len(rows), "page": page, "size": size}
336
+
337
+ async def get_account_agent(self, agent_id: str) -> dict[str, Any]:
338
+ for item in reversed(self.deployments):
339
+ if item.agent_id == agent_id:
340
+ return {
341
+ "agentId": agent_id,
342
+ "name": agent_id,
343
+ "status": item.status,
344
+ "endpoint": item.endpoint,
345
+ "versionId": item.version_id,
346
+ }
347
+ raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 不存在", status_code=404)
348
+
349
+ async def list_account_agent_versions(
350
+ self, agent_id: str, *, page: int, size: int
351
+ ) -> dict[str, Any]:
352
+ detail = await self.get_account_agent(agent_id)
353
+ version_id = str(detail.get("versionId") or "").strip()
354
+ items = []
355
+ if version_id:
356
+ items.append(
357
+ {
358
+ "versionId": version_id,
359
+ "versionName": version_id,
360
+ "tag": "",
361
+ "status": "current",
362
+ "trafficPercentage": 100,
363
+ "canRollback": False,
364
+ "rollbackDisabledReason": "当前版本不可回滚至自身",
365
+ "createdAt": None,
366
+ "createdBy": "",
367
+ }
368
+ )
369
+ return {
370
+ "items": items,
371
+ "total": len(items),
372
+ "currentVersionId": version_id or None,
373
+ }
374
+
375
+ async def rollback_account_agent_version(
376
+ self, agent_id: str, *, version_id: str
377
+ ) -> dict[str, Any]:
378
+ await self.get_account_agent(agent_id)
379
+ return {
380
+ "agentId": agent_id,
381
+ "targetVersionId": version_id,
382
+ "status": "UPDATING",
383
+ "noop": False,
384
+ }
385
+
386
+ async def get_account_agent_dashboard_access(
387
+ self, agent_id: str
388
+ ) -> dict[str, str | None]:
389
+ return {
390
+ "access_url": f"memory://dashboard/{agent_id}",
391
+ "agent_id": agent_id,
392
+ "instance_id": None,
393
+ "expires_at": None,
394
+ }
395
+
396
+ async def delete_account_agent(self, agent_id: str) -> bool:
397
+ self.deleted_agent_ids.append(agent_id)
398
+ return True
399
+
400
+ async def create_managed_runtime_deployment(self, **kwargs) -> DeploymentRecord:
401
+ digest = str(kwargs["manifest_digest"])
402
+ record = DeploymentRecord(
403
+ id=f"dep_{uuid4().hex}",
404
+ build_id=str(kwargs["build_id"]),
405
+ bundle_digest=f"sha256:{digest}",
406
+ version_id=f"managed-{digest[:16]}",
407
+ status="READY",
408
+ target=kwargs["request"].target,
409
+ artifact_id="managed-runtime",
410
+ )
411
+ self.deployments.append(record)
412
+ return record
413
+
414
+ async def replace_managed_runtime_deployment(
415
+ self, deployment: DeploymentRecord, **kwargs
416
+ ) -> DeploymentRecord:
417
+ digest = str(kwargs["manifest_digest"])
418
+ record = DeploymentRecord(
419
+ id=f"dep_{uuid4().hex}",
420
+ build_id=str(kwargs["build_id"]),
421
+ bundle_digest=f"sha256:{digest}",
422
+ version_id=f"managed-{digest[:16]}",
423
+ status="READY",
424
+ target=kwargs["request"].target,
425
+ agent_id=deployment.agent_id,
426
+ instance_id=deployment.instance_id,
427
+ endpoint=deployment.endpoint,
428
+ artifact_id="managed-runtime",
429
+ )
430
+ self.deployments.append(record)
431
+ return record
432
+
433
+
434
+ class DirectAgentEngineCloudDeploymentGateway:
435
+ """Deploy a Studio Bundle with the established KS3 and signed Agent APIs.
436
+
437
+ A local Studio has the user's existing AK/SK and therefore uses the same
438
+ two-step path as ``agentengine build --push`` then ``agentengine deploy``:
439
+ upload an immutable ZIP to KS3, then call ``CreateAgent`` (or
440
+ ``UpdateAgent`` for rollback) through :class:`AgentEngineClient`. It does
441
+ not introduce an Artifact Action, a browser-provided trusted header, or a
442
+ second account-control authentication scheme.
443
+ """
444
+
445
+ requires_hosted_kernel_bundle_preflight = True
446
+
447
+ def __init__(
448
+ self,
449
+ *,
450
+ region: str,
451
+ client: Any | None = None,
452
+ stream_client: Any | None = None,
453
+ uploader_factory: Callable[..., Any] = KS3Uploader,
454
+ bucket: str | None = None,
455
+ ks3_credentials: dict[str, str] | None = None,
456
+ ) -> None:
457
+ self.region = region.strip()
458
+ self.ks3_region = "cn-beijing-6" if self.region.lower() == "pre-online" else self.region
459
+ self.uploader_factory = uploader_factory
460
+ self.bucket = bucket or os.environ.get("KS3_BUCKET", "").strip() or None
461
+ supplied_credentials = ks3_credentials or {}
462
+ self._ks3_credentials = {
463
+ "access_key": str(
464
+ supplied_credentials.get("access_key")
465
+ or os.environ.get("KSYUN_ACCESS_KEY")
466
+ or os.environ.get("KS3_ACCESS_KEY")
467
+ or ""
468
+ ).strip(),
469
+ "secret_key": str(
470
+ supplied_credentials.get("secret_key")
471
+ or os.environ.get("KSYUN_SECRET_KEY")
472
+ or os.environ.get("KS3_SECRET_KEY")
473
+ or ""
474
+ ).strip(),
475
+ }
476
+ if not all(self._ks3_credentials.values()):
477
+ raise ValueError("Studio cloud gateway requires process-only KS3 credentials")
478
+ # The same process-only AK/SK signs AgentEngine control-plane actions.
479
+ # Never let Studio silently fall through to an unsigned client just
480
+ # because an internal development ingress happens to be reachable.
481
+ self.client = client or AgentEngineClient(
482
+ region=self.region,
483
+ access_key=self._ks3_credentials["access_key"],
484
+ secret_key=self._ks3_credentials["secret_key"],
485
+ )
486
+ # Streaming can use a dedicated Server ingress while ordinary control
487
+ # actions continue through KOP. Some KOP deployments buffer the
488
+ # complete response body even when RunAgent returns SSE; both clients
489
+ # still use the same process-only V4 credentials and Server admission.
490
+ self.stream_client = stream_client or self.client
491
+ self._bundles: dict[str, dict[str, str]] = {}
492
+
493
+ async def upload_bundle(self, **kwargs) -> str:
494
+ bundle = bytes(kwargs["bundle"])
495
+ provenance = dict(kwargs["provenance"])
496
+ bundle_digest = str(kwargs["bundle_digest"])
497
+ agent_id = str(provenance.get("agentId") or "").strip()
498
+ runtime_type = str(provenance.get("runtimeType") or "").strip().lower()
499
+ if not agent_id or not runtime_type:
500
+ raise StudioError(
501
+ "CLOUD_BUNDLE_METADATA_INVALID",
502
+ "本地 Bundle 缺少 agentId 或 runtimeType,不能上云",
503
+ status_code=422,
504
+ )
505
+ archive_sha = hashlib.sha256(bundle).hexdigest()
506
+ if not bundle_digest.startswith("sha256:"):
507
+ raise StudioError(
508
+ "CLOUD_BUNDLE_METADATA_INVALID",
509
+ "本地 Bundle 缺少 sha256 digest,不能上云",
510
+ status_code=422,
511
+ )
512
+ object_key = f"studio-bundles/{_safe_object_component(agent_id)}/{archive_sha}/bundle.zip"
513
+ uploader = self.uploader_factory(region=self.ks3_region, bucket=self.bucket)
514
+ local_path: Path | None = None
515
+ try:
516
+ with tempfile.NamedTemporaryFile(
517
+ mode="wb", prefix="agentkit-studio-", suffix=".zip", delete=False
518
+ ) as output:
519
+ output.write(bundle)
520
+ local_path = Path(output.name)
521
+ bundle_uri = await uploader.upload(local_path, object_key)
522
+ finally:
523
+ if local_path is not None:
524
+ local_path.unlink(missing_ok=True)
525
+ if not bundle_uri:
526
+ raise StudioError(
527
+ "CLOUD_BUNDLE_UPLOAD_FAILED",
528
+ "Bundle 上传到 KS3 失败,未创建云端 Agent",
529
+ status_code=502,
530
+ )
531
+ uri = str(bundle_uri)
532
+ self._bundles[uri] = {
533
+ "agent_id": agent_id,
534
+ "archive_sha": archive_sha,
535
+ "bundle_digest": bundle_digest,
536
+ "runtime_type": runtime_type,
537
+ "bucket": str(getattr(uploader, "bucket_name", self.bucket or "")),
538
+ }
539
+ return uri
540
+
541
+ async def create_version(self, **kwargs) -> str:
542
+ bundle_uri = str(kwargs["bundle_uri"])
543
+ bundle = self._bundles.get(bundle_uri)
544
+ if bundle is None:
545
+ raise StudioError(
546
+ "CLOUD_BUNDLE_REFERENCE_INVALID",
547
+ "未知的 KS3 Bundle 引用",
548
+ status_code=502,
549
+ )
550
+ if bundle["agent_id"] != str(kwargs["agent_id"]):
551
+ raise StudioError(
552
+ "CLOUD_BUNDLE_REFERENCE_INVALID",
553
+ "Bundle 与当前 Agent 不匹配",
554
+ status_code=502,
555
+ )
556
+ if bundle["bundle_digest"] != str(kwargs["bundle_digest"]):
557
+ raise StudioError(
558
+ "CLOUD_BUNDLE_REFERENCE_INVALID",
559
+ "Bundle digest 不匹配",
560
+ status_code=502,
561
+ )
562
+ return bundle_uri
563
+
564
+ async def create_deployment(self, **kwargs) -> DeploymentRecord:
565
+ bundle_uri = str(kwargs["version_id"])
566
+ bundle = self._bundle_for_deployment(bundle_uri, kwargs["bundle_digest"])
567
+ request: DeploymentRequest = kwargs["request"]
568
+ result = await self.client.create_agent(
569
+ self._create_payload(bundle_uri=bundle_uri, bundle=bundle, request=request)
570
+ )
571
+ agent_id = str(result.get("agent_id") or "").strip()
572
+ instance_id = str(result.get("instance_id") or "").strip() or None
573
+ if not agent_id:
574
+ raise StudioError(
575
+ "CLOUD_DEPLOYMENT_PROTOCOL_INVALID",
576
+ "CreateAgentProduct 未返回 AgentId",
577
+ status_code=502,
578
+ )
579
+ return self._receipt(
580
+ build_id=str(kwargs["build_id"]),
581
+ bundle_digest=str(kwargs["bundle_digest"]),
582
+ bundle_uri=bundle_uri,
583
+ agent_id=agent_id,
584
+ instance_id=instance_id,
585
+ endpoint=str(result.get("endpoint") or "").strip() or None,
586
+ target=request.target,
587
+ status="DEPLOYING",
588
+ )
589
+
590
+ async def replace_deployment(
591
+ self,
592
+ deployment: DeploymentRecord,
593
+ **kwargs,
594
+ ) -> DeploymentRecord:
595
+ if not deployment.agent_id:
596
+ raise StudioError(
597
+ "DEPLOYMENT_PROTOCOL_INVALID",
598
+ "部署 receipt 缺少 AgentId",
599
+ status_code=502,
600
+ )
601
+ bundle_uri = str(kwargs["version_id"])
602
+ bundle = self._bundle_for_deployment(bundle_uri, kwargs["bundle_digest"])
603
+ request: DeploymentRequest = kwargs["request"]
604
+ await self.client.update_agent(
605
+ deployment.agent_id,
606
+ {
607
+ "artifact_type": "Code",
608
+ "artifact_path": bundle_uri,
609
+ "code_checksum": bundle["archive_sha"],
610
+ "code_command": list(_STUDIO_CODE_COMMAND),
611
+ "ks3": self._code_config(bundle["bucket"]),
612
+ },
613
+ )
614
+ return self._receipt(
615
+ build_id=str(kwargs["build_id"]),
616
+ bundle_digest=str(kwargs["bundle_digest"]),
617
+ bundle_uri=bundle_uri,
618
+ agent_id=deployment.agent_id,
619
+ instance_id=deployment.instance_id,
620
+ endpoint=deployment.endpoint,
621
+ target=request.target,
622
+ status="DEPLOYING",
623
+ )
624
+
625
+ async def get_deployment_status(self, deployment: DeploymentRecord) -> DeploymentRecord:
626
+ if not deployment.agent_id:
627
+ return deployment
628
+ try:
629
+ payload = await self.client.get_agent(agent_id=deployment.agent_id)
630
+ except AgentEngineAPIError as exc:
631
+ # A receipt can outlive the cloud Agent it originally created. Do
632
+ # not leave that receipt in DEPLOYING forever: the Server's 404 is
633
+ # an authoritative terminal fact, not a transient readiness gap.
634
+ if exc.code == 404 or exc.details.get("http_status") == 404:
635
+ return deployment.model_copy(update={"status": "FAILED"})
636
+ raise
637
+ deployment_detail = payload.get("deployment") or {}
638
+ kernel_ready = bool(
639
+ payload.get("agent_kernel_ready") or deployment_detail.get("agent_kernel_ready")
640
+ )
641
+ status = (
642
+ str(
643
+ payload.get("status")
644
+ or (payload.get("basic") or {}).get("status")
645
+ or (payload.get("deployment") or {}).get("status")
646
+ or ""
647
+ )
648
+ .strip()
649
+ .upper()
650
+ )
651
+ projected = (
652
+ "FAILED"
653
+ if status in {"FAILED", "TERMINATED", "ERROR"}
654
+ else "READY"
655
+ if kernel_ready
656
+ or (
657
+ not deployment.requires_kernel
658
+ and deployment.artifact_id == "managed-runtime"
659
+ and status in {"RUNNING", "READY"}
660
+ )
661
+ else "DEPLOYING"
662
+ )
663
+ endpoint = (
664
+ str(
665
+ payload.get("endpoint")
666
+ or (payload.get("basic") or {}).get("endpoint")
667
+ or deployment_detail.get("endpoint")
668
+ or deployment.endpoint
669
+ or ""
670
+ ).strip()
671
+ or None
672
+ )
673
+ return deployment.model_copy(update={"status": projected, "endpoint": endpoint})
674
+
675
+ async def get_deployment_dashboard_access(
676
+ self, deployment: DeploymentRecord
677
+ ) -> dict[str, str | None]:
678
+ if not deployment.agent_id:
679
+ raise StudioError(
680
+ "DEPLOYMENT_DASHBOARD_UNAVAILABLE",
681
+ "Deployment receipt 缺少云端 Agent 标识",
682
+ status_code=409,
683
+ )
684
+ link = await self.client.create_dashboard_access_link(
685
+ agent_id=deployment.agent_id,
686
+ link_type="private",
687
+ path=_HOSTED_AGENT_UI_PATH,
688
+ )
689
+ access_url = str(link.get("access_url") or "").strip()
690
+ if not access_url:
691
+ raise StudioError(
692
+ "DEPLOYMENT_DASHBOARD_UNAVAILABLE",
693
+ "云端未返回可用的 Agent UI 地址",
694
+ status_code=502,
695
+ )
696
+ return {
697
+ "access_url": access_url,
698
+ "agent_id": deployment.agent_id,
699
+ "instance_id": deployment.instance_id,
700
+ "expires_at": str(link.get("expires_at") or "").strip() or None,
701
+ }
702
+
703
+ async def delete_deployment(self, deployment: DeploymentRecord) -> bool:
704
+ agent_id = str(deployment.agent_id or "").strip()
705
+ if not agent_id:
706
+ raise StudioError(
707
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
708
+ "Deployment receipt 缺少云端 Agent 标识",
709
+ status_code=409,
710
+ )
711
+ try:
712
+ deleted = await self.client.delete_agent(agent_id)
713
+ except AgentEngineAPIError as exc:
714
+ if exc.code == 404 or exc.details.get("http_status") == 404:
715
+ return True
716
+ raise
717
+ if not deleted:
718
+ raise StudioError(
719
+ "CLOUD_AGENT_DELETE_FAILED",
720
+ "云端未确认 Agent 删除结果",
721
+ status_code=502,
722
+ )
723
+ return True
724
+
725
+ @staticmethod
726
+ def _session_event_chat_declared(capabilities: Any) -> bool:
727
+ if not isinstance(capabilities, dict):
728
+ return False
729
+ declaration = None
730
+ for name in ("session_event_chat", "sessionEventChat", "SessionEventChat"):
731
+ if name in capabilities:
732
+ declaration = capabilities[name]
733
+ break
734
+ if isinstance(declaration, bool):
735
+ return declaration
736
+ if not isinstance(declaration, dict):
737
+ return False
738
+ for name in ("enabled", "Enabled", "supported", "Supported"):
739
+ if name in declaration:
740
+ return declaration[name] is True
741
+ return False
742
+
743
+ @classmethod
744
+ def _cloud_chat_route(
745
+ cls, *, runtime_type: str, capabilities: Any
746
+ ) -> tuple[str, str]:
747
+ if cls._session_event_chat_declared(capabilities):
748
+ return (
749
+ "studio-session-events",
750
+ "declared-session-event-chat-capability",
751
+ )
752
+ if runtime_type in _NATIVE_DASHBOARD_FRAMEWORKS:
753
+ return (
754
+ "official-dashboard",
755
+ "native-runtime-without-session-event-chat-capability",
756
+ )
757
+ return "studio-session-events", "studio-compatible-framework"
758
+
759
+ @staticmethod
760
+ def _account_agent_view(payload: dict[str, Any], *, fallback_id: str = "") -> dict[str, Any]:
761
+ basic = payload.get("basic") if isinstance(payload.get("basic"), dict) else {}
762
+ quick_access = (
763
+ payload.get("quick_access")
764
+ if isinstance(payload.get("quick_access"), dict)
765
+ else payload.get("quickAccess")
766
+ if isinstance(payload.get("quickAccess"), dict)
767
+ else {}
768
+ )
769
+ deployment = (
770
+ payload.get("deployment")
771
+ if isinstance(payload.get("deployment"), dict)
772
+ else {}
773
+ )
774
+ runtime_config = (
775
+ deployment.get("runtime_config")
776
+ if isinstance(deployment.get("runtime_config"), dict)
777
+ else deployment.get("runtimeConfig")
778
+ if isinstance(deployment.get("runtimeConfig"), dict)
779
+ else {}
780
+ )
781
+
782
+ def first(*names: str) -> Any:
783
+ for source in (payload, basic, deployment):
784
+ for name in names:
785
+ value = source.get(name)
786
+ if value is not None and str(value).strip():
787
+ return value
788
+ return None
789
+
790
+ # GetAgent retains a few compatibility fields at the response root.
791
+ # They can lag behind ``basic`` while Runtime-Service is reconciling;
792
+ # presenting that stale root ``CREATING`` value in Studio hid a
793
+ # genuinely RUNNING Agent. Basic is the Server's lifecycle read model
794
+ # (and is also what the CLI renders), so it is authoritative here.
795
+ def lifecycle_first(*names: str) -> Any:
796
+ for source in (basic, deployment, payload):
797
+ for name in names:
798
+ value = source.get(name)
799
+ if value is not None and str(value).strip():
800
+ return value
801
+ return None
802
+
803
+ agent_id = str(
804
+ first("agent_id", "agentId", "agent_runtime_id", "agentRuntimeId", "id")
805
+ or fallback_id
806
+ ).strip()
807
+ runtime_type = str(
808
+ first(
809
+ "framework",
810
+ "runtime_kind",
811
+ "runtimeKind",
812
+ "runtime_type",
813
+ "runtimeType",
814
+ "runtime_name",
815
+ "runtimeName",
816
+ )
817
+ or ""
818
+ ).strip().lower()
819
+ capabilities = first("capabilities", "Capabilities")
820
+ chat_transport, chat_routing_reason = (
821
+ DirectAgentEngineCloudDeploymentGateway._cloud_chat_route(
822
+ runtime_type=runtime_type,
823
+ capabilities=capabilities,
824
+ )
825
+ )
826
+ version_id = str(first("version_id", "versionId", "revision") or "").strip()
827
+ manifest_sha256 = str(
828
+ runtime_config.get("manifest_sha256")
829
+ or runtime_config.get("manifestSha256")
830
+ or ""
831
+ ).strip().lower()
832
+ if not version_id and len(manifest_sha256) == 64:
833
+ try:
834
+ bytes.fromhex(manifest_sha256)
835
+ except ValueError:
836
+ pass
837
+ else:
838
+ version_id = f"managed-{manifest_sha256[:16]}"
839
+ return {
840
+ "agentId": agent_id,
841
+ "name": str(
842
+ first(
843
+ "name",
844
+ "agent_name",
845
+ "agentName",
846
+ "agent_runtime_name",
847
+ "agentRuntimeName",
848
+ )
849
+ or agent_id
850
+ ),
851
+ # Server resolves Creator through IAM when an Agent is created.
852
+ # It is the human-readable sub-account name; preserve it for the
853
+ # Studio directory rather than trying to derive a name client-side.
854
+ "creatorName": str(first("creator", "Creator") or "").strip() or None,
855
+ "status": str(lifecycle_first("status", "phase") or "UNKNOWN").upper(),
856
+ "endpoint": str(
857
+ quick_access.get("public_endpoint")
858
+ or quick_access.get("publicEndpoint")
859
+ or quick_access.get("private_endpoint")
860
+ or quick_access.get("privateEndpoint")
861
+ or lifecycle_first("endpoint")
862
+ or ""
863
+ ).strip() or None,
864
+ "framework": runtime_type or None,
865
+ "runtimeType": runtime_type or None,
866
+ "capabilities": capabilities if isinstance(capabilities, dict) else None,
867
+ "chatTransport": chat_transport,
868
+ "chatRoutingReason": chat_routing_reason,
869
+ "region": str(first("region") or "").strip() or None,
870
+ "instanceId": str(first("instance_id", "instanceId") or "").strip() or None,
871
+ "versionId": version_id or None,
872
+ "updatedAt": str(
873
+ lifecycle_first("updated_at", "updatedAt", "update_time", "updateTime") or ""
874
+ ).strip()
875
+ or None,
876
+ }
877
+
878
+ async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]:
879
+ payload = await self.client.list_agents(page=page, page_size=size)
880
+ raw_items = payload.get("agents") or payload.get("Agents") or []
881
+ items = [
882
+ self._account_agent_view(item)
883
+ for item in raw_items
884
+ if isinstance(item, dict)
885
+ ]
886
+ items = [
887
+ item
888
+ for item in items
889
+ if item["agentId"] and item["status"] != "DELETED"
890
+ ]
891
+ return {
892
+ "items": items,
893
+ "total": len(items),
894
+ "page": page,
895
+ "size": size,
896
+ }
897
+
898
+ async def get_account_agent(self, agent_id: str) -> dict[str, Any]:
899
+ normalized_id = str(agent_id or "").strip()
900
+ if not normalized_id:
901
+ raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 标识不能为空", status_code=404)
902
+ payload = await self.client.get_agent(agent_id=normalized_id)
903
+ return self._account_agent_view(payload, fallback_id=normalized_id)
904
+
905
+ @staticmethod
906
+ def _account_agent_version_view(payload: dict[str, Any]) -> dict[str, Any]:
907
+ def first(*names: str, default: Any = None) -> Any:
908
+ for name in names:
909
+ if name in payload and payload[name] is not None:
910
+ return payload[name]
911
+ return default
912
+
913
+ return {
914
+ "versionId": str(first("version_id", "VersionId", default="") or "").strip(),
915
+ "versionName": str(
916
+ first("version_name", "VersionName", default="") or ""
917
+ ).strip(),
918
+ "tag": str(first("tag", "Tag", default="") or "").strip(),
919
+ "status": str(first("status", "Status", default="") or "").strip(),
920
+ "trafficPercentage": int(
921
+ first("traffic_percentage", "TrafficPercentage", default=0) or 0
922
+ ),
923
+ "canRollback": first("can_rollback", "CanRollback", default=False) is True,
924
+ "rollbackDisabledReason": str(
925
+ first(
926
+ "rollback_disabled_reason",
927
+ "RollbackDisabledReason",
928
+ default="",
929
+ )
930
+ or ""
931
+ ).strip(),
932
+ "createdAt": str(first("created_at", "CreatedAt", default="") or "").strip()
933
+ or None,
934
+ "createdBy": str(first("created_by", "CreatedBy", default="") or "").strip(),
935
+ }
936
+
937
+ async def list_account_agent_versions(
938
+ self, agent_id: str, *, page: int, size: int
939
+ ) -> dict[str, Any]:
940
+ normalized_id = str(agent_id or "").strip()
941
+ if not normalized_id:
942
+ raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 标识不能为空", status_code=404)
943
+ try:
944
+ payload = await self.client.list_versions(normalized_id, page=page, size=size)
945
+ except AgentEngineAPIError as exc:
946
+ raise StudioError(
947
+ "CLOUD_AGENT_VERSION_DIRECTORY_FAILED",
948
+ exc.message,
949
+ status_code=502,
950
+ details={
951
+ "serverCode": exc.raw_code,
952
+ **{
953
+ key: value
954
+ for key, value in exc.details.items()
955
+ if key in {"request_id", "action"}
956
+ },
957
+ },
958
+ ) from exc
959
+ raw_items = payload.get("versions") or payload.get("Versions") or []
960
+ items = [
961
+ self._account_agent_version_view(item)
962
+ for item in raw_items
963
+ if isinstance(item, dict)
964
+ ]
965
+ items = [item for item in items if item["versionId"]]
966
+ current = next(
967
+ (
968
+ item["versionId"]
969
+ for item in items
970
+ if str(item["status"]).strip().lower() == "current"
971
+ ),
972
+ None,
973
+ )
974
+ return {
975
+ "items": items,
976
+ "total": int(
977
+ payload.get("total_count")
978
+ or payload.get("TotalCount")
979
+ or len(items)
980
+ ),
981
+ "currentVersionId": current,
982
+ }
983
+
984
+ async def rollback_account_agent_version(
985
+ self, agent_id: str, *, version_id: str
986
+ ) -> dict[str, Any]:
987
+ normalized_id = str(agent_id or "").strip()
988
+ normalized_version_id = str(version_id or "").strip()
989
+ if not normalized_id:
990
+ raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 标识不能为空", status_code=404)
991
+ if not normalized_version_id:
992
+ raise StudioError(
993
+ "CLOUD_AGENT_VERSION_REQUIRED",
994
+ "请选择要回滚的云端版本",
995
+ status_code=422,
996
+ field="versionId",
997
+ )
998
+ try:
999
+ payload = await self.client.rollback_version(
1000
+ normalized_id,
1001
+ target_version_id=normalized_version_id,
1002
+ )
1003
+ except AgentEngineAPIError as exc:
1004
+ raise StudioError(
1005
+ "CLOUD_AGENT_VERSION_ROLLBACK_FAILED",
1006
+ exc.message,
1007
+ status_code=502,
1008
+ details={
1009
+ "serverCode": exc.raw_code,
1010
+ **{
1011
+ key: value
1012
+ for key, value in exc.details.items()
1013
+ if key in {"request_id", "action"}
1014
+ },
1015
+ },
1016
+ ) from exc
1017
+ return {
1018
+ "agentId": str(payload.get("agent_id") or normalized_id),
1019
+ "targetVersionId": str(
1020
+ payload.get("target_version_id") or normalized_version_id
1021
+ ),
1022
+ "status": str(payload.get("status") or "UPDATING"),
1023
+ "noop": bool(payload.get("noop")),
1024
+ }
1025
+
1026
+ async def get_account_agent_dashboard_access(
1027
+ self, agent_id: str
1028
+ ) -> dict[str, str | None]:
1029
+ detail = await self.get_account_agent(agent_id)
1030
+ path = (
1031
+ _NATIVE_DASHBOARD_UI_PATH
1032
+ if detail.get("chatTransport") == "official-dashboard"
1033
+ else _HOSTED_AGENT_UI_PATH
1034
+ )
1035
+ link = await self.client.create_dashboard_access_link(
1036
+ agent_id=agent_id,
1037
+ link_type="private",
1038
+ path=path,
1039
+ )
1040
+ access_url = str(link.get("access_url") or "").strip()
1041
+ if not access_url:
1042
+ raise StudioError(
1043
+ "DEPLOYMENT_DASHBOARD_UNAVAILABLE",
1044
+ "云端未返回可用的 Agent UI 地址",
1045
+ status_code=502,
1046
+ )
1047
+ return {
1048
+ "access_url": access_url,
1049
+ "agent_id": agent_id,
1050
+ "instance_id": None,
1051
+ "expires_at": str(link.get("expires_at") or "").strip() or None,
1052
+ }
1053
+
1054
+ async def delete_account_agent(self, agent_id: str) -> bool:
1055
+ await self.get_account_agent(agent_id)
1056
+ try:
1057
+ deleted = await self.client.delete_agent(agent_id)
1058
+ except AgentEngineAPIError as exc:
1059
+ if exc.code == 404 or exc.details.get("http_status") == 404:
1060
+ return True
1061
+ raise
1062
+ if not deleted:
1063
+ raise StudioError(
1064
+ "CLOUD_AGENT_DELETE_FAILED",
1065
+ "云端未确认 Agent 删除结果",
1066
+ status_code=502,
1067
+ )
1068
+ return True
1069
+
1070
+ def _chat_agent_id(
1071
+ self, deployment: DeploymentRecord | AccountCloudAgentReference
1072
+ ) -> str:
1073
+ """Bind local cloud chat to an immutable Studio deployment receipt.
1074
+
1075
+ In particular, the browser cannot provide an arbitrary AgentId and
1076
+ turn the loopback Studio process into a signed control-plane proxy.
1077
+ """
1078
+
1079
+ agent_id = str(deployment.agent_id or "").strip()
1080
+ if not agent_id:
1081
+ raise StudioError(
1082
+ "DEPLOYMENT_CLOUD_CHAT_UNAVAILABLE",
1083
+ "部署 receipt 缺少云端 Agent 标识",
1084
+ status_code=409,
1085
+ )
1086
+ return agent_id
1087
+
1088
+ async def list_deployment_chat_sessions(
1089
+ self,
1090
+ deployment: DeploymentRecord,
1091
+ *,
1092
+ page: int = 1,
1093
+ size: int = 50,
1094
+ ) -> dict[str, Any]:
1095
+ """Read sessions through the authenticated Server projection."""
1096
+
1097
+ return await self.client.list_sessions(
1098
+ self._chat_agent_id(deployment), page=page, size=size
1099
+ )
1100
+
1101
+ async def create_deployment_chat_session(self, deployment: DeploymentRecord) -> dict[str, Any]:
1102
+ """Create one Server-owned session before its first cloud message."""
1103
+
1104
+ return await self.client.create_session(self._chat_agent_id(deployment))
1105
+
1106
+ async def list_deployment_chat_messages(
1107
+ self,
1108
+ deployment: DeploymentRecord,
1109
+ *,
1110
+ session_id: str,
1111
+ after_seq_id: int | None = None,
1112
+ limit: int = 100,
1113
+ ) -> dict[str, Any]:
1114
+ """Return the Server/Runtime message projection for a bound session."""
1115
+
1116
+ try:
1117
+ return await self.client.list_session_messages(
1118
+ agent_id=self._chat_agent_id(deployment),
1119
+ session_id=session_id,
1120
+ after_seq_id=after_seq_id,
1121
+ limit=limit,
1122
+ )
1123
+ except AgentEngineAPIError as exc:
1124
+ # A poll already in flight can complete after DeleteSession. The
1125
+ # deleted projection is an empty terminal view for Studio, not a
1126
+ # local API failure that should surface as a 500/toast.
1127
+ if exc.code == 404 or exc.details.get("http_status") == 404:
1128
+ return {"messages": [], "session_deleted": True}
1129
+ raise
1130
+
1131
+ async def delete_deployment_chat_session(
1132
+ self, deployment: DeploymentRecord, *, session_id: str
1133
+ ) -> bool:
1134
+ """Delete only a session that Server scopes to the signed caller."""
1135
+
1136
+ # The Server resolves session ownership from the authenticated caller;
1137
+ # the receipt binding above only establishes the enclosing Agent scope.
1138
+ self._chat_agent_id(deployment)
1139
+ deleted = await self.client.delete_session(session_id)
1140
+ if not deleted:
1141
+ raise StudioError(
1142
+ "CLOUD_CHAT_SESSION_DELETE_FAILED",
1143
+ "云端会话删除失败,请刷新后重试",
1144
+ status_code=502,
1145
+ )
1146
+ return True
1147
+
1148
+ async def list_deployment_chat_events(
1149
+ self,
1150
+ deployment: DeploymentRecord,
1151
+ *,
1152
+ session_id: str,
1153
+ after_seq_id: int | None = None,
1154
+ limit: int = 200,
1155
+ ) -> dict[str, Any]:
1156
+ """Read canonical events, including public Interaction/v1 frames."""
1157
+
1158
+ try:
1159
+ return await self.client.list_session_events(
1160
+ agent_id=self._chat_agent_id(deployment),
1161
+ session_id=session_id,
1162
+ after_seq_id=after_seq_id,
1163
+ limit=limit,
1164
+ )
1165
+ except AgentEngineAPIError as exc:
1166
+ if exc.code == 404 or exc.details.get("http_status") == 404:
1167
+ return {"events": [], "session_deleted": True}
1168
+ raise
1169
+
1170
+ async def send_deployment_chat_message(
1171
+ self,
1172
+ deployment: DeploymentRecord,
1173
+ *,
1174
+ session_id: str,
1175
+ content: Any,
1176
+ model: str | None = None,
1177
+ model_options: dict[str, Any] | None = None,
1178
+ tool_approval_mode: str | None = None,
1179
+ collaboration_mode: str | None = None,
1180
+ goal_objective: str | None = None,
1181
+ ) -> dict[str, Any]:
1182
+ """Submit a foreground message via RunAgent and Server admission.
1183
+
1184
+ Kernel-enabled Agents return a durable receipt immediately; clients
1185
+ then read the canonical message/session event stream rather than
1186
+ treating a synchronous proxy body as the source of truth.
1187
+ """
1188
+
1189
+ kwargs: dict[str, Any] = {
1190
+ "session_id": session_id,
1191
+ "model": model,
1192
+ "model_options": model_options,
1193
+ "tool_approval_mode": tool_approval_mode,
1194
+ "collaboration_mode": collaboration_mode,
1195
+ "goal_objective": goal_objective,
1196
+ }
1197
+ return await self.client.chat(
1198
+ self._chat_agent_id(deployment),
1199
+ content,
1200
+ **kwargs,
1201
+ )
1202
+
1203
+ async def stream_deployment_chat_message(
1204
+ self,
1205
+ deployment: DeploymentRecord | AccountCloudAgentReference,
1206
+ *,
1207
+ session_id: str,
1208
+ content: Any,
1209
+ model: str | None = None,
1210
+ model_options: dict[str, Any] | None = None,
1211
+ tool_approval_mode: str | None = None,
1212
+ collaboration_mode: str | None = None,
1213
+ goal_objective: str | None = None,
1214
+ ) -> AsyncIterator[bytes]:
1215
+ """Open the Server-admitted foreground RunAgent SSE connection."""
1216
+
1217
+ try:
1218
+ return await self.stream_client.chat_stream(
1219
+ self._chat_agent_id(deployment),
1220
+ content,
1221
+ session_id=session_id,
1222
+ model=model,
1223
+ model_options=model_options,
1224
+ tool_approval_mode=tool_approval_mode,
1225
+ collaboration_mode=collaboration_mode,
1226
+ goal_objective=goal_objective,
1227
+ )
1228
+ except AgentEngineAPIError as exc:
1229
+ raise StudioError(
1230
+ "CLOUD_CHAT_STREAM_FAILED",
1231
+ exc.message,
1232
+ status_code=502,
1233
+ details={
1234
+ "serverCode": exc.raw_code,
1235
+ **{
1236
+ key: value
1237
+ for key, value in exc.details.items()
1238
+ if key in {"request_id", "action", "http_status"}
1239
+ },
1240
+ },
1241
+ ) from exc
1242
+
1243
+ async def list_deployment_chat_models(
1244
+ self, deployment: DeploymentRecord
1245
+ ) -> dict[str, Any]:
1246
+ """Read the Server-authoritative model catalog for this Agent."""
1247
+
1248
+ return await self.client.list_agent_models(
1249
+ agent_id=self._chat_agent_id(deployment)
1250
+ )
1251
+
1252
+ async def submit_deployment_chat_interaction(
1253
+ self,
1254
+ deployment: DeploymentRecord,
1255
+ *,
1256
+ session_id: str,
1257
+ run_id: str,
1258
+ interaction_id: str,
1259
+ expected_revision: int,
1260
+ action: str,
1261
+ response: dict[str, Any],
1262
+ idempotency_key: str,
1263
+ ) -> dict[str, Any]:
1264
+ """Forward only Interaction/v1's caller-visible response fields."""
1265
+
1266
+ return await self.client.submit_interaction(
1267
+ agent_id=self._chat_agent_id(deployment),
1268
+ session_id=session_id,
1269
+ run_id=run_id,
1270
+ interaction_id=interaction_id,
1271
+ expected_revision=expected_revision,
1272
+ action=action,
1273
+ response=response,
1274
+ idempotency_key=idempotency_key,
1275
+ )
1276
+
1277
+ async def create_managed_runtime_deployment(self, **kwargs) -> DeploymentRecord:
1278
+ request: DeploymentRequest = kwargs["request"]
1279
+ digest = str(kwargs["manifest_digest"])
1280
+ runtime_environment = dict(kwargs.get("runtime_environment") or {})
1281
+ result = await self.client.create_agent(
1282
+ self._managed_runtime_payload(
1283
+ agent_name=str(kwargs["agent_name"]),
1284
+ manifest=str(kwargs["manifest"]),
1285
+ runtime_name=str(kwargs["runtime_name"]),
1286
+ runtime_version=str(kwargs["runtime_version"]),
1287
+ request=request,
1288
+ runtime_environment=runtime_environment,
1289
+ )
1290
+ )
1291
+ agent_id = str(result.get("agent_id") or "").strip()
1292
+ if not agent_id:
1293
+ raise StudioError(
1294
+ "CLOUD_DEPLOYMENT_PROTOCOL_INVALID",
1295
+ "CreateAgentProduct 未返回 AgentId",
1296
+ status_code=502,
1297
+ )
1298
+ return DeploymentRecord(
1299
+ id=f"dep_{uuid4().hex}",
1300
+ build_id=str(kwargs["build_id"]),
1301
+ bundle_digest=f"sha256:{digest}",
1302
+ version_id=f"managed-{digest[:16]}",
1303
+ status="DEPLOYING",
1304
+ target=request.target,
1305
+ agent_id=agent_id,
1306
+ instance_id=str(result.get("instance_id") or "").strip() or None,
1307
+ endpoint=str(result.get("endpoint") or "").strip() or None,
1308
+ artifact_id="managed-runtime",
1309
+ requires_kernel=True,
1310
+ )
1311
+
1312
+ async def replace_managed_runtime_deployment(
1313
+ self, deployment: DeploymentRecord, **kwargs
1314
+ ) -> DeploymentRecord:
1315
+ if not deployment.agent_id:
1316
+ raise StudioError(
1317
+ "DEPLOYMENT_PROTOCOL_INVALID",
1318
+ "部署 receipt 缺少 AgentId",
1319
+ status_code=502,
1320
+ )
1321
+ request: DeploymentRequest = kwargs["request"]
1322
+ digest = str(kwargs["manifest_digest"])
1323
+ runtime_environment = dict(kwargs.get("runtime_environment") or {})
1324
+ update_payload: dict[str, Any] = {
1325
+ "artifact_type": "ManagedRuntime",
1326
+ # UpdateAgent resolves a fresh immutable runtime image from the
1327
+ # complete YAML declaration. RuntimeConfig is the resolved
1328
+ # read-model and cannot be used as an input for a retry.
1329
+ "managed_runtime_config": {
1330
+ "runtime_name": str(kwargs["runtime_name"]),
1331
+ "runtime_version": str(kwargs["runtime_version"]),
1332
+ "manifest": str(kwargs["manifest"]),
1333
+ },
1334
+ }
1335
+ if runtime_environment:
1336
+ # A changed MCP/model binding can introduce a new credential ref.
1337
+ # Re-resolve it for every immutable revision instead of relying on
1338
+ # the environment captured by the first CreateAgent call.
1339
+ update_payload["environment_variables"] = runtime_environment
1340
+ await self.client.update_agent(
1341
+ deployment.agent_id,
1342
+ update_payload,
1343
+ )
1344
+ return DeploymentRecord(
1345
+ id=f"dep_{uuid4().hex}",
1346
+ build_id=str(kwargs["build_id"]),
1347
+ bundle_digest=f"sha256:{digest}",
1348
+ version_id=f"managed-{digest[:16]}",
1349
+ status="DEPLOYING",
1350
+ target=request.target,
1351
+ agent_id=deployment.agent_id,
1352
+ instance_id=deployment.instance_id,
1353
+ endpoint=deployment.endpoint,
1354
+ artifact_id="managed-runtime",
1355
+ requires_kernel=True,
1356
+ )
1357
+
1358
+ def _bundle_for_deployment(self, bundle_uri: str, bundle_digest: Any) -> dict[str, str]:
1359
+ bundle = self._bundles.get(bundle_uri)
1360
+ if bundle is None or bundle["bundle_digest"] != str(bundle_digest):
1361
+ raise StudioError(
1362
+ "CLOUD_BUNDLE_REFERENCE_INVALID",
1363
+ "Bundle 引用或 digest 不匹配",
1364
+ status_code=502,
1365
+ )
1366
+ return bundle
1367
+
1368
+ def _create_payload(
1369
+ self,
1370
+ *,
1371
+ bundle_uri: str,
1372
+ bundle: dict[str, str],
1373
+ request: DeploymentRequest,
1374
+ ) -> dict[str, Any]:
1375
+ return {
1376
+ "name": _server_agent_name(bundle["agent_id"]),
1377
+ "description": "Created by AgentKit Studio",
1378
+ "framework": bundle["runtime_type"],
1379
+ "artifact_type": "Code",
1380
+ "artifact_path": bundle_uri,
1381
+ "code_checksum": bundle["archive_sha"],
1382
+ "code_command": list(_STUDIO_CODE_COMMAND),
1383
+ "region": request.target.region,
1384
+ "ks3": self._code_config(bundle["bucket"]),
1385
+ "resources": {"cpu": 2, "memory": "4Gi"},
1386
+ "scaling": {"min_replicas": 1, "max_replicas": 1, "concurrency": 20},
1387
+ "auth_type": "ApiKey",
1388
+ }
1389
+
1390
+ @staticmethod
1391
+ def _managed_runtime_payload(
1392
+ *,
1393
+ agent_name: str,
1394
+ manifest: str,
1395
+ runtime_name: str,
1396
+ runtime_version: str,
1397
+ request: DeploymentRequest,
1398
+ runtime_environment: dict[str, str] | None = None,
1399
+ ) -> dict[str, Any]:
1400
+ payload = {
1401
+ "name": _server_agent_name(agent_name),
1402
+ "description": "Created by AgentKit Studio",
1403
+ "framework": runtime_name,
1404
+ "artifact_type": "ManagedRuntime",
1405
+ # ManagedRuntimeConfig is the public declaration accepted by
1406
+ # CreateAgent. RuntimeConfig is only the Server-resolved state.
1407
+ "managed_runtime_config": {
1408
+ "runtime_name": runtime_name,
1409
+ "runtime_version": runtime_version,
1410
+ "manifest": manifest,
1411
+ },
1412
+ "region": request.target.region,
1413
+ # YAML agents run from a platform-owned runtime image and have no
1414
+ # user code bundle to build or unpack. Keep their default small;
1415
+ # high-code deployments retain their separate 2 CPU / 4 GiB
1416
+ # profile in _create_payload above.
1417
+ "resources": {"cpu": 1, "memory": "2Gi"},
1418
+ "scaling": {"min_replicas": 1, "max_replicas": 1, "concurrency": 20},
1419
+ "auth_type": "ApiKey",
1420
+ }
1421
+ if runtime_environment:
1422
+ # Model credentials are resolved only for this in-memory deployment
1423
+ # request. They are never written into the YAML build or local
1424
+ # deployment receipt.
1425
+ payload["environment_variables"] = dict(runtime_environment)
1426
+ return payload
1427
+
1428
+ def _code_config(self, bucket: str) -> dict[str, str]:
1429
+ return {
1430
+ **self._ks3_credentials,
1431
+ "region": self.ks3_region,
1432
+ "bucket": bucket,
1433
+ }
1434
+
1435
+ @staticmethod
1436
+ def _receipt(
1437
+ *,
1438
+ build_id: str,
1439
+ bundle_digest: str,
1440
+ bundle_uri: str,
1441
+ agent_id: str,
1442
+ instance_id: str | None,
1443
+ endpoint: str | None,
1444
+ target,
1445
+ status: str,
1446
+ ) -> DeploymentRecord:
1447
+ return DeploymentRecord(
1448
+ id=f"dep_{uuid4().hex}",
1449
+ build_id=build_id,
1450
+ bundle_digest=bundle_digest,
1451
+ version_id=f"bundle-{hashlib.sha256(bundle_uri.encode('utf-8')).hexdigest()[:16]}",
1452
+ status=cast(Any, status),
1453
+ target=target,
1454
+ agent_id=agent_id,
1455
+ instance_id=instance_id,
1456
+ endpoint=endpoint,
1457
+ bundle_uri=bundle_uri,
1458
+ requires_kernel=True,
1459
+ )
1460
+
1461
+
1462
+ def _server_agent_name(agent_id: str) -> str:
1463
+ normalized = "".join(
1464
+ char if char.isalnum() or char == "-" else "-" for char in agent_id.lower()
1465
+ )
1466
+ normalized = normalized.strip("-") or "agent"
1467
+ if not normalized[0].isalpha():
1468
+ normalized = f"agent-{normalized}"
1469
+ if not normalized.startswith("studio-"):
1470
+ normalized = f"studio-{normalized}"
1471
+ return normalized[:63].rstrip("-")
1472
+
1473
+
1474
+ def _safe_object_component(value: str) -> str:
1475
+ """Keep an immutable KS3 key below the Studio-owned prefix."""
1476
+
1477
+ normalized = "".join(
1478
+ char if char.isalnum() or char in {"-", "_"} else "-" for char in value.lower()
1479
+ ).strip("-")
1480
+ return normalized[:96] or "agent"
1481
+
1482
+
1483
+ class CloudDeploymentService:
1484
+ def __init__(
1485
+ self,
1486
+ workspace: Workspace,
1487
+ *,
1488
+ gateway: CloudDeploymentGateway,
1489
+ build_repository: BuildRepository | None = None,
1490
+ ) -> None:
1491
+ self.workspace = workspace
1492
+ self.gateway = gateway
1493
+ self.build_repository = build_repository or BuildRepository(workspace)
1494
+
1495
+ async def deploy(
1496
+ self,
1497
+ build_id: str,
1498
+ request: DeploymentRequest,
1499
+ ) -> DeploymentRecord:
1500
+ return await self._deploy_build(build_id, request)
1501
+
1502
+ async def deploy_managed_runtime(
1503
+ self,
1504
+ *,
1505
+ build_id: str,
1506
+ agent_name: str,
1507
+ manifest: str,
1508
+ runtime_name: str,
1509
+ runtime_version: str,
1510
+ manifest_digest: str,
1511
+ request: DeploymentRequest,
1512
+ runtime_environment: dict[str, str] | None = None,
1513
+ replacing: DeploymentRecord | None = None,
1514
+ ) -> DeploymentRecord:
1515
+ # A YAML/ManagedRuntime revision is deliberately not a migration
1516
+ # mechanism for a user-code Agent. In particular, Studio may manage
1517
+ # the lifecycle of an existing Code deployment, but it must never
1518
+ # replace that deployment's artifact with a generated declaration.
1519
+ # The receipt is the local proof that this Agent was created through
1520
+ # the ManagedRuntime path in the first place.
1521
+ if replacing is not None and replacing.artifact_id != "managed-runtime":
1522
+ raise StudioError(
1523
+ "MANAGED_RUNTIME_REPLACEMENT_FORBIDDEN",
1524
+ "声明式 YAML 只能更新由 Studio 声明式路径创建的 Agent,不能覆盖高代码 Agent",
1525
+ status_code=409,
1526
+ details={"deploymentId": replacing.id},
1527
+ )
1528
+ if replacing is None:
1529
+ record = await self.gateway.create_managed_runtime_deployment(
1530
+ build_id=build_id,
1531
+ agent_name=agent_name,
1532
+ manifest=manifest,
1533
+ runtime_name=runtime_name,
1534
+ runtime_version=runtime_version,
1535
+ manifest_digest=manifest_digest,
1536
+ request=request,
1537
+ runtime_environment=runtime_environment,
1538
+ )
1539
+ else:
1540
+ record = await self.gateway.replace_managed_runtime_deployment(
1541
+ replacing,
1542
+ build_id=build_id,
1543
+ manifest=manifest,
1544
+ runtime_name=runtime_name,
1545
+ runtime_version=runtime_version,
1546
+ manifest_digest=manifest_digest,
1547
+ request=request,
1548
+ runtime_environment=runtime_environment,
1549
+ )
1550
+ self._save(record, request)
1551
+ return record
1552
+
1553
+ async def _deploy_build(
1554
+ self,
1555
+ build_id: str,
1556
+ request: DeploymentRequest,
1557
+ *,
1558
+ replacing: DeploymentRecord | None = None,
1559
+ ) -> DeploymentRecord:
1560
+ build, bundle, provenance = self._prepared_build(build_id)
1561
+ bundle_uri = await self.gateway.upload_bundle(
1562
+ bundle=bundle,
1563
+ bundle_digest=build.bundle_digest,
1564
+ provenance=provenance,
1565
+ )
1566
+ version_id = await self.gateway.create_version(
1567
+ agent_id=build.agent_id,
1568
+ bundle_uri=bundle_uri,
1569
+ bundle_digest=build.bundle_digest,
1570
+ provenance=provenance,
1571
+ )
1572
+ if replacing is None:
1573
+ record = await self.gateway.create_deployment(
1574
+ build_id=build_id,
1575
+ version_id=version_id,
1576
+ bundle_digest=build.bundle_digest,
1577
+ request=request,
1578
+ )
1579
+ else:
1580
+ record = await self.gateway.replace_deployment(
1581
+ replacing,
1582
+ build_id=build_id,
1583
+ version_id=version_id,
1584
+ bundle_digest=build.bundle_digest,
1585
+ request=request,
1586
+ )
1587
+ self._save(record, request)
1588
+ return record
1589
+
1590
+ def _prepared_build(self, build_id: str) -> tuple[BuildRecord, bytes, dict[str, Any]]:
1591
+ build = self.build_repository.get(build_id)
1592
+ if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
1593
+ raise StudioError(
1594
+ "BUILD_NOT_READY",
1595
+ "只有成功 Build 可以部署",
1596
+ status_code=409,
1597
+ )
1598
+ archive = self.workspace.resolve(build.artifact_path, must_exist=True)
1599
+ bundle = archive.read_bytes()
1600
+ checked_bundle = (
1601
+ preflight_hosted_kernel_bundle(bundle)
1602
+ if getattr(self.gateway, "requires_hosted_kernel_bundle_preflight", False)
1603
+ else None
1604
+ )
1605
+ manifest = (
1606
+ checked_bundle.manifest
1607
+ if checked_bundle is not None
1608
+ else json.loads(
1609
+ (archive.parent / "agent-bundle" / "manifest.json").read_text(encoding="utf-8")
1610
+ )
1611
+ )
1612
+ if manifest.get("bundleDigest") != build.bundle_digest:
1613
+ raise StudioError(
1614
+ "BUILD_DIGEST_MISMATCH",
1615
+ "上传前 Bundle digest 校验失败",
1616
+ status_code=422,
1617
+ )
1618
+ if checked_bundle is not None and manifest.get("agentId") != build.agent_id:
1619
+ raise StudioError(
1620
+ "BUILD_AGENT_MISMATCH",
1621
+ "上传 Bundle 的 AgentId 与 Build 记录不一致",
1622
+ status_code=422,
1623
+ )
1624
+ archive_sha256 = f"sha256:{hashlib.sha256(bundle).hexdigest()}"
1625
+ provenance = (
1626
+ dict(checked_bundle.provenance)
1627
+ if checked_bundle is not None
1628
+ else json.loads(
1629
+ (archive.parent / "agent-bundle" / "provenance.json").read_text(encoding="utf-8")
1630
+ )
1631
+ )
1632
+ provenance["archiveSha256"] = archive_sha256
1633
+ # The Server owns the profile-to-image mapping, but it must select the
1634
+ # profile for the concrete framework the deterministic build produced.
1635
+ provenance["runtimeType"] = str(manifest.get("runtimeType") or build.runtime_type)
1636
+ return build, bundle, provenance
1637
+
1638
+ def get(self, deployment_id: str) -> DeploymentRecord:
1639
+ path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
1640
+ if not path.is_file():
1641
+ raise StudioError(
1642
+ "DEPLOYMENT_NOT_FOUND",
1643
+ "Deployment 不存在",
1644
+ status_code=404,
1645
+ details={"id": deployment_id},
1646
+ )
1647
+ payload = json.loads(path.read_text(encoding="utf-8"))
1648
+ return cast(
1649
+ DeploymentRecord,
1650
+ DeploymentRecord.model_validate(payload["record"]),
1651
+ )
1652
+
1653
+ def request_for(self, deployment_id: str) -> DeploymentRequest:
1654
+ """Return the immutable target stored with a deployment receipt."""
1655
+
1656
+ path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
1657
+ if not path.is_file():
1658
+ self.get(deployment_id)
1659
+ try:
1660
+ payload = json.loads(path.read_text(encoding="utf-8"))
1661
+ return DeploymentRequest.model_validate(payload["request"])
1662
+ except (OSError, ValueError, KeyError, json.JSONDecodeError, ValidationError) as exc:
1663
+ raise StudioError(
1664
+ "DEPLOYMENT_RECEIPT_INVALID",
1665
+ "Deployment 回执损坏,不能执行回滚",
1666
+ status_code=409,
1667
+ details={"id": deployment_id},
1668
+ ) from exc
1669
+
1670
+ def list(self) -> list[DeploymentRecord]:
1671
+ """List only valid, workspace-local deployment receipts.
1672
+
1673
+ This is deliberately a local receipt read. Refreshing every row here
1674
+ would turn opening the Studio page into unbounded control-plane calls;
1675
+ callers refresh a named receipt explicitly instead.
1676
+ """
1677
+
1678
+ directory = self.workspace.resolve(".agentkit/deployments")
1679
+ if not directory.is_dir():
1680
+ return []
1681
+ resolved_directory = directory.resolve()
1682
+ records: list[DeploymentRecord] = []
1683
+ for path in sorted(directory.glob("dep_*.json"), key=lambda item: item.name, reverse=True):
1684
+ try:
1685
+ resolved_path = path.resolve(strict=True)
1686
+ resolved_path.relative_to(resolved_directory)
1687
+ payload = json.loads(resolved_path.read_text(encoding="utf-8"))
1688
+ record = DeploymentRecord.model_validate(payload["record"])
1689
+ if resolved_path.name != f"{record.id}.json":
1690
+ raise ValueError("deployment receipt filename does not match record id")
1691
+ except (
1692
+ OSError,
1693
+ ValueError,
1694
+ KeyError,
1695
+ json.JSONDecodeError,
1696
+ ValidationError,
1697
+ ):
1698
+ logger.warning("Ignoring invalid Studio deployment receipt: %s", path.name)
1699
+ continue
1700
+ records.append(record)
1701
+ return records
1702
+
1703
+ async def refresh(self, deployment_id: str) -> DeploymentRecord:
1704
+ """Refresh only from the Server-owned instance status projection."""
1705
+
1706
+ deployment = self.get(deployment_id)
1707
+ status_reader = getattr(self.gateway, "get_deployment_status", None)
1708
+ if status_reader is None:
1709
+ return deployment
1710
+ refreshed = await status_reader(deployment)
1711
+ path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
1712
+ payload = json.loads(path.read_text(encoding="utf-8"))
1713
+ self._save(
1714
+ refreshed,
1715
+ DeploymentRequest.model_validate(payload["request"]),
1716
+ )
1717
+ return refreshed
1718
+
1719
+ async def dashboard_access(self, deployment_id: str) -> dict[str, str | None]:
1720
+ """Create a private, receipt-bound Hosted UI link on explicit request."""
1721
+
1722
+ deployment = self.get(deployment_id)
1723
+ dashboard_reader = getattr(self.gateway, "get_deployment_dashboard_access", None)
1724
+ if dashboard_reader is None:
1725
+ raise StudioError(
1726
+ "DEPLOYMENT_DASHBOARD_UNAVAILABLE",
1727
+ "当前云端网关不支持打开 Agent UI",
1728
+ status_code=501,
1729
+ )
1730
+ return await dashboard_reader(deployment)
1731
+
1732
+ async def delete(self, deployment_id: str) -> dict[str, Any]:
1733
+ """Delete the receipt-bound cloud Agent, then remove its local receipts."""
1734
+
1735
+ deployment = self.get(deployment_id)
1736
+ agent_id = str(deployment.agent_id or "").strip()
1737
+ if not agent_id:
1738
+ raise StudioError(
1739
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
1740
+ "Deployment receipt 缺少云端 Agent 标识",
1741
+ status_code=409,
1742
+ )
1743
+ deleter = getattr(self.gateway, "delete_deployment", None)
1744
+ if deleter is None:
1745
+ raise StudioError(
1746
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
1747
+ "当前云端网关不支持删除 Agent",
1748
+ status_code=501,
1749
+ )
1750
+ await deleter(deployment)
1751
+
1752
+ deleted_receipts = self._delete_receipts_for_agent(
1753
+ agent_id, required=deployment
1754
+ )
1755
+ return {"agentId": agent_id, "deletedReceiptIds": deleted_receipts}
1756
+
1757
+ def _delete_receipts_for_agent(
1758
+ self, agent_id: str, *, required: DeploymentRecord | None = None
1759
+ ) -> list[str]:
1760
+ related_receipts = {required.id: required} if required is not None else {}
1761
+ for record in self.list():
1762
+ if str(record.agent_id or "").strip() != agent_id:
1763
+ continue
1764
+ related_receipts[record.id] = record
1765
+
1766
+ deleted_receipts: list[str] = []
1767
+ for record in related_receipts.values():
1768
+ receipt_path = self.workspace.resolve(
1769
+ Path(".agentkit/deployments") / f"{record.id}.json"
1770
+ )
1771
+ receipt_path.unlink(missing_ok=True)
1772
+ deleted_receipts.append(record.id)
1773
+ return deleted_receipts
1774
+
1775
+ async def list_account_agents(self, *, page: int = 1, size: int = 100) -> dict[str, Any]:
1776
+ reader = getattr(self.gateway, "list_account_agents", None)
1777
+ if reader is None:
1778
+ raise StudioError(
1779
+ "CLOUD_AGENT_DIRECTORY_UNAVAILABLE",
1780
+ "当前云端网关不支持读取账号 Agent",
1781
+ status_code=501,
1782
+ )
1783
+ return await reader(page=page, size=size)
1784
+
1785
+ async def get_account_agent(self, agent_id: str) -> dict[str, Any]:
1786
+ reader = getattr(self.gateway, "get_account_agent", None)
1787
+ if reader is None:
1788
+ raise StudioError(
1789
+ "CLOUD_AGENT_DIRECTORY_UNAVAILABLE",
1790
+ "当前云端网关不支持读取账号 Agent",
1791
+ status_code=501,
1792
+ )
1793
+ return await reader(agent_id)
1794
+
1795
+ async def list_account_agent_versions(
1796
+ self, agent_id: str, *, page: int = 1, size: int = 100
1797
+ ) -> dict[str, Any]:
1798
+ reader = getattr(self.gateway, "list_account_agent_versions", None)
1799
+ if reader is None:
1800
+ raise StudioError(
1801
+ "CLOUD_AGENT_VERSION_DIRECTORY_UNAVAILABLE",
1802
+ "当前云端网关不支持读取 Agent 版本",
1803
+ status_code=501,
1804
+ )
1805
+ return await reader(agent_id, page=page, size=size)
1806
+
1807
+ async def rollback_account_agent_version(
1808
+ self, agent_id: str, *, version_id: str
1809
+ ) -> dict[str, Any]:
1810
+ rollback = getattr(self.gateway, "rollback_account_agent_version", None)
1811
+ if rollback is None:
1812
+ raise StudioError(
1813
+ "CLOUD_AGENT_VERSION_ROLLBACK_UNAVAILABLE",
1814
+ "当前云端网关不支持回滚 Agent 版本",
1815
+ status_code=501,
1816
+ )
1817
+ return await rollback(agent_id, version_id=version_id)
1818
+
1819
+ async def account_agent_dashboard_access(
1820
+ self, agent_id: str
1821
+ ) -> dict[str, str | None]:
1822
+ reader = getattr(self.gateway, "get_account_agent_dashboard_access", None)
1823
+ if reader is None:
1824
+ raise StudioError(
1825
+ "DEPLOYMENT_DASHBOARD_UNAVAILABLE",
1826
+ "当前云端网关不支持打开 Agent UI",
1827
+ status_code=501,
1828
+ )
1829
+ return await reader(agent_id)
1830
+
1831
+ async def delete_account_agent(self, agent_id: str) -> dict[str, Any]:
1832
+ deleter = getattr(self.gateway, "delete_account_agent", None)
1833
+ if deleter is None:
1834
+ raise StudioError(
1835
+ "CLOUD_AGENT_DELETE_UNAVAILABLE",
1836
+ "当前云端网关不支持删除 Agent",
1837
+ status_code=501,
1838
+ )
1839
+ await deleter(agent_id)
1840
+ return {
1841
+ "agentId": agent_id,
1842
+ "deletedReceiptIds": self._delete_receipts_for_agent(agent_id),
1843
+ }
1844
+
1845
+ async def _chat_target(
1846
+ self, target_id: str
1847
+ ) -> DeploymentRecord | AccountCloudAgentReference:
1848
+ if not target_id.startswith("account:"):
1849
+ return self.get(target_id)
1850
+ agent_id = target_id.removeprefix("account:").strip()
1851
+ detail = await self.get_account_agent(agent_id)
1852
+ resolved_id = str(detail.get("agentId") or "").strip()
1853
+ if not agent_id or resolved_id != agent_id:
1854
+ raise StudioError(
1855
+ "CLOUD_AGENT_REFERENCE_INVALID",
1856
+ "账号 Agent 引用与 Server 返回不一致",
1857
+ status_code=409,
1858
+ )
1859
+ if detail.get("chatTransport") != "studio-session-events":
1860
+ raise StudioError(
1861
+ "CLOUD_CHAT_TRANSPORT_UNSUPPORTED",
1862
+ "该类型 Agent 未声明统一 SessionEvent 会话能力,请使用官方 Dashboard",
1863
+ status_code=409,
1864
+ details={
1865
+ "agentId": agent_id,
1866
+ "chatTransport": detail.get("chatTransport"),
1867
+ "reason": detail.get("chatRoutingReason"),
1868
+ },
1869
+ )
1870
+ return AccountCloudAgentReference(agent_id=agent_id)
1871
+
1872
+ async def list_cloud_chat_sessions(
1873
+ self, deployment_id: str, *, page: int = 1, size: int = 50
1874
+ ) -> dict[str, Any]:
1875
+ deployment = await self._chat_target(deployment_id)
1876
+ reader = getattr(self.gateway, "list_deployment_chat_sessions", None)
1877
+ if reader is None:
1878
+ raise StudioError(
1879
+ "CLOUD_CHAT_UNAVAILABLE",
1880
+ "当前云端网关不支持本地会话代理",
1881
+ status_code=501,
1882
+ )
1883
+ return await reader(deployment, page=page, size=size)
1884
+
1885
+ async def create_cloud_chat_session(self, deployment_id: str) -> dict[str, Any]:
1886
+ deployment = await self._chat_target(deployment_id)
1887
+ creator = getattr(self.gateway, "create_deployment_chat_session", None)
1888
+ if creator is None:
1889
+ raise StudioError(
1890
+ "CLOUD_CHAT_UNAVAILABLE",
1891
+ "当前云端网关不支持本地会话代理",
1892
+ status_code=501,
1893
+ )
1894
+ return await creator(deployment)
1895
+
1896
+ async def list_cloud_chat_messages(
1897
+ self,
1898
+ deployment_id: str,
1899
+ *,
1900
+ session_id: str,
1901
+ after_seq_id: int | None = None,
1902
+ limit: int = 100,
1903
+ ) -> dict[str, Any]:
1904
+ deployment = await self._chat_target(deployment_id)
1905
+ reader = getattr(self.gateway, "list_deployment_chat_messages", None)
1906
+ if reader is None:
1907
+ raise StudioError(
1908
+ "CLOUD_CHAT_UNAVAILABLE",
1909
+ "当前云端网关不支持本地会话代理",
1910
+ status_code=501,
1911
+ )
1912
+ return await reader(
1913
+ deployment,
1914
+ session_id=session_id,
1915
+ after_seq_id=after_seq_id,
1916
+ limit=limit,
1917
+ )
1918
+
1919
+ async def list_cloud_chat_events(
1920
+ self,
1921
+ deployment_id: str,
1922
+ *,
1923
+ session_id: str,
1924
+ after_seq_id: int | None = None,
1925
+ limit: int = 200,
1926
+ ) -> dict[str, Any]:
1927
+ deployment = await self._chat_target(deployment_id)
1928
+ reader = getattr(self.gateway, "list_deployment_chat_events", None)
1929
+ if reader is None:
1930
+ raise StudioError(
1931
+ "CLOUD_CHAT_UNAVAILABLE",
1932
+ "当前云端网关不支持本地会话代理",
1933
+ status_code=501,
1934
+ )
1935
+ return await reader(
1936
+ deployment,
1937
+ session_id=session_id,
1938
+ after_seq_id=after_seq_id,
1939
+ limit=limit,
1940
+ )
1941
+
1942
+ async def send_cloud_chat_message(
1943
+ self,
1944
+ deployment_id: str,
1945
+ *,
1946
+ session_id: str,
1947
+ content: Any,
1948
+ model: str | None = None,
1949
+ model_options: dict[str, Any] | None = None,
1950
+ tool_approval_mode: str | None = None,
1951
+ collaboration_mode: str | None = None,
1952
+ goal_objective: str | None = None,
1953
+ ) -> dict[str, Any]:
1954
+ deployment = await self._chat_target(deployment_id)
1955
+ sender = getattr(self.gateway, "send_deployment_chat_message", None)
1956
+ if sender is None:
1957
+ raise StudioError(
1958
+ "CLOUD_CHAT_UNAVAILABLE",
1959
+ "当前云端网关不支持本地会话代理",
1960
+ status_code=501,
1961
+ )
1962
+ kwargs: dict[str, Any] = {
1963
+ "session_id": session_id,
1964
+ "content": content,
1965
+ }
1966
+ if model is not None:
1967
+ kwargs["model"] = model
1968
+ if model_options:
1969
+ kwargs["model_options"] = model_options
1970
+ if tool_approval_mode is not None:
1971
+ kwargs["tool_approval_mode"] = tool_approval_mode
1972
+ if collaboration_mode is not None:
1973
+ kwargs["collaboration_mode"] = collaboration_mode
1974
+ if goal_objective is not None:
1975
+ kwargs["goal_objective"] = goal_objective
1976
+ return await sender(deployment, **kwargs)
1977
+
1978
+ async def stream_cloud_chat_message(
1979
+ self,
1980
+ deployment_id: str,
1981
+ *,
1982
+ session_id: str,
1983
+ content: Any,
1984
+ model: str | None = None,
1985
+ model_options: dict[str, Any] | None = None,
1986
+ tool_approval_mode: str | None = None,
1987
+ collaboration_mode: str | None = None,
1988
+ goal_objective: str | None = None,
1989
+ ) -> AsyncIterator[bytes]:
1990
+ deployment = await self._chat_target(deployment_id)
1991
+ sender = getattr(self.gateway, "stream_deployment_chat_message", None)
1992
+ if sender is None:
1993
+ raise StudioError(
1994
+ "CLOUD_CHAT_STREAM_UNAVAILABLE",
1995
+ "当前云端网关不支持实时会话代理",
1996
+ status_code=501,
1997
+ )
1998
+ return await sender(
1999
+ deployment,
2000
+ session_id=session_id,
2001
+ content=content,
2002
+ model=model,
2003
+ model_options=model_options,
2004
+ tool_approval_mode=tool_approval_mode,
2005
+ collaboration_mode=collaboration_mode,
2006
+ goal_objective=goal_objective,
2007
+ )
2008
+
2009
+ async def list_cloud_chat_models(self, deployment_id: str) -> dict[str, Any]:
2010
+ deployment = await self._chat_target(deployment_id)
2011
+ reader = getattr(self.gateway, "list_deployment_chat_models", None)
2012
+ if reader is None:
2013
+ raise StudioError(
2014
+ "CLOUD_CHAT_UNAVAILABLE",
2015
+ "当前云端网关不支持模型目录代理",
2016
+ status_code=501,
2017
+ )
2018
+ return await reader(deployment)
2019
+
2020
+ async def submit_cloud_chat_interaction(
2021
+ self,
2022
+ deployment_id: str,
2023
+ *,
2024
+ session_id: str,
2025
+ run_id: str,
2026
+ interaction_id: str,
2027
+ expected_revision: int,
2028
+ action: str,
2029
+ response: dict[str, Any],
2030
+ idempotency_key: str,
2031
+ ) -> dict[str, Any]:
2032
+ deployment = await self._chat_target(deployment_id)
2033
+ submitter = getattr(self.gateway, "submit_deployment_chat_interaction", None)
2034
+ if submitter is None:
2035
+ raise StudioError(
2036
+ "CLOUD_CHAT_UNAVAILABLE",
2037
+ "当前云端网关不支持本地会话代理",
2038
+ status_code=501,
2039
+ )
2040
+ return await submitter(
2041
+ deployment,
2042
+ session_id=session_id,
2043
+ run_id=run_id,
2044
+ interaction_id=interaction_id,
2045
+ expected_revision=expected_revision,
2046
+ action=action,
2047
+ response=response,
2048
+ idempotency_key=idempotency_key,
2049
+ )
2050
+
2051
+ async def delete_cloud_chat_session(self, deployment_id: str, *, session_id: str) -> bool:
2052
+ deployment = await self._chat_target(deployment_id)
2053
+ deleter = getattr(self.gateway, "delete_deployment_chat_session", None)
2054
+ if deleter is None:
2055
+ raise StudioError(
2056
+ "CLOUD_CHAT_UNAVAILABLE",
2057
+ "当前云端网关不支持本地会话代理",
2058
+ status_code=501,
2059
+ )
2060
+ return await deleter(deployment, session_id=session_id)
2061
+
2062
+ async def rollback(
2063
+ self,
2064
+ deployment_id: str,
2065
+ *,
2066
+ target_build_id: str,
2067
+ ) -> DeploymentRecord:
2068
+ path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
2069
+ if not path.is_file():
2070
+ self.get(deployment_id)
2071
+ request = self.request_for(deployment_id)
2072
+ payload = json.loads(path.read_text(encoding="utf-8"))
2073
+ deployment = DeploymentRecord.model_validate(payload["record"])
2074
+ return await self._deploy_build(
2075
+ target_build_id,
2076
+ request,
2077
+ replacing=deployment,
2078
+ )
2079
+
2080
+ def _save(
2081
+ self,
2082
+ record: DeploymentRecord,
2083
+ request: DeploymentRequest,
2084
+ ) -> None:
2085
+ directory = self.workspace.resolve(".agentkit/deployments")
2086
+ directory.mkdir(parents=True, exist_ok=True)
2087
+ self.workspace.atomic_write_text(
2088
+ directory / f"{record.id}.json",
2089
+ json.dumps(
2090
+ {
2091
+ "record": record.model_dump(by_alias=True, exclude_none=True, mode="json"),
2092
+ "request": request.model_dump(by_alias=True, exclude_none=True, mode="json"),
2093
+ },
2094
+ ensure_ascii=False,
2095
+ sort_keys=True,
2096
+ indent=2,
2097
+ )
2098
+ + "\n",
2099
+ )