agentengine-sdk-python 0.8.0__py3-none-any.whl → 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (494) hide show
  1. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
  2. agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
  3. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +2 -0
  5. ksadk/a2a/_space_client_events.py +381 -0
  6. ksadk/a2a/bootstrap.py +0 -2
  7. ksadk/a2a/card.py +6 -111
  8. ksadk/a2a/control_plane.py +239 -1
  9. ksadk/a2a/event_adapter.py +156 -64
  10. ksadk/a2a/executor.py +195 -172
  11. ksadk/a2a/routes.py +12 -5
  12. ksadk/a2a/server.py +1 -8
  13. ksadk/a2a/service_env.py +33 -0
  14. ksadk/a2a/space_client.py +43 -349
  15. ksadk/a2a_card.py +101 -0
  16. ksadk/a2ui/core.py +222 -53
  17. ksadk/agui/_agent_helpers.py +243 -0
  18. ksadk/agui/a2ui_projection.py +12 -5
  19. ksadk/agui/agent.py +474 -275
  20. ksadk/agui/config.py +11 -5
  21. ksadk/agui/routes.py +7 -15
  22. ksadk/api/client.py +530 -40
  23. ksadk/api/environment.py +27 -0
  24. ksadk/builders/code_builder.py +352 -53
  25. ksadk/builders/container_builder.py +71 -44
  26. ksadk/builders/framework_requirements.py +2 -1
  27. ksadk/builders/ks3_uploader.py +2 -2
  28. ksadk/builders/managed_runtime_builder.py +60 -28
  29. ksadk/cli/__init__.py +25 -0
  30. ksadk/cli/cmd_a2a.py +18 -31
  31. ksadk/cli/cmd_create.py +13 -26
  32. ksadk/cli/cmd_dashboard.py +15 -1
  33. ksadk/cli/cmd_deploy.py +28 -3
  34. ksadk/cli/cmd_eval.py +443 -0
  35. ksadk/cli/cmd_evalset.py +284 -0
  36. ksadk/cli/cmd_files.py +5 -1
  37. ksadk/cli/cmd_hermes.py +137 -163
  38. ksadk/cli/cmd_invoke.py +28 -38
  39. ksadk/cli/cmd_launch.py +12 -0
  40. ksadk/cli/cmd_managed_runtime.py +95 -0
  41. ksadk/cli/cmd_observe.py +85 -0
  42. ksadk/cli/cmd_openclaw.py +85 -479
  43. ksadk/cli/cmd_replay.py +28 -4
  44. ksadk/cli/cmd_run.py +29 -67
  45. ksadk/cli/cmd_studio.py +201 -0
  46. ksadk/cli/cmd_web.py +15 -6
  47. ksadk/cli/dry_run.py +2 -1
  48. ksadk/cli/env_options.py +83 -0
  49. ksadk/cli/global_options.py +3 -1
  50. ksadk/cli/hermes_env.py +151 -0
  51. ksadk/cli/invoke_payload.py +49 -0
  52. ksadk/cli/network_options.py +7 -5
  53. ksadk/cli/openclaw_env.py +458 -0
  54. ksadk/cli/runtime_bootstrap.py +51 -0
  55. ksadk/cli/ui.py +6 -0
  56. ksadk/cli/workflow_common.py +5 -5
  57. ksadk/codex/__init__.py +3 -3
  58. ksadk/codex/client.py +833 -49
  59. ksadk/codex/phase.py +1 -1
  60. ksadk/codex/runtime.py +720 -127
  61. ksadk/common/kop_client.py +232 -0
  62. ksadk/configs/__init__.py +1 -3
  63. ksadk/configs/env_registry.py +244 -45
  64. ksadk/configs/env_registry_pcm.py +181 -0
  65. ksadk/configs/env_var_spec.py +18 -0
  66. ksadk/configs/global_config.py +6 -1
  67. ksadk/configs/settings.py +5 -108
  68. ksadk/context_engine/__init__.py +98 -0
  69. ksadk/context_engine/assembler.py +176 -0
  70. ksadk/context_engine/baseline.py +419 -0
  71. ksadk/context_engine/cache_observability.py +228 -0
  72. ksadk/context_engine/capabilities.py +443 -0
  73. ksadk/context_engine/contributors.py +344 -0
  74. ksadk/context_engine/hosted_pipeline.py +417 -0
  75. ksadk/context_engine/models.py +117 -0
  76. ksadk/context_engine/planner.py +698 -0
  77. ksadk/context_engine/policies.py +319 -0
  78. ksadk/context_engine/projection.py +31 -0
  79. ksadk/context_engine/shadow_plan.py +266 -0
  80. ksadk/context_engine/tokenizer.py +164 -0
  81. ksadk/conversations/__init__.py +85 -94
  82. ksadk/conversations/context.py +126 -6
  83. ksadk/conversations/message_projection.py +292 -28
  84. ksadk/conversations/model_context.py +49 -3
  85. ksadk/conversations/runtime_compaction.py +207 -5
  86. ksadk/conversations/runtime_input.py +193 -3
  87. ksadk/conversations/runtime_invocation.py +81 -3
  88. ksadk/conversations/runtime_observability.py +157 -4
  89. ksadk/conversations/runtime_payloads.py +42 -0
  90. ksadk/conversations/runtime_preparation.py +280 -2
  91. ksadk/conversations/runtime_resume.py +71 -32
  92. ksadk/conversations/runtime_stream_events.py +94 -9
  93. ksadk/conversations/runtime_streaming.py +175 -22
  94. ksadk/conversations/semantic_summary.py +311 -1
  95. ksadk/conversations/session_lock.py +51 -0
  96. ksadk/deployment/env_forward.py +72 -0
  97. ksadk/deployment/managed_runtime.py +38 -0
  98. ksadk/deployment/manager.py +24 -8
  99. ksadk/deployment/providers/serverless.py +228 -92
  100. ksadk/evaluation/__init__.py +116 -0
  101. ksadk/evaluation/a2a_adapter.py +452 -0
  102. ksadk/evaluation/adapters.py +67 -0
  103. ksadk/evaluation/agent_eval_client.py +395 -0
  104. ksadk/evaluation/cloud_binding.py +80 -0
  105. ksadk/evaluation/cloud_converter.py +194 -0
  106. ksadk/evaluation/cloud_service.py +199 -0
  107. ksadk/evaluation/contracts.py +478 -0
  108. ksadk/evaluation/evalset.py +351 -0
  109. ksadk/evaluation/evaluators.py +627 -0
  110. ksadk/evaluation/evidence.py +258 -0
  111. ksadk/evaluation/executor.py +143 -0
  112. ksadk/evaluation/local_adapter.py +550 -0
  113. ksadk/evaluation/service_env.py +27 -0
  114. ksadk/evaluation/storage.py +68 -0
  115. ksadk/evaluation/studio_build_adapter.py +253 -0
  116. ksadk/evaluation/target.py +82 -0
  117. ksadk/events/__init__.py +13 -13
  118. ksadk/events/_v1_compat/__init__.py +1 -0
  119. ksadk/events/_v1_compat/models.py +299 -0
  120. ksadk/events/_v1_compat/parser.py +247 -0
  121. ksadk/events/_v1_compat/projection.py +753 -0
  122. ksadk/events/adapters/__init__.py +34 -0
  123. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  124. ksadk/events/adapters/_a2a_support.py +275 -0
  125. ksadk/events/adapters/_codex_interactions.py +375 -0
  126. ksadk/events/adapters/_codex_items.py +801 -0
  127. ksadk/events/adapters/_codex_validators.py +311 -0
  128. ksadk/events/adapters/_langgraph_support.py +785 -0
  129. ksadk/events/adapters/a2a.py +841 -0
  130. ksadk/events/adapters/adk.py +503 -0
  131. ksadk/events/adapters/codex.py +717 -0
  132. ksadk/events/adapters/langgraph.py +745 -0
  133. ksadk/events/canonical.py +440 -0
  134. ksadk/events/canonical_replay.py +497 -0
  135. ksadk/events/canonical_store.py +468 -0
  136. ksadk/events/cold_recovery.py +257 -0
  137. ksadk/events/content.py +89 -0
  138. ksadk/events/identity.py +86 -0
  139. ksadk/events/pipeline.py +455 -0
  140. ksadk/events/projections.py +66 -0
  141. ksadk/events/reducer.py +661 -0
  142. ksadk/events/replay.py +3 -36
  143. ksadk/events/runtime_event.py +20 -256
  144. ksadk/events/session_event.py +324 -0
  145. ksadk/events/store.py +3 -316
  146. ksadk/events/v1_compat.py +41 -0
  147. ksadk/harness/__init__.py +3 -2
  148. ksadk/harness/app.py +114 -123
  149. ksadk/harness/config.py +1 -1
  150. ksadk/harness/runtime.py +467 -0
  151. ksadk/hermes_terminal.py +5 -0
  152. ksadk/identity/resolver.py +4 -3
  153. ksadk/interaction/__init__.py +28 -0
  154. ksadk/interaction/contracts.py +136 -0
  155. ksadk/interaction/ledger.py +189 -0
  156. ksadk/interaction/provider.py +122 -0
  157. ksadk/interaction/providers/__init__.py +52 -0
  158. ksadk/interaction/providers/adk.py +40 -0
  159. ksadk/interaction/providers/codex.py +94 -0
  160. ksadk/interaction/providers/langgraph.py +58 -0
  161. ksadk/kernel/__init__.py +117 -0
  162. ksadk/kernel/authorization.py +258 -0
  163. ksadk/kernel/bootstrap.py +1074 -0
  164. ksadk/kernel/contract_fingerprints.py +61 -0
  165. ksadk/kernel/contracts.py +362 -0
  166. ksadk/kernel/control.py +219 -0
  167. ksadk/kernel/errors.py +79 -0
  168. ksadk/kernel/ingress.py +1102 -0
  169. ksadk/kernel/mapping.py +111 -0
  170. ksadk/kernel/memory_store.py +1133 -0
  171. ksadk/kernel/postgres_store.py +1756 -0
  172. ksadk/kernel/recovery.py +452 -0
  173. ksadk/kernel/runtime_identity.py +103 -0
  174. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  175. ksadk/kernel/sqlite_store.py +1410 -0
  176. ksadk/kernel/state.py +145 -0
  177. ksadk/kernel/store.py +232 -0
  178. ksadk/kernel/worker.py +953 -0
  179. ksadk/knowledge_base/client.py +22 -4
  180. ksadk/knowledge_base/service.py +32 -5
  181. ksadk/managed_a2a_card.py +160 -0
  182. ksadk/managed_runtime.py +16 -9
  183. ksadk/memory/__init__.py +61 -0
  184. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  185. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  186. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  187. ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
  188. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  189. ksadk/memory/adk/long_term_memory.py +3 -2
  190. ksadk/memory/coordinator.py +461 -0
  191. ksadk/memory/events.py +202 -0
  192. ksadk/memory/extraction.py +228 -0
  193. ksadk/memory/ltm_backend_factory.py +16 -3
  194. ksadk/memory/models.py +316 -0
  195. ksadk/memory/policy.py +215 -0
  196. ksadk/memory/provider.py +47 -0
  197. ksadk/memory/provider_adapter.py +159 -0
  198. ksadk/memory/provider_resolver.py +71 -0
  199. ksadk/memory/providers/__init__.py +5 -0
  200. ksadk/memory/providers/local_sqlite.py +490 -0
  201. ksadk/memory/resolved_policy.py +145 -0
  202. ksadk/memory/service.py +198 -8
  203. ksadk/memory/tool.py +4 -0
  204. ksadk/model_proxy/bootstrap.py +16 -9
  205. ksadk/model_proxy/config.py +22 -1
  206. ksadk/model_proxy/detect.py +202 -9
  207. ksadk/model_proxy/namespace.py +67 -15
  208. ksadk/model_proxy/server.py +168 -6
  209. ksadk/model_proxy/transform.py +105 -3
  210. ksadk/observability/__init__.py +25 -0
  211. ksadk/observability/session_log.py +420 -0
  212. ksadk/observability/trajectory.py +105 -0
  213. ksadk/prompts/__init__.py +71 -0
  214. ksadk/prompts/compiler.py +215 -0
  215. ksadk/prompts/models.py +82 -0
  216. ksadk/prompts/projection.py +89 -0
  217. ksadk/prompts/resolved.py +137 -0
  218. ksadk/prompts/sources.py +233 -0
  219. ksadk/runners/_langgraph_runner_streams.py +822 -0
  220. ksadk/runners/adk_runner.py +14 -31
  221. ksadk/runners/base_runner.py +44 -3
  222. ksadk/runners/factory.py +3 -3
  223. ksadk/runners/langgraph_runner.py +252 -433
  224. ksadk/runners/utils/__init__.py +0 -10
  225. ksadk/runners/utils/loader.py +16 -0
  226. ksadk/runtime/__init__.py +67 -41
  227. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  228. ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
  229. ksadk/runtime/adapter.py +199 -16
  230. ksadk/runtime/conversation_execution.py +687 -0
  231. ksadk/runtime/executor.py +392 -0
  232. ksadk/runtime/factory.py +342 -0
  233. ksadk/runtime/framework_adapters.py +6 -12
  234. ksadk/runtime/hosted_finalizer.py +268 -0
  235. ksadk/runtime/launch.py +61 -0
  236. ksadk/runtime/preprocessing.py +49 -14
  237. ksadk/runtime/runner_adapter.py +330 -391
  238. ksadk/runtime/runner_loading.py +36 -0
  239. ksadk/runtime/usage.py +33 -0
  240. ksadk/sandbox/backends/e2b.py +36 -3
  241. ksadk/server/__init__.py +39 -4
  242. ksadk/server/app.py +18 -492
  243. ksadk/server/composition.py +36 -2
  244. ksadk/server/factory.py +210 -149
  245. ksadk/server/routes/common.py +15 -46
  246. ksadk/server/routes/control.py +13 -9
  247. ksadk/server/routes/kernel_ingress.py +258 -0
  248. ksadk/server/routes/misc.py +10 -33
  249. ksadk/server/routes/models.py +14 -6
  250. ksadk/server/routes/openai_compat.py +129 -27
  251. ksadk/server/routes/projection.py +84 -16
  252. ksadk/server/routes/run.py +347 -460
  253. ksadk/server/routes/sessions.py +37 -40
  254. ksadk/server/routes/streaming.py +52 -7
  255. ksadk/server/routes/workspace.py +54 -21
  256. ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
  257. ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
  258. ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
  259. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  260. ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
  261. ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
  262. ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
  263. ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
  264. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
  265. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
  266. ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
  267. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
  268. ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
  269. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
  270. ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
  271. ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
  272. ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
  273. ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
  274. ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
  275. ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
  276. ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
  277. ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
  278. ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
  279. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
  280. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
  281. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
  282. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
  283. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
  284. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  285. ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
  286. ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
  287. ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
  288. ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
  289. ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
  290. ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
  291. ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
  292. ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
  293. ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
  294. ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
  295. ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
  296. ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
  297. ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
  298. ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
  299. ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
  300. ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
  301. ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
  302. ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
  303. ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
  304. ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
  305. ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
  306. ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
  307. ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
  308. ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
  309. ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
  310. ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
  311. ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
  312. ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
  313. ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
  314. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
  315. ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
  316. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
  317. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
  318. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
  319. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
  320. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
  321. ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
  322. ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
  323. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
  324. ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
  325. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
  326. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
  327. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
  328. ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
  329. ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
  330. ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
  331. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
  332. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
  333. ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
  334. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
  335. ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
  336. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
  337. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
  338. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
  339. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
  340. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
  341. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
  342. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
  343. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
  344. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
  345. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
  346. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
  347. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
  348. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
  349. ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
  350. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
  351. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
  352. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
  353. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
  354. ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
  355. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
  356. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
  357. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
  358. ksadk/server/static/index.html +2 -2
  359. ksadk/sessions/__init__.py +26 -9
  360. ksadk/sessions/_local_service_sync.py +688 -0
  361. ksadk/sessions/_local_tables.py +21 -0
  362. ksadk/sessions/_postgres_schema.py +305 -0
  363. ksadk/sessions/_postgres_tables.py +15 -0
  364. ksadk/sessions/base.py +80 -1
  365. ksadk/sessions/in_memory.py +53 -2
  366. ksadk/sessions/local_service.py +63 -604
  367. ksadk/sessions/postgres_service.py +58 -148
  368. ksadk/sessions/resilient.py +35 -1
  369. ksadk/skills/runtime/backends/e2b.py +1 -1
  370. ksadk/skills/service_client.py +13 -3
  371. ksadk/studio/__init__.py +5 -0
  372. ksadk/studio/agent_avatar_assets.py +120 -0
  373. ksadk/studio/agent_lifecycle.py +51 -0
  374. ksadk/studio/api.py +1598 -0
  375. ksadk/studio/api_catalog_routes.py +324 -0
  376. ksadk/studio/api_contracts.py +286 -0
  377. ksadk/studio/api_helpers.py +132 -0
  378. ksadk/studio/api_memory_routes.py +82 -0
  379. ksadk/studio/authoring.py +749 -0
  380. ksadk/studio/authoring_coordinator.py +1017 -0
  381. ksadk/studio/builder.py +397 -0
  382. ksadk/studio/capabilities.py +368 -0
  383. ksadk/studio/cloud.py +2099 -0
  384. ksadk/studio/codex_agent_service.py +694 -0
  385. ksadk/studio/codex_authoring.py +469 -0
  386. ksadk/studio/codex_builder.py +437 -0
  387. ksadk/studio/codex_manifest.py +307 -0
  388. ksadk/studio/codex_run.py +395 -0
  389. ksadk/studio/compiler.py +190 -0
  390. ksadk/studio/contracts.py +755 -0
  391. ksadk/studio/errors.py +47 -0
  392. ksadk/studio/event_store.py +214 -0
  393. ksadk/studio/framework_run.py +342 -0
  394. ksadk/studio/hosted_kernel.py +317 -0
  395. ksadk/studio/identifiers.py +33 -0
  396. ksadk/studio/manifest_resolver.py +131 -0
  397. ksadk/studio/mcp_runtime.py +229 -0
  398. ksadk/studio/model_client.py +629 -0
  399. ksadk/studio/model_profile_service.py +214 -0
  400. ksadk/studio/operations.py +275 -0
  401. ksadk/studio/otel_trace.py +1060 -0
  402. ksadk/studio/pagination.py +182 -0
  403. ksadk/studio/pcm_memory.py +85 -0
  404. ksadk/studio/python_tool_inspection.py +183 -0
  405. ksadk/studio/repository.py +327 -0
  406. ksadk/studio/resource_catalog.py +1260 -0
  407. ksadk/studio/run_service.py +1133 -0
  408. ksadk/studio/runtime_catalog.py +74 -0
  409. ksadk/studio/runtime_source.py +365 -0
  410. ksadk/studio/service.py +2249 -0
  411. ksadk/studio/shared_web.py +1004 -0
  412. ksadk/studio/skill_discovery.py +628 -0
  413. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  414. ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
  415. ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
  416. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  417. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  418. ksadk/studio/static/index.html +15 -0
  419. ksadk/studio/templates.py +722 -0
  420. ksadk/studio/validator.py +265 -0
  421. ksadk/studio/workspace.py +135 -0
  422. ksadk/tools/gateway.py +9 -0
  423. ksadk/toolsets/__init__.py +27 -1
  424. ksadk/toolsets/a2a.py +426 -0
  425. ksadk/tracing/__init__.py +19 -21
  426. ksadk/tracing/setup.py +317 -403
  427. ksadk/tracing/span_utils.py +0 -44
  428. ksadk/version.py +1 -1
  429. ksadk_runtime_common/memory_backend/registry.py +56 -56
  430. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  431. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  432. ksadk_runtime_common/workspace_files/router.py +8 -2
  433. agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
  434. ksadk/a2a/card_builder.py +0 -64
  435. ksadk/a2a/client.py +0 -309
  436. ksadk/events/parser.py +0 -191
  437. ksadk/harness/runner.py +0 -211
  438. ksadk/runners/codex_runner.py +0 -132
  439. ksadk/runners/utils/langfuse.py +0 -230
  440. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  441. ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
  442. ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
  443. ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
  444. ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
  445. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
  446. ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
  447. ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
  448. ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
  449. ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
  450. ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
  451. ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
  452. ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
  453. ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
  454. ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
  455. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
  456. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  457. ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
  458. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  459. ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
  460. ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
  461. ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
  462. ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
  463. ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
  464. ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
  465. ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
  466. ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
  467. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
  468. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
  469. ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
  470. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
  471. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  472. ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
  473. ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
  474. ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
  475. ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
  476. ksadk/server/static/assets/katex-DATssjp1.js +0 -265
  477. ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
  478. ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
  479. ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
  480. ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
  481. ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
  482. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
  483. ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
  484. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
  485. ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
  486. ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
  487. ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
  488. ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
  489. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
  490. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
  491. ksadk/tracing/exporters/langfuse_exporter.py +0 -460
  492. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
  493. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
  494. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1133 @@
1
+ """Studio Run persistence on top of the canonical RuntimeExecutor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from collections.abc import Callable, Mapping
9
+ from dataclasses import asdict, dataclass, field
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+ from uuid import uuid4
13
+
14
+ from ksadk.agui.a2ui_projection import project_a2ui_operations
15
+ from ksadk.events.canonical import (
16
+ ContinuationCreated,
17
+ ContinuationResumed,
18
+ ErrorInfo,
19
+ InteractionRequested,
20
+ InteractionResolved,
21
+ ItemCompleted,
22
+ ItemFailed,
23
+ ItemStarted,
24
+ ItemUpdated,
25
+ RunCanceled,
26
+ RunCompleted,
27
+ RunFailed,
28
+ RunInterrupted,
29
+ RunProgress,
30
+ RunStarted,
31
+ RuntimeEvent,
32
+ SourceRef,
33
+ UsageReported,
34
+ dump_runtime_event,
35
+ )
36
+ from ksadk.events.content import (
37
+ ContentSnapshot,
38
+ DataContent,
39
+ TextContent,
40
+ ToolCallContent,
41
+ ToolResultContent,
42
+ )
43
+ from ksadk.events.store import RuntimeEventStore
44
+ from ksadk.runtime import (
45
+ CONVERSATION_PREPROCESSING_METADATA_KEY,
46
+ PauseResult,
47
+ ResumePayload,
48
+ ResumeTarget,
49
+ RuntimeExecutor,
50
+ RuntimeLaunchContext,
51
+ StartRequest,
52
+ )
53
+ from ksadk.sessions.base import BaseSessionService
54
+ from ksadk.sessions.local_service import LocalSessionService
55
+ from ksadk.studio.contracts import RunEvent, RunRecord, RunStatus, Usage
56
+ from ksadk.studio.errors import StudioError
57
+ from ksadk.studio.event_store import RunEventStore
58
+ from ksadk.studio.workspace import Workspace
59
+
60
+ _CANCEL_TIMEOUT_SECONDS = 2.0
61
+
62
+
63
+ logger = logging.getLogger(__name__)
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class StudioRunSpec:
68
+ """Product metadata plus the core launch context for one Studio run."""
69
+
70
+ launch_context: RuntimeLaunchContext
71
+ build_id: str
72
+ agent_id: str
73
+ model: str | None = None
74
+ request_config: Mapping[str, Any] = field(default_factory=dict)
75
+ manifest_sha256: str = ""
76
+
77
+
78
+ class StudioRunService:
79
+ """Persist Studio state without selecting or wrapping a RuntimeAdapter."""
80
+
81
+ def __init__(
82
+ self,
83
+ workspace: Workspace,
84
+ executor: RuntimeExecutor,
85
+ *,
86
+ event_store: RunEventStore | None = None,
87
+ session_service: BaseSessionService | None = None,
88
+ runtime_events: RuntimeEventStore | None = None,
89
+ ) -> None:
90
+ self.workspace = workspace
91
+ self.executor = executor
92
+ self.event_store = event_store or RunEventStore(workspace)
93
+ self.session_service = session_service or LocalSessionService(
94
+ project_dir=str(workspace.root)
95
+ )
96
+ self.runtime_events = runtime_events or RuntimeEventStore(self.session_service)
97
+ self._active_handles: dict[str, Any] = {}
98
+ self._cancel_flags: dict[str, bool] = {}
99
+ self._control_queues: dict[str, asyncio.Queue[tuple[str, ResumePayload | None]]] = {}
100
+ self._waiting_modes: dict[str, str] = {}
101
+
102
+ async def recover_interrupted(self) -> None:
103
+ """Settle local runs left active across a Studio restart.
104
+
105
+ An in-process handle cannot be resumed safely. Keep its durable event
106
+ history and record an explicit interruption; lease-backed hosted
107
+ recovery remains owned by the Kernel composition root.
108
+ """
109
+ active = {RunStatus.RUNNING, RunStatus.WAITING_INPUT}
110
+ for record in self.event_store.list_runs():
111
+ if record.status not in active:
112
+ continue
113
+ record.status = RunStatus.INTERRUPTED
114
+ record.completed_at = datetime.now(timezone.utc)
115
+ record.error = {
116
+ "code": "LOCAL_STUDIO_RESTARTED",
117
+ "message": "本地 Studio 重启,运行未在本地恢复。",
118
+ }
119
+ self.event_store.save(record)
120
+ self.event_store.append(
121
+ record.id,
122
+ "run.interrupted",
123
+ {"reason": "local_studio_restarted", "recoverable": False},
124
+ )
125
+
126
+ async def events(self, run_id: str, *, after: int = 0) -> list[RunEvent]:
127
+ """Read the durable Studio event timeline for API/SSE replay."""
128
+ return self.event_store.events(run_id, after=after)
129
+
130
+ async def run(
131
+ self,
132
+ spec: StudioRunSpec,
133
+ user_input: str,
134
+ *,
135
+ runtime_input: Any = None,
136
+ session_id: str | None = None,
137
+ on_event: Callable[[RunEvent], None] | None = None,
138
+ ) -> RunRecord:
139
+ run_id = f"run_{uuid4().hex}"
140
+ session = session_id or f"ses_{uuid4().hex}"
141
+ runtime_type = spec.launch_context.runtime_type.strip().lower()
142
+ record = RunRecord(
143
+ id=run_id,
144
+ build_id=spec.build_id,
145
+ agent_id=spec.agent_id,
146
+ session_id=session,
147
+ trace_id=uuid4().hex,
148
+ manifest_sha256=spec.manifest_sha256,
149
+ runtime_type=runtime_type,
150
+ model=str(spec.model or ""),
151
+ collaboration_mode=str(spec.request_config.get("collaboration_mode") or ""),
152
+ goal_objective=str(spec.request_config.get("goal_objective") or ""),
153
+ input=user_input,
154
+ )
155
+ await self.session_service.create_session(spec.agent_id, "local-user", session)
156
+ self.event_store.create(record)
157
+ prepared_turn = await self._capture_pcm_evidence(record, spec, user_input)
158
+ created = self.event_store.append(
159
+ record.id,
160
+ "run.created",
161
+ {
162
+ "runId": record.id,
163
+ "buildId": spec.build_id,
164
+ "sessionId": session,
165
+ "traceId": record.trace_id,
166
+ "runtimeType": runtime_type,
167
+ "manifestSha256": spec.manifest_sha256,
168
+ "model": record.model,
169
+ "collaborationMode": record.collaboration_mode,
170
+ "goalObjective": record.goal_objective,
171
+ },
172
+ )
173
+ if on_event is not None:
174
+ on_event(created)
175
+
176
+ from ksadk.kernel.ingress import kernel_route_active
177
+
178
+ if kernel_route_active():
179
+ return await self._kernel_run(
180
+ spec, user_input, record=record, on_event=on_event
181
+ )
182
+
183
+ started = time.monotonic()
184
+ record.status = RunStatus.RUNNING
185
+ record.started_at = datetime.now(timezone.utc)
186
+ self.event_store.save(record)
187
+
188
+ async def persist(runtime_event: RuntimeEvent) -> RunEvent:
189
+ persisted = await self.runtime_events.append_one(record.session_id, runtime_event)
190
+ event_type, data = project_runtime_event(persisted)
191
+ stored = self.event_store.append(record.id, event_type, data)
192
+ if on_event is not None:
193
+ on_event(stored)
194
+ return stored
195
+
196
+ handle = None
197
+ final_text = ""
198
+ streamed_final = ""
199
+ runtime_duration_ms: int | None = None
200
+ item_phases: dict[tuple[str, str], str | None] = {}
201
+ control_queue: asyncio.Queue[tuple[str, ResumePayload | None]] = asyncio.Queue()
202
+ self._control_queues[run_id] = control_queue
203
+ try:
204
+ tool_approval_mode = str(spec.request_config.get("tool_approval_mode") or "")
205
+ conversation_request: dict[str, Any] = {
206
+ "messages": self._conversation_messages(
207
+ spec.agent_id,
208
+ session,
209
+ user_input,
210
+ )
211
+ }
212
+ if tool_approval_mode:
213
+ conversation_request["request_metadata"] = {
214
+ "tool_approval_mode": tool_approval_mode,
215
+ }
216
+ if prepared_turn is not None:
217
+ conversation_request["prepared_turn"] = asdict(prepared_turn)
218
+ request = StartRequest(
219
+ input=runtime_input if runtime_input is not None else user_input,
220
+ user_id="local-user",
221
+ session_id=session,
222
+ agent_id=spec.agent_id,
223
+ model=spec.model,
224
+ config=dict(spec.request_config),
225
+ metadata={
226
+ "invocation_id": run_id,
227
+ CONVERSATION_PREPROCESSING_METADATA_KEY: conversation_request,
228
+ **self._native_session_metadata(spec.agent_id, session, runtime_type),
229
+ },
230
+ )
231
+ handle = await self.executor.start(spec.launch_context, request)
232
+ record.runtime_handle = handle.model_dump(mode="json")
233
+ self.event_store.save(record)
234
+ self._active_handles[run_id] = handle
235
+ while True:
236
+ terminal_seen = False
237
+ should_resume = False
238
+ async for event in self.executor.stream(handle):
239
+ await persist(event)
240
+ if self._cancel_flags.get(run_id):
241
+ raise asyncio.CancelledError()
242
+ if isinstance(event, ItemStarted) and event.item_kind == "message":
243
+ item_phases[(event.scope_id, event.item_id)] = event.phase
244
+ elif (
245
+ isinstance(event, ItemUpdated)
246
+ and event.item_kind == "message"
247
+ and item_phases.get(
248
+ (event.scope_id, event.item_id), "final_answer"
249
+ )
250
+ == "final_answer"
251
+ ):
252
+ text = event.update.text if isinstance(event.update, TextContent) else ""
253
+ if event.op == "replace":
254
+ streamed_final = text
255
+ else:
256
+ streamed_final += text
257
+ elif (
258
+ isinstance(event, ItemCompleted)
259
+ and event.item_kind == "message"
260
+ and item_phases.get(
261
+ (event.scope_id, event.item_id), "final_answer"
262
+ )
263
+ == "final_answer"
264
+ ):
265
+ for part in event.snapshot.parts:
266
+ if isinstance(part, TextContent):
267
+ final_text = part.text
268
+ break
269
+ elif isinstance(event, UsageReported):
270
+ record.usage = Usage(
271
+ input_tokens=event.input_tokens,
272
+ output_tokens=event.output_tokens,
273
+ total_tokens=event.total_tokens,
274
+ cached_input_tokens=event.cached_tokens,
275
+ reasoning_output_tokens=event.reasoning_tokens,
276
+ reported=True,
277
+ source=str(event.source.framework or runtime_type),
278
+ )
279
+ elif isinstance(event, InteractionRequested):
280
+ record.status = RunStatus.WAITING_INPUT
281
+ self._waiting_modes[run_id] = "live"
282
+ self.event_store.save(record)
283
+ if event.interaction_kind == "approval":
284
+ self._persist_approval_surface(record, event, on_event=on_event)
285
+ elif isinstance(event, InteractionResolved):
286
+ record.status = RunStatus.RUNNING
287
+ self._waiting_modes.pop(run_id, None)
288
+ self.event_store.save(record)
289
+ elif isinstance(event, RunFailed):
290
+ terminal_seen = True
291
+ record.status = RunStatus.FAILED
292
+ record.error = {
293
+ "code": "RUNTIME_RUN_FAILED",
294
+ "message": str(event.error.message or "Runtime 运行失败"),
295
+ }
296
+ elif isinstance(event, RunCanceled):
297
+ terminal_seen = True
298
+ record.status = RunStatus.CANCELLED
299
+ record.error = {
300
+ "code": "RUN_CANCELLED",
301
+ "message": str(event.reason or "运行已取消"),
302
+ }
303
+ elif isinstance(event, RunInterrupted):
304
+ if event.reason == "user_pause":
305
+ record.status = RunStatus.PAUSED
306
+ should_resume = True
307
+ elif record.status == RunStatus.WAITING_INPUT:
308
+ self._waiting_modes[run_id] = "resume"
309
+ should_resume = True
310
+ else:
311
+ terminal_seen = True
312
+ record.status = RunStatus.INTERRUPTED
313
+ record.error = {
314
+ "code": "RUN_INTERRUPTED",
315
+ "message": event.reason or "运行已中断",
316
+ }
317
+ self.event_store.save(record)
318
+ elif isinstance(event, RunCompleted):
319
+ terminal_seen = True
320
+ record.status = RunStatus.COMPLETED
321
+ raw_duration = event.source.metadata.get("duration_ms")
322
+ if raw_duration is None:
323
+ metrics = event.source.metadata.get("metrics")
324
+ if isinstance(metrics, dict):
325
+ raw_duration = metrics.get("duration_ms")
326
+ if raw_duration is not None:
327
+ runtime_duration_ms = max(0, int(raw_duration))
328
+ if terminal_seen:
329
+ break
330
+ if not should_resume:
331
+ record.status = RunStatus.COMPLETED
332
+ break
333
+
334
+ command, resume_payload = await control_queue.get()
335
+ if command == "cancel" or self._cancel_flags.get(run_id):
336
+ raise asyncio.CancelledError()
337
+ if command != "resume":
338
+ continue
339
+ native_thread_id = handle.native_ref.get("thread_id")
340
+ target = (
341
+ ResumeTarget(kind="thread_id", id=str(native_thread_id))
342
+ if native_thread_id
343
+ else ResumeTarget(kind="invocation_id", id=handle.run_id)
344
+ )
345
+ handle = await self.executor.resume(
346
+ handle,
347
+ target,
348
+ resume_payload,
349
+ )
350
+ self._active_handles[run_id] = handle
351
+ record.runtime_handle = handle.model_dump(mode="json")
352
+ record.status = RunStatus.RUNNING
353
+ record.error = None
354
+ self._waiting_modes.pop(run_id, None)
355
+ resumed = self.event_store.append(
356
+ run_id,
357
+ "run.resumed",
358
+ {"runId": run_id, "runtimeHandle": record.runtime_handle},
359
+ )
360
+ if on_event is not None:
361
+ on_event(resumed)
362
+ self.event_store.save(record)
363
+ record.output = final_text or streamed_final
364
+ except asyncio.CancelledError:
365
+ cancel_result = "task_cancelled"
366
+ if handle is not None and self.executor.is_attached(handle):
367
+ try:
368
+ result = await asyncio.wait_for(
369
+ asyncio.shield(self.executor.cancel(handle)),
370
+ timeout=_CANCEL_TIMEOUT_SECONDS,
371
+ )
372
+ cancel_result = result.value
373
+ except TimeoutError:
374
+ cancel_result = "cancel_timed_out"
375
+ except Exception: # best effort; close still owns resource cleanup
376
+ cancel_result = "cancel_failed"
377
+ record.status = RunStatus.CANCELLED
378
+ record.error = {"code": "RUN_CANCELLED", "message": "运行已取消"}
379
+ cancelled_run_id = handle.run_id if handle is not None else run_id
380
+ cancelled = RunCanceled(
381
+ schema_version=2,
382
+ event_id=f"evt_cancel_{uuid4().hex}",
383
+ seq=len(self.event_store.events(run_id)) + 1,
384
+ timestamp=time.time(),
385
+ run_id=cancelled_run_id,
386
+ scope_id=cancelled_run_id,
387
+ source=SourceRef(
388
+ framework="ksadk",
389
+ metadata={"cancel_result": cancel_result},
390
+ ),
391
+ status="canceled",
392
+ reason="cancelled",
393
+ )
394
+ await persist(cancelled)
395
+ raise
396
+ except Exception as exc: # noqa: BLE001
397
+ record.status = RunStatus.FAILED
398
+ record.error = {"code": "RUNTIME_RUN_FAILED", "message": str(exc)}
399
+ failed_run_id = handle.run_id if handle is not None else run_id
400
+ failure = RunFailed(
401
+ schema_version=2,
402
+ event_id=f"evt_fail_{uuid4().hex}",
403
+ seq=len(self.event_store.events(run_id)) + 1,
404
+ timestamp=time.time(),
405
+ run_id=failed_run_id,
406
+ scope_id=failed_run_id,
407
+ source=SourceRef(framework="ksadk"),
408
+ status="failed",
409
+ error=ErrorInfo(
410
+ code="RUNTIME_RUN_FAILED",
411
+ message=str(exc),
412
+ source="ksadk",
413
+ scope_id=failed_run_id,
414
+ ),
415
+ )
416
+ await persist(failure)
417
+ finally:
418
+ if handle is not None and self.executor.is_attached(handle):
419
+ try:
420
+ await asyncio.shield(self.executor.close(handle))
421
+ except asyncio.CancelledError:
422
+ pass
423
+ except Exception: # cleanup is best effort; run state is already durable
424
+ pass
425
+ self._active_handles.pop(run_id, None)
426
+ self._cancel_flags.pop(run_id, None)
427
+ self._control_queues.pop(run_id, None)
428
+ self._waiting_modes.pop(run_id, None)
429
+ record.completed_at = datetime.now(timezone.utc)
430
+ if runtime_duration_ms is not None:
431
+ record.duration_ms = runtime_duration_ms
432
+ record.duration_source = "runtime"
433
+ else:
434
+ record.duration_ms = int((time.monotonic() - started) * 1000)
435
+ record.duration_source = "studio"
436
+ self.event_store.save(record)
437
+ await self._sync_trace(record)
438
+ return record
439
+
440
+ async def _sync_trace(self, record: RunRecord) -> None:
441
+ self.event_store.trace_store.sync(record, await self.events(record.id))
442
+
443
+
444
+ async def _kernel_run(
445
+ self,
446
+ spec: StudioRunSpec,
447
+ user_input: str,
448
+ *,
449
+ record: RunRecord,
450
+ on_event: Callable[[RunEvent], None] | None,
451
+ ) -> RunRecord:
452
+ """kernel 路径(灰度 opt-in):Studio run -> AgentControlCommand -> receipt。
453
+
454
+ mutation 只走 kernel.submit;Studio RunEvent shape 保留,cursor 源自
455
+ 同一 Session seq(SessionEventSubscription.after_seq)。
456
+ """
457
+ from ksadk.kernel import ingress as _kernel_ingress
458
+
459
+ started = time.monotonic()
460
+ record.status = RunStatus.RUNNING
461
+ record.started_at = datetime.now(timezone.utc)
462
+ self.event_store.save(record)
463
+ try:
464
+ trusted = _kernel_ingress.trusted_context(
465
+ source_kind="studio",
466
+ source_ref=record.id,
467
+ session_id=record.session_id,
468
+ operations=("enqueue",),
469
+ launch_context=spec.launch_context,
470
+ )
471
+ idempotency_key = str(
472
+ (spec.request_config or {}).get("idempotency_key") or record.id
473
+ )
474
+ command = _kernel_ingress.map_studio_request(
475
+ session_id=record.session_id,
476
+ idempotency_key=idempotency_key,
477
+ content=user_input,
478
+ run_id=record.id,
479
+ trusted=trusted,
480
+ )
481
+ receipt = await _kernel_ingress.submit_command(
482
+ command, permit=trusted.permit
483
+ )
484
+ if receipt.status not in ("accepted", "duplicate"):
485
+ record.status = RunStatus.FAILED
486
+ record.error = {
487
+ "code": "kernel_command_rejected",
488
+ "message": f"agent kernel rejected command: {receipt.status}",
489
+ }
490
+ self.event_store.save(record)
491
+ return record
492
+ output_text = ""
493
+ async for _seq, projected in _kernel_ingress.subscribe_projected(
494
+ record.session_id,
495
+ trusted=trusted,
496
+ after_seq=int(receipt.accepted_seq or 0),
497
+ projector=_studio_envelope_projection,
498
+ ):
499
+ if projected is None:
500
+ continue
501
+ event_type, data = projected
502
+ if event_type == "message.delta":
503
+ output_text += str(data.get("delta") or "")
504
+ elif event_type == "run.completed":
505
+ output_text = str(data.get("output_text") or output_text)
506
+ stored = self.event_store.append(record.id, event_type, data)
507
+ if on_event is not None:
508
+ on_event(stored)
509
+ record.output = output_text
510
+ record.status = RunStatus.COMPLETED
511
+ record.completed_at = datetime.now(timezone.utc)
512
+ record.duration_ms = int((time.monotonic() - started) * 1000)
513
+ self.event_store.save(record)
514
+ await self._sync_trace(record)
515
+ return record
516
+ except Exception as exc: # noqa: BLE001
517
+ logger.exception("Studio kernel ingress failed for run %s", record.id)
518
+ record.status = RunStatus.FAILED
519
+ record.error = {
520
+ "code": "kernel_ingress_failed",
521
+ "message": str(exc),
522
+ }
523
+ record.completed_at = datetime.now(timezone.utc)
524
+ record.duration_ms = int((time.monotonic() - started) * 1000)
525
+ self.event_store.save(record)
526
+ await self._sync_trace(record)
527
+ return record
528
+
529
+ async def cancel_run(self, run_id: str) -> dict[str, str]:
530
+ """Request cancellation; the flag and executor perform the actual stop."""
531
+ self._cancel_flags[run_id] = True
532
+ queue = self._control_queues.get(run_id)
533
+ if queue is not None:
534
+ queue.put_nowait(("cancel", None))
535
+ handle = self._active_handles.get(run_id)
536
+ if handle is not None and self.executor.is_attached(handle):
537
+ try:
538
+ await asyncio.wait_for(
539
+ asyncio.shield(self.executor.cancel(handle)),
540
+ timeout=_CANCEL_TIMEOUT_SECONDS,
541
+ )
542
+ except TimeoutError:
543
+ # The cancellation flag wakes the Studio state machine; final
544
+ # transport teardown remains close()'s responsibility.
545
+ pass
546
+ except Exception:
547
+ pass
548
+ return {"runId": run_id, "status": "cancelling"}
549
+
550
+ async def pause_run(self, run_id: str) -> dict[str, str]:
551
+ record = self.event_store.get(run_id)
552
+ if record.status == RunStatus.WAITING_INPUT:
553
+ raise StudioError(
554
+ "RUN_WAITING_INPUT",
555
+ "当前运行正在等待交互输入,请先处理卡片",
556
+ status_code=409,
557
+ )
558
+ if record.status != RunStatus.RUNNING:
559
+ raise StudioError("RUN_NOT_RUNNING", "只有运行中的 Run 可以暂停", status_code=409)
560
+ handle = self._active_handles.get(run_id)
561
+ if handle is None or not self.executor.is_attached(handle):
562
+ raise StudioError("RUN_NOT_ATTACHED", "运行句柄已不在当前进程", status_code=409)
563
+ result = await self.executor.pause(handle)
564
+ if result is PauseResult.NOT_SUPPORTED:
565
+ raise StudioError(
566
+ "RUN_PAUSE_UNSUPPORTED",
567
+ "当前 Runtime 不支持可恢复暂停",
568
+ status_code=409,
569
+ )
570
+ if result is not PauseResult.PAUSED_ACTIVE_TURN:
571
+ raise StudioError("RUN_PAUSE_FAILED", "暂停运行失败", status_code=409)
572
+ return {"runId": run_id, "status": "pausing"}
573
+
574
+ async def resume_run(self, run_id: str) -> dict[str, str]:
575
+ record = self.event_store.get(run_id)
576
+ if record.status != RunStatus.PAUSED:
577
+ raise StudioError("RUN_NOT_PAUSED", "只有已暂停的 Run 可以继续", status_code=409)
578
+ queue = self._control_queues.get(run_id)
579
+ if queue is None:
580
+ raise StudioError(
581
+ "RUN_RESUME_UNAVAILABLE",
582
+ "Studio 进程已重启,当前暂停点无法恢复",
583
+ status_code=409,
584
+ )
585
+ queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行")))
586
+ return {"runId": run_id, "status": "resuming"}
587
+
588
+ async def submit_interaction(
589
+ self,
590
+ run_id: str,
591
+ interaction_id: str,
592
+ *,
593
+ name: str,
594
+ data: dict[str, Any] | None = None,
595
+ ) -> dict[str, Any]:
596
+ record = self.event_store.get(run_id)
597
+ if record.status != RunStatus.WAITING_INPUT:
598
+ raise StudioError(
599
+ "INTERACTION_NOT_PENDING",
600
+ "该 Run 当前没有等待中的交互",
601
+ status_code=409,
602
+ )
603
+ prior = [
604
+ event
605
+ for event in self.event_store.events(run_id)
606
+ if event.type == "a2ui.action"
607
+ and str(event.data.get("interactionId") or event.data.get("interaction_id") or "")
608
+ == interaction_id
609
+ ]
610
+ if prior:
611
+ return {"runId": run_id, "interactionId": interaction_id, "status": "resolved"}
612
+
613
+ interaction = next(
614
+ (
615
+ event
616
+ for event in reversed(self.event_store.events(run_id))
617
+ if event.type == "a2ui.interaction"
618
+ and str(event.data.get("interactionId") or event.data.get("interaction_id") or "")
619
+ == interaction_id
620
+ ),
621
+ None,
622
+ )
623
+ if interaction is None:
624
+ raise StudioError("INTERACTION_NOT_FOUND", "交互请求不存在", status_code=404)
625
+ kind = str(interaction.data.get("kind") or "form")
626
+ payload_data = {"decision": name, **dict(data or {})}
627
+ payload = ResumePayload(
628
+ kind="approval_decision" if kind == "approval" else "hitl_answer",
629
+ call_id=interaction_id,
630
+ data=payload_data,
631
+ )
632
+ mode = self._waiting_modes.get(run_id)
633
+ handle = self._active_handles.get(run_id)
634
+ if mode == "live":
635
+ if handle is None or not self.executor.is_attached(handle):
636
+ raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
637
+ try:
638
+ await self.executor.submit(handle, payload)
639
+ except (RuntimeError, ValueError) as exc:
640
+ raise StudioError(
641
+ "INTERACTION_SUBMIT_FAILED",
642
+ str(exc),
643
+ status_code=409,
644
+ ) from exc
645
+ elif mode == "resume":
646
+ queue = self._control_queues.get(run_id)
647
+ if queue is None:
648
+ raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
649
+ queue.put_nowait(("resume", payload))
650
+ else:
651
+ raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
652
+
653
+ resolved = self.event_store.append(
654
+ run_id,
655
+ "approval.resolved" if kind == "approval" else "interaction.resolved",
656
+ {
657
+ "runId": run_id,
658
+ "interactionId": interaction_id,
659
+ "callId": interaction_id,
660
+ "name": name,
661
+ "data": dict(data or {}),
662
+ },
663
+ )
664
+ action = self.event_store.append(
665
+ run_id,
666
+ "a2ui.action",
667
+ {
668
+ "runId": run_id,
669
+ "surfaceId": str(
670
+ interaction.data.get("surfaceId") or interaction.data.get("surface_id") or ""
671
+ ),
672
+ "interactionId": interaction_id,
673
+ "actionId": f"action-{interaction_id}",
674
+ "name": name,
675
+ "data": dict(data or {}),
676
+ },
677
+ )
678
+ record.status = RunStatus.RUNNING
679
+ self.event_store.save(record)
680
+ self._waiting_modes.pop(run_id, None)
681
+ return {
682
+ "runId": run_id,
683
+ "interactionId": interaction_id,
684
+ "status": "resolved",
685
+ "eventId": action.id,
686
+ "resolutionEventId": resolved.id,
687
+ }
688
+
689
+ async def _capture_pcm_evidence(
690
+ self,
691
+ record: RunRecord,
692
+ spec: StudioRunSpec,
693
+ user_input: str,
694
+ ) -> Any | None:
695
+ """Prepare once and persist the exact prompt/context input sent to Runtime."""
696
+
697
+ try:
698
+ from ksadk.conversations.runtime_preparation import build_run_input
699
+ from ksadk.sessions.in_memory import InMemorySessionService
700
+
701
+ temporary_sessions = InMemorySessionService()
702
+ await temporary_sessions.create_session(
703
+ agent_id=spec.agent_id,
704
+ user_id="local-user",
705
+ session_id=record.session_id,
706
+ )
707
+ config = dict(spec.request_config or {})
708
+ prepared = await build_run_input(
709
+ agent_id=spec.agent_id,
710
+ user_id="local-user",
711
+ session_id=record.session_id,
712
+ messages=self._conversation_messages(
713
+ spec.agent_id,
714
+ record.session_id,
715
+ user_input,
716
+ ),
717
+ model=spec.model,
718
+ instructions=str(config.get("instructions") or ""),
719
+ agent_system=str(config.get("agent_system") or ""),
720
+ agent_task=str(config.get("agent_task") or ""),
721
+ prompt_integration_mode=str(config.get("prompt_integration_mode") or ""),
722
+ context_engine_rollout=str(config.get("context_engine_rollout") or "") or None,
723
+ memory_recall_enabled=config.get("memory_recall_enabled"),
724
+ memory_write_rollout=str(config.get("memory_write_rollout") or "") or None,
725
+ memory_enabled=config.get("memory_enabled"),
726
+ memory_write_mode=str(config.get("memory_write_mode") or "candidate"),
727
+ flush_before_compaction=bool(config.get("flush_before_compaction", True)),
728
+ provider_ref=str(config.get("provider_ref") or "local-default"),
729
+ runtime_type=spec.launch_context.runtime_type,
730
+ deployment_mode=spec.launch_context.deployment_mode,
731
+ invocation_id=record.id,
732
+ session_service_provider=lambda: temporary_sessions,
733
+ agent_max_input_tokens=config.get("max_input_tokens"),
734
+ agent_reserve_output_tokens=config.get("reserve_output_tokens"),
735
+ )
736
+ record.context_plan = prepared.context_plan
737
+ compiled = prepared.compiled_prompt
738
+ shadow = prepared.shadow_context_plan or {}
739
+ record.prompt_evidence = {
740
+ "contentHash": (compiled or {}).get("prompt_content_hash"),
741
+ "stablePrefixHash": (compiled or {}).get("prompt_stable_prefix_hash"),
742
+ "sectionHashes": (compiled or {}).get("prompt_section_hashes", {}),
743
+ "tokensBySection": (compiled or {}).get("prompt_tokens_by_section", {}),
744
+ "estimatedTokens": (compiled or {}).get("prompt_estimated_tokens"),
745
+ "sectionCount": (compiled or {}).get("prompt_section_count"),
746
+ "integrationMode": shadow.get("integration_mode")
747
+ or config.get("prompt_integration_mode")
748
+ or "native",
749
+ "accountingAccuracy": shadow.get("accounting_accuracy") or "opaque",
750
+ "promptOwner": shadow.get("prompt_owner") or "runtime",
751
+ "runtimeType": shadow.get("runtime_type") or spec.launch_context.runtime_type,
752
+ "deploymentMode": shadow.get("deployment_mode")
753
+ or spec.launch_context.deployment_mode,
754
+ "capabilityHash": shadow.get("capability_hash"),
755
+ "tokensByKind": shadow.get("tokens_by_kind", {}),
756
+ "plannedInputTokens": shadow.get("planned_input_tokens"),
757
+ }
758
+ record.working_state = prepared.working_state
759
+ for event in getattr(prepared, "memory_recall_events", []):
760
+ self.event_store.append(
761
+ record.id,
762
+ event.get("type", "memory.recall.event"),
763
+ event,
764
+ )
765
+ self.event_store.save(record)
766
+ return prepared
767
+ except Exception: # noqa: BLE001 - evidence collection is best effort
768
+ logger.exception("Studio PCM evidence capture failed for run %s", record.id)
769
+ return None
770
+
771
+ def _persist_approval_surface(
772
+ self,
773
+ record: RunRecord,
774
+ event: InteractionRequested,
775
+ *,
776
+ on_event: Callable[[RunEvent], None] | None,
777
+ ) -> None:
778
+ approval_id = event.interaction_id
779
+ if not approval_id:
780
+ return
781
+ surface_id = f"approval-{approval_id}"
782
+ detail_value = event.request.detail
783
+ detail = detail_value if isinstance(detail_value, dict) else {}
784
+ command = str(detail.get("command") or detail.get("reason") or "")
785
+ kind = str(event.request.kind or "tool")
786
+ components = [
787
+ {
788
+ "id": "root",
789
+ "component": "Card",
790
+ "title": "需要你的确认",
791
+ "children": ["approval"],
792
+ },
793
+ {
794
+ "id": "approval",
795
+ "component": "ApprovalBar",
796
+ "tool_name": kind,
797
+ "summary": command or f"Agent 请求执行 {kind} 操作",
798
+ "approve_label": "批准",
799
+ "deny_label": "拒绝",
800
+ },
801
+ ]
802
+ begin = self.event_store.append(
803
+ record.id,
804
+ "a2ui.surface.begin",
805
+ {
806
+ "runId": record.id,
807
+ "surfaceId": surface_id,
808
+ "a2uiOperations": [
809
+ {
810
+ "version": "v0.9",
811
+ "createSurface": {
812
+ "surfaceId": surface_id,
813
+ "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json",
814
+ },
815
+ },
816
+ {
817
+ "version": "v0.9",
818
+ "updateComponents": {
819
+ "surfaceId": surface_id,
820
+ "components": components,
821
+ },
822
+ },
823
+ ],
824
+ },
825
+ )
826
+ interaction = self.event_store.append(
827
+ record.id,
828
+ "a2ui.interaction",
829
+ {
830
+ "runId": record.id,
831
+ "surfaceId": surface_id,
832
+ "interactionId": approval_id,
833
+ "kind": "approval",
834
+ "inputSchema": {
835
+ "type": "object",
836
+ "properties": {
837
+ "decision": {
838
+ "type": "string",
839
+ "enum": ["approve", "approve_session", "deny"],
840
+ }
841
+ },
842
+ "required": ["decision"],
843
+ },
844
+ },
845
+ )
846
+ if on_event is not None:
847
+ on_event(begin)
848
+ on_event(interaction)
849
+
850
+ def _conversation_messages(
851
+ self,
852
+ agent_id: str,
853
+ session_id: str,
854
+ user_input: str,
855
+ ) -> list[dict[str, str]]:
856
+ messages: list[dict[str, str]] = []
857
+ for previous in self.event_store.list_runs(session_id=session_id):
858
+ if previous.agent_id != agent_id or previous.status != RunStatus.COMPLETED:
859
+ continue
860
+ messages.append({"role": "user", "content": previous.input})
861
+ messages.append({"role": "assistant", "content": previous.output})
862
+ messages.append({"role": "user", "content": user_input})
863
+ return messages
864
+
865
+ def _native_session_metadata(
866
+ self,
867
+ agent_id: str,
868
+ session_id: str,
869
+ runtime_type: str,
870
+ ) -> dict[str, str]:
871
+ if runtime_type != "codex":
872
+ return {}
873
+ for previous in self.event_store.list_runs(session_id=session_id):
874
+ if previous.agent_id != agent_id or previous.status != RunStatus.COMPLETED:
875
+ continue
876
+ native_ref = previous.runtime_handle.get("native_ref")
877
+ native_ref = native_ref if isinstance(native_ref, dict) else {}
878
+ thread_id = str(native_ref.get("thread_id") or "")
879
+ if thread_id:
880
+ return {"thread_id": thread_id}
881
+ return {}
882
+
883
+
884
+ def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
885
+ """Project the canonical RuntimeEvent into Studio's persisted event view.
886
+
887
+ 公开承诺字段(契约声明见 ``ksadk/events/projections.py``,执行形态为
888
+ ``tests/protocol/test_cross_projection_golden.py``):
889
+ - 所有事件 payload 必含 ``runId``/``scopeId``;
890
+ - ``message.*``/``thinking.*`` 含 ``itemId``(delta 另含 ``partId``);
891
+ - ``tool.*``/``command.*``/``approval.*``/``a2ui.*`` 含 ``itemId``;
892
+ - ``a2ui.surface.*`` 含 ``surfaceId`` 与 ``a2uiOperations`` 列表。
893
+
894
+ 内部不保证字段:除上述外的 payload 附加键、事件类型枚举的完备性
895
+ (新增 canonical 事件类型在未适配前可能整条丢弃)。
896
+ """
897
+
898
+ if isinstance(event, RunStarted):
899
+ projected = "run.started"
900
+ payload: dict[str, Any] = {}
901
+ elif isinstance(event, RunProgress):
902
+ projected = "run.progress"
903
+ payload = {"progress": event.progress, "message": event.message}
904
+ elif isinstance(event, RunCompleted):
905
+ projected = "run.completed"
906
+ payload = {}
907
+ elif isinstance(event, RunFailed):
908
+ projected = "run.failed"
909
+ payload = {"error": event.error.message or ""}
910
+ elif isinstance(event, RunCanceled):
911
+ projected = "run.cancelled"
912
+ payload = {"reason": event.reason or ""}
913
+ elif isinstance(event, RunInterrupted):
914
+ projected = "run.paused" if event.reason == "user_pause" else "run.interrupted"
915
+ payload = {"reason": event.reason or ""}
916
+ elif isinstance(event, ItemUpdated) and event.item_kind in {"message", "reasoning"}:
917
+ text = event.update.text if isinstance(event.update, TextContent) else ""
918
+ is_thinking = event.item_kind == "reasoning"
919
+ projected = "thinking.delta" if is_thinking else "message.delta"
920
+ payload = {"text": text}
921
+ elif isinstance(event, ItemCompleted) and event.item_kind in {"message", "reasoning"}:
922
+ text = ""
923
+ for part in event.snapshot.parts:
924
+ if isinstance(part, TextContent):
925
+ text = part.text
926
+ break
927
+ is_thinking = event.item_kind == "reasoning"
928
+ projected = "thinking.completed" if is_thinking else "message.completed"
929
+ payload = {"text": text}
930
+ elif isinstance(event, ItemStarted) and event.item_kind == "tool_call":
931
+ tool_part = _first_content(event.initial, ToolCallContent) if event.initial else None
932
+ if tool_part is not None and tool_part.name == "codex.command":
933
+ projected = "command.started"
934
+ args = tool_part.arguments if isinstance(tool_part.arguments, dict) else {}
935
+ payload = {
936
+ "callId": tool_part.call_id,
937
+ "command": str(args.get("command") or ""),
938
+ "cwd": str(args.get("cwd") or ""),
939
+ "commandActions": args.get("command_actions") or [],
940
+ }
941
+ elif tool_part is not None:
942
+ projected = "tool.started"
943
+ payload = {
944
+ "callId": tool_part.call_id,
945
+ "tool": tool_part.name,
946
+ "args": tool_part.arguments,
947
+ }
948
+ else:
949
+ projected = "tool.started"
950
+ payload = {}
951
+ elif isinstance(event, ItemCompleted) and event.item_kind == "tool_call":
952
+ tool_call = _first_content(event.snapshot, ToolCallContent)
953
+ tool_result = _first_content(event.snapshot, ToolResultContent)
954
+ if tool_call is not None and tool_call.name == "codex.command":
955
+ projected = "command.completed"
956
+ result = (
957
+ tool_result.result
958
+ if tool_result is not None and isinstance(tool_result.result, dict)
959
+ else {}
960
+ )
961
+ payload = {
962
+ "callId": tool_call.call_id,
963
+ "status": str(result.get("status") or ""),
964
+ "exitCode": result.get("exit_code"),
965
+ "durationMs": result.get("duration_ms"),
966
+ "output": str(result.get("output") or ""),
967
+ }
968
+ else:
969
+ projected = "tool.completed"
970
+ result = (
971
+ tool_result.result
972
+ if tool_result is not None and isinstance(tool_result.result, dict)
973
+ else {}
974
+ )
975
+ tool_status = str(result.get("status") or "")
976
+ if tool_result is not None and tool_result.is_error:
977
+ tool_status = "failed"
978
+ call_id = (tool_call or tool_result).call_id if (tool_call or tool_result) else ""
979
+ payload = {
980
+ "callId": call_id,
981
+ "tool": tool_call.name if tool_call is not None else "",
982
+ "status": tool_status,
983
+ "durationMs": result.get("duration_ms"),
984
+ "output": str(result.get("output") or ""),
985
+ }
986
+ if tool_result is not None and tool_result.is_error:
987
+ payload["error"] = str(result.get("error") or "")
988
+ elif (
989
+ isinstance(event, (ItemStarted, ItemUpdated, ItemCompleted))
990
+ and event.item_kind == "data"
991
+ and event.source.protocol == "a2ui"
992
+ ):
993
+ projected, payload = _project_a2ui_surface(event)
994
+ elif isinstance(event, InteractionRequested):
995
+ if event.interaction_kind == "approval":
996
+ projected = "approval.requested"
997
+ payload = {
998
+ "approvalId": event.interaction_id,
999
+ "callId": event.request.call_id or "",
1000
+ "kind": event.request.kind,
1001
+ "detail": event.request.detail,
1002
+ }
1003
+ else:
1004
+ projected = "a2ui.interaction"
1005
+ payload = {
1006
+ "interactionId": event.interaction_id,
1007
+ "kind": "form",
1008
+ "inputSchema": {},
1009
+ }
1010
+ elif isinstance(event, InteractionResolved):
1011
+ if event.interaction_kind == "approval":
1012
+ projected = "approval.resolved"
1013
+ payload = {
1014
+ "approvalId": event.interaction_id,
1015
+ "callId": "",
1016
+ "decision": "",
1017
+ }
1018
+ else:
1019
+ projected = "a2ui.action"
1020
+ payload = {"interactionId": event.interaction_id}
1021
+ elif isinstance(event, ContinuationCreated):
1022
+ projected = "checkpoint.created"
1023
+ payload = {
1024
+ "checkpointId": event.continuation_id,
1025
+ "granularity": event.ref.get("granularity", "snapshot"),
1026
+ "resumable": event.resumable,
1027
+ }
1028
+ elif isinstance(event, ContinuationResumed):
1029
+ projected = "checkpoint.resumed"
1030
+ payload = {
1031
+ "checkpointId": event.continuation_id,
1032
+ "resumeAttemptId": event.resume_attempt_id,
1033
+ }
1034
+ elif isinstance(event, UsageReported):
1035
+ projected = "usage.reported"
1036
+ payload = {
1037
+ "inputTokens": event.input_tokens,
1038
+ "outputTokens": event.output_tokens,
1039
+ "totalTokens": event.total_tokens,
1040
+ "cachedTokens": event.cached_tokens,
1041
+ "reasoningTokens": event.reasoning_tokens,
1042
+ }
1043
+ else:
1044
+ projected = event.event_type
1045
+ payload = {}
1046
+
1047
+ _attach_studio_identity(payload, event)
1048
+ payload["runtimeEvent"] = dump_runtime_event(event)
1049
+ return projected, payload
1050
+
1051
+
1052
+ def _first_content(
1053
+ snapshot: ContentSnapshot | None, content_type: type
1054
+ ) -> Any | None:
1055
+ if snapshot is None:
1056
+ return None
1057
+ for part in snapshot.parts:
1058
+ if isinstance(part, content_type):
1059
+ return part
1060
+ return None
1061
+
1062
+
1063
+ def _project_a2ui_surface(
1064
+ event: ItemStarted | ItemUpdated | ItemCompleted,
1065
+ ) -> tuple[str, dict[str, Any]]:
1066
+ if isinstance(event, ItemStarted):
1067
+ projected = "a2ui.surface.begin"
1068
+ data_parts = event.initial.parts if event.initial is not None else ()
1069
+ elif isinstance(event, ItemUpdated):
1070
+ projected = "a2ui.surface.update"
1071
+ data_parts = (event.update,) if isinstance(event.update, DataContent) else ()
1072
+ else:
1073
+ projected = "a2ui.surface.end"
1074
+ data_parts = event.snapshot.parts
1075
+
1076
+ surface_id = str(event.source.metadata.get("surface_id") or "")
1077
+ operations: list[dict[str, Any]] = []
1078
+ for part in data_parts:
1079
+ if isinstance(part, DataContent):
1080
+ data = part.data
1081
+ if isinstance(data, list):
1082
+ operations.extend(dict(op) for op in data if isinstance(op, Mapping))
1083
+ if not operations:
1084
+ operations = project_a2ui_operations(projected, {"surface_id": surface_id})
1085
+ payload: dict[str, Any] = {
1086
+ "surfaceId": surface_id,
1087
+ "a2uiOperations": operations,
1088
+ }
1089
+ return projected, payload
1090
+
1091
+
1092
+ def _attach_studio_identity(payload: dict[str, Any], event: RuntimeEvent) -> None:
1093
+ """Attach §8.4 identity fields (runId/scopeId/itemId/partId/operation)."""
1094
+ payload["runId"] = event.run_id
1095
+ payload["scopeId"] = event.scope_id
1096
+ if isinstance(event, (ItemStarted, ItemUpdated, ItemCompleted, ItemFailed)):
1097
+ payload["itemId"] = event.item_id
1098
+ if isinstance(event, ItemUpdated):
1099
+ payload["operation"] = event.op
1100
+ if isinstance(event, ItemUpdated) and hasattr(event.update, "part_id"):
1101
+ payload["partId"] = event.update.part_id
1102
+ if isinstance(event, ItemStarted) and event.initial is not None:
1103
+ for part in event.initial.parts:
1104
+ if hasattr(part, "part_id"):
1105
+ payload["partId"] = part.part_id
1106
+ break
1107
+ if isinstance(event, ItemCompleted):
1108
+ for part in event.snapshot.parts:
1109
+ if hasattr(part, "part_id"):
1110
+ payload["partId"] = part.part_id
1111
+ break
1112
+ if isinstance(event, (InteractionRequested, InteractionResolved)):
1113
+ payload["itemId"] = event.interaction_id
1114
+ if isinstance(event, (ContinuationCreated, ContinuationResumed)):
1115
+ payload["itemId"] = event.continuation_id
1116
+
1117
+
1118
+ __all__ = ["StudioRunService", "StudioRunSpec", "project_runtime_event"]
1119
+
1120
+
1121
+ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
1122
+ """Session envelope -> Studio RunEvent 投影;cursor 仍用 envelope.seq。"""
1123
+
1124
+ payload = envelope.payload or {}
1125
+ if envelope.event_type == "run.completed":
1126
+ return "run.completed", {
1127
+ "runId": envelope.run_id or "",
1128
+ "output_text": str(payload.get("output_text") or ""),
1129
+ }
1130
+ text = str(payload.get("delta") or payload.get("text") or "")
1131
+ if text:
1132
+ return "message.delta", {"delta": text}
1133
+ return None