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/api.py ADDED
@@ -0,0 +1,1598 @@
1
+ """FastAPI application for the loopback-only AgentKit Studio control plane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hmac
7
+ import json
8
+ import os
9
+ import secrets
10
+ from collections.abc import AsyncIterator
11
+ from contextlib import asynccontextmanager
12
+ from pathlib import Path
13
+ from typing import Any, Literal
14
+
15
+ from fastapi import FastAPI, File, Header, Query, Request, Response, UploadFile
16
+ from fastapi.exceptions import RequestValidationError
17
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
18
+ from fastapi.staticfiles import StaticFiles
19
+ from starlette.background import BackgroundTask
20
+
21
+ from ksadk.studio.api_catalog_routes import register_catalog_routes
22
+ from ksadk.studio.api_contracts import (
23
+ AuthoringCommitRequest,
24
+ BuildRequest,
25
+ CloudAgentVersionRollbackRequest,
26
+ CloudChatInteractionSubmitRequest,
27
+ CloudChatMessageRequest,
28
+ ContextPreviewRequest,
29
+ ConversationAuthoringRequest,
30
+ CreateAgentRequest,
31
+ ImportRootRequest,
32
+ InteractionSubmitRequest,
33
+ ProjectInspectRequest,
34
+ PromptCompileRequest,
35
+ QuickAuthoringRequest,
36
+ RollbackRequest,
37
+ RunRequest,
38
+ SessionExchangeRequest,
39
+ StudioEvaluationCreate,
40
+ ValidationRequest,
41
+ WorkspaceOpenRequest,
42
+ )
43
+ from ksadk.studio.api_helpers import (
44
+ error_response as _error_response,
45
+ )
46
+ from ksadk.studio.api_helpers import (
47
+ is_local_origin,
48
+ )
49
+ from ksadk.studio.api_helpers import (
50
+ optional_int as _optional_int,
51
+ )
52
+ from ksadk.studio.api_helpers import (
53
+ parse_revision as _parse_revision,
54
+ )
55
+ from ksadk.studio.api_helpers import (
56
+ require_idempotency_key as _require_idempotency_key,
57
+ )
58
+ from ksadk.studio.api_helpers import (
59
+ responses_input as _responses_input,
60
+ )
61
+ from ksadk.studio.api_helpers import (
62
+ responses_session_id as _responses_session_id,
63
+ )
64
+ from ksadk.studio.api_helpers import (
65
+ sse as _sse,
66
+ )
67
+ from ksadk.studio.api_memory_routes import register_memory_routes
68
+ from ksadk.studio.codex_manifest import CodexAgentManifest
69
+ from ksadk.studio.contracts import (
70
+ AgentAppearance,
71
+ AgentBindings,
72
+ AgentSpec,
73
+ AgentTemplateComposeRequest,
74
+ DeploymentRequest,
75
+ OperationStatus,
76
+ )
77
+ from ksadk.studio.errors import StudioError
78
+ from ksadk.studio.service import StudioService
79
+ from ksadk.studio.shared_web import StudioSharedWebBridge
80
+
81
+ _WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
82
+ _PUBLIC_API_PATHS = {
83
+ "/api/v1/system/health",
84
+ "/api/v1/system/session",
85
+ }
86
+ _LOCAL_HOSTS = {"127.0.0.1", "::1", "localhost", "testserver"}
87
+
88
+
89
+ def create_studio_app(
90
+ root: Path | str,
91
+ *,
92
+ service: StudioService | None = None,
93
+ session_token: str | None = None,
94
+ csrf_token: str | None = None,
95
+ security_enabled: bool = True,
96
+ ) -> FastAPI:
97
+ studio = service or StudioService(root)
98
+ session_secret = session_token or secrets.token_urlsafe(32)
99
+ csrf_secret = csrf_token or secrets.token_urlsafe(24)
100
+
101
+ @asynccontextmanager
102
+ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
103
+ try:
104
+ await studio.run_service.recover_interrupted()
105
+ yield
106
+ finally:
107
+ studio.credentials.clear_session()
108
+
109
+ app = FastAPI(
110
+ title="AgentKit Local Studio",
111
+ version="1.0.0",
112
+ docs_url=None,
113
+ redoc_url=None,
114
+ openapi_url="/api/v1/openapi.json",
115
+ lifespan=lifespan,
116
+ )
117
+ app.state.studio_service = studio
118
+ app.state.session_token = session_secret
119
+ app.state.csrf_token = csrf_secret
120
+ static_root = Path(__file__).with_name("static")
121
+ shared_web = StudioSharedWebBridge(studio)
122
+ app.state.shared_web_bridge = shared_web
123
+ app.mount("/static", StaticFiles(directory=static_root), name="studio-static")
124
+
125
+ @app.middleware("http")
126
+ async def local_security(request: Request, call_next):
127
+ request.state.request_id = request.headers.get("X-Request-Id") or (
128
+ f"req_{secrets.token_hex(12)}"
129
+ )
130
+ host = (request.url.hostname or "").lower()
131
+ if host not in _LOCAL_HOSTS:
132
+ return _error_response(
133
+ StudioError(
134
+ "LOCAL_HOST_FORBIDDEN",
135
+ "Studio 只接受 loopback Host",
136
+ status_code=403,
137
+ ),
138
+ request,
139
+ )
140
+ origin = request.headers.get("Origin")
141
+ if origin and not is_local_origin(origin, local_hosts=_LOCAL_HOSTS):
142
+ return _error_response(
143
+ StudioError(
144
+ "LOCAL_ORIGIN_FORBIDDEN",
145
+ "请求 Origin 不是本地 Studio",
146
+ status_code=403,
147
+ ),
148
+ request,
149
+ )
150
+ content_length = request.headers.get("Content-Length")
151
+ large_upload_paths = {
152
+ "/api/v1/catalog/skills:import": 52 * 1024 * 1024,
153
+ "/api/v1/authoring/imports:inspect": 102 * 1024 * 1024,
154
+ "/api/v1/evaluation-files": 2 * 1024 * 1024 + 64 * 1024,
155
+ }
156
+ request_limit = large_upload_paths.get(request.url.path, 2 * 1024 * 1024)
157
+ if content_length and int(content_length) > request_limit:
158
+ return _error_response(
159
+ StudioError(
160
+ "REQUEST_TOO_LARGE",
161
+ "请求体超过 2 MiB 限制",
162
+ status_code=413,
163
+ ),
164
+ request,
165
+ )
166
+ studio_api = request.url.path.startswith("/api/v1")
167
+ shared_web_api = request.url.path.startswith("/agentengine/api/v1")
168
+ responses_api = request.url.path == "/v1/responses" or request.url.path.startswith(
169
+ "/v1/responses/"
170
+ )
171
+ if security_enabled and (
172
+ (studio_api and request.url.path not in _PUBLIC_API_PATHS)
173
+ or shared_web_api
174
+ or responses_api
175
+ ):
176
+ supplied = request.cookies.get("agentkit_studio_session") or request.headers.get(
177
+ "X-AgentKit-Session"
178
+ )
179
+ if not supplied or not hmac.compare_digest(supplied, session_secret):
180
+ return _error_response(
181
+ StudioError(
182
+ "LOCAL_SESSION_REQUIRED",
183
+ "缺少有效的 Studio 本地会话",
184
+ status_code=401,
185
+ ),
186
+ request,
187
+ )
188
+ if (studio_api or responses_api) and request.method in _WRITE_METHODS:
189
+ csrf = request.headers.get("X-CSRF-Token")
190
+ if not csrf or not hmac.compare_digest(csrf, csrf_secret):
191
+ return _error_response(
192
+ StudioError(
193
+ "CSRF_TOKEN_INVALID",
194
+ "缺少有效的 CSRF token",
195
+ status_code=403,
196
+ ),
197
+ request,
198
+ )
199
+ response = await call_next(request)
200
+ response.headers["X-Request-Id"] = request.state.request_id
201
+ response.headers["Cache-Control"] = (
202
+ "no-store"
203
+ if request.url.path.startswith(("/api/", "/v1/"))
204
+ else response.headers.get("Cache-Control", "no-cache")
205
+ )
206
+ response.headers["X-Content-Type-Options"] = "nosniff"
207
+ response.headers["X-Frame-Options"] = "DENY"
208
+ response.headers["Referrer-Policy"] = "no-referrer"
209
+ return response
210
+
211
+ @app.exception_handler(StudioError)
212
+ async def studio_error_handler(request: Request, exc: StudioError):
213
+ return _error_response(exc, request)
214
+
215
+ @app.exception_handler(RequestValidationError)
216
+ async def validation_error_handler(request: Request, exc: RequestValidationError):
217
+ first = exc.errors()[0] if exc.errors() else {}
218
+ field = ".".join(str(value) for value in first.get("loc", [])[1:])
219
+ return _error_response(
220
+ StudioError(
221
+ "REQUEST_VALIDATION_FAILED",
222
+ str(first.get("msg") or "请求参数无效"),
223
+ status_code=422,
224
+ field=field or None,
225
+ ),
226
+ request,
227
+ )
228
+
229
+ @app.get("/")
230
+ async def index():
231
+ path = static_root / "index.html"
232
+ response = FileResponse(path, media_type="text/html")
233
+ if security_enabled:
234
+ response.set_cookie(
235
+ "agentkit_studio_session",
236
+ session_secret,
237
+ httponly=True,
238
+ samesite="strict",
239
+ secure=False,
240
+ path="/",
241
+ )
242
+ return response
243
+
244
+ @app.get("/favicon.ico")
245
+ async def favicon():
246
+ return Response(status_code=204)
247
+
248
+ async def runtime_model_catalog():
249
+ api_base = os.getenv("OPENAI_BASE_URL") or os.getenv("OPENAI_API_BASE")
250
+ current_model = os.getenv("OPENAI_MODEL_NAME") or os.getenv("MODEL_NAME")
251
+ try:
252
+ api_key = studio.credentials.resolve("env://OPENAI_API_KEY")
253
+ except StudioError:
254
+ api_key = None
255
+ return await studio.catalog.discover_provider_models(
256
+ api_base=api_base,
257
+ api_key=api_key,
258
+ current_model=current_model,
259
+ )
260
+
261
+ @app.get("/v1/models")
262
+ async def openai_models():
263
+ models, source = await runtime_model_catalog()
264
+ return {
265
+ "object": "list",
266
+ "data": [dict(item.contract.get("metadata") or {"id": item.name}) for item in models],
267
+ "current": os.getenv("OPENAI_MODEL_NAME") or os.getenv("MODEL_NAME"),
268
+ "source": source,
269
+ }
270
+
271
+ @app.post("/v1/responses")
272
+ async def openai_responses(payload: dict[str, Any]):
273
+ metadata = payload.get("metadata")
274
+ metadata = metadata if isinstance(metadata, dict) else {}
275
+ agent_id = str(metadata.get("agent_id") or metadata.get("agentId") or "") or None
276
+ requested_approval_mode = (
277
+ str(metadata.get("approval_mode") or metadata.get("approvalMode") or "").strip().lower()
278
+ )
279
+ if requested_approval_mode and requested_approval_mode not in {"ask", "risk", "full"}:
280
+ raise StudioError(
281
+ "APPROVAL_MODE_INVALID",
282
+ "批准模式必须是 ask、risk 或 full",
283
+ status_code=422,
284
+ field="metadata.approval_mode",
285
+ )
286
+ collaboration_mode = (
287
+ str(metadata.get("collaboration_mode") or metadata.get("collaborationMode") or "")
288
+ .strip()
289
+ .lower()
290
+ )
291
+ if collaboration_mode and collaboration_mode not in {"default", "plan"}:
292
+ raise StudioError(
293
+ "COLLABORATION_MODE_INVALID",
294
+ "协作模式必须是 default 或 plan",
295
+ status_code=422,
296
+ field="metadata.collaboration_mode",
297
+ )
298
+ goal_objective = str(
299
+ metadata.get("goal_objective") or metadata.get("goalObjective") or ""
300
+ ).strip()
301
+ reasoning = payload.get("reasoning")
302
+ reasoning = reasoning if isinstance(reasoning, dict) else {}
303
+ reasoning_effort = str(reasoning.get("effort") or "").strip().lower()
304
+ if reasoning_effort and reasoning_effort not in {"low", "medium", "high"}:
305
+ raise StudioError(
306
+ "REASONING_EFFORT_INVALID",
307
+ "推理强度必须是 low、medium 或 high",
308
+ status_code=422,
309
+ field="reasoning.effort",
310
+ )
311
+ session_id = _responses_session_id(payload, bridge=shared_web)
312
+ response_id = str(
313
+ metadata.get("invocation_id")
314
+ or metadata.get("invocationId")
315
+ or f"resp_{secrets.token_hex(12)}"
316
+ )
317
+ bridge_payload: dict[str, Any] = {
318
+ "AgentId": shared_web.resolve_agent_id(agent_id),
319
+ "SessionId": session_id or f"ses_{secrets.token_hex(12)}",
320
+ "InvocationId": response_id,
321
+ "Model": str(payload.get("model") or ""),
322
+ "ResponsesInput": _responses_input(payload.get("input")),
323
+ "ApprovalMode": requested_approval_mode,
324
+ "CollaborationMode": collaboration_mode,
325
+ "GoalObjective": goal_objective,
326
+ "ReasoningEffort": reasoning_effort,
327
+ }
328
+ bridge_payload["Model"] = shared_web.select_model(
329
+ bridge_payload["AgentId"],
330
+ bridge_payload["Model"],
331
+ )
332
+ if bool(payload.get("stream")):
333
+ return StreamingResponse(
334
+ shared_web.stream_run(bridge_payload),
335
+ media_type="text/event-stream",
336
+ headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
337
+ )
338
+ return await shared_web.invoke_response(bridge_payload)
339
+
340
+ @app.post("/v1/responses/{response_id}/cancel")
341
+ async def cancel_openai_response(response_id: str):
342
+ result = shared_web.cancel_run(response_id)
343
+ return {
344
+ "id": response_id,
345
+ "object": "response",
346
+ "status": "cancelled" if result["Cancelled"] else "not_found",
347
+ }
348
+
349
+ @app.post("/v1/responses/{response_id}:pause", status_code=202)
350
+ async def pause_openai_response(response_id: str):
351
+ return await shared_web.pause_run(response_id)
352
+
353
+ @app.post("/v1/responses/{response_id}:resume", status_code=202)
354
+ async def resume_openai_response(response_id: str):
355
+ return await shared_web.resume_run(response_id)
356
+
357
+ @app.post("/agentengine/api/v1/{action}")
358
+ async def shared_chat_action(
359
+ action: str,
360
+ request: Request,
361
+ payload: dict[str, Any],
362
+ ):
363
+ try:
364
+ cookie_agent_id = request.cookies.get("agentkit_studio_chat_agent")
365
+ requested_agent_id = str(payload.get("AgentId") or cookie_agent_id or "")
366
+ if action == "GetAgentUiBootstrap":
367
+ data = shared_web.bootstrap(shared_web.resolve_agent_id(requested_agent_id or None))
368
+ elif action == "ListAgentModels":
369
+ data = shared_web.list_models(
370
+ shared_web.resolve_agent_id(requested_agent_id or None)
371
+ )
372
+ elif action == "ListSessions":
373
+ data = shared_web.list_sessions(
374
+ shared_web.resolve_agent_id(requested_agent_id or None),
375
+ page=int(payload.get("Page") or 1),
376
+ page_size=int(payload.get("PageSize") or 30),
377
+ )
378
+ elif action == "CreateSession":
379
+ data = shared_web.create_session(
380
+ shared_web.resolve_agent_id(requested_agent_id or None)
381
+ )
382
+ elif action == "GetSession":
383
+ data = shared_web.get_session(str(payload.get("SessionId") or ""))
384
+ elif action == "DeleteSession":
385
+ data = shared_web.delete_session(str(payload.get("SessionId") or ""))
386
+ elif action == "ListSessionMessages":
387
+ data = await shared_web.list_messages(
388
+ str(payload.get("SessionId") or ""),
389
+ after_seq_id=_optional_int(payload.get("AfterSeqId")),
390
+ before_seq_id=_optional_int(payload.get("BeforeSeqId")),
391
+ limit=int(payload.get("Limit") or 50),
392
+ )
393
+ elif action == "ListSessionEvents":
394
+ data = await shared_web.list_session_events(str(payload.get("SessionId") or ""))
395
+ elif action == "RunAgent":
396
+ return StreamingResponse(
397
+ shared_web.stream_run(payload),
398
+ media_type="text/event-stream",
399
+ headers={
400
+ "Cache-Control": "no-store",
401
+ "X-Accel-Buffering": "no",
402
+ },
403
+ )
404
+ elif action == "CancelRun":
405
+ data = shared_web.cancel_run(str(payload.get("InvocationId") or ""))
406
+ elif action in {
407
+ "GetResponseFeedback",
408
+ "UpsertResponseFeedback",
409
+ "DeleteResponseFeedback",
410
+ }:
411
+ data = {"Feedback": None} if action == "GetResponseFeedback" else {}
412
+ elif action == "ListSessionCheckpoints":
413
+ data = {"Checkpoints": []}
414
+ elif action == "ListToolReceipts":
415
+ data = {"ToolReceipts": []}
416
+ else:
417
+ return JSONResponse(
418
+ status_code=404,
419
+ content={
420
+ "Code": 404,
421
+ "Message": f"Studio 尚未实现共享 Web 动作:{action}",
422
+ "Data": {},
423
+ },
424
+ )
425
+ return {"Code": 0, "Message": "OK", "Data": data}
426
+ except StudioError as exc:
427
+ return JSONResponse(
428
+ status_code=exc.status_code,
429
+ content={
430
+ "Code": exc.status_code,
431
+ "Message": exc.message,
432
+ "Data": {"errorCode": exc.code},
433
+ },
434
+ )
435
+
436
+ @app.get("/agentengine/api/v1/SubscribeRunEvents")
437
+ async def shared_chat_subscribe_run_events():
438
+ async def completed_stream():
439
+ yield "event: done\ndata: [DONE]\n\n"
440
+
441
+ return StreamingResponse(
442
+ completed_stream(),
443
+ media_type="text/event-stream",
444
+ headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
445
+ )
446
+
447
+ @app.get("/api/v1/system/health")
448
+ async def health():
449
+ return {
450
+ "status": "ok",
451
+ "version": "1.0.0",
452
+ "workspaceReady": studio.workspace.root.is_dir(),
453
+ }
454
+
455
+ @app.post("/api/v1/system/session")
456
+ async def exchange_session(payload: SessionExchangeRequest, response: Response):
457
+ if security_enabled and not hmac.compare_digest(payload.token, session_secret):
458
+ raise StudioError(
459
+ "LOCAL_SESSION_INVALID",
460
+ "Studio 启动会话无效",
461
+ status_code=401,
462
+ )
463
+ response.set_cookie(
464
+ "agentkit_studio_session",
465
+ session_secret,
466
+ httponly=True,
467
+ samesite="strict",
468
+ secure=False,
469
+ path="/",
470
+ )
471
+ return {"csrfToken": csrf_secret}
472
+
473
+ @app.get("/api/v1/system/bootstrap")
474
+ async def bootstrap():
475
+ return {
476
+ "serverVersion": "1.0.0",
477
+ "apiVersion": "v1",
478
+ "sessionTokenRequired": security_enabled,
479
+ "csrfToken": csrf_secret,
480
+ "workspace": {
481
+ "name": studio.workspace.root.name,
482
+ "path": str(studio.workspace.root),
483
+ },
484
+ "operationScope": studio.deployment_operation_scope(),
485
+ "features": {
486
+ "build": True,
487
+ "run": True,
488
+ "runtimeRegistry": True,
489
+ "runtimeTypes": ["codex", "adk", "langgraph"],
490
+ "evaluation": True,
491
+ "deployment": True,
492
+ "cloudRebuild": False,
493
+ "reactChat": True,
494
+ },
495
+ "runtimes": studio.runtime_catalog(),
496
+ "importableProject": studio.detect_importable_project(),
497
+ }
498
+
499
+ @app.get("/api/v1/system/settings")
500
+ async def get_settings():
501
+ return studio.get_settings()
502
+
503
+ @app.put("/api/v1/system/settings")
504
+ async def update_settings(payload: dict[str, Any]):
505
+ return studio.update_settings(payload)
506
+
507
+ @app.post("/api/v1/workspaces:open")
508
+ async def open_workspace(payload: WorkspaceOpenRequest):
509
+ # This endpoint only reconnects to the daemon's already-bound root. Do
510
+ # not resolve or otherwise touch a caller-provided filesystem path.
511
+ requested = os.path.normcase(os.path.abspath(os.path.expanduser(payload.path)))
512
+ bound_root = os.path.normcase(str(studio.workspace.root))
513
+ if requested != bound_root:
514
+ raise StudioError(
515
+ "WORKSPACE_PATH_FORBIDDEN",
516
+ "当前 Daemon 不允许切换到启动 root 之外的工作区",
517
+ status_code=403,
518
+ )
519
+ return {
520
+ "name": studio.workspace.root.name,
521
+ "path": str(studio.workspace.root),
522
+ }
523
+
524
+ @app.get("/api/v1/codex/manifest")
525
+ async def get_codex_manifest():
526
+ return studio.codex_manifest_state()
527
+
528
+ @app.put("/api/v1/codex/manifest")
529
+ async def put_codex_manifest(payload: CodexAgentManifest):
530
+ return studio.save_codex_manifest(payload)
531
+
532
+ @app.post("/api/v1/codex/manifest:validate")
533
+ async def validate_codex_manifest(_payload: CodexAgentManifest):
534
+ return {"valid": True, "diagnostics": []}
535
+
536
+ @app.post("/api/v1/codex/builds", status_code=202)
537
+ async def create_codex_build(
538
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
539
+ ):
540
+ return studio.submit_codex_build(idempotency_key=_require_idempotency_key(idempotency_key))
541
+
542
+ @app.get("/api/v1/codex/builds")
543
+ async def list_codex_builds():
544
+ return {"items": studio.codex_builds.list()}
545
+
546
+ @app.get("/api/v1/codex/builds/{build_id}")
547
+ async def get_codex_build(build_id: str):
548
+ return studio.codex_builds.get(build_id)
549
+
550
+ @app.post("/api/v1/codex/builds/{build_id}/runs", status_code=202)
551
+ async def create_codex_run(
552
+ build_id: str,
553
+ payload: RunRequest,
554
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
555
+ ):
556
+ if payload.environment != "local":
557
+ raise StudioError(
558
+ "RUN_ENVIRONMENT_UNSUPPORTED",
559
+ "Codex Studio 只支持只读本地运行",
560
+ status_code=422,
561
+ )
562
+ return studio.submit_codex_run(
563
+ build_id,
564
+ payload.input.content,
565
+ session_id=payload.session_id,
566
+ model=payload.model,
567
+ idempotency_key=_require_idempotency_key(idempotency_key),
568
+ )
569
+
570
+ @app.post("/api/v1/builds/{build_id}/run:stream")
571
+ async def stream_run(
572
+ build_id: str,
573
+ payload: RunRequest,
574
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
575
+ ):
576
+ key = _require_idempotency_key(idempotency_key)
577
+ if payload.environment != "local":
578
+ raise StudioError(
579
+ "RUN_ENVIRONMENT_UNSUPPORTED",
580
+ "Codex Studio 只支持只读本地运行",
581
+ status_code=422,
582
+ )
583
+
584
+ queue: asyncio.Queue = asyncio.Queue()
585
+
586
+ def observe(event):
587
+ queue.put_nowait(event)
588
+
589
+ # The Operation owns the runtime task. The SSE response is only one
590
+ # observer, so refreshing or switching chats cannot cancel the Run.
591
+ operation = studio.submit_studio_run(
592
+ build_id,
593
+ payload.input.content,
594
+ session_id=payload.session_id,
595
+ model=payload.model,
596
+ sandbox=payload.sandbox,
597
+ idempotency_key=key,
598
+ on_event=observe,
599
+ )
600
+
601
+ async def render():
602
+ while True:
603
+ try:
604
+ event = await asyncio.wait_for(queue.get(), timeout=0.1)
605
+ except TimeoutError:
606
+ event = None
607
+ if event is not None:
608
+ data = json.dumps(
609
+ event.data,
610
+ ensure_ascii=False,
611
+ separators=(",", ":"),
612
+ )
613
+ yield f"id: {event.id}\nevent: {event.type}\ndata: {data}\n\n"
614
+ current = studio.operations.get(operation.id)
615
+ if (
616
+ current.status
617
+ in {
618
+ OperationStatus.SUCCEEDED,
619
+ OperationStatus.FAILED,
620
+ OperationStatus.CANCELLED,
621
+ OperationStatus.INTERRUPTED,
622
+ }
623
+ and queue.empty()
624
+ ):
625
+ if current.status == OperationStatus.FAILED:
626
+ data = json.dumps(current.error or {}, ensure_ascii=False)
627
+ yield f"event: run.failed\ndata: {data}\n\n"
628
+ break
629
+
630
+ return StreamingResponse(
631
+ render(),
632
+ media_type="text/event-stream",
633
+ headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
634
+ )
635
+
636
+ @app.post("/api/v1/agents", status_code=201)
637
+ async def create_agent(payload: CreateAgentRequest):
638
+ return studio.create_studio_agent(
639
+ agent_id=payload.id,
640
+ name=payload.name,
641
+ description=payload.description,
642
+ template=payload.template,
643
+ spec=payload.spec,
644
+ runtime=payload.runtime,
645
+ )
646
+
647
+ @app.post("/api/v1/assets/agent-avatars", status_code=201)
648
+ async def upload_agent_avatar(request: Request):
649
+ declared_size = request.headers.get("Content-Length")
650
+ if (
651
+ declared_size
652
+ and declared_size.isdigit()
653
+ and int(declared_size) > studio.avatar_assets.MAX_BYTES
654
+ ):
655
+ raise StudioError(
656
+ "AGENT_AVATAR_TOO_LARGE",
657
+ "头像文件不能超过 2 MiB",
658
+ status_code=413,
659
+ field="file",
660
+ details={"maxBytes": studio.avatar_assets.MAX_BYTES},
661
+ )
662
+ return studio.avatar_assets.store(
663
+ await request.body(),
664
+ content_type=request.headers.get("Content-Type", ""),
665
+ )
666
+
667
+ @app.get("/api/v1/assets/agent-avatars/{asset_name}")
668
+ async def get_agent_avatar(asset_name: str):
669
+ path, media_type = studio.avatar_assets.get(asset_name)
670
+ return FileResponse(
671
+ path,
672
+ media_type=media_type,
673
+ headers={"Cache-Control": "private, max-age=31536000, immutable"},
674
+ )
675
+
676
+ @app.get("/api/v1/agent-templates")
677
+ async def agent_templates():
678
+ return {"items": studio.list_agent_templates()}
679
+
680
+ @app.post("/api/v1/agent-templates/{template_id}:compose")
681
+ async def compose_agent_template(
682
+ template_id: str,
683
+ payload: AgentTemplateComposeRequest,
684
+ ):
685
+ return studio.compose_agent_template(template_id, payload)
686
+
687
+ @app.post("/api/v1/authoring/quick", status_code=201)
688
+ async def quick_author_agent(payload: QuickAuthoringRequest):
689
+ return studio.create_authored_agent(
690
+ name=payload.name,
691
+ slug=payload.slug,
692
+ runtime_type=payload.runtime_type,
693
+ template=payload.template,
694
+ description=payload.description,
695
+ spec=payload.spec,
696
+ )
697
+
698
+ @app.post("/api/v1/authoring/conversations:compose")
699
+ async def compose_authoring_conversation(payload: ConversationAuthoringRequest):
700
+ return await studio.compose_agent_conversation(
701
+ messages=[item.model_dump(mode="json") for item in payload.messages],
702
+ model_profile_id=payload.model_profile_id,
703
+ runtime_type=payload.runtime_type,
704
+ agent_model_profile_ids=payload.agent_model_profile_ids,
705
+ agent_default_model_profile_id=payload.agent_default_model_profile_id,
706
+ tool_resource_ids=payload.tool_resource_ids,
707
+ mcp_resource_ids=payload.mcp_resource_ids,
708
+ skill_resource_ids=payload.skill_resource_ids,
709
+ request_id=payload.request_id,
710
+ )
711
+
712
+ @app.get("/api/v1/authoring/conversations:status/{request_id}")
713
+ async def get_authoring_conversation_status(request_id: str):
714
+ status = studio.conversation_authoring_status(request_id)
715
+ if status is None:
716
+ raise StudioError(
717
+ "AUTHORING_STATUS_NOT_FOUND",
718
+ "未找到该构建请求的阶段记录",
719
+ status_code=404,
720
+ )
721
+ return status
722
+
723
+ @app.post("/api/v1/authoring/imports:inspect")
724
+ async def inspect_agent_import(file: UploadFile = File(...)):
725
+ content = await file.read(100 * 1024 * 1024 + 1)
726
+ return studio.inspect_agent_import(
727
+ content,
728
+ filename=file.filename or "agent.yaml",
729
+ )
730
+
731
+ @app.post(
732
+ "/api/v1/authoring/imports/{inspection_token}:commit",
733
+ status_code=201,
734
+ )
735
+ async def commit_agent_import(
736
+ inspection_token: str,
737
+ payload: AuthoringCommitRequest,
738
+ ):
739
+ return studio.commit_agent_import(
740
+ inspection_token,
741
+ name=payload.name,
742
+ slug=payload.slug,
743
+ )
744
+
745
+ @app.post("/api/v1/authoring/projects:inspect")
746
+ async def inspect_agent_project(payload: ProjectInspectRequest):
747
+ return studio.inspect_agent_project(payload.path)
748
+
749
+ @app.post(
750
+ "/api/v1/authoring/projects/{inspection_token}:commit",
751
+ status_code=201,
752
+ )
753
+ async def commit_agent_project(
754
+ inspection_token: str,
755
+ payload: AuthoringCommitRequest,
756
+ ):
757
+ return studio.commit_agent_project(
758
+ inspection_token,
759
+ name=payload.name,
760
+ slug=payload.slug,
761
+ model_profile_id=payload.model_profile_id,
762
+ )
763
+
764
+ @app.get("/api/v1/agents")
765
+ async def list_agents(
766
+ limit: int = Query(default=50, ge=1, le=200),
767
+ query: str = "",
768
+ ):
769
+ return {
770
+ "items": studio.list_agents(query=query, limit=limit),
771
+ "nextCursor": None,
772
+ }
773
+
774
+ @app.get("/api/v1/agents/{agent_id}")
775
+ async def get_agent(agent_id: str):
776
+ return studio.agent_detail(agent_id)
777
+
778
+ @app.get("/api/v1/agents/{agent_id}/models")
779
+ async def get_agent_models(agent_id: str):
780
+ return shared_web.list_models(shared_web.resolve_agent_id(agent_id))
781
+
782
+ @app.put("/api/v1/agents/{agent_id}")
783
+ async def update_agent(
784
+ agent_id: str,
785
+ spec: AgentSpec,
786
+ if_match: str | None = Header(default=None, alias="If-Match"),
787
+ name: str | None = Query(default=None, min_length=1, max_length=128),
788
+ ):
789
+ if not if_match:
790
+ raise StudioError(
791
+ "AGENT_REVISION_REQUIRED",
792
+ "更新 Agent 必须提供 If-Match revision",
793
+ status_code=428,
794
+ )
795
+ try:
796
+ revision = int(if_match.strip().strip('"'))
797
+ except ValueError as exc:
798
+ raise StudioError(
799
+ "AGENT_REVISION_INVALID",
800
+ "If-Match 必须是整数 revision",
801
+ status_code=400,
802
+ ) from exc
803
+ return studio.update_studio_agent(
804
+ agent_id,
805
+ spec,
806
+ expected_revision=revision,
807
+ name=name,
808
+ )
809
+
810
+ @app.put("/api/v1/agents/{agent_id}/bindings")
811
+ async def update_agent_bindings(
812
+ agent_id: str,
813
+ bindings: AgentBindings,
814
+ if_match: str | None = Header(default=None, alias="If-Match"),
815
+ ):
816
+ revision = _parse_revision(if_match)
817
+ return studio.update_studio_agent_bindings(
818
+ agent_id,
819
+ bindings,
820
+ expected_revision=revision,
821
+ )
822
+
823
+ @app.put("/api/v1/agents/{agent_id}/appearance")
824
+ async def update_agent_appearance(
825
+ agent_id: str,
826
+ appearance: AgentAppearance,
827
+ if_match: str | None = Header(default=None, alias="If-Match"),
828
+ ):
829
+ return studio.update_studio_agent_appearance(
830
+ agent_id,
831
+ appearance,
832
+ expected_revision=_parse_revision(if_match),
833
+ )
834
+
835
+ @app.delete("/api/v1/agents/{agent_id}", status_code=204)
836
+ async def delete_agent(agent_id: str, purge: bool = False):
837
+ studio.delete_studio_agent(agent_id, purge=purge)
838
+ return Response(status_code=204)
839
+
840
+ @app.post("/api/v1/agents/{agent_id}/validations")
841
+ async def validate_agent(agent_id: str, payload: ValidationRequest):
842
+ return studio.validate_studio_agent(
843
+ agent_id,
844
+ revision=payload.revision,
845
+ level=payload.level,
846
+ )
847
+
848
+ @app.post("/api/v1/workspace:import-root", status_code=201)
849
+ async def import_root_project(payload: ImportRootRequest):
850
+ """PR-S6:一键导入根 Framework 项目(方案 §6.1)。"""
851
+ return studio.import_root_project(name=payload.name, slug=payload.slug)
852
+
853
+ @app.post("/api/v1/agents/{agent_id}/prompt:compile")
854
+ async def compile_prompt(agent_id: str, payload: PromptCompileRequest):
855
+ """PR-S2:Prompt 编译预览(方案 §6.2)。只读,不写 Session/Trace/Build。"""
856
+ return studio.compile_prompt_preview(
857
+ agent_id,
858
+ request_instructions=payload.request_instructions,
859
+ include_content=payload.include_content,
860
+ )
861
+
862
+ @app.post("/api/v1/agents/{agent_id}/context:preview")
863
+ async def preview_context(agent_id: str, payload: ContextPreviewRequest):
864
+ """PR-S2:Context 预览(方案 §6.2)。复用真实 Planner,不调模型。"""
865
+ return await studio.preview_context(
866
+ agent_id,
867
+ user_input=payload.user_input,
868
+ request_instructions=payload.request_instructions,
869
+ simulated_history=[
870
+ {"role": m.role, "content": m.content} for m in payload.simulated_history
871
+ ],
872
+ include_content=payload.include_content,
873
+ )
874
+
875
+ @app.post("/api/v1/agents/{agent_id}/builds", status_code=202)
876
+ async def create_build(
877
+ agent_id: str,
878
+ payload: BuildRequest,
879
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
880
+ ):
881
+ return studio.submit_studio_build(
882
+ agent_id,
883
+ revision=payload.revision,
884
+ idempotency_key=_require_idempotency_key(idempotency_key),
885
+ )
886
+
887
+ @app.get("/api/v1/builds/{build_id}")
888
+ async def get_build(build_id: str):
889
+ return studio.build_view(build_id)
890
+
891
+ @app.get("/api/v1/builds/{build_id}/manifest")
892
+ async def get_build_manifest(build_id: str):
893
+ try:
894
+ record = studio.codex_builds.get(build_id)
895
+ except StudioError as exc:
896
+ if exc.status_code != 404:
897
+ raise
898
+ else:
899
+ return {
900
+ "agentengine": studio.codex_manifests.load(record.agent_name).manifest,
901
+ "runtimeLock": record.runtime_lock,
902
+ "manifestSha256": record.manifest_sha256,
903
+ }
904
+ build = studio.builds.get(build_id)
905
+ if not build.artifact_path:
906
+ raise StudioError("BUILD_NOT_READY", "Build 尚未生成制品", status_code=409)
907
+ archive = studio.workspace.resolve(build.artifact_path)
908
+ manifest = archive.parent / "agent-bundle" / "manifest.json"
909
+ return json.loads(manifest.read_text(encoding="utf-8"))
910
+
911
+ @app.post("/api/v1/builds/{build_id}/runs", status_code=202)
912
+ async def create_run(
913
+ build_id: str,
914
+ payload: RunRequest,
915
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
916
+ ):
917
+ if payload.environment != "local":
918
+ raise StudioError(
919
+ "RUN_ENVIRONMENT_UNSUPPORTED",
920
+ "一期 Local Run 只支持 environment=local",
921
+ status_code=422,
922
+ )
923
+ return studio.submit_studio_run(
924
+ build_id,
925
+ payload.input.content,
926
+ session_id=payload.session_id,
927
+ model=payload.model,
928
+ idempotency_key=_require_idempotency_key(idempotency_key),
929
+ )
930
+
931
+ @app.get("/api/v1/runs/{run_id}")
932
+ async def get_run(run_id: str):
933
+ return studio.event_store.get(run_id)
934
+
935
+ @app.post("/api/v1/runs/{run_id}:cancel", status_code=202)
936
+ async def cancel_run(run_id: str):
937
+ return await studio.run_service.cancel_run(run_id)
938
+
939
+ @app.post("/api/v1/runs/{run_id}:pause", status_code=202)
940
+ async def pause_run(run_id: str):
941
+ return await studio.run_service.pause_run(run_id)
942
+
943
+ @app.post("/api/v1/runs/{run_id}:resume", status_code=202)
944
+ async def resume_run(run_id: str):
945
+ return await studio.run_service.resume_run(run_id)
946
+
947
+ @app.post("/api/v1/runs/{run_id}/interactions/{interaction_id}:submit")
948
+ async def submit_run_interaction(
949
+ run_id: str,
950
+ interaction_id: str,
951
+ payload: InteractionSubmitRequest,
952
+ ):
953
+ return await studio.run_service.submit_interaction(
954
+ run_id,
955
+ interaction_id,
956
+ name=payload.name,
957
+ data=payload.data,
958
+ )
959
+
960
+ @app.get("/api/v1/runs/{run_id}/context")
961
+ async def get_run_context(run_id: str):
962
+ """Runtime Context Evidence:planned/projected/actual + 精度 + ownership。"""
963
+ record = studio.event_store.get(run_id)
964
+ plan = record.context_plan or {}
965
+ evidence = record.prompt_evidence or {}
966
+ return {
967
+ "planId": plan.get("plan_id"),
968
+ "accuracy": evidence.get("accountingAccuracy")
969
+ or plan.get("accounting_accuracy", "opaque"),
970
+ "policyVersion": plan.get("policy_version"),
971
+ "tokensByKind": plan.get("tokens_by_kind", {}),
972
+ "plannedInputTokens": plan.get("planned_input_tokens"),
973
+ "projectedInputTokens": plan.get("projected_input_tokens"),
974
+ "runtimeReportedInputTokens": plan.get("runtime_reported_input_tokens"),
975
+ "selected": plan.get("selected", []),
976
+ "decisions": plan.get("decisions", []),
977
+ "ownership": {
978
+ "promptOwner": evidence.get("promptOwner"),
979
+ "historyOwner": (plan.get("history_owner") if isinstance(plan, dict) else None),
980
+ "integrationMode": evidence.get("integrationMode"),
981
+ "runtimeType": evidence.get("runtimeType"),
982
+ "deploymentMode": evidence.get("deploymentMode"),
983
+ "capabilityHash": evidence.get("capabilityHash"),
984
+ },
985
+ "warnings": [],
986
+ }
987
+
988
+ @app.get("/api/v1/runs/{run_id}/prompt")
989
+ async def get_run_prompt(run_id: str, include_content: bool = Query(default=False)):
990
+ """PR-S4:Prompt evidence(方案 §6.3 / §7.3)。section hash/版本,默认不返回正文。"""
991
+ record = studio.event_store.get(run_id)
992
+ evidence = record.prompt_evidence or {}
993
+ result = {
994
+ "contentHash": evidence.get("contentHash"),
995
+ "stablePrefixHash": evidence.get("stablePrefixHash"),
996
+ "sectionHashes": evidence.get("sectionHashes", {}),
997
+ "tokensBySection": evidence.get("tokensBySection", {}),
998
+ "estimatedTokens": evidence.get("estimatedTokens"),
999
+ "sectionCount": evidence.get("sectionCount"),
1000
+ "plannedInputTokens": evidence.get("plannedInputTokens"),
1001
+ "accountingAccuracy": evidence.get("accountingAccuracy"),
1002
+ "runtimeType": evidence.get("runtimeType"),
1003
+ "integrationMode": evidence.get("integrationMode"),
1004
+ }
1005
+ if include_content:
1006
+ result["reveal"] = studio.reveal_run_prompt(run_id)
1007
+ return result
1008
+
1009
+ @app.get("/api/v1/runs/{run_id}/working-state")
1010
+ async def get_run_working_state(run_id: str):
1011
+ """PR-S4:Working State evidence(方案 §6.5)。从 checkpoint/read record 读取。"""
1012
+ record = studio.event_store.get(run_id)
1013
+ return {"workingState": record.working_state}
1014
+
1015
+ @app.delete("/api/v1/sessions/{session_id}", status_code=204)
1016
+ async def delete_studio_session(session_id: str):
1017
+ await studio.delete_session(session_id)
1018
+ return Response(status_code=204)
1019
+
1020
+ @app.get("/api/v1/sessions/{session_id}/events")
1021
+ async def session_events(
1022
+ session_id: str,
1023
+ before_seq_id: int | None = Query(default=None, ge=1, alias="beforeSeqId"),
1024
+ invocation_id: str | None = Query(default=None, alias="invocationId"),
1025
+ limit: int = Query(default=100, ge=1, le=500),
1026
+ ):
1027
+ return await studio.trajectory_page(
1028
+ session_id,
1029
+ before_seq_id=before_seq_id,
1030
+ invocation_id=invocation_id,
1031
+ limit=limit,
1032
+ )
1033
+
1034
+ @app.get("/api/v1/sessions/{session_id}/events/stream")
1035
+ async def session_event_stream(
1036
+ session_id: str,
1037
+ request: Request,
1038
+ after_seq_id: int = Query(default=0, ge=0, alias="afterSeqId"),
1039
+ invocation_id: str | None = Query(default=None, alias="invocationId"),
1040
+ ):
1041
+ await studio._require_runtime_session(session_id)
1042
+ last = request.headers.get("Last-Event-ID")
1043
+ cursor = int(last) if last and last.isdigit() else after_seq_id
1044
+ stream = studio.stream_trajectory(
1045
+ session_id,
1046
+ cursor,
1047
+ invocation_id=invocation_id,
1048
+ )
1049
+
1050
+ async def frames():
1051
+ try:
1052
+ async for frame in stream:
1053
+ if await request.is_disconnected():
1054
+ return
1055
+ yield frame
1056
+ finally:
1057
+ await stream.aclose()
1058
+
1059
+ return StreamingResponse(
1060
+ frames(),
1061
+ media_type="text/event-stream",
1062
+ headers={
1063
+ "Cache-Control": "no-store",
1064
+ "X-Accel-Buffering": "no",
1065
+ },
1066
+ )
1067
+
1068
+ @app.post("/api/v1/sessions/{session_id}:export")
1069
+ async def export_session(session_id: str, payload: dict[str, Any]):
1070
+ filename = payload.get("filename")
1071
+ invocation_id = payload.get("invocationId")
1072
+ download = payload.get("download", False)
1073
+ if not isinstance(filename, str):
1074
+ raise StudioError(
1075
+ "SESSION_EXPORT_FILENAME_INVALID",
1076
+ "filename 必须是字符串",
1077
+ status_code=422,
1078
+ field="filename",
1079
+ )
1080
+ if invocation_id is not None and not isinstance(invocation_id, str):
1081
+ raise StudioError(
1082
+ "SESSION_EXPORT_INVOCATION_INVALID",
1083
+ "invocationId 必须是字符串",
1084
+ status_code=422,
1085
+ field="invocationId",
1086
+ )
1087
+ if not isinstance(download, bool):
1088
+ raise StudioError(
1089
+ "SESSION_EXPORT_DOWNLOAD_INVALID",
1090
+ "download 必须是布尔值",
1091
+ status_code=422,
1092
+ field="download",
1093
+ )
1094
+ result = await studio.export_runtime_session(
1095
+ session_id,
1096
+ filename=filename,
1097
+ invocation_id=invocation_id,
1098
+ )
1099
+ if not download:
1100
+ return result
1101
+
1102
+ path = studio.workspace.resolve(result["path"])
1103
+ return FileResponse(
1104
+ path,
1105
+ filename=filename,
1106
+ media_type="application/x-ndjson",
1107
+ headers={"X-Session-Event-Count": str(result["eventCount"])},
1108
+ background=BackgroundTask(path.unlink, missing_ok=True),
1109
+ )
1110
+
1111
+ @app.get("/api/v1/runs")
1112
+ async def list_runs(session_id: str | None = Query(default=None, alias="sessionId")):
1113
+ return {"items": studio.event_store.list_runs(session_id=session_id)}
1114
+
1115
+ @app.get("/api/v1/runs/{run_id}/events")
1116
+ async def run_events(
1117
+ run_id: str,
1118
+ request: Request,
1119
+ after: int = Query(default=0, ge=0),
1120
+ ):
1121
+ last = request.headers.get("Last-Event-ID")
1122
+ cursor = int(last) if last and last.isdigit() else after
1123
+ events = await studio.run_service.events(run_id, after=cursor)
1124
+ return _sse(events)
1125
+
1126
+ @app.get("/api/v1/traces/overview")
1127
+ async def trace_overview(
1128
+ range_name: Literal["24h", "7d"] = Query(default="24h", alias="range"),
1129
+ agent_id: str | None = Query(default=None, alias="agentId"),
1130
+ status: str | None = Query(default=None),
1131
+ ):
1132
+ return studio.event_store.trace_overview(
1133
+ range_name=range_name,
1134
+ agent_id=agent_id,
1135
+ status=status,
1136
+ )
1137
+
1138
+ @app.get("/api/v1/traces")
1139
+ async def list_traces(
1140
+ agent_id: str | None = Query(default=None, alias="agentId"),
1141
+ status: str | None = Query(default=None),
1142
+ query: str = Query(default=""),
1143
+ limit: int = Query(default=50, ge=1, le=1000),
1144
+ cursor: str | None = Query(default=None),
1145
+ sort: Literal["startedAt:desc", "startedAt:asc"] = "startedAt:desc",
1146
+ ):
1147
+ return studio.event_store.list_traces_page(
1148
+ agent_id=agent_id,
1149
+ status=status,
1150
+ query=query,
1151
+ limit=limit,
1152
+ cursor=cursor,
1153
+ sort=sort,
1154
+ )
1155
+
1156
+ @app.get("/api/v1/traces/{trace_id}")
1157
+ async def get_trace(trace_id: str):
1158
+ return studio.event_store.trace(trace_id)
1159
+
1160
+ @app.get("/api/v1/traces/{trace_id}/otlp")
1161
+ async def get_trace_otlp(trace_id: str):
1162
+ return studio.event_store.trace_otlp(trace_id)
1163
+
1164
+ @app.post("/api/v1/evaluations", status_code=202)
1165
+ async def create_public_evaluation(
1166
+ payload: StudioEvaluationCreate,
1167
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
1168
+ ):
1169
+ return studio.submit_public_evaluation(
1170
+ payload.evalset_file,
1171
+ payload.target,
1172
+ payload.config,
1173
+ idempotency_key=_require_idempotency_key(idempotency_key),
1174
+ )
1175
+
1176
+ @app.post("/api/v1/evaluation-files", status_code=201)
1177
+ async def import_evaluation_file(file: UploadFile = File(...)):
1178
+ return studio.import_evaluation_file(
1179
+ await file.read(2 * 1024 * 1024 + 1),
1180
+ filename=file.filename or "evalset.yaml",
1181
+ )
1182
+
1183
+ @app.get("/api/v1/evaluations")
1184
+ async def list_public_evaluations():
1185
+ return {"items": studio.list_public_evaluations()}
1186
+
1187
+ @app.get("/api/v1/evaluation-runs")
1188
+ async def list_public_evaluation_runs():
1189
+ return {"items": studio.list_public_evaluation_runs()}
1190
+
1191
+ @app.get("/api/v1/evaluation-runs/{evaluation_id}")
1192
+ async def get_public_evaluation_run(evaluation_id: str):
1193
+ return studio.get_public_evaluation_run(evaluation_id)
1194
+
1195
+ @app.get("/api/v1/evaluation-targets")
1196
+ async def list_evaluation_targets():
1197
+ return studio.evaluation_catalog()
1198
+
1199
+ @app.get("/api/v1/evaluation-cloud/catalog")
1200
+ async def list_evaluation_cloud_catalog(
1201
+ project_id: str | None = Query(default=None, alias="projectId"),
1202
+ ):
1203
+ items = await studio.evaluation_cloud_catalog(project_id=project_id)
1204
+ return {"items": items}
1205
+
1206
+ @app.get("/api/v1/evaluations/{evaluation_id}")
1207
+ async def get_evaluation(evaluation_id: str):
1208
+ return studio.get_public_evaluation(evaluation_id)
1209
+
1210
+ @app.get("/api/v1/evaluations/{evaluation_id}/cases/{case_id}")
1211
+ async def get_public_evaluation_case(evaluation_id: str, case_id: str):
1212
+ report = studio.get_public_evaluation(evaluation_id)
1213
+ for case_run in report.case_runs:
1214
+ if case_run.case_id == case_id:
1215
+ return case_run
1216
+ raise StudioError(
1217
+ "EVALUATION_CASE_NOT_FOUND",
1218
+ "Evaluation Case 不存在",
1219
+ status_code=404,
1220
+ details={"id": case_id, "evaluationId": evaluation_id},
1221
+ )
1222
+
1223
+ @app.post("/api/v1/builds/{build_id}/deployments", status_code=202)
1224
+ async def create_deployment(
1225
+ build_id: str,
1226
+ payload: DeploymentRequest,
1227
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
1228
+ ):
1229
+ return studio.submit_deployment(
1230
+ build_id,
1231
+ payload,
1232
+ idempotency_key=_require_idempotency_key(idempotency_key),
1233
+ )
1234
+
1235
+ @app.get("/api/v1/deployments")
1236
+ async def list_deployments():
1237
+ """Read local deployment receipts without implicit cloud refreshes."""
1238
+
1239
+ return {"items": studio.cloud.list()}
1240
+
1241
+ @app.get("/api/v1/cloud-agents")
1242
+ async def list_account_cloud_agents(
1243
+ page: int = Query(default=1, ge=1),
1244
+ size: int = Query(default=100, ge=1, le=100),
1245
+ ):
1246
+ """List Agents visible to Studio's configured signed cloud account."""
1247
+
1248
+ return await studio.cloud.list_account_agents(page=page, size=size)
1249
+
1250
+ @app.get("/api/v1/cloud-agents/{agent_id}")
1251
+ async def get_account_cloud_agent(agent_id: str):
1252
+ return await studio.cloud.get_account_agent(agent_id)
1253
+
1254
+ @app.get("/api/v1/cloud-agents/{agent_id}/versions")
1255
+ async def list_account_cloud_agent_versions(
1256
+ agent_id: str,
1257
+ page: int = Query(default=1, ge=1),
1258
+ size: int = Query(default=100, ge=1, le=100),
1259
+ ):
1260
+ """List the Server-owned version history and rollback eligibility."""
1261
+
1262
+ return await studio.cloud.list_account_agent_versions(
1263
+ agent_id,
1264
+ page=page,
1265
+ size=size,
1266
+ )
1267
+
1268
+ @app.post(
1269
+ "/api/v1/cloud-agents/{agent_id}:rollback-version",
1270
+ status_code=202,
1271
+ )
1272
+ async def rollback_account_cloud_agent_version(
1273
+ agent_id: str,
1274
+ payload: CloudAgentVersionRollbackRequest,
1275
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
1276
+ ):
1277
+ """Submit Server RollbackVersion through Studio's process-only AK/SK."""
1278
+
1279
+ return studio.submit_account_agent_version_rollback(
1280
+ agent_id,
1281
+ version_id=payload.version_id,
1282
+ idempotency_key=_require_idempotency_key(idempotency_key),
1283
+ )
1284
+
1285
+ @app.post("/api/v1/cloud-agents/{agent_id}:dashboard")
1286
+ async def open_account_cloud_agent_dashboard(agent_id: str):
1287
+ return await studio.cloud.account_agent_dashboard_access(agent_id)
1288
+
1289
+ @app.delete("/api/v1/cloud-agents/{agent_id}")
1290
+ async def delete_account_cloud_agent(agent_id: str):
1291
+ return await studio.cloud.delete_account_agent(agent_id)
1292
+
1293
+ @app.get("/api/v1/deployments/{deployment_id}")
1294
+ async def get_deployment(deployment_id: str):
1295
+ return await studio.cloud.refresh(deployment_id)
1296
+
1297
+ @app.post("/api/v1/deployments/{deployment_id}:dashboard")
1298
+ async def open_deployment_dashboard(deployment_id: str):
1299
+ return await studio.deployment_dashboard_access(deployment_id)
1300
+
1301
+ @app.delete("/api/v1/deployments/{deployment_id}")
1302
+ async def delete_deployment(deployment_id: str):
1303
+ """Delete the receipt-bound cloud Agent and its superseded local receipts."""
1304
+
1305
+ return await studio.cloud.delete(deployment_id)
1306
+
1307
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/sessions")
1308
+ async def list_cloud_chat_sessions(
1309
+ deployment_id: str,
1310
+ page: int = Query(default=1, ge=1),
1311
+ size: int = Query(default=50, ge=1, le=100),
1312
+ ):
1313
+ """List Server-owned sessions for this local deployment receipt only."""
1314
+
1315
+ return await studio.cloud.list_cloud_chat_sessions(
1316
+ deployment_id, page=page, size=size
1317
+ )
1318
+
1319
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/models")
1320
+ async def list_cloud_chat_models(deployment_id: str):
1321
+ """List models through Studio's signed Server client."""
1322
+
1323
+ return await studio.cloud.list_cloud_chat_models(deployment_id)
1324
+
1325
+ @app.post(
1326
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions",
1327
+ status_code=201,
1328
+ )
1329
+ async def create_cloud_chat_session(deployment_id: str):
1330
+ """Create a cloud session via loopback-held AK/SK; no secret reaches JS."""
1331
+
1332
+ return await studio.cloud.create_cloud_chat_session(deployment_id)
1333
+
1334
+ @app.get(
1335
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/messages"
1336
+ )
1337
+ async def list_cloud_chat_messages(
1338
+ deployment_id: str,
1339
+ session_id: str,
1340
+ after_seq_id: int | None = Query(default=None, alias="afterSeqId", ge=0),
1341
+ limit: int = Query(default=100, ge=1, le=200),
1342
+ ):
1343
+ return await studio.cloud.list_cloud_chat_messages(
1344
+ deployment_id,
1345
+ session_id=session_id,
1346
+ after_seq_id=after_seq_id,
1347
+ limit=limit,
1348
+ )
1349
+
1350
+ @app.get(
1351
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/events"
1352
+ )
1353
+ async def list_cloud_chat_events(
1354
+ deployment_id: str,
1355
+ session_id: str,
1356
+ after_seq_id: int | None = Query(default=None, alias="afterSeqId", ge=0),
1357
+ limit: int = Query(default=200, ge=1, le=1000),
1358
+ ):
1359
+ return await studio.cloud.list_cloud_chat_events(
1360
+ deployment_id,
1361
+ session_id=session_id,
1362
+ after_seq_id=after_seq_id,
1363
+ limit=limit,
1364
+ )
1365
+
1366
+ @app.get(
1367
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/events/stream"
1368
+ )
1369
+ async def stream_cloud_chat_events(
1370
+ request: Request,
1371
+ deployment_id: str,
1372
+ session_id: str,
1373
+ after_seq_id: int = Query(default=0, alias="afterSeqId", ge=0),
1374
+ ):
1375
+ """Stream canonical cloud events to the loopback browser.
1376
+
1377
+ The public cloud control plane currently exposes cursor reads for this
1378
+ surface. Keep that cursor in the Studio backend and present one SSE
1379
+ response to the browser, so assistant deltas arrive before the durable
1380
+ terminal message projection without exposing cloud credentials to JS.
1381
+ """
1382
+
1383
+ async def event_stream() -> AsyncIterator[str]:
1384
+ cursor = after_seq_id
1385
+ idle_polls = 0
1386
+ while not await request.is_disconnected():
1387
+ payload = await studio.cloud.list_cloud_chat_events(
1388
+ deployment_id,
1389
+ session_id=session_id,
1390
+ after_seq_id=cursor,
1391
+ limit=200,
1392
+ )
1393
+ events = payload.get("events") or []
1394
+ if not isinstance(events, list):
1395
+ events = []
1396
+ terminal = False
1397
+ emitted = False
1398
+ for event in events:
1399
+ if not isinstance(event, dict):
1400
+ continue
1401
+ event_payload = (
1402
+ event.get("payload")
1403
+ if isinstance(event.get("payload"), dict)
1404
+ else event
1405
+ )
1406
+ seq = int(
1407
+ event_payload.get("seq")
1408
+ or event_payload.get("seq_id")
1409
+ or event_payload.get("source_session_seq")
1410
+ or event.get("seq")
1411
+ or event.get("seq_id")
1412
+ or 0
1413
+ )
1414
+ if seq and seq <= cursor:
1415
+ continue
1416
+ if seq:
1417
+ cursor = max(cursor, seq)
1418
+ event_type = str(
1419
+ event.get("event_type")
1420
+ or event.get("eventType")
1421
+ or event_payload.get("event_type")
1422
+ or event_payload.get("eventType")
1423
+ or ""
1424
+ ).lower()
1425
+ content = (
1426
+ event_payload.get("content")
1427
+ if isinstance(event_payload.get("content"), dict)
1428
+ else {}
1429
+ )
1430
+ status = str(
1431
+ event_payload.get("status") or content.get("status") or ""
1432
+ ).lower()
1433
+ event_is_terminal = event_type in {
1434
+ "run.completed", "run.complete", "run.succeeded",
1435
+ "run.failed", "run.cancelled", "run.expired", "run.error",
1436
+ } or (
1437
+ event_type in {"run_status", "run.status"}
1438
+ and status in {
1439
+ "completed", "complete", "succeeded", "success",
1440
+ "failed", "cancelled", "canceled", "expired", "error", "aborted",
1441
+ }
1442
+ )
1443
+ terminal = terminal or event_is_terminal
1444
+ emitted = True
1445
+ yield (
1446
+ (f"id: {seq}\n" if seq else "")
1447
+ + "event: session.event\n"
1448
+ + f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
1449
+ )
1450
+ if terminal or payload.get("session_deleted"):
1451
+ break
1452
+ idle_polls = 0 if emitted else idle_polls + 1
1453
+ if idle_polls and idle_polls % 20 == 0:
1454
+ yield ": keepalive\n\n"
1455
+ await asyncio.sleep(0.25)
1456
+
1457
+ return StreamingResponse(
1458
+ event_stream(),
1459
+ media_type="text/event-stream",
1460
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
1461
+ )
1462
+
1463
+ @app.delete(
1464
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}",
1465
+ status_code=204,
1466
+ )
1467
+ async def delete_cloud_chat_session(deployment_id: str, session_id: str):
1468
+ await studio.cloud.delete_cloud_chat_session(
1469
+ deployment_id, session_id=session_id
1470
+ )
1471
+ return Response(status_code=204)
1472
+
1473
+ @app.post(
1474
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/messages",
1475
+ status_code=202,
1476
+ )
1477
+ async def send_cloud_chat_message(
1478
+ deployment_id: str,
1479
+ session_id: str,
1480
+ payload: CloudChatMessageRequest,
1481
+ ):
1482
+ """Admit one cloud message through Server; response is a durable receipt."""
1483
+
1484
+ return await studio.cloud.send_cloud_chat_message(
1485
+ deployment_id,
1486
+ session_id=session_id,
1487
+ content=payload.content,
1488
+ model=payload.model,
1489
+ model_options=payload.model_options,
1490
+ tool_approval_mode=payload.tool_approval_mode,
1491
+ collaboration_mode=payload.collaboration_mode,
1492
+ goal_objective=payload.goal_objective,
1493
+ )
1494
+
1495
+ @app.post(
1496
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/messages/stream"
1497
+ )
1498
+ async def stream_cloud_chat_message(
1499
+ request: Request,
1500
+ deployment_id: str,
1501
+ session_id: str,
1502
+ payload: CloudChatMessageRequest,
1503
+ ):
1504
+ """Proxy one signed foreground RunAgent SSE response to loopback UI."""
1505
+
1506
+ upstream = await studio.cloud.stream_cloud_chat_message(
1507
+ deployment_id,
1508
+ session_id=session_id,
1509
+ content=payload.content,
1510
+ model=payload.model,
1511
+ model_options=payload.model_options,
1512
+ tool_approval_mode=payload.tool_approval_mode,
1513
+ collaboration_mode=payload.collaboration_mode,
1514
+ goal_objective=payload.goal_objective,
1515
+ )
1516
+
1517
+ async def proxy_stream() -> AsyncIterator[bytes]:
1518
+ try:
1519
+ while not await request.is_disconnected():
1520
+ try:
1521
+ chunk = await anext(upstream)
1522
+ except StopAsyncIteration:
1523
+ break
1524
+ if await request.is_disconnected():
1525
+ break
1526
+ yield chunk
1527
+ finally:
1528
+ close = getattr(upstream, "aclose", None)
1529
+ if close is not None:
1530
+ await close()
1531
+
1532
+ return StreamingResponse(
1533
+ proxy_stream(),
1534
+ media_type="text/event-stream",
1535
+ headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
1536
+ )
1537
+
1538
+ @app.post(
1539
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/interactions",
1540
+ status_code=202,
1541
+ )
1542
+ async def submit_cloud_chat_interaction(
1543
+ deployment_id: str,
1544
+ session_id: str,
1545
+ payload: CloudChatInteractionSubmitRequest,
1546
+ ):
1547
+ return await studio.cloud.submit_cloud_chat_interaction(
1548
+ deployment_id,
1549
+ session_id=session_id,
1550
+ run_id=payload.run_id,
1551
+ interaction_id=payload.interaction_id,
1552
+ expected_revision=payload.expected_revision,
1553
+ action=payload.action,
1554
+ response=payload.response,
1555
+ idempotency_key=payload.idempotency_key,
1556
+ )
1557
+
1558
+ @app.post("/api/v1/deployments/{deployment_id}:rollback", status_code=202)
1559
+ async def rollback_deployment(
1560
+ deployment_id: str,
1561
+ payload: RollbackRequest,
1562
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
1563
+ ):
1564
+ return studio.submit_rollback(
1565
+ deployment_id,
1566
+ target_build_id=payload.target_build_id,
1567
+ idempotency_key=_require_idempotency_key(idempotency_key),
1568
+ )
1569
+
1570
+ @app.get("/api/v1/operations/{operation_id}")
1571
+ async def get_operation(operation_id: str):
1572
+ return studio.operations.get(operation_id)
1573
+
1574
+ @app.post("/api/v1/operations/{operation_id}:cancel")
1575
+ async def cancel_operation(operation_id: str):
1576
+ return studio.operations.cancel(operation_id)
1577
+
1578
+ @app.get("/api/v1/operations/{operation_id}/events")
1579
+ async def operation_events(
1580
+ operation_id: str,
1581
+ request: Request,
1582
+ after: int = Query(default=0, ge=0),
1583
+ ):
1584
+ last = request.headers.get("Last-Event-ID")
1585
+ cursor = int(last) if last and last.isdigit() else after
1586
+ events = studio.operations.events(operation_id, after=cursor)
1587
+ if "application/json" in request.headers.get("Accept", ""):
1588
+ return {"items": events}
1589
+ return _sse(events)
1590
+
1591
+ register_catalog_routes(
1592
+ app,
1593
+ studio,
1594
+ runtime_model_catalog=runtime_model_catalog,
1595
+ )
1596
+ register_memory_routes(app, studio)
1597
+
1598
+ return app