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/api.py CHANGED
@@ -3,10 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import base64
6
7
  import hmac
7
8
  import json
8
9
  import os
9
10
  import secrets
11
+ import time
10
12
  from collections.abc import AsyncIterator
11
13
  from contextlib import asynccontextmanager
12
14
  from pathlib import Path
@@ -16,16 +18,33 @@ from fastapi import FastAPI, File, Header, Query, Request, Response, UploadFile
16
18
  from fastapi.exceptions import RequestValidationError
17
19
  from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
18
20
  from fastapi.staticfiles import StaticFiles
21
+ from starlette.background import BackgroundTask
19
22
 
23
+ from ksadk.api.client import AgentEngineAPIError
24
+ from ksadk.conversations.contracts import (
25
+ ConversationAttachmentPart,
26
+ ConversationTextPart,
27
+ validate_conversation_input,
28
+ )
29
+ from ksadk.conversations.projector import project_conversation_item
30
+ from ksadk.events.canonical import parse_runtime_event_lenient
31
+ from ksadk.scheduler.contracts import ScheduledTask
20
32
  from ksadk.studio.api_catalog_routes import register_catalog_routes
21
33
  from ksadk.studio.api_contracts import (
34
+ AgentScheduleRequest,
22
35
  AuthoringCommitRequest,
23
36
  BuildRequest,
37
+ CloudAgentVersionRollbackRequest,
38
+ CloudChatInteractionSubmitRequest,
39
+ CloudChatMessageRequest,
40
+ ContextPreviewRequest,
24
41
  ConversationAuthoringRequest,
42
+ ConversationTurnRequest,
25
43
  CreateAgentRequest,
26
- EvaluationRequest,
44
+ ImportRootRequest,
27
45
  InteractionSubmitRequest,
28
46
  ProjectInspectRequest,
47
+ PromptCompileRequest,
29
48
  QuickAuthoringRequest,
30
49
  RollbackRequest,
31
50
  RunRequest,
@@ -58,6 +77,8 @@ from ksadk.studio.api_helpers import (
58
77
  from ksadk.studio.api_helpers import (
59
78
  sse as _sse,
60
79
  )
80
+ from ksadk.studio.api_memory_routes import register_memory_routes
81
+ from ksadk.studio.api_plugin_routes import register_plugin_routes
61
82
  from ksadk.studio.codex_manifest import CodexAgentManifest
62
83
  from ksadk.studio.contracts import (
63
84
  AgentAppearance,
@@ -78,6 +99,117 @@ _PUBLIC_API_PATHS = {
78
99
  }
79
100
  _LOCAL_HOSTS = {"127.0.0.1", "::1", "localhost", "testserver"}
80
101
 
102
+ # RuntimeEvent/v2 itself is snake_case. Some pre-existing cloud SessionEvent
103
+ # projections, however, serialized the *envelope* with the REST camelCase
104
+ # convention. This is a narrow transport normalizer, not a provider payload
105
+ # rewrite: only fields owned by the frozen RuntimeEvent envelope/content
106
+ # contracts are translated before strict parsing.
107
+ _RUNTIME_EVENT_WIRE_ALIASES = {
108
+ "schemaVersion": "schema_version",
109
+ "eventId": "event_id",
110
+ "runId": "run_id",
111
+ "runSeq": "run_seq",
112
+ "scopeId": "scope_id",
113
+ "parentScopeId": "parent_scope_id",
114
+ "eventType": "event_type",
115
+ "itemId": "item_id",
116
+ "itemKind": "item_kind",
117
+ "interactionId": "interaction_id",
118
+ "interactionKind": "interaction_kind",
119
+ "continuationId": "continuation_id",
120
+ "resumeAttemptId": "resume_attempt_id",
121
+ "outputRefs": "output_refs",
122
+ "inputTokens": "input_tokens",
123
+ "outputTokens": "output_tokens",
124
+ "totalTokens": "total_tokens",
125
+ "cachedTokens": "cached_tokens",
126
+ "reasoningTokens": "reasoning_tokens",
127
+ }
128
+ _SOURCE_WIRE_ALIASES = {
129
+ "nativeEventId": "native_event_id",
130
+ "nativeCursor": "native_cursor",
131
+ "nativeRunId": "native_run_id",
132
+ "nativeItemId": "native_item_id",
133
+ }
134
+ _CONTENT_WIRE_ALIASES = {
135
+ "contentType": "content_type",
136
+ "partId": "part_id",
137
+ "callId": "call_id",
138
+ "artifactId": "artifact_id",
139
+ "mimeType": "mime_type",
140
+ "isError": "is_error",
141
+ }
142
+
143
+
144
+ def _normalize_cloud_runtime_event_wire(value: dict[str, Any]) -> dict[str, Any]:
145
+ """Normalize only historic REST casing at the RuntimeEvent boundary."""
146
+
147
+ normalized = {
148
+ _RUNTIME_EVENT_WIRE_ALIASES.get(key, key): item for key, item in value.items()
149
+ }
150
+ source = normalized.get("source")
151
+ if isinstance(source, dict):
152
+ normalized["source"] = {
153
+ _SOURCE_WIRE_ALIASES.get(key, key): item for key, item in source.items()
154
+ }
155
+
156
+ def normalize_content(content: Any) -> Any:
157
+ if not isinstance(content, dict):
158
+ return content
159
+ return {_CONTENT_WIRE_ALIASES.get(key, key): item for key, item in content.items()}
160
+
161
+ normalized["update"] = normalize_content(normalized.get("update"))
162
+ for key in ("initial", "snapshot"):
163
+ snapshot = normalized.get(key)
164
+ if not isinstance(snapshot, dict):
165
+ continue
166
+ parts = snapshot.get("parts")
167
+ if isinstance(parts, list):
168
+ normalized[key] = {
169
+ **snapshot,
170
+ "parts": [normalize_content(part) for part in parts],
171
+ }
172
+ return normalized
173
+
174
+
175
+ def _cloud_event_conversation_item(
176
+ event: dict[str, Any], *, session_id: str
177
+ ) -> dict[str, Any] | None:
178
+ """Project a complete cloud RuntimeEvent without inventing a browser item.
179
+
180
+ Cloud SessionEvent history has a compatibility envelope around the runtime
181
+ payload. Keep the source event untouched, but add the same typed
182
+ ConversationItem that local Studio streams use whenever the nested event
183
+ validates against RuntimeEvent/v2. A malformed or older envelope is still
184
+ observable to diagnostics, never guessed into an actionable chat card.
185
+ """
186
+
187
+ payload = event.get("payload") if isinstance(event.get("payload"), dict) else event
188
+ content = payload.get("content") if isinstance(payload.get("content"), dict) else {}
189
+ candidates = (
190
+ content.get("runtime_event"),
191
+ content.get("runtimeEvent"),
192
+ payload.get("runtime_event"),
193
+ payload.get("runtimeEvent"),
194
+ event.get("runtime_event"),
195
+ event.get("runtimeEvent"),
196
+ )
197
+ raw_event = next((candidate for candidate in candidates if isinstance(candidate, dict)), None)
198
+ if raw_event is None:
199
+ return None
200
+ try:
201
+ runtime_event = parse_runtime_event_lenient(
202
+ _normalize_cloud_runtime_event_wire(raw_event)
203
+ )
204
+ item = project_conversation_item(
205
+ runtime_event, # type: ignore[arg-type]
206
+ session_id=session_id,
207
+ run_id=runtime_event.run_id,
208
+ )
209
+ except (TypeError, ValueError):
210
+ return None
211
+ return item.model_dump(by_alias=True, exclude_none=True, mode="json")
212
+
81
213
 
82
214
  def create_studio_app(
83
215
  root: Path | str,
@@ -94,8 +226,13 @@ def create_studio_app(
94
226
  @asynccontextmanager
95
227
  async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
96
228
  try:
229
+ await studio.start()
230
+ await studio.run_service.recover_interrupted()
231
+ await studio.scheduler.start_if_available()
97
232
  yield
98
233
  finally:
234
+ await studio.scheduler.stop()
235
+ await studio.aclose()
99
236
  studio.credentials.clear_session()
100
237
 
101
238
  app = FastAPI(
@@ -109,7 +246,82 @@ def create_studio_app(
109
246
  app.state.studio_service = studio
110
247
  app.state.session_token = session_secret
111
248
  app.state.csrf_token = csrf_secret
249
+
250
+ def _stream_studio_run(
251
+ build_id: str,
252
+ *,
253
+ user_input: str,
254
+ session_id: str | None,
255
+ model: str | None,
256
+ sandbox: str | None,
257
+ reasoning_effort: str | None = None,
258
+ approval_mode: str | None = None,
259
+ collaboration_mode: str | None = None,
260
+ goal_objective: str | None = None,
261
+ runtime_input: Any = None,
262
+ idempotency_key: str,
263
+ ) -> StreamingResponse:
264
+ """Create an observer-only SSE response for one durable Studio run."""
265
+
266
+ queue: asyncio.Queue = asyncio.Queue()
267
+
268
+ def observe(event):
269
+ queue.put_nowait(event)
270
+
271
+ # The Operation owns the runtime task. The SSE response is only one
272
+ # observer, so refreshing or switching chats cannot cancel the Run.
273
+ operation = studio.submit_studio_run(
274
+ build_id,
275
+ user_input,
276
+ session_id=session_id,
277
+ model=model,
278
+ sandbox=sandbox,
279
+ reasoning_effort=reasoning_effort,
280
+ approval_mode=approval_mode,
281
+ collaboration_mode=collaboration_mode,
282
+ goal_objective=goal_objective,
283
+ runtime_input=runtime_input,
284
+ idempotency_key=idempotency_key,
285
+ on_event=observe,
286
+ )
287
+
288
+ async def render():
289
+ while True:
290
+ try:
291
+ event = await asyncio.wait_for(queue.get(), timeout=0.1)
292
+ except TimeoutError:
293
+ event = None
294
+ if event is not None:
295
+ data = json.dumps(
296
+ event.data,
297
+ ensure_ascii=False,
298
+ separators=(",", ":"),
299
+ )
300
+ yield f"id: {event.id}\nevent: {event.type}\ndata: {data}\n\n"
301
+ current = studio.operations.get(operation.id)
302
+ if (
303
+ current.status
304
+ in {
305
+ OperationStatus.SUCCEEDED,
306
+ OperationStatus.FAILED,
307
+ OperationStatus.CANCELLED,
308
+ OperationStatus.INTERRUPTED,
309
+ }
310
+ and queue.empty()
311
+ ):
312
+ if current.status == OperationStatus.FAILED:
313
+ data = json.dumps(current.error or {}, ensure_ascii=False)
314
+ yield f"event: run.failed\ndata: {data}\n\n"
315
+ break
316
+
317
+ return StreamingResponse(
318
+ render(),
319
+ media_type="text/event-stream",
320
+ headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
321
+ )
322
+
112
323
  static_root = Path(__file__).with_name("static")
324
+ _studio_startup_epoch = str(int(time.time()))
113
325
  shared_web = StudioSharedWebBridge(studio)
114
326
  app.state.shared_web_bridge = shared_web
115
327
  app.mount("/static", StaticFiles(directory=static_root), name="studio-static")
@@ -143,6 +355,7 @@ def create_studio_app(
143
355
  large_upload_paths = {
144
356
  "/api/v1/catalog/skills:import": 52 * 1024 * 1024,
145
357
  "/api/v1/authoring/imports:inspect": 102 * 1024 * 1024,
358
+ "/api/v1/evaluation-files": 2 * 1024 * 1024 + 64 * 1024,
146
359
  }
147
360
  request_limit = large_upload_paths.get(request.url.path, 2 * 1024 * 1024)
148
361
  if content_length and int(content_length) > request_limit:
@@ -203,6 +416,39 @@ def create_studio_app(
203
416
  async def studio_error_handler(request: Request, exc: StudioError):
204
417
  return _error_response(exc, request)
205
418
 
419
+ @app.exception_handler(AgentEngineAPIError)
420
+ async def agentengine_api_error_handler(request: Request, exc: AgentEngineAPIError):
421
+ http_status = exc.details.get("http_status", 502)
422
+ status_code = (
423
+ http_status if isinstance(http_status, int) and 400 <= http_status < 600 else 502
424
+ )
425
+ return _error_response(
426
+ StudioError(
427
+ "CLOUD_API_ERROR",
428
+ exc.message or "云端服务返回错误",
429
+ status_code=status_code,
430
+ details={"api_code": exc.code, "raw_code": exc.raw_code},
431
+ ),
432
+ request,
433
+ )
434
+
435
+ @app.exception_handler(Exception)
436
+ async def generic_error_handler(request: Request, exc: Exception):
437
+ # Catch-all for bare Exception raises from the API client (e.g. HTTP
438
+ # errors, JSON parse failures) so they surface as structured JSON
439
+ # instead of opaque 500s. StudioError and AgentEngineAPIError are
440
+ # handled by their own dedicated handlers above.
441
+ if isinstance(exc, StudioError):
442
+ return _error_response(exc, request)
443
+ return _error_response(
444
+ StudioError(
445
+ "INTERNAL_ERROR",
446
+ "Studio 内部错误,请根据请求 ID 查看本地诊断日志。",
447
+ status_code=500,
448
+ ),
449
+ request,
450
+ )
451
+
206
452
  @app.exception_handler(RequestValidationError)
207
453
  async def validation_error_handler(request: Request, exc: RequestValidationError):
208
454
  first = exc.errors()[0] if exc.errors() else {}
@@ -220,7 +466,24 @@ def create_studio_app(
220
466
  @app.get("/")
221
467
  async def index():
222
468
  path = static_root / "index.html"
223
- response = FileResponse(path, media_type="text/html")
469
+ html = path.read_text(encoding="utf-8")
470
+ # Inject a startup-scoped version so a restarted Studio with a rebuilt
471
+ # bundle always wins over a stale browser tab. The bundle filenames
472
+ # are already content-hashed; this only defeats cached index.html.
473
+ if "?v=" not in html:
474
+ import re
475
+
476
+ def _add_version(match: "re.Match[str]") -> str:
477
+ attr, path_part = match.group(1), match.group(2)
478
+ return f'{attr}="/static/assets/{path_part}?v={_studio_startup_epoch}"'
479
+
480
+ html = re.sub(
481
+ r'(src|href)="/static/assets/([^"]+)"',
482
+ _add_version,
483
+ html,
484
+ )
485
+ response = Response(content=html, media_type="text/html")
486
+ response.headers["Cache-Control"] = "no-store"
224
487
  if security_enabled:
225
488
  response.set_cookie(
226
489
  "agentkit_studio_session",
@@ -289,6 +552,16 @@ def create_studio_app(
289
552
  goal_objective = str(
290
553
  metadata.get("goal_objective") or metadata.get("goalObjective") or ""
291
554
  ).strip()
555
+ reasoning = payload.get("reasoning")
556
+ reasoning = reasoning if isinstance(reasoning, dict) else {}
557
+ reasoning_effort = str(reasoning.get("effort") or "").strip().lower()
558
+ if reasoning_effort and reasoning_effort not in {"low", "medium", "high"}:
559
+ raise StudioError(
560
+ "REASONING_EFFORT_INVALID",
561
+ "推理强度必须是 low、medium 或 high",
562
+ status_code=422,
563
+ field="reasoning.effort",
564
+ )
292
565
  session_id = _responses_session_id(payload, bridge=shared_web)
293
566
  response_id = str(
294
567
  metadata.get("invocation_id")
@@ -304,6 +577,8 @@ def create_studio_app(
304
577
  "ApprovalMode": requested_approval_mode,
305
578
  "CollaborationMode": collaboration_mode,
306
579
  "GoalObjective": goal_objective,
580
+ "ReasoningEffort": reasoning_effort,
581
+ "ModelExplicit": bool(str(payload.get("model") or "").strip()),
307
582
  }
308
583
  bridge_payload["Model"] = shared_web.select_model(
309
584
  bridge_payload["AgentId"],
@@ -364,14 +639,14 @@ def create_studio_app(
364
639
  elif action == "DeleteSession":
365
640
  data = shared_web.delete_session(str(payload.get("SessionId") or ""))
366
641
  elif action == "ListSessionMessages":
367
- data = shared_web.list_messages(
642
+ data = await shared_web.list_messages(
368
643
  str(payload.get("SessionId") or ""),
369
644
  after_seq_id=_optional_int(payload.get("AfterSeqId")),
370
645
  before_seq_id=_optional_int(payload.get("BeforeSeqId")),
371
646
  limit=int(payload.get("Limit") or 50),
372
647
  )
373
648
  elif action == "ListSessionEvents":
374
- data = shared_web.list_session_events(str(payload.get("SessionId") or ""))
649
+ data = await shared_web.list_session_events(str(payload.get("SessionId") or ""))
375
650
  elif action == "RunAgent":
376
651
  return StreamingResponse(
377
652
  shared_web.stream_run(payload),
@@ -461,6 +736,7 @@ def create_studio_app(
461
736
  "name": studio.workspace.root.name,
462
737
  "path": str(studio.workspace.root),
463
738
  },
739
+ "operationScope": studio.deployment_operation_scope(),
464
740
  "features": {
465
741
  "build": True,
466
742
  "run": True,
@@ -470,8 +746,10 @@ def create_studio_app(
470
746
  "deployment": True,
471
747
  "cloudRebuild": False,
472
748
  "reactChat": True,
749
+ "scheduler": studio.scheduler.availability(),
473
750
  },
474
751
  "runtimes": studio.runtime_catalog(),
752
+ "importableProject": studio.detect_importable_project(),
475
753
  }
476
754
 
477
755
  @app.get("/api/v1/system/settings")
@@ -482,9 +760,118 @@ def create_studio_app(
482
760
  async def update_settings(payload: dict[str, Any]):
483
761
  return studio.update_settings(payload)
484
762
 
763
+ @app.get("/api/v1/schedules")
764
+ async def list_schedules():
765
+ return {
766
+ "items": studio.scheduler.list_tasks(),
767
+ "availability": studio.scheduler.availability(),
768
+ }
769
+
770
+ @app.post("/api/v1/schedules", status_code=201)
771
+ async def create_schedule(payload: ScheduledTask):
772
+ studio.validate_schedule_build(
773
+ payload.target.agent_version_ref,
774
+ agent_id=payload.target.agent_id,
775
+ )
776
+ return studio.scheduler.create_task(payload)
777
+
778
+ @app.get("/api/v1/schedules/{task_id}")
779
+ async def get_schedule(task_id: str):
780
+ return studio.scheduler.get_task(task_id)
781
+
782
+ @app.put("/api/v1/schedules/{task_id}")
783
+ async def update_schedule(task_id: str, payload: ScheduledTask):
784
+ studio.validate_schedule_build(
785
+ payload.target.agent_version_ref,
786
+ agent_id=payload.target.agent_id,
787
+ )
788
+ return studio.scheduler.update_task(task_id, payload)
789
+
790
+ @app.delete("/api/v1/schedules/{task_id}", status_code=204)
791
+ async def delete_schedule(task_id: str):
792
+ studio.scheduler.delete_task(task_id)
793
+ return Response(status_code=204)
794
+
795
+ @app.post("/api/v1/schedules/{task_id}:run", status_code=202)
796
+ async def run_schedule_now(task_id: str):
797
+ return await studio.scheduler.run_now(task_id)
798
+
799
+ @app.get("/api/v1/schedules/{task_id}/occurrences")
800
+ async def list_schedule_occurrences(task_id: str, limit: int = Query(default=50, ge=1, le=200)):
801
+ return {"items": studio.scheduler.list_occurrences(task_id, limit=limit)}
802
+
803
+ @app.get("/api/v1/schedule-occurrences")
804
+ async def list_all_schedule_occurrences(
805
+ limit: int = Query(default=200, ge=1, le=500),
806
+ ):
807
+ return {"items": studio.scheduler.list_all_occurrences(limit=limit)}
808
+
809
+ @app.get("/api/v1/agents/{agent_id}/schedules")
810
+ async def list_agent_schedules(agent_id: str):
811
+ return {
812
+ "items": studio.list_agent_schedules(agent_id),
813
+ "availability": studio.scheduler.availability(),
814
+ }
815
+
816
+ @app.post("/api/v1/agents/{agent_id}/schedules", status_code=201)
817
+ async def create_agent_schedule(agent_id: str, payload: AgentScheduleRequest):
818
+ return await studio.create_agent_schedule(
819
+ agent_id,
820
+ display_name=payload.display_name,
821
+ prompt=payload.prompt,
822
+ schedule=payload.schedule,
823
+ enabled=payload.enabled,
824
+ continuity=payload.continuity,
825
+ session_id=payload.session_id,
826
+ )
827
+
828
+ @app.get("/api/v1/agents/{agent_id}/schedules/{task_id}")
829
+ async def get_agent_schedule(agent_id: str, task_id: str):
830
+ return studio.get_agent_schedule(agent_id, task_id)
831
+
832
+ @app.put("/api/v1/agents/{agent_id}/schedules/{task_id}")
833
+ async def update_agent_schedule(
834
+ agent_id: str,
835
+ task_id: str,
836
+ payload: AgentScheduleRequest,
837
+ ):
838
+ return studio.update_agent_schedule(
839
+ agent_id,
840
+ task_id,
841
+ display_name=payload.display_name,
842
+ prompt=payload.prompt,
843
+ schedule=payload.schedule,
844
+ enabled=payload.enabled,
845
+ continuity=payload.continuity,
846
+ session_id=payload.session_id,
847
+ )
848
+
849
+ @app.delete("/api/v1/agents/{agent_id}/schedules/{task_id}", status_code=204)
850
+ async def delete_agent_schedule(agent_id: str, task_id: str):
851
+ studio.get_agent_schedule(agent_id, task_id)
852
+ studio.scheduler.delete_task(task_id)
853
+ return Response(status_code=204)
854
+
855
+ @app.post("/api/v1/agents/{agent_id}/schedules/{task_id}:run", status_code=202)
856
+ async def run_agent_schedule_now(agent_id: str, task_id: str):
857
+ return await studio.run_agent_schedule_now(agent_id, task_id)
858
+
859
+ @app.get("/api/v1/agents/{agent_id}/schedules/{task_id}/occurrences")
860
+ async def list_agent_schedule_occurrences(
861
+ agent_id: str,
862
+ task_id: str,
863
+ limit: int = Query(default=50, ge=1, le=200),
864
+ ):
865
+ studio.get_agent_schedule(agent_id, task_id)
866
+ return {"items": studio.scheduler.list_occurrences(task_id, limit=limit)}
867
+
485
868
  @app.post("/api/v1/workspaces:open")
486
869
  async def open_workspace(payload: WorkspaceOpenRequest):
487
- if not studio.workspace.matches_configured_root_path(payload.path):
870
+ # This endpoint only reconnects to the daemon's already-bound root. Do
871
+ # not resolve or otherwise touch a caller-provided filesystem path.
872
+ requested = os.path.normcase(os.path.abspath(os.path.expanduser(payload.path)))
873
+ bound_root = os.path.normcase(str(studio.workspace.root))
874
+ if requested != bound_root:
488
875
  raise StudioError(
489
876
  "WORKSPACE_PATH_FORBIDDEN",
490
877
  "当前 Daemon 不允许切换到启动 root 之外的工作区",
@@ -554,57 +941,117 @@ def create_studio_app(
554
941
  "Codex Studio 只支持只读本地运行",
555
942
  status_code=422,
556
943
  )
557
-
558
- queue: asyncio.Queue = asyncio.Queue()
559
-
560
- def observe(event):
561
- queue.put_nowait(event)
562
-
563
- # The Operation owns the runtime task. The SSE response is only one
564
- # observer, so refreshing or switching chats cannot cancel the Run.
565
- operation = studio.submit_studio_run(
944
+ return _stream_studio_run(
566
945
  build_id,
567
- payload.input.content,
946
+ user_input=payload.input.content,
568
947
  session_id=payload.session_id,
569
948
  model=payload.model,
570
949
  sandbox=payload.sandbox,
571
950
  idempotency_key=key,
572
- on_event=observe,
573
951
  )
574
952
 
575
- async def render():
576
- while True:
577
- try:
578
- event = await asyncio.wait_for(queue.get(), timeout=0.1)
579
- except TimeoutError:
580
- event = None
581
- if event is not None:
582
- data = json.dumps(
583
- event.data,
584
- ensure_ascii=False,
585
- separators=(",", ":"),
586
- )
587
- yield f"id: {event.id}\nevent: {event.type}\ndata: {data}\n\n"
588
- current = studio.operations.get(operation.id)
589
- if (
590
- current.status
591
- in {
592
- OperationStatus.SUCCEEDED,
593
- OperationStatus.FAILED,
594
- OperationStatus.CANCELLED,
595
- OperationStatus.INTERRUPTED,
596
- }
597
- and queue.empty()
598
- ):
599
- if current.status == OperationStatus.FAILED:
600
- data = json.dumps(current.error or {}, ensure_ascii=False)
601
- yield f"event: run.failed\ndata: {data}\n\n"
602
- break
953
+ @app.get("/api/v1/builds/{build_id}/conversation-surface")
954
+ async def get_conversation_surface(
955
+ build_id: str,
956
+ session_id: str = Query(alias="sessionId", min_length=1, max_length=256),
957
+ ):
958
+ return studio.conversation_surface(build_id, session_id=session_id)
603
959
 
604
- return StreamingResponse(
605
- render(),
606
- media_type="text/event-stream",
607
- headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
960
+ @app.get("/api/v1/agents/{agent_id}/conversation-surface")
961
+ async def get_agent_conversation_surface(
962
+ agent_id: str,
963
+ session_id: str = Query(alias="sessionId", min_length=1, max_length=256),
964
+ ):
965
+ """Resolve the immutable Build and its composer contract atomically.
966
+
967
+ The browser selects an Agent, not a mutable Draft or a Build id. This
968
+ route gives it the same current-Build decision used by chat and local
969
+ Scheduler authoring, so controls cannot be rendered from a stale or
970
+ unrelated Build.
971
+ """
972
+
973
+ build = await studio.ensure_current_build(agent_id)
974
+ return {
975
+ "buildId": build.id,
976
+ "surface": studio.conversation_surface(
977
+ build.id,
978
+ session_id=session_id,
979
+ ),
980
+ }
981
+
982
+ @app.post("/api/v1/builds/{build_id}/conversation:stream")
983
+ async def stream_conversation_turn(
984
+ build_id: str,
985
+ payload: ConversationTurnRequest,
986
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
987
+ ):
988
+ key = _require_idempotency_key(idempotency_key)
989
+ conversation_input = payload.input
990
+ if conversation_input.idempotency_key != key:
991
+ raise StudioError(
992
+ "CONVERSATION_IDEMPOTENCY_MISMATCH",
993
+ "ConversationInput 的幂等键必须与请求头一致",
994
+ status_code=422,
995
+ )
996
+ surface = studio.conversation_surface(
997
+ build_id,
998
+ session_id=conversation_input.session_id,
999
+ )
1000
+ try:
1001
+ validate_conversation_input(surface, conversation_input)
1002
+ except ValueError as exc:
1003
+ raise StudioError(
1004
+ "CONVERSATION_INPUT_UNSUPPORTED",
1005
+ "当前 Agent 不支持此会话输入",
1006
+ status_code=422,
1007
+ details={"reason": str(exc), "surfaceId": surface.surface_id},
1008
+ ) from exc
1009
+ text_parts = [
1010
+ part.text for part in conversation_input.parts if isinstance(part, ConversationTextPart)
1011
+ ]
1012
+ runtime_parts: list[dict[str, str]] = [
1013
+ {"type": "text", "text": value} for value in text_parts
1014
+ ]
1015
+ attachment_names: list[str] = []
1016
+ for part in conversation_input.parts:
1017
+ if not isinstance(part, ConversationAttachmentPart):
1018
+ continue
1019
+ stored = studio.conversation_attachments.resolve(part.attachment_ref)
1020
+ if part.media_type.lower() != stored.media_type.lower():
1021
+ raise StudioError(
1022
+ "CONVERSATION_ATTACHMENT_METADATA_MISMATCH",
1023
+ "会话附件类型与已上传内容不一致",
1024
+ status_code=422,
1025
+ details={"attachmentRef": part.attachment_ref},
1026
+ )
1027
+ encoded = base64.b64encode(stored.path.read_bytes()).decode("ascii")
1028
+ data_url = f"data:{stored.media_type};base64,{encoded}"
1029
+ attachment_names.append(part.name or stored.name)
1030
+ if stored.media_type.startswith("image/"):
1031
+ runtime_parts.append({"type": "image", "url": data_url})
1032
+ else:
1033
+ runtime_parts.append(
1034
+ {
1035
+ "type": "input_file",
1036
+ "filename": part.name or stored.name,
1037
+ "file_data": data_url,
1038
+ }
1039
+ )
1040
+ display_input = "\n".join(text_parts).strip()
1041
+ if not display_input:
1042
+ display_input = "请处理本轮附件:" + "、".join(attachment_names)
1043
+ return _stream_studio_run(
1044
+ build_id,
1045
+ user_input=display_input,
1046
+ session_id=conversation_input.session_id,
1047
+ model=conversation_input.model_ref,
1048
+ sandbox=payload.sandbox,
1049
+ reasoning_effort=conversation_input.reasoning,
1050
+ approval_mode=conversation_input.approval_mode,
1051
+ collaboration_mode=conversation_input.collaboration_mode,
1052
+ goal_objective=conversation_input.goal_objective,
1053
+ runtime_input=runtime_parts if attachment_names else None,
1054
+ idempotency_key=key,
608
1055
  )
609
1056
 
610
1057
  @app.post("/api/v1/agents", status_code=201)
@@ -618,6 +1065,15 @@ def create_studio_app(
618
1065
  runtime=payload.runtime,
619
1066
  )
620
1067
 
1068
+ @app.post("/api/v1/conversation-attachments", status_code=201)
1069
+ async def upload_conversation_attachment(file: UploadFile = File(...)):
1070
+ content = await file.read(studio.conversation_attachments.MAX_BYTES + 1)
1071
+ return studio.conversation_attachments.store(
1072
+ content,
1073
+ filename=file.filename or "attachment",
1074
+ media_type=file.content_type or "application/octet-stream",
1075
+ )
1076
+
621
1077
  @app.post("/api/v1/assets/agent-avatars", status_code=201)
622
1078
  async def upload_agent_avatar(request: Request):
623
1079
  declared_size = request.headers.get("Content-Length")
@@ -674,8 +1130,26 @@ def create_studio_app(
674
1130
  return await studio.compose_agent_conversation(
675
1131
  messages=[item.model_dump(mode="json") for item in payload.messages],
676
1132
  model_profile_id=payload.model_profile_id,
1133
+ runtime_type=payload.runtime_type,
1134
+ agent_model_profile_ids=payload.agent_model_profile_ids,
1135
+ agent_default_model_profile_id=payload.agent_default_model_profile_id,
1136
+ tool_resource_ids=payload.tool_resource_ids,
1137
+ mcp_resource_ids=payload.mcp_resource_ids,
1138
+ skill_resource_ids=payload.skill_resource_ids,
1139
+ request_id=payload.request_id,
677
1140
  )
678
1141
 
1142
+ @app.get("/api/v1/authoring/conversations:status/{request_id}")
1143
+ async def get_authoring_conversation_status(request_id: str):
1144
+ status = studio.conversation_authoring_status(request_id)
1145
+ if status is None:
1146
+ raise StudioError(
1147
+ "AUTHORING_STATUS_NOT_FOUND",
1148
+ "未找到该构建请求的阶段记录",
1149
+ status_code=404,
1150
+ )
1151
+ return status
1152
+
679
1153
  @app.post("/api/v1/authoring/imports:inspect")
680
1154
  async def inspect_agent_import(file: UploadFile = File(...)):
681
1155
  content = await file.read(100 * 1024 * 1024 + 1)
@@ -740,6 +1214,7 @@ def create_studio_app(
740
1214
  agent_id: str,
741
1215
  spec: AgentSpec,
742
1216
  if_match: str | None = Header(default=None, alias="If-Match"),
1217
+ name: str | None = Query(default=None, min_length=1, max_length=128),
743
1218
  ):
744
1219
  if not if_match:
745
1220
  raise StudioError(
@@ -759,6 +1234,7 @@ def create_studio_app(
759
1234
  agent_id,
760
1235
  spec,
761
1236
  expected_revision=revision,
1237
+ name=name,
762
1238
  )
763
1239
 
764
1240
  @app.put("/api/v1/agents/{agent_id}/bindings")
@@ -799,6 +1275,33 @@ def create_studio_app(
799
1275
  level=payload.level,
800
1276
  )
801
1277
 
1278
+ @app.post("/api/v1/workspace:import-root", status_code=201)
1279
+ async def import_root_project(payload: ImportRootRequest):
1280
+ """PR-S6:一键导入根 Framework 项目(方案 §6.1)。"""
1281
+ return studio.import_root_project(name=payload.name, slug=payload.slug)
1282
+
1283
+ @app.post("/api/v1/agents/{agent_id}/prompt:compile")
1284
+ async def compile_prompt(agent_id: str, payload: PromptCompileRequest):
1285
+ """PR-S2:Prompt 编译预览(方案 §6.2)。只读,不写 Session/Trace/Build。"""
1286
+ return studio.compile_prompt_preview(
1287
+ agent_id,
1288
+ request_instructions=payload.request_instructions,
1289
+ include_content=payload.include_content,
1290
+ )
1291
+
1292
+ @app.post("/api/v1/agents/{agent_id}/context:preview")
1293
+ async def preview_context(agent_id: str, payload: ContextPreviewRequest):
1294
+ """PR-S2:Context 预览(方案 §6.2)。复用真实 Planner,不调模型。"""
1295
+ return await studio.preview_context(
1296
+ agent_id,
1297
+ user_input=payload.user_input,
1298
+ request_instructions=payload.request_instructions,
1299
+ simulated_history=[
1300
+ {"role": m.role, "content": m.content} for m in payload.simulated_history
1301
+ ],
1302
+ include_content=payload.include_content,
1303
+ )
1304
+
802
1305
  @app.post("/api/v1/agents/{agent_id}/builds", status_code=202)
803
1306
  async def create_build(
804
1307
  agent_id: str,
@@ -882,13 +1385,161 @@ def create_studio_app(
882
1385
  interaction_id,
883
1386
  name=payload.name,
884
1387
  data=payload.data,
1388
+ expected_revision=payload.expected_revision,
1389
+ idempotency_key=payload.idempotency_key,
885
1390
  )
886
1391
 
1392
+ @app.get("/api/v1/runs/{run_id}/context")
1393
+ async def get_run_context(run_id: str):
1394
+ """Runtime Context Evidence:planned/projected/actual + 精度 + ownership。"""
1395
+ record = studio.event_store.get(run_id)
1396
+ plan = record.context_plan or {}
1397
+ evidence = record.prompt_evidence or {}
1398
+ return {
1399
+ "planId": plan.get("plan_id"),
1400
+ "accuracy": evidence.get("accountingAccuracy")
1401
+ or plan.get("accounting_accuracy", "opaque"),
1402
+ "policyVersion": plan.get("policy_version"),
1403
+ "tokensByKind": plan.get("tokens_by_kind", {}),
1404
+ "plannedInputTokens": plan.get("planned_input_tokens"),
1405
+ "projectedInputTokens": plan.get("projected_input_tokens"),
1406
+ "runtimeReportedInputTokens": plan.get("runtime_reported_input_tokens"),
1407
+ "selected": plan.get("selected", []),
1408
+ "decisions": plan.get("decisions", []),
1409
+ "ownership": {
1410
+ "promptOwner": evidence.get("promptOwner"),
1411
+ "historyOwner": (plan.get("history_owner") if isinstance(plan, dict) else None),
1412
+ "integrationMode": evidence.get("integrationMode"),
1413
+ "runtimeType": evidence.get("runtimeType"),
1414
+ "deploymentMode": evidence.get("deploymentMode"),
1415
+ "capabilityHash": evidence.get("capabilityHash"),
1416
+ },
1417
+ "warnings": [],
1418
+ }
1419
+
1420
+ @app.get("/api/v1/runs/{run_id}/prompt")
1421
+ async def get_run_prompt(run_id: str, include_content: bool = Query(default=False)):
1422
+ """PR-S4:Prompt evidence(方案 §6.3 / §7.3)。section hash/版本,默认不返回正文。"""
1423
+ record = studio.event_store.get(run_id)
1424
+ evidence = record.prompt_evidence or {}
1425
+ result = {
1426
+ "contentHash": evidence.get("contentHash"),
1427
+ "stablePrefixHash": evidence.get("stablePrefixHash"),
1428
+ "sectionHashes": evidence.get("sectionHashes", {}),
1429
+ "tokensBySection": evidence.get("tokensBySection", {}),
1430
+ "estimatedTokens": evidence.get("estimatedTokens"),
1431
+ "sectionCount": evidence.get("sectionCount"),
1432
+ "plannedInputTokens": evidence.get("plannedInputTokens"),
1433
+ "accountingAccuracy": evidence.get("accountingAccuracy"),
1434
+ "runtimeType": evidence.get("runtimeType"),
1435
+ "integrationMode": evidence.get("integrationMode"),
1436
+ }
1437
+ if include_content:
1438
+ result["reveal"] = studio.reveal_run_prompt(run_id)
1439
+ return result
1440
+
1441
+ @app.get("/api/v1/runs/{run_id}/working-state")
1442
+ async def get_run_working_state(run_id: str):
1443
+ """PR-S4:Working State evidence(方案 §6.5)。从 checkpoint/read record 读取。"""
1444
+ record = studio.event_store.get(run_id)
1445
+ return {"workingState": record.working_state}
1446
+
887
1447
  @app.delete("/api/v1/sessions/{session_id}", status_code=204)
888
1448
  async def delete_studio_session(session_id: str):
889
- studio.delete_session(session_id)
1449
+ await studio.delete_session(session_id)
890
1450
  return Response(status_code=204)
891
1451
 
1452
+ @app.get("/api/v1/sessions/{session_id}/events")
1453
+ async def session_events(
1454
+ session_id: str,
1455
+ before_seq_id: int | None = Query(default=None, ge=1, alias="beforeSeqId"),
1456
+ invocation_id: str | None = Query(default=None, alias="invocationId"),
1457
+ limit: int = Query(default=100, ge=1, le=500),
1458
+ ):
1459
+ return await studio.trajectory_page(
1460
+ session_id,
1461
+ before_seq_id=before_seq_id,
1462
+ invocation_id=invocation_id,
1463
+ limit=limit,
1464
+ )
1465
+
1466
+ @app.get("/api/v1/sessions/{session_id}/events/stream")
1467
+ async def session_event_stream(
1468
+ session_id: str,
1469
+ request: Request,
1470
+ after_seq_id: int = Query(default=0, ge=0, alias="afterSeqId"),
1471
+ invocation_id: str | None = Query(default=None, alias="invocationId"),
1472
+ ):
1473
+ await studio._require_runtime_session(session_id)
1474
+ last = request.headers.get("Last-Event-ID")
1475
+ cursor = int(last) if last and last.isdigit() else after_seq_id
1476
+ stream = studio.stream_trajectory(
1477
+ session_id,
1478
+ cursor,
1479
+ invocation_id=invocation_id,
1480
+ )
1481
+
1482
+ async def frames():
1483
+ try:
1484
+ async for frame in stream:
1485
+ if await request.is_disconnected():
1486
+ return
1487
+ yield frame
1488
+ finally:
1489
+ await stream.aclose()
1490
+
1491
+ return StreamingResponse(
1492
+ frames(),
1493
+ media_type="text/event-stream",
1494
+ headers={
1495
+ "Cache-Control": "no-store",
1496
+ "X-Accel-Buffering": "no",
1497
+ },
1498
+ )
1499
+
1500
+ @app.post("/api/v1/sessions/{session_id}:export")
1501
+ async def export_session(session_id: str, payload: dict[str, Any]):
1502
+ filename = payload.get("filename")
1503
+ invocation_id = payload.get("invocationId")
1504
+ download = payload.get("download", False)
1505
+ if not isinstance(filename, str):
1506
+ raise StudioError(
1507
+ "SESSION_EXPORT_FILENAME_INVALID",
1508
+ "filename 必须是字符串",
1509
+ status_code=422,
1510
+ field="filename",
1511
+ )
1512
+ if invocation_id is not None and not isinstance(invocation_id, str):
1513
+ raise StudioError(
1514
+ "SESSION_EXPORT_INVOCATION_INVALID",
1515
+ "invocationId 必须是字符串",
1516
+ status_code=422,
1517
+ field="invocationId",
1518
+ )
1519
+ if not isinstance(download, bool):
1520
+ raise StudioError(
1521
+ "SESSION_EXPORT_DOWNLOAD_INVALID",
1522
+ "download 必须是布尔值",
1523
+ status_code=422,
1524
+ field="download",
1525
+ )
1526
+ result = await studio.export_runtime_session(
1527
+ session_id,
1528
+ filename=filename,
1529
+ invocation_id=invocation_id,
1530
+ )
1531
+ if not download:
1532
+ return result
1533
+
1534
+ path = studio.workspace.resolve(result["path"])
1535
+ return FileResponse(
1536
+ path,
1537
+ filename=filename,
1538
+ media_type="application/x-ndjson",
1539
+ headers={"X-Session-Event-Count": str(result["eventCount"])},
1540
+ background=BackgroundTask(path.unlink, missing_ok=True),
1541
+ )
1542
+
892
1543
  @app.get("/api/v1/runs")
893
1544
  async def list_runs(session_id: str | None = Query(default=None, alias="sessionId")):
894
1545
  return {"items": studio.event_store.list_runs(session_id=session_id)}
@@ -901,7 +1552,7 @@ def create_studio_app(
901
1552
  ):
902
1553
  last = request.headers.get("Last-Event-ID")
903
1554
  cursor = int(last) if last and last.isdigit() else after
904
- events = studio.event_store.events(run_id, after=cursor)
1555
+ events = await studio.run_service.events(run_id, after=cursor)
905
1556
  return _sse(events)
906
1557
 
907
1558
  @app.get("/api/v1/traces/overview")
@@ -942,19 +1593,6 @@ def create_studio_app(
942
1593
  async def get_trace_otlp(trace_id: str):
943
1594
  return studio.event_store.trace_otlp(trace_id)
944
1595
 
945
- @app.post("/api/v1/builds/{build_id}/evaluations", status_code=202)
946
- async def create_evaluation(
947
- build_id: str,
948
- payload: EvaluationRequest,
949
- idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
950
- ):
951
- return studio.submit_evaluation(
952
- build_id,
953
- payload.suite_refs,
954
- fail_fast=payload.fail_fast,
955
- idempotency_key=_require_idempotency_key(idempotency_key),
956
- )
957
-
958
1596
  @app.post("/api/v1/evaluations", status_code=202)
959
1597
  async def create_public_evaluation(
960
1598
  payload: StudioEvaluationCreate,
@@ -967,16 +1605,39 @@ def create_studio_app(
967
1605
  idempotency_key=_require_idempotency_key(idempotency_key),
968
1606
  )
969
1607
 
1608
+ @app.post("/api/v1/evaluation-files", status_code=201)
1609
+ async def import_evaluation_file(file: UploadFile = File(...)):
1610
+ return studio.import_evaluation_file(
1611
+ await file.read(2 * 1024 * 1024 + 1),
1612
+ filename=file.filename or "evalset.yaml",
1613
+ )
1614
+
970
1615
  @app.get("/api/v1/evaluations")
971
1616
  async def list_public_evaluations():
972
1617
  return {"items": studio.list_public_evaluations()}
973
1618
 
1619
+ @app.get("/api/v1/evaluation-runs")
1620
+ async def list_public_evaluation_runs():
1621
+ return {"items": studio.list_public_evaluation_runs()}
1622
+
1623
+ @app.get("/api/v1/evaluation-runs/{evaluation_id}")
1624
+ async def get_public_evaluation_run(evaluation_id: str):
1625
+ return studio.get_public_evaluation_run(evaluation_id)
1626
+
1627
+ @app.get("/api/v1/evaluation-targets")
1628
+ async def list_evaluation_targets():
1629
+ return studio.evaluation_catalog()
1630
+
1631
+ @app.get("/api/v1/evaluation-cloud/catalog")
1632
+ async def list_evaluation_cloud_catalog(
1633
+ project_id: str | None = Query(default=None, alias="projectId"),
1634
+ ):
1635
+ items = await studio.evaluation_cloud_catalog(project_id=project_id)
1636
+ return {"items": items}
1637
+
974
1638
  @app.get("/api/v1/evaluations/{evaluation_id}")
975
1639
  async def get_evaluation(evaluation_id: str):
976
- report_path = studio.evaluation_storage.report_path(evaluation_id)
977
- if report_path.is_file():
978
- return studio.get_public_evaluation(evaluation_id)
979
- return studio.evaluations.get(evaluation_id)
1640
+ return studio.get_public_evaluation(evaluation_id)
980
1641
 
981
1642
  @app.get("/api/v1/evaluations/{evaluation_id}/cases/{case_id}")
982
1643
  async def get_public_evaluation_case(evaluation_id: str, case_id: str):
@@ -1003,9 +1664,337 @@ def create_studio_app(
1003
1664
  idempotency_key=_require_idempotency_key(idempotency_key),
1004
1665
  )
1005
1666
 
1667
+ @app.get("/api/v1/deployments")
1668
+ async def list_deployments():
1669
+ """Read local deployment receipts without implicit cloud refreshes."""
1670
+
1671
+ return {"items": studio.cloud.list()}
1672
+
1673
+ @app.get("/api/v1/cloud-agents")
1674
+ async def list_account_cloud_agents(
1675
+ page: int = Query(default=1, ge=1),
1676
+ size: int = Query(default=100, ge=1, le=100),
1677
+ ):
1678
+ """List Agents visible to Studio's configured signed cloud account."""
1679
+
1680
+ return await studio.cloud.list_account_agents(page=page, size=size)
1681
+
1682
+ @app.get("/api/v1/cloud-agents/{agent_id}")
1683
+ async def get_account_cloud_agent(agent_id: str):
1684
+ return await studio.cloud.get_account_agent(agent_id)
1685
+
1686
+ @app.get("/api/v1/cloud-agents/{agent_id}/versions")
1687
+ async def list_account_cloud_agent_versions(
1688
+ agent_id: str,
1689
+ page: int = Query(default=1, ge=1),
1690
+ size: int = Query(default=100, ge=1, le=100),
1691
+ ):
1692
+ """List the Server-owned version history and rollback eligibility."""
1693
+
1694
+ return await studio.cloud.list_account_agent_versions(
1695
+ agent_id,
1696
+ page=page,
1697
+ size=size,
1698
+ )
1699
+
1700
+ @app.post(
1701
+ "/api/v1/cloud-agents/{agent_id}:rollback-version",
1702
+ status_code=202,
1703
+ )
1704
+ async def rollback_account_cloud_agent_version(
1705
+ agent_id: str,
1706
+ payload: CloudAgentVersionRollbackRequest,
1707
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
1708
+ ):
1709
+ """Submit Server RollbackVersion through Studio's process-only AK/SK."""
1710
+
1711
+ return studio.submit_account_agent_version_rollback(
1712
+ agent_id,
1713
+ version_id=payload.version_id,
1714
+ idempotency_key=_require_idempotency_key(idempotency_key),
1715
+ )
1716
+
1717
+ @app.post("/api/v1/cloud-agents/{agent_id}:dashboard")
1718
+ async def open_account_cloud_agent_dashboard(agent_id: str):
1719
+ return await studio.cloud.account_agent_dashboard_access(agent_id)
1720
+
1721
+ @app.delete("/api/v1/cloud-agents/{agent_id}")
1722
+ async def delete_account_cloud_agent(agent_id: str):
1723
+ return await studio.cloud.delete_account_agent(agent_id)
1724
+
1006
1725
  @app.get("/api/v1/deployments/{deployment_id}")
1007
1726
  async def get_deployment(deployment_id: str):
1008
- return studio.cloud.get(deployment_id)
1727
+ return await studio.cloud.refresh(deployment_id)
1728
+
1729
+ @app.post("/api/v1/deployments/{deployment_id}:dashboard")
1730
+ async def open_deployment_dashboard(deployment_id: str):
1731
+ return await studio.deployment_dashboard_access(deployment_id)
1732
+
1733
+ @app.delete("/api/v1/deployments/{deployment_id}")
1734
+ async def delete_deployment(deployment_id: str):
1735
+ """Delete the receipt-bound cloud Agent and its superseded local receipts."""
1736
+
1737
+ return await studio.cloud.delete(deployment_id)
1738
+
1739
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/sessions")
1740
+ async def list_cloud_chat_sessions(
1741
+ deployment_id: str,
1742
+ page: int = Query(default=1, ge=1),
1743
+ size: int = Query(default=50, ge=1, le=100),
1744
+ ):
1745
+ """List Server-owned sessions for this local deployment receipt only."""
1746
+
1747
+ return await studio.cloud.list_cloud_chat_sessions(deployment_id, page=page, size=size)
1748
+
1749
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/models")
1750
+ async def list_cloud_chat_models(deployment_id: str):
1751
+ """List models through Studio's signed Server client."""
1752
+
1753
+ return await studio.cloud.list_cloud_chat_models(deployment_id)
1754
+
1755
+ @app.post(
1756
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions",
1757
+ status_code=201,
1758
+ )
1759
+ async def create_cloud_chat_session(deployment_id: str):
1760
+ """Create a cloud session via loopback-held AK/SK; no secret reaches JS."""
1761
+
1762
+ return await studio.cloud.create_cloud_chat_session(deployment_id)
1763
+
1764
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/messages")
1765
+ async def list_cloud_chat_messages(
1766
+ deployment_id: str,
1767
+ session_id: str,
1768
+ after_seq_id: int | None = Query(default=None, alias="afterSeqId", ge=0),
1769
+ limit: int = Query(default=100, ge=1, le=200),
1770
+ ):
1771
+ return await studio.cloud.list_cloud_chat_messages(
1772
+ deployment_id,
1773
+ session_id=session_id,
1774
+ after_seq_id=after_seq_id,
1775
+ limit=limit,
1776
+ )
1777
+
1778
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/events")
1779
+ async def list_cloud_chat_events(
1780
+ deployment_id: str,
1781
+ session_id: str,
1782
+ after_seq_id: int | None = Query(default=None, alias="afterSeqId", ge=0),
1783
+ offset: int | None = Query(default=None, ge=0),
1784
+ limit: int = Query(default=200, ge=1, le=1000),
1785
+ ):
1786
+ return await studio.cloud.list_cloud_chat_events(
1787
+ deployment_id,
1788
+ session_id=session_id,
1789
+ after_seq_id=after_seq_id,
1790
+ offset=offset,
1791
+ limit=limit,
1792
+ )
1793
+
1794
+ @app.get("/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/events/stream")
1795
+ async def stream_cloud_chat_events(
1796
+ request: Request,
1797
+ deployment_id: str,
1798
+ session_id: str,
1799
+ after_seq_id: int = Query(default=0, alias="afterSeqId", ge=0),
1800
+ ):
1801
+ """Stream canonical cloud events to the loopback browser.
1802
+
1803
+ The public cloud control plane currently exposes cursor reads for this
1804
+ surface. Keep that cursor in the Studio backend and present one SSE
1805
+ response to the browser, so assistant deltas arrive before the durable
1806
+ terminal message projection without exposing cloud credentials to JS.
1807
+ """
1808
+
1809
+ async def event_stream() -> AsyncIterator[str]:
1810
+ cursor = after_seq_id
1811
+ idle_polls = 0
1812
+ while not await request.is_disconnected():
1813
+ payload = await studio.cloud.list_cloud_chat_events(
1814
+ deployment_id,
1815
+ session_id=session_id,
1816
+ after_seq_id=cursor,
1817
+ limit=200,
1818
+ )
1819
+ events = payload.get("events") or []
1820
+ if not isinstance(events, list):
1821
+ events = []
1822
+ terminal = False
1823
+ emitted = False
1824
+ for event in events:
1825
+ if not isinstance(event, dict):
1826
+ continue
1827
+ event_payload = (
1828
+ event.get("payload") if isinstance(event.get("payload"), dict) else event
1829
+ )
1830
+ seq = int(
1831
+ event_payload.get("seq")
1832
+ or event_payload.get("seq_id")
1833
+ or event_payload.get("source_session_seq")
1834
+ or event.get("seq")
1835
+ or event.get("seq_id")
1836
+ or 0
1837
+ )
1838
+ if seq and seq <= cursor:
1839
+ continue
1840
+ if seq:
1841
+ cursor = max(cursor, seq)
1842
+ event_type = str(
1843
+ event.get("event_type")
1844
+ or event.get("eventType")
1845
+ or event_payload.get("event_type")
1846
+ or event_payload.get("eventType")
1847
+ or ""
1848
+ ).lower()
1849
+ content = (
1850
+ event_payload.get("content")
1851
+ if isinstance(event_payload.get("content"), dict)
1852
+ else {}
1853
+ )
1854
+ status = str(event_payload.get("status") or content.get("status") or "").lower()
1855
+ event_is_terminal = event_type in {
1856
+ "run.completed",
1857
+ "run.complete",
1858
+ "run.succeeded",
1859
+ "run.failed",
1860
+ "run.cancelled",
1861
+ "run.expired",
1862
+ "run.error",
1863
+ } or (
1864
+ event_type in {"run_status", "run.status"}
1865
+ and status
1866
+ in {
1867
+ "completed",
1868
+ "complete",
1869
+ "succeeded",
1870
+ "success",
1871
+ "failed",
1872
+ "cancelled",
1873
+ "canceled",
1874
+ "expired",
1875
+ "error",
1876
+ "aborted",
1877
+ }
1878
+ )
1879
+ terminal = terminal or event_is_terminal
1880
+ emitted = True
1881
+ projected_event = dict(event)
1882
+ conversation_item = _cloud_event_conversation_item(
1883
+ event,
1884
+ session_id=session_id,
1885
+ )
1886
+ if conversation_item is not None:
1887
+ projected_event["conversationItem"] = conversation_item
1888
+ yield (
1889
+ (f"id: {seq}\n" if seq else "")
1890
+ + "event: session.event\n"
1891
+ + f"data: {json.dumps(projected_event, ensure_ascii=False)}\n\n"
1892
+ )
1893
+ if terminal or payload.get("session_deleted"):
1894
+ break
1895
+ idle_polls = 0 if emitted else idle_polls + 1
1896
+ if idle_polls and idle_polls % 20 == 0:
1897
+ yield ": keepalive\n\n"
1898
+ await asyncio.sleep(0.25)
1899
+
1900
+ return StreamingResponse(
1901
+ event_stream(),
1902
+ media_type="text/event-stream",
1903
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
1904
+ )
1905
+
1906
+ @app.delete(
1907
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}",
1908
+ status_code=204,
1909
+ )
1910
+ async def delete_cloud_chat_session(deployment_id: str, session_id: str):
1911
+ await studio.cloud.delete_cloud_chat_session(deployment_id, session_id=session_id)
1912
+ return Response(status_code=204)
1913
+
1914
+ @app.post(
1915
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/messages",
1916
+ status_code=202,
1917
+ )
1918
+ async def send_cloud_chat_message(
1919
+ deployment_id: str,
1920
+ session_id: str,
1921
+ payload: CloudChatMessageRequest,
1922
+ ):
1923
+ """Admit one cloud message through Server; response is a durable receipt."""
1924
+
1925
+ return await studio.cloud.send_cloud_chat_message(
1926
+ deployment_id,
1927
+ session_id=session_id,
1928
+ content=payload.content,
1929
+ model=payload.model,
1930
+ model_options=payload.model_options,
1931
+ tool_approval_mode=payload.tool_approval_mode,
1932
+ collaboration_mode=payload.collaboration_mode,
1933
+ goal_objective=payload.goal_objective,
1934
+ )
1935
+
1936
+ @app.post(
1937
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/messages/stream"
1938
+ )
1939
+ async def stream_cloud_chat_message(
1940
+ request: Request,
1941
+ deployment_id: str,
1942
+ session_id: str,
1943
+ payload: CloudChatMessageRequest,
1944
+ ):
1945
+ """Proxy one signed foreground RunAgent SSE response to loopback UI."""
1946
+
1947
+ upstream = await studio.cloud.stream_cloud_chat_message(
1948
+ deployment_id,
1949
+ session_id=session_id,
1950
+ content=payload.content,
1951
+ model=payload.model,
1952
+ model_options=payload.model_options,
1953
+ tool_approval_mode=payload.tool_approval_mode,
1954
+ collaboration_mode=payload.collaboration_mode,
1955
+ goal_objective=payload.goal_objective,
1956
+ )
1957
+
1958
+ async def proxy_stream() -> AsyncIterator[bytes]:
1959
+ try:
1960
+ while not await request.is_disconnected():
1961
+ try:
1962
+ chunk = await anext(upstream)
1963
+ except StopAsyncIteration:
1964
+ break
1965
+ if await request.is_disconnected():
1966
+ break
1967
+ yield chunk
1968
+ finally:
1969
+ close = getattr(upstream, "aclose", None)
1970
+ if close is not None:
1971
+ await close()
1972
+
1973
+ return StreamingResponse(
1974
+ proxy_stream(),
1975
+ media_type="text/event-stream",
1976
+ headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
1977
+ )
1978
+
1979
+ @app.post(
1980
+ "/api/v1/deployments/{deployment_id}/cloud-chat/sessions/{session_id}/interactions",
1981
+ status_code=202,
1982
+ )
1983
+ async def submit_cloud_chat_interaction(
1984
+ deployment_id: str,
1985
+ session_id: str,
1986
+ payload: CloudChatInteractionSubmitRequest,
1987
+ ):
1988
+ return await studio.cloud.submit_cloud_chat_interaction(
1989
+ deployment_id,
1990
+ session_id=session_id,
1991
+ run_id=payload.run_id,
1992
+ interaction_id=payload.interaction_id,
1993
+ expected_revision=payload.expected_revision,
1994
+ action=payload.action,
1995
+ response=payload.response,
1996
+ idempotency_key=payload.idempotency_key,
1997
+ )
1009
1998
 
1010
1999
  @app.post("/api/v1/deployments/{deployment_id}:rollback", status_code=202)
1011
2000
  async def rollback_deployment(
@@ -1035,12 +2024,17 @@ def create_studio_app(
1035
2024
  ):
1036
2025
  last = request.headers.get("Last-Event-ID")
1037
2026
  cursor = int(last) if last and last.isdigit() else after
1038
- return _sse(studio.operations.events(operation_id, after=cursor))
2027
+ events = studio.operations.events(operation_id, after=cursor)
2028
+ if "application/json" in request.headers.get("Accept", ""):
2029
+ return {"items": events}
2030
+ return _sse(events)
1039
2031
 
1040
2032
  register_catalog_routes(
1041
2033
  app,
1042
2034
  studio,
1043
2035
  runtime_model_catalog=runtime_model_catalog,
1044
2036
  )
2037
+ register_memory_routes(app, studio)
2038
+ register_plugin_routes(app, studio)
1045
2039
 
1046
2040
  return app