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
@@ -0,0 +1,2249 @@
1
+ """Application service composing Studio modules behind one local API boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import logging
8
+ import os
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Literal, cast
12
+ from urllib.parse import urlparse
13
+ from uuid import uuid4
14
+
15
+ from ksadk.api import AgentEngineClient
16
+ from ksadk.evaluation import (
17
+ EvaluationConfig as PublicEvaluationConfig,
18
+ )
19
+ from ksadk.evaluation import (
20
+ EvaluationExecutionError,
21
+ EvaluationNotImplementedError,
22
+ EvaluationStorage,
23
+ EvaluationStorageError,
24
+ TargetKind,
25
+ TargetRef,
26
+ execute_evaluation,
27
+ load_evalset,
28
+ )
29
+ from ksadk.evaluation import (
30
+ EvaluationRequest as PublicEvaluationRequest,
31
+ )
32
+ from ksadk.evaluation.evalset import EvalSetParseError
33
+ from ksadk.evaluation.evidence import EvidenceStore
34
+ from ksadk.evaluation.studio_build_adapter import (
35
+ StudioBuildResolution,
36
+ StudioBuildTargetAdapter,
37
+ StudioBuildTargetError,
38
+ )
39
+ from ksadk.events.store import RuntimeEventStore
40
+ from ksadk.observability.session_log import SessionLogError, export_session_log
41
+ from ksadk.observability.trajectory import encode_sse, project_trajectory_event
42
+ from ksadk.runtime import RuntimeExecutor, build_default_runtime_registry
43
+ from ksadk.sessions.local_service import LocalSessionService
44
+ from ksadk.studio.agent_avatar_assets import AgentAvatarAssetStore
45
+ from ksadk.studio.agent_lifecycle import delete_framework_agent
46
+ from ksadk.studio.authoring_coordinator import StudioAuthoringCoordinator
47
+ from ksadk.studio.builder import AgentBundleBuilder
48
+ from ksadk.studio.capabilities import builtin_tool_contracts
49
+ from ksadk.studio.cloud import (
50
+ CloudDeploymentGateway,
51
+ CloudDeploymentService,
52
+ DirectAgentEngineCloudDeploymentGateway,
53
+ UnavailableCloudGateway,
54
+ )
55
+ from ksadk.studio.codex_agent_service import CodexAgentService, CodexDraftRepository
56
+ from ksadk.studio.codex_builder import (
57
+ CodexBuildRecord,
58
+ CodexBuildRepository,
59
+ CodexStudioBuilder,
60
+ RuntimeInspector,
61
+ current_proxy_mode,
62
+ normalize_proxy_mode,
63
+ proxy_mode_env_value,
64
+ )
65
+ from ksadk.studio.codex_manifest import (
66
+ CodexAgentManifest,
67
+ CodexManifestRepository,
68
+ )
69
+ from ksadk.studio.codex_run import CodexRunSpecResolver
70
+ from ksadk.studio.compiler import AgentCompiler
71
+ from ksadk.studio.contracts import (
72
+ AgentAppearance,
73
+ AgentBindings,
74
+ AgentDraft,
75
+ AgentSpec,
76
+ AgentTemplateComposeRequest,
77
+ AgentTemplateComposition,
78
+ BuildStatus,
79
+ DeploymentRequest,
80
+ Operation,
81
+ OperationKind,
82
+ RunEvent,
83
+ RunStatus,
84
+ RuntimeRef,
85
+ ToolContract,
86
+ )
87
+ from ksadk.studio.errors import StudioError
88
+ from ksadk.studio.event_store import RunEventStore
89
+ from ksadk.studio.framework_run import FrameworkRunSpecResolver
90
+ from ksadk.studio.mcp_runtime import MCPRuntimeAdapter
91
+ from ksadk.studio.model_client import CredentialResolver, OpenAICompatibleModelClient
92
+ from ksadk.studio.model_profile_service import test_model_profile_connection
93
+ from ksadk.studio.operations import OperationManager
94
+ from ksadk.studio.repository import AgentDraftRepository, BuildRepository, load_yaml_file
95
+ from ksadk.studio.resource_catalog import LocalResourceCatalog
96
+ from ksadk.studio.run_service import StudioRunService
97
+ from ksadk.studio.runtime_catalog import inspect_runtime_catalog
98
+ from ksadk.studio.runtime_source import materialize_generated_runtime_source
99
+ from ksadk.studio.templates import (
100
+ compose_blank_agent,
101
+ compose_research_agent,
102
+ default_agent_spec,
103
+ list_agent_templates,
104
+ )
105
+ from ksadk.studio.validator import AgentValidator
106
+ from ksadk.studio.workspace import Workspace
107
+
108
+ _TRAJECTORY_KEEPALIVE_SECONDS = 15.0
109
+ _EVALUATION_TARGET_LABELS = {
110
+ TargetKind.A2A: "A2A Agent",
111
+ TargetKind.LOCAL_SOURCE: "本地源码",
112
+ TargetKind.STUDIO_BUILD: "Studio Build",
113
+ TargetKind.CODEX_WORKTREE: "Codex Worktree",
114
+ }
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class _OperationResource:
119
+ id: str
120
+
121
+
122
+ class StudioService:
123
+ def __init__(
124
+ self,
125
+ root: Path | str,
126
+ *,
127
+ model_client: OpenAICompatibleModelClient | None = None,
128
+ credential_resolver: CredentialResolver | None = None,
129
+ cloud_gateway: CloudDeploymentGateway | None = None,
130
+ codex_runtime_inspector: RuntimeInspector | None = None,
131
+ runtime_executor: RuntimeExecutor | None = None,
132
+ ) -> None:
133
+ self.workspace = Workspace(root)
134
+ self.workspace.initialize()
135
+ self._apply_persisted_settings()
136
+ self.avatar_assets = AgentAvatarAssetStore(self.workspace)
137
+ self.drafts = AgentDraftRepository(self.workspace)
138
+ self.catalog = LocalResourceCatalog(self.workspace)
139
+ self.builds = BuildRepository(self.workspace)
140
+ self.validator = AgentValidator()
141
+ self.builder = AgentBundleBuilder(
142
+ self.workspace,
143
+ compiler=AgentCompiler(
144
+ self.workspace,
145
+ validator=self.validator,
146
+ catalog=self.catalog,
147
+ ),
148
+ repository=self.builds,
149
+ )
150
+ self.event_store = RunEventStore(self.workspace)
151
+ self.session_service = LocalSessionService(project_dir=str(self.workspace.root))
152
+ self.runtime_events = RuntimeEventStore(self.session_service)
153
+ self.codex_manifests = CodexManifestRepository(self.workspace)
154
+ self.codex_builds = CodexBuildRepository(self.workspace)
155
+ self.codex_drafts = CodexDraftRepository(self.workspace)
156
+ codex_builder_kwargs = {}
157
+ if codex_runtime_inspector is not None:
158
+ codex_builder_kwargs["runtime_inspector"] = codex_runtime_inspector
159
+ self.codex_builder = CodexStudioBuilder(
160
+ self.workspace,
161
+ manifest_repository=self.codex_manifests,
162
+ build_repository=self.codex_builds,
163
+ resource_catalog=self.catalog,
164
+ draft_repository=self.codex_drafts,
165
+ **codex_builder_kwargs,
166
+ )
167
+ self.runtime_executor = runtime_executor or RuntimeExecutor(
168
+ build_default_runtime_registry()
169
+ )
170
+ self.run_service = StudioRunService(
171
+ self.workspace,
172
+ self.runtime_executor,
173
+ event_store=self.event_store,
174
+ session_service=self.session_service,
175
+ runtime_events=self.runtime_events,
176
+ )
177
+ self.credentials = (
178
+ credential_resolver
179
+ or getattr(model_client, "credential_resolver", None)
180
+ or CredentialResolver(self.workspace)
181
+ )
182
+ self.codex_runs = CodexRunSpecResolver(
183
+ self.workspace,
184
+ build_repository=self.codex_builds,
185
+ manifest_repository=self.codex_manifests,
186
+ credential_resolver=self.credentials,
187
+ resource_catalog=self.catalog,
188
+ )
189
+ self.framework_runs = FrameworkRunSpecResolver(
190
+ self.workspace,
191
+ build_repository=self.builds,
192
+ )
193
+ runtime_model_client = model_client or OpenAICompatibleModelClient(
194
+ credential_resolver=self.credentials
195
+ )
196
+ self.model_client = runtime_model_client
197
+ self.mcp_runtime = MCPRuntimeAdapter(self.workspace, credentials=self.credentials)
198
+ self._cloud_gateway_override = cloud_gateway
199
+ self.cloud = CloudDeploymentService(
200
+ self.workspace,
201
+ gateway=cloud_gateway or self._configured_cloud_gateway(),
202
+ build_repository=self.builds,
203
+ )
204
+ self.operations = OperationManager(self.workspace)
205
+ self.evaluation_storage = EvaluationStorage(
206
+ self.workspace.resolve(".agentkit/evaluations")
207
+ )
208
+ self.authoring = StudioAuthoringCoordinator(self)
209
+ self.codex_agents = CodexAgentService(self)
210
+
211
+ def runtime_catalog(self) -> list[dict]:
212
+ return inspect_runtime_catalog(self.runtime_executor)
213
+
214
+ def codex_manifest_state(self, agent_id: str | None = None) -> dict:
215
+ return self.codex_agents.manifest_state(agent_id)
216
+
217
+ def save_codex_manifest(self, manifest: CodexAgentManifest) -> dict:
218
+ return self.codex_agents.save_manifest(manifest)
219
+
220
+ def list_codex_agents(self, *, query: str = "", limit: int = 50) -> list[AgentDraft]:
221
+ return self.codex_agents.list(query=query, limit=limit)
222
+
223
+ def create_codex_agent(
224
+ self,
225
+ *,
226
+ agent_id: str,
227
+ spec: AgentSpec | None,
228
+ name: str | None = None,
229
+ labels: dict[str, str] | None = None,
230
+ ) -> AgentDraft:
231
+ return self.codex_agents.create(
232
+ agent_id=agent_id,
233
+ spec=spec,
234
+ name=name,
235
+ labels=labels,
236
+ )
237
+
238
+ def update_codex_agent(
239
+ self,
240
+ agent_id: str,
241
+ spec: AgentSpec,
242
+ *,
243
+ expected_revision: int,
244
+ name: str | None = None,
245
+ ) -> AgentDraft:
246
+ return self.codex_agents.update(
247
+ agent_id,
248
+ spec,
249
+ expected_revision=expected_revision,
250
+ name=name,
251
+ )
252
+
253
+ def delete_codex_agent(self, agent_id: str, *, purge: bool = False) -> None:
254
+ self.codex_agents.delete(agent_id, purge=purge)
255
+
256
+ def codex_agent_detail(self, agent_id: str | None = None) -> dict:
257
+ return self.codex_agents.detail(agent_id)
258
+
259
+ @staticmethod
260
+ def codex_build_view(record: CodexBuildRecord) -> dict:
261
+ return CodexAgentService.build_view(record)
262
+
263
+ def submit_codex_build(
264
+ self,
265
+ *,
266
+ idempotency_key: str,
267
+ agent_id: str | None = None,
268
+ ) -> Operation:
269
+ return self.codex_agents.submit_build(
270
+ idempotency_key=idempotency_key,
271
+ agent_id=agent_id,
272
+ )
273
+
274
+ def submit_codex_run(
275
+ self,
276
+ build_id: str,
277
+ user_input: str,
278
+ *,
279
+ session_id: str | None,
280
+ model: str | None = None,
281
+ sandbox: str | None = None,
282
+ approval_mode: str | None = None,
283
+ collaboration_mode: str | None = None,
284
+ goal_objective: str | None = None,
285
+ reasoning_effort: str | None = None,
286
+ runtime_input: Any = None,
287
+ idempotency_key: str,
288
+ on_event: Callable[[RunEvent], None] | None = None,
289
+ ) -> Operation:
290
+ return self.codex_agents.submit_run(
291
+ build_id,
292
+ user_input,
293
+ session_id=session_id,
294
+ model=model,
295
+ sandbox=sandbox,
296
+ approval_mode=approval_mode,
297
+ collaboration_mode=collaboration_mode,
298
+ goal_objective=goal_objective,
299
+ reasoning_effort=reasoning_effort,
300
+ runtime_input=runtime_input,
301
+ idempotency_key=idempotency_key,
302
+ on_event=on_event,
303
+ )
304
+
305
+ async def delete_session(self, session_id: str) -> None:
306
+ from ksadk.studio.errors import not_found
307
+
308
+ runs = self.event_store.list_runs(session_id=session_id)
309
+ if not runs:
310
+ raise not_found("session", session_id)
311
+ if any(run.status == RunStatus.RUNNING for run in runs):
312
+ raise StudioError(
313
+ "SESSION_RUN_ACTIVE",
314
+ "会话仍在运行,请先停止运行后再删除",
315
+ status_code=409,
316
+ details={"sessionId": session_id},
317
+ )
318
+ await self.session_service.delete_session(session_id)
319
+ self.event_store.delete_session(session_id)
320
+
321
+ async def _require_runtime_session(self, session_id: str) -> None:
322
+ if await self.session_service.get_session_metadata(session_id) is None:
323
+ raise StudioError(
324
+ "SESSION_NOT_FOUND",
325
+ "session 不存在",
326
+ status_code=404,
327
+ details={"id": session_id},
328
+ )
329
+
330
+ async def trajectory_page(
331
+ self,
332
+ session_id: str,
333
+ *,
334
+ before_seq_id: int | None = None,
335
+ invocation_id: str | None = None,
336
+ limit: int = 100,
337
+ ) -> dict[str, Any]:
338
+ """Return the bounded, canonical RuntimeEvent trajectory for a session."""
339
+
340
+ await self._require_runtime_session(session_id)
341
+ events = await self.runtime_events.list(
342
+ session_id,
343
+ before_seq=before_seq_id,
344
+ run_id=invocation_id,
345
+ limit=limit,
346
+ )
347
+ oldest_seq_id = events[0].seq if events else None
348
+ latest_seq_id = events[-1].seq if events else None
349
+ older = (
350
+ await self.runtime_events.list(
351
+ session_id,
352
+ before_seq=oldest_seq_id,
353
+ run_id=invocation_id,
354
+ limit=1,
355
+ )
356
+ if oldest_seq_id is not None
357
+ else []
358
+ )
359
+ return {
360
+ "items": [project_trajectory_event(event) for event in events],
361
+ "page": {
362
+ "oldestSeqId": oldest_seq_id,
363
+ "latestSeqId": latest_seq_id,
364
+ "hasMore": bool(older),
365
+ },
366
+ }
367
+
368
+ async def stream_trajectory(
369
+ self,
370
+ session_id: str,
371
+ after_seq_id: int = 0,
372
+ *,
373
+ invocation_id: str | None = None,
374
+ ):
375
+ await self._require_runtime_session(session_id)
376
+ cursor = after_seq_id
377
+ while True:
378
+ async for event in self.runtime_events.subscribe_session(
379
+ session_id,
380
+ after_seq=cursor,
381
+ poll_interval=min(0.25, _TRAJECTORY_KEEPALIVE_SECONDS),
382
+ timeout=_TRAJECTORY_KEEPALIVE_SECONDS,
383
+ ):
384
+ cursor = event.seq
385
+ if invocation_id is not None and event.run_id != invocation_id:
386
+ continue
387
+ yield encode_sse(project_trajectory_event(event), event_id=cursor)
388
+ yield ": keepalive\n\n"
389
+
390
+ async def export_runtime_session(
391
+ self,
392
+ session_id: str,
393
+ *,
394
+ filename: str,
395
+ invocation_id: str | None = None,
396
+ ) -> dict[str, Any]:
397
+ await self._require_runtime_session(session_id)
398
+ relative = Path(filename)
399
+ if (
400
+ not filename
401
+ or relative.is_absolute()
402
+ or len(relative.parts) != 1
403
+ or relative.name in {".", ".."}
404
+ ):
405
+ raise StudioError(
406
+ "SESSION_EXPORT_FILENAME_INVALID",
407
+ "导出文件名必须是普通文件名",
408
+ status_code=422,
409
+ field="filename",
410
+ )
411
+ export_dir = self.workspace.resolve(".agentkit/exports")
412
+ export_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
413
+ export_dir.chmod(0o700)
414
+ try:
415
+ result = await export_session_log(
416
+ self.session_service,
417
+ session_id,
418
+ export_dir / relative.name,
419
+ invocation_id=invocation_id,
420
+ )
421
+ except SessionLogError as exc:
422
+ code = str(exc).partition(":")[0]
423
+ raise StudioError(
424
+ code,
425
+ str(exc),
426
+ status_code=409 if code == "SESSION_LOG_TARGET_EXISTS" else 422,
427
+ ) from exc
428
+ return {
429
+ "path": result.path.relative_to(self.workspace.root).as_posix(),
430
+ "eventCount": result.event_count,
431
+ "firstSeqId": result.first_seq_id,
432
+ "lastSeqId": result.last_seq_id,
433
+ "exportedThroughSeqId": result.exported_through_seq_id,
434
+ }
435
+
436
+ async def test_model_profile(self, resource_id: str) -> dict:
437
+ return await test_model_profile_connection(
438
+ catalog=self.catalog,
439
+ model_client=self.model_client,
440
+ resource_id=resource_id,
441
+ )
442
+
443
+ def create_agent(
444
+ self,
445
+ *,
446
+ agent_id: str,
447
+ name: str,
448
+ description: str = "",
449
+ template: str = "blank",
450
+ spec: AgentSpec | None = None,
451
+ labels: dict[str, str] | None = None,
452
+ ):
453
+ if self.codex_manifests.exists(agent_id):
454
+ from ksadk.studio.errors import StudioError
455
+
456
+ raise StudioError(
457
+ "AGENT_ALREADY_EXISTS",
458
+ "Agent ID 已存在",
459
+ status_code=409,
460
+ details={"id": agent_id},
461
+ )
462
+ resolved_spec = spec or default_agent_spec(
463
+ template,
464
+ description=description,
465
+ )
466
+ self._validate_bindings(resolved_spec.bindings)
467
+ self._validate_framework_binding_support(
468
+ resolved_spec.runtime,
469
+ resolved_spec.bindings,
470
+ )
471
+ draft = self.drafts.create(
472
+ agent_id=agent_id,
473
+ name=name,
474
+ description=description,
475
+ template=template,
476
+ spec=resolved_spec,
477
+ labels=labels,
478
+ )
479
+ materialize_generated_runtime_source(
480
+ self.workspace,
481
+ draft,
482
+ catalog=self.catalog,
483
+ )
484
+ return draft
485
+
486
+ def create_authored_agent(
487
+ self,
488
+ *,
489
+ name: str,
490
+ slug: str | None = None,
491
+ runtime_type: str,
492
+ template: str = "blank",
493
+ description: str = "",
494
+ spec: AgentSpec | None = None,
495
+ ) -> AgentDraft:
496
+ return self.authoring.create(
497
+ name=name,
498
+ slug=slug,
499
+ runtime_type=runtime_type,
500
+ description=description,
501
+ template=template,
502
+ spec=spec,
503
+ )
504
+
505
+ def inspect_agent_import(self, content: bytes, *, filename: str) -> dict:
506
+ return self.authoring.inspect_import(content, filename=filename)
507
+
508
+ def commit_agent_import(
509
+ self,
510
+ inspection_token: str,
511
+ *,
512
+ name: str | None = None,
513
+ slug: str | None = None,
514
+ ) -> AgentDraft:
515
+ return self.authoring.commit_import(
516
+ inspection_token,
517
+ name=name,
518
+ slug=slug,
519
+ )
520
+
521
+ def inspect_agent_project(self, project_path: str) -> dict:
522
+ return self.authoring.inspect_project(project_path)
523
+
524
+ def commit_agent_project(
525
+ self,
526
+ inspection_token: str,
527
+ *,
528
+ name: str | None = None,
529
+ slug: str | None = None,
530
+ model_profile_id: str | None = None,
531
+ ) -> AgentDraft:
532
+ return self.authoring.commit_project(
533
+ inspection_token,
534
+ name=name,
535
+ slug=slug,
536
+ model_profile_id=model_profile_id,
537
+ )
538
+
539
+ async def compose_agent_conversation(
540
+ self,
541
+ *,
542
+ messages: list[dict[str, str]],
543
+ model_profile_id: str,
544
+ runtime_type: str = "codex",
545
+ agent_model_profile_ids: list[str] | None = None,
546
+ agent_default_model_profile_id: str | None = None,
547
+ tool_resource_ids: list[str] | None = None,
548
+ mcp_resource_ids: list[str] | None = None,
549
+ skill_resource_ids: list[str] | None = None,
550
+ request_id: str | None = None,
551
+ ) -> dict:
552
+ return await self.authoring.compose_conversation(
553
+ messages=messages,
554
+ model_profile_id=model_profile_id,
555
+ runtime_type=runtime_type,
556
+ agent_model_profile_ids=agent_model_profile_ids,
557
+ agent_default_model_profile_id=agent_default_model_profile_id,
558
+ tool_resource_ids=tool_resource_ids,
559
+ mcp_resource_ids=mcp_resource_ids,
560
+ skill_resource_ids=skill_resource_ids,
561
+ request_id=request_id,
562
+ )
563
+
564
+ def conversation_authoring_status(self, request_id: str) -> dict | None:
565
+ """Return the in-memory stage snapshot for one authoring request."""
566
+
567
+ return self.authoring.conversation_status(request_id)
568
+
569
+ def is_codex_agent(self, agent_id: str) -> bool:
570
+ """Return whether one Agent is backed by the Codex YAML contract."""
571
+
572
+ return self.codex_manifests.exists(agent_id)
573
+
574
+ def agent_runtime_type(self, agent_id: str) -> str:
575
+ """Resolve runtime from the Agent itself, never from Studio process state."""
576
+
577
+ if self.is_codex_agent(agent_id):
578
+ return "codex"
579
+ draft = self.drafts.get(agent_id)
580
+ runtime = draft.spec.runtime
581
+ if runtime is None:
582
+ # Read compatibility for Agent drafts written before RuntimeRef existed.
583
+ framework = draft.metadata.labels.get("agentkit.ksyun.com/framework", "")
584
+ return framework.strip().lower() or "adk"
585
+ return runtime.type
586
+
587
+ def _agent_prompt_sources(self, agent_id: str) -> tuple[str, str, str]:
588
+ """Resolve the canonical prompt inputs used by preview and execution."""
589
+
590
+ if self.is_codex_agent(agent_id):
591
+ snapshot = self.codex_manifests.load(agent_id)
592
+ return (
593
+ snapshot.manifest.prompt or "",
594
+ snapshot.manifest.task_prompt or "",
595
+ "codex",
596
+ )
597
+ draft = self.drafts.get(agent_id)
598
+ instructions = draft.spec.instructions
599
+ return (
600
+ str(instructions.system or ""),
601
+ str(instructions.task or ""),
602
+ self.agent_runtime_type(agent_id),
603
+ )
604
+
605
+ def compile_prompt_preview(
606
+ self,
607
+ agent_id: str,
608
+ *,
609
+ request_instructions: str = "",
610
+ include_content: bool = False,
611
+ ) -> dict:
612
+ """Compile the real prompt without creating a Session, Trace, or Build."""
613
+
614
+ from ksadk.prompts.resolved import (
615
+ ResolvedPromptSources,
616
+ compile_resolved_prompt_dict,
617
+ get_default_platform_policy_source,
618
+ )
619
+
620
+ agent_system, agent_task, runtime_type = self._agent_prompt_sources(agent_id)
621
+ compiled = compile_resolved_prompt_dict(
622
+ ResolvedPromptSources(
623
+ agent_system=agent_system,
624
+ agent_task=agent_task,
625
+ request_instructions=str(request_instructions or "").strip(),
626
+ platform_policy_source=get_default_platform_policy_source(),
627
+ )
628
+ )
629
+ if compiled is None:
630
+ return {
631
+ "promptVersion": "v1",
632
+ "contentHash": "",
633
+ "stablePrefixHash": "",
634
+ "sections": [],
635
+ "runtimeType": runtime_type,
636
+ "warnings": ["no prompt content to compile"],
637
+ }
638
+ result: dict[str, Any] = {
639
+ "promptVersion": compiled["prompt_compiler_version"],
640
+ "contentHash": compiled["prompt_content_hash"],
641
+ "stablePrefixHash": compiled["prompt_stable_prefix_hash"],
642
+ "sectionHashes": compiled["prompt_section_hashes"],
643
+ "tokensBySection": compiled["prompt_tokens_by_section"],
644
+ "estimatedTokens": compiled["prompt_estimated_tokens"],
645
+ "sectionCount": compiled["prompt_section_count"],
646
+ "runtimeType": runtime_type,
647
+ "platformPolicyVersion": compiled.get("prompt_platform_policy_version"),
648
+ "warnings": [],
649
+ }
650
+ if include_content:
651
+ result["content"] = compiled["prompt_content"]
652
+ return result
653
+
654
+ def reveal_run_prompt(self, run_id: str) -> dict:
655
+ """Rebuild immutable Run prompt sections and reveal only on hash match."""
656
+
657
+ from ksadk.prompts.resolved import (
658
+ ResolvedPromptSources,
659
+ compile_resolved_prompt_dict,
660
+ get_default_platform_policy_source,
661
+ sections_from_resolved_sources,
662
+ )
663
+
664
+ record = self.event_store.get(run_id)
665
+ resolver = self.codex_runs if record.runtime_type == "codex" else self.framework_runs
666
+ spec = resolver.resolve(record.build_id, model=record.model or None)
667
+ config = dict(spec.request_config or {})
668
+ sources = ResolvedPromptSources(
669
+ agent_system=str(config.get("agent_system") or ""),
670
+ agent_task=str(config.get("agent_task") or ""),
671
+ request_instructions=str(config.get("instructions") or ""),
672
+ platform_policy_source=get_default_platform_policy_source(),
673
+ )
674
+ compiled = compile_resolved_prompt_dict(sources)
675
+ expected_hash = str((record.prompt_evidence or {}).get("contentHash") or "")
676
+ actual_hash = str((compiled or {}).get("prompt_content_hash") or "")
677
+ if not compiled or not expected_hash or actual_hash != expected_hash:
678
+ return {
679
+ "available": False,
680
+ "reason": "无法证明重建内容与本次运行一致,已拒绝展示正文。",
681
+ }
682
+ return {
683
+ "available": True,
684
+ "contentHash": actual_hash,
685
+ "sections": [
686
+ {
687
+ "id": section.section_id,
688
+ "source": section.source,
689
+ "content": section.content,
690
+ }
691
+ for section in sections_from_resolved_sources(sources)
692
+ ],
693
+ }
694
+
695
+ async def preview_context(
696
+ self,
697
+ agent_id: str,
698
+ *,
699
+ user_input: str = "",
700
+ request_instructions: str = "",
701
+ simulated_history: list[dict[str, str]] | None = None,
702
+ include_content: bool = False,
703
+ ) -> dict:
704
+ """Run the production context planner without model or persistence side effects."""
705
+
706
+ from ksadk.context_engine.capabilities import capabilities_for_runtime_type
707
+ from ksadk.context_engine.hosted_pipeline import run_hosted_pipeline
708
+ from ksadk.prompts.resolved import (
709
+ ResolvedPromptSources,
710
+ compile_resolved_prompt_dict,
711
+ get_default_platform_policy_source,
712
+ )
713
+
714
+ agent_system, agent_task, runtime_type = self._agent_prompt_sources(agent_id)
715
+ capabilities = capabilities_for_runtime_type(runtime_type)
716
+ compiled_prompt = compile_resolved_prompt_dict(
717
+ ResolvedPromptSources(
718
+ agent_system=agent_system,
719
+ agent_task=agent_task,
720
+ request_instructions=str(request_instructions or "").strip(),
721
+ platform_policy_source=get_default_platform_policy_source(),
722
+ )
723
+ )
724
+ history = [
725
+ {
726
+ "role": item.get("role", "user"),
727
+ "content": item.get("content", ""),
728
+ }
729
+ for item in (simulated_history or [])
730
+ ]
731
+ pipeline = await run_hosted_pipeline(
732
+ compiled_prompt=compiled_prompt,
733
+ user_input=str(user_input or "").strip(),
734
+ history=history,
735
+ working_state=None,
736
+ model_metadata={
737
+ "context_window_tokens": 200_000,
738
+ "max_output_tokens": 32_000,
739
+ },
740
+ contributors=None,
741
+ integration_mode=capabilities.integration_mode,
742
+ accounting_accuracy="estimated",
743
+ session_id=f"preview-{agent_id}",
744
+ invocation_id=f"preview-{agent_id}",
745
+ )
746
+ if pipeline is None:
747
+ return {
748
+ "accuracy": "estimated",
749
+ "warnings": ["no content to plan"],
750
+ "items": [],
751
+ }
752
+ plan = pipeline.plan
753
+ assembled = pipeline.assembled
754
+ return {
755
+ "accuracy": plan.get("accounting_accuracy", "estimated"),
756
+ "policyVersion": plan.get("policy_version"),
757
+ "planId": plan.get("plan_id"),
758
+ "budget": {
759
+ "maxInputTokens": (plan.get("budget") or {}).get("max_input_tokens"),
760
+ "softLimitTokens": (plan.get("budget") or {}).get("soft_limit_tokens"),
761
+ "hardLimitTokens": (plan.get("budget") or {}).get("hard_limit_tokens"),
762
+ },
763
+ "items": plan.get("selected", []),
764
+ "decisions": plan.get("decisions", []),
765
+ "totalsByKind": plan.get("tokens_by_kind", {}),
766
+ "plannedInputTokens": plan.get("planned_input_tokens"),
767
+ "warnings": list(assembled.warnings),
768
+ "projection": {
769
+ "runtimeType": runtime_type,
770
+ "integrationMode": capabilities.integration_mode,
771
+ "promptOwner": capabilities.prompt_owner,
772
+ },
773
+ **({"system": assembled.system} if include_content else {}),
774
+ }
775
+
776
+ def detect_importable_project(self) -> dict | None:
777
+ """Expose a root framework project for explicit Studio import only."""
778
+ from ksadk.studio.manifest_resolver import detect_manifest_kind
779
+
780
+ result = detect_manifest_kind(self.workspace.root)
781
+ if result.kind != "framework":
782
+ return None
783
+ import yaml
784
+
785
+ try:
786
+ payload = yaml.safe_load(result.path.read_text(encoding="utf-8-sig")) or {}
787
+ except Exception: # noqa: BLE001
788
+ return None
789
+ return {
790
+ "kind": "framework",
791
+ "runtimeType": result.framework or result.runtime_type,
792
+ "name": str(payload.get("name") or self.workspace.root.name or "imported-agent"),
793
+ "model": str(payload.get("model") or ""),
794
+ "prompt": str(payload.get("prompt") or payload.get("instruction") or ""),
795
+ "task": str(payload.get("task") or ""),
796
+ "manifestPath": "agentengine.yaml",
797
+ "requiresConfirmation": True,
798
+ }
799
+
800
+ def import_root_project(
801
+ self,
802
+ *,
803
+ name: str | None = None,
804
+ slug: str | None = None,
805
+ ) -> AgentDraft:
806
+ """Import an explicitly detected root Framework project as a Studio draft."""
807
+
808
+ importable = self.detect_importable_project()
809
+ if importable is None:
810
+ raise StudioError(
811
+ "PROJECT_NOT_IMPORTABLE",
812
+ "当前工作区根没有可导入的 Framework 项目",
813
+ status_code=422,
814
+ )
815
+ inspection = self.inspect_agent_project(".")
816
+ return self.commit_agent_project(
817
+ inspection["inspectionToken"],
818
+ name=name or importable.get("name"),
819
+ slug=slug or importable.get("name"),
820
+ )
821
+
822
+ def list_agents(self, *, query: str = "", limit: int = 50) -> list[AgentDraft]:
823
+ """List all local Agents from one registry view across runtime types."""
824
+
825
+ if limit < 1:
826
+ return []
827
+ normalized = query.strip().lower()
828
+ combined: dict[str, AgentDraft] = {
829
+ draft.metadata.id: draft for draft in self.list_codex_agents(query=query, limit=limit)
830
+ }
831
+ for draft in self.drafts.list(query=query, limit=limit):
832
+ combined.setdefault(draft.metadata.id, draft)
833
+ values = list(combined.values())
834
+ if normalized:
835
+ values = [
836
+ item
837
+ for item in values
838
+ if normalized in item.metadata.id.lower()
839
+ or normalized in item.metadata.name.lower()
840
+ ]
841
+ return values[:limit]
842
+
843
+ def agent_detail(self, agent_id: str) -> dict:
844
+ if self.is_codex_agent(agent_id):
845
+ return self.codex_agent_detail(agent_id)
846
+ draft = self.drafts.get(agent_id)
847
+ return {
848
+ "draft": draft,
849
+ "builds": self.builds.list_for_agent(agent_id)[:10],
850
+ "validation": self.validator.validate(draft),
851
+ }
852
+
853
+ def create_studio_agent(
854
+ self,
855
+ *,
856
+ agent_id: str,
857
+ name: str,
858
+ description: str = "",
859
+ template: str = "blank",
860
+ spec: AgentSpec | None = None,
861
+ runtime: RuntimeRef | None = None,
862
+ labels: dict[str, str] | None = None,
863
+ ) -> AgentDraft:
864
+ """Create one Agent and dispatch from its RuntimeRef."""
865
+
866
+ resolved_spec = (spec or default_agent_spec(template, description=description)).model_copy(
867
+ deep=True
868
+ )
869
+ selected = runtime or resolved_spec.runtime
870
+ resolved_spec.runtime = selected
871
+ if selected is not None and selected.type == "codex":
872
+ return cast(
873
+ AgentDraft,
874
+ self.create_codex_agent(
875
+ agent_id=agent_id,
876
+ spec=resolved_spec,
877
+ name=name,
878
+ labels=labels,
879
+ ),
880
+ )
881
+ return cast(
882
+ AgentDraft,
883
+ self.create_agent(
884
+ agent_id=agent_id,
885
+ name=name,
886
+ description=description,
887
+ template=template,
888
+ spec=resolved_spec,
889
+ labels=labels,
890
+ ),
891
+ )
892
+
893
+ def update_studio_agent(
894
+ self,
895
+ agent_id: str,
896
+ spec: AgentSpec,
897
+ *,
898
+ expected_revision: int,
899
+ name: str | None = None,
900
+ ) -> AgentDraft:
901
+ if self.is_codex_agent(agent_id):
902
+ spec.runtime = self.agent_detail(agent_id)["draft"].spec.runtime
903
+ return cast(
904
+ AgentDraft,
905
+ self.update_codex_agent(
906
+ agent_id,
907
+ spec,
908
+ expected_revision=expected_revision,
909
+ name=name,
910
+ ),
911
+ )
912
+ current = self.drafts.get(agent_id)
913
+ if spec.runtime is None:
914
+ spec.runtime = current.spec.runtime
915
+ elif current.spec.runtime is not None and spec.runtime.type != current.spec.runtime.type:
916
+ from ksadk.studio.errors import StudioError
917
+
918
+ raise StudioError(
919
+ "AGENT_RUNTIME_IMMUTABLE",
920
+ "编辑 Agent 时不能直接切换 Runtime;请通过导入/迁移创建新 Agent",
921
+ status_code=422,
922
+ field="runtime.type",
923
+ )
924
+ return cast(
925
+ AgentDraft,
926
+ self.update_agent(
927
+ agent_id,
928
+ spec,
929
+ expected_revision=expected_revision,
930
+ name=name,
931
+ ),
932
+ )
933
+
934
+ def update_studio_agent_bindings(
935
+ self,
936
+ agent_id: str,
937
+ bindings: AgentBindings,
938
+ *,
939
+ expected_revision: int,
940
+ ) -> AgentDraft:
941
+ detail = self.agent_detail(agent_id)
942
+ spec = detail["draft"].spec.model_copy(deep=True)
943
+ spec.bindings = bindings
944
+ return self.update_studio_agent(
945
+ agent_id,
946
+ spec,
947
+ expected_revision=expected_revision,
948
+ )
949
+
950
+ def update_studio_agent_appearance(
951
+ self,
952
+ agent_id: str,
953
+ appearance: AgentAppearance,
954
+ *,
955
+ expected_revision: int,
956
+ ) -> AgentDraft:
957
+ if self.is_codex_agent(agent_id):
958
+ return self.codex_agents.update_appearance(
959
+ agent_id,
960
+ appearance,
961
+ expected_revision=expected_revision,
962
+ )
963
+ return self.drafts.update_appearance(
964
+ agent_id,
965
+ appearance,
966
+ expected_revision=expected_revision,
967
+ )
968
+
969
+ def delete_studio_agent(self, agent_id: str, *, purge: bool = False) -> None:
970
+ if self.is_codex_agent(agent_id):
971
+ self.delete_codex_agent(agent_id, purge=purge)
972
+ return
973
+ delete_framework_agent(self, agent_id, purge=purge)
974
+
975
+ def validate_studio_agent(
976
+ self,
977
+ agent_id: str,
978
+ *,
979
+ revision: int,
980
+ level: Literal["schema", "build", "release"] = "build",
981
+ ):
982
+ detail = self.agent_detail(agent_id)
983
+ draft = detail["draft"]
984
+ if draft.metadata.revision != revision:
985
+ from ksadk.studio.errors import StudioError
986
+
987
+ raise StudioError(
988
+ "AGENT_REVISION_CONFLICT",
989
+ "Validation revision 与当前 Agent 不一致",
990
+ status_code=409,
991
+ )
992
+ if self.is_codex_agent(agent_id):
993
+ return detail["validation"]
994
+ return self.validator.validate(draft, level=level)
995
+
996
+ def submit_studio_build(
997
+ self,
998
+ agent_id: str,
999
+ *,
1000
+ revision: int,
1001
+ idempotency_key: str,
1002
+ ) -> Operation:
1003
+ runtime_type = self.agent_runtime_type(agent_id)
1004
+ runtime = next(
1005
+ (item for item in self.runtime_catalog() if item["runtimeType"] == runtime_type),
1006
+ None,
1007
+ )
1008
+ if runtime is None:
1009
+ from ksadk.studio.errors import StudioError
1010
+
1011
+ raise StudioError(
1012
+ "RUNTIME_NOT_REGISTERED",
1013
+ "Agent 引用的 RuntimeAdapter 未注册",
1014
+ status_code=422,
1015
+ details={"runtimeType": runtime_type},
1016
+ )
1017
+ if runtime["status"] != "ready":
1018
+ from ksadk.studio.errors import StudioError
1019
+
1020
+ raise StudioError(
1021
+ "RUNTIME_DEPENDENCY_MISSING",
1022
+ f"{runtime['displayName']} Runtime 依赖未安装",
1023
+ status_code=422,
1024
+ details={
1025
+ "runtimeType": runtime_type,
1026
+ "installCommand": runtime["installCommand"],
1027
+ },
1028
+ )
1029
+ if self.is_codex_agent(agent_id):
1030
+ detail = self.agent_detail(agent_id)
1031
+ if revision != detail["draft"].metadata.revision:
1032
+ from ksadk.studio.errors import StudioError
1033
+
1034
+ raise StudioError(
1035
+ "AGENT_REVISION_CONFLICT",
1036
+ "Build revision 与当前 Agent 不一致",
1037
+ status_code=409,
1038
+ )
1039
+ return self.submit_codex_build(
1040
+ idempotency_key=idempotency_key,
1041
+ agent_id=agent_id,
1042
+ )
1043
+ return self.submit_build(
1044
+ agent_id,
1045
+ revision=revision,
1046
+ idempotency_key=idempotency_key,
1047
+ )
1048
+
1049
+ def build_view(self, build_id: str):
1050
+ try:
1051
+ return self.codex_build_view(self.codex_builds.get(build_id))
1052
+ except Exception as exc: # repository not-found is the only fallback contract
1053
+ if getattr(exc, "status_code", None) != 404:
1054
+ raise
1055
+ return self.builds.get(build_id)
1056
+
1057
+ def submit_studio_run(
1058
+ self,
1059
+ build_id: str,
1060
+ user_input: str,
1061
+ *,
1062
+ session_id: str | None,
1063
+ model: str | None,
1064
+ idempotency_key: str,
1065
+ sandbox: str | None = None,
1066
+ approval_mode: str | None = None,
1067
+ collaboration_mode: str | None = None,
1068
+ goal_objective: str | None = None,
1069
+ reasoning_effort: str | None = None,
1070
+ runtime_input: Any = None,
1071
+ on_event: Callable[[RunEvent], None] | None = None,
1072
+ ) -> Operation:
1073
+ try:
1074
+ self.codex_builds.get(build_id)
1075
+ except Exception as exc:
1076
+ if getattr(exc, "status_code", None) != 404:
1077
+ raise
1078
+ else:
1079
+ return self.submit_codex_run(
1080
+ build_id,
1081
+ user_input,
1082
+ session_id=session_id,
1083
+ model=model,
1084
+ sandbox=sandbox,
1085
+ approval_mode=approval_mode,
1086
+ collaboration_mode=collaboration_mode,
1087
+ goal_objective=goal_objective,
1088
+ reasoning_effort=reasoning_effort,
1089
+ runtime_input=runtime_input,
1090
+ idempotency_key=idempotency_key,
1091
+ on_event=on_event,
1092
+ )
1093
+ return self.submit_run(
1094
+ build_id,
1095
+ user_input,
1096
+ session_id=session_id,
1097
+ model=model,
1098
+ sandbox=sandbox,
1099
+ approval_mode=approval_mode,
1100
+ collaboration_mode=collaboration_mode,
1101
+ goal_objective=goal_objective,
1102
+ reasoning_effort=reasoning_effort,
1103
+ runtime_input=runtime_input,
1104
+ idempotency_key=idempotency_key,
1105
+ on_event=on_event,
1106
+ )
1107
+
1108
+ def _draft_exists(self, agent_id: str) -> bool:
1109
+ try:
1110
+ self.drafts.get(agent_id)
1111
+ except Exception as exc:
1112
+ if getattr(exc, "status_code", None) == 404:
1113
+ return False
1114
+ raise
1115
+ return True
1116
+
1117
+ @staticmethod
1118
+ def list_agent_templates() -> list[dict]:
1119
+ return list_agent_templates()
1120
+
1121
+ def compose_agent_template(
1122
+ self,
1123
+ template_id: str,
1124
+ request: AgentTemplateComposeRequest,
1125
+ ) -> AgentTemplateComposition:
1126
+ composers = {
1127
+ "blank": compose_blank_agent,
1128
+ "research": compose_research_agent,
1129
+ }
1130
+ composer = composers.get(template_id)
1131
+ if composer is None:
1132
+ from ksadk.studio.errors import StudioError
1133
+
1134
+ raise StudioError(
1135
+ "AGENT_TEMPLATE_UNSUPPORTED",
1136
+ "当前模板不支持自动编排",
1137
+ status_code=422,
1138
+ field="templateId",
1139
+ details={"templateId": template_id},
1140
+ )
1141
+ composition = composer(self.workspace, self.catalog, request)
1142
+ self._validate_bindings(composition.spec.bindings)
1143
+ return composition
1144
+
1145
+ def update_agent(
1146
+ self,
1147
+ agent_id: str,
1148
+ spec: AgentSpec,
1149
+ *,
1150
+ expected_revision: int,
1151
+ name: str | None = None,
1152
+ ):
1153
+ current = self.drafts.get(agent_id)
1154
+ if current.metadata.revision != expected_revision:
1155
+ raise StudioError(
1156
+ "AGENT_REVISION_CONFLICT",
1157
+ "Agent 已被其他操作更新",
1158
+ status_code=409,
1159
+ field="metadata.revision",
1160
+ details={
1161
+ "expected": expected_revision,
1162
+ "actual": current.metadata.revision,
1163
+ },
1164
+ )
1165
+ materializable_bindings = self._materializable_update_bindings(
1166
+ current.spec.bindings,
1167
+ spec.bindings,
1168
+ runtime=spec.runtime or current.spec.runtime,
1169
+ )
1170
+ self._validate_bindings(materializable_bindings)
1171
+ self._validate_framework_binding_support(
1172
+ spec.runtime or current.spec.runtime,
1173
+ materializable_bindings,
1174
+ )
1175
+ preview = current.model_copy(deep=True)
1176
+ preview.metadata.revision += 1
1177
+ if name is not None:
1178
+ preview.metadata.name = name
1179
+ preview.spec = spec.model_copy(deep=True)
1180
+ preview.spec.bindings = materializable_bindings
1181
+ # Generated source is a materialized view of the candidate Revision.
1182
+ # Complete it before the authoritative draft write so any resolver,
1183
+ # digest or runtime-capability failure leaves Revision/content intact.
1184
+ materialize_generated_runtime_source(
1185
+ self.workspace,
1186
+ preview,
1187
+ catalog=self.catalog,
1188
+ )
1189
+ return self.drafts.update(
1190
+ agent_id,
1191
+ spec,
1192
+ expected_revision=expected_revision,
1193
+ name=name,
1194
+ )
1195
+
1196
+ def update_agent_bindings(
1197
+ self,
1198
+ agent_id: str,
1199
+ bindings: AgentBindings,
1200
+ *,
1201
+ expected_revision: int,
1202
+ ):
1203
+ draft = self.drafts.get(agent_id)
1204
+ spec = draft.spec.model_copy(deep=True)
1205
+ spec.bindings = bindings
1206
+ return self.update_agent(
1207
+ agent_id,
1208
+ spec,
1209
+ expected_revision=expected_revision,
1210
+ )
1211
+
1212
+ def _validate_bindings(self, bindings: AgentBindings) -> None:
1213
+ self.catalog.resolve_model(bindings)
1214
+ self.catalog.resolve_models(bindings)
1215
+ self.catalog.policy_preview(bindings)
1216
+ self.catalog.resolve_mcp_servers(bindings)
1217
+ self.catalog.resolve_mcp_tools(bindings)
1218
+ self.catalog.resolve_skills(bindings)
1219
+
1220
+ def _materializable_update_bindings(
1221
+ self,
1222
+ current: AgentBindings,
1223
+ candidate: AgentBindings,
1224
+ *,
1225
+ runtime: RuntimeRef | None,
1226
+ ) -> AgentBindings:
1227
+ """Return bindings that can be safely resolved into this Revision.
1228
+
1229
+ Unknown bindings written by older Studio versions remain authoritative
1230
+ draft data, but are dormant until they re-enter the resource catalog.
1231
+ A new or modified unknown binding is still rejected by the normal
1232
+ resolver. Framework MCP bindings are likewise read-only until generated
1233
+ runtime source has a real injection path.
1234
+ """
1235
+
1236
+ if (
1237
+ runtime is not None
1238
+ and runtime.type in {"adk", "langgraph"}
1239
+ and candidate.mcp_servers != current.mcp_servers
1240
+ ):
1241
+ raise StudioError(
1242
+ "MCP_RUNTIME_INCOMPATIBLE",
1243
+ "当前 Runtime 尚未支持把 MCP 绑定注入生成源码;历史绑定仅可保留",
1244
+ status_code=422,
1245
+ field="spec.bindings.mcpServers",
1246
+ details={"runtimeType": runtime.type},
1247
+ )
1248
+
1249
+ known = {item.resource_id for item in self.catalog.list(limit=10_000)}
1250
+
1251
+ def selected_for_materialization(
1252
+ selected: list,
1253
+ historical: list,
1254
+ ) -> list:
1255
+ return [
1256
+ binding
1257
+ for binding in selected
1258
+ if binding.resource_id in known or binding not in historical
1259
+ ]
1260
+
1261
+ resolved = candidate.model_copy(deep=True)
1262
+ if candidate.model_profile_id not in known:
1263
+ unchanged_model = (
1264
+ candidate.model_profile_id == current.model_profile_id
1265
+ and candidate.model_profile_ids == current.model_profile_ids
1266
+ )
1267
+ if unchanged_model:
1268
+ resolved.model_profile_id = None
1269
+ resolved.model_profile_ids = []
1270
+ if resolved.model_profile_id is not None:
1271
+ resolved.model_profile_ids = [
1272
+ resource_id
1273
+ for resource_id in resolved.model_profile_ids
1274
+ if resource_id in known
1275
+ or resource_id not in current.model_profile_ids
1276
+ ]
1277
+ resolved.skills = selected_for_materialization(
1278
+ candidate.skills,
1279
+ current.skills,
1280
+ )
1281
+ resolved.tools = selected_for_materialization(
1282
+ candidate.tools,
1283
+ current.tools,
1284
+ )
1285
+ # No framework generated source consumes MCP today. Exact historical
1286
+ # values remain in `candidate`, while this preview omits them honestly.
1287
+ if runtime is not None and runtime.type in {"adk", "langgraph"}:
1288
+ resolved.mcp_servers = []
1289
+ else:
1290
+ resolved.mcp_servers = selected_for_materialization(
1291
+ candidate.mcp_servers,
1292
+ current.mcp_servers,
1293
+ )
1294
+ return resolved
1295
+
1296
+ def _validate_framework_binding_support(
1297
+ self,
1298
+ runtime: RuntimeRef | None,
1299
+ bindings: AgentBindings,
1300
+ ) -> None:
1301
+ if runtime is None or runtime.type not in {"adk", "langgraph"}:
1302
+ return
1303
+ if bindings.mcp_servers:
1304
+ raise StudioError(
1305
+ "MCP_RUNTIME_INCOMPATIBLE",
1306
+ "当前 Runtime 尚未支持把 MCP 绑定注入生成源码",
1307
+ status_code=422,
1308
+ field="spec.bindings.mcpServers",
1309
+ details={"runtimeType": runtime.type},
1310
+ )
1311
+ unsupported: list[str] = []
1312
+ for binding in bindings.tools:
1313
+ if not binding.enabled:
1314
+ continue
1315
+ descriptor = self.catalog.get(binding.resource_id)
1316
+ contract = ToolContract.model_validate(descriptor.contract)
1317
+ if contract.executor not in {"builtin", "python"}:
1318
+ unsupported.append(binding.resource_id)
1319
+ if unsupported:
1320
+ raise StudioError(
1321
+ "TOOL_RUNTIME_INCOMPATIBLE",
1322
+ "当前 Runtime 仅支持 builtin/python Tool",
1323
+ status_code=422,
1324
+ field="spec.bindings.tools",
1325
+ details={
1326
+ "runtimeType": runtime.type,
1327
+ "resourceIds": unsupported,
1328
+ },
1329
+ )
1330
+
1331
+ def validate_agent(
1332
+ self,
1333
+ agent_id: str,
1334
+ *,
1335
+ level: Literal["schema", "build", "release"] = "build",
1336
+ ):
1337
+ return self.validator.validate(self.drafts.get(agent_id), level=level)
1338
+
1339
+ def submit_build(
1340
+ self,
1341
+ agent_id: str,
1342
+ *,
1343
+ revision: int,
1344
+ idempotency_key: str,
1345
+ ) -> Operation:
1346
+ draft = self.drafts.get(agent_id)
1347
+ if draft.metadata.revision != revision:
1348
+ from ksadk.studio.errors import StudioError
1349
+
1350
+ raise StudioError(
1351
+ "AGENT_REVISION_CONFLICT",
1352
+ "Build revision 与当前 Agent 不一致",
1353
+ status_code=409,
1354
+ details={"expected": revision, "actual": draft.metadata.revision},
1355
+ )
1356
+ snapshot = draft.model_copy(deep=True)
1357
+
1358
+ async def runner(_operation_id: str):
1359
+ return await asyncio.to_thread(self.builder.build, snapshot)
1360
+
1361
+ return self.operations.submit(
1362
+ kind=OperationKind.BUILD,
1363
+ resource_id=agent_id,
1364
+ idempotency_key=idempotency_key,
1365
+ runner=runner,
1366
+ )
1367
+
1368
+ def submit_run(
1369
+ self,
1370
+ build_id: str,
1371
+ user_input: str,
1372
+ *,
1373
+ session_id: str | None,
1374
+ model: str | None = None,
1375
+ sandbox: str | None = None,
1376
+ approval_mode: str | None = None,
1377
+ collaboration_mode: str | None = None,
1378
+ goal_objective: str | None = None,
1379
+ reasoning_effort: str | None = None,
1380
+ runtime_input: Any = None,
1381
+ idempotency_key: str,
1382
+ on_event: Callable[[RunEvent], None] | None = None,
1383
+ ) -> Operation:
1384
+ async def runner(_operation_id: str):
1385
+ return await self.run_build(
1386
+ build_id,
1387
+ user_input,
1388
+ session_id,
1389
+ model=model,
1390
+ sandbox=sandbox,
1391
+ approval_mode=approval_mode,
1392
+ collaboration_mode=collaboration_mode,
1393
+ goal_objective=goal_objective,
1394
+ reasoning_effort=reasoning_effort,
1395
+ runtime_input=runtime_input,
1396
+ on_event=on_event,
1397
+ )
1398
+
1399
+ return self.operations.submit(
1400
+ kind=OperationKind.RUN,
1401
+ resource_id=build_id,
1402
+ idempotency_key=idempotency_key,
1403
+ runner=runner,
1404
+ )
1405
+
1406
+ async def run_build(
1407
+ self,
1408
+ build_id: str,
1409
+ user_input: str,
1410
+ session_id: str | None,
1411
+ *,
1412
+ model: str | None = None,
1413
+ sandbox: str | None = None,
1414
+ approval_mode: str | None = None,
1415
+ collaboration_mode: str | None = None,
1416
+ goal_objective: str | None = None,
1417
+ reasoning_effort: str | None = None,
1418
+ runtime_input: Any = None,
1419
+ on_event: Callable[[RunEvent], None] | None = None,
1420
+ ):
1421
+ """Execute any immutable Studio Build through the canonical executor."""
1422
+
1423
+ try:
1424
+ spec = self.codex_runs.resolve(
1425
+ build_id,
1426
+ model=model,
1427
+ sandbox=sandbox,
1428
+ approval_mode=approval_mode,
1429
+ )
1430
+ except Exception as exc:
1431
+ if getattr(exc, "status_code", None) != 404:
1432
+ raise
1433
+ spec = self.framework_runs.resolve(
1434
+ build_id,
1435
+ model=model,
1436
+ approval_mode=approval_mode,
1437
+ )
1438
+ if collaboration_mode or goal_objective or reasoning_effort:
1439
+ from dataclasses import replace
1440
+
1441
+ request_config = dict(spec.request_config)
1442
+ if collaboration_mode:
1443
+ request_config["collaboration_mode"] = collaboration_mode
1444
+ if goal_objective:
1445
+ request_config["goal_objective"] = goal_objective
1446
+ request_config["ephemeral"] = False
1447
+ if reasoning_effort:
1448
+ request_config["effort"] = reasoning_effort
1449
+ spec = replace(spec, request_config=request_config)
1450
+ return await self.run_service.run(
1451
+ spec,
1452
+ user_input,
1453
+ runtime_input=runtime_input,
1454
+ session_id=session_id,
1455
+ on_event=on_event,
1456
+ )
1457
+
1458
+ def submit_public_evaluation(
1459
+ self,
1460
+ evalset_file: str,
1461
+ target: TargetRef,
1462
+ config: PublicEvaluationConfig,
1463
+ *,
1464
+ idempotency_key: str,
1465
+ ) -> Operation:
1466
+ """Queue the public CLI/Studio handoff without exposing adapter internals."""
1467
+
1468
+ try:
1469
+ path = self.workspace.resolve(evalset_file, must_exist=True)
1470
+ except StudioError:
1471
+ raise
1472
+ if not path.is_file():
1473
+ raise StudioError(
1474
+ "EVALSET_FILE_INVALID",
1475
+ "EvalSet 必须是工作区内的文件",
1476
+ status_code=422,
1477
+ field="evalsetFile",
1478
+ )
1479
+ try:
1480
+ evalset = load_evalset(path)
1481
+ except EvalSetParseError as exc:
1482
+ raise StudioError(
1483
+ exc.code,
1484
+ str(exc),
1485
+ status_code=422,
1486
+ field="evalsetFile",
1487
+ ) from exc
1488
+ target = self._normalize_public_evaluation_target(target)
1489
+ request = PublicEvaluationRequest(
1490
+ evalset=evalset,
1491
+ target=target,
1492
+ config=config,
1493
+ report_dir=str(self.evaluation_storage.root),
1494
+ )
1495
+ evaluation_id = f"eval_{uuid4().hex}"
1496
+
1497
+ async def runner(operation_id: str):
1498
+ try:
1499
+ report = await execute_evaluation(
1500
+ request,
1501
+ adapter=self._public_evaluation_adapter(request),
1502
+ run_id=evaluation_id,
1503
+ on_case_started=lambda case_id, index, total: self.operations.append(
1504
+ operation_id,
1505
+ "evaluation.case.started",
1506
+ {"caseId": case_id, "index": index, "total": total},
1507
+ ),
1508
+ )
1509
+ except EvaluationNotImplementedError as exc:
1510
+ raise StudioError(
1511
+ "EVALUATION_EXECUTOR_UNAVAILABLE",
1512
+ str(exc),
1513
+ status_code=501,
1514
+ ) from exc
1515
+ except EvaluationExecutionError as exc:
1516
+ raise StudioError(
1517
+ "EVALUATION_EXECUTION_FAILED",
1518
+ str(exc),
1519
+ status_code=502,
1520
+ ) from exc
1521
+ return _OperationResource(id=report.spec.id)
1522
+
1523
+ return self.operations.submit(
1524
+ kind=OperationKind.EVALUATION,
1525
+ resource_id=evaluation_id,
1526
+ idempotency_key=idempotency_key,
1527
+ metadata={
1528
+ "evalset": {"name": evalset.name, "caseCount": len(evalset.cases)},
1529
+ "target": {
1530
+ "kind": target.kind.value,
1531
+ "label": self._evaluation_target_label(target.kind, build_id=target.locator),
1532
+ },
1533
+ "evaluators": list(config.evaluators),
1534
+ },
1535
+ runner=runner,
1536
+ )
1537
+
1538
+ def list_public_evaluations(self):
1539
+ return self.evaluation_storage.list_reports()
1540
+
1541
+ def list_public_evaluation_runs(self) -> list[dict[str, Any]]:
1542
+ return [
1543
+ self._public_evaluation_run(operation)
1544
+ for operation in self.operations.list(kind=OperationKind.EVALUATION)
1545
+ ]
1546
+
1547
+ def get_public_evaluation_run(self, evaluation_id: str) -> dict[str, Any]:
1548
+ operation = next(
1549
+ (
1550
+ item
1551
+ for item in self.operations.list(kind=OperationKind.EVALUATION)
1552
+ if item.resource_id == evaluation_id
1553
+ ),
1554
+ None,
1555
+ )
1556
+ if operation is None:
1557
+ raise StudioError(
1558
+ "EVALUATION_RUN_NOT_FOUND",
1559
+ "Evaluation Run 不存在",
1560
+ status_code=404,
1561
+ details={"id": evaluation_id},
1562
+ )
1563
+ return self._public_evaluation_run(operation, include_report=True)
1564
+
1565
+ def _public_evaluation_run(
1566
+ self, operation: Operation, *, include_report: bool = False
1567
+ ) -> dict[str, Any]:
1568
+ try:
1569
+ report = self.evaluation_storage.read_report(operation.resource_id)
1570
+ except EvaluationStorageError:
1571
+ report = None
1572
+ progress = None
1573
+ for event in self.operations.events(operation.id):
1574
+ if event.type == "evaluation.case.started":
1575
+ progress = {
1576
+ "current": event.data.get("index", 0),
1577
+ "total": event.data.get("total", 0),
1578
+ "caseId": event.data.get("caseId"),
1579
+ }
1580
+ metadata = operation.metadata or {}
1581
+ evalset = metadata.get("evalset")
1582
+ target = metadata.get("target")
1583
+ evaluators = metadata.get("evaluators")
1584
+ if report is not None:
1585
+ if not evalset:
1586
+ evalset = {
1587
+ "name": report.spec.evalset.name,
1588
+ "caseCount": len(report.spec.evalset.cases),
1589
+ }
1590
+ if not target:
1591
+ target = {
1592
+ "kind": report.spec.target.kind.value,
1593
+ "label": self._evaluation_target_label(
1594
+ report.spec.target.kind,
1595
+ metadata=report.spec.target.metadata,
1596
+ ),
1597
+ }
1598
+ if not evaluators:
1599
+ evaluators = list(report.spec.config.evaluators)
1600
+ payload: dict[str, Any] = {
1601
+ "id": operation.resource_id,
1602
+ "operationId": operation.id,
1603
+ "status": report.status.value if report else operation.status,
1604
+ "createdAt": operation.created_at,
1605
+ "completedAt": operation.completed_at,
1606
+ "evalset": evalset or {},
1607
+ "target": target or {},
1608
+ "evaluators": evaluators or [],
1609
+ "progress": progress,
1610
+ "summary": report.summary if report else None,
1611
+ "hasReport": report is not None,
1612
+ "error": operation.error,
1613
+ }
1614
+ if include_report:
1615
+ payload["report"] = report
1616
+ return payload
1617
+
1618
+ def _evaluation_target_label(
1619
+ self,
1620
+ kind: TargetKind,
1621
+ *,
1622
+ build_id: str | None = None,
1623
+ metadata: dict[str, Any] | None = None,
1624
+ ) -> str:
1625
+ if kind is not TargetKind.STUDIO_BUILD:
1626
+ return _EVALUATION_TARGET_LABELS[kind]
1627
+ agent_id = str((metadata or {}).get("agentId") or "")
1628
+ if not agent_id and build_id:
1629
+ agent_id = self.builds.get(build_id).agent_id
1630
+ if not agent_id:
1631
+ return _EVALUATION_TARGET_LABELS[kind]
1632
+ try:
1633
+ return self.drafts.get(agent_id).metadata.name
1634
+ except StudioError as exc:
1635
+ if exc.status_code != 404:
1636
+ raise
1637
+ return agent_id
1638
+
1639
+ def evaluation_catalog(self) -> dict[str, list[dict]]:
1640
+ builds = [
1641
+ {
1642
+ "id": record.id,
1643
+ "agentId": record.agent_id,
1644
+ "runtime": record.runtime_type,
1645
+ "digest": _sha256_digest(record.bundle_digest),
1646
+ "createdAt": record.created_at.isoformat().replace("+00:00", "Z"),
1647
+ }
1648
+ for record in self.builds.list()
1649
+ if record.status == BuildStatus.SUCCEEDED and record.artifact_path
1650
+ ]
1651
+ builds.sort(key=lambda item: item["createdAt"], reverse=True)
1652
+ evalsets: list[dict] = []
1653
+ candidates: set[Path] = set()
1654
+ for pattern in ("*.yaml", "*.yml", "*.json"):
1655
+ candidates.update(self.workspace.root.glob(pattern))
1656
+ candidates.update(self.workspace.root.glob(f"evaluations/**/{pattern}"))
1657
+ candidates.update(self.workspace.root.glob(f"agents/*/evaluations/**/{pattern}"))
1658
+ for path in sorted(candidates):
1659
+ try:
1660
+ evalset = load_evalset(path)
1661
+ except (EvalSetParseError, OSError):
1662
+ continue
1663
+ evalsets.append({
1664
+ "path": path.relative_to(self.workspace.root).as_posix(),
1665
+ "name": evalset.name,
1666
+ "caseCount": len(evalset.cases),
1667
+ "contentDigest": evalset.content_digest,
1668
+ })
1669
+ evalsets.sort(key=lambda item: item["path"])
1670
+ return {"builds": builds, "evalsets": evalsets}
1671
+
1672
+ def import_evaluation_file(self, content: bytes, *, filename: str) -> dict:
1673
+ if len(content) > 2 * 1024 * 1024:
1674
+ raise StudioError(
1675
+ "EVALSET_FILE_TOO_LARGE",
1676
+ "EvalSet 文件不能超过 2 MiB",
1677
+ status_code=413,
1678
+ field="file",
1679
+ )
1680
+ safe_name = Path(filename.replace("\\", "/")).name or "evalset.yaml"
1681
+ if Path(safe_name).suffix.lower() not in {".yaml", ".yml", ".json"}:
1682
+ raise StudioError(
1683
+ "EVALSET_FILE_TYPE_INVALID",
1684
+ "EvalSet 只支持 YAML 或 JSON 文件",
1685
+ status_code=422,
1686
+ field="file",
1687
+ )
1688
+ target = self.workspace.resolve(
1689
+ Path("evaluations/uploads") / f"{uuid4().hex[:12]}-{safe_name}"
1690
+ )
1691
+ self.workspace.atomic_write_bytes(target, content)
1692
+ try:
1693
+ evalset = load_evalset(target)
1694
+ except EvalSetParseError as exc:
1695
+ target.unlink(missing_ok=True)
1696
+ raise StudioError(exc.code, str(exc), status_code=422, field="file") from exc
1697
+ return {
1698
+ "path": self.workspace.relative(target),
1699
+ "name": evalset.name,
1700
+ "caseCount": len(evalset.cases),
1701
+ "contentDigest": evalset.content_digest,
1702
+ }
1703
+
1704
+ def get_public_evaluation(self, evaluation_id: str):
1705
+ try:
1706
+ return self.evaluation_storage.read_report(evaluation_id)
1707
+ except EvaluationStorageError as exc:
1708
+ raise StudioError(
1709
+ "EVALUATION_NOT_FOUND",
1710
+ "Evaluation 不存在",
1711
+ status_code=404,
1712
+ details={"id": evaluation_id},
1713
+ ) from exc
1714
+
1715
+ def _normalize_public_evaluation_target(self, target: TargetRef) -> TargetRef:
1716
+ if target.kind == TargetKind.A2A:
1717
+ parsed = urlparse(target.locator)
1718
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
1719
+ raise StudioError(
1720
+ "EVALUATION_A2A_URL_INVALID",
1721
+ "A2A target 必须是 http 或 https URL",
1722
+ status_code=422,
1723
+ field="target.locator",
1724
+ )
1725
+ return target
1726
+ if target.kind == TargetKind.STUDIO_BUILD:
1727
+ self._validate_evaluation_build(target.locator)
1728
+ return target
1729
+ path = self.workspace.resolve(target.locator, must_exist=True)
1730
+ if not path.is_dir():
1731
+ raise StudioError(
1732
+ "EVALUATION_TARGET_INVALID",
1733
+ "本地 target 必须是工作区内的目录",
1734
+ status_code=422,
1735
+ field="target.locator",
1736
+ )
1737
+ return target.model_copy(update={"locator": str(path)})
1738
+
1739
+ def _public_evaluation_adapter(self, request: PublicEvaluationRequest):
1740
+ if request.target.kind != TargetKind.STUDIO_BUILD:
1741
+ return None
1742
+ evidence_store = EvidenceStore(request.report_dir) if request.report_dir else None
1743
+ return StudioBuildTargetAdapter(
1744
+ timeout_seconds=request.config.timeout_seconds,
1745
+ resolve_build=self._resolve_evaluation_build,
1746
+ run_service=self.run_service,
1747
+ evidence_store=evidence_store,
1748
+ )
1749
+
1750
+ def _resolve_evaluation_build(self, build_id: str) -> StudioBuildResolution:
1751
+ try:
1752
+ self.codex_builds.get(build_id)
1753
+ except Exception as exc:
1754
+ if getattr(exc, "status_code", None) != 404:
1755
+ raise
1756
+ else:
1757
+ raise StudioError(
1758
+ "CODEX_BUILD_NOT_IMMUTABLE",
1759
+ "Codex Build 尚未冻结工作区源码,不能作为不可变评测 Target",
1760
+ status_code=422,
1761
+ field="target.locator",
1762
+ )
1763
+ build = self.builds.get(build_id)
1764
+ if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
1765
+ raise StudioBuildTargetError(
1766
+ "STUDIO_BUILD_NOT_READY",
1767
+ "Studio Build must be SUCCEEDED before evaluation",
1768
+ )
1769
+ run_spec = self.framework_runs.resolve(build_id)
1770
+ revision_digest = str(build.bundle_digest or build.resolved_digest).strip()
1771
+ if not revision_digest:
1772
+ raise StudioBuildTargetError(
1773
+ "STUDIO_BUILD_INVALID",
1774
+ "Studio Build is missing an immutable digest",
1775
+ )
1776
+ return StudioBuildResolution(
1777
+ build_id=build.id,
1778
+ agent_id=build.agent_id,
1779
+ revision_digest=revision_digest,
1780
+ runtime=build.runtime_type,
1781
+ model=run_spec.model,
1782
+ run_spec=run_spec,
1783
+ metadata={
1784
+ "bundleDigest": build.bundle_digest,
1785
+ "resolvedDigest": build.resolved_digest,
1786
+ "sourceDigest": build.source_digest,
1787
+ "sourceRevision": build.source_revision,
1788
+ },
1789
+ )
1790
+
1791
+ def _validate_evaluation_build(self, build_id: str) -> None:
1792
+ try:
1793
+ self.codex_builds.get(build_id)
1794
+ except Exception as exc:
1795
+ if getattr(exc, "status_code", None) != 404:
1796
+ raise
1797
+ else:
1798
+ raise StudioError(
1799
+ "CODEX_BUILD_NOT_IMMUTABLE",
1800
+ "Codex Build 尚未冻结工作区源码,不能作为不可变评测 Target",
1801
+ status_code=422,
1802
+ field="target.locator",
1803
+ )
1804
+ build = self.builds.get(build_id)
1805
+ if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
1806
+ raise StudioBuildTargetError(
1807
+ "STUDIO_BUILD_NOT_READY",
1808
+ "Studio Build must be SUCCEEDED before evaluation",
1809
+ )
1810
+
1811
+ def submit_deployment(
1812
+ self,
1813
+ build_id: str,
1814
+ request: DeploymentRequest,
1815
+ *,
1816
+ idempotency_key: str,
1817
+ ) -> Operation:
1818
+ logging.getLogger(__name__).info(
1819
+ "deployment submitted: build=%s idempotencyKey=%s environment=%s",
1820
+ build_id,
1821
+ idempotency_key,
1822
+ getattr(request, "environment", "-"),
1823
+ )
1824
+ try:
1825
+ codex_build = self.codex_builds.get(build_id)
1826
+ except StudioError as exc:
1827
+ if exc.status_code != 404:
1828
+ raise
1829
+ else:
1830
+ snapshot = self.codex_manifests.load(codex_build.agent_name)
1831
+ if not self.codex_builder.is_current(codex_build):
1832
+ raise StudioError(
1833
+ "BUILD_NOT_CURRENT",
1834
+ "Codex YAML 已变更,请重新 Build 后再部署",
1835
+ status_code=409,
1836
+ details={"buildId": build_id},
1837
+ )
1838
+ # The deployed YAML records only the selected model identity. Its
1839
+ # credential stays in the local resolver and is materialized for
1840
+ # this outbound control-plane request only, never in the Build or
1841
+ # deployment receipt.
1842
+ launch = self.codex_runs.resolve(build_id)
1843
+ runtime_environment = dict(
1844
+ (launch.launch_context.config or {}).get("env") or {}
1845
+ )
1846
+ # Retrying a YAML deployment must update the Agent that the prior
1847
+ # receipt already created. CreateAgent can have succeeded before
1848
+ # a downstream Runtime-Service start failed; creating again would
1849
+ # leave duplicate cloud Agents and make the retry path non-idempotent.
1850
+ # A YAML edit necessarily produces a new immutable Build. Match
1851
+ # the prior receipt through that Build's immutable agent_name,
1852
+ # rather than by build_id, otherwise every edit attempts a second
1853
+ # CreateAgent and Server correctly rejects the duplicate name.
1854
+ replacement_candidates = []
1855
+ for deployment in self.cloud.list():
1856
+ if deployment.artifact_id != "managed-runtime" or not deployment.agent_id:
1857
+ continue
1858
+ try:
1859
+ deployed_build = self.codex_builds.get(deployment.build_id)
1860
+ except StudioError:
1861
+ # A malformed/legacy local receipt cannot establish
1862
+ # ownership of this Agent and must not block a new deploy.
1863
+ continue
1864
+ if deployed_build.agent_name == codex_build.agent_name:
1865
+ replacement_candidates.append((deployed_build, deployment))
1866
+ replacing = (
1867
+ max(
1868
+ replacement_candidates,
1869
+ key=lambda item: (item[0].source_revision, item[0].created_at),
1870
+ )[1]
1871
+ if replacement_candidates
1872
+ else None
1873
+ )
1874
+
1875
+ async def managed_runtime_runner(_operation_id: str):
1876
+ return await self.cloud.deploy_managed_runtime(
1877
+ build_id=build_id,
1878
+ agent_name=codex_build.agent_name,
1879
+ manifest=snapshot.source_bytes.decode("utf-8"),
1880
+ runtime_name=codex_build.runtime_name,
1881
+ runtime_version=codex_build.runtime_version,
1882
+ # Server canonicalizes and records its own digest. This
1883
+ # source digest remains the immutable Studio Build receipt.
1884
+ manifest_digest=codex_build.manifest_sha256,
1885
+ request=request,
1886
+ runtime_environment=runtime_environment,
1887
+ replacing=replacing,
1888
+ )
1889
+
1890
+ return self.operations.submit(
1891
+ kind=OperationKind.DEPLOYMENT,
1892
+ resource_id=build_id,
1893
+ idempotency_key=idempotency_key,
1894
+ runner=managed_runtime_runner,
1895
+ )
1896
+
1897
+ async def runner(_operation_id: str):
1898
+ return await self.cloud.deploy(build_id, request)
1899
+
1900
+ return self.operations.submit(
1901
+ kind=OperationKind.DEPLOYMENT,
1902
+ resource_id=build_id,
1903
+ idempotency_key=idempotency_key,
1904
+ runner=runner,
1905
+ )
1906
+
1907
+ def submit_rollback(
1908
+ self,
1909
+ deployment_id: str,
1910
+ *,
1911
+ target_build_id: str,
1912
+ idempotency_key: str,
1913
+ ) -> Operation:
1914
+ deployment = self.cloud.get(deployment_id)
1915
+ if deployment.artifact_id == "managed-runtime":
1916
+ deployed_build = self.codex_builds.get(deployment.build_id)
1917
+ target_build = self.codex_builds.get(target_build_id)
1918
+ if target_build.agent_name != deployed_build.agent_name:
1919
+ raise StudioError(
1920
+ "MANAGED_RUNTIME_ROLLBACK_AGENT_MISMATCH",
1921
+ "声明式 Agent 只能回滚到同一 Agent 的 Build",
1922
+ status_code=409,
1923
+ details={
1924
+ "deploymentId": deployment_id,
1925
+ "targetBuildId": target_build_id,
1926
+ },
1927
+ )
1928
+ manifest = self.codex_builds.manifest_text(target_build)
1929
+ request = self.cloud.request_for(deployment_id)
1930
+
1931
+ async def managed_runtime_runner(_operation_id: str):
1932
+ return await self.cloud.deploy_managed_runtime(
1933
+ build_id=target_build.id,
1934
+ agent_name=target_build.agent_name,
1935
+ manifest=manifest,
1936
+ runtime_name=target_build.runtime_name,
1937
+ runtime_version=target_build.runtime_version,
1938
+ manifest_digest=target_build.manifest_sha256,
1939
+ request=request,
1940
+ replacing=deployment,
1941
+ )
1942
+
1943
+ return self.operations.submit(
1944
+ kind=OperationKind.DEPLOYMENT,
1945
+ resource_id=deployment_id,
1946
+ idempotency_key=idempotency_key,
1947
+ runner=managed_runtime_runner,
1948
+ )
1949
+
1950
+ deployed_build = self.builds.get(deployment.build_id)
1951
+ target_build = self.builds.get(target_build_id)
1952
+ if target_build.agent_id != deployed_build.agent_id:
1953
+ raise StudioError(
1954
+ "DEPLOYMENT_ROLLBACK_AGENT_MISMATCH",
1955
+ "高代码 Agent 只能回滚到同一 Agent 的 Build",
1956
+ status_code=409,
1957
+ details={
1958
+ "deploymentId": deployment_id,
1959
+ "targetBuildId": target_build_id,
1960
+ },
1961
+ )
1962
+
1963
+ async def runner(_operation_id: str):
1964
+ return await self.cloud.rollback(
1965
+ deployment_id,
1966
+ target_build_id=target_build_id,
1967
+ )
1968
+
1969
+ return self.operations.submit(
1970
+ kind=OperationKind.DEPLOYMENT,
1971
+ resource_id=deployment_id,
1972
+ idempotency_key=idempotency_key,
1973
+ runner=runner,
1974
+ )
1975
+
1976
+ def submit_account_agent_version_rollback(
1977
+ self,
1978
+ agent_id: str,
1979
+ *,
1980
+ version_id: str,
1981
+ idempotency_key: str,
1982
+ ) -> Operation:
1983
+ normalized_agent_id = str(agent_id or "").strip()
1984
+ normalized_version_id = str(version_id or "").strip()
1985
+ if not normalized_agent_id:
1986
+ raise StudioError(
1987
+ "CLOUD_AGENT_NOT_FOUND",
1988
+ "云端 Agent 标识不能为空",
1989
+ status_code=404,
1990
+ )
1991
+ if not normalized_version_id:
1992
+ raise StudioError(
1993
+ "CLOUD_AGENT_VERSION_REQUIRED",
1994
+ "请选择要回滚的云端版本",
1995
+ status_code=422,
1996
+ field="versionId",
1997
+ )
1998
+ resource_id = f"{normalized_agent_id}:{normalized_version_id}"
1999
+
2000
+ async def runner(_operation_id: str):
2001
+ await self.cloud.rollback_account_agent_version(
2002
+ normalized_agent_id,
2003
+ version_id=normalized_version_id,
2004
+ )
2005
+ return _OperationResource(id=resource_id)
2006
+
2007
+ return self.operations.submit(
2008
+ kind=OperationKind.DEPLOYMENT,
2009
+ resource_id=resource_id,
2010
+ idempotency_key=idempotency_key,
2011
+ metadata={
2012
+ "agentId": normalized_agent_id,
2013
+ "targetVersionId": normalized_version_id,
2014
+ "source": "server-version",
2015
+ },
2016
+ runner=runner,
2017
+ )
2018
+
2019
+ async def deployment_dashboard_access(self, deployment_id: str) -> dict[str, str | None]:
2020
+ """Return a receipt-bound private Hosted UI link on explicit user request."""
2021
+
2022
+ return await self.cloud.dashboard_access(deployment_id)
2023
+
2024
+ def deployment_operation_scope(self) -> dict[str, str]:
2025
+ """Return opaque browser storage scopes without exposing cloud credentials."""
2026
+
2027
+ workspace_identity = str(self.workspace.root.resolve())
2028
+ region = (
2029
+ os.environ.get("AGENTENGINE_REGION")
2030
+ or os.environ.get("KSYUN_REGION")
2031
+ or "cn-beijing-6"
2032
+ ).strip()
2033
+ access_key = (
2034
+ os.environ.get("KSYUN_ACCESS_KEY")
2035
+ or os.environ.get("KS3_ACCESS_KEY")
2036
+ or "unsigned"
2037
+ ).strip()
2038
+
2039
+ def opaque(value: str) -> str:
2040
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
2041
+
2042
+ return {
2043
+ "workspace": opaque(workspace_identity),
2044
+ "cloudCredential": opaque(f"{region}\0{access_key}"),
2045
+ }
2046
+
2047
+ def get_settings(self) -> dict[str, Any]:
2048
+ path = self.workspace.resolve(".agentkit/settings.yaml")
2049
+ data: dict[str, Any] = {}
2050
+ if path.is_file():
2051
+ try:
2052
+ data = load_yaml_file(path) or {}
2053
+ except Exception:
2054
+ data = {}
2055
+ defaults = {
2056
+ "sandbox": os.environ.get("KSADK_CODEX_SANDBOX", "read_only"),
2057
+ "buildAfterCreate": True,
2058
+ "codexProxy": current_proxy_mode(),
2059
+ "cloudRegion": os.environ.get(
2060
+ "AGENTENGINE_REGION", os.environ.get("KSYUN_REGION", "cn-beijing-6")
2061
+ ),
2062
+ "cloudBucket": os.environ.get("KS3_BUCKET", ""),
2063
+ "cloudAccountId": (
2064
+ os.environ.get("KSYUN_ACCOUNT_ID", "").strip()
2065
+ ),
2066
+ # AK/SK 不回显(避免本地 UI 回传 secret);只暴露配置状态。
2067
+ # 已配置 = 启动环境或已持久化 settings 里 AK+SK 齐全。
2068
+ "cloudAccountConfigured": bool(
2069
+ (
2070
+ os.environ.get("KSYUN_ACCESS_KEY")
2071
+ or os.environ.get("KS3_ACCESS_KEY", "")
2072
+ ).strip()
2073
+ and (
2074
+ os.environ.get("KSYUN_SECRET_KEY")
2075
+ or os.environ.get("KS3_SECRET_KEY", "")
2076
+ ).strip()
2077
+ ),
2078
+ "cloudSignedAccountConfigured": bool(
2079
+ (
2080
+ os.environ.get("KSYUN_ACCESS_KEY")
2081
+ or os.environ.get("KS3_ACCESS_KEY", "")
2082
+ ).strip()
2083
+ and (
2084
+ os.environ.get("KSYUN_SECRET_KEY")
2085
+ or os.environ.get("KS3_SECRET_KEY", "")
2086
+ ).strip()
2087
+ ),
2088
+ "traceContent": os.environ.get("KSADK_STUDIO_TRACE_CONTENT", "1") != "0",
2089
+ }
2090
+ defaults.update({k: v for k, v in data.items() if k in defaults and v is not None})
2091
+ defaults["codexProxy"] = normalize_proxy_mode(defaults.get("codexProxy"))
2092
+ return defaults
2093
+
2094
+ def update_settings(self, payload: dict[str, Any]) -> dict[str, Any]:
2095
+ allowed = {
2096
+ "sandbox",
2097
+ "buildAfterCreate",
2098
+ "codexProxy",
2099
+ "cloudRegion",
2100
+ "cloudBucket",
2101
+ "traceContent",
2102
+ # 云账号:AK/SK 留空 = 不修改(保留已存值);AccountID 可单独更新。
2103
+ "cloudAccessKey",
2104
+ "cloudSecretKey",
2105
+ "cloudAccountId",
2106
+ }
2107
+ data = {k: payload[k] for k in allowed if k in payload}
2108
+ if "cloudAccessKey" in data and not str(data["cloudAccessKey"]).strip():
2109
+ data.pop("cloudAccessKey")
2110
+ if "cloudSecretKey" in data and not str(data["cloudSecretKey"]).strip():
2111
+ data.pop("cloudSecretKey")
2112
+ if "cloudAccountId" in data:
2113
+ data["cloudAccountId"] = str(data["cloudAccountId"]).strip()
2114
+ if not data["cloudAccountId"]:
2115
+ data.pop("cloudAccountId")
2116
+ if data.get("sandbox") and data["sandbox"] not in {
2117
+ "read-only",
2118
+ "workspace-write",
2119
+ "workspace-write-auto",
2120
+ "full-access",
2121
+ "read_only",
2122
+ "workspace_write",
2123
+ "workspace_write_auto",
2124
+ "full_access",
2125
+ }:
2126
+ raise StudioError("SETTINGS_INVALID", "sandbox 取值非法", status_code=422)
2127
+ if "codexProxy" in data and data["codexProxy"] not in {"auto", "forced", "direct"}:
2128
+ raise StudioError("SETTINGS_INVALID", "codexProxy 取值非法", status_code=422)
2129
+ path = self.workspace.resolve(".agentkit/settings.yaml")
2130
+ self.workspace.atomic_write_yaml(path, data)
2131
+ # env 桥接用落盘后的全量数据:局部更新(只改 AccountID)时,
2132
+ # 已持久化的 AK/SK 也要继续桥接,否则签名凭证丢失。
2133
+ try:
2134
+ persisted = load_yaml_file(path) or {}
2135
+ except Exception:
2136
+ persisted = data
2137
+ self._apply_settings_to_env(persisted if isinstance(persisted, dict) else data)
2138
+ if self._cloud_gateway_override is None:
2139
+ self.cloud.gateway = self._configured_cloud_gateway()
2140
+ return self.get_settings()
2141
+
2142
+ def _apply_persisted_settings(self) -> None:
2143
+ """启动时把 settings.yaml 回填到进程环境。
2144
+
2145
+ update_settings 只在 PUT 时桥接 env;重启后 env 丢失,运行解析
2146
+ (如 _resolve_sandbox 读 KSADK_CODEX_SANDBOX)会回落默认,表现为
2147
+ 「设置页显示 workspace-write-auto,实际运行 read-only」。
2148
+ """
2149
+ path = self.workspace.resolve(".agentkit/settings.yaml")
2150
+ if not path.is_file():
2151
+ return
2152
+ try:
2153
+ data = load_yaml_file(path) or {}
2154
+ except Exception:
2155
+ return
2156
+ if isinstance(data, dict):
2157
+ self._apply_settings_to_env(data)
2158
+
2159
+ @staticmethod
2160
+ def _apply_settings_to_env(data: dict[str, Any]) -> None:
2161
+ if data.get("sandbox"):
2162
+ os.environ["KSADK_CODEX_SANDBOX"] = data["sandbox"]
2163
+ if "codexProxy" in data:
2164
+ proxy_env = proxy_mode_env_value(data["codexProxy"])
2165
+ if proxy_env is None:
2166
+ os.environ.pop("KSADK_CODEX_USE_PROXY", None)
2167
+ else:
2168
+ os.environ["KSADK_CODEX_USE_PROXY"] = proxy_env
2169
+ if data.get("cloudRegion"):
2170
+ os.environ["AGENTENGINE_REGION"] = data["cloudRegion"]
2171
+ if data.get("cloudBucket"):
2172
+ os.environ["KS3_BUCKET"] = data["cloudBucket"]
2173
+ if "traceContent" in data:
2174
+ os.environ["KSADK_STUDIO_TRACE_CONTENT"] = "1" if data["traceContent"] else "0"
2175
+ # 云账号凭证:桥接到 KSYUN_* env(AgentEngineClient 的 V4 签名与
2176
+ # X-Ksc-Account-Id 注入都从这里取值)。
2177
+ if data.get("cloudAccessKey"):
2178
+ os.environ["KSYUN_ACCESS_KEY"] = str(data["cloudAccessKey"])
2179
+ if data.get("cloudSecretKey"):
2180
+ os.environ["KSYUN_SECRET_KEY"] = str(data["cloudSecretKey"])
2181
+ if data.get("cloudAccountId"):
2182
+ os.environ["KSYUN_ACCOUNT_ID"] = str(data["cloudAccountId"])
2183
+
2184
+ @staticmethod
2185
+ def _configured_cloud_gateway() -> CloudDeploymentGateway:
2186
+ """Compose the existing signed Code deployment path from process-only credentials."""
2187
+
2188
+ access_key = (
2189
+ os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")
2190
+ ).strip()
2191
+ secret_key = (
2192
+ os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
2193
+ ).strip()
2194
+ region = os.environ.get(
2195
+ "AGENTENGINE_REGION", os.environ.get("KSYUN_REGION", "")
2196
+ ).strip()
2197
+ if not all((access_key, secret_key, region)):
2198
+ return UnavailableCloudGateway()
2199
+ control_client = AgentEngineClient(
2200
+ region=region,
2201
+ access_key=access_key,
2202
+ secret_key=secret_key,
2203
+ )
2204
+ stream_base_url = os.environ.get("AGENTENGINE_STREAM_SERVER_URL", "").strip()
2205
+ if not stream_base_url and region.lower() == "pre-online":
2206
+ # The pre-online KOP response path currently buffers SSE until
2207
+ # EOF. Its internal Server ingress validates the same V4
2208
+ # signature and preserves the RunAgent streaming response.
2209
+ stream_base_url = "http://agent-api-pre.kspmas-internal.ksyun.com"
2210
+ stream_client = (
2211
+ AgentEngineClient(
2212
+ base_url=stream_base_url,
2213
+ region=region,
2214
+ access_key=access_key,
2215
+ secret_key=secret_key,
2216
+ )
2217
+ if stream_base_url
2218
+ else control_client
2219
+ )
2220
+ return DirectAgentEngineCloudDeploymentGateway(
2221
+ region=region,
2222
+ client=control_client,
2223
+ stream_client=stream_client,
2224
+ bucket=os.environ.get("KS3_BUCKET", "").strip() or None,
2225
+ ks3_credentials={
2226
+ "access_key": access_key,
2227
+ "secret_key": secret_key,
2228
+ },
2229
+ )
2230
+
2231
+ @staticmethod
2232
+ def builtin_capabilities():
2233
+ return list(builtin_tool_contracts().values())
2234
+
2235
+ def list_capabilities(
2236
+ self,
2237
+ *,
2238
+ kind: str | None = None,
2239
+ query: str = "",
2240
+ ) -> list[dict]:
2241
+ return [
2242
+ item.model_dump(by_alias=True, exclude_none=True, mode="json")
2243
+ for item in self.catalog.list(kind=kind, query=query, limit=200)
2244
+ ]
2245
+
2246
+
2247
+ def _sha256_digest(value: str) -> str:
2248
+ digest = str(value or "").strip()
2249
+ return digest if digest.startswith("sha256:") else f"sha256:{digest}"