agentengine-sdk-python 0.8.2__py3-none-any.whl → 0.8.4__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 (563) hide show
  1. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/METADATA +22 -28
  2. agentengine_sdk_python-0.8.4.dist-info/RECORD +1070 -0
  3. ksadk/_build_provenance.json +1 -0
  4. ksadk/agui/_agent_helpers.py +4 -0
  5. ksadk/api/client.py +108 -91
  6. ksadk/builders/code_builder.py +36 -9
  7. ksadk/builders/framework_requirements.py +2 -1
  8. ksadk/builders/ks3_uploader.py +36 -7
  9. ksadk/builders/managed_runtime_builder.py +4 -0
  10. ksadk/cli/__init__.py +38 -3
  11. ksadk/cli/cmd_managed_runtime.py +13 -1
  12. ksadk/cli/cmd_plugin.py +662 -0
  13. ksadk/cli/cmd_web.py +2 -5
  14. ksadk/cli/runtime_bootstrap.py +23 -1
  15. ksadk/codex/client.py +424 -96
  16. ksadk/codex/projection.py +24 -0
  17. ksadk/codex/runtime.py +83 -283
  18. ksadk/common/auth.py +16 -10
  19. ksadk/configs/env_registry.py +59 -3
  20. ksadk/configs/settings.py +9 -0
  21. ksadk/context_engine/contributors.py +73 -8
  22. ksadk/conversations/__init__.py +19 -0
  23. ksadk/conversations/context.py +104 -24
  24. ksadk/conversations/contracts.py +266 -0
  25. ksadk/conversations/message_projection.py +154 -15
  26. ksadk/conversations/projector.py +614 -0
  27. ksadk/conversations/reducer.py +98 -0
  28. ksadk/conversations/runtime_invocation.py +5 -0
  29. ksadk/conversations/runtime_payloads.py +1 -0
  30. ksadk/conversations/runtime_preparation.py +30 -27
  31. ksadk/conversations/runtime_resume.py +50 -2
  32. ksadk/conversations/runtime_stream_events.py +5 -0
  33. ksadk/conversations/runtime_streaming.py +25 -7
  34. ksadk/deployment/env_forward.py +2 -2
  35. ksadk/deployment/providers/serverless.py +1 -1
  36. ksadk/events/_v1_compat/projection.py +2 -0
  37. ksadk/events/adapters/_codex_validators.py +18 -7
  38. ksadk/events/adapters/codex.py +190 -0
  39. ksadk/events/adapters/langgraph.py +15 -1
  40. ksadk/events/canonical_store.py +3 -1
  41. ksadk/events/projections.py +1 -1
  42. ksadk/harness/__init__.py +45 -35
  43. ksadk/harness/config.py +2 -2
  44. ksadk/harness/runtime.py +75 -16
  45. ksadk/harness/tools.py +38 -13
  46. ksadk/interaction/provider.py +1 -0
  47. ksadk/interaction/providers/langgraph.py +1 -0
  48. ksadk/kernel/bootstrap.py +28 -4
  49. ksadk/kernel/contract_fingerprints.py +5 -4
  50. ksadk/kernel/contracts.py +8 -0
  51. ksadk/kernel/control.py +1 -0
  52. ksadk/kernel/ingress.py +36 -2
  53. ksadk/kernel/memory_store.py +1 -0
  54. ksadk/kernel/postgres_store.py +1 -0
  55. ksadk/kernel/sqlite_store.py +28 -0
  56. ksadk/kernel/store.py +3 -0
  57. ksadk/kernel/worker.py +76 -10
  58. ksadk/knowledge_base/client.py +57 -27
  59. ksadk/mcp_runtime/__init__.py +12 -4
  60. ksadk/memory/adk/backends/sdk_ltm_backend.py +161 -41
  61. ksadk/memory/adk/resilient_session_service.py +25 -17
  62. ksadk/memory/adk/short_term_memory.py +26 -9
  63. ksadk/memory/models.py +7 -4
  64. ksadk/memory/provider_resolver.py +3 -0
  65. ksadk/memory/providers/local_sqlite.py +7 -0
  66. ksadk/model_proxy/server.py +10 -5
  67. ksadk/plugins/__init__.py +171 -0
  68. ksadk/plugins/artifacts.py +275 -0
  69. ksadk/plugins/bridges/__init__.py +47 -0
  70. ksadk/plugins/bridges/codex.py +659 -0
  71. ksadk/plugins/bridges/dsh.py +1300 -0
  72. ksadk/plugins/builtins.py +746 -0
  73. ksadk/plugins/bundle.py +246 -0
  74. ksadk/plugins/bundle_security.py +191 -0
  75. ksadk/plugins/codex_manifest.py +983 -0
  76. ksadk/plugins/composition.py +537 -0
  77. ksadk/plugins/context_contributor.py +437 -0
  78. ksadk/plugins/contracts.py +569 -0
  79. ksadk/plugins/delivery.py +153 -0
  80. ksadk/plugins/dsh_installation_digest.py +71 -0
  81. ksadk/plugins/dsh_toolchain.py +1024 -0
  82. ksadk/plugins/ecosystem_bridge.py +1105 -0
  83. ksadk/plugins/ecosystem_probe.py +191 -0
  84. ksadk/plugins/host.py +787 -0
  85. ksadk/plugins/kernel_adapter.py +205 -0
  86. ksadk/plugins/providers/__init__.py +108 -0
  87. ksadk/plugins/providers/bundles/dsh-knowledge/cordis.patch.yml +3 -0
  88. ksadk/plugins/providers/bundles/dsh-knowledge/index.mjs +53 -0
  89. ksadk/plugins/providers/bundles/dsh-knowledge/package.json +16 -0
  90. ksadk/plugins/providers/bundles/dsh-memory/cordis.patch.yml +3 -0
  91. ksadk/plugins/providers/bundles/dsh-memory/index.mjs +175 -0
  92. ksadk/plugins/providers/bundles/dsh-memory/package.json +16 -0
  93. ksadk/plugins/providers/bundles/dsh-platform-resources/client.mjs +166 -0
  94. ksadk/plugins/providers/bundles/dsh-platform-resources/cordis.patch.yml +3 -0
  95. ksadk/plugins/providers/bundles/dsh-platform-resources/index.mjs +9 -0
  96. ksadk/plugins/providers/bundles/dsh-platform-resources/package.json +12 -0
  97. ksadk/plugins/providers/bundles/dsh-skill-center/cordis.patch.yml +3 -0
  98. ksadk/plugins/providers/bundles/dsh-skill-center/index.mjs +125 -0
  99. ksadk/plugins/providers/bundles/dsh-skill-center/package.json +16 -0
  100. ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
  101. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
  102. ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
  103. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/cordis.patch.yml +3 -0
  104. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/index.mjs +870 -0
  105. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/package.json +27 -0
  106. ksadk/plugins/providers/bundles/ksadk-dsh-studio/client.js +92 -0
  107. ksadk/plugins/providers/bundles/ksadk-dsh-studio/index.mjs +15 -0
  108. ksadk/plugins/providers/bundles/ksadk-dsh-studio/package.json +7 -0
  109. ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
  110. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
  111. ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
  112. ksadk/plugins/providers/codex.py +988 -0
  113. ksadk/plugins/providers/codex_dsh.py +83 -0
  114. ksadk/plugins/providers/codex_native.py +368 -0
  115. ksadk/plugins/providers/codex_turn.py +301 -0
  116. ksadk/plugins/providers/dsh.py +951 -0
  117. ksadk/plugins/providers/dsh_capabilities.py +1389 -0
  118. ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
  119. ksadk/plugins/providers/dsh_mcp.py +467 -0
  120. ksadk/plugins/providers/harness.py +632 -0
  121. ksadk/plugins/providers/harness_dsh.py +91 -0
  122. ksadk/plugins/providers/legacy.py +215 -0
  123. ksadk/plugins/providers/legacy_catalog.py +100 -0
  124. ksadk/plugins/providers/mcp_projection.py +150 -0
  125. ksadk/plugins/providers/platform_resources.py +657 -0
  126. ksadk/plugins/providers/shipped_dsh.py +370 -0
  127. ksadk/plugins/resolver.py +338 -0
  128. ksadk/plugins/subagent_providers/__init__.py +12 -0
  129. ksadk/plugins/subagent_providers/codex.py +443 -0
  130. ksadk/plugins/subagents.py +333 -0
  131. ksadk/resource_runtime/__init__.py +4 -0
  132. ksadk/resource_runtime/broker.py +616 -0
  133. ksadk/resource_runtime/build_artifacts.py +165 -0
  134. ksadk/resource_runtime/contracts.py +138 -0
  135. ksadk/resource_runtime/discovery_receipts.py +100 -0
  136. ksadk/resource_runtime/discovery_skills.py +172 -0
  137. ksadk/resource_runtime/errors.py +6 -0
  138. ksadk/resource_runtime/ipc.py +142 -0
  139. ksadk/resource_runtime/kernel_approval.py +165 -0
  140. ksadk/resource_runtime/knowledge.py +114 -0
  141. ksadk/resource_runtime/langgraph.py +94 -0
  142. ksadk/resource_runtime/leases.py +191 -0
  143. ksadk/resource_runtime/managed_projection.py +186 -0
  144. ksadk/resource_runtime/mcp_server.py +138 -0
  145. ksadk/resource_runtime/memory_provider.py +318 -0
  146. ksadk/resource_runtime/memory_receipts.py +34 -0
  147. ksadk/resource_runtime/operation_ledger.py +211 -0
  148. ksadk/resource_runtime/plugin_config.py +48 -0
  149. ksadk/resource_runtime/policy_authorization.py +54 -0
  150. ksadk/resource_runtime/process.py +110 -0
  151. ksadk/resource_runtime/skill_artifacts.py +153 -0
  152. ksadk/resource_runtime/skill_runtime.py +42 -0
  153. ksadk/resource_runtime/skill_workspace.py +118 -0
  154. ksadk/resource_runtime/skills.py +215 -0
  155. ksadk/resource_runtime/snapshots.py +180 -0
  156. ksadk/resource_runtime/socket_server.py +103 -0
  157. ksadk/resource_runtime/supervisor.py +380 -0
  158. ksadk/resource_runtime/worker.py +504 -0
  159. ksadk/runners/_langgraph_runner_streams.py +74 -6
  160. ksadk/runners/adk_runner.py +149 -11
  161. ksadk/runners/base_runner.py +29 -1
  162. ksadk/runners/langgraph_runner.py +475 -145
  163. ksadk/runtime/_runner_adapter/stream_mapping.py +193 -60
  164. ksadk/runtime/adapter.py +3 -0
  165. ksadk/runtime/conversation_execution.py +92 -16
  166. ksadk/runtime/executor.py +78 -2
  167. ksadk/runtime/factory.py +30 -200
  168. ksadk/runtime/framework_adapters.py +41 -0
  169. ksadk/runtime/preprocessing.py +10 -2
  170. ksadk/runtime/runner_adapter.py +78 -14
  171. ksadk/runtime_context.py +40 -2
  172. ksadk/sandbox/__init__.py +2 -1
  173. ksadk/sandbox/backends/e2b.py +52 -11
  174. ksadk/sandbox/backends/local_process.py +9 -0
  175. ksadk/sandbox/base.py +2 -0
  176. ksadk/sandbox/e2b_connection.py +66 -0
  177. ksadk/sandbox/factory.py +72 -0
  178. ksadk/scheduler/__init__.py +18 -0
  179. ksadk/scheduler/calendar.py +102 -0
  180. ksadk/scheduler/contracts.py +254 -0
  181. ksadk/scheduler/dispatcher.py +129 -0
  182. ksadk/scheduler/engine.py +416 -0
  183. ksadk/scheduler/sqlite_store.py +581 -0
  184. ksadk/server/composition.py +2 -0
  185. ksadk/server/factory.py +5 -0
  186. ksadk/server/persistence_capability.py +516 -0
  187. ksadk/server/routes/control.py +210 -46
  188. ksadk/server/routes/dependencies.py +15 -0
  189. ksadk/server/routes/models.py +47 -4
  190. ksadk/server/routes/projection.py +252 -12
  191. ksadk/server/routes/sessions.py +284 -107
  192. ksadk/server/routes/streaming.py +43 -9
  193. ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-CGbWlMNh.js} +1 -1
  194. ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-b4C7upmO.js} +3 -3
  195. ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-DbdW5A4_.js} +4 -4
  196. ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock-BEBN4Ksz.js} +6 -6
  197. ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-zvL-WpAn.js} +1 -1
  198. ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-BYafW7Za.js} +1 -1
  199. ksadk/server/static/assets/{arc-C4FzinUA.js → arc-bTohInsS.js} +1 -1
  200. ksadk/server/static/assets/architecture-7GRP2DOG-BW5UtSnx.js +1 -0
  201. ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-Ch5Lbzsi.js} +1 -1
  202. ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-kqF4MDOG.js} +1 -1
  203. ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-8uSRIODv.js} +1 -1
  204. ksadk/server/static/assets/channel-CF5Up3hQ.js +1 -0
  205. ksadk/server/static/assets/chunk-2Q5K7J3B-KkXX47WI.js +1 -0
  206. ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-DkhehZDr.js} +1 -1
  207. ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-BdXhtPe9.js} +1 -1
  208. ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-DPTpvpwi.js} +1 -1
  209. ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-Pqoh38el.js} +1 -1
  210. ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-DeU00qC-.js} +1 -1
  211. ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CzxScFkr.js} +1 -1
  212. ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-DpqMx-ux.js} +1 -1
  213. ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-C6t0SypQ.js} +1 -1
  214. ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-DVaeafZk.js} +1 -1
  215. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Cz81acRQ.js +1 -0
  216. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Cz81acRQ.js +1 -0
  217. ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-Bf9D3tzJ.js} +1 -1
  218. ksadk/server/static/assets/cynefin-OW5HDTMX-DVWs8a2t.js +1 -0
  219. ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-B9cY3iTz.js} +1 -1
  220. ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-CSwlx3CB.js} +1 -1
  221. ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-UBpgon5q.js} +1 -1
  222. ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-C9akhN9a.js} +1 -1
  223. ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-CgedhTPI.js} +1 -1
  224. ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-Cjucz5BY.js} +1 -1
  225. ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-Dd4Vleyn.js} +1 -1
  226. ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-CZ2jGtYW.js} +1 -1
  227. ksadk/server/static/assets/{dist-B7seoj3d.js → dist-B2_CVgKv.js} +1 -1
  228. ksadk/server/static/assets/{dist-CSh_DE14.js → dist-B3FR1SKK.js} +1 -1
  229. ksadk/server/static/assets/{dist-DUX-id_F.js → dist-BIr4DSRq.js} +1 -1
  230. ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-BgV_owcB.js} +1 -1
  231. ksadk/server/static/assets/{dist-W-TIVntx.js → dist-Bj730Ib4.js} +1 -1
  232. ksadk/server/static/assets/{dist-BytlxIDT.js → dist-BnaTiyzN.js} +1 -1
  233. ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C1NMAY5a.js} +1 -1
  234. ksadk/server/static/assets/{dist-dDdADKFA.js → dist-C5_ZAZsG.js} +1 -1
  235. ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-CB9iJrvg.js} +1 -1
  236. ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-CFzsn6BG.js} +1 -1
  237. ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-Cf125P_m.js} +1 -1
  238. ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-ClYUxfeK.js} +1 -1
  239. ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DFD6Qa6T.js} +1 -1
  240. ksadk/server/static/assets/{dist-BX8z72IC.js → dist-DVCOWtha.js} +1 -1
  241. ksadk/server/static/assets/{dist-dydu68Fl.js → dist-D_ctSS2i.js} +1 -1
  242. ksadk/server/static/assets/{dist-ClxGviKw.js → dist-DiixqL0J.js} +1 -1
  243. ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-DoVSwoDb.js} +1 -1
  244. ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-DvFK7p5S.js} +1 -1
  245. ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-QfkEHOlb.js} +1 -1
  246. ksadk/server/static/assets/{dist-CttYUqzS.js → dist-btbmmX5O.js} +1 -1
  247. ksadk/server/static/assets/{dist-DmldrHd_.js → dist-fBO2v5Kt.js} +1 -1
  248. ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-ufY3F5p6.js} +1 -1
  249. ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-Ci8NqqZu.js} +1 -1
  250. ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CCIOhLZG.js} +1 -1
  251. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CQqR2hg3.js +1 -0
  252. ksadk/server/static/assets/flowDiagram-A5DVABFB-DDRRbGPm.js +1 -0
  253. ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-Cp_r9Hb6.js} +1 -1
  254. ksadk/server/static/assets/gitGraph-4MIJSDKK-fgWEf7UD.js +1 -0
  255. ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-Dlhc8Wr8.js} +1 -1
  256. ksadk/server/static/assets/index-DcTML1Up.css +2 -0
  257. ksadk/server/static/assets/index-Dh2XHqPK.js +244 -0
  258. ksadk/server/static/assets/info-A6RAGUB7-BUecLTdt.js +1 -0
  259. ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CO0rRTRA.js} +1 -1
  260. ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CHiaedSS.js} +1 -1
  261. ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-CHcCIB09.js} +1 -1
  262. ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CJ_ebBZf.js} +1 -1
  263. ksadk/server/static/assets/{katex-vFWytM5c.js → katex-B4weu383.js} +1 -1
  264. ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-V-MgYHW1.js} +1 -1
  265. ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-CBijEX_c.js} +2 -2
  266. ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-Bc-pQvpp.js} +1 -1
  267. ksadk/server/static/assets/packet-AYTQ26CC-BeW-btFv.js +1 -0
  268. ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-D7genGLl.js} +1 -1
  269. ksadk/server/static/assets/pie-WAS4IAKB-X0xh4SaT.js +1 -0
  270. ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-Bxj32zwD.js} +1 -1
  271. ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-BURv2mGh.js} +1 -1
  272. ksadk/server/static/assets/radar-RG4KPBEZ-DzRMSqjd.js +1 -0
  273. ksadk/server/static/assets/railroad-74A4TZTK-Cc44icX-.js +1 -0
  274. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-D6S4BdV-.js +1 -0
  275. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-vD3aVE2o.js +1 -0
  276. ksadk/server/static/assets/railroad-peg-WCYAUIDC-CYbW_b4T.js +1 -0
  277. ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-CCsh3avW.js} +1 -1
  278. ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-_FRRBKEd.js} +1 -1
  279. ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-C-gr2o9M.js} +1 -1
  280. ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-D97NmfKg.js} +1 -1
  281. ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DjrPC5FX.js} +1 -1
  282. ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-DzMNg1hh.js} +1 -1
  283. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BIcjKFhF.js +1 -0
  284. ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-Cssf_CsU.js} +1 -1
  285. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-tq3K3Ybl.js +8 -0
  286. ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-BWTHzyS1.js} +1 -1
  287. ksadk/server/static/assets/treeView-Q6P3EWNA-DBXt3GdU.js +1 -0
  288. ksadk/server/static/assets/treemap-WGGIJYW6-DrCy4E_l.js +1 -0
  289. ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-BSKgJsmR.js} +1 -1
  290. ksadk/server/static/assets/wardley-WFR3VGLG-CefhOliM.js +1 -0
  291. ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-eflHNoaZ.js} +1 -1
  292. ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-eYqrQ-v4.js} +1 -1
  293. ksadk/server/static/index.html +2 -2
  294. ksadk/session_context.py +66 -0
  295. ksadk/sessions/__init__.py +11 -21
  296. ksadk/sessions/_local_service_sync.py +263 -1
  297. ksadk/sessions/base.py +176 -0
  298. ksadk/sessions/errors.py +4 -0
  299. ksadk/sessions/in_memory.py +309 -19
  300. ksadk/sessions/local_service.py +83 -1
  301. ksadk/sessions/persistence.py +275 -0
  302. ksadk/sessions/postgres_service.py +358 -0
  303. ksadk/sessions/resilience.py +8 -0
  304. ksadk/sessions/resilient.py +362 -4
  305. ksadk/sessions/topology.py +135 -0
  306. ksadk/skills/models.py +26 -4
  307. ksadk/skills/package_store.py +186 -38
  308. ksadk/skills/runtime/agent.py +31 -6
  309. ksadk/skills/runtime/artifact_delivery.py +139 -0
  310. ksadk/skills/runtime/backends/e2b.py +98 -6
  311. ksadk/skills/runtime/backends/local.py +73 -8
  312. ksadk/skills/runtime/base.py +3 -0
  313. ksadk/skills/runtime/executor.py +2 -0
  314. ksadk/skills/runtime/loader.py +10 -2
  315. ksadk/skills/runtime/pinned.py +114 -0
  316. ksadk/skills/runtime/request.py +31 -0
  317. ksadk/skills/service_client.py +211 -24
  318. ksadk/studio/agent_lifecycle.py +26 -20
  319. ksadk/studio/api.py +714 -90
  320. ksadk/studio/api_catalog_routes.py +23 -0
  321. ksadk/studio/api_contracts.py +34 -1
  322. ksadk/studio/api_plugin_routes.py +911 -0
  323. ksadk/studio/api_resource_connections.py +183 -0
  324. ksadk/studio/attachment_store.py +187 -0
  325. ksadk/studio/authoring.py +10 -4
  326. ksadk/studio/authoring_coordinator.py +31 -16
  327. ksadk/studio/builder.py +199 -43
  328. ksadk/studio/cloud.py +346 -104
  329. ksadk/studio/cloud_shared_web.py +565 -0
  330. ksadk/studio/codex_agent_service.py +140 -27
  331. ksadk/studio/codex_builder.py +294 -16
  332. ksadk/studio/codex_credentials.py +90 -0
  333. ksadk/studio/codex_manifest.py +51 -4
  334. ksadk/studio/codex_plugin_store.py +746 -0
  335. ksadk/studio/codex_provider_build.py +231 -0
  336. ksadk/studio/codex_run.py +200 -29
  337. ksadk/studio/compatibility_report.py +456 -0
  338. ksadk/studio/compiler.py +84 -15
  339. ksadk/studio/contracts.py +249 -4
  340. ksadk/studio/dsh_application.py +137 -0
  341. ksadk/studio/dsh_capability_service.py +1107 -0
  342. ksadk/studio/dsh_models.py +59 -0
  343. ksadk/studio/dsh_provider_registration.py +1075 -0
  344. ksadk/studio/event_store.py +54 -6
  345. ksadk/studio/framework_run.py +37 -4
  346. ksadk/studio/hosted_kernel.py +89 -14
  347. ksadk/studio/mcp_runtime.py +11 -3
  348. ksadk/studio/model_client.py +2 -2
  349. ksadk/studio/operations.py +12 -2
  350. ksadk/studio/plugin_composition.py +468 -0
  351. ksadk/studio/plugin_kernel_adapter.py +28 -0
  352. ksadk/studio/plugin_runtime.py +831 -0
  353. ksadk/studio/resource_authority.py +637 -0
  354. ksadk/studio/resource_binding_validation.py +77 -0
  355. ksadk/studio/resource_build_admission.py +196 -0
  356. ksadk/studio/resource_build_materializer.py +149 -0
  357. ksadk/studio/resource_catalog.py +149 -3
  358. ksadk/studio/resource_connections.py +220 -0
  359. ksadk/studio/run_service.py +1144 -96
  360. ksadk/studio/runtime_source.py +24 -8
  361. ksadk/studio/scheduler_assistant.py +317 -0
  362. ksadk/studio/scheduler_runtime.py +383 -0
  363. ksadk/studio/scheduler_service.py +214 -0
  364. ksadk/studio/service.py +1131 -95
  365. ksadk/studio/shared_web.py +610 -87
  366. ksadk/studio/soul.py +52 -0
  367. ksadk/studio/static/assets/CodeBlock-DCUUY0PL-BLjA7kbS.js +41 -0
  368. ksadk/studio/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  369. ksadk/studio/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  370. ksadk/studio/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  371. ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  372. ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  373. ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  374. ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  375. ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  376. ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  377. ksadk/studio/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  378. ksadk/studio/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  379. ksadk/studio/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  380. ksadk/studio/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  381. ksadk/studio/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  382. ksadk/studio/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  383. ksadk/studio/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  384. ksadk/studio/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  385. ksadk/studio/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  386. ksadk/studio/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  387. ksadk/studio/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  388. ksadk/studio/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  389. ksadk/studio/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  390. ksadk/studio/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  391. ksadk/studio/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  392. ksadk/studio/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  393. ksadk/studio/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  394. ksadk/studio/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  395. ksadk/studio/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  396. ksadk/studio/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  397. ksadk/studio/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  398. ksadk/studio/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  399. ksadk/studio/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  400. ksadk/studio/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  401. ksadk/studio/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  402. ksadk/studio/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  403. ksadk/studio/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  404. ksadk/studio/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  405. ksadk/studio/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  406. ksadk/studio/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  407. ksadk/studio/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  408. ksadk/studio/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  409. ksadk/studio/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  410. ksadk/studio/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  411. ksadk/studio/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  412. ksadk/studio/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  413. ksadk/studio/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  414. ksadk/studio/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  415. ksadk/studio/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  416. ksadk/studio/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  417. ksadk/studio/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  418. ksadk/studio/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  419. ksadk/studio/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  420. ksadk/studio/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  421. ksadk/studio/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  422. ksadk/studio/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  423. ksadk/studio/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  424. ksadk/studio/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  425. ksadk/studio/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  426. ksadk/studio/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  427. ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--BEOyPC3w.css +1 -0
  428. ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--CUYIXwgI.js +8 -0
  429. ksadk/studio/static/assets/MermaidBlock-DBFTztfQ-BTYKWEbD.js +44 -0
  430. ksadk/studio/static/assets/PrismRenderer-wiXRHURw.js +3 -0
  431. ksadk/studio/static/assets/abnfDiagram-VCTEODGH-CLETGjKq.js +1 -0
  432. ksadk/studio/static/assets/arc-Pry-7G1_.js +1 -0
  433. ksadk/studio/static/assets/architecture-7GRP2DOG-hXMiO_AU.js +1 -0
  434. ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-B5EQysk6.js +36 -0
  435. ksadk/studio/static/assets/array-BifhSqXX.js +1 -0
  436. ksadk/studio/static/assets/blockDiagram-I7D4REHJ-CFmPl1de.js +129 -0
  437. ksadk/studio/static/assets/c4Diagram-7LVT6UL2-BRrGu8qt.js +38 -0
  438. ksadk/studio/static/assets/channel-BqO-gtEw.js +1 -0
  439. ksadk/studio/static/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  440. ksadk/studio/static/assets/chunk-4HAMMTFA-D220vUtq.js +62 -0
  441. ksadk/studio/static/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  442. ksadk/studio/static/assets/chunk-75Z2AOVW-C8lLv_qc.js +2 -0
  443. ksadk/studio/static/assets/chunk-DU6HZSFF-C4DtVpCu.js +128 -0
  444. ksadk/studio/static/assets/chunk-F27PBJKO-Cpw14SNG.js +1 -0
  445. ksadk/studio/static/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
  446. ksadk/studio/static/assets/chunk-GMAD6QVW-DiQA-u2N.js +72 -0
  447. ksadk/studio/static/assets/chunk-GVQU2GXP-DUdNBUCi.js +1 -0
  448. ksadk/studio/static/assets/chunk-IMKFNOWR-BSlbjT_b.js +231 -0
  449. ksadk/studio/static/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  450. ksadk/studio/static/assets/chunk-L3NEJ4N5-Db8Ab22W.js +1 -0
  451. ksadk/studio/static/assets/chunk-OSK3NFVY-M2m6G3j4.js +10 -0
  452. ksadk/studio/static/assets/chunk-P2QGCYS3-DezaQxiK.js +1 -0
  453. ksadk/studio/static/assets/chunk-POPQ4Y6H-DOpufJ3t.js +1 -0
  454. ksadk/studio/static/assets/chunk-PWAF6VOD-CfLb1gSc.js +1 -0
  455. ksadk/studio/static/assets/chunk-SHT3W25Y-DAyIVp5H.js +168 -0
  456. ksadk/studio/static/assets/chunk-SVP7TREG-DulomOrS.js +88 -0
  457. ksadk/studio/static/assets/chunk-TICWLB2K-bHJYoflj.js +206 -0
  458. ksadk/studio/static/assets/chunk-XXDRQBXY-7PcdR2B1.js +1 -0
  459. ksadk/studio/static/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  460. ksadk/studio/static/assets/classDiagram-ZZMXUADV-D_bW8JjA.js +1 -0
  461. ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-D_bW8JjA.js +1 -0
  462. ksadk/studio/static/assets/cose-bilkent-JH36ORCC-snL88wkX.js +1 -0
  463. ksadk/studio/static/assets/cynefin-OW5HDTMX-Bd1YhRo1.js +1 -0
  464. ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-BBrIG_mI.js +62 -0
  465. ksadk/studio/static/assets/cytoscape.esm-CGd-uY3x.js +321 -0
  466. ksadk/studio/static/assets/dagre-GXQ25YYZ-DtF1nGdY.js +4 -0
  467. ksadk/studio/static/assets/dagre-PrKaheQc.js +1 -0
  468. ksadk/studio/static/assets/defaultLocale-BFoDCU3G.js +1 -0
  469. ksadk/studio/static/assets/diagram-S7CK7UJ4-0BZ98mya.js +30 -0
  470. ksadk/studio/static/assets/diagram-UQ7AKVKN-6mLKEo6L.js +41 -0
  471. ksadk/studio/static/assets/diagram-VSXAHHWV-CJBIaiZ2.js +3 -0
  472. ksadk/studio/static/assets/diagram-VX7I27RA-BohLEE1C.js +24 -0
  473. ksadk/studio/static/assets/diagram-Z3DM3KII-D8KIGonG.js +24 -0
  474. ksadk/studio/static/assets/dist-v5Q1xZ2K.js +1 -0
  475. ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-C99HU61t.js +1 -0
  476. ksadk/studio/static/assets/erDiagram-RLTQ6QDP-DLGry2Ap.js +99 -0
  477. ksadk/studio/static/assets/eventmodeling-NTZA5JFV-B1_5Ac8j.js +1 -0
  478. ksadk/studio/static/assets/flowDiagram-HODETNUW-DD5hmicn.js +1 -0
  479. ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BPJrVLSG.js +292 -0
  480. ksadk/studio/static/assets/gitGraph-4MIJSDKK-Cy_ZQBGi.js +1 -0
  481. ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-CZCUz4MT.js +106 -0
  482. ksadk/studio/static/assets/graphlib-DS17s2tU.js +1 -0
  483. ksadk/studio/static/assets/index-CeHL9ARV.css +2 -0
  484. ksadk/studio/static/assets/index-D9QEcVpL.js +320 -0
  485. ksadk/studio/static/assets/info-A6RAGUB7-DBzDQB_I.js +1 -0
  486. ksadk/studio/static/assets/infoDiagram-27XIBGKW-B-sq07Hi.js +2 -0
  487. ksadk/studio/static/assets/init-C-OQMol4.js +1 -0
  488. ksadk/studio/static/assets/ishikawaDiagram-5VMMS53U-CljI2wyf.js +70 -0
  489. ksadk/studio/static/assets/journeyDiagram-3NMN7TZE-D50Fo2sL.js +139 -0
  490. ksadk/studio/static/assets/kanban-definition-UXKFOSKX-DimfOTam.js +89 -0
  491. ksadk/studio/static/assets/katex-DhON-x74.js +257 -0
  492. ksadk/studio/static/assets/line-CX5BOOUP.js +1 -0
  493. ksadk/studio/static/assets/linear-BCxN9JLv.js +1 -0
  494. ksadk/studio/static/assets/mermaid-parser.core-BdCz8fkS.js +7 -0
  495. ksadk/studio/static/assets/mindmap-definition-YA3MSWOX-Bizzqy5R.js +96 -0
  496. ksadk/studio/static/assets/ordinal-BDEzSJ7C.js +1 -0
  497. ksadk/studio/static/assets/packet-AYTQ26CC-gPln-_kG.js +1 -0
  498. ksadk/studio/static/assets/path-fybaL0A-.js +1 -0
  499. ksadk/studio/static/assets/pegDiagram-XKGWAZYB-Dy3fB-gW.js +1 -0
  500. ksadk/studio/static/assets/pie-WAS4IAKB-qZdHMr7B.js +1 -0
  501. ksadk/studio/static/assets/pieDiagram-E7YTZNPT-B9mrFSDF.js +39 -0
  502. ksadk/studio/static/assets/quadrantDiagram-AXDQQJYC-NzUfCbMR.js +7 -0
  503. ksadk/studio/static/assets/radar-RG4KPBEZ-BZlnaea9.js +1 -0
  504. ksadk/studio/static/assets/railroad-74A4TZTK-Ce3__H9c.js +1 -0
  505. ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-DqQn7EzD.js +1 -0
  506. ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-B55kCA8F.js +1 -0
  507. ksadk/studio/static/assets/railroad-peg-WCYAUIDC-DfQnvkHd.js +1 -0
  508. ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-DnhMjZCk.js +1 -0
  509. ksadk/studio/static/assets/requirementDiagram-BXWQKSXE-BF3d103U.js +84 -0
  510. ksadk/studio/static/assets/rolldown-runtime-Dy4uBu1J-DK3Fl9T5.js +1 -0
  511. ksadk/studio/static/assets/rough.esm-Dy-Kn_BL.js +1 -0
  512. ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-Bo-JTwt0.js +40 -0
  513. ksadk/studio/static/assets/sequenceDiagram-WJ2MYXX4-Dh4wZtUm.js +162 -0
  514. ksadk/studio/static/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  515. ksadk/studio/static/assets/src-SFn9St9H.js +1 -0
  516. ksadk/studio/static/assets/stateDiagram-D77RDMKH-DH774G6d.js +1 -0
  517. ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-DZvVWPBo.js +1 -0
  518. ksadk/studio/static/assets/swimlanes-42K2YHIH-Bl13OvTX.js +1 -0
  519. ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-BnFOTALJ.js +8 -0
  520. ksadk/studio/static/assets/timeline-definition-24CTP7MA-DxmSSjRB.js +120 -0
  521. ksadk/studio/static/assets/treeView-Q6P3EWNA-D5Nn9Swz.js +1 -0
  522. ksadk/studio/static/assets/treemap-WGGIJYW6-CgHgnQhE.js +1 -0
  523. ksadk/studio/static/assets/vennDiagram-4TSXK5OY-B9QEXcJq.js +34 -0
  524. ksadk/studio/static/assets/wardley-WFR3VGLG-DXrK2RRY.js +1 -0
  525. ksadk/studio/static/assets/wardleyDiagram-VM6X3IG4-Ba1AOWoe.js +78 -0
  526. ksadk/studio/static/assets/xychartDiagram-S5SC5T6Z-Cr5pt5Y-.js +7 -0
  527. ksadk/studio/static/index.html +2 -2
  528. ksadk/studio/validator.py +1 -1
  529. ksadk/studio/workspace.py +1 -0
  530. ksadk/version.py +1 -1
  531. ksadk_runtime_common/workspace_files/path_utils.py +41 -0
  532. agentengine_sdk_python-0.8.2.dist-info/RECORD +0 -778
  533. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
  534. ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
  535. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
  536. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
  537. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
  538. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
  539. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
  540. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
  541. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
  542. ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
  543. ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
  544. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
  545. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
  546. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
  547. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
  548. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
  549. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
  550. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
  551. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
  552. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
  553. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
  554. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
  555. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
  556. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
  557. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +0 -3
  558. ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
  559. ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
  560. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/WHEEL +0 -0
  561. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/entry_points.txt +0 -0
  562. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/licenses/LICENSE +0 -0
  563. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/top_level.txt +0 -0
@@ -3,15 +3,24 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import hashlib
7
+ import json
6
8
  import logging
7
9
  import time
8
10
  from collections.abc import Callable, Mapping
9
11
  from dataclasses import asdict, dataclass, field
10
12
  from datetime import datetime, timezone
13
+ from pathlib import Path
11
14
  from typing import Any
12
15
  from uuid import uuid4
13
16
 
17
+ from jsonschema import Draft202012Validator
18
+
14
19
  from ksadk.agui.a2ui_projection import project_a2ui_operations
20
+ from ksadk.conversations.projector import (
21
+ project_conversation_item,
22
+ project_interaction_conversation_item,
23
+ )
15
24
  from ksadk.events.canonical import (
16
25
  ContinuationCreated,
17
26
  ContinuationResumed,
@@ -22,6 +31,7 @@ from ksadk.events.canonical import (
22
31
  ItemFailed,
23
32
  ItemStarted,
24
33
  ItemUpdated,
34
+ OutputRef,
25
35
  RunCanceled,
26
36
  RunCompleted,
27
37
  RunFailed,
@@ -32,7 +42,9 @@ from ksadk.events.canonical import (
32
42
  SourceRef,
33
43
  UsageReported,
34
44
  dump_runtime_event,
45
+ parse_runtime_event,
35
46
  )
47
+ from ksadk.events.canonical_store import session_event_to_runtime_event
36
48
  from ksadk.events.content import (
37
49
  ContentSnapshot,
38
50
  DataContent,
@@ -50,6 +62,7 @@ from ksadk.runtime import (
50
62
  RuntimeLaunchContext,
51
63
  StartRequest,
52
64
  )
65
+ from ksadk.runtime.executor import RuntimeStartPreparation
53
66
  from ksadk.sessions.base import BaseSessionService
54
67
  from ksadk.sessions.local_service import LocalSessionService
55
68
  from ksadk.studio.contracts import RunEvent, RunRecord, RunStatus, Usage
@@ -58,6 +71,7 @@ from ksadk.studio.event_store import RunEventStore
58
71
  from ksadk.studio.workspace import Workspace
59
72
 
60
73
  _CANCEL_TIMEOUT_SECONDS = 2.0
74
+ _INTERACTION_SUBMIT_TIMEOUT_SECONDS = 10.0
61
75
 
62
76
 
63
77
  logger = logging.getLogger(__name__)
@@ -73,6 +87,7 @@ class StudioRunSpec:
73
87
  model: str | None = None
74
88
  request_config: Mapping[str, Any] = field(default_factory=dict)
75
89
  manifest_sha256: str = ""
90
+ plugin_bundle_root: Path | None = None
76
91
 
77
92
 
78
93
  class StudioRunService:
@@ -86,6 +101,7 @@ class StudioRunService:
86
101
  event_store: RunEventStore | None = None,
87
102
  session_service: BaseSessionService | None = None,
88
103
  runtime_events: RuntimeEventStore | None = None,
104
+ plugin_runtime: Any | None = None,
89
105
  ) -> None:
90
106
  self.workspace = workspace
91
107
  self.executor = executor
@@ -94,10 +110,21 @@ class StudioRunService:
94
110
  project_dir=str(workspace.root)
95
111
  )
96
112
  self.runtime_events = runtime_events or RuntimeEventStore(self.session_service)
113
+ self.plugin_runtime = plugin_runtime
114
+ self.schedule_assistant: Any | None = None
97
115
  self._active_handles: dict[str, Any] = {}
116
+ self._active_sessions: set[tuple[str, str]] = set()
98
117
  self._cancel_flags: dict[str, bool] = {}
99
- self._control_queues: dict[str, asyncio.Queue[tuple[str, ResumePayload | None]]] = {}
118
+ self._control_queues: dict[
119
+ str,
120
+ asyncio.Queue[tuple[str, ResumePayload | None, asyncio.Future[None] | None]],
121
+ ] = {}
100
122
  self._waiting_modes: dict[str, str] = {}
123
+ self._submitted_interactions: dict[str, set[str]] = {}
124
+ # Interaction resolution appends to one run-level event file. Serialise
125
+ # every decision for that run, not only identical interaction ids, so
126
+ # two concurrently visible cards cannot overwrite each other's receipt.
127
+ self._interaction_locks: dict[str, asyncio.Lock] = {}
101
128
 
102
129
  async def recover_interrupted(self) -> None:
103
130
  """Settle local runs left active across a Studio restart.
@@ -122,6 +149,7 @@ class StudioRunService:
122
149
  "run.interrupted",
123
150
  {"reason": "local_studio_restarted", "recoverable": False},
124
151
  )
152
+ self._expire_unresolved_interactions(record)
125
153
 
126
154
  async def events(self, run_id: str, *, after: int = 0) -> list[RunEvent]:
127
155
  """Read the durable Studio event timeline for API/SSE replay."""
@@ -135,6 +163,38 @@ class StudioRunService:
135
163
  runtime_input: Any = None,
136
164
  session_id: str | None = None,
137
165
  on_event: Callable[[RunEvent], None] | None = None,
166
+ ) -> RunRecord:
167
+ session = session_id or f"ses_{uuid4().hex}"
168
+ key = (spec.agent_id, session)
169
+ # Claim before the first await, including preparation and plugin runs.
170
+ # A second turn must never attach another adapter to the live thread.
171
+ if key in self._active_sessions:
172
+ raise StudioError(
173
+ "SESSION_RUN_ACTIVE",
174
+ "上一轮仍在运行或等待回答,请先完成回答或停止运行。",
175
+ status_code=409,
176
+ details={"sessionId": session},
177
+ )
178
+ self._active_sessions.add(key)
179
+ try:
180
+ return await self._run_owned(
181
+ spec,
182
+ user_input,
183
+ runtime_input=runtime_input,
184
+ session_id=session,
185
+ on_event=on_event,
186
+ )
187
+ finally:
188
+ self._active_sessions.discard(key)
189
+
190
+ async def _run_owned(
191
+ self,
192
+ spec: StudioRunSpec,
193
+ user_input: str,
194
+ *,
195
+ runtime_input: Any,
196
+ session_id: str,
197
+ on_event: Callable[[RunEvent], None] | None,
138
198
  ) -> RunRecord:
139
199
  run_id = f"run_{uuid4().hex}"
140
200
  session = session_id or f"ses_{uuid4().hex}"
@@ -173,11 +233,41 @@ class StudioRunService:
173
233
  if on_event is not None:
174
234
  on_event(created)
175
235
 
176
- from ksadk.kernel.ingress import kernel_route_active
236
+ if self.schedule_assistant is not None and (
237
+ self.schedule_assistant.matches(user_input)
238
+ or self.schedule_assistant.matches_followup(spec.agent_id, session, user_input)
239
+ ):
240
+ handled = await self._schedule_run(record, user_input, on_event=on_event)
241
+ if handled is not None:
242
+ return handled
177
243
 
178
- if kernel_route_active():
244
+ provider_runtime_adapter = (
245
+ spec.plugin_bundle_root is not None
246
+ and (
247
+ runtime_type == "codex"
248
+ or bool(spec.request_config.get("provider_runtime_adapter"))
249
+ )
250
+ )
251
+ # A process-global Codex Kernel can share Agent/cwd with this Build
252
+ # while owning a legacy direct factory. New Builds must retain their
253
+ # exact Provider activation, including when that global Kernel exists.
254
+ kernel_runtime = (
255
+ None if provider_runtime_adapter else self._kernel_runtime_for_spec(spec)
256
+ )
257
+ if kernel_runtime is not None:
179
258
  return await self._kernel_run(
180
- spec, user_input, record=record, on_event=on_event
259
+ spec,
260
+ user_input,
261
+ record=record,
262
+ on_event=on_event,
263
+ kernel_runtime=kernel_runtime,
264
+ )
265
+ if spec.plugin_bundle_root is not None and not provider_runtime_adapter:
266
+ return await self._plugin_run(
267
+ spec,
268
+ user_input,
269
+ record=record,
270
+ on_event=on_event,
181
271
  )
182
272
 
183
273
  started = time.monotonic()
@@ -187,18 +277,32 @@ class StudioRunService:
187
277
 
188
278
  async def persist(runtime_event: RuntimeEvent) -> RunEvent:
189
279
  persisted = await self.runtime_events.append_one(record.session_id, runtime_event)
190
- event_type, data = project_runtime_event(persisted)
280
+ event_type, data = project_runtime_event(
281
+ persisted,
282
+ session_id=record.session_id,
283
+ public_run_id=record.id,
284
+ )
285
+ if isinstance(persisted, InteractionRequested):
286
+ data["revision"] = 1
287
+ conversation_item = data.get("conversationItem")
288
+ if isinstance(conversation_item, dict):
289
+ item_payload = conversation_item.get("payload")
290
+ if isinstance(item_payload, dict):
291
+ item_payload["revision"] = 1
191
292
  stored = self.event_store.append(record.id, event_type, data)
192
293
  if on_event is not None:
193
294
  on_event(stored)
194
295
  return stored
195
296
 
196
297
  handle = None
197
- final_text = ""
198
- streamed_final = ""
298
+ completed_text_by_item: dict[tuple[str, str], str] = {}
299
+ streamed_text_by_item: dict[tuple[str, str], str] = {}
300
+ terminal_output_refs: tuple[OutputRef, ...] = ()
199
301
  runtime_duration_ms: int | None = None
200
302
  item_phases: dict[tuple[str, str], str | None] = {}
201
- control_queue: asyncio.Queue[tuple[str, ResumePayload | None]] = asyncio.Queue()
303
+ control_queue: asyncio.Queue[
304
+ tuple[str, ResumePayload | None, asyncio.Future[None] | None]
305
+ ] = asyncio.Queue()
202
306
  self._control_queues[run_id] = control_queue
203
307
  try:
204
308
  tool_approval_mode = str(spec.request_config.get("tool_approval_mode") or "")
@@ -225,10 +329,29 @@ class StudioRunService:
225
329
  metadata={
226
330
  "invocation_id": run_id,
227
331
  CONVERSATION_PREPROCESSING_METADATA_KEY: conversation_request,
228
- **self._native_session_metadata(spec.agent_id, session, runtime_type),
332
+ **self._native_session_metadata(
333
+ spec.agent_id,
334
+ session,
335
+ str(
336
+ spec.request_config.get("provider_runtime_type")
337
+ or runtime_type
338
+ ),
339
+ spec.build_id,
340
+ ),
229
341
  },
230
342
  )
231
- handle = await self.executor.start(spec.launch_context, request)
343
+ preparation = None
344
+ if provider_runtime_adapter:
345
+ if self.plugin_runtime is None:
346
+ raise StudioError(
347
+ "PLUGIN_RUNTIME_UNAVAILABLE", "Codex Provider Runtime 不可用",
348
+ status_code=503,
349
+ )
350
+ adapter = self.plugin_runtime.kernel_adapter_provider(spec)()
351
+ preparation = RuntimeStartPreparation(context=spec.launch_context, adapter=adapter)
352
+ handle = await self.executor.start(
353
+ spec.launch_context, request, preparation=preparation,
354
+ )
232
355
  record.runtime_handle = handle.model_dump(mode="json")
233
356
  self.event_store.save(record)
234
357
  self._active_handles[run_id] = handle
@@ -236,6 +359,27 @@ class StudioRunService:
236
359
  terminal_seen = False
237
360
  should_resume = False
238
361
  async for event in self.executor.stream(handle):
362
+ if (
363
+ isinstance(event, RunInterrupted)
364
+ and event.interaction_id
365
+ and record.status == RunStatus.WAITING_INPUT
366
+ and event.source.framework == "codex"
367
+ ):
368
+ # Codex emits this companion event immediately after a
369
+ # live JSON-RPC approval request. The native turn stays
370
+ # attached and must be released through submit; trying
371
+ # to resume it deadlocks that request and later breaks
372
+ # the app-server pipe.
373
+ self._waiting_modes[run_id] = "live"
374
+ continue
375
+ if isinstance(event, InteractionResolved) and event.source.framework == "codex":
376
+ submitted = self._submitted_interactions.get(run_id, set())
377
+ provider_call_id = str(getattr(event.response, "call_id", None) or "")
378
+ if event.interaction_id in submitted or provider_call_id in submitted:
379
+ # submit_interaction atomically persists the public
380
+ # terminal fact and receipt. Ignore the provider's
381
+ # echo so replay never shows the approval twice.
382
+ continue
239
383
  await persist(event)
240
384
  if self._cancel_flags.get(run_id):
241
385
  raise asyncio.CancelledError()
@@ -244,28 +388,28 @@ class StudioRunService:
244
388
  elif (
245
389
  isinstance(event, ItemUpdated)
246
390
  and event.item_kind == "message"
247
- and item_phases.get(
248
- (event.scope_id, event.item_id), "final_answer"
249
- )
391
+ and item_phases.get((event.scope_id, event.item_id), "final_answer")
250
392
  == "final_answer"
251
393
  ):
252
394
  text = event.update.text if isinstance(event.update, TextContent) else ""
395
+ item_key = (event.scope_id, event.item_id)
253
396
  if event.op == "replace":
254
- streamed_final = text
397
+ streamed_text_by_item[item_key] = text
255
398
  else:
256
- streamed_final += text
399
+ streamed_text_by_item[item_key] = (
400
+ streamed_text_by_item.get(item_key, "") + text
401
+ )
257
402
  elif (
258
403
  isinstance(event, ItemCompleted)
259
404
  and event.item_kind == "message"
260
- and item_phases.get(
261
- (event.scope_id, event.item_id), "final_answer"
262
- )
405
+ and item_phases.get((event.scope_id, event.item_id), "final_answer")
263
406
  == "final_answer"
264
407
  ):
265
- for part in event.snapshot.parts:
266
- if isinstance(part, TextContent):
267
- final_text = part.text
268
- break
408
+ completed_text_by_item[(event.scope_id, event.item_id)] = "".join(
409
+ part.text
410
+ for part in event.snapshot.parts
411
+ if isinstance(part, TextContent)
412
+ )
269
413
  elif isinstance(event, UsageReported):
270
414
  record.usage = Usage(
271
415
  input_tokens=event.input_tokens,
@@ -318,6 +462,7 @@ class StudioRunService:
318
462
  elif isinstance(event, RunCompleted):
319
463
  terminal_seen = True
320
464
  record.status = RunStatus.COMPLETED
465
+ terminal_output_refs = event.output_refs
321
466
  raw_duration = event.source.metadata.get("duration_ms")
322
467
  if raw_duration is None:
323
468
  metrics = event.source.metadata.get("metrics")
@@ -331,7 +476,7 @@ class StudioRunService:
331
476
  record.status = RunStatus.COMPLETED
332
477
  break
333
478
 
334
- command, resume_payload = await control_queue.get()
479
+ command, resume_payload, submit_ack = await control_queue.get()
335
480
  if command == "cancel" or self._cancel_flags.get(run_id):
336
481
  raise asyncio.CancelledError()
337
482
  if command != "resume":
@@ -342,11 +487,18 @@ class StudioRunService:
342
487
  if native_thread_id
343
488
  else ResumeTarget(kind="invocation_id", id=handle.run_id)
344
489
  )
345
- handle = await self.executor.resume(
346
- handle,
347
- target,
348
- resume_payload,
349
- )
490
+ try:
491
+ handle = await self.executor.resume(
492
+ handle,
493
+ target,
494
+ resume_payload,
495
+ )
496
+ except Exception as exc:
497
+ if submit_ack is not None and not submit_ack.done():
498
+ submit_ack.set_exception(exc)
499
+ raise
500
+ if submit_ack is not None and not submit_ack.done():
501
+ submit_ack.set_result(None)
350
502
  self._active_handles[run_id] = handle
351
503
  record.runtime_handle = handle.model_dump(mode="json")
352
504
  record.status = RunStatus.RUNNING
@@ -360,7 +512,17 @@ class StudioRunService:
360
512
  if on_event is not None:
361
513
  on_event(resumed)
362
514
  self.event_store.save(record)
363
- record.output = final_text or streamed_final
515
+ text_by_item = {**streamed_text_by_item, **completed_text_by_item}
516
+ if terminal_output_refs:
517
+ terminal_parts = [
518
+ text_by_item.get((ref.scope_id, ref.item_id), "")
519
+ for ref in terminal_output_refs
520
+ ]
521
+ record.output = "\n\n".join(part for part in terminal_parts if part)
522
+ else:
523
+ completed_parts = [part for part in completed_text_by_item.values() if part]
524
+ fallback_parts = [part for part in streamed_text_by_item.values() if part]
525
+ record.output = "\n\n".join(completed_parts or fallback_parts)
364
526
  except asyncio.CancelledError:
365
527
  cancel_result = "task_cancelled"
366
528
  if handle is not None and self.executor.is_attached(handle):
@@ -426,7 +588,9 @@ class StudioRunService:
426
588
  self._cancel_flags.pop(run_id, None)
427
589
  self._control_queues.pop(run_id, None)
428
590
  self._waiting_modes.pop(run_id, None)
591
+ self._submitted_interactions.pop(run_id, None)
429
592
  record.completed_at = datetime.now(timezone.utc)
593
+ self._expire_unresolved_interactions(record, on_event=on_event)
430
594
  if runtime_duration_ms is not None:
431
595
  record.duration_ms = runtime_duration_ms
432
596
  record.duration_source = "runtime"
@@ -437,9 +601,400 @@ class StudioRunService:
437
601
  await self._sync_trace(record)
438
602
  return record
439
603
 
604
+ async def _schedule_run(
605
+ self,
606
+ record: RunRecord,
607
+ user_input: str,
608
+ *,
609
+ on_event: Callable[[RunEvent], None] | None,
610
+ ) -> RunRecord | None:
611
+ """Persist real host tool receipts on the canonical conversation stream."""
612
+ started = time.monotonic()
613
+
614
+ def envelope(kind: str) -> dict[str, Any]:
615
+ result = _plugin_event_envelope(record, kind)
616
+ result["source"] = SourceRef(
617
+ framework="ksadk",
618
+ metadata={"runtime": "studio-scheduler", "agent_id": record.agent_id},
619
+ )
620
+ return result
621
+
622
+ async def publish(event: RuntimeEvent) -> None:
623
+ persisted = await self.runtime_events.append_one(record.session_id, event)
624
+ kind, data = project_runtime_event(
625
+ persisted, session_id=record.session_id, public_run_id=record.id
626
+ )
627
+ stored = self.event_store.append(record.id, kind, data)
628
+ if on_event is not None:
629
+ on_event(stored)
630
+
631
+ call_id = None
632
+ try:
633
+ intent = await self.schedule_assistant.plan(
634
+ record.agent_id,
635
+ user_input,
636
+ record.session_id,
637
+ build_id=record.build_id,
638
+ model_name=record.model,
639
+ )
640
+ if self._cancel_flags.get(record.id):
641
+ raise asyncio.CancelledError()
642
+ if intent.action == "ignore":
643
+ return None
644
+ record.status = RunStatus.RUNNING
645
+ record.started_at = datetime.now(timezone.utc)
646
+ record.runtime_handle = {"provider": "studio-scheduler"}
647
+ self.event_store.save(record)
648
+ await publish(RunStarted(**envelope("run.started"), status="running"))
649
+ call_id = f"{record.id}:schedule"
650
+ arguments = intent.model_dump(mode="json", exclude_none=True)
651
+ await publish(
652
+ ItemStarted(
653
+ **envelope("tool.started"),
654
+ item_id=call_id,
655
+ item_kind="tool_call",
656
+ initial=ContentSnapshot(
657
+ parts=(
658
+ ToolCallContent(
659
+ part_id=f"{call_id}:call",
660
+ call_id=call_id,
661
+ name="studio_schedule",
662
+ arguments=arguments,
663
+ ),
664
+ )
665
+ ),
666
+ )
667
+ )
668
+ result = await self.schedule_assistant.execute(
669
+ record.agent_id, record.session_id, intent
670
+ )
671
+ await publish(
672
+ ItemCompleted(
673
+ **envelope("tool.completed"),
674
+ item_id=call_id,
675
+ item_kind="tool_call",
676
+ snapshot=ContentSnapshot(
677
+ parts=(
678
+ ToolResultContent(
679
+ part_id=f"{call_id}:result", call_id=call_id, result=result
680
+ ),
681
+ )
682
+ ),
683
+ )
684
+ )
685
+ output = result["message"]
686
+ item_id, part_id = f"{record.id}:assistant", f"{record.id}:text"
687
+ await publish(
688
+ ItemStarted(
689
+ **envelope("item.started"),
690
+ item_id=item_id,
691
+ item_kind="message",
692
+ phase="final_answer",
693
+ )
694
+ )
695
+ await publish(
696
+ ItemUpdated(
697
+ **envelope("item.updated"),
698
+ item_id=item_id,
699
+ item_kind="message",
700
+ op="append",
701
+ update=TextContent(part_id=part_id, text=output),
702
+ )
703
+ )
704
+ await publish(
705
+ ItemCompleted(
706
+ **envelope("item.completed"),
707
+ item_id=item_id,
708
+ item_kind="message",
709
+ snapshot=ContentSnapshot(parts=(TextContent(part_id=part_id, text=output),)),
710
+ )
711
+ )
712
+ if intent._usage is not None and intent._usage.reported:
713
+ record.usage = intent._usage.model_copy(update={"source": "studio-scheduler"})
714
+ await publish(
715
+ UsageReported(
716
+ **envelope("usage.reported"),
717
+ input_tokens=record.usage.input_tokens,
718
+ output_tokens=record.usage.output_tokens,
719
+ total_tokens=record.usage.total_tokens,
720
+ cached_tokens=record.usage.cached_input_tokens,
721
+ reasoning_tokens=record.usage.reasoning_output_tokens,
722
+ )
723
+ )
724
+ await publish(
725
+ RunCompleted(
726
+ **envelope("run.completed"),
727
+ status="completed",
728
+ output_refs=(OutputRef(scope_id=record.id, item_id=item_id, part_id=part_id),),
729
+ )
730
+ )
731
+ record.output = output
732
+ record.status = RunStatus.COMPLETED
733
+ except asyncio.CancelledError:
734
+ record.status = RunStatus.CANCELLED
735
+ record.completed_at = datetime.now(timezone.utc)
736
+ self._cancel_flags.pop(record.id, None)
737
+ await publish(
738
+ RunCanceled(**envelope("run.canceled"), status="canceled", reason="cancelled")
739
+ )
740
+ self.event_store.save(record)
741
+ raise
742
+ except Exception as error:
743
+ code = str(getattr(error, "code", "SCHEDULE_REQUEST_FAILED"))
744
+ record.status = RunStatus.FAILED
745
+ record.error = {"code": code, "message": str(error)}
746
+ if call_id is not None:
747
+ await publish(
748
+ ItemFailed(
749
+ **envelope("tool.failed"),
750
+ item_id=call_id,
751
+ item_kind="tool_call",
752
+ error=ErrorInfo(
753
+ code=code,
754
+ message=str(error),
755
+ source="studio-scheduler",
756
+ scope_id=record.id,
757
+ ),
758
+ )
759
+ )
760
+ await publish(
761
+ RunFailed(
762
+ **envelope("run.failed"),
763
+ status="failed",
764
+ error=ErrorInfo(
765
+ code=code, message=str(error), source="studio-scheduler", scope_id=record.id
766
+ ),
767
+ )
768
+ )
769
+ record.completed_at = datetime.now(timezone.utc)
770
+ record.duration_ms = int((time.monotonic() - started) * 1000)
771
+ record.duration_source = "studio"
772
+ self._cancel_flags.pop(record.id, None)
773
+ self.event_store.save(record)
774
+ await self._sync_trace(record)
775
+ return record
776
+
440
777
  async def _sync_trace(self, record: RunRecord) -> None:
441
778
  self.event_store.trace_store.sync(record, await self.events(record.id))
442
779
 
780
+ async def _plugin_run(
781
+ self,
782
+ spec: StudioRunSpec,
783
+ user_input: str,
784
+ *,
785
+ record: RunRecord,
786
+ on_event: Callable[[RunEvent], None] | None,
787
+ ) -> RunRecord:
788
+ """Execute a composed Build without translating it into ADK/LangGraph.
789
+
790
+ PluginHost retains the activation under the Studio session id. Harness
791
+ providers write canonical RuntimeEvents directly; providers that only
792
+ implement the minimum request/result protocol receive a canonical
793
+ envelope here so Studio still has one durable conversation/event model.
794
+ """
795
+
796
+ started = time.monotonic()
797
+ record.status = RunStatus.RUNNING
798
+ record.started_at = datetime.now(timezone.utc)
799
+ record.runtime_handle = {
800
+ "provider": "pluginhost",
801
+ "bundleDigest": str(spec.request_config.get("plugin_bundle_digest") or ""),
802
+ "activationKey": record.session_id,
803
+ }
804
+ self.event_store.save(record)
805
+ rows_before = await self.session_service.get_events(record.session_id)
806
+ after_seq = max((int(row.seq_id or 0) for row in rows_before), default=0)
807
+
808
+ async def publish(events: list[RuntimeEvent]) -> None:
809
+ for runtime_event in events:
810
+ event_type, data = project_runtime_event(
811
+ runtime_event,
812
+ session_id=record.session_id,
813
+ )
814
+ stored = self.event_store.append(record.id, event_type, data)
815
+ if on_event is not None:
816
+ on_event(stored)
817
+
818
+ try:
819
+ if self.plugin_runtime is None:
820
+ raise StudioError(
821
+ "PLUGIN_RUNTIME_UNAVAILABLE",
822
+ "Studio 尚未配置 PluginHost Runtime",
823
+ status_code=503,
824
+ )
825
+ request_metadata = {
826
+ key: value
827
+ for key, value in {
828
+ "tool_approval_mode": spec.request_config.get("tool_approval_mode"),
829
+ "collaboration_mode": spec.request_config.get("collaboration_mode"),
830
+ "goal_objective": spec.request_config.get("goal_objective"),
831
+ "reasoning_effort": spec.request_config.get("effort"),
832
+ }.items()
833
+ if value not in {None, ""}
834
+ }
835
+ result = await self.plugin_runtime.execute(
836
+ spec,
837
+ {
838
+ "user_id": "local-user",
839
+ "session_id": record.session_id,
840
+ "invocation_id": record.id,
841
+ "messages": [{"role": "user", "content": user_input}],
842
+ "model": spec.model,
843
+ "request_metadata": request_metadata,
844
+ },
845
+ session_id=record.session_id,
846
+ )
847
+ if result.session_id != record.session_id:
848
+ raise StudioError(
849
+ "PLUGIN_SESSION_MISMATCH",
850
+ "AgentProvider 返回了不同的 Session",
851
+ status_code=502,
852
+ details={
853
+ "expected": record.session_id,
854
+ "actual": result.session_id,
855
+ },
856
+ )
857
+ canonical = await self._plugin_events_after(
858
+ record.session_id,
859
+ after_seq=after_seq,
860
+ run_id=record.id,
861
+ )
862
+ if not canonical:
863
+ canonical = await self._persist_plugin_result_events(
864
+ record,
865
+ result.output_text,
866
+ usage=result.usage,
867
+ )
868
+ await publish(canonical)
869
+ record.output = result.output_text
870
+ record.status = RunStatus.COMPLETED
871
+ _apply_plugin_usage(record, result.usage)
872
+ except asyncio.CancelledError:
873
+ record.status = RunStatus.CANCELLED
874
+ record.error = {"code": "RUN_CANCELLED", "message": "运行已取消"}
875
+ cancelled = await self.runtime_events.append_one(
876
+ record.session_id,
877
+ RunCanceled(
878
+ **_plugin_event_envelope(record, "run.canceled"),
879
+ status="canceled",
880
+ reason="cancelled",
881
+ ),
882
+ )
883
+ await publish([cancelled])
884
+ raise
885
+ except Exception as exc: # noqa: BLE001 - plugin boundary is typed below
886
+ record.status = RunStatus.FAILED
887
+ code = str(getattr(exc, "code", "PLUGIN_RUNTIME_FAILED"))
888
+ record.error = {"code": code, "message": str(exc)}
889
+ canonical = await self._plugin_events_after(
890
+ record.session_id,
891
+ after_seq=after_seq,
892
+ run_id=record.id,
893
+ )
894
+ if canonical:
895
+ await publish(canonical)
896
+ else:
897
+ failed = await self.runtime_events.append_one(
898
+ record.session_id,
899
+ RunFailed(
900
+ **_plugin_event_envelope(record, "run.failed"),
901
+ status="failed",
902
+ error=ErrorInfo(
903
+ code=code,
904
+ message=str(exc),
905
+ source="pluginhost",
906
+ scope_id=record.id,
907
+ ),
908
+ ),
909
+ )
910
+ await publish([failed])
911
+ finally:
912
+ record.completed_at = datetime.now(timezone.utc)
913
+ record.duration_ms = int((time.monotonic() - started) * 1000)
914
+ record.duration_source = "studio"
915
+ self.event_store.save(record)
916
+ await self._sync_trace(record)
917
+ return record
918
+
919
+ async def _plugin_events_after(
920
+ self,
921
+ session_id: str,
922
+ *,
923
+ after_seq: int,
924
+ run_id: str,
925
+ ) -> list[RuntimeEvent]:
926
+ rows = await self.session_service.get_events(
927
+ session_id,
928
+ after_seq_id=after_seq,
929
+ )
930
+ events: list[RuntimeEvent] = []
931
+ for row in rows:
932
+ event = session_event_to_runtime_event(row)
933
+ if event is not None and event.run_id == run_id:
934
+ events.append(event)
935
+ return events
936
+
937
+ async def _persist_plugin_result_events(
938
+ self,
939
+ record: RunRecord,
940
+ output_text: str,
941
+ *,
942
+ usage: Mapping[str, Any],
943
+ ) -> list[RuntimeEvent]:
944
+ item_id = f"{record.id}:assistant"
945
+ part_id = f"{record.id}:text"
946
+ snapshot = ContentSnapshot(parts=(TextContent(part_id=part_id, text=output_text),))
947
+ events: list[RuntimeEvent] = [
948
+ RunStarted(
949
+ **_plugin_event_envelope(record, "run.started"),
950
+ status="running",
951
+ ),
952
+ ItemStarted(
953
+ **_plugin_event_envelope(record, "item.started"),
954
+ item_id=item_id,
955
+ item_kind="message",
956
+ phase="final_answer",
957
+ ),
958
+ ItemUpdated(
959
+ **_plugin_event_envelope(record, "item.updated"),
960
+ item_id=item_id,
961
+ item_kind="message",
962
+ op="append",
963
+ update=TextContent(part_id=part_id, text=output_text),
964
+ ),
965
+ ItemCompleted(
966
+ **_plugin_event_envelope(record, "item.completed"),
967
+ item_id=item_id,
968
+ item_kind="message",
969
+ snapshot=snapshot,
970
+ ),
971
+ ]
972
+ normalized = _normalized_plugin_usage(usage)
973
+ if normalized["reported"]:
974
+ events.append(
975
+ UsageReported(
976
+ **_plugin_event_envelope(record, "usage.reported"),
977
+ input_tokens=normalized["input_tokens"],
978
+ output_tokens=normalized["output_tokens"],
979
+ total_tokens=normalized["total_tokens"],
980
+ cached_tokens=normalized["cached_tokens"],
981
+ reasoning_tokens=normalized["reasoning_tokens"],
982
+ )
983
+ )
984
+ events.append(
985
+ RunCompleted(
986
+ **_plugin_event_envelope(record, "run.completed"),
987
+ status="completed",
988
+ output_refs=(
989
+ OutputRef(
990
+ scope_id=record.id,
991
+ item_id=item_id,
992
+ part_id=part_id,
993
+ ),
994
+ ),
995
+ )
996
+ )
997
+ return [await self.runtime_events.append_one(record.session_id, event) for event in events]
443
998
 
444
999
  async def _kernel_run(
445
1000
  self,
@@ -448,6 +1003,7 @@ class StudioRunService:
448
1003
  *,
449
1004
  record: RunRecord,
450
1005
  on_event: Callable[[RunEvent], None] | None,
1006
+ kernel_runtime: Any,
451
1007
  ) -> RunRecord:
452
1008
  """kernel 路径(灰度 opt-in):Studio run -> AgentControlCommand -> receipt。
453
1009
 
@@ -464,13 +1020,17 @@ class StudioRunService:
464
1020
  trusted = _kernel_ingress.trusted_context(
465
1021
  source_kind="studio",
466
1022
  source_ref=record.id,
1023
+ # The Worker polls the concrete Kernel Runtime instance, not
1024
+ # whichever synthetic ``local-agent`` happened to be inferred
1025
+ # from a Studio request. A Studio Build may use the Kernel
1026
+ # path only after ``_kernel_runtime_for_spec`` established that
1027
+ # it is this Runtime's exact Agent/Runtime binding.
1028
+ tenant_id=str(kernel_runtime.config.tenant_id),
1029
+ agent_instance_id=str(kernel_runtime.config.agent_instance_id),
467
1030
  session_id=record.session_id,
468
1031
  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
1032
  )
1033
+ idempotency_key = str((spec.request_config or {}).get("idempotency_key") or record.id)
474
1034
  command = _kernel_ingress.map_studio_request(
475
1035
  session_id=record.session_id,
476
1036
  idempotency_key=idempotency_key,
@@ -478,9 +1038,7 @@ class StudioRunService:
478
1038
  run_id=record.id,
479
1039
  trusted=trusted,
480
1040
  )
481
- receipt = await _kernel_ingress.submit_command(
482
- command, permit=trusted.permit
483
- )
1041
+ receipt = await _kernel_ingress.submit_command(command, permit=trusted.permit)
484
1042
  if receipt.status not in ("accepted", "duplicate"):
485
1043
  record.status = RunStatus.FAILED
486
1044
  record.error = {
@@ -513,6 +1071,7 @@ class StudioRunService:
513
1071
  self.event_store.save(record)
514
1072
  await self._sync_trace(record)
515
1073
  return record
1074
+
516
1075
  except Exception as exc: # noqa: BLE001
517
1076
  logger.exception("Studio kernel ingress failed for run %s", record.id)
518
1077
  record.status = RunStatus.FAILED
@@ -526,12 +1085,58 @@ class StudioRunService:
526
1085
  await self._sync_trace(record)
527
1086
  return record
528
1087
 
1088
+ @staticmethod
1089
+ def _kernel_runtime_for_spec(spec: StudioRunSpec) -> Any | None:
1090
+ """Return the active Kernel Runtime only for its bound Studio Build.
1091
+
1092
+ An in-process AgentKernel owns one concrete RuntimeAdapter factory and
1093
+ its immutable startup defaults. Merely observing that a Kernel is
1094
+ enabled is therefore insufficient: sending an arbitrary Studio Build
1095
+ through it can silently execute the default Adapter instead. When the
1096
+ binding is not exact, the existing direct Studio path remains the
1097
+ compatible local behaviour; it is safer than manufacturing a false
1098
+ AgentControl/Scheduler success.
1099
+ """
1100
+
1101
+ from ksadk.kernel.bootstrap import get_agent_kernel_runtime
1102
+ from ksadk.kernel.ingress import kernel_route_active
1103
+
1104
+ if not kernel_route_active():
1105
+ return None
1106
+ runtime = get_agent_kernel_runtime()
1107
+ if runtime is None:
1108
+ return None
1109
+ active_context = getattr(runtime.config, "launch_context", None)
1110
+ if active_context is None:
1111
+ return None
1112
+ if str(getattr(active_context, "runtime_type", "")).strip().lower() != (
1113
+ spec.launch_context.runtime_type.strip().lower()
1114
+ ):
1115
+ return None
1116
+ try:
1117
+ active_project = Path(active_context.project_dir).resolve()
1118
+ build_project = Path(spec.launch_context.project_dir).resolve()
1119
+ except (OSError, TypeError, ValueError):
1120
+ return None
1121
+ if active_project != build_project:
1122
+ return None
1123
+ defaults = getattr(runtime.config, "start_request_defaults", {}) or {}
1124
+ bound_agent_id = str(defaults.get("agent_id") or runtime.config.agent_instance_id).strip()
1125
+ if bound_agent_id != spec.agent_id:
1126
+ return None
1127
+ declared_instance = str(
1128
+ (spec.launch_context.config or {}).get("agent_instance_id") or ""
1129
+ ).strip()
1130
+ if declared_instance and declared_instance != str(runtime.config.agent_instance_id):
1131
+ return None
1132
+ return runtime
1133
+
529
1134
  async def cancel_run(self, run_id: str) -> dict[str, str]:
530
1135
  """Request cancellation; the flag and executor perform the actual stop."""
531
1136
  self._cancel_flags[run_id] = True
532
1137
  queue = self._control_queues.get(run_id)
533
1138
  if queue is not None:
534
- queue.put_nowait(("cancel", None))
1139
+ queue.put_nowait(("cancel", None, None))
535
1140
  handle = self._active_handles.get(run_id)
536
1141
  if handle is not None and self.executor.is_attached(handle):
537
1142
  try:
@@ -582,7 +1187,7 @@ class StudioRunService:
582
1187
  "Studio 进程已重启,当前暂停点无法恢复",
583
1188
  status_code=409,
584
1189
  )
585
- queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行")))
1190
+ queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行"), None))
586
1191
  return {"runId": run_id, "status": "resuming"}
587
1192
 
588
1193
  async def submit_interaction(
@@ -592,28 +1197,73 @@ class StudioRunService:
592
1197
  *,
593
1198
  name: str,
594
1199
  data: dict[str, Any] | None = None,
1200
+ expected_revision: int,
1201
+ idempotency_key: str,
595
1202
  ) -> 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,
1203
+ lock = self._interaction_locks.setdefault(run_id, asyncio.Lock())
1204
+ payload_data = dict(data or {})
1205
+ digest = hashlib.sha256(
1206
+ json.dumps(
1207
+ {"name": name, "data": payload_data},
1208
+ ensure_ascii=False,
1209
+ sort_keys=True,
1210
+ separators=(",", ":"),
1211
+ ).encode("utf-8")
1212
+ ).hexdigest()
1213
+ async with lock:
1214
+ return await self._submit_interaction_locked(
1215
+ run_id,
1216
+ interaction_id,
1217
+ name=name,
1218
+ data=payload_data,
1219
+ expected_revision=expected_revision,
1220
+ idempotency_key=idempotency_key,
1221
+ request_digest=digest,
602
1222
  )
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"}
1223
+
1224
+ async def _submit_interaction_locked(
1225
+ self,
1226
+ run_id: str,
1227
+ interaction_id: str,
1228
+ *,
1229
+ name: str,
1230
+ data: dict[str, Any],
1231
+ expected_revision: int,
1232
+ idempotency_key: str,
1233
+ request_digest: str,
1234
+ ) -> dict[str, Any]:
1235
+ events = self.event_store.events(run_id)
1236
+ prior = next(
1237
+ (
1238
+ event
1239
+ for event in reversed(events)
1240
+ if event.type == "a2ui.action"
1241
+ and str(event.data.get("interactionId") or "") == interaction_id
1242
+ and _interaction_revision(event.data) > 0
1243
+ ),
1244
+ None,
1245
+ )
1246
+ if prior is not None:
1247
+ if str(prior.data.get("idempotencyKey") or "") != idempotency_key:
1248
+ raise StudioError(
1249
+ "INTERACTION_ALREADY_RESOLVED",
1250
+ "该交互已由其他请求处理",
1251
+ status_code=409,
1252
+ )
1253
+ if str(prior.data.get("requestDigest") or "") != request_digest:
1254
+ raise StudioError(
1255
+ "INTERACTION_IDEMPOTENCY_CONFLICT",
1256
+ "同一幂等键不能提交不同内容",
1257
+ status_code=409,
1258
+ )
1259
+ # A receipt replay is read-only. The run may already be waiting
1260
+ # for a different interaction; never clear that newer wait state.
1261
+ return dict(prior.data.get("receipt") or {})
612
1262
 
613
1263
  interaction = next(
614
1264
  (
615
1265
  event
616
- for event in reversed(self.event_store.events(run_id))
1266
+ for event in reversed(events)
617
1267
  if event.type == "a2ui.interaction"
618
1268
  and str(event.data.get("interactionId") or event.data.get("interaction_id") or "")
619
1269
  == interaction_id
@@ -622,11 +1272,73 @@ class StudioRunService:
622
1272
  )
623
1273
  if interaction is None:
624
1274
  raise StudioError("INTERACTION_NOT_FOUND", "交互请求不存在", status_code=404)
1275
+ revision = _interaction_revision(interaction.data)
1276
+ if revision < 1:
1277
+ raise StudioError(
1278
+ "INTERACTION_READ_ONLY",
1279
+ "交互请求缺少权威 revision,只能查看不能操作",
1280
+ status_code=409,
1281
+ )
1282
+ if expected_revision != revision:
1283
+ raise StudioError(
1284
+ "INTERACTION_REVISION_MISMATCH",
1285
+ "交互版本已变化,请刷新后重试",
1286
+ status_code=409,
1287
+ details={"expectedRevision": expected_revision, "actualRevision": revision},
1288
+ )
1289
+ record = self.event_store.get(run_id)
1290
+ if record.status != RunStatus.WAITING_INPUT:
1291
+ raise StudioError(
1292
+ "INTERACTION_NOT_PENDING",
1293
+ "该 Run 当前没有等待中的交互",
1294
+ status_code=409,
1295
+ )
625
1296
  kind = str(interaction.data.get("kind") or "form")
626
- payload_data = {"decision": name, **dict(data or {})}
1297
+ if kind != "approval" and name not in {"cancel", "skip"}:
1298
+ schema = interaction.data.get("inputSchema") or {}
1299
+ errors = list(Draft202012Validator(schema).iter_errors(data))
1300
+ if errors:
1301
+ raise StudioError(
1302
+ "INTERACTION_RESPONSE_INVALID",
1303
+ "请填写必填问题后再提交。",
1304
+ status_code=422,
1305
+ )
1306
+ provider_call_id = str(
1307
+ interaction.data.get("callId") or interaction.data.get("call_id") or ""
1308
+ )
1309
+ if kind == "approval" and not provider_call_id:
1310
+ approval_request = next(
1311
+ (
1312
+ event
1313
+ for event in reversed(events)
1314
+ if event.type == "approval.requested"
1315
+ and str(event.data.get("approvalId") or event.data.get("interactionId") or "")
1316
+ == interaction_id
1317
+ ),
1318
+ None,
1319
+ )
1320
+ if approval_request is not None:
1321
+ provider_call_id = str(
1322
+ approval_request.data.get("callId")
1323
+ or approval_request.data.get("call_id")
1324
+ or ""
1325
+ )
1326
+ provider_call_id = provider_call_id or interaction_id
1327
+ submitted = self._submitted_interactions.setdefault(run_id, set())
1328
+ submitted.update({interaction_id, provider_call_id})
1329
+ decision = str(data.get("decision") or name)
1330
+ if kind == "approval" and record.runtime_type == "codex":
1331
+ # The shared Web action is named `reject`; Codex app-server calls
1332
+ # the same decision `deny`/`decline`. Do not let a UI alias fall
1333
+ # through as an unsupported native approval decision.
1334
+ decision = {
1335
+ "reject": "deny",
1336
+ "rejected": "deny",
1337
+ }.get(decision.lower(), decision)
1338
+ payload_data = {**data, "decision": decision}
627
1339
  payload = ResumePayload(
628
1340
  kind="approval_decision" if kind == "approval" else "hitl_answer",
629
- call_id=interaction_id,
1341
+ call_id=provider_call_id,
630
1342
  data=payload_data,
631
1343
  )
632
1344
  mode = self._waiting_modes.get(run_id)
@@ -635,8 +1347,19 @@ class StudioRunService:
635
1347
  if handle is None or not self.executor.is_attached(handle):
636
1348
  raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
637
1349
  try:
638
- await self.executor.submit(handle, payload)
639
- except (RuntimeError, ValueError) as exc:
1350
+ await asyncio.wait_for(
1351
+ self.executor.submit(handle, payload),
1352
+ timeout=_INTERACTION_SUBMIT_TIMEOUT_SECONDS,
1353
+ )
1354
+ except TimeoutError as exc:
1355
+ submitted.difference_update({interaction_id, provider_call_id})
1356
+ raise StudioError(
1357
+ "INTERACTION_SUBMIT_FAILED",
1358
+ "运行时未及时确认交互提交,请重试",
1359
+ status_code=409,
1360
+ ) from exc
1361
+ except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
1362
+ submitted.difference_update({interaction_id, provider_call_id})
640
1363
  raise StudioError(
641
1364
  "INTERACTION_SUBMIT_FAILED",
642
1365
  str(exc),
@@ -646,45 +1369,143 @@ class StudioRunService:
646
1369
  queue = self._control_queues.get(run_id)
647
1370
  if queue is None:
648
1371
  raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
649
- queue.put_nowait(("resume", payload))
1372
+ submit_ack = asyncio.get_running_loop().create_future()
1373
+ queue.put_nowait(("resume", payload, submit_ack))
1374
+ try:
1375
+ await asyncio.wait_for(
1376
+ submit_ack,
1377
+ timeout=_INTERACTION_SUBMIT_TIMEOUT_SECONDS,
1378
+ )
1379
+ except TimeoutError as exc:
1380
+ submitted.difference_update({interaction_id, provider_call_id})
1381
+ raise StudioError(
1382
+ "INTERACTION_SUBMIT_FAILED",
1383
+ "运行时未及时确认交互提交,请重试",
1384
+ status_code=409,
1385
+ ) from exc
1386
+ except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
1387
+ submitted.difference_update({interaction_id, provider_call_id})
1388
+ raise StudioError(
1389
+ "INTERACTION_SUBMIT_FAILED",
1390
+ str(exc),
1391
+ status_code=409,
1392
+ ) from exc
650
1393
  else:
1394
+ submitted.difference_update({interaction_id, provider_call_id})
651
1395
  raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
652
1396
 
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(
1397
+ next_revision = revision + 1
1398
+ normalized_action = name.lower()
1399
+ if normalized_action in {"approve", "approved", "accept", "accepted"}:
1400
+ outcome = "approved"
1401
+ response_summary = "已同意"
1402
+ elif normalized_action in {"reject", "rejected", "deny", "denied", "decline"}:
1403
+ outcome = "rejected"
1404
+ response_summary = "已拒绝"
1405
+ elif normalized_action in {"cancel", "cancelled", "canceled"}:
1406
+ outcome = "cancelled"
1407
+ response_summary = "已反馈给 Agent" if data.get("feedback") else "已取消"
1408
+ else:
1409
+ outcome = "submitted"
1410
+ response_summary = "已提交"
1411
+ resolved_data = {
1412
+ "runId": run_id,
1413
+ "interactionId": interaction_id,
1414
+ "callId": provider_call_id,
1415
+ "name": name,
1416
+ "action": name,
1417
+ "outcome": outcome,
1418
+ "actor": "user",
1419
+ "responseSummary": response_summary,
1420
+ "data": data,
1421
+ "revision": next_revision,
1422
+ "expectedRevision": expected_revision,
1423
+ "idempotencyKey": idempotency_key,
1424
+ }
1425
+ action_data = {
1426
+ "runId": run_id,
1427
+ "surfaceId": str(
1428
+ interaction.data.get("surfaceId") or interaction.data.get("surface_id") or ""
1429
+ ),
1430
+ "interactionId": interaction_id,
1431
+ "callId": provider_call_id,
1432
+ "actionId": f"action-{interaction_id}",
1433
+ "name": name,
1434
+ "data": data,
1435
+ "revision": next_revision,
1436
+ "expectedRevision": expected_revision,
1437
+ "idempotencyKey": idempotency_key,
1438
+ "requestDigest": request_digest,
1439
+ }
1440
+ _, _, receipt = self.event_store.append_interaction_resolution(
665
1441
  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
- },
1442
+ resolved_type=("approval.resolved" if kind == "approval" else "interaction.resolved"),
1443
+ resolved_data=resolved_data,
1444
+ action_data=action_data,
677
1445
  )
678
1446
  record.status = RunStatus.RUNNING
679
1447
  self.event_store.save(record)
680
1448
  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,
1449
+ return receipt
1450
+
1451
+ def _expire_unresolved_interactions(
1452
+ self,
1453
+ record: RunRecord,
1454
+ *,
1455
+ on_event: Callable[[RunEvent], None] | None = None,
1456
+ ) -> None:
1457
+ """Close orphaned cards whenever their owning run is terminal."""
1458
+
1459
+ if record.status not in {
1460
+ RunStatus.COMPLETED,
1461
+ RunStatus.FAILED,
1462
+ RunStatus.CANCELLED,
1463
+ RunStatus.INTERRUPTED,
1464
+ RunStatus.TIMED_OUT,
1465
+ }:
1466
+ return
1467
+ events = self.event_store.events(record.id)
1468
+ closed_ids = {
1469
+ str(event.data.get("interactionId") or event.data.get("approvalId") or "")
1470
+ for event in events
1471
+ if event.type
1472
+ in {
1473
+ "a2ui.action",
1474
+ "approval.resolved",
1475
+ "interaction.resolved",
1476
+ "interaction.cancelled",
1477
+ "interaction.expired",
1478
+ }
687
1479
  }
1480
+ pending: dict[str, RunEvent] = {}
1481
+ for event in events:
1482
+ if event.type != "a2ui.interaction":
1483
+ continue
1484
+ interaction_id = str(
1485
+ event.data.get("interactionId") or event.data.get("interaction_id") or ""
1486
+ )
1487
+ if interaction_id and interaction_id not in closed_ids:
1488
+ pending[interaction_id] = event
1489
+ for interaction_id, requested in pending.items():
1490
+ data = requested.data
1491
+ expired = self.event_store.append(
1492
+ record.id,
1493
+ "interaction.expired",
1494
+ {
1495
+ "runId": record.id,
1496
+ "interactionId": interaction_id,
1497
+ "approvalId": interaction_id,
1498
+ "callId": str(data.get("callId") or data.get("call_id") or ""),
1499
+ "kind": str(data.get("kind") or "form"),
1500
+ "title": data.get("title"),
1501
+ "message": data.get("message"),
1502
+ "detail": data.get("detail"),
1503
+ "outcome": "expired",
1504
+ "revision": _interaction_revision(data) + 1,
1505
+ },
1506
+ )
1507
+ if on_event is not None:
1508
+ on_event(expired)
688
1509
 
689
1510
  async def _capture_pcm_evidence(
690
1511
  self,
@@ -830,7 +1651,12 @@ class StudioRunService:
830
1651
  "runId": record.id,
831
1652
  "surfaceId": surface_id,
832
1653
  "interactionId": approval_id,
1654
+ "callId": event.request.call_id or approval_id,
833
1655
  "kind": "approval",
1656
+ "title": "需要审批",
1657
+ "message": "此次操作需要确认后才能继续。",
1658
+ "detail": detail,
1659
+ "revision": 1,
834
1660
  "inputSchema": {
835
1661
  "type": "object",
836
1662
  "properties": {
@@ -867,21 +1693,120 @@ class StudioRunService:
867
1693
  agent_id: str,
868
1694
  session_id: str,
869
1695
  runtime_type: str,
1696
+ build_id: str,
870
1697
  ) -> dict[str, str]:
871
1698
  if runtime_type != "codex":
872
1699
  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
1700
+ runs = self.event_store.list_runs(session_id=session_id, agent_id=agent_id)
1701
+ thread_builds: dict[str, str] = {}
1702
+ for previous in runs:
1703
+ native_ref = previous.runtime_handle.get("native_ref") or {}
1704
+ if isinstance(native_ref, dict) and native_ref.get("thread_id"):
1705
+ # Older Studio versions also saved failed cross-build resumes.
1706
+ # The first occurrence identifies the thread's actual home.
1707
+ thread_builds.setdefault(str(native_ref["thread_id"]), previous.build_id)
1708
+ for previous in reversed(runs):
1709
+ # Each immutable build has its own CODEX_HOME and plugin snapshot.
1710
+ # Starting a thread in that home applies the new capabilities; the
1711
+ # transport-neutral history above preserves the Studio conversation.
1712
+ if previous.build_id != build_id:
1713
+ return {}
876
1714
  native_ref = previous.runtime_handle.get("native_ref")
877
1715
  native_ref = native_ref if isinstance(native_ref, dict) else {}
878
1716
  thread_id = str(native_ref.get("thread_id") or "")
879
1717
  if thread_id:
1718
+ if thread_builds[thread_id] != build_id:
1719
+ return {}
880
1720
  return {"thread_id": thread_id}
881
1721
  return {}
882
1722
 
883
1723
 
884
- def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
1724
+ def _plugin_event_envelope(record: RunRecord, event_type: str) -> dict[str, Any]:
1725
+ return {
1726
+ "schema_version": 2,
1727
+ "event_id": f"{record.id}:{event_type}",
1728
+ "seq": 0,
1729
+ "timestamp": time.time(),
1730
+ "run_id": record.id,
1731
+ "scope_id": record.id,
1732
+ "source": SourceRef(
1733
+ framework="ksadk",
1734
+ metadata={
1735
+ "runtime": "pluginhost",
1736
+ "agent_id": record.agent_id,
1737
+ "session_id": record.session_id,
1738
+ "build_id": record.build_id,
1739
+ },
1740
+ ),
1741
+ }
1742
+
1743
+
1744
+ def _normalized_plugin_usage(usage: Mapping[str, Any]) -> dict[str, Any]:
1745
+ def number(*keys: str) -> int:
1746
+ for key in keys:
1747
+ value = usage.get(key)
1748
+ if value is not None:
1749
+ try:
1750
+ return max(0, int(value))
1751
+ except (TypeError, ValueError):
1752
+ return 0
1753
+ return 0
1754
+
1755
+ normalized = {
1756
+ "input_tokens": number("input_tokens", "inputTokens", "prompt_tokens"),
1757
+ "output_tokens": number("output_tokens", "outputTokens", "completion_tokens"),
1758
+ "total_tokens": number("total_tokens", "totalTokens"),
1759
+ "cached_tokens": number("cached_tokens", "cachedTokens"),
1760
+ "reasoning_tokens": number("reasoning_tokens", "reasoningTokens"),
1761
+ }
1762
+ if not normalized["total_tokens"]:
1763
+ normalized["total_tokens"] = normalized["input_tokens"] + normalized["output_tokens"]
1764
+ normalized["reported"] = bool(usage) and any(
1765
+ key in usage
1766
+ for key in (
1767
+ "input_tokens",
1768
+ "inputTokens",
1769
+ "prompt_tokens",
1770
+ "output_tokens",
1771
+ "outputTokens",
1772
+ "completion_tokens",
1773
+ "total_tokens",
1774
+ "totalTokens",
1775
+ )
1776
+ )
1777
+ return normalized
1778
+
1779
+
1780
+ def _interaction_revision(data: Mapping[str, Any]) -> int:
1781
+ """Treat malformed or historical interaction revisions as read-only."""
1782
+
1783
+ try:
1784
+ return max(0, int(data.get("revision") or 0))
1785
+ except (TypeError, ValueError):
1786
+ return 0
1787
+
1788
+
1789
+ def _apply_plugin_usage(record: RunRecord, usage: Mapping[str, Any]) -> None:
1790
+ normalized = _normalized_plugin_usage(usage)
1791
+ if not normalized["reported"]:
1792
+ return
1793
+ record.usage = Usage(
1794
+ input_tokens=normalized["input_tokens"],
1795
+ output_tokens=normalized["output_tokens"],
1796
+ total_tokens=normalized["total_tokens"],
1797
+ cached_input_tokens=normalized["cached_tokens"],
1798
+ reasoning_output_tokens=normalized["reasoning_tokens"],
1799
+ reported=True,
1800
+ source="pluginhost",
1801
+ )
1802
+
1803
+
1804
+ def project_runtime_event(
1805
+ event: RuntimeEvent,
1806
+ *,
1807
+ session_id: str | None = None,
1808
+ public_run_id: str | None = None,
1809
+ ) -> tuple[str, dict[str, Any]]:
885
1810
  """Project the canonical RuntimeEvent into Studio's persisted event view.
886
1811
 
887
1812
  公开承诺字段(契约声明见 ``ksadk/events/projections.py``,执行形态为
@@ -1005,7 +1930,9 @@ def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
1005
1930
  payload = {
1006
1931
  "interactionId": event.interaction_id,
1007
1932
  "kind": "form",
1008
- "inputSchema": {},
1933
+ "inputSchema": event.request.schema_,
1934
+ "message": event.request.prompt or "",
1935
+ "callId": event.source.native_event_id or event.interaction_id,
1009
1936
  }
1010
1937
  elif isinstance(event, InteractionResolved):
1011
1938
  if event.interaction_kind == "approval":
@@ -1045,13 +1972,22 @@ def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
1045
1972
  payload = {}
1046
1973
 
1047
1974
  _attach_studio_identity(payload, event)
1975
+ if public_run_id and public_run_id != event.run_id:
1976
+ payload["runtimeRunId"] = event.run_id
1977
+ payload["runId"] = public_run_id
1048
1978
  payload["runtimeEvent"] = dump_runtime_event(event)
1979
+ # Additive only: legacy Studio/Web consumers keep reading the established
1980
+ # event type and payload keys. New surfaces may opt into this typed,
1981
+ # identity-aware representation without reconstructing one from text.
1982
+ payload["conversationItem"] = project_conversation_item(
1983
+ event,
1984
+ session_id=session_id,
1985
+ run_id=public_run_id,
1986
+ ).model_dump(by_alias=True, exclude_none=True, mode="json")
1049
1987
  return projected, payload
1050
1988
 
1051
1989
 
1052
- def _first_content(
1053
- snapshot: ContentSnapshot | None, content_type: type
1054
- ) -> Any | None:
1990
+ def _first_content(snapshot: ContentSnapshot | None, content_type: type) -> Any | None:
1055
1991
  if snapshot is None:
1056
1992
  return None
1057
1993
  for part in snapshot.parts:
@@ -1073,6 +2009,11 @@ def _project_a2ui_surface(
1073
2009
  projected = "a2ui.surface.end"
1074
2010
  data_parts = event.snapshot.parts
1075
2011
 
2012
+ if event.source.metadata.get("operation_batch") is True and isinstance(event, ItemCompleted):
2013
+ # Completion closes the immutable canonical batch; it is not a request
2014
+ # to remove the surface that the batch just created.
2015
+ projected = "a2ui.surface.begin"
2016
+
1076
2017
  surface_id = str(event.source.metadata.get("surface_id") or "")
1077
2018
  operations: list[dict[str, Any]] = []
1078
2019
  for part in data_parts:
@@ -1080,6 +2021,8 @@ def _project_a2ui_surface(
1080
2021
  data = part.data
1081
2022
  if isinstance(data, list):
1082
2023
  operations.extend(dict(op) for op in data if isinstance(op, Mapping))
2024
+ elif isinstance(data, Mapping):
2025
+ operations.extend(project_a2ui_operations(projected, data))
1083
2026
  if not operations:
1084
2027
  operations = project_a2ui_operations(projected, {"surface_id": surface_id})
1085
2028
  payload: dict[str, Any] = {
@@ -1122,6 +2065,44 @@ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
1122
2065
  """Session envelope -> Studio RunEvent 投影;cursor 仍用 envelope.seq。"""
1123
2066
 
1124
2067
  payload = envelope.payload or {}
2068
+ # Kernel RuntimeEvent is persisted as a typed runtime/v2 SessionEvent
2069
+ # envelope. Reuse the exact same projector as the direct Studio path so
2070
+ # a consumer receives its additive ConversationItem regardless of which
2071
+ # ingress delivered the Run. Legacy event families retain the historical
2072
+ # text-only fallback below.
2073
+ if envelope.family == "runtime" and envelope.family_version == 2:
2074
+ try:
2075
+ runtime_event = parse_runtime_event(payload)
2076
+ except (TypeError, ValueError):
2077
+ # A damaged/unknown runtime payload must not make old Studio
2078
+ # subscriptions fail. Preserve its event type for legacy
2079
+ # observability while refusing to invent a typed conversation item.
2080
+ return envelope.event_type, {"runId": envelope.run_id or ""}
2081
+ return project_runtime_event(runtime_event, session_id=envelope.session_id)
2082
+ if envelope.family == "interaction" and envelope.family_version == 1:
2083
+ conversation_item = project_interaction_conversation_item(envelope)
2084
+ if conversation_item is None:
2085
+ # Keep a malformed durable fact observable without reusing an
2086
+ # actionable legacy event name. Existing Studio reducers turn
2087
+ # ``approval.requested`` into a submit button, so emitting that
2088
+ # name without an authoritative revision would bypass the typed
2089
+ # ConversationItem fail-closed boundary.
2090
+ payload = envelope.payload or {}
2091
+ return envelope.event_type, {
2092
+ "runId": envelope.run_id or str(payload.get("run_id") or ""),
2093
+ "itemId": str(payload.get("interaction_id") or envelope.event_id),
2094
+ "interactionReadOnly": True,
2095
+ }
2096
+ projected = _project_interaction_envelope_legacy(envelope)
2097
+ if projected is None:
2098
+ return None
2099
+ event_type, data = projected
2100
+ data["conversationItem"] = conversation_item.model_dump(
2101
+ by_alias=True,
2102
+ exclude_none=True,
2103
+ mode="json",
2104
+ )
2105
+ return event_type, data
1125
2106
  if envelope.event_type == "run.completed":
1126
2107
  return "run.completed", {
1127
2108
  "runId": envelope.run_id or "",
@@ -1131,3 +2112,70 @@ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
1131
2112
  if text:
1132
2113
  return "message.delta", {"delta": text}
1133
2114
  return None
2115
+
2116
+
2117
+ def _project_interaction_envelope_legacy(
2118
+ envelope,
2119
+ ) -> tuple[str, dict[str, Any]] | None:
2120
+ """Keep established Studio event names while Interaction/v1 owns truth."""
2121
+
2122
+ payload = envelope.payload or {}
2123
+ interaction_id = str(payload.get("interaction_id") or "")
2124
+ interaction_kind = str(payload.get("kind") or "")
2125
+ revision = payload.get("revision")
2126
+ request = payload.get("request")
2127
+ request = request if isinstance(request, Mapping) else {}
2128
+ presentation = request.get("presentation")
2129
+ presentation = presentation if isinstance(presentation, Mapping) else {}
2130
+ common: dict[str, Any] = {
2131
+ "runId": envelope.run_id or str(payload.get("run_id") or ""),
2132
+ "itemId": interaction_id,
2133
+ "interactionId": interaction_id,
2134
+ }
2135
+ # Expose the value as received for legacy/read-only diagnostics. Only the
2136
+ # typed ConversationItem validates it as an authoritative writable token.
2137
+ if revision is not None:
2138
+ common["revision"] = revision
2139
+
2140
+ if envelope.event_type == "interaction.requested":
2141
+ if interaction_kind == "approval":
2142
+ return "approval.requested", {
2143
+ **common,
2144
+ "approvalId": interaction_id,
2145
+ "callId": "",
2146
+ "kind": str(presentation.get("title") or interaction_kind),
2147
+ "detail": presentation.get("description"),
2148
+ }
2149
+ return "a2ui.interaction", {
2150
+ **common,
2151
+ "kind": interaction_kind or "form",
2152
+ "inputSchema": (
2153
+ dict(request.get("request_schema"))
2154
+ if isinstance(request.get("request_schema"), Mapping)
2155
+ else {}
2156
+ ),
2157
+ }
2158
+ if envelope.event_type in {
2159
+ "interaction.resolved",
2160
+ "interaction.cancelled",
2161
+ "interaction.expired",
2162
+ }:
2163
+ outcome = str(
2164
+ payload.get("outcome")
2165
+ or (
2166
+ "cancelled"
2167
+ if envelope.event_type == "interaction.cancelled"
2168
+ else "expired"
2169
+ if envelope.event_type == "interaction.expired"
2170
+ else ""
2171
+ )
2172
+ )
2173
+ if interaction_kind == "approval":
2174
+ return "approval.resolved", {
2175
+ **common,
2176
+ "approvalId": interaction_id,
2177
+ "callId": "",
2178
+ "decision": outcome,
2179
+ }
2180
+ return "a2ui.action", {**common, "name": outcome}
2181
+ return None