agentengine-sdk-python 0.8.1__py3-none-any.whl → 0.8.3__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 (476) hide show
  1. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +25 -12
  2. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +421 -232
  3. ksadk/_build_provenance.json +1 -0
  4. ksadk/a2a/_space_client_events.py +381 -0
  5. ksadk/a2a/event_adapter.py +156 -64
  6. ksadk/a2a/executor.py +179 -63
  7. ksadk/a2a/space_client.py +6 -333
  8. ksadk/a2ui/core.py +222 -53
  9. ksadk/agui/_agent_helpers.py +247 -0
  10. ksadk/agui/a2ui_projection.py +12 -5
  11. ksadk/agui/agent.py +443 -259
  12. ksadk/api/client.py +528 -27
  13. ksadk/builders/code_builder.py +284 -8
  14. ksadk/builders/container_builder.py +8 -1
  15. ksadk/builders/managed_runtime_builder.py +42 -35
  16. ksadk/cli/__init__.py +23 -0
  17. ksadk/cli/cmd_dashboard.py +15 -1
  18. ksadk/cli/cmd_deploy.py +10 -2
  19. ksadk/cli/cmd_eval.py +66 -9
  20. ksadk/cli/cmd_evalset.py +284 -0
  21. ksadk/cli/cmd_files.py +5 -1
  22. ksadk/cli/cmd_hermes.py +1 -126
  23. ksadk/cli/cmd_invoke.py +28 -38
  24. ksadk/cli/cmd_managed_runtime.py +95 -0
  25. ksadk/cli/cmd_observe.py +85 -0
  26. ksadk/cli/cmd_openclaw.py +1 -435
  27. ksadk/cli/cmd_plugin.py +657 -0
  28. ksadk/cli/cmd_replay.py +28 -4
  29. ksadk/cli/cmd_studio.py +99 -8
  30. ksadk/cli/cmd_web.py +10 -2
  31. ksadk/cli/hermes_env.py +151 -0
  32. ksadk/cli/invoke_payload.py +49 -0
  33. ksadk/cli/openclaw_env.py +458 -0
  34. ksadk/codex/client.py +180 -30
  35. ksadk/codex/runtime.py +475 -331
  36. ksadk/configs/env_registry.py +94 -12
  37. ksadk/configs/env_registry_pcm.py +181 -0
  38. ksadk/configs/env_var_spec.py +18 -0
  39. ksadk/configs/global_config.py +5 -0
  40. ksadk/configs/settings.py +9 -0
  41. ksadk/context_engine/__init__.py +98 -0
  42. ksadk/context_engine/assembler.py +176 -0
  43. ksadk/context_engine/baseline.py +419 -0
  44. ksadk/context_engine/cache_observability.py +228 -0
  45. ksadk/context_engine/capabilities.py +443 -0
  46. ksadk/context_engine/contributors.py +409 -0
  47. ksadk/context_engine/hosted_pipeline.py +417 -0
  48. ksadk/context_engine/models.py +117 -0
  49. ksadk/context_engine/planner.py +698 -0
  50. ksadk/context_engine/policies.py +319 -0
  51. ksadk/context_engine/projection.py +31 -0
  52. ksadk/context_engine/shadow_plan.py +266 -0
  53. ksadk/context_engine/tokenizer.py +164 -0
  54. ksadk/conversations/__init__.py +19 -0
  55. ksadk/conversations/context.py +230 -30
  56. ksadk/conversations/contracts.py +266 -0
  57. ksadk/conversations/message_projection.py +308 -26
  58. ksadk/conversations/model_context.py +49 -3
  59. ksadk/conversations/projector.py +616 -0
  60. ksadk/conversations/reducer.py +98 -0
  61. ksadk/conversations/runtime_compaction.py +207 -5
  62. ksadk/conversations/runtime_input.py +184 -2
  63. ksadk/conversations/runtime_invocation.py +81 -3
  64. ksadk/conversations/runtime_observability.py +141 -0
  65. ksadk/conversations/runtime_payloads.py +42 -0
  66. ksadk/conversations/runtime_preparation.py +272 -2
  67. ksadk/conversations/runtime_resume.py +20 -1
  68. ksadk/conversations/runtime_stream_events.py +94 -9
  69. ksadk/conversations/semantic_summary.py +311 -1
  70. ksadk/conversations/session_lock.py +51 -0
  71. ksadk/deployment/env_forward.py +72 -0
  72. ksadk/deployment/managed_runtime.py +5 -0
  73. ksadk/deployment/providers/serverless.py +143 -69
  74. ksadk/evaluation/__init__.py +32 -0
  75. ksadk/evaluation/a2a_adapter.py +9 -1
  76. ksadk/evaluation/adapters.py +18 -5
  77. ksadk/evaluation/agent_eval_client.py +395 -0
  78. ksadk/evaluation/cloud_binding.py +80 -0
  79. ksadk/evaluation/cloud_converter.py +194 -0
  80. ksadk/evaluation/cloud_service.py +199 -0
  81. ksadk/evaluation/contracts.py +73 -4
  82. ksadk/evaluation/evaluators.py +183 -17
  83. ksadk/evaluation/evidence.py +258 -0
  84. ksadk/evaluation/executor.py +31 -6
  85. ksadk/evaluation/local_adapter.py +550 -0
  86. ksadk/evaluation/service_env.py +27 -0
  87. ksadk/evaluation/studio_build_adapter.py +253 -0
  88. ksadk/evaluation/target.py +15 -6
  89. ksadk/events/__init__.py +13 -13
  90. ksadk/events/_v1_compat/__init__.py +1 -0
  91. ksadk/events/_v1_compat/models.py +299 -0
  92. ksadk/events/_v1_compat/parser.py +247 -0
  93. ksadk/events/_v1_compat/projection.py +755 -0
  94. ksadk/events/adapters/__init__.py +34 -0
  95. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  96. ksadk/events/adapters/_a2a_support.py +275 -0
  97. ksadk/events/adapters/_codex_interactions.py +375 -0
  98. ksadk/events/adapters/_codex_items.py +801 -0
  99. ksadk/events/adapters/_codex_validators.py +311 -0
  100. ksadk/events/adapters/_langgraph_support.py +785 -0
  101. ksadk/events/adapters/a2a.py +841 -0
  102. ksadk/events/adapters/adk.py +503 -0
  103. ksadk/events/adapters/codex.py +907 -0
  104. ksadk/events/adapters/langgraph.py +745 -0
  105. ksadk/events/canonical.py +440 -0
  106. ksadk/events/canonical_replay.py +497 -0
  107. ksadk/events/canonical_store.py +468 -0
  108. ksadk/events/cold_recovery.py +257 -0
  109. ksadk/events/content.py +89 -0
  110. ksadk/events/identity.py +86 -0
  111. ksadk/events/pipeline.py +455 -0
  112. ksadk/events/projections.py +66 -0
  113. ksadk/events/reducer.py +661 -0
  114. ksadk/events/replay.py +3 -36
  115. ksadk/events/runtime_event.py +20 -265
  116. ksadk/events/session_event.py +324 -0
  117. ksadk/events/store.py +3 -316
  118. ksadk/events/v1_compat.py +41 -0
  119. ksadk/harness/__init__.py +45 -35
  120. ksadk/harness/runtime.py +220 -58
  121. ksadk/identity/resolver.py +4 -3
  122. ksadk/interaction/__init__.py +28 -0
  123. ksadk/interaction/contracts.py +136 -0
  124. ksadk/interaction/ledger.py +189 -0
  125. ksadk/interaction/provider.py +122 -0
  126. ksadk/interaction/providers/__init__.py +52 -0
  127. ksadk/interaction/providers/adk.py +40 -0
  128. ksadk/interaction/providers/codex.py +94 -0
  129. ksadk/interaction/providers/langgraph.py +58 -0
  130. ksadk/kernel/__init__.py +117 -0
  131. ksadk/kernel/authorization.py +258 -0
  132. ksadk/kernel/bootstrap.py +1074 -0
  133. ksadk/kernel/contract_fingerprints.py +62 -0
  134. ksadk/kernel/contracts.py +370 -0
  135. ksadk/kernel/control.py +220 -0
  136. ksadk/kernel/errors.py +79 -0
  137. ksadk/kernel/ingress.py +1123 -0
  138. ksadk/kernel/mapping.py +111 -0
  139. ksadk/kernel/memory_store.py +1134 -0
  140. ksadk/kernel/postgres_store.py +1757 -0
  141. ksadk/kernel/recovery.py +452 -0
  142. ksadk/kernel/runtime_identity.py +103 -0
  143. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  144. ksadk/kernel/sqlite_store.py +1411 -0
  145. ksadk/kernel/state.py +145 -0
  146. ksadk/kernel/store.py +232 -0
  147. ksadk/kernel/worker.py +964 -0
  148. ksadk/knowledge_base/client.py +7 -0
  149. ksadk/knowledge_base/service.py +32 -5
  150. ksadk/memory/__init__.py +61 -0
  151. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  152. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  153. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  154. ksadk/memory/adk/backends/sdk_ltm_backend.py +384 -2
  155. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  156. ksadk/memory/coordinator.py +461 -0
  157. ksadk/memory/events.py +202 -0
  158. ksadk/memory/extraction.py +228 -0
  159. ksadk/memory/ltm_backend_factory.py +16 -3
  160. ksadk/memory/models.py +316 -0
  161. ksadk/memory/policy.py +215 -0
  162. ksadk/memory/provider.py +47 -0
  163. ksadk/memory/provider_adapter.py +159 -0
  164. ksadk/memory/provider_resolver.py +71 -0
  165. ksadk/memory/providers/__init__.py +5 -0
  166. ksadk/memory/providers/local_sqlite.py +497 -0
  167. ksadk/memory/resolved_policy.py +145 -0
  168. ksadk/memory/service.py +188 -6
  169. ksadk/model_proxy/bootstrap.py +16 -9
  170. ksadk/model_proxy/detect.py +202 -9
  171. ksadk/model_proxy/namespace.py +39 -15
  172. ksadk/model_proxy/server.py +24 -5
  173. ksadk/model_proxy/transform.py +60 -2
  174. ksadk/observability/__init__.py +25 -0
  175. ksadk/observability/session_log.py +420 -0
  176. ksadk/observability/trajectory.py +105 -0
  177. ksadk/plugins/__init__.py +167 -0
  178. ksadk/plugins/bridges/__init__.py +45 -0
  179. ksadk/plugins/bridges/codex.py +552 -0
  180. ksadk/plugins/bridges/dsh.py +1116 -0
  181. ksadk/plugins/builtins.py +747 -0
  182. ksadk/plugins/bundle.py +246 -0
  183. ksadk/plugins/bundle_security.py +167 -0
  184. ksadk/plugins/composition.py +529 -0
  185. ksadk/plugins/context_contributor.py +437 -0
  186. ksadk/plugins/contracts.py +451 -0
  187. ksadk/plugins/dsh_toolchain.py +935 -0
  188. ksadk/plugins/ecosystem_bridge.py +1105 -0
  189. ksadk/plugins/ecosystem_probe.py +191 -0
  190. ksadk/plugins/host.py +724 -0
  191. ksadk/plugins/providers/__init__.py +85 -0
  192. ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
  193. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
  194. ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
  195. ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
  196. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
  197. ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
  198. ksadk/plugins/providers/codex.py +722 -0
  199. ksadk/plugins/providers/codex_dsh.py +83 -0
  200. ksadk/plugins/providers/dsh.py +951 -0
  201. ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
  202. ksadk/plugins/providers/harness.py +627 -0
  203. ksadk/plugins/providers/harness_dsh.py +91 -0
  204. ksadk/plugins/providers/legacy.py +215 -0
  205. ksadk/plugins/providers/legacy_catalog.py +100 -0
  206. ksadk/plugins/providers/shipped_dsh.py +370 -0
  207. ksadk/plugins/resolver.py +331 -0
  208. ksadk/plugins/subagent_providers/__init__.py +12 -0
  209. ksadk/plugins/subagent_providers/codex.py +443 -0
  210. ksadk/plugins/subagents.py +333 -0
  211. ksadk/prompts/__init__.py +71 -0
  212. ksadk/prompts/compiler.py +215 -0
  213. ksadk/prompts/models.py +82 -0
  214. ksadk/prompts/projection.py +89 -0
  215. ksadk/prompts/resolved.py +137 -0
  216. ksadk/prompts/sources.py +233 -0
  217. ksadk/runners/_langgraph_runner_streams.py +822 -0
  218. ksadk/runners/adk_runner.py +5 -5
  219. ksadk/runners/base_runner.py +16 -0
  220. ksadk/runners/factory.py +1 -2
  221. ksadk/runners/langgraph_runner.py +198 -437
  222. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  223. ksadk/runtime/_runner_adapter/stream_mapping.py +797 -0
  224. ksadk/runtime/adapter.py +119 -2
  225. ksadk/runtime/conversation_execution.py +316 -182
  226. ksadk/runtime/executor.py +144 -3
  227. ksadk/runtime/factory.py +268 -8
  228. ksadk/runtime/framework_adapters.py +18 -0
  229. ksadk/runtime/hosted_finalizer.py +268 -0
  230. ksadk/runtime/launch.py +14 -1
  231. ksadk/runtime/preprocessing.py +25 -3
  232. ksadk/runtime/runner_adapter.py +293 -508
  233. ksadk/runtime/usage.py +33 -0
  234. ksadk/sandbox/__init__.py +2 -1
  235. ksadk/sandbox/backends/e2b.py +35 -2
  236. ksadk/sandbox/factory.py +72 -0
  237. ksadk/scheduler/__init__.py +18 -0
  238. ksadk/scheduler/calendar.py +102 -0
  239. ksadk/scheduler/contracts.py +254 -0
  240. ksadk/scheduler/dispatcher.py +129 -0
  241. ksadk/scheduler/engine.py +416 -0
  242. ksadk/scheduler/sqlite_store.py +563 -0
  243. ksadk/server/composition.py +8 -0
  244. ksadk/server/factory.py +69 -6
  245. ksadk/server/routes/kernel_ingress.py +258 -0
  246. ksadk/server/routes/openai_compat.py +88 -8
  247. ksadk/server/routes/projection.py +35 -20
  248. ksadk/server/routes/run.py +94 -0
  249. ksadk/server/routes/sessions.py +2 -0
  250. ksadk/server/routes/streaming.py +5 -2
  251. ksadk/server/static/assets/{ArtifactsPanel-CakCqxEN.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
  252. ksadk/server/static/assets/{CodeBlock-CynLknyO.js → CodeBlock-BXk1l-G1.js} +4 -4
  253. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  254. ksadk/server/static/assets/{MathMessageMarkdown-3sQdOysS.js → MathMessageMarkdown-D9uB55Oh.js} +4 -4
  255. ksadk/server/static/assets/MermaidBlock--OEYoXIJ.js +312 -0
  256. ksadk/server/static/assets/NativeTerminalPanel-DrEe1syq.js +1 -0
  257. ksadk/server/static/assets/abnfDiagram-VCTEODGH-eeTFJLvo.js +1 -0
  258. ksadk/server/static/assets/arc-S3ZNbIAd.js +1 -0
  259. ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
  260. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-CRjgEJDX.js +36 -0
  261. ksadk/server/static/assets/blockDiagram-NRAW4CY4-C7bsBoNH.js +129 -0
  262. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-BX3c33Ob.js +38 -0
  263. ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
  264. ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
  265. ksadk/server/static/assets/{chunk-FMBD7UC4-B_fH2YuH.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
  266. ksadk/server/static/assets/{chunk-ND2GUHAM-D6rFLaVV.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
  267. ksadk/server/static/assets/chunk-G27WJ6UU-CZwF3oUP.js +231 -0
  268. ksadk/server/static/assets/{chunk-4BX2VUAB-B9drTgOW.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
  269. ksadk/server/static/assets/chunk-LCL6LL3I-CnorGV5q.js +206 -0
  270. ksadk/server/static/assets/chunk-POPQ4Y6H-CexntQA-.js +1 -0
  271. ksadk/server/static/assets/chunk-RHFEMEQ7-CHTC7UxA.js +168 -0
  272. ksadk/server/static/assets/chunk-SVP7TREG-BC1EWt_-.js +88 -0
  273. ksadk/server/static/assets/chunk-XXDRQBXY-BTG24xN0.js +1 -0
  274. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
  275. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
  276. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-ChNs_Mdk.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
  277. ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
  278. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-DEOCBe15.js +62 -0
  279. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  280. ksadk/server/static/assets/dagre-3AP2YEHR-DBThPuLa.js +4 -0
  281. ksadk/server/static/assets/dagre-ClTPrTht.js +1 -0
  282. ksadk/server/static/assets/diagram-S7CK7UJ4-DsZHrnpJ.js +30 -0
  283. ksadk/server/static/assets/diagram-UQ7AKVKN-BJR4nhVr.js +41 -0
  284. ksadk/server/static/assets/diagram-VSXAHHWV-xzcERX0a.js +3 -0
  285. ksadk/server/static/assets/diagram-VX7I27RA-tn2xM1xu.js +24 -0
  286. ksadk/server/static/assets/diagram-Z3DM3KII-BZPGBkw5.js +24 -0
  287. ksadk/server/static/assets/{dist-gLnrGsoU.js → dist-4zN1_tq5.js} +1 -1
  288. ksadk/server/static/assets/{dist-C1sz5Eo3.js → dist-8VWI-2bZ.js} +1 -1
  289. ksadk/server/static/assets/{dist-BjOdlDjj.js → dist-B-46q3Hk.js} +1 -1
  290. ksadk/server/static/assets/{dist-o2OVmKvt.js → dist-B0gdk4rl.js} +1 -1
  291. ksadk/server/static/assets/{dist-4N2NMVYg.js → dist-B1oWRmrH.js} +1 -1
  292. ksadk/server/static/assets/{dist-CmZHzlwZ.js → dist-BI1qQH4n.js} +1 -1
  293. ksadk/server/static/assets/{dist-X4TnqwOK.js → dist-BJLUwH6W.js} +1 -1
  294. ksadk/server/static/assets/{dist-B5oS1zcj.js → dist-Bbwqa8TG.js} +1 -1
  295. ksadk/server/static/assets/{dist-j6Dxmf_j.js → dist-Btyo4vX4.js} +1 -1
  296. ksadk/server/static/assets/{dist-DQU9EWw-.js → dist-C71CsujO.js} +1 -1
  297. ksadk/server/static/assets/{dist-BIlKoW0k.js → dist-CXpH_Rdd.js} +1 -1
  298. ksadk/server/static/assets/{dist-DJN_6Y39.js → dist-Cq4OL6nO.js} +1 -1
  299. ksadk/server/static/assets/{dist-Dli7qW-B.js → dist-D1V3f1B4.js} +1 -1
  300. ksadk/server/static/assets/{dist-fhOuWds5.js → dist-DJmCPRVB.js} +1 -1
  301. ksadk/server/static/assets/{dist-CWV4lDr1.js → dist-DQAB0qn_.js} +2 -2
  302. ksadk/server/static/assets/{dist-D6MpCZtz.js → dist-DUwKa_l6.js} +1 -1
  303. ksadk/server/static/assets/dist-DdRBIceA.js +1 -0
  304. ksadk/server/static/assets/{dist-DWKByxWV.js → dist-DfGFrb0v.js} +1 -1
  305. ksadk/server/static/assets/{dist-DsHPdk3W.js → dist-DjhwGUgZ.js} +1 -1
  306. ksadk/server/static/assets/{dist-B-jNR6Vn.js → dist-DyqV4fL4.js} +1 -1
  307. ksadk/server/static/assets/{dist-BB3-OwTQ.js → dist-M67Hwu0n.js} +1 -1
  308. ksadk/server/static/assets/{dist-_pHPTyZ-.js → dist-u8tVH5Ak.js} +1 -1
  309. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-DVfMsLge.js +1 -0
  310. ksadk/server/static/assets/erDiagram-SSCWMZ5O-CWEZSYaw.js +99 -0
  311. ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
  312. ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
  313. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-TWIxoyYL.js +292 -0
  314. ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
  315. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-DDh2hd2S.js +106 -0
  316. ksadk/server/static/assets/index-B2k_urY8.js +241 -0
  317. ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
  318. ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
  319. ksadk/server/static/assets/infoDiagram-RXCK75RN-CgoyWiDR.js +2 -0
  320. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-K0OOfVna.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +6 -6
  321. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-8LeOJaZp.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +6 -6
  322. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-C6uCgZrW.js → kanban-definition-3QL26DDD-CNP3ui66.js} +10 -10
  323. ksadk/server/static/assets/katex-DWEzQy1d.js +257 -0
  324. ksadk/server/static/assets/{linear-hdBcYzM8.js → linear-C-qHp7d2.js} +1 -1
  325. ksadk/server/static/assets/mermaid-parser.core-Cl-K943T.js +166 -0
  326. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-Bjy_2v4_.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +29 -29
  327. ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
  328. ksadk/server/static/assets/pegDiagram-XKGWAZYB-BQVc9ia_.js +1 -0
  329. ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
  330. ksadk/server/static/assets/pieDiagram-E7YTZNPT-B6UCAvtY.js +39 -0
  331. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-BmkRbwCg.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +6 -6
  332. ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
  333. ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
  334. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
  335. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
  336. ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
  337. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-Dt3FFDmK.js +1 -0
  338. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-Cz_vQtgF.js +84 -0
  339. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-l6iXmU65.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +7 -7
  340. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-Cq6UBeQB.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +10 -10
  341. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-DXX54iyO.js +1 -0
  342. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-Dp_lw9II.js +1 -0
  343. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
  344. ksadk/server/static/assets/swimlanes-XN3QIQJK-D8O5UlLG.js +1 -0
  345. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
  346. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-DEz24vV0.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +7 -7
  347. ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
  348. ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
  349. ksadk/server/static/assets/vennDiagram-4TSXK5OY-EnNl0DE7.js +34 -0
  350. ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
  351. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DBu5fbYr.js +78 -0
  352. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-BSVXs4IY.js +7 -0
  353. ksadk/server/static/index.html +2 -2
  354. ksadk/sessions/__init__.py +26 -9
  355. ksadk/sessions/_local_service_sync.py +688 -0
  356. ksadk/sessions/_local_tables.py +21 -0
  357. ksadk/sessions/_postgres_schema.py +305 -0
  358. ksadk/sessions/_postgres_tables.py +15 -0
  359. ksadk/sessions/base.py +80 -1
  360. ksadk/sessions/in_memory.py +53 -2
  361. ksadk/sessions/local_service.py +63 -604
  362. ksadk/sessions/postgres_service.py +58 -148
  363. ksadk/sessions/resilient.py +35 -1
  364. ksadk/skills/service_client.py +48 -0
  365. ksadk/studio/agent_lifecycle.py +26 -20
  366. ksadk/studio/api.py +1063 -69
  367. ksadk/studio/api_catalog_routes.py +5 -0
  368. ksadk/studio/api_contracts.py +148 -6
  369. ksadk/studio/api_memory_routes.py +82 -0
  370. ksadk/studio/api_plugin_routes.py +480 -0
  371. ksadk/studio/attachment_store.py +187 -0
  372. ksadk/studio/authoring.py +259 -28
  373. ksadk/studio/authoring_coordinator.py +809 -67
  374. ksadk/studio/builder.py +197 -40
  375. ksadk/studio/capabilities.py +17 -0
  376. ksadk/studio/cloud.py +1920 -95
  377. ksadk/studio/codex_agent_service.py +137 -15
  378. ksadk/studio/codex_authoring.py +469 -0
  379. ksadk/studio/codex_builder.py +177 -25
  380. ksadk/studio/codex_manifest.py +71 -22
  381. ksadk/studio/codex_run.py +59 -4
  382. ksadk/studio/compatibility_report.py +445 -0
  383. ksadk/studio/compiler.py +7 -1
  384. ksadk/studio/contracts.py +234 -4
  385. ksadk/studio/dsh_provider_registration.py +768 -0
  386. ksadk/studio/event_store.py +79 -101
  387. ksadk/studio/framework_run.py +264 -3
  388. ksadk/studio/hosted_kernel.py +317 -0
  389. ksadk/studio/manifest_resolver.py +131 -0
  390. ksadk/studio/model_client.py +95 -9
  391. ksadk/studio/model_profile_service.py +1 -1
  392. ksadk/studio/operations.py +47 -5
  393. ksadk/studio/otel_trace.py +348 -42
  394. ksadk/studio/pcm_memory.py +85 -0
  395. ksadk/studio/plugin_composition.py +301 -0
  396. ksadk/studio/plugin_kernel_adapter.py +128 -0
  397. ksadk/studio/plugin_runtime.py +652 -0
  398. ksadk/studio/repository.py +23 -2
  399. ksadk/studio/resource_catalog.py +49 -1
  400. ksadk/studio/run_service.py +1266 -202
  401. ksadk/studio/runtime_source.py +51 -17
  402. ksadk/studio/scheduler_runtime.py +374 -0
  403. ksadk/studio/scheduler_service.py +211 -0
  404. ksadk/studio/service.py +1815 -117
  405. ksadk/studio/shared_web.py +199 -50
  406. ksadk/studio/soul.py +52 -0
  407. ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
  408. ksadk/studio/static/assets/index-DeDqwTsD.js +283 -0
  409. ksadk/studio/static/index.html +2 -2
  410. ksadk/studio/templates.py +100 -8
  411. ksadk/studio/validator.py +20 -1
  412. ksadk/studio/workspace.py +31 -32
  413. ksadk/tracing/setup.py +304 -3
  414. ksadk/version.py +1 -1
  415. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  416. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  417. ksadk_runtime_common/workspace_files/path_utils.py +41 -0
  418. ksadk/events/parser.py +0 -191
  419. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  420. ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +0 -303
  421. ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +0 -1
  422. ksadk/server/static/assets/arc-DrgjcKhl.js +0 -1
  423. ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +0 -1
  424. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-BXoVRbQ6.js +0 -36
  425. ksadk/server/static/assets/blockDiagram-GPEHLZMM-BnaB3Ywv.js +0 -132
  426. ksadk/server/static/assets/c4Diagram-AAUBKEIU-Dw4weZcp.js +0 -10
  427. ksadk/server/static/assets/channel-Dt3rQTFI.js +0 -1
  428. ksadk/server/static/assets/chunk-2J33WTMH-ChjpLK7j.js +0 -1
  429. ksadk/server/static/assets/chunk-55IACEB6-CK4ROATy.js +0 -1
  430. ksadk/server/static/assets/chunk-727SXJPM-B9KtTTgw.js +0 -206
  431. ksadk/server/static/assets/chunk-AQP2D5EJ-1O1mki3m.js +0 -231
  432. ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +0 -1
  433. ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +0 -1
  434. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +0 -1
  435. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  436. ksadk/server/static/assets/dagre-BM42HDAG-D0QXg_MQ.js +0 -4
  437. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  438. ksadk/server/static/assets/diagram-2AECGRRQ-DBDDKwBQ.js +0 -43
  439. ksadk/server/static/assets/diagram-5GNKFQAL-BkwDOxmO.js +0 -10
  440. ksadk/server/static/assets/diagram-KO2AKTUF-Bv8waYvY.js +0 -3
  441. ksadk/server/static/assets/diagram-LMA3HP47-e1VECDdu.js +0 -24
  442. ksadk/server/static/assets/diagram-OG6HWLK6-Ds9adfRl.js +0 -24
  443. ksadk/server/static/assets/dist-B5J2uhNk.js +0 -1
  444. ksadk/server/static/assets/erDiagram-TEJ5UH35-BPItFKH6.js +0 -85
  445. ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +0 -1
  446. ksadk/server/static/assets/flowDiagram-I6XJVG4X-BbYB9w5Y.js +0 -162
  447. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-TQXa0KXv.js +0 -292
  448. ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +0 -1
  449. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-bKblCnpa.js +0 -106
  450. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  451. ksadk/server/static/assets/index-R52pJJvg.js +0 -177
  452. ksadk/server/static/assets/index-jJr3_955.css +0 -2
  453. ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +0 -1
  454. ksadk/server/static/assets/infoDiagram-5YYISTIA-BGVT7J_i.js +0 -2
  455. ksadk/server/static/assets/katex-CYWseY5E.js +0 -265
  456. ksadk/server/static/assets/mermaid-parser.core-BWrDjSZM.js +0 -161
  457. ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +0 -1
  458. ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +0 -1
  459. ksadk/server/static/assets/pieDiagram-4H26LBE5-GmmqNiVH.js +0 -30
  460. ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +0 -1
  461. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DSWFqL7c.js +0 -84
  462. ksadk/server/static/assets/stateDiagram-AJRCARHV-qzTQpg3Y.js +0 -1
  463. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +0 -1
  464. ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +0 -1
  465. ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +0 -1
  466. ksadk/server/static/assets/vennDiagram-CIIHVFJN-CMCixki2.js +0 -34
  467. ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +0 -1
  468. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-CVzB8F9K.js +0 -78
  469. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-BFBqBLc1.js +0 -7
  470. ksadk/studio/evaluation.py +0 -192
  471. ksadk/studio/static/assets/index-CaEN0eTR.css +0 -1
  472. ksadk/studio/static/assets/index-DI2eY_NH.js +0 -252
  473. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/WHEEL +0 -0
  474. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
  475. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
  476. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
ksadk/studio/service.py CHANGED
@@ -3,11 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import hashlib
7
+ import json
8
+ import logging
6
9
  import os
10
+ import zipfile
11
+ from collections.abc import Mapping, Sequence
12
+ from dataclasses import dataclass
7
13
  from pathlib import Path
8
14
  from typing import Any, Callable, Literal, cast
9
15
  from urllib.parse import urlparse
16
+ from uuid import uuid4
10
17
 
18
+ from ksadk.api import AgentEngineClient
19
+ from ksadk.conversations.contracts import ConversationCapability, ConversationSurface
11
20
  from ksadk.evaluation import (
12
21
  EvaluationConfig as PublicEvaluationConfig,
13
22
  )
@@ -25,15 +34,38 @@ from ksadk.evaluation import (
25
34
  EvaluationRequest as PublicEvaluationRequest,
26
35
  )
27
36
  from ksadk.evaluation.evalset import EvalSetParseError
37
+ from ksadk.evaluation.evidence import EvidenceStore
38
+ from ksadk.evaluation.studio_build_adapter import (
39
+ StudioBuildResolution,
40
+ StudioBuildTargetAdapter,
41
+ StudioBuildTargetError,
42
+ )
43
+ from ksadk.events.store import RuntimeEventStore
44
+ from ksadk.observability.session_log import SessionLogError, export_session_log
45
+ from ksadk.observability.trajectory import encode_sse, project_trajectory_event
46
+ from ksadk.plugins.contracts import PluginManifest
47
+ from ksadk.plugins.providers.legacy import LegacyHarnessSource
48
+ from ksadk.plugins.providers.legacy_catalog import (
49
+ KSADK_HARNESS_AGENT_PROVIDER_PLUGIN_ID,
50
+ )
28
51
  from ksadk.runtime import RuntimeExecutor, build_default_runtime_registry
52
+ from ksadk.scheduler.contracts import (
53
+ ScheduleCommandTemplate,
54
+ ScheduledTask,
55
+ ScheduledTaskTarget,
56
+ ScheduleSpec,
57
+ )
58
+ from ksadk.sessions.local_service import LocalSessionService
29
59
  from ksadk.studio.agent_avatar_assets import AgentAvatarAssetStore
30
60
  from ksadk.studio.agent_lifecycle import delete_framework_agent
61
+ from ksadk.studio.attachment_store import ConversationAttachmentStore
31
62
  from ksadk.studio.authoring_coordinator import StudioAuthoringCoordinator
32
63
  from ksadk.studio.builder import AgentBundleBuilder
33
64
  from ksadk.studio.capabilities import builtin_tool_contracts
34
65
  from ksadk.studio.cloud import (
35
66
  CloudDeploymentGateway,
36
67
  CloudDeploymentService,
68
+ DirectAgentEngineCloudDeploymentGateway,
37
69
  UnavailableCloudGateway,
38
70
  )
39
71
  from ksadk.studio.codex_agent_service import CodexAgentService, CodexDraftRepository
@@ -42,6 +74,9 @@ from ksadk.studio.codex_builder import (
42
74
  CodexBuildRepository,
43
75
  CodexStudioBuilder,
44
76
  RuntimeInspector,
77
+ current_proxy_mode,
78
+ normalize_proxy_mode,
79
+ proxy_mode_env_value,
45
80
  )
46
81
  from ksadk.studio.codex_manifest import (
47
82
  CodexAgentManifest,
@@ -56,26 +91,39 @@ from ksadk.studio.contracts import (
56
91
  AgentSpec,
57
92
  AgentTemplateComposeRequest,
58
93
  AgentTemplateComposition,
94
+ BuildStatus,
59
95
  DeploymentRequest,
60
96
  Operation,
61
97
  OperationKind,
62
98
  RunEvent,
63
99
  RunStatus,
64
100
  RuntimeRef,
101
+ ToolContract,
102
+ )
103
+ from ksadk.studio.dsh_provider_registration import (
104
+ StudioDshProviderRegistrationError,
105
+ StudioDshProviderRegistrationManager,
65
106
  )
66
107
  from ksadk.studio.errors import StudioError
67
- from ksadk.studio.evaluation import EvaluationRunner
68
108
  from ksadk.studio.event_store import RunEventStore
69
109
  from ksadk.studio.framework_run import FrameworkRunSpecResolver
70
110
  from ksadk.studio.mcp_runtime import MCPRuntimeAdapter
71
111
  from ksadk.studio.model_client import CredentialResolver, OpenAICompatibleModelClient
72
112
  from ksadk.studio.model_profile_service import test_model_profile_connection
73
113
  from ksadk.studio.operations import OperationManager
114
+ from ksadk.studio.plugin_composition import StudioPluginCompositionCompiler
115
+ from ksadk.studio.plugin_runtime import StudioPluginRuntime
74
116
  from ksadk.studio.repository import AgentDraftRepository, BuildRepository, load_yaml_file
75
117
  from ksadk.studio.resource_catalog import LocalResourceCatalog
76
- from ksadk.studio.run_service import StudioRunService
118
+ from ksadk.studio.run_service import StudioRunService, StudioRunSpec
77
119
  from ksadk.studio.runtime_catalog import inspect_runtime_catalog
78
120
  from ksadk.studio.runtime_source import materialize_generated_runtime_source
121
+ from ksadk.studio.scheduler_runtime import (
122
+ StudioScheduledKernelRegistry,
123
+ StudioSchedulerRuntimeError,
124
+ )
125
+ from ksadk.studio.scheduler_service import StudioSchedulerService
126
+ from ksadk.studio.soul import soul_digest
79
127
  from ksadk.studio.templates import (
80
128
  compose_blank_agent,
81
129
  compose_research_agent,
@@ -85,6 +133,19 @@ from ksadk.studio.templates import (
85
133
  from ksadk.studio.validator import AgentValidator
86
134
  from ksadk.studio.workspace import Workspace
87
135
 
136
+ _TRAJECTORY_KEEPALIVE_SECONDS = 15.0
137
+ _EVALUATION_TARGET_LABELS = {
138
+ TargetKind.A2A: "A2A Agent",
139
+ TargetKind.LOCAL_SOURCE: "本地源码",
140
+ TargetKind.STUDIO_BUILD: "Studio Build",
141
+ TargetKind.CODEX_WORKTREE: "Codex Worktree",
142
+ }
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class _OperationResource:
147
+ id: str
148
+
88
149
 
89
150
  class StudioService:
90
151
  def __init__(
@@ -96,11 +157,34 @@ class StudioService:
96
157
  cloud_gateway: CloudDeploymentGateway | None = None,
97
158
  codex_runtime_inspector: RuntimeInspector | None = None,
98
159
  runtime_executor: RuntimeExecutor | None = None,
160
+ harness_reasoner: Any | None = None,
161
+ plugin_provider_manifests: Mapping[str, PluginManifest] | None = None,
162
+ plugin_provider_factories: Mapping[str, Any] | None = None,
163
+ legacy_harness_sources: Sequence[LegacyHarnessSource] = (),
164
+ dsh_provider_registration_manager: StudioDshProviderRegistrationManager | None = None,
99
165
  ) -> None:
166
+ provider_manifests = dict(plugin_provider_manifests or {})
167
+ provider_factories = dict(plugin_provider_factories or {})
168
+ if provider_manifests.keys() != provider_factories.keys():
169
+ raise ValueError(
170
+ "plugin provider manifests and factories must use the same exact references"
171
+ )
100
172
  self.workspace = Workspace(root)
101
173
  self.workspace.initialize()
174
+ self._startup_provider_manifests = provider_manifests
175
+ self._startup_provider_factories = provider_factories
176
+ self._active_provider_manifests = dict(provider_manifests)
177
+ self._dsh_provider_registration_manager = (
178
+ dsh_provider_registration_manager
179
+ or StudioDshProviderRegistrationManager.discover_or_create_workspace_default(
180
+ self.workspace.root
181
+ )
182
+ )
183
+ self._start_lock = asyncio.Lock()
184
+ self._started = False
102
185
  self._apply_persisted_settings()
103
186
  self.avatar_assets = AgentAvatarAssetStore(self.workspace)
187
+ self.conversation_attachments = ConversationAttachmentStore(self.workspace)
104
188
  self.drafts = AgentDraftRepository(self.workspace)
105
189
  self.catalog = LocalResourceCatalog(self.workspace)
106
190
  self.builds = BuildRepository(self.workspace)
@@ -114,8 +198,14 @@ class StudioService:
114
198
  ),
115
199
  repository=self.builds,
116
200
  )
201
+ self.plugin_compositions = StudioPluginCompositionCompiler(
202
+ self.workspace,
203
+ self.catalog,
204
+ provider_manifests=provider_manifests,
205
+ )
117
206
  self.event_store = RunEventStore(self.workspace)
118
- self.event_store.recover_interrupted()
207
+ self.session_service = LocalSessionService(project_dir=str(self.workspace.root))
208
+ self.runtime_events = RuntimeEventStore(self.session_service)
119
209
  self.codex_manifests = CodexManifestRepository(self.workspace)
120
210
  self.codex_builds = CodexBuildRepository(self.workspace)
121
211
  self.codex_drafts = CodexDraftRepository(self.workspace)
@@ -137,6 +227,8 @@ class StudioService:
137
227
  self.workspace,
138
228
  self.runtime_executor,
139
229
  event_store=self.event_store,
230
+ session_service=self.session_service,
231
+ runtime_events=self.runtime_events,
140
232
  )
141
233
  self.credentials = (
142
234
  credential_resolver
@@ -158,28 +250,565 @@ class StudioService:
158
250
  credential_resolver=self.credentials
159
251
  )
160
252
  self.model_client = runtime_model_client
161
- self.mcp_runtime = MCPRuntimeAdapter(self.workspace, credentials=self.credentials)
162
- self.evaluations = EvaluationRunner(
253
+ self.plugin_runs = StudioPluginRuntime(
163
254
  self.workspace,
164
- run_agent=self.run_build,
165
- event_store=self.event_store,
166
255
  build_repository=self.builds,
256
+ session_service=self.session_service,
257
+ model_client=self.model_client,
258
+ secret_resolver=self.credentials,
259
+ harness_reasoner=harness_reasoner,
260
+ provider_manifests=provider_manifests,
261
+ provider_factories=provider_factories,
262
+ legacy_harness_sources=legacy_harness_sources,
167
263
  )
264
+ self.run_service.plugin_runtime = self.plugin_runs
265
+ self.scheduler_runtimes = StudioScheduledKernelRegistry(
266
+ resolve_build=self.resolve_run_spec,
267
+ resolve_adapter_provider=self._scheduler_adapter_provider,
268
+ session_service=self.session_service,
269
+ runtime_executor=self.runtime_executor,
270
+ )
271
+ self.scheduler = StudioSchedulerService(
272
+ self.workspace,
273
+ runtime_registry=self.scheduler_runtimes,
274
+ )
275
+ self.mcp_runtime = MCPRuntimeAdapter(self.workspace, credentials=self.credentials)
276
+ self._cloud_gateway_override = cloud_gateway
168
277
  self.cloud = CloudDeploymentService(
169
278
  self.workspace,
170
- gateway=cloud_gateway or UnavailableCloudGateway(),
279
+ gateway=cloud_gateway or self._configured_cloud_gateway(),
171
280
  build_repository=self.builds,
172
281
  )
173
282
  self.operations = OperationManager(self.workspace)
174
- self.evaluation_storage = EvaluationStorage(
175
- self.workspace.resolve(".agentkit/evaluations")
176
- )
283
+ self.evaluation_storage = EvaluationStorage(self.workspace.resolve(".agentkit/evaluations"))
177
284
  self.authoring = StudioAuthoringCoordinator(self)
178
285
  self.codex_agents = CodexAgentService(self)
179
286
 
287
+ async def start(self) -> None:
288
+ """Bind ready managed DSH registrations before build or execution."""
289
+
290
+ async with self._start_lock:
291
+ if self._started:
292
+ return
293
+ await self._bootstrap_official_dsh_defaults()
294
+ manifests, factories, manager_refs = await self._provider_snapshot(refresh=False)
295
+ self.plugin_compositions.replace_provider_registrations(manifests)
296
+ self.plugin_runs.replace_provider_registrations(manifests, factories)
297
+ self._active_provider_manifests = manifests
298
+ manager = self._dsh_provider_registration_manager
299
+ if manager is not None and manager_refs is not None:
300
+ manager.mark_bound(manager_refs)
301
+ self._started = True
302
+
303
+ async def refresh_dsh_provider_registrations(self) -> None:
304
+ """Rebind the exact current DSH Profile and release stale activations."""
305
+
306
+ async with self._start_lock:
307
+ if self._dsh_provider_registration_manager is None:
308
+ self._dsh_provider_registration_manager = (
309
+ StudioDshProviderRegistrationManager.discover_or_create_workspace_default(
310
+ self.workspace.root
311
+ )
312
+ )
313
+ await self._bootstrap_official_dsh_defaults()
314
+ if self._started:
315
+ await self.plugin_runs.aclose()
316
+ manifests, factories, manager_refs = await self._provider_snapshot(refresh=True)
317
+ self.plugin_compositions.replace_provider_registrations(manifests)
318
+ self.plugin_runs.replace_provider_registrations(manifests, factories)
319
+ self._active_provider_manifests = manifests
320
+ manager = self._dsh_provider_registration_manager
321
+ if manager is not None and manager_refs is not None:
322
+ manager.mark_bound(manager_refs)
323
+ self._started = True
324
+
325
+ async def _bootstrap_official_dsh_defaults(self) -> None:
326
+ manager = self._dsh_provider_registration_manager
327
+ if manager is None:
328
+ return
329
+ try:
330
+ result = await manager.bootstrap_official_codex_provider()
331
+ except Exception as error: # optional DSH must fail closed to legacy paths
332
+ logging.getLogger(__name__).warning(
333
+ "official DSH provider bootstrap skipped: %s", error
334
+ )
335
+ return
336
+ if result in {"installed", "already_enabled"}:
337
+ logging.getLogger(__name__).info("official Codex DSH provider bootstrap: %s", result)
338
+
339
+ async def _provider_snapshot(
340
+ self, *, refresh: bool
341
+ ) -> tuple[dict[str, PluginManifest], dict[str, Any], tuple[str, ...] | None]:
342
+ manifests = dict(self._startup_provider_manifests)
343
+ factories = dict(self._startup_provider_factories)
344
+ manager = self._dsh_provider_registration_manager
345
+ if manager is None:
346
+ return manifests, factories, None
347
+ try:
348
+ registrations = await (manager.refresh() if refresh else manager.start())
349
+ except StudioDshProviderRegistrationError as error:
350
+ logging.getLogger(__name__).warning(
351
+ "DSH AgentProvider discovery failed closed: %s", error.code
352
+ )
353
+ return manifests, factories, None
354
+ if registrations.manifests.keys() != registrations.factories.keys():
355
+ logging.getLogger(__name__).warning(
356
+ "DSH AgentProvider discovery returned a partial registration set"
357
+ )
358
+ return manifests, factories, None
359
+ for provider_ref, manifest in registrations.manifests.items():
360
+ if provider_ref in manifests and (
361
+ manifests[provider_ref] != manifest
362
+ or factories[provider_ref] is not registrations.factories[provider_ref]
363
+ ):
364
+ logging.getLogger(__name__).warning(
365
+ "DSH AgentProvider registration conflicts with startup provider: %s",
366
+ provider_ref,
367
+ )
368
+ return (
369
+ dict(self._startup_provider_manifests),
370
+ dict(self._startup_provider_factories),
371
+ None,
372
+ )
373
+ manifests[provider_ref] = manifest
374
+ factories[provider_ref] = registrations.factories[provider_ref]
375
+ return manifests, factories, tuple(registrations.manifests)
376
+
377
+ def agent_provider_catalog(self) -> list[dict[str, Any]]:
378
+ """Return only external providers that reached Studio's bound selector."""
379
+
380
+ reserved = {
381
+ KSADK_HARNESS_AGENT_PROVIDER_PLUGIN_ID,
382
+ }
383
+ package_by_ref: dict[str, str] = {}
384
+ display_name_by_ref: dict[str, str] = {}
385
+ manager = self._dsh_provider_registration_manager
386
+ if manager is not None:
387
+ package_by_ref = {
388
+ item.provider_ref: item.package_name
389
+ for item in manager.inventory.packages
390
+ if item.state == "bound" and item.provider_ref is not None
391
+ }
392
+ display_name_by_ref = {
393
+ item.provider_ref: item.display_name
394
+ for item in manager.inventory.packages
395
+ if item.state == "bound" and item.provider_ref is not None and item.display_name
396
+ }
397
+ items = []
398
+ for provider_ref, manifest in sorted(self._active_provider_manifests.items()):
399
+ if manifest.metadata.id in reserved:
400
+ continue
401
+ items.append(
402
+ {
403
+ "providerRef": provider_ref,
404
+ "pluginId": package_by_ref.get(provider_ref, manifest.metadata.id),
405
+ "resolvedVersion": manifest.metadata.version,
406
+ "displayName": display_name_by_ref.get(provider_ref, manifest.metadata.id),
407
+ "state": "enabled",
408
+ "compatible": True,
409
+ "selectable": True,
410
+ "reason": None,
411
+ "permissions": list(manifest.spec.permissions),
412
+ "isolation": manifest.spec.isolation,
413
+ "configSchemaDeclared": manifest.spec.config_schema is not None,
414
+ "secretFields": list(manifest.spec.secret_fields),
415
+ }
416
+ )
417
+ return items
418
+
419
+ def dsh_provider_runtime_state(self, package_name: str) -> dict[str, Any] | None:
420
+ """Project one manager-owned package lifecycle state for plugin APIs."""
421
+
422
+ manager = self._dsh_provider_registration_manager
423
+ if manager is None:
424
+ return None
425
+ status = next(
426
+ (item for item in manager.inventory.packages if item.package_name == package_name),
427
+ None,
428
+ )
429
+ if status is None:
430
+ return None
431
+ return {
432
+ "state": status.state,
433
+ "providerRef": status.provider_ref,
434
+ "errorCode": status.error_code,
435
+ }
436
+
180
437
  def runtime_catalog(self) -> list[dict]:
181
438
  return inspect_runtime_catalog(self.runtime_executor)
182
439
 
440
+ def _scheduler_adapter_provider(self, spec: StudioRunSpec): # type: ignore[no-untyped-def]
441
+ if spec.plugin_bundle_root is not None:
442
+ return self.plugin_runs.kernel_adapter_provider(spec)
443
+ return lambda: self.runtime_executor.create_adapter(spec.launch_context)
444
+
445
+ def resolve_run_spec(
446
+ self,
447
+ build_id: str,
448
+ *,
449
+ model: str | None = None,
450
+ sandbox: str | None = None,
451
+ approval_mode: str | None = None,
452
+ ) -> StudioRunSpec:
453
+ """Resolve one immutable Build through its only compatible resolver."""
454
+
455
+ try:
456
+ return self.codex_runs.resolve(
457
+ build_id,
458
+ model=model,
459
+ sandbox=sandbox,
460
+ approval_mode=approval_mode,
461
+ )
462
+ except Exception as exc:
463
+ if getattr(exc, "status_code", None) != 404:
464
+ raise
465
+ framework_build = self.builds.get(build_id)
466
+ runtime_type = framework_build.runtime_type.strip().lower()
467
+ if runtime_type in {"harness", "plugin"}:
468
+ return self.plugin_runs.resolve(build_id, model=model)
469
+ return self.framework_runs.resolve(
470
+ build_id,
471
+ model=model,
472
+ approval_mode=approval_mode,
473
+ )
474
+
475
+ def validate_schedule_build(
476
+ self,
477
+ build_ref: str | None,
478
+ *,
479
+ agent_id: str | None = None,
480
+ ) -> None:
481
+ """Reject a ScheduledTask target build that is not a real immutable Build.
482
+
483
+ The agent-scoped authoring path resolves the Build server-side via
484
+ :meth:`ensure_current_build`, so this guard is primarily for the raw
485
+ ``/api/v1/schedules`` surface that accepts a caller-supplied
486
+ ``agent_version_ref``. A version name or tag (for example "v1") is
487
+ not an immutable Build id and must fail at admission with an actionable
488
+ error instead of collapsing into a later opaque DISPATCH_FAILED.
489
+ """
490
+ ref = (build_ref or "").strip()
491
+ if not ref:
492
+ raise StudioError(
493
+ "SCHEDULE_BUILD_REQUIRED",
494
+ "定时任务必须绑定一个已成功构建的 Build id(不能为空或使用版本名)",
495
+ status_code=422,
496
+ field="target.agentVersionRef",
497
+ )
498
+ try:
499
+ spec = self.resolve_run_spec(ref)
500
+ except StudioError as error:
501
+ if error.status_code == 404:
502
+ raise StudioError(
503
+ "SCHEDULE_BUILD_UNAVAILABLE",
504
+ f"定时任务绑定的 Build {ref!r} 不可用:请使用已成功构建的 Build id,而非版本名",
505
+ status_code=422,
506
+ field="target.agentVersionRef",
507
+ details={"buildId": ref},
508
+ ) from error
509
+ raise
510
+ except Exception as error:
511
+ raise StudioError(
512
+ "SCHEDULE_BUILD_UNAVAILABLE",
513
+ f"定时任务绑定的 Build {ref!r} 不可用:{error}",
514
+ status_code=422,
515
+ field="target.agentVersionRef",
516
+ details={"buildId": ref},
517
+ ) from error
518
+ if agent_id and spec.agent_id != agent_id:
519
+ raise StudioError(
520
+ "SCHEDULE_AGENT_MISMATCH",
521
+ "定时任务绑定的 Build 与所选 Agent 不一致",
522
+ status_code=422,
523
+ field="target.agentVersionRef",
524
+ details={"buildId": ref, "agentId": agent_id},
525
+ )
526
+
527
+ def conversation_surface(
528
+ self,
529
+ build_id: str,
530
+ *,
531
+ session_id: str,
532
+ ) -> ConversationSurface:
533
+ """Describe the actual local composer contract for one Build/session.
534
+
535
+ This is intentionally conservative. It derives the declaration from
536
+ the resolved Build, rather than guessing support from a model name or
537
+ sending provider-specific fields to every Runtime.
538
+ """
539
+
540
+ spec = self.resolve_run_spec(build_id)
541
+ runtime_type = spec.launch_context.runtime_type.strip().lower()
542
+ runtime_mode: Literal["native", "translated"] = (
543
+ "native" if runtime_type == "codex" else "translated"
544
+ )
545
+ inputs = [ConversationCapability(name="text", mode="native")]
546
+ if spec.model:
547
+ inputs.append(ConversationCapability(name="model.select", mode=runtime_mode))
548
+ inputs.append(ConversationCapability(name="approval", mode=runtime_mode))
549
+ if runtime_type == "codex":
550
+ inputs.extend(
551
+ (
552
+ # Studio projects image data URLs to native Codex ImageInput.
553
+ ConversationCapability(name="attachment.image", mode="native"),
554
+ # Bounded UTF-8/code attachments are made model-visible as
555
+ # deterministic text parts by the Responses compatibility
556
+ # adapter. Binary files remain unavailable in Phase 2.
557
+ ConversationCapability(name="attachment.file", mode="translated"),
558
+ ConversationCapability(name="reasoning.effort", mode="native"),
559
+ ConversationCapability(name="goal", mode="native"),
560
+ ConversationCapability(name="plan", mode="native"),
561
+ )
562
+ )
563
+
564
+ outputs = [
565
+ ConversationCapability(name="text", mode="native"),
566
+ ConversationCapability(name="streaming", mode=runtime_mode),
567
+ ]
568
+ if runtime_type == "codex":
569
+ outputs.extend(
570
+ (
571
+ ConversationCapability(name="reasoning", mode="native"),
572
+ ConversationCapability(name="tool.inspect", mode="native"),
573
+ ConversationCapability(name="approval", mode="native"),
574
+ ConversationCapability(name="goal", mode="native"),
575
+ ConversationCapability(name="plan", mode="native"),
576
+ ConversationCapability(name="cancel", mode="native"),
577
+ )
578
+ )
579
+ return ConversationSurface(
580
+ surface_id=f"studio.build.{spec.build_id}",
581
+ session_id=session_id,
582
+ provider_ref=f"studio.runtime.{runtime_type}",
583
+ inputs=tuple(inputs),
584
+ outputs=tuple(outputs),
585
+ )
586
+
587
+ async def ensure_current_build(self, agent_id: str):
588
+ """Return the Build that is eligible for a local Studio turn.
589
+
590
+ This is deliberately one shared resolver for Web chat and Scheduler
591
+ authoring. A Scheduler must never select a mutable Draft or a stale
592
+ Codex manifest merely because a UI happened to show it as an Agent.
593
+ """
594
+
595
+ await self.start()
596
+ if self.is_codex_agent(agent_id):
597
+ self.codex_agent_detail(agent_id)
598
+ builds = [
599
+ record
600
+ for record in self.codex_builds.list()
601
+ if record.agent_name == agent_id and self.codex_builder.is_current(record)
602
+ ]
603
+ if builds:
604
+ return builds[0]
605
+ return await asyncio.to_thread(self.codex_builder.build, agent_id)
606
+ draft = self.drafts.get(agent_id)
607
+ composition_required = self.plugin_compositions.required_for(draft)
608
+ for record in self.builds.list_for_agent(agent_id):
609
+ if record.status == BuildStatus.SUCCEEDED:
610
+ if composition_required and (
611
+ record.source_revision != draft.metadata.revision
612
+ or not self._build_has_composition(record)
613
+ ):
614
+ continue
615
+ if composition_required:
616
+ composition = self.plugin_compositions.compile_if_required(draft)
617
+ if composition is None: # pragma: no cover - guarded by required_for
618
+ raise StudioError(
619
+ "PLUGIN_COMPOSITION_REQUIRED",
620
+ "当前 Agent 需要插件组合,但无法生成 Composition",
621
+ status_code=409,
622
+ )
623
+ self.plugin_compositions.bind_build(
624
+ composition,
625
+ agent_id=draft.metadata.id,
626
+ build_id=record.id,
627
+ )
628
+ return record
629
+ if draft.spec.model is None and not draft.spec.bindings.model_profile_id:
630
+ raise StudioError(
631
+ "AGENT_MODEL_REQUIRED",
632
+ "当前 Agent 未绑定 Model Profile,请先在 Agent 配置中选择模型;"
633
+ "API Key 只提供访问凭证,不会自动绑定模型。",
634
+ status_code=422,
635
+ field="spec.bindings.modelProfileId",
636
+ )
637
+ return await asyncio.to_thread(self._build_agent_bundle, draft)
638
+
639
+ def _build_agent_bundle(self, draft: AgentDraft):
640
+ composition = self.plugin_compositions.compile_if_required(draft)
641
+ record = self.builder.build(draft, composition=composition)
642
+ if composition is not None:
643
+ self.plugin_compositions.bind_build(
644
+ composition,
645
+ agent_id=draft.metadata.id,
646
+ build_id=record.id,
647
+ )
648
+ return record
649
+
650
+ def _build_has_composition(self, record: Any) -> bool:
651
+ """Reject stale pre-Phase-2 builds for a composed runtime.
652
+
653
+ This check is intentionally scoped to Harness/plugin runtimes. Legacy
654
+ ADK/LangGraph build selection retains its existing behavior.
655
+ """
656
+
657
+ if not record.artifact_path:
658
+ return False
659
+ try:
660
+ archive = self.workspace.resolve(record.artifact_path, must_exist=True)
661
+ with zipfile.ZipFile(archive) as bundle:
662
+ manifest = json.loads(bundle.read("manifest.json"))
663
+ names = frozenset(bundle.namelist())
664
+ return bool(
665
+ isinstance(manifest, dict)
666
+ and manifest.get("compositionProfileDigest")
667
+ and "composition-profile.json" in names
668
+ and "plugin-lock.json" in names
669
+ )
670
+ except (KeyError, OSError, UnicodeError, ValueError, zipfile.BadZipFile):
671
+ return False
672
+
673
+ async def create_agent_schedule(
674
+ self,
675
+ agent_id: str,
676
+ *,
677
+ display_name: str,
678
+ prompt: str,
679
+ schedule: ScheduleSpec,
680
+ enabled: bool = True,
681
+ continuity: Literal["new_session", "continue_session"] = "new_session",
682
+ session_id: str | None = None,
683
+ ) -> ScheduledTask:
684
+ """Create a local-only schedule for the Kernel Runtime that owns it.
685
+
686
+ The browser provides only human intent (prompt/calendar/continuity).
687
+ The trusted Studio process resolves the immutable Build and concrete
688
+ Kernel instance. Thus a task cannot be pointed at another Agent
689
+ Instance, smuggle a permit reference, or claim cloud scheduling.
690
+ """
691
+
692
+ await self.start()
693
+ build = await self.ensure_current_build(agent_id)
694
+ spec = self.resolve_run_spec(build.id)
695
+ await self.scheduler_runtimes.start()
696
+ try:
697
+ target = await self.scheduler_runtimes.ensure_build(
698
+ build.id,
699
+ expected_agent_id=agent_id,
700
+ )
701
+ runtime = self.scheduler_runtimes.runtime_for_build(build.id)
702
+ except StudioSchedulerRuntimeError as error:
703
+ raise StudioError(
704
+ error.code,
705
+ str(error),
706
+ status_code=409,
707
+ details={"agentId": agent_id, "buildId": build.id},
708
+ ) from error
709
+ if continuity == "continue_session" and not session_id:
710
+ raise StudioError(
711
+ "SCHEDULE_SESSION_REQUIRED",
712
+ "继续会话的定时任务必须选择一个已有会话。",
713
+ status_code=422,
714
+ field="sessionId",
715
+ )
716
+ if continuity == "continue_session":
717
+ self._require_schedule_continuation(runtime, spec.launch_context.runtime_type)
718
+ task = ScheduledTask(
719
+ task_id=f"sched-{uuid4().hex[:20]}",
720
+ display_name=display_name,
721
+ target=ScheduledTaskTarget(
722
+ agent_id=agent_id,
723
+ tenant_id=target.tenant_id,
724
+ agent_instance_id=target.agent_instance_id,
725
+ agent_version_ref=str(build.id),
726
+ session_id=session_id,
727
+ # This is an opaque local authority marker, never a browser
728
+ # supplied credential. The dispatcher obtains a short-lived
729
+ # in-process permit at the AgentControl ingress boundary.
730
+ authorization_ref="runtime://local-kernel-ingress",
731
+ ),
732
+ schedule=schedule,
733
+ command=ScheduleCommandTemplate(payload={"content": prompt}),
734
+ enabled=enabled,
735
+ continuity=continuity,
736
+ )
737
+ return self.scheduler.create_task(task)
738
+
739
+ @staticmethod
740
+ def _require_schedule_continuation(runtime: Any, runtime_type: str) -> None:
741
+ """Fail closed unless the bound Provider can preserve one conversation.
742
+
743
+ Harness owns process-local Session history without exposing the generic
744
+ checkpoint ``resume`` verb. Codex and external adapters must declare
745
+ the typed resume capability. An older Runtime that lacks either proof
746
+ remains usable for normal conversations and ``new_session`` schedules;
747
+ only the optional follow-up mode is rejected.
748
+ """
749
+
750
+ if runtime_type.strip().lower() == "harness":
751
+ return
752
+ kernel = getattr(runtime, "kernel", None)
753
+ describe = getattr(kernel, "capabilities", None)
754
+ matrix = describe() if callable(describe) else None
755
+ resume = getattr(matrix, "resume", None)
756
+ if not bool(getattr(resume, "supported", False)):
757
+ raise StudioError(
758
+ "SCHEDULE_CONTINUATION_UNAVAILABLE",
759
+ "当前 Runtime 未声明可恢复的会话续接能力;可改用新会话定时任务。",
760
+ status_code=409,
761
+ field="continuity",
762
+ details={"runtimeType": runtime_type},
763
+ )
764
+
765
+ def list_agent_schedules(self, agent_id: str) -> list[ScheduledTask]:
766
+ return [task for task in self.scheduler.list_tasks() if task.target.agent_id == agent_id]
767
+
768
+ def get_agent_schedule(self, agent_id: str, task_id: str) -> ScheduledTask:
769
+ task = self.scheduler.get_task(task_id)
770
+ if task.target.agent_id != agent_id:
771
+ from ksadk.studio.errors import not_found
772
+
773
+ raise not_found("schedule", task_id)
774
+ return task
775
+
776
+ def update_agent_schedule(
777
+ self,
778
+ agent_id: str,
779
+ task_id: str,
780
+ *,
781
+ display_name: str,
782
+ prompt: str,
783
+ schedule: ScheduleSpec,
784
+ enabled: bool,
785
+ continuity: Literal["new_session", "continue_session"],
786
+ session_id: str | None,
787
+ ) -> ScheduledTask:
788
+ existing = self.get_agent_schedule(agent_id, task_id)
789
+ if continuity == "continue_session" and not session_id:
790
+ raise StudioError(
791
+ "SCHEDULE_SESSION_REQUIRED",
792
+ "继续会话的定时任务必须选择一个已有会话。",
793
+ status_code=422,
794
+ field="sessionId",
795
+ )
796
+ task = existing.model_copy(
797
+ update={
798
+ "display_name": display_name,
799
+ "schedule": schedule,
800
+ "command": ScheduleCommandTemplate(payload={"content": prompt}),
801
+ "enabled": enabled,
802
+ "continuity": continuity,
803
+ "target": existing.target.model_copy(update={"session_id": session_id}),
804
+ }
805
+ )
806
+ return self.scheduler.update_task(task_id, task)
807
+
808
+ async def run_agent_schedule_now(self, agent_id: str, task_id: str):
809
+ self.get_agent_schedule(agent_id, task_id)
810
+ return await self.scheduler.run_now(task_id)
811
+
183
812
  def codex_manifest_state(self, agent_id: str | None = None) -> dict:
184
813
  return self.codex_agents.manifest_state(agent_id)
185
814
 
@@ -210,11 +839,13 @@ class StudioService:
210
839
  spec: AgentSpec,
211
840
  *,
212
841
  expected_revision: int,
842
+ name: str | None = None,
213
843
  ) -> AgentDraft:
214
844
  return self.codex_agents.update(
215
845
  agent_id,
216
846
  spec,
217
847
  expected_revision=expected_revision,
848
+ name=name,
218
849
  )
219
850
 
220
851
  def delete_codex_agent(self, agent_id: str, *, purge: bool = False) -> None:
@@ -249,6 +880,7 @@ class StudioService:
249
880
  approval_mode: str | None = None,
250
881
  collaboration_mode: str | None = None,
251
882
  goal_objective: str | None = None,
883
+ reasoning_effort: str | None = None,
252
884
  runtime_input: Any = None,
253
885
  idempotency_key: str,
254
886
  on_event: Callable[[RunEvent], None] | None = None,
@@ -262,12 +894,13 @@ class StudioService:
262
894
  approval_mode=approval_mode,
263
895
  collaboration_mode=collaboration_mode,
264
896
  goal_objective=goal_objective,
897
+ reasoning_effort=reasoning_effort,
265
898
  runtime_input=runtime_input,
266
899
  idempotency_key=idempotency_key,
267
900
  on_event=on_event,
268
901
  )
269
902
 
270
- def delete_session(self, session_id: str) -> None:
903
+ async def delete_session(self, session_id: str) -> None:
271
904
  from ksadk.studio.errors import not_found
272
905
 
273
906
  runs = self.event_store.list_runs(session_id=session_id)
@@ -280,8 +913,132 @@ class StudioService:
280
913
  status_code=409,
281
914
  details={"sessionId": session_id},
282
915
  )
916
+ await self.plugin_runs.close_session(session_id)
917
+ await self.session_service.delete_session(session_id)
283
918
  self.event_store.delete_session(session_id)
284
919
 
920
+ async def aclose(self) -> None:
921
+ """Release local provider activations and supervised plugin processes."""
922
+
923
+ await self.plugin_runs.aclose()
924
+ if self._dsh_provider_registration_manager is not None:
925
+ await self._dsh_provider_registration_manager.aclose()
926
+
927
+ async def _require_runtime_session(self, session_id: str) -> None:
928
+ if await self.session_service.get_session_metadata(session_id) is None:
929
+ raise StudioError(
930
+ "SESSION_NOT_FOUND",
931
+ "session 不存在",
932
+ status_code=404,
933
+ details={"id": session_id},
934
+ )
935
+
936
+ async def trajectory_page(
937
+ self,
938
+ session_id: str,
939
+ *,
940
+ before_seq_id: int | None = None,
941
+ invocation_id: str | None = None,
942
+ limit: int = 100,
943
+ ) -> dict[str, Any]:
944
+ """Return the bounded, canonical RuntimeEvent trajectory for a session."""
945
+
946
+ await self._require_runtime_session(session_id)
947
+ events = await self.runtime_events.list(
948
+ session_id,
949
+ before_seq=before_seq_id,
950
+ run_id=invocation_id,
951
+ limit=limit,
952
+ )
953
+ oldest_seq_id = events[0].seq if events else None
954
+ latest_seq_id = events[-1].seq if events else None
955
+ older = (
956
+ await self.runtime_events.list(
957
+ session_id,
958
+ before_seq=oldest_seq_id,
959
+ run_id=invocation_id,
960
+ limit=1,
961
+ )
962
+ if oldest_seq_id is not None
963
+ else []
964
+ )
965
+ return {
966
+ "items": [project_trajectory_event(event) for event in events],
967
+ "page": {
968
+ "oldestSeqId": oldest_seq_id,
969
+ "latestSeqId": latest_seq_id,
970
+ "hasMore": bool(older),
971
+ },
972
+ }
973
+
974
+ async def stream_trajectory(
975
+ self,
976
+ session_id: str,
977
+ after_seq_id: int = 0,
978
+ *,
979
+ invocation_id: str | None = None,
980
+ ):
981
+ await self._require_runtime_session(session_id)
982
+ cursor = after_seq_id
983
+ while True:
984
+ async for event in self.runtime_events.subscribe_session(
985
+ session_id,
986
+ after_seq=cursor,
987
+ poll_interval=min(0.25, _TRAJECTORY_KEEPALIVE_SECONDS),
988
+ timeout=_TRAJECTORY_KEEPALIVE_SECONDS,
989
+ ):
990
+ cursor = event.seq
991
+ if invocation_id is not None and event.run_id != invocation_id:
992
+ continue
993
+ yield encode_sse(project_trajectory_event(event), event_id=cursor)
994
+ yield ": keepalive\n\n"
995
+
996
+ async def export_runtime_session(
997
+ self,
998
+ session_id: str,
999
+ *,
1000
+ filename: str,
1001
+ invocation_id: str | None = None,
1002
+ ) -> dict[str, Any]:
1003
+ await self._require_runtime_session(session_id)
1004
+ relative = Path(filename)
1005
+ if (
1006
+ not filename
1007
+ or relative.is_absolute()
1008
+ or len(relative.parts) != 1
1009
+ or relative.name in {".", ".."}
1010
+ ):
1011
+ raise StudioError(
1012
+ "SESSION_EXPORT_FILENAME_INVALID",
1013
+ "导出文件名必须是普通文件名",
1014
+ status_code=422,
1015
+ field="filename",
1016
+ )
1017
+ export_dir = self.workspace.resolve(".agentkit/exports")
1018
+ export_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
1019
+ export_dir.chmod(0o700)
1020
+ try:
1021
+ result = await export_session_log(
1022
+ self.session_service,
1023
+ session_id,
1024
+ export_dir / relative.name,
1025
+ invocation_id=invocation_id,
1026
+ )
1027
+ except SessionLogError as exc:
1028
+ code = str(exc).partition(":")[0]
1029
+ raise StudioError(
1030
+ code,
1031
+ str(exc),
1032
+ status_code=409 if code == "SESSION_LOG_TARGET_EXISTS" else 422,
1033
+ ) from exc
1034
+ return {
1035
+ "path": result.path.relative_to(self.workspace.root).as_posix(),
1036
+ "eventCount": result.event_count,
1037
+ "firstSeqId": result.first_seq_id,
1038
+ "lastSeqId": result.last_seq_id,
1039
+ "exportedThroughSeqId": result.exported_through_seq_id,
1040
+ }
1041
+
285
1042
  async def test_model_profile(self, resource_id: str) -> dict:
286
1043
  return await test_model_profile_connection(
287
1044
  catalog=self.catalog,
@@ -313,6 +1070,10 @@ class StudioService:
313
1070
  description=description,
314
1071
  )
315
1072
  self._validate_bindings(resolved_spec.bindings)
1073
+ self._validate_framework_binding_support(
1074
+ resolved_spec.runtime,
1075
+ resolved_spec.bindings,
1076
+ )
316
1077
  draft = self.drafts.create(
317
1078
  agent_id=agent_id,
318
1079
  name=name,
@@ -321,7 +1082,11 @@ class StudioService:
321
1082
  spec=resolved_spec,
322
1083
  labels=labels,
323
1084
  )
324
- materialize_generated_runtime_source(self.workspace, draft)
1085
+ materialize_generated_runtime_source(
1086
+ self.workspace,
1087
+ draft,
1088
+ catalog=self.catalog,
1089
+ )
325
1090
  return draft
326
1091
 
327
1092
  def create_authored_agent(
@@ -382,12 +1147,31 @@ class StudioService:
382
1147
  *,
383
1148
  messages: list[dict[str, str]],
384
1149
  model_profile_id: str,
1150
+ runtime_type: str = "codex",
1151
+ agent_model_profile_ids: list[str] | None = None,
1152
+ agent_default_model_profile_id: str | None = None,
1153
+ tool_resource_ids: list[str] | None = None,
1154
+ mcp_resource_ids: list[str] | None = None,
1155
+ skill_resource_ids: list[str] | None = None,
1156
+ request_id: str | None = None,
385
1157
  ) -> dict:
386
1158
  return await self.authoring.compose_conversation(
387
1159
  messages=messages,
388
1160
  model_profile_id=model_profile_id,
1161
+ runtime_type=runtime_type,
1162
+ agent_model_profile_ids=agent_model_profile_ids,
1163
+ agent_default_model_profile_id=agent_default_model_profile_id,
1164
+ tool_resource_ids=tool_resource_ids,
1165
+ mcp_resource_ids=mcp_resource_ids,
1166
+ skill_resource_ids=skill_resource_ids,
1167
+ request_id=request_id,
389
1168
  )
390
1169
 
1170
+ def conversation_authoring_status(self, request_id: str) -> dict | None:
1171
+ """Return the in-memory stage snapshot for one authoring request."""
1172
+
1173
+ return self.authoring.conversation_status(request_id)
1174
+
391
1175
  def is_codex_agent(self, agent_id: str) -> bool:
392
1176
  """Return whether one Agent is backed by the Codex YAML contract."""
393
1177
 
@@ -406,6 +1190,241 @@ class StudioService:
406
1190
  return framework.strip().lower() or "adk"
407
1191
  return runtime.type
408
1192
 
1193
+ def _agent_prompt_sources(self, agent_id: str) -> tuple[str, str, str]:
1194
+ """Resolve the canonical prompt inputs used by preview and execution."""
1195
+
1196
+ if self.is_codex_agent(agent_id):
1197
+ snapshot = self.codex_manifests.load(agent_id)
1198
+ return (
1199
+ snapshot.manifest.prompt or "",
1200
+ snapshot.manifest.task_prompt or "",
1201
+ "codex",
1202
+ )
1203
+ draft = self.drafts.get(agent_id)
1204
+ instructions = draft.spec.instructions
1205
+ return (
1206
+ str(instructions.system or ""),
1207
+ str(instructions.task or ""),
1208
+ self.agent_runtime_type(agent_id),
1209
+ )
1210
+
1211
+ def compile_prompt_preview(
1212
+ self,
1213
+ agent_id: str,
1214
+ *,
1215
+ request_instructions: str = "",
1216
+ include_content: bool = False,
1217
+ ) -> dict:
1218
+ """Compile the real prompt without creating a Session, Trace, or Build."""
1219
+
1220
+ from ksadk.prompts.resolved import (
1221
+ ResolvedPromptSources,
1222
+ compile_resolved_prompt_dict,
1223
+ get_default_platform_policy_source,
1224
+ )
1225
+
1226
+ agent_system, agent_task, runtime_type = self._agent_prompt_sources(agent_id)
1227
+ compiled = compile_resolved_prompt_dict(
1228
+ ResolvedPromptSources(
1229
+ agent_system=agent_system,
1230
+ agent_task=agent_task,
1231
+ request_instructions=str(request_instructions or "").strip(),
1232
+ platform_policy_source=get_default_platform_policy_source(),
1233
+ )
1234
+ )
1235
+ if compiled is None:
1236
+ return {
1237
+ "promptVersion": "v1",
1238
+ "contentHash": "",
1239
+ "stablePrefixHash": "",
1240
+ "sections": [],
1241
+ "runtimeType": runtime_type,
1242
+ "warnings": ["no prompt content to compile"],
1243
+ }
1244
+ result: dict[str, Any] = {
1245
+ "promptVersion": compiled["prompt_compiler_version"],
1246
+ "contentHash": compiled["prompt_content_hash"],
1247
+ "stablePrefixHash": compiled["prompt_stable_prefix_hash"],
1248
+ "sectionHashes": compiled["prompt_section_hashes"],
1249
+ "tokensBySection": compiled["prompt_tokens_by_section"],
1250
+ "estimatedTokens": compiled["prompt_estimated_tokens"],
1251
+ "sectionCount": compiled["prompt_section_count"],
1252
+ "runtimeType": runtime_type,
1253
+ "platformPolicyVersion": compiled.get("prompt_platform_policy_version"),
1254
+ "warnings": [],
1255
+ }
1256
+ if include_content:
1257
+ result["content"] = compiled["prompt_content"]
1258
+ return result
1259
+
1260
+ def reveal_run_prompt(self, run_id: str) -> dict:
1261
+ """Rebuild immutable Run prompt sections and reveal only on hash match."""
1262
+
1263
+ from ksadk.prompts.resolved import (
1264
+ ResolvedPromptSources,
1265
+ compile_resolved_prompt_dict,
1266
+ get_default_platform_policy_source,
1267
+ sections_from_resolved_sources,
1268
+ )
1269
+
1270
+ record = self.event_store.get(run_id)
1271
+ resolver = self.codex_runs if record.runtime_type == "codex" else self.framework_runs
1272
+ spec = resolver.resolve(record.build_id, model=record.model or None)
1273
+ config = dict(spec.request_config or {})
1274
+ sources = ResolvedPromptSources(
1275
+ agent_system=str(config.get("agent_system") or ""),
1276
+ agent_task=str(config.get("agent_task") or ""),
1277
+ request_instructions=str(config.get("instructions") or ""),
1278
+ platform_policy_source=get_default_platform_policy_source(),
1279
+ )
1280
+ compiled = compile_resolved_prompt_dict(sources)
1281
+ expected_hash = str((record.prompt_evidence or {}).get("contentHash") or "")
1282
+ actual_hash = str((compiled or {}).get("prompt_content_hash") or "")
1283
+ if not compiled or not expected_hash or actual_hash != expected_hash:
1284
+ return {
1285
+ "available": False,
1286
+ "reason": "无法证明重建内容与本次运行一致,已拒绝展示正文。",
1287
+ }
1288
+ return {
1289
+ "available": True,
1290
+ "contentHash": actual_hash,
1291
+ "sections": [
1292
+ {
1293
+ "id": section.section_id,
1294
+ "source": section.source,
1295
+ "content": section.content,
1296
+ }
1297
+ for section in sections_from_resolved_sources(sources)
1298
+ ],
1299
+ }
1300
+
1301
+ async def preview_context(
1302
+ self,
1303
+ agent_id: str,
1304
+ *,
1305
+ user_input: str = "",
1306
+ request_instructions: str = "",
1307
+ simulated_history: list[dict[str, str]] | None = None,
1308
+ include_content: bool = False,
1309
+ ) -> dict:
1310
+ """Run the production context planner without model or persistence side effects."""
1311
+
1312
+ from ksadk.context_engine.capabilities import capabilities_for_runtime_type
1313
+ from ksadk.context_engine.hosted_pipeline import run_hosted_pipeline
1314
+ from ksadk.prompts.resolved import (
1315
+ ResolvedPromptSources,
1316
+ compile_resolved_prompt_dict,
1317
+ get_default_platform_policy_source,
1318
+ )
1319
+
1320
+ agent_system, agent_task, runtime_type = self._agent_prompt_sources(agent_id)
1321
+ capabilities = capabilities_for_runtime_type(runtime_type)
1322
+ compiled_prompt = compile_resolved_prompt_dict(
1323
+ ResolvedPromptSources(
1324
+ agent_system=agent_system,
1325
+ agent_task=agent_task,
1326
+ request_instructions=str(request_instructions or "").strip(),
1327
+ platform_policy_source=get_default_platform_policy_source(),
1328
+ )
1329
+ )
1330
+ history = [
1331
+ {
1332
+ "role": item.get("role", "user"),
1333
+ "content": item.get("content", ""),
1334
+ }
1335
+ for item in (simulated_history or [])
1336
+ ]
1337
+ pipeline = await run_hosted_pipeline(
1338
+ compiled_prompt=compiled_prompt,
1339
+ user_input=str(user_input or "").strip(),
1340
+ history=history,
1341
+ working_state=None,
1342
+ model_metadata={
1343
+ "context_window_tokens": 200_000,
1344
+ "max_output_tokens": 32_000,
1345
+ },
1346
+ contributors=None,
1347
+ integration_mode=capabilities.integration_mode,
1348
+ accounting_accuracy="estimated",
1349
+ session_id=f"preview-{agent_id}",
1350
+ invocation_id=f"preview-{agent_id}",
1351
+ )
1352
+ if pipeline is None:
1353
+ return {
1354
+ "accuracy": "estimated",
1355
+ "warnings": ["no content to plan"],
1356
+ "items": [],
1357
+ }
1358
+ plan = pipeline.plan
1359
+ assembled = pipeline.assembled
1360
+ return {
1361
+ "accuracy": plan.get("accounting_accuracy", "estimated"),
1362
+ "policyVersion": plan.get("policy_version"),
1363
+ "planId": plan.get("plan_id"),
1364
+ "budget": {
1365
+ "maxInputTokens": (plan.get("budget") or {}).get("max_input_tokens"),
1366
+ "softLimitTokens": (plan.get("budget") or {}).get("soft_limit_tokens"),
1367
+ "hardLimitTokens": (plan.get("budget") or {}).get("hard_limit_tokens"),
1368
+ },
1369
+ "items": plan.get("selected", []),
1370
+ "decisions": plan.get("decisions", []),
1371
+ "totalsByKind": plan.get("tokens_by_kind", {}),
1372
+ "plannedInputTokens": plan.get("planned_input_tokens"),
1373
+ "warnings": list(assembled.warnings),
1374
+ "projection": {
1375
+ "runtimeType": runtime_type,
1376
+ "integrationMode": capabilities.integration_mode,
1377
+ "promptOwner": capabilities.prompt_owner,
1378
+ },
1379
+ **({"system": assembled.system} if include_content else {}),
1380
+ }
1381
+
1382
+ def detect_importable_project(self) -> dict | None:
1383
+ """Expose a root framework project for explicit Studio import only."""
1384
+ from ksadk.studio.manifest_resolver import detect_manifest_kind
1385
+
1386
+ result = detect_manifest_kind(self.workspace.root)
1387
+ if result.kind != "framework":
1388
+ return None
1389
+ import yaml
1390
+
1391
+ try:
1392
+ payload = yaml.safe_load(result.path.read_text(encoding="utf-8-sig")) or {}
1393
+ except Exception: # noqa: BLE001
1394
+ return None
1395
+ return {
1396
+ "kind": "framework",
1397
+ "runtimeType": result.framework or result.runtime_type,
1398
+ "name": str(payload.get("name") or self.workspace.root.name or "imported-agent"),
1399
+ "model": str(payload.get("model") or ""),
1400
+ "prompt": str(payload.get("prompt") or payload.get("instruction") or ""),
1401
+ "task": str(payload.get("task") or ""),
1402
+ "manifestPath": "agentengine.yaml",
1403
+ "requiresConfirmation": True,
1404
+ }
1405
+
1406
+ def import_root_project(
1407
+ self,
1408
+ *,
1409
+ name: str | None = None,
1410
+ slug: str | None = None,
1411
+ ) -> AgentDraft:
1412
+ """Import an explicitly detected root Framework project as a Studio draft."""
1413
+
1414
+ importable = self.detect_importable_project()
1415
+ if importable is None:
1416
+ raise StudioError(
1417
+ "PROJECT_NOT_IMPORTABLE",
1418
+ "当前工作区根没有可导入的 Framework 项目",
1419
+ status_code=422,
1420
+ )
1421
+ inspection = self.inspect_agent_project(".")
1422
+ return self.commit_agent_project(
1423
+ inspection["inspectionToken"],
1424
+ name=name or importable.get("name"),
1425
+ slug=slug or importable.get("name"),
1426
+ )
1427
+
409
1428
  def list_agents(self, *, query: str = "", limit: int = 50) -> list[AgentDraft]:
410
1429
  """List all local Agents from one registry view across runtime types."""
411
1430
 
@@ -429,12 +1448,36 @@ class StudioService:
429
1448
 
430
1449
  def agent_detail(self, agent_id: str) -> dict:
431
1450
  if self.is_codex_agent(agent_id):
432
- return self.codex_agent_detail(agent_id)
1451
+ detail = self.codex_agent_detail(agent_id)
1452
+ detail["soulProjection"] = self._soul_projection(detail["draft"])
1453
+ return detail
433
1454
  draft = self.drafts.get(agent_id)
434
1455
  return {
435
1456
  "draft": draft,
436
1457
  "builds": self.builds.list_for_agent(agent_id)[:10],
437
1458
  "validation": self.validator.validate(draft),
1459
+ "soulProjection": self._soul_projection(draft),
1460
+ }
1461
+
1462
+ @staticmethod
1463
+ def _soul_projection(draft: AgentDraft) -> dict[str, object]:
1464
+ """Describe the reviewed Soul source without claiming runtime learning."""
1465
+
1466
+ soul = draft.spec.soul
1467
+ runtime_type = draft.spec.runtime.type if draft.spec.runtime is not None else ""
1468
+ compile_target = {
1469
+ "codex": "managed-runtime.base_instructions",
1470
+ "plugin": "instructions/soul.md",
1471
+ }.get(runtime_type, "resolved-agent-spec.instructions.system")
1472
+ return {
1473
+ "present": soul is not None,
1474
+ "source": "AgentSpec.soul",
1475
+ "sourceRevision": draft.metadata.revision,
1476
+ "schemaVersion": soul.schema_version if soul is not None else "agentkit.soul/v1",
1477
+ "digest": soul_digest(soul) if soul is not None else None,
1478
+ "digestAlgorithm": "sha256-canonical-json",
1479
+ "compileTarget": compile_target,
1480
+ "compileOrder": "before-instructions.system",
438
1481
  }
439
1482
 
440
1483
  def create_studio_agent(
@@ -483,6 +1526,7 @@ class StudioService:
483
1526
  spec: AgentSpec,
484
1527
  *,
485
1528
  expected_revision: int,
1529
+ name: str | None = None,
486
1530
  ) -> AgentDraft:
487
1531
  if self.is_codex_agent(agent_id):
488
1532
  spec.runtime = self.agent_detail(agent_id)["draft"].spec.runtime
@@ -492,6 +1536,7 @@ class StudioService:
492
1536
  agent_id,
493
1537
  spec,
494
1538
  expected_revision=expected_revision,
1539
+ name=name,
495
1540
  ),
496
1541
  )
497
1542
  current = self.drafts.get(agent_id)
@@ -508,7 +1553,12 @@ class StudioService:
508
1553
  )
509
1554
  return cast(
510
1555
  AgentDraft,
511
- self.update_agent(agent_id, spec, expected_revision=expected_revision),
1556
+ self.update_agent(
1557
+ agent_id,
1558
+ spec,
1559
+ expected_revision=expected_revision,
1560
+ name=name,
1561
+ ),
512
1562
  )
513
1563
 
514
1564
  def update_studio_agent_bindings(
@@ -580,32 +1630,11 @@ class StudioService:
580
1630
  revision: int,
581
1631
  idempotency_key: str,
582
1632
  ) -> Operation:
583
- runtime_type = self.agent_runtime_type(agent_id)
584
- runtime = next(
585
- (item for item in self.runtime_catalog() if item["runtimeType"] == runtime_type),
586
- None,
587
- )
588
- if runtime is None:
589
- from ksadk.studio.errors import StudioError
590
-
591
- raise StudioError(
592
- "RUNTIME_NOT_REGISTERED",
593
- "Agent 引用的 RuntimeAdapter 未注册",
594
- status_code=422,
595
- details={"runtimeType": runtime_type},
596
- )
597
- if runtime["status"] != "ready":
598
- from ksadk.studio.errors import StudioError
599
-
600
- raise StudioError(
601
- "RUNTIME_DEPENDENCY_MISSING",
602
- f"{runtime['displayName']} Runtime 依赖未安装",
603
- status_code=422,
604
- details={
605
- "runtimeType": runtime_type,
606
- "installCommand": runtime["installCommand"],
607
- },
608
- )
1633
+ # Codex build admission already runs the injected runtime inspector and
1634
+ # pins its result into the immutable build. Checking package metadata
1635
+ # first would reject an explicitly supplied RuntimeExecutor/inspector
1636
+ # (including an out-of-process Codex runtime) merely because the local
1637
+ # Studio interpreter does not have the optional wheel installed.
609
1638
  if self.is_codex_agent(agent_id):
610
1639
  detail = self.agent_detail(agent_id)
611
1640
  if revision != detail["draft"].metadata.revision:
@@ -620,6 +1649,12 @@ class StudioService:
620
1649
  idempotency_key=idempotency_key,
621
1650
  agent_id=agent_id,
622
1651
  )
1652
+
1653
+ # A framework Build compiles and seals source/configuration only. Its
1654
+ # optional Runtime dependency belongs to run preflight, and may live in
1655
+ # a different process or deployment image. Rejecting the Build from
1656
+ # this Studio interpreter's package metadata would make portable
1657
+ # bundles impossible and incorrectly couple authoring to execution.
623
1658
  return self.submit_build(
624
1659
  agent_id,
625
1660
  revision=revision,
@@ -646,6 +1681,7 @@ class StudioService:
646
1681
  approval_mode: str | None = None,
647
1682
  collaboration_mode: str | None = None,
648
1683
  goal_objective: str | None = None,
1684
+ reasoning_effort: str | None = None,
649
1685
  runtime_input: Any = None,
650
1686
  on_event: Callable[[RunEvent], None] | None = None,
651
1687
  ) -> Operation:
@@ -664,6 +1700,7 @@ class StudioService:
664
1700
  approval_mode=approval_mode,
665
1701
  collaboration_mode=collaboration_mode,
666
1702
  goal_objective=goal_objective,
1703
+ reasoning_effort=reasoning_effort,
667
1704
  runtime_input=runtime_input,
668
1705
  idempotency_key=idempotency_key,
669
1706
  on_event=on_event,
@@ -677,6 +1714,7 @@ class StudioService:
677
1714
  approval_mode=approval_mode,
678
1715
  collaboration_mode=collaboration_mode,
679
1716
  goal_objective=goal_objective,
1717
+ reasoning_effort=reasoning_effort,
680
1718
  runtime_input=runtime_input,
681
1719
  idempotency_key=idempotency_key,
682
1720
  on_event=on_event,
@@ -725,15 +1763,50 @@ class StudioService:
725
1763
  spec: AgentSpec,
726
1764
  *,
727
1765
  expected_revision: int,
1766
+ name: str | None = None,
728
1767
  ):
729
- self._validate_bindings(spec.bindings)
730
- updated = self.drafts.update(
1768
+ current = self.drafts.get(agent_id)
1769
+ if current.metadata.revision != expected_revision:
1770
+ raise StudioError(
1771
+ "AGENT_REVISION_CONFLICT",
1772
+ "Agent 已被其他操作更新",
1773
+ status_code=409,
1774
+ field="metadata.revision",
1775
+ details={
1776
+ "expected": expected_revision,
1777
+ "actual": current.metadata.revision,
1778
+ },
1779
+ )
1780
+ materializable_bindings = self._materializable_update_bindings(
1781
+ current.spec.bindings,
1782
+ spec.bindings,
1783
+ runtime=spec.runtime or current.spec.runtime,
1784
+ )
1785
+ self._validate_bindings(materializable_bindings)
1786
+ self._validate_framework_binding_support(
1787
+ spec.runtime or current.spec.runtime,
1788
+ materializable_bindings,
1789
+ )
1790
+ preview = current.model_copy(deep=True)
1791
+ preview.metadata.revision += 1
1792
+ if name is not None:
1793
+ preview.metadata.name = name
1794
+ preview.spec = spec.model_copy(deep=True)
1795
+ preview.spec.bindings = materializable_bindings
1796
+ # Generated source is a materialized view of the candidate Revision.
1797
+ # Complete it before the authoritative draft write so any resolver,
1798
+ # digest or runtime-capability failure leaves Revision/content intact.
1799
+ materialize_generated_runtime_source(
1800
+ self.workspace,
1801
+ preview,
1802
+ catalog=self.catalog,
1803
+ )
1804
+ return self.drafts.update(
731
1805
  agent_id,
732
1806
  spec,
733
1807
  expected_revision=expected_revision,
1808
+ name=name,
734
1809
  )
735
- materialize_generated_runtime_source(self.workspace, updated)
736
- return updated
737
1810
 
738
1811
  def update_agent_bindings(
739
1812
  self,
@@ -742,11 +1815,10 @@ class StudioService:
742
1815
  *,
743
1816
  expected_revision: int,
744
1817
  ):
745
- self._validate_bindings(bindings)
746
1818
  draft = self.drafts.get(agent_id)
747
1819
  spec = draft.spec.model_copy(deep=True)
748
1820
  spec.bindings = bindings
749
- return self.drafts.update(
1821
+ return self.update_agent(
750
1822
  agent_id,
751
1823
  spec,
752
1824
  expected_revision=expected_revision,
@@ -760,6 +1832,116 @@ class StudioService:
760
1832
  self.catalog.resolve_mcp_tools(bindings)
761
1833
  self.catalog.resolve_skills(bindings)
762
1834
 
1835
+ def _materializable_update_bindings(
1836
+ self,
1837
+ current: AgentBindings,
1838
+ candidate: AgentBindings,
1839
+ *,
1840
+ runtime: RuntimeRef | None,
1841
+ ) -> AgentBindings:
1842
+ """Return bindings that can be safely resolved into this Revision.
1843
+
1844
+ Unknown bindings written by older Studio versions remain authoritative
1845
+ draft data, but are dormant until they re-enter the resource catalog.
1846
+ A new or modified unknown binding is still rejected by the normal
1847
+ resolver. Framework MCP bindings are likewise read-only until generated
1848
+ runtime source has a real injection path.
1849
+ """
1850
+
1851
+ if (
1852
+ runtime is not None
1853
+ and runtime.type in {"adk", "langgraph"}
1854
+ and candidate.mcp_servers != current.mcp_servers
1855
+ ):
1856
+ raise StudioError(
1857
+ "MCP_RUNTIME_INCOMPATIBLE",
1858
+ "当前 Runtime 尚未支持把 MCP 绑定注入生成源码;历史绑定仅可保留",
1859
+ status_code=422,
1860
+ field="spec.bindings.mcpServers",
1861
+ details={"runtimeType": runtime.type},
1862
+ )
1863
+
1864
+ known = {item.resource_id for item in self.catalog.list(limit=10_000)}
1865
+
1866
+ def selected_for_materialization(
1867
+ selected: list,
1868
+ historical: list,
1869
+ ) -> list:
1870
+ return [
1871
+ binding
1872
+ for binding in selected
1873
+ if binding.resource_id in known or binding not in historical
1874
+ ]
1875
+
1876
+ resolved = candidate.model_copy(deep=True)
1877
+ if candidate.model_profile_id not in known:
1878
+ unchanged_model = (
1879
+ candidate.model_profile_id == current.model_profile_id
1880
+ and candidate.model_profile_ids == current.model_profile_ids
1881
+ )
1882
+ if unchanged_model:
1883
+ resolved.model_profile_id = None
1884
+ resolved.model_profile_ids = []
1885
+ if resolved.model_profile_id is not None:
1886
+ resolved.model_profile_ids = [
1887
+ resource_id
1888
+ for resource_id in resolved.model_profile_ids
1889
+ if resource_id in known or resource_id not in current.model_profile_ids
1890
+ ]
1891
+ resolved.skills = selected_for_materialization(
1892
+ candidate.skills,
1893
+ current.skills,
1894
+ )
1895
+ resolved.tools = selected_for_materialization(
1896
+ candidate.tools,
1897
+ current.tools,
1898
+ )
1899
+ # No framework generated source consumes MCP today. Exact historical
1900
+ # values remain in `candidate`, while this preview omits them honestly.
1901
+ if runtime is not None and runtime.type in {"adk", "langgraph"}:
1902
+ resolved.mcp_servers = []
1903
+ else:
1904
+ resolved.mcp_servers = selected_for_materialization(
1905
+ candidate.mcp_servers,
1906
+ current.mcp_servers,
1907
+ )
1908
+ return resolved
1909
+
1910
+ def _validate_framework_binding_support(
1911
+ self,
1912
+ runtime: RuntimeRef | None,
1913
+ bindings: AgentBindings,
1914
+ ) -> None:
1915
+ if runtime is None or runtime.type not in {"adk", "langgraph"}:
1916
+ return
1917
+ if bindings.mcp_servers:
1918
+ raise StudioError(
1919
+ "MCP_RUNTIME_INCOMPATIBLE",
1920
+ "当前 Runtime 尚未支持把 MCP 绑定注入生成源码",
1921
+ status_code=422,
1922
+ field="spec.bindings.mcpServers",
1923
+ details={"runtimeType": runtime.type},
1924
+ )
1925
+ unsupported: list[str] = []
1926
+ for binding in bindings.tools:
1927
+ if not binding.enabled:
1928
+ continue
1929
+ descriptor = self.catalog.get(binding.resource_id)
1930
+ contract = ToolContract.model_validate(descriptor.contract)
1931
+ if contract.executor not in {"builtin", "python"}:
1932
+ unsupported.append(binding.resource_id)
1933
+ if unsupported:
1934
+ raise StudioError(
1935
+ "TOOL_RUNTIME_INCOMPATIBLE",
1936
+ "当前 Runtime 仅支持 builtin/python Tool",
1937
+ status_code=422,
1938
+ field="spec.bindings.tools",
1939
+ details={
1940
+ "runtimeType": runtime.type,
1941
+ "resourceIds": unsupported,
1942
+ },
1943
+ )
1944
+
763
1945
  def validate_agent(
764
1946
  self,
765
1947
  agent_id: str,
@@ -787,8 +1969,8 @@ class StudioService:
787
1969
  )
788
1970
  snapshot = draft.model_copy(deep=True)
789
1971
 
790
- async def runner():
791
- return await asyncio.to_thread(self.builder.build, snapshot)
1972
+ async def runner(_operation_id: str):
1973
+ return await asyncio.to_thread(self._build_agent_bundle, snapshot)
792
1974
 
793
1975
  return self.operations.submit(
794
1976
  kind=OperationKind.BUILD,
@@ -808,11 +1990,12 @@ class StudioService:
808
1990
  approval_mode: str | None = None,
809
1991
  collaboration_mode: str | None = None,
810
1992
  goal_objective: str | None = None,
1993
+ reasoning_effort: str | None = None,
811
1994
  runtime_input: Any = None,
812
1995
  idempotency_key: str,
813
1996
  on_event: Callable[[RunEvent], None] | None = None,
814
1997
  ) -> Operation:
815
- async def runner():
1998
+ async def runner(_operation_id: str):
816
1999
  return await self.run_build(
817
2000
  build_id,
818
2001
  user_input,
@@ -822,6 +2005,7 @@ class StudioService:
822
2005
  approval_mode=approval_mode,
823
2006
  collaboration_mode=collaboration_mode,
824
2007
  goal_objective=goal_objective,
2008
+ reasoning_effort=reasoning_effort,
825
2009
  runtime_input=runtime_input,
826
2010
  on_event=on_event,
827
2011
  )
@@ -844,27 +2028,20 @@ class StudioService:
844
2028
  approval_mode: str | None = None,
845
2029
  collaboration_mode: str | None = None,
846
2030
  goal_objective: str | None = None,
2031
+ reasoning_effort: str | None = None,
847
2032
  runtime_input: Any = None,
848
2033
  on_event: Callable[[RunEvent], None] | None = None,
849
2034
  ):
850
2035
  """Execute any immutable Studio Build through the canonical executor."""
851
2036
 
852
- try:
853
- spec = self.codex_runs.resolve(
854
- build_id,
855
- model=model,
856
- sandbox=sandbox,
857
- approval_mode=approval_mode,
858
- )
859
- except Exception as exc:
860
- if getattr(exc, "status_code", None) != 404:
861
- raise
862
- spec = self.framework_runs.resolve(
863
- build_id,
864
- model=model,
865
- approval_mode=approval_mode,
866
- )
867
- if collaboration_mode or goal_objective:
2037
+ await self.start()
2038
+ spec = self.resolve_run_spec(
2039
+ build_id,
2040
+ model=model,
2041
+ sandbox=sandbox,
2042
+ approval_mode=approval_mode,
2043
+ )
2044
+ if collaboration_mode or goal_objective or reasoning_effort:
868
2045
  from dataclasses import replace
869
2046
 
870
2047
  request_config = dict(spec.request_config)
@@ -873,6 +2050,8 @@ class StudioService:
873
2050
  if goal_objective:
874
2051
  request_config["goal_objective"] = goal_objective
875
2052
  request_config["ephemeral"] = False
2053
+ if reasoning_effort:
2054
+ request_config["effort"] = reasoning_effort
876
2055
  spec = replace(spec, request_config=request_config)
877
2056
  return await self.run_service.run(
878
2057
  spec,
@@ -882,28 +2061,6 @@ class StudioService:
882
2061
  on_event=on_event,
883
2062
  )
884
2063
 
885
- def submit_evaluation(
886
- self,
887
- build_id: str,
888
- suite_refs: list[str],
889
- *,
890
- fail_fast: bool,
891
- idempotency_key: str,
892
- ) -> Operation:
893
- async def runner():
894
- return await self.evaluations.run(
895
- build_id,
896
- suite_refs,
897
- fail_fast=fail_fast,
898
- )
899
-
900
- return self.operations.submit(
901
- kind=OperationKind.EVALUATION,
902
- resource_id=build_id,
903
- idempotency_key=idempotency_key,
904
- runner=runner,
905
- )
906
-
907
2064
  def submit_public_evaluation(
908
2065
  self,
909
2066
  evalset_file: str,
@@ -941,10 +2098,20 @@ class StudioService:
941
2098
  config=config,
942
2099
  report_dir=str(self.evaluation_storage.root),
943
2100
  )
2101
+ evaluation_id = f"eval_{uuid4().hex}"
944
2102
 
945
- async def runner():
2103
+ async def runner(operation_id: str):
946
2104
  try:
947
- report = await execute_evaluation(request)
2105
+ report = await execute_evaluation(
2106
+ request,
2107
+ adapter=self._public_evaluation_adapter(request),
2108
+ run_id=evaluation_id,
2109
+ on_case_started=lambda case_id, index, total: self.operations.append(
2110
+ operation_id,
2111
+ "evaluation.case.started",
2112
+ {"caseId": case_id, "index": index, "total": total},
2113
+ ),
2114
+ )
948
2115
  except EvaluationNotImplementedError as exc:
949
2116
  raise StudioError(
950
2117
  "EVALUATION_EXECUTOR_UNAVAILABLE",
@@ -957,26 +2124,191 @@ class StudioService:
957
2124
  str(exc),
958
2125
  status_code=502,
959
2126
  ) from exc
960
- try:
961
- self.evaluation_storage.write_report(report)
962
- except EvaluationStorageError as exc:
963
- raise StudioError(
964
- "EVALUATION_REPORT_WRITE_FAILED",
965
- "评测报告写入失败",
966
- status_code=500,
967
- ) from exc
968
- return report
2127
+ return _OperationResource(id=report.spec.id)
969
2128
 
970
2129
  return self.operations.submit(
971
2130
  kind=OperationKind.EVALUATION,
972
- resource_id=evalset.content_digest,
2131
+ resource_id=evaluation_id,
973
2132
  idempotency_key=idempotency_key,
2133
+ metadata={
2134
+ "evalset": {"name": evalset.name, "caseCount": len(evalset.cases)},
2135
+ "target": {
2136
+ "kind": target.kind.value,
2137
+ "label": self._evaluation_target_label(target.kind, build_id=target.locator),
2138
+ },
2139
+ "evaluators": list(config.evaluators),
2140
+ },
974
2141
  runner=runner,
975
2142
  )
976
2143
 
977
2144
  def list_public_evaluations(self):
978
2145
  return self.evaluation_storage.list_reports()
979
2146
 
2147
+ def list_public_evaluation_runs(self) -> list[dict[str, Any]]:
2148
+ return [
2149
+ self._public_evaluation_run(operation)
2150
+ for operation in self.operations.list(kind=OperationKind.EVALUATION)
2151
+ ]
2152
+
2153
+ def get_public_evaluation_run(self, evaluation_id: str) -> dict[str, Any]:
2154
+ operation = next(
2155
+ (
2156
+ item
2157
+ for item in self.operations.list(kind=OperationKind.EVALUATION)
2158
+ if item.resource_id == evaluation_id
2159
+ ),
2160
+ None,
2161
+ )
2162
+ if operation is None:
2163
+ raise StudioError(
2164
+ "EVALUATION_RUN_NOT_FOUND",
2165
+ "Evaluation Run 不存在",
2166
+ status_code=404,
2167
+ details={"id": evaluation_id},
2168
+ )
2169
+ return self._public_evaluation_run(operation, include_report=True)
2170
+
2171
+ def _public_evaluation_run(
2172
+ self, operation: Operation, *, include_report: bool = False
2173
+ ) -> dict[str, Any]:
2174
+ try:
2175
+ report = self.evaluation_storage.read_report(operation.resource_id)
2176
+ except EvaluationStorageError:
2177
+ report = None
2178
+ progress = None
2179
+ for event in self.operations.events(operation.id):
2180
+ if event.type == "evaluation.case.started":
2181
+ progress = {
2182
+ "current": event.data.get("index", 0),
2183
+ "total": event.data.get("total", 0),
2184
+ "caseId": event.data.get("caseId"),
2185
+ }
2186
+ metadata = operation.metadata or {}
2187
+ evalset = metadata.get("evalset")
2188
+ target = metadata.get("target")
2189
+ evaluators = metadata.get("evaluators")
2190
+ if report is not None:
2191
+ if not evalset:
2192
+ evalset = {
2193
+ "name": report.spec.evalset.name,
2194
+ "caseCount": len(report.spec.evalset.cases),
2195
+ }
2196
+ if not target:
2197
+ target = {
2198
+ "kind": report.spec.target.kind.value,
2199
+ "label": self._evaluation_target_label(
2200
+ report.spec.target.kind,
2201
+ metadata=report.spec.target.metadata,
2202
+ ),
2203
+ }
2204
+ if not evaluators:
2205
+ evaluators = list(report.spec.config.evaluators)
2206
+ payload: dict[str, Any] = {
2207
+ "id": operation.resource_id,
2208
+ "operationId": operation.id,
2209
+ "status": report.status.value if report else operation.status,
2210
+ "createdAt": operation.created_at,
2211
+ "completedAt": operation.completed_at,
2212
+ "evalset": evalset or {},
2213
+ "target": target or {},
2214
+ "evaluators": evaluators or [],
2215
+ "progress": progress,
2216
+ "summary": report.summary if report else None,
2217
+ "hasReport": report is not None,
2218
+ "error": operation.error,
2219
+ }
2220
+ if include_report:
2221
+ payload["report"] = report
2222
+ return payload
2223
+
2224
+ def _evaluation_target_label(
2225
+ self,
2226
+ kind: TargetKind,
2227
+ *,
2228
+ build_id: str | None = None,
2229
+ metadata: dict[str, Any] | None = None,
2230
+ ) -> str:
2231
+ if kind is not TargetKind.STUDIO_BUILD:
2232
+ return _EVALUATION_TARGET_LABELS[kind]
2233
+ agent_id = str((metadata or {}).get("agentId") or "")
2234
+ if not agent_id and build_id:
2235
+ agent_id = self.builds.get(build_id).agent_id
2236
+ if not agent_id:
2237
+ return _EVALUATION_TARGET_LABELS[kind]
2238
+ try:
2239
+ return self.drafts.get(agent_id).metadata.name
2240
+ except StudioError as exc:
2241
+ if exc.status_code != 404:
2242
+ raise
2243
+ return agent_id
2244
+
2245
+ def evaluation_catalog(self) -> dict[str, list[dict]]:
2246
+ builds = [
2247
+ {
2248
+ "id": record.id,
2249
+ "agentId": record.agent_id,
2250
+ "runtime": record.runtime_type,
2251
+ "digest": _sha256_digest(record.bundle_digest),
2252
+ "createdAt": record.created_at.isoformat().replace("+00:00", "Z"),
2253
+ }
2254
+ for record in self.builds.list()
2255
+ if record.status == BuildStatus.SUCCEEDED and record.artifact_path
2256
+ ]
2257
+ builds.sort(key=lambda item: item["createdAt"], reverse=True)
2258
+ evalsets: list[dict] = []
2259
+ candidates: set[Path] = set()
2260
+ for pattern in ("*.yaml", "*.yml", "*.json"):
2261
+ candidates.update(self.workspace.root.glob(pattern))
2262
+ candidates.update(self.workspace.root.glob(f"evaluations/**/{pattern}"))
2263
+ candidates.update(self.workspace.root.glob(f"agents/*/evaluations/**/{pattern}"))
2264
+ for path in sorted(candidates):
2265
+ try:
2266
+ evalset = load_evalset(path)
2267
+ except (EvalSetParseError, OSError):
2268
+ continue
2269
+ evalsets.append(
2270
+ {
2271
+ "path": path.relative_to(self.workspace.root).as_posix(),
2272
+ "name": evalset.name,
2273
+ "caseCount": len(evalset.cases),
2274
+ "contentDigest": evalset.content_digest,
2275
+ }
2276
+ )
2277
+ evalsets.sort(key=lambda item: item["path"])
2278
+ return {"builds": builds, "evalsets": evalsets}
2279
+
2280
+ def import_evaluation_file(self, content: bytes, *, filename: str) -> dict:
2281
+ if len(content) > 2 * 1024 * 1024:
2282
+ raise StudioError(
2283
+ "EVALSET_FILE_TOO_LARGE",
2284
+ "EvalSet 文件不能超过 2 MiB",
2285
+ status_code=413,
2286
+ field="file",
2287
+ )
2288
+ safe_name = Path(filename.replace("\\", "/")).name or "evalset.yaml"
2289
+ if Path(safe_name).suffix.lower() not in {".yaml", ".yml", ".json"}:
2290
+ raise StudioError(
2291
+ "EVALSET_FILE_TYPE_INVALID",
2292
+ "EvalSet 只支持 YAML 或 JSON 文件",
2293
+ status_code=422,
2294
+ field="file",
2295
+ )
2296
+ target = self.workspace.resolve(
2297
+ Path("evaluations/uploads") / f"{uuid4().hex[:12]}-{safe_name}"
2298
+ )
2299
+ self.workspace.atomic_write_bytes(target, content)
2300
+ try:
2301
+ evalset = load_evalset(target)
2302
+ except EvalSetParseError as exc:
2303
+ target.unlink(missing_ok=True)
2304
+ raise StudioError(exc.code, str(exc), status_code=422, field="file") from exc
2305
+ return {
2306
+ "path": self.workspace.relative(target),
2307
+ "name": evalset.name,
2308
+ "caseCount": len(evalset.cases),
2309
+ "contentDigest": evalset.content_digest,
2310
+ }
2311
+
980
2312
  def get_public_evaluation(self, evaluation_id: str):
981
2313
  try:
982
2314
  return self.evaluation_storage.read_report(evaluation_id)
@@ -999,6 +2331,9 @@ class StudioService:
999
2331
  field="target.locator",
1000
2332
  )
1001
2333
  return target
2334
+ if target.kind == TargetKind.STUDIO_BUILD:
2335
+ self._validate_evaluation_build(target.locator)
2336
+ return target
1002
2337
  path = self.workspace.resolve(target.locator, must_exist=True)
1003
2338
  if not path.is_dir():
1004
2339
  raise StudioError(
@@ -1009,6 +2344,78 @@ class StudioService:
1009
2344
  )
1010
2345
  return target.model_copy(update={"locator": str(path)})
1011
2346
 
2347
+ def _public_evaluation_adapter(self, request: PublicEvaluationRequest):
2348
+ if request.target.kind != TargetKind.STUDIO_BUILD:
2349
+ return None
2350
+ evidence_store = EvidenceStore(request.report_dir) if request.report_dir else None
2351
+ return StudioBuildTargetAdapter(
2352
+ timeout_seconds=request.config.timeout_seconds,
2353
+ resolve_build=self._resolve_evaluation_build,
2354
+ run_service=self.run_service,
2355
+ evidence_store=evidence_store,
2356
+ )
2357
+
2358
+ def _resolve_evaluation_build(self, build_id: str) -> StudioBuildResolution:
2359
+ try:
2360
+ self.codex_builds.get(build_id)
2361
+ except Exception as exc:
2362
+ if getattr(exc, "status_code", None) != 404:
2363
+ raise
2364
+ else:
2365
+ raise StudioError(
2366
+ "CODEX_BUILD_NOT_IMMUTABLE",
2367
+ "Codex Build 尚未冻结工作区源码,不能作为不可变评测 Target",
2368
+ status_code=422,
2369
+ field="target.locator",
2370
+ )
2371
+ build = self.builds.get(build_id)
2372
+ if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
2373
+ raise StudioBuildTargetError(
2374
+ "STUDIO_BUILD_NOT_READY",
2375
+ "Studio Build must be SUCCEEDED before evaluation",
2376
+ )
2377
+ run_spec = self.framework_runs.resolve(build_id)
2378
+ revision_digest = str(build.bundle_digest or build.resolved_digest).strip()
2379
+ if not revision_digest:
2380
+ raise StudioBuildTargetError(
2381
+ "STUDIO_BUILD_INVALID",
2382
+ "Studio Build is missing an immutable digest",
2383
+ )
2384
+ return StudioBuildResolution(
2385
+ build_id=build.id,
2386
+ agent_id=build.agent_id,
2387
+ revision_digest=revision_digest,
2388
+ runtime=build.runtime_type,
2389
+ model=run_spec.model,
2390
+ run_spec=run_spec,
2391
+ metadata={
2392
+ "bundleDigest": build.bundle_digest,
2393
+ "resolvedDigest": build.resolved_digest,
2394
+ "sourceDigest": build.source_digest,
2395
+ "sourceRevision": build.source_revision,
2396
+ },
2397
+ )
2398
+
2399
+ def _validate_evaluation_build(self, build_id: str) -> None:
2400
+ try:
2401
+ self.codex_builds.get(build_id)
2402
+ except Exception as exc:
2403
+ if getattr(exc, "status_code", None) != 404:
2404
+ raise
2405
+ else:
2406
+ raise StudioError(
2407
+ "CODEX_BUILD_NOT_IMMUTABLE",
2408
+ "Codex Build 尚未冻结工作区源码,不能作为不可变评测 Target",
2409
+ status_code=422,
2410
+ field="target.locator",
2411
+ )
2412
+ build = self.builds.get(build_id)
2413
+ if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
2414
+ raise StudioBuildTargetError(
2415
+ "STUDIO_BUILD_NOT_READY",
2416
+ "Studio Build must be SUCCEEDED before evaluation",
2417
+ )
2418
+
1012
2419
  def submit_deployment(
1013
2420
  self,
1014
2421
  build_id: str,
@@ -1016,7 +2423,84 @@ class StudioService:
1016
2423
  *,
1017
2424
  idempotency_key: str,
1018
2425
  ) -> Operation:
1019
- async def runner():
2426
+ logging.getLogger(__name__).info(
2427
+ "deployment submitted: build=%s idempotencyKey=%s environment=%s",
2428
+ build_id,
2429
+ idempotency_key,
2430
+ getattr(request, "environment", "-"),
2431
+ )
2432
+ try:
2433
+ codex_build = self.codex_builds.get(build_id)
2434
+ except StudioError as exc:
2435
+ if exc.status_code != 404:
2436
+ raise
2437
+ else:
2438
+ snapshot = self.codex_manifests.load(codex_build.agent_name)
2439
+ if not self.codex_builder.is_current(codex_build):
2440
+ raise StudioError(
2441
+ "BUILD_NOT_CURRENT",
2442
+ "Codex YAML 已变更,请重新 Build 后再部署",
2443
+ status_code=409,
2444
+ details={"buildId": build_id},
2445
+ )
2446
+ # The deployed YAML records only the selected model identity. Its
2447
+ # credential stays in the local resolver and is materialized for
2448
+ # this outbound control-plane request only, never in the Build or
2449
+ # deployment receipt.
2450
+ launch = self.codex_runs.resolve(build_id)
2451
+ runtime_environment = dict((launch.launch_context.config or {}).get("env") or {})
2452
+ # Retrying a YAML deployment must update the Agent that the prior
2453
+ # receipt already created. CreateAgent can have succeeded before
2454
+ # a downstream Runtime-Service start failed; creating again would
2455
+ # leave duplicate cloud Agents and make the retry path non-idempotent.
2456
+ # A YAML edit necessarily produces a new immutable Build. Match
2457
+ # the prior receipt through that Build's immutable agent_name,
2458
+ # rather than by build_id, otherwise every edit attempts a second
2459
+ # CreateAgent and Server correctly rejects the duplicate name.
2460
+ replacement_candidates = []
2461
+ for deployment in self.cloud.list():
2462
+ if deployment.artifact_id != "managed-runtime" or not deployment.agent_id:
2463
+ continue
2464
+ try:
2465
+ deployed_build = self.codex_builds.get(deployment.build_id)
2466
+ except StudioError:
2467
+ # A malformed/legacy local receipt cannot establish
2468
+ # ownership of this Agent and must not block a new deploy.
2469
+ continue
2470
+ if deployed_build.agent_name == codex_build.agent_name:
2471
+ replacement_candidates.append((deployed_build, deployment))
2472
+ replacing = (
2473
+ max(
2474
+ replacement_candidates,
2475
+ key=lambda item: (item[0].source_revision, item[0].created_at),
2476
+ )[1]
2477
+ if replacement_candidates
2478
+ else None
2479
+ )
2480
+
2481
+ async def managed_runtime_runner(_operation_id: str):
2482
+ return await self.cloud.deploy_managed_runtime(
2483
+ build_id=build_id,
2484
+ agent_name=codex_build.agent_name,
2485
+ manifest=snapshot.source_bytes.decode("utf-8"),
2486
+ runtime_name=codex_build.runtime_name,
2487
+ runtime_version=codex_build.runtime_version,
2488
+ # Server canonicalizes and records its own digest. This
2489
+ # source digest remains the immutable Studio Build receipt.
2490
+ manifest_digest=codex_build.manifest_sha256,
2491
+ request=request,
2492
+ runtime_environment=runtime_environment,
2493
+ replacing=replacing,
2494
+ )
2495
+
2496
+ return self.operations.submit(
2497
+ kind=OperationKind.DEPLOYMENT,
2498
+ resource_id=build_id,
2499
+ idempotency_key=idempotency_key,
2500
+ runner=managed_runtime_runner,
2501
+ )
2502
+
2503
+ async def runner(_operation_id: str):
1020
2504
  return await self.cloud.deploy(build_id, request)
1021
2505
 
1022
2506
  return self.operations.submit(
@@ -1033,7 +2517,56 @@ class StudioService:
1033
2517
  target_build_id: str,
1034
2518
  idempotency_key: str,
1035
2519
  ) -> Operation:
1036
- async def runner():
2520
+ deployment = self.cloud.get(deployment_id)
2521
+ if deployment.artifact_id == "managed-runtime":
2522
+ deployed_build = self.codex_builds.get(deployment.build_id)
2523
+ target_build = self.codex_builds.get(target_build_id)
2524
+ if target_build.agent_name != deployed_build.agent_name:
2525
+ raise StudioError(
2526
+ "MANAGED_RUNTIME_ROLLBACK_AGENT_MISMATCH",
2527
+ "声明式 Agent 只能回滚到同一 Agent 的 Build",
2528
+ status_code=409,
2529
+ details={
2530
+ "deploymentId": deployment_id,
2531
+ "targetBuildId": target_build_id,
2532
+ },
2533
+ )
2534
+ manifest = self.codex_builds.manifest_text(target_build)
2535
+ request = self.cloud.request_for(deployment_id)
2536
+
2537
+ async def managed_runtime_runner(_operation_id: str):
2538
+ return await self.cloud.deploy_managed_runtime(
2539
+ build_id=target_build.id,
2540
+ agent_name=target_build.agent_name,
2541
+ manifest=manifest,
2542
+ runtime_name=target_build.runtime_name,
2543
+ runtime_version=target_build.runtime_version,
2544
+ manifest_digest=target_build.manifest_sha256,
2545
+ request=request,
2546
+ replacing=deployment,
2547
+ )
2548
+
2549
+ return self.operations.submit(
2550
+ kind=OperationKind.DEPLOYMENT,
2551
+ resource_id=deployment_id,
2552
+ idempotency_key=idempotency_key,
2553
+ runner=managed_runtime_runner,
2554
+ )
2555
+
2556
+ deployed_build = self.builds.get(deployment.build_id)
2557
+ target_build = self.builds.get(target_build_id)
2558
+ if target_build.agent_id != deployed_build.agent_id:
2559
+ raise StudioError(
2560
+ "DEPLOYMENT_ROLLBACK_AGENT_MISMATCH",
2561
+ "高代码 Agent 只能回滚到同一 Agent 的 Build",
2562
+ status_code=409,
2563
+ details={
2564
+ "deploymentId": deployment_id,
2565
+ "targetBuildId": target_build_id,
2566
+ },
2567
+ )
2568
+
2569
+ async def runner(_operation_id: str):
1037
2570
  return await self.cloud.rollback(
1038
2571
  deployment_id,
1039
2572
  target_build_id=target_build_id,
@@ -1046,6 +2579,73 @@ class StudioService:
1046
2579
  runner=runner,
1047
2580
  )
1048
2581
 
2582
+ def submit_account_agent_version_rollback(
2583
+ self,
2584
+ agent_id: str,
2585
+ *,
2586
+ version_id: str,
2587
+ idempotency_key: str,
2588
+ ) -> Operation:
2589
+ normalized_agent_id = str(agent_id or "").strip()
2590
+ normalized_version_id = str(version_id or "").strip()
2591
+ if not normalized_agent_id:
2592
+ raise StudioError(
2593
+ "CLOUD_AGENT_NOT_FOUND",
2594
+ "云端 Agent 标识不能为空",
2595
+ status_code=404,
2596
+ )
2597
+ if not normalized_version_id:
2598
+ raise StudioError(
2599
+ "CLOUD_AGENT_VERSION_REQUIRED",
2600
+ "请选择要回滚的云端版本",
2601
+ status_code=422,
2602
+ field="versionId",
2603
+ )
2604
+ resource_id = f"{normalized_agent_id}:{normalized_version_id}"
2605
+
2606
+ async def runner(_operation_id: str):
2607
+ await self.cloud.rollback_account_agent_version(
2608
+ normalized_agent_id,
2609
+ version_id=normalized_version_id,
2610
+ )
2611
+ return _OperationResource(id=resource_id)
2612
+
2613
+ return self.operations.submit(
2614
+ kind=OperationKind.DEPLOYMENT,
2615
+ resource_id=resource_id,
2616
+ idempotency_key=idempotency_key,
2617
+ metadata={
2618
+ "agentId": normalized_agent_id,
2619
+ "targetVersionId": normalized_version_id,
2620
+ "source": "server-version",
2621
+ },
2622
+ runner=runner,
2623
+ )
2624
+
2625
+ async def deployment_dashboard_access(self, deployment_id: str) -> dict[str, str | None]:
2626
+ """Return a receipt-bound private Hosted UI link on explicit user request."""
2627
+
2628
+ return await self.cloud.dashboard_access(deployment_id)
2629
+
2630
+ def deployment_operation_scope(self) -> dict[str, str]:
2631
+ """Return opaque browser storage scopes without exposing cloud credentials."""
2632
+
2633
+ workspace_identity = str(self.workspace.root.resolve())
2634
+ region = (
2635
+ os.environ.get("AGENTENGINE_REGION") or os.environ.get("KSYUN_REGION") or "cn-beijing-6"
2636
+ ).strip()
2637
+ access_key = (
2638
+ os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY") or "unsigned"
2639
+ ).strip()
2640
+
2641
+ def opaque(value: str) -> str:
2642
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
2643
+
2644
+ return {
2645
+ "workspace": opaque(workspace_identity),
2646
+ "cloudCredential": opaque(f"{region}\0{access_key}"),
2647
+ }
2648
+
1049
2649
  def get_settings(self) -> dict[str, Any]:
1050
2650
  path = self.workspace.resolve(".agentkit/settings.yaml")
1051
2651
  data: dict[str, Any] = {}
@@ -1057,13 +2657,30 @@ class StudioService:
1057
2657
  defaults = {
1058
2658
  "sandbox": os.environ.get("KSADK_CODEX_SANDBOX", "read_only"),
1059
2659
  "buildAfterCreate": True,
1060
- "codexProxy": os.environ.get("KSADK_CODEX_USE_PROXY", "auto"),
1061
- "cloudAccessKey": os.environ.get("KINGSOFTCLOUD_ACCESS_KEY", ""),
1062
- "cloudSecretKey": os.environ.get("KINGSOFTCLOUD_SECRET_KEY", ""),
1063
- "cloudRegion": os.environ.get("KSYUN_REGION", "cn-beijing-6"),
2660
+ "codexProxy": current_proxy_mode(),
2661
+ "cloudRegion": os.environ.get(
2662
+ "AGENTENGINE_REGION", os.environ.get("KSYUN_REGION", "cn-beijing-6")
2663
+ ),
2664
+ "cloudBucket": os.environ.get("KS3_BUCKET", ""),
2665
+ "cloudAccountId": (os.environ.get("KSYUN_ACCOUNT_ID", "").strip()),
2666
+ # AK/SK 不回显(避免本地 UI 回传 secret);只暴露配置状态。
2667
+ # 已配置 = 启动环境或已持久化 settings 里 AK+SK 齐全。
2668
+ "cloudAccountConfigured": bool(
2669
+ (os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")).strip()
2670
+ and (
2671
+ os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
2672
+ ).strip()
2673
+ ),
2674
+ "cloudSignedAccountConfigured": bool(
2675
+ (os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")).strip()
2676
+ and (
2677
+ os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
2678
+ ).strip()
2679
+ ),
1064
2680
  "traceContent": os.environ.get("KSADK_STUDIO_TRACE_CONTENT", "1") != "0",
1065
2681
  }
1066
- defaults.update({k: v for k, v in data.items() if v is not None})
2682
+ defaults.update({k: v for k, v in data.items() if k in defaults and v is not None})
2683
+ defaults["codexProxy"] = normalize_proxy_mode(defaults.get("codexProxy"))
1067
2684
  return defaults
1068
2685
 
1069
2686
  def update_settings(self, payload: dict[str, Any]) -> dict[str, Any]:
@@ -1071,12 +2688,23 @@ class StudioService:
1071
2688
  "sandbox",
1072
2689
  "buildAfterCreate",
1073
2690
  "codexProxy",
1074
- "cloudAccessKey",
1075
- "cloudSecretKey",
1076
2691
  "cloudRegion",
2692
+ "cloudBucket",
1077
2693
  "traceContent",
2694
+ # 云账号:AK/SK 留空 = 不修改(保留已存值);AccountID 可单独更新。
2695
+ "cloudAccessKey",
2696
+ "cloudSecretKey",
2697
+ "cloudAccountId",
1078
2698
  }
1079
2699
  data = {k: payload[k] for k in allowed if k in payload}
2700
+ if "cloudAccessKey" in data and not str(data["cloudAccessKey"]).strip():
2701
+ data.pop("cloudAccessKey")
2702
+ if "cloudSecretKey" in data and not str(data["cloudSecretKey"]).strip():
2703
+ data.pop("cloudSecretKey")
2704
+ if "cloudAccountId" in data:
2705
+ data["cloudAccountId"] = str(data["cloudAccountId"]).strip()
2706
+ if not data["cloudAccountId"]:
2707
+ data.pop("cloudAccountId")
1080
2708
  if data.get("sandbox") and data["sandbox"] not in {
1081
2709
  "read-only",
1082
2710
  "workspace-write",
@@ -1088,9 +2716,19 @@ class StudioService:
1088
2716
  "full_access",
1089
2717
  }:
1090
2718
  raise StudioError("SETTINGS_INVALID", "sandbox 取值非法", status_code=422)
2719
+ if "codexProxy" in data and data["codexProxy"] not in {"auto", "forced", "direct"}:
2720
+ raise StudioError("SETTINGS_INVALID", "codexProxy 取值非法", status_code=422)
1091
2721
  path = self.workspace.resolve(".agentkit/settings.yaml")
1092
2722
  self.workspace.atomic_write_yaml(path, data)
1093
- self._apply_settings_to_env(data)
2723
+ # env 桥接用落盘后的全量数据:局部更新(只改 AccountID)时,
2724
+ # 已持久化的 AK/SK 也要继续桥接,否则签名凭证丢失。
2725
+ try:
2726
+ persisted = load_yaml_file(path) or {}
2727
+ except Exception:
2728
+ persisted = data
2729
+ self._apply_settings_to_env(persisted if isinstance(persisted, dict) else data)
2730
+ if self._cloud_gateway_override is None:
2731
+ self.cloud.gateway = self._configured_cloud_gateway()
1094
2732
  return self.get_settings()
1095
2733
 
1096
2734
  def _apply_persisted_settings(self) -> None:
@@ -1114,16 +2752,71 @@ class StudioService:
1114
2752
  def _apply_settings_to_env(data: dict[str, Any]) -> None:
1115
2753
  if data.get("sandbox"):
1116
2754
  os.environ["KSADK_CODEX_SANDBOX"] = data["sandbox"]
1117
- if data.get("codexProxy"):
1118
- os.environ["KSADK_CODEX_USE_PROXY"] = data["codexProxy"]
1119
- if data.get("cloudAccessKey"):
1120
- os.environ["KINGSOFTCLOUD_ACCESS_KEY"] = data["cloudAccessKey"]
1121
- if data.get("cloudSecretKey"):
1122
- os.environ["KINGSOFTCLOUD_SECRET_KEY"] = data["cloudSecretKey"]
2755
+ if "codexProxy" in data:
2756
+ proxy_env = proxy_mode_env_value(data["codexProxy"])
2757
+ if proxy_env is None:
2758
+ os.environ.pop("KSADK_CODEX_USE_PROXY", None)
2759
+ else:
2760
+ os.environ["KSADK_CODEX_USE_PROXY"] = proxy_env
1123
2761
  if data.get("cloudRegion"):
1124
- os.environ["KSYUN_REGION"] = data["cloudRegion"]
2762
+ os.environ["AGENTENGINE_REGION"] = data["cloudRegion"]
2763
+ if data.get("cloudBucket"):
2764
+ os.environ["KS3_BUCKET"] = data["cloudBucket"]
1125
2765
  if "traceContent" in data:
1126
2766
  os.environ["KSADK_STUDIO_TRACE_CONTENT"] = "1" if data["traceContent"] else "0"
2767
+ # 云账号凭证:桥接到 KSYUN_* env(AgentEngineClient 的 V4 签名与
2768
+ # X-Ksc-Account-Id 注入都从这里取值)。
2769
+ if data.get("cloudAccessKey"):
2770
+ os.environ["KSYUN_ACCESS_KEY"] = str(data["cloudAccessKey"])
2771
+ if data.get("cloudSecretKey"):
2772
+ os.environ["KSYUN_SECRET_KEY"] = str(data["cloudSecretKey"])
2773
+ if data.get("cloudAccountId"):
2774
+ os.environ["KSYUN_ACCOUNT_ID"] = str(data["cloudAccountId"])
2775
+
2776
+ @staticmethod
2777
+ def _configured_cloud_gateway() -> CloudDeploymentGateway:
2778
+ """Compose the existing signed Code deployment path from process-only credentials."""
2779
+
2780
+ access_key = (
2781
+ os.environ.get("KSYUN_ACCESS_KEY") or os.environ.get("KS3_ACCESS_KEY", "")
2782
+ ).strip()
2783
+ secret_key = (
2784
+ os.environ.get("KSYUN_SECRET_KEY") or os.environ.get("KS3_SECRET_KEY", "")
2785
+ ).strip()
2786
+ region = os.environ.get("AGENTENGINE_REGION", os.environ.get("KSYUN_REGION", "")).strip()
2787
+ if not all((access_key, secret_key, region)):
2788
+ return UnavailableCloudGateway()
2789
+ control_client = AgentEngineClient(
2790
+ region=region,
2791
+ access_key=access_key,
2792
+ secret_key=secret_key,
2793
+ )
2794
+ stream_base_url = os.environ.get("AGENTENGINE_STREAM_SERVER_URL", "").strip()
2795
+ if not stream_base_url and region.lower() == "pre-online":
2796
+ # The pre-online KOP response path currently buffers SSE until
2797
+ # EOF. Its internal Server ingress validates the same V4
2798
+ # signature and preserves the RunAgent streaming response.
2799
+ stream_base_url = "http://agent-api-pre.kspmas-internal.ksyun.com"
2800
+ stream_client = (
2801
+ AgentEngineClient(
2802
+ base_url=stream_base_url,
2803
+ region=region,
2804
+ access_key=access_key,
2805
+ secret_key=secret_key,
2806
+ )
2807
+ if stream_base_url
2808
+ else control_client
2809
+ )
2810
+ return DirectAgentEngineCloudDeploymentGateway(
2811
+ region=region,
2812
+ client=control_client,
2813
+ stream_client=stream_client,
2814
+ bucket=os.environ.get("KS3_BUCKET", "").strip() or None,
2815
+ ks3_credentials={
2816
+ "access_key": access_key,
2817
+ "secret_key": secret_key,
2818
+ },
2819
+ )
1127
2820
 
1128
2821
  @staticmethod
1129
2822
  def builtin_capabilities():
@@ -1139,3 +2832,8 @@ class StudioService:
1139
2832
  item.model_dump(by_alias=True, exclude_none=True, mode="json")
1140
2833
  for item in self.catalog.list(kind=kind, query=query, limit=200)
1141
2834
  ]
2835
+
2836
+
2837
+ def _sha256_digest(value: str) -> str:
2838
+ digest = str(value or "").strip()
2839
+ return digest if digest.startswith("sha256:") else f"sha256:{digest}"