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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +18 -25
  2. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +224 -169
  3. ksadk/_build_provenance.json +1 -0
  4. ksadk/agui/_agent_helpers.py +4 -0
  5. ksadk/api/client.py +3 -0
  6. ksadk/builders/managed_runtime_builder.py +3 -0
  7. ksadk/cli/__init__.py +6 -0
  8. ksadk/cli/cmd_plugin.py +657 -0
  9. ksadk/codex/runtime.py +50 -8
  10. ksadk/configs/env_registry.py +31 -3
  11. ksadk/configs/settings.py +9 -0
  12. ksadk/context_engine/contributors.py +73 -8
  13. ksadk/conversations/__init__.py +19 -0
  14. ksadk/conversations/context.py +104 -24
  15. ksadk/conversations/contracts.py +266 -0
  16. ksadk/conversations/message_projection.py +19 -1
  17. ksadk/conversations/projector.py +616 -0
  18. ksadk/conversations/reducer.py +98 -0
  19. ksadk/events/_v1_compat/projection.py +2 -0
  20. ksadk/events/adapters/codex.py +190 -0
  21. ksadk/harness/__init__.py +45 -35
  22. ksadk/harness/runtime.py +60 -1
  23. ksadk/kernel/contract_fingerprints.py +5 -4
  24. ksadk/kernel/contracts.py +8 -0
  25. ksadk/kernel/control.py +1 -0
  26. ksadk/kernel/ingress.py +21 -0
  27. ksadk/kernel/memory_store.py +1 -0
  28. ksadk/kernel/postgres_store.py +1 -0
  29. ksadk/kernel/sqlite_store.py +1 -0
  30. ksadk/kernel/worker.py +11 -0
  31. ksadk/memory/providers/local_sqlite.py +7 -0
  32. ksadk/plugins/__init__.py +167 -0
  33. ksadk/plugins/bridges/__init__.py +45 -0
  34. ksadk/plugins/bridges/codex.py +552 -0
  35. ksadk/plugins/bridges/dsh.py +1116 -0
  36. ksadk/plugins/builtins.py +747 -0
  37. ksadk/plugins/bundle.py +246 -0
  38. ksadk/plugins/bundle_security.py +167 -0
  39. ksadk/plugins/composition.py +529 -0
  40. ksadk/plugins/context_contributor.py +437 -0
  41. ksadk/plugins/contracts.py +451 -0
  42. ksadk/plugins/dsh_toolchain.py +935 -0
  43. ksadk/plugins/ecosystem_bridge.py +1105 -0
  44. ksadk/plugins/ecosystem_probe.py +191 -0
  45. ksadk/plugins/host.py +724 -0
  46. ksadk/plugins/providers/__init__.py +85 -0
  47. ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
  48. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
  49. ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
  50. ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
  51. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
  52. ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
  53. ksadk/plugins/providers/codex.py +722 -0
  54. ksadk/plugins/providers/codex_dsh.py +83 -0
  55. ksadk/plugins/providers/dsh.py +951 -0
  56. ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
  57. ksadk/plugins/providers/harness.py +627 -0
  58. ksadk/plugins/providers/harness_dsh.py +91 -0
  59. ksadk/plugins/providers/legacy.py +215 -0
  60. ksadk/plugins/providers/legacy_catalog.py +100 -0
  61. ksadk/plugins/providers/shipped_dsh.py +370 -0
  62. ksadk/plugins/resolver.py +331 -0
  63. ksadk/plugins/subagent_providers/__init__.py +12 -0
  64. ksadk/plugins/subagent_providers/codex.py +443 -0
  65. ksadk/plugins/subagents.py +333 -0
  66. ksadk/runtime/_runner_adapter/stream_mapping.py +51 -42
  67. ksadk/runtime/adapter.py +1 -0
  68. ksadk/runtime/factory.py +76 -2
  69. ksadk/runtime/framework_adapters.py +18 -0
  70. ksadk/runtime/runner_adapter.py +5 -0
  71. ksadk/sandbox/__init__.py +2 -1
  72. ksadk/sandbox/factory.py +72 -0
  73. ksadk/scheduler/__init__.py +18 -0
  74. ksadk/scheduler/calendar.py +102 -0
  75. ksadk/scheduler/contracts.py +254 -0
  76. ksadk/scheduler/dispatcher.py +129 -0
  77. ksadk/scheduler/engine.py +416 -0
  78. ksadk/scheduler/sqlite_store.py +563 -0
  79. ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
  80. ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-BXk1l-G1.js} +1 -1
  81. ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-D9uB55Oh.js} +1 -1
  82. ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock--OEYoXIJ.js} +6 -6
  83. ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-DrEe1syq.js} +1 -1
  84. ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-eeTFJLvo.js} +1 -1
  85. ksadk/server/static/assets/{arc-C4FzinUA.js → arc-S3ZNbIAd.js} +1 -1
  86. ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
  87. ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-CRjgEJDX.js} +1 -1
  88. ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-C7bsBoNH.js} +1 -1
  89. ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-BX3c33Ob.js} +1 -1
  90. ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
  91. ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
  92. ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
  93. ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
  94. ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-CZwF3oUP.js} +1 -1
  95. ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
  96. ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-CnorGV5q.js} +1 -1
  97. ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CexntQA-.js} +1 -1
  98. ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-CHTC7UxA.js} +1 -1
  99. ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-BC1EWt_-.js} +1 -1
  100. ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-BTG24xN0.js} +1 -1
  101. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
  102. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
  103. ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
  104. ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
  105. ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-DEOCBe15.js} +1 -1
  106. ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-DBThPuLa.js} +1 -1
  107. ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-ClTPrTht.js} +1 -1
  108. ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-DsZHrnpJ.js} +1 -1
  109. ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-BJR4nhVr.js} +1 -1
  110. ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-xzcERX0a.js} +1 -1
  111. ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-tn2xM1xu.js} +1 -1
  112. ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-BZPGBkw5.js} +1 -1
  113. ksadk/server/static/assets/{dist-ClxGviKw.js → dist-4zN1_tq5.js} +1 -1
  114. ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-8VWI-2bZ.js} +1 -1
  115. ksadk/server/static/assets/{dist-BX8z72IC.js → dist-B-46q3Hk.js} +1 -1
  116. ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-B0gdk4rl.js} +1 -1
  117. ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-B1oWRmrH.js} +1 -1
  118. ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-BI1qQH4n.js} +1 -1
  119. ksadk/server/static/assets/{dist-dydu68Fl.js → dist-BJLUwH6W.js} +1 -1
  120. ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-Bbwqa8TG.js} +1 -1
  121. ksadk/server/static/assets/{dist-CttYUqzS.js → dist-Btyo4vX4.js} +1 -1
  122. ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C71CsujO.js} +1 -1
  123. ksadk/server/static/assets/{dist-dDdADKFA.js → dist-CXpH_Rdd.js} +1 -1
  124. ksadk/server/static/assets/{dist-CSh_DE14.js → dist-Cq4OL6nO.js} +1 -1
  125. ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-D1V3f1B4.js} +1 -1
  126. ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-DJmCPRVB.js} +1 -1
  127. ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-DQAB0qn_.js} +1 -1
  128. ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DUwKa_l6.js} +1 -1
  129. ksadk/server/static/assets/{dist-B7seoj3d.js → dist-DdRBIceA.js} +1 -1
  130. ksadk/server/static/assets/{dist-BytlxIDT.js → dist-DfGFrb0v.js} +1 -1
  131. ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-DjhwGUgZ.js} +1 -1
  132. ksadk/server/static/assets/{dist-W-TIVntx.js → dist-DyqV4fL4.js} +1 -1
  133. ksadk/server/static/assets/{dist-DUX-id_F.js → dist-M67Hwu0n.js} +1 -1
  134. ksadk/server/static/assets/{dist-DmldrHd_.js → dist-u8tVH5Ak.js} +1 -1
  135. ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-DVfMsLge.js} +1 -1
  136. ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CWEZSYaw.js} +1 -1
  137. ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
  138. ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
  139. ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-TWIxoyYL.js} +1 -1
  140. ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
  141. ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-DDh2hd2S.js} +1 -1
  142. ksadk/server/static/assets/index-B2k_urY8.js +241 -0
  143. ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
  144. ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
  145. ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CgoyWiDR.js} +1 -1
  146. ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +1 -1
  147. ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +1 -1
  148. ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CNP3ui66.js} +1 -1
  149. ksadk/server/static/assets/{katex-vFWytM5c.js → katex-DWEzQy1d.js} +1 -1
  150. ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-C-qHp7d2.js} +1 -1
  151. ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-Cl-K943T.js} +2 -2
  152. ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +1 -1
  153. ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
  154. ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-BQVc9ia_.js} +1 -1
  155. ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
  156. ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-B6UCAvtY.js} +1 -1
  157. ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +1 -1
  158. ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
  159. ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
  160. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
  161. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
  162. ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
  163. ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-Dt3FFDmK.js} +1 -1
  164. ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-Cz_vQtgF.js} +1 -1
  165. ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +1 -1
  166. ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +1 -1
  167. ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DXX54iyO.js} +1 -1
  168. ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-Dp_lw9II.js} +1 -1
  169. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
  170. ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-D8O5UlLG.js} +1 -1
  171. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
  172. ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +1 -1
  173. ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
  174. ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
  175. ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-EnNl0DE7.js} +1 -1
  176. ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
  177. ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-DBu5fbYr.js} +1 -1
  178. ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-BSVXs4IY.js} +1 -1
  179. ksadk/server/static/index.html +2 -2
  180. ksadk/skills/service_client.py +48 -0
  181. ksadk/studio/agent_lifecycle.py +26 -20
  182. ksadk/studio/api.py +513 -71
  183. ksadk/studio/api_catalog_routes.py +5 -0
  184. ksadk/studio/api_contracts.py +34 -1
  185. ksadk/studio/api_plugin_routes.py +480 -0
  186. ksadk/studio/attachment_store.py +187 -0
  187. ksadk/studio/authoring.py +10 -4
  188. ksadk/studio/authoring_coordinator.py +12 -2
  189. ksadk/studio/builder.py +123 -43
  190. ksadk/studio/cloud.py +4 -0
  191. ksadk/studio/codex_agent_service.py +12 -1
  192. ksadk/studio/codex_builder.py +54 -15
  193. ksadk/studio/codex_manifest.py +21 -1
  194. ksadk/studio/codex_run.py +16 -4
  195. ksadk/studio/compatibility_report.py +445 -0
  196. ksadk/studio/compiler.py +6 -1
  197. ksadk/studio/contracts.py +123 -4
  198. ksadk/studio/dsh_provider_registration.py +768 -0
  199. ksadk/studio/event_store.py +54 -6
  200. ksadk/studio/framework_run.py +37 -4
  201. ksadk/studio/hosted_kernel.py +1 -1
  202. ksadk/studio/operations.py +12 -2
  203. ksadk/studio/plugin_composition.py +301 -0
  204. ksadk/studio/plugin_kernel_adapter.py +128 -0
  205. ksadk/studio/plugin_runtime.py +652 -0
  206. ksadk/studio/resource_catalog.py +6 -1
  207. ksadk/studio/run_service.py +713 -75
  208. ksadk/studio/runtime_source.py +9 -1
  209. ksadk/studio/scheduler_runtime.py +374 -0
  210. ksadk/studio/scheduler_service.py +211 -0
  211. ksadk/studio/service.py +672 -82
  212. ksadk/studio/shared_web.py +167 -28
  213. ksadk/studio/soul.py +52 -0
  214. ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
  215. ksadk/studio/static/assets/index-DeDqwTsD.js +283 -0
  216. ksadk/studio/static/index.html +2 -2
  217. ksadk/studio/validator.py +1 -1
  218. ksadk/studio/workspace.py +1 -0
  219. ksadk/version.py +1 -1
  220. ksadk_runtime_common/workspace_files/path_utils.py +41 -0
  221. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
  222. ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
  223. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
  224. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
  225. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
  226. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
  227. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
  228. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
  229. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
  230. ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
  231. ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
  232. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
  233. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
  234. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
  235. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
  236. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
  237. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
  238. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
  239. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
  240. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
  241. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
  242. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
  243. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
  244. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
  245. ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
  246. ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
  247. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/WHEEL +0 -0
  248. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
  249. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
  250. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
@@ -3,15 +3,22 @@
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
 
14
17
  from ksadk.agui.a2ui_projection import project_a2ui_operations
18
+ from ksadk.conversations.projector import (
19
+ project_conversation_item,
20
+ project_interaction_conversation_item,
21
+ )
15
22
  from ksadk.events.canonical import (
16
23
  ContinuationCreated,
17
24
  ContinuationResumed,
@@ -22,6 +29,7 @@ from ksadk.events.canonical import (
22
29
  ItemFailed,
23
30
  ItemStarted,
24
31
  ItemUpdated,
32
+ OutputRef,
25
33
  RunCanceled,
26
34
  RunCompleted,
27
35
  RunFailed,
@@ -32,7 +40,9 @@ from ksadk.events.canonical import (
32
40
  SourceRef,
33
41
  UsageReported,
34
42
  dump_runtime_event,
43
+ parse_runtime_event,
35
44
  )
45
+ from ksadk.events.canonical_store import session_event_to_runtime_event
36
46
  from ksadk.events.content import (
37
47
  ContentSnapshot,
38
48
  DataContent,
@@ -73,6 +83,7 @@ class StudioRunSpec:
73
83
  model: str | None = None
74
84
  request_config: Mapping[str, Any] = field(default_factory=dict)
75
85
  manifest_sha256: str = ""
86
+ plugin_bundle_root: Path | None = None
76
87
 
77
88
 
78
89
  class StudioRunService:
@@ -86,6 +97,7 @@ class StudioRunService:
86
97
  event_store: RunEventStore | None = None,
87
98
  session_service: BaseSessionService | None = None,
88
99
  runtime_events: RuntimeEventStore | None = None,
100
+ plugin_runtime: Any | None = None,
89
101
  ) -> None:
90
102
  self.workspace = workspace
91
103
  self.executor = executor
@@ -94,10 +106,18 @@ class StudioRunService:
94
106
  project_dir=str(workspace.root)
95
107
  )
96
108
  self.runtime_events = runtime_events or RuntimeEventStore(self.session_service)
109
+ self.plugin_runtime = plugin_runtime
97
110
  self._active_handles: dict[str, Any] = {}
98
111
  self._cancel_flags: dict[str, bool] = {}
99
- self._control_queues: dict[str, asyncio.Queue[tuple[str, ResumePayload | None]]] = {}
112
+ self._control_queues: dict[
113
+ str,
114
+ asyncio.Queue[tuple[str, ResumePayload | None, asyncio.Future[None] | None]],
115
+ ] = {}
100
116
  self._waiting_modes: dict[str, str] = {}
117
+ # Interaction resolution appends to one run-level event file. Serialise
118
+ # every decision for that run, not only identical interaction ids, so
119
+ # two concurrently visible cards cannot overwrite each other's receipt.
120
+ self._interaction_locks: dict[str, asyncio.Lock] = {}
101
121
 
102
122
  async def recover_interrupted(self) -> None:
103
123
  """Settle local runs left active across a Studio restart.
@@ -173,11 +193,21 @@ class StudioRunService:
173
193
  if on_event is not None:
174
194
  on_event(created)
175
195
 
176
- from ksadk.kernel.ingress import kernel_route_active
177
-
178
- if kernel_route_active():
196
+ kernel_runtime = self._kernel_runtime_for_spec(spec)
197
+ if kernel_runtime is not None:
179
198
  return await self._kernel_run(
180
- spec, user_input, record=record, on_event=on_event
199
+ spec,
200
+ user_input,
201
+ record=record,
202
+ on_event=on_event,
203
+ kernel_runtime=kernel_runtime,
204
+ )
205
+ if spec.plugin_bundle_root is not None:
206
+ return await self._plugin_run(
207
+ spec,
208
+ user_input,
209
+ record=record,
210
+ on_event=on_event,
181
211
  )
182
212
 
183
213
  started = time.monotonic()
@@ -187,18 +217,32 @@ class StudioRunService:
187
217
 
188
218
  async def persist(runtime_event: RuntimeEvent) -> RunEvent:
189
219
  persisted = await self.runtime_events.append_one(record.session_id, runtime_event)
190
- event_type, data = project_runtime_event(persisted)
220
+ event_type, data = project_runtime_event(
221
+ persisted,
222
+ session_id=record.session_id,
223
+ public_run_id=record.id,
224
+ )
225
+ if isinstance(persisted, InteractionRequested):
226
+ data["revision"] = 1
227
+ conversation_item = data.get("conversationItem")
228
+ if isinstance(conversation_item, dict):
229
+ item_payload = conversation_item.get("payload")
230
+ if isinstance(item_payload, dict):
231
+ item_payload["revision"] = 1
191
232
  stored = self.event_store.append(record.id, event_type, data)
192
233
  if on_event is not None:
193
234
  on_event(stored)
194
235
  return stored
195
236
 
196
237
  handle = None
197
- final_text = ""
198
- streamed_final = ""
238
+ completed_text_by_item: dict[tuple[str, str], str] = {}
239
+ streamed_text_by_item: dict[tuple[str, str], str] = {}
240
+ terminal_output_refs: tuple[OutputRef, ...] = ()
199
241
  runtime_duration_ms: int | None = None
200
242
  item_phases: dict[tuple[str, str], str | None] = {}
201
- control_queue: asyncio.Queue[tuple[str, ResumePayload | None]] = asyncio.Queue()
243
+ control_queue: asyncio.Queue[
244
+ tuple[str, ResumePayload | None, asyncio.Future[None] | None]
245
+ ] = asyncio.Queue()
202
246
  self._control_queues[run_id] = control_queue
203
247
  try:
204
248
  tool_approval_mode = str(spec.request_config.get("tool_approval_mode") or "")
@@ -250,10 +294,13 @@ class StudioRunService:
250
294
  == "final_answer"
251
295
  ):
252
296
  text = event.update.text if isinstance(event.update, TextContent) else ""
297
+ item_key = (event.scope_id, event.item_id)
253
298
  if event.op == "replace":
254
- streamed_final = text
299
+ streamed_text_by_item[item_key] = text
255
300
  else:
256
- streamed_final += text
301
+ streamed_text_by_item[item_key] = (
302
+ streamed_text_by_item.get(item_key, "") + text
303
+ )
257
304
  elif (
258
305
  isinstance(event, ItemCompleted)
259
306
  and event.item_kind == "message"
@@ -262,10 +309,11 @@ class StudioRunService:
262
309
  )
263
310
  == "final_answer"
264
311
  ):
265
- for part in event.snapshot.parts:
266
- if isinstance(part, TextContent):
267
- final_text = part.text
268
- break
312
+ completed_text_by_item[(event.scope_id, event.item_id)] = "".join(
313
+ part.text
314
+ for part in event.snapshot.parts
315
+ if isinstance(part, TextContent)
316
+ )
269
317
  elif isinstance(event, UsageReported):
270
318
  record.usage = Usage(
271
319
  input_tokens=event.input_tokens,
@@ -318,6 +366,7 @@ class StudioRunService:
318
366
  elif isinstance(event, RunCompleted):
319
367
  terminal_seen = True
320
368
  record.status = RunStatus.COMPLETED
369
+ terminal_output_refs = event.output_refs
321
370
  raw_duration = event.source.metadata.get("duration_ms")
322
371
  if raw_duration is None:
323
372
  metrics = event.source.metadata.get("metrics")
@@ -331,7 +380,7 @@ class StudioRunService:
331
380
  record.status = RunStatus.COMPLETED
332
381
  break
333
382
 
334
- command, resume_payload = await control_queue.get()
383
+ command, resume_payload, submit_ack = await control_queue.get()
335
384
  if command == "cancel" or self._cancel_flags.get(run_id):
336
385
  raise asyncio.CancelledError()
337
386
  if command != "resume":
@@ -342,11 +391,18 @@ class StudioRunService:
342
391
  if native_thread_id
343
392
  else ResumeTarget(kind="invocation_id", id=handle.run_id)
344
393
  )
345
- handle = await self.executor.resume(
346
- handle,
347
- target,
348
- resume_payload,
349
- )
394
+ try:
395
+ handle = await self.executor.resume(
396
+ handle,
397
+ target,
398
+ resume_payload,
399
+ )
400
+ except Exception as exc:
401
+ if submit_ack is not None and not submit_ack.done():
402
+ submit_ack.set_exception(exc)
403
+ raise
404
+ if submit_ack is not None and not submit_ack.done():
405
+ submit_ack.set_result(None)
350
406
  self._active_handles[run_id] = handle
351
407
  record.runtime_handle = handle.model_dump(mode="json")
352
408
  record.status = RunStatus.RUNNING
@@ -360,7 +416,17 @@ class StudioRunService:
360
416
  if on_event is not None:
361
417
  on_event(resumed)
362
418
  self.event_store.save(record)
363
- record.output = final_text or streamed_final
419
+ text_by_item = {**streamed_text_by_item, **completed_text_by_item}
420
+ if terminal_output_refs:
421
+ terminal_parts = [
422
+ text_by_item.get((ref.scope_id, ref.item_id), "")
423
+ for ref in terminal_output_refs
424
+ ]
425
+ record.output = "\n\n".join(part for part in terminal_parts if part)
426
+ else:
427
+ completed_parts = [part for part in completed_text_by_item.values() if part]
428
+ fallback_parts = [part for part in streamed_text_by_item.values() if part]
429
+ record.output = "\n\n".join(completed_parts or fallback_parts)
364
430
  except asyncio.CancelledError:
365
431
  cancel_result = "task_cancelled"
366
432
  if handle is not None and self.executor.is_attached(handle):
@@ -440,6 +506,229 @@ class StudioRunService:
440
506
  async def _sync_trace(self, record: RunRecord) -> None:
441
507
  self.event_store.trace_store.sync(record, await self.events(record.id))
442
508
 
509
+ async def _plugin_run(
510
+ self,
511
+ spec: StudioRunSpec,
512
+ user_input: str,
513
+ *,
514
+ record: RunRecord,
515
+ on_event: Callable[[RunEvent], None] | None,
516
+ ) -> RunRecord:
517
+ """Execute a composed Build without translating it into ADK/LangGraph.
518
+
519
+ PluginHost retains the activation under the Studio session id. Harness
520
+ providers write canonical RuntimeEvents directly; providers that only
521
+ implement the minimum request/result protocol receive a canonical
522
+ envelope here so Studio still has one durable conversation/event model.
523
+ """
524
+
525
+ started = time.monotonic()
526
+ record.status = RunStatus.RUNNING
527
+ record.started_at = datetime.now(timezone.utc)
528
+ record.runtime_handle = {
529
+ "provider": "pluginhost",
530
+ "bundleDigest": str(spec.request_config.get("plugin_bundle_digest") or ""),
531
+ "activationKey": record.session_id,
532
+ }
533
+ self.event_store.save(record)
534
+ rows_before = await self.session_service.get_events(record.session_id)
535
+ after_seq = max((int(row.seq_id or 0) for row in rows_before), default=0)
536
+
537
+ async def publish(events: list[RuntimeEvent]) -> None:
538
+ for runtime_event in events:
539
+ event_type, data = project_runtime_event(
540
+ runtime_event,
541
+ session_id=record.session_id,
542
+ )
543
+ stored = self.event_store.append(record.id, event_type, data)
544
+ if on_event is not None:
545
+ on_event(stored)
546
+
547
+ try:
548
+ if self.plugin_runtime is None:
549
+ raise StudioError(
550
+ "PLUGIN_RUNTIME_UNAVAILABLE",
551
+ "Studio 尚未配置 PluginHost Runtime",
552
+ status_code=503,
553
+ )
554
+ request_metadata = {
555
+ key: value
556
+ for key, value in {
557
+ "tool_approval_mode": spec.request_config.get("tool_approval_mode"),
558
+ "collaboration_mode": spec.request_config.get("collaboration_mode"),
559
+ "goal_objective": spec.request_config.get("goal_objective"),
560
+ "reasoning_effort": spec.request_config.get("effort"),
561
+ }.items()
562
+ if value not in {None, ""}
563
+ }
564
+ result = await self.plugin_runtime.execute(
565
+ spec,
566
+ {
567
+ "user_id": "local-user",
568
+ "session_id": record.session_id,
569
+ "invocation_id": record.id,
570
+ "messages": [{"role": "user", "content": user_input}],
571
+ "model": spec.model,
572
+ "request_metadata": request_metadata,
573
+ },
574
+ session_id=record.session_id,
575
+ )
576
+ if result.session_id != record.session_id:
577
+ raise StudioError(
578
+ "PLUGIN_SESSION_MISMATCH",
579
+ "AgentProvider 返回了不同的 Session",
580
+ status_code=502,
581
+ details={
582
+ "expected": record.session_id,
583
+ "actual": result.session_id,
584
+ },
585
+ )
586
+ canonical = await self._plugin_events_after(
587
+ record.session_id,
588
+ after_seq=after_seq,
589
+ run_id=record.id,
590
+ )
591
+ if not canonical:
592
+ canonical = await self._persist_plugin_result_events(
593
+ record,
594
+ result.output_text,
595
+ usage=result.usage,
596
+ )
597
+ await publish(canonical)
598
+ record.output = result.output_text
599
+ record.status = RunStatus.COMPLETED
600
+ _apply_plugin_usage(record, result.usage)
601
+ except asyncio.CancelledError:
602
+ record.status = RunStatus.CANCELLED
603
+ record.error = {"code": "RUN_CANCELLED", "message": "运行已取消"}
604
+ cancelled = await self.runtime_events.append_one(
605
+ record.session_id,
606
+ RunCanceled(
607
+ **_plugin_event_envelope(record, "run.canceled"),
608
+ status="canceled",
609
+ reason="cancelled",
610
+ ),
611
+ )
612
+ await publish([cancelled])
613
+ raise
614
+ except Exception as exc: # noqa: BLE001 - plugin boundary is typed below
615
+ record.status = RunStatus.FAILED
616
+ code = str(getattr(exc, "code", "PLUGIN_RUNTIME_FAILED"))
617
+ record.error = {"code": code, "message": str(exc)}
618
+ canonical = await self._plugin_events_after(
619
+ record.session_id,
620
+ after_seq=after_seq,
621
+ run_id=record.id,
622
+ )
623
+ if canonical:
624
+ await publish(canonical)
625
+ else:
626
+ failed = await self.runtime_events.append_one(
627
+ record.session_id,
628
+ RunFailed(
629
+ **_plugin_event_envelope(record, "run.failed"),
630
+ status="failed",
631
+ error=ErrorInfo(
632
+ code=code,
633
+ message=str(exc),
634
+ source="pluginhost",
635
+ scope_id=record.id,
636
+ ),
637
+ ),
638
+ )
639
+ await publish([failed])
640
+ finally:
641
+ record.completed_at = datetime.now(timezone.utc)
642
+ record.duration_ms = int((time.monotonic() - started) * 1000)
643
+ record.duration_source = "studio"
644
+ self.event_store.save(record)
645
+ await self._sync_trace(record)
646
+ return record
647
+
648
+ async def _plugin_events_after(
649
+ self,
650
+ session_id: str,
651
+ *,
652
+ after_seq: int,
653
+ run_id: str,
654
+ ) -> list[RuntimeEvent]:
655
+ rows = await self.session_service.get_events(
656
+ session_id,
657
+ after_seq_id=after_seq,
658
+ )
659
+ events: list[RuntimeEvent] = []
660
+ for row in rows:
661
+ event = session_event_to_runtime_event(row)
662
+ if event is not None and event.run_id == run_id:
663
+ events.append(event)
664
+ return events
665
+
666
+ async def _persist_plugin_result_events(
667
+ self,
668
+ record: RunRecord,
669
+ output_text: str,
670
+ *,
671
+ usage: Mapping[str, Any],
672
+ ) -> list[RuntimeEvent]:
673
+ item_id = f"{record.id}:assistant"
674
+ part_id = f"{record.id}:text"
675
+ snapshot = ContentSnapshot(
676
+ parts=(TextContent(part_id=part_id, text=output_text),)
677
+ )
678
+ events: list[RuntimeEvent] = [
679
+ RunStarted(
680
+ **_plugin_event_envelope(record, "run.started"),
681
+ status="running",
682
+ ),
683
+ ItemStarted(
684
+ **_plugin_event_envelope(record, "item.started"),
685
+ item_id=item_id,
686
+ item_kind="message",
687
+ phase="final_answer",
688
+ ),
689
+ ItemUpdated(
690
+ **_plugin_event_envelope(record, "item.updated"),
691
+ item_id=item_id,
692
+ item_kind="message",
693
+ op="append",
694
+ update=TextContent(part_id=part_id, text=output_text),
695
+ ),
696
+ ItemCompleted(
697
+ **_plugin_event_envelope(record, "item.completed"),
698
+ item_id=item_id,
699
+ item_kind="message",
700
+ snapshot=snapshot,
701
+ ),
702
+ ]
703
+ normalized = _normalized_plugin_usage(usage)
704
+ if normalized["reported"]:
705
+ events.append(
706
+ UsageReported(
707
+ **_plugin_event_envelope(record, "usage.reported"),
708
+ input_tokens=normalized["input_tokens"],
709
+ output_tokens=normalized["output_tokens"],
710
+ total_tokens=normalized["total_tokens"],
711
+ cached_tokens=normalized["cached_tokens"],
712
+ reasoning_tokens=normalized["reasoning_tokens"],
713
+ )
714
+ )
715
+ events.append(
716
+ RunCompleted(
717
+ **_plugin_event_envelope(record, "run.completed"),
718
+ status="completed",
719
+ output_refs=(
720
+ OutputRef(
721
+ scope_id=record.id,
722
+ item_id=item_id,
723
+ part_id=part_id,
724
+ ),
725
+ ),
726
+ )
727
+ )
728
+ return [
729
+ await self.runtime_events.append_one(record.session_id, event)
730
+ for event in events
731
+ ]
443
732
 
444
733
  async def _kernel_run(
445
734
  self,
@@ -448,6 +737,7 @@ class StudioRunService:
448
737
  *,
449
738
  record: RunRecord,
450
739
  on_event: Callable[[RunEvent], None] | None,
740
+ kernel_runtime: Any,
451
741
  ) -> RunRecord:
452
742
  """kernel 路径(灰度 opt-in):Studio run -> AgentControlCommand -> receipt。
453
743
 
@@ -464,9 +754,15 @@ class StudioRunService:
464
754
  trusted = _kernel_ingress.trusted_context(
465
755
  source_kind="studio",
466
756
  source_ref=record.id,
757
+ # The Worker polls the concrete Kernel Runtime instance, not
758
+ # whichever synthetic ``local-agent`` happened to be inferred
759
+ # from a Studio request. A Studio Build may use the Kernel
760
+ # path only after ``_kernel_runtime_for_spec`` established that
761
+ # it is this Runtime's exact Agent/Runtime binding.
762
+ tenant_id=str(kernel_runtime.config.tenant_id),
763
+ agent_instance_id=str(kernel_runtime.config.agent_instance_id),
467
764
  session_id=record.session_id,
468
765
  operations=("enqueue",),
469
- launch_context=spec.launch_context,
470
766
  )
471
767
  idempotency_key = str(
472
768
  (spec.request_config or {}).get("idempotency_key") or record.id
@@ -513,6 +809,7 @@ class StudioRunService:
513
809
  self.event_store.save(record)
514
810
  await self._sync_trace(record)
515
811
  return record
812
+
516
813
  except Exception as exc: # noqa: BLE001
517
814
  logger.exception("Studio kernel ingress failed for run %s", record.id)
518
815
  record.status = RunStatus.FAILED
@@ -526,12 +823,62 @@ class StudioRunService:
526
823
  await self._sync_trace(record)
527
824
  return record
528
825
 
826
+ @staticmethod
827
+ def _kernel_runtime_for_spec(spec: StudioRunSpec) -> Any | None:
828
+ """Return the active Kernel Runtime only for its bound Studio Build.
829
+
830
+ An in-process AgentKernel owns one concrete RuntimeAdapter factory and
831
+ its immutable startup defaults. Merely observing that a Kernel is
832
+ enabled is therefore insufficient: sending an arbitrary Studio Build
833
+ through it can silently execute the default Adapter instead. When the
834
+ binding is not exact, the existing direct Studio path remains the
835
+ compatible local behaviour; it is safer than manufacturing a false
836
+ AgentControl/Scheduler success.
837
+ """
838
+
839
+ from ksadk.kernel.bootstrap import get_agent_kernel_runtime
840
+ from ksadk.kernel.ingress import kernel_route_active
841
+
842
+ if not kernel_route_active():
843
+ return None
844
+ runtime = get_agent_kernel_runtime()
845
+ if runtime is None:
846
+ return None
847
+ active_context = getattr(runtime.config, "launch_context", None)
848
+ if active_context is None:
849
+ return None
850
+ if str(getattr(active_context, "runtime_type", "")).strip().lower() != (
851
+ spec.launch_context.runtime_type.strip().lower()
852
+ ):
853
+ return None
854
+ try:
855
+ active_project = Path(active_context.project_dir).resolve()
856
+ build_project = Path(spec.launch_context.project_dir).resolve()
857
+ except (OSError, TypeError, ValueError):
858
+ return None
859
+ if active_project != build_project:
860
+ return None
861
+ defaults = getattr(runtime.config, "start_request_defaults", {}) or {}
862
+ bound_agent_id = str(
863
+ defaults.get("agent_id") or runtime.config.agent_instance_id
864
+ ).strip()
865
+ if bound_agent_id != spec.agent_id:
866
+ return None
867
+ declared_instance = str(
868
+ (spec.launch_context.config or {}).get("agent_instance_id") or ""
869
+ ).strip()
870
+ if declared_instance and declared_instance != str(
871
+ runtime.config.agent_instance_id
872
+ ):
873
+ return None
874
+ return runtime
875
+
529
876
  async def cancel_run(self, run_id: str) -> dict[str, str]:
530
877
  """Request cancellation; the flag and executor perform the actual stop."""
531
878
  self._cancel_flags[run_id] = True
532
879
  queue = self._control_queues.get(run_id)
533
880
  if queue is not None:
534
- queue.put_nowait(("cancel", None))
881
+ queue.put_nowait(("cancel", None, None))
535
882
  handle = self._active_handles.get(run_id)
536
883
  if handle is not None and self.executor.is_attached(handle):
537
884
  try:
@@ -582,7 +929,7 @@ class StudioRunService:
582
929
  "Studio 进程已重启,当前暂停点无法恢复",
583
930
  status_code=409,
584
931
  )
585
- queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行")))
932
+ queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行"), None))
586
933
  return {"runId": run_id, "status": "resuming"}
587
934
 
588
935
  async def submit_interaction(
@@ -592,28 +939,76 @@ class StudioRunService:
592
939
  *,
593
940
  name: str,
594
941
  data: dict[str, Any] | None = None,
942
+ expected_revision: int,
943
+ idempotency_key: str,
595
944
  ) -> 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,
945
+ lock = self._interaction_locks.setdefault(run_id, asyncio.Lock())
946
+ payload_data = dict(data or {})
947
+ digest = hashlib.sha256(
948
+ json.dumps(
949
+ {"name": name, "data": payload_data},
950
+ ensure_ascii=False,
951
+ sort_keys=True,
952
+ separators=(",", ":"),
953
+ ).encode("utf-8")
954
+ ).hexdigest()
955
+ async with lock:
956
+ return await self._submit_interaction_locked(
957
+ run_id,
958
+ interaction_id,
959
+ name=name,
960
+ data=payload_data,
961
+ expected_revision=expected_revision,
962
+ idempotency_key=idempotency_key,
963
+ request_digest=digest,
602
964
  )
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"}
965
+
966
+ async def _submit_interaction_locked(
967
+ self,
968
+ run_id: str,
969
+ interaction_id: str,
970
+ *,
971
+ name: str,
972
+ data: dict[str, Any],
973
+ expected_revision: int,
974
+ idempotency_key: str,
975
+ request_digest: str,
976
+ ) -> dict[str, Any]:
977
+ events = self.event_store.events(run_id)
978
+ prior = next(
979
+ (
980
+ event
981
+ for event in reversed(events)
982
+ if event.type == "a2ui.action"
983
+ and str(event.data.get("interactionId") or "") == interaction_id
984
+ and _interaction_revision(event.data) > 0
985
+ ),
986
+ None,
987
+ )
988
+ if prior is not None:
989
+ if str(prior.data.get("idempotencyKey") or "") != idempotency_key:
990
+ raise StudioError(
991
+ "INTERACTION_ALREADY_RESOLVED",
992
+ "该交互已由其他请求处理",
993
+ status_code=409,
994
+ )
995
+ if str(prior.data.get("requestDigest") or "") != request_digest:
996
+ raise StudioError(
997
+ "INTERACTION_IDEMPOTENCY_CONFLICT",
998
+ "同一幂等键不能提交不同内容",
999
+ status_code=409,
1000
+ )
1001
+ record = self.event_store.get(run_id)
1002
+ if record.status == RunStatus.WAITING_INPUT:
1003
+ record.status = RunStatus.RUNNING
1004
+ self.event_store.save(record)
1005
+ self._waiting_modes.pop(run_id, None)
1006
+ return dict(prior.data.get("receipt") or {})
612
1007
 
613
1008
  interaction = next(
614
1009
  (
615
1010
  event
616
- for event in reversed(self.event_store.events(run_id))
1011
+ for event in reversed(events)
617
1012
  if event.type == "a2ui.interaction"
618
1013
  and str(event.data.get("interactionId") or event.data.get("interaction_id") or "")
619
1014
  == interaction_id
@@ -622,8 +1017,29 @@ class StudioRunService:
622
1017
  )
623
1018
  if interaction is None:
624
1019
  raise StudioError("INTERACTION_NOT_FOUND", "交互请求不存在", status_code=404)
1020
+ revision = _interaction_revision(interaction.data)
1021
+ if revision < 1:
1022
+ raise StudioError(
1023
+ "INTERACTION_READ_ONLY",
1024
+ "交互请求缺少权威 revision,只能查看不能操作",
1025
+ status_code=409,
1026
+ )
1027
+ if expected_revision != revision:
1028
+ raise StudioError(
1029
+ "INTERACTION_REVISION_MISMATCH",
1030
+ "交互版本已变化,请刷新后重试",
1031
+ status_code=409,
1032
+ details={"expectedRevision": expected_revision, "actualRevision": revision},
1033
+ )
1034
+ record = self.event_store.get(run_id)
1035
+ if record.status != RunStatus.WAITING_INPUT:
1036
+ raise StudioError(
1037
+ "INTERACTION_NOT_PENDING",
1038
+ "该 Run 当前没有等待中的交互",
1039
+ status_code=409,
1040
+ )
625
1041
  kind = str(interaction.data.get("kind") or "form")
626
- payload_data = {"decision": name, **dict(data or {})}
1042
+ payload_data = {"decision": name, **data}
627
1043
  payload = ResumePayload(
628
1044
  kind="approval_decision" if kind == "approval" else "hitl_answer",
629
1045
  call_id=interaction_id,
@@ -636,7 +1052,7 @@ class StudioRunService:
636
1052
  raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
637
1053
  try:
638
1054
  await self.executor.submit(handle, payload)
639
- except (RuntimeError, ValueError) as exc:
1055
+ except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
640
1056
  raise StudioError(
641
1057
  "INTERACTION_SUBMIT_FAILED",
642
1058
  str(exc),
@@ -646,45 +1062,54 @@ class StudioRunService:
646
1062
  queue = self._control_queues.get(run_id)
647
1063
  if queue is None:
648
1064
  raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
649
- queue.put_nowait(("resume", payload))
1065
+ submit_ack = asyncio.get_running_loop().create_future()
1066
+ queue.put_nowait(("resume", payload, submit_ack))
1067
+ try:
1068
+ await submit_ack
1069
+ except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
1070
+ raise StudioError(
1071
+ "INTERACTION_SUBMIT_FAILED",
1072
+ str(exc),
1073
+ status_code=409,
1074
+ ) from exc
650
1075
  else:
651
1076
  raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
652
1077
 
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(
1078
+ next_revision = revision + 1
1079
+ resolved_data = {
1080
+ "runId": run_id,
1081
+ "interactionId": interaction_id,
1082
+ "callId": interaction_id,
1083
+ "name": name,
1084
+ "data": data,
1085
+ "revision": next_revision,
1086
+ "expectedRevision": expected_revision,
1087
+ "idempotencyKey": idempotency_key,
1088
+ }
1089
+ action_data = {
1090
+ "runId": run_id,
1091
+ "surfaceId": str(
1092
+ interaction.data.get("surfaceId") or interaction.data.get("surface_id") or ""
1093
+ ),
1094
+ "interactionId": interaction_id,
1095
+ "actionId": f"action-{interaction_id}",
1096
+ "name": name,
1097
+ "data": data,
1098
+ "revision": next_revision,
1099
+ "expectedRevision": expected_revision,
1100
+ "idempotencyKey": idempotency_key,
1101
+ "requestDigest": request_digest,
1102
+ }
1103
+ _, _, receipt = self.event_store.append_interaction_resolution(
665
1104
  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
- },
1105
+ resolved_type=("approval.resolved" if kind == "approval" else "interaction.resolved"),
1106
+ resolved_data=resolved_data,
1107
+ action_data=action_data,
677
1108
  )
678
1109
  record.status = RunStatus.RUNNING
679
1110
  self.event_store.save(record)
680
1111
  self._waiting_modes.pop(run_id, None)
681
- return {
682
- "runId": run_id,
683
- "interactionId": interaction_id,
684
- "status": "resolved",
685
- "eventId": action.id,
686
- "resolutionEventId": resolved.id,
687
- }
1112
+ return receipt
688
1113
 
689
1114
  async def _capture_pcm_evidence(
690
1115
  self,
@@ -831,6 +1256,7 @@ class StudioRunService:
831
1256
  "surfaceId": surface_id,
832
1257
  "interactionId": approval_id,
833
1258
  "kind": "approval",
1259
+ "revision": 1,
834
1260
  "inputSchema": {
835
1261
  "type": "object",
836
1262
  "properties": {
@@ -881,7 +1307,96 @@ class StudioRunService:
881
1307
  return {}
882
1308
 
883
1309
 
884
- def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
1310
+ def _plugin_event_envelope(record: RunRecord, event_type: str) -> dict[str, Any]:
1311
+ return {
1312
+ "schema_version": 2,
1313
+ "event_id": f"{record.id}:{event_type}",
1314
+ "seq": 0,
1315
+ "timestamp": time.time(),
1316
+ "run_id": record.id,
1317
+ "scope_id": record.id,
1318
+ "source": SourceRef(
1319
+ framework="ksadk",
1320
+ metadata={
1321
+ "runtime": "pluginhost",
1322
+ "agent_id": record.agent_id,
1323
+ "session_id": record.session_id,
1324
+ "build_id": record.build_id,
1325
+ },
1326
+ ),
1327
+ }
1328
+
1329
+
1330
+ def _normalized_plugin_usage(usage: Mapping[str, Any]) -> dict[str, Any]:
1331
+ def number(*keys: str) -> int:
1332
+ for key in keys:
1333
+ value = usage.get(key)
1334
+ if value is not None:
1335
+ try:
1336
+ return max(0, int(value))
1337
+ except (TypeError, ValueError):
1338
+ return 0
1339
+ return 0
1340
+
1341
+ normalized = {
1342
+ "input_tokens": number("input_tokens", "inputTokens", "prompt_tokens"),
1343
+ "output_tokens": number(
1344
+ "output_tokens", "outputTokens", "completion_tokens"
1345
+ ),
1346
+ "total_tokens": number("total_tokens", "totalTokens"),
1347
+ "cached_tokens": number("cached_tokens", "cachedTokens"),
1348
+ "reasoning_tokens": number("reasoning_tokens", "reasoningTokens"),
1349
+ }
1350
+ if not normalized["total_tokens"]:
1351
+ normalized["total_tokens"] = (
1352
+ normalized["input_tokens"] + normalized["output_tokens"]
1353
+ )
1354
+ normalized["reported"] = bool(usage) and any(
1355
+ key in usage
1356
+ for key in (
1357
+ "input_tokens",
1358
+ "inputTokens",
1359
+ "prompt_tokens",
1360
+ "output_tokens",
1361
+ "outputTokens",
1362
+ "completion_tokens",
1363
+ "total_tokens",
1364
+ "totalTokens",
1365
+ )
1366
+ )
1367
+ return normalized
1368
+
1369
+
1370
+ def _interaction_revision(data: Mapping[str, Any]) -> int:
1371
+ """Treat malformed or historical interaction revisions as read-only."""
1372
+
1373
+ try:
1374
+ return max(0, int(data.get("revision") or 0))
1375
+ except (TypeError, ValueError):
1376
+ return 0
1377
+
1378
+
1379
+ def _apply_plugin_usage(record: RunRecord, usage: Mapping[str, Any]) -> None:
1380
+ normalized = _normalized_plugin_usage(usage)
1381
+ if not normalized["reported"]:
1382
+ return
1383
+ record.usage = Usage(
1384
+ input_tokens=normalized["input_tokens"],
1385
+ output_tokens=normalized["output_tokens"],
1386
+ total_tokens=normalized["total_tokens"],
1387
+ cached_input_tokens=normalized["cached_tokens"],
1388
+ reasoning_output_tokens=normalized["reasoning_tokens"],
1389
+ reported=True,
1390
+ source="pluginhost",
1391
+ )
1392
+
1393
+
1394
+ def project_runtime_event(
1395
+ event: RuntimeEvent,
1396
+ *,
1397
+ session_id: str | None = None,
1398
+ public_run_id: str | None = None,
1399
+ ) -> tuple[str, dict[str, Any]]:
885
1400
  """Project the canonical RuntimeEvent into Studio's persisted event view.
886
1401
 
887
1402
  公开承诺字段(契约声明见 ``ksadk/events/projections.py``,执行形态为
@@ -1045,7 +1560,18 @@ def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
1045
1560
  payload = {}
1046
1561
 
1047
1562
  _attach_studio_identity(payload, event)
1563
+ if public_run_id and public_run_id != event.run_id:
1564
+ payload["runtimeRunId"] = event.run_id
1565
+ payload["runId"] = public_run_id
1048
1566
  payload["runtimeEvent"] = dump_runtime_event(event)
1567
+ # Additive only: legacy Studio/Web consumers keep reading the established
1568
+ # event type and payload keys. New surfaces may opt into this typed,
1569
+ # identity-aware representation without reconstructing one from text.
1570
+ payload["conversationItem"] = project_conversation_item(
1571
+ event,
1572
+ session_id=session_id,
1573
+ run_id=public_run_id,
1574
+ ).model_dump(by_alias=True, exclude_none=True, mode="json")
1049
1575
  return projected, payload
1050
1576
 
1051
1577
 
@@ -1073,6 +1599,11 @@ def _project_a2ui_surface(
1073
1599
  projected = "a2ui.surface.end"
1074
1600
  data_parts = event.snapshot.parts
1075
1601
 
1602
+ if event.source.metadata.get("operation_batch") is True and isinstance(event, ItemCompleted):
1603
+ # Completion closes the immutable canonical batch; it is not a request
1604
+ # to remove the surface that the batch just created.
1605
+ projected = "a2ui.surface.begin"
1606
+
1076
1607
  surface_id = str(event.source.metadata.get("surface_id") or "")
1077
1608
  operations: list[dict[str, Any]] = []
1078
1609
  for part in data_parts:
@@ -1080,6 +1611,8 @@ def _project_a2ui_surface(
1080
1611
  data = part.data
1081
1612
  if isinstance(data, list):
1082
1613
  operations.extend(dict(op) for op in data if isinstance(op, Mapping))
1614
+ elif isinstance(data, Mapping):
1615
+ operations.extend(project_a2ui_operations(projected, data))
1083
1616
  if not operations:
1084
1617
  operations = project_a2ui_operations(projected, {"surface_id": surface_id})
1085
1618
  payload: dict[str, Any] = {
@@ -1122,6 +1655,44 @@ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
1122
1655
  """Session envelope -> Studio RunEvent 投影;cursor 仍用 envelope.seq。"""
1123
1656
 
1124
1657
  payload = envelope.payload or {}
1658
+ # Kernel RuntimeEvent is persisted as a typed runtime/v2 SessionEvent
1659
+ # envelope. Reuse the exact same projector as the direct Studio path so
1660
+ # a consumer receives its additive ConversationItem regardless of which
1661
+ # ingress delivered the Run. Legacy event families retain the historical
1662
+ # text-only fallback below.
1663
+ if envelope.family == "runtime" and envelope.family_version == 2:
1664
+ try:
1665
+ runtime_event = parse_runtime_event(payload)
1666
+ except (TypeError, ValueError):
1667
+ # A damaged/unknown runtime payload must not make old Studio
1668
+ # subscriptions fail. Preserve its event type for legacy
1669
+ # observability while refusing to invent a typed conversation item.
1670
+ return envelope.event_type, {"runId": envelope.run_id or ""}
1671
+ return project_runtime_event(runtime_event, session_id=envelope.session_id)
1672
+ if envelope.family == "interaction" and envelope.family_version == 1:
1673
+ conversation_item = project_interaction_conversation_item(envelope)
1674
+ if conversation_item is None:
1675
+ # Keep a malformed durable fact observable without reusing an
1676
+ # actionable legacy event name. Existing Studio reducers turn
1677
+ # ``approval.requested`` into a submit button, so emitting that
1678
+ # name without an authoritative revision would bypass the typed
1679
+ # ConversationItem fail-closed boundary.
1680
+ payload = envelope.payload or {}
1681
+ return envelope.event_type, {
1682
+ "runId": envelope.run_id or str(payload.get("run_id") or ""),
1683
+ "itemId": str(payload.get("interaction_id") or envelope.event_id),
1684
+ "interactionReadOnly": True,
1685
+ }
1686
+ projected = _project_interaction_envelope_legacy(envelope)
1687
+ if projected is None:
1688
+ return None
1689
+ event_type, data = projected
1690
+ data["conversationItem"] = conversation_item.model_dump(
1691
+ by_alias=True,
1692
+ exclude_none=True,
1693
+ mode="json",
1694
+ )
1695
+ return event_type, data
1125
1696
  if envelope.event_type == "run.completed":
1126
1697
  return "run.completed", {
1127
1698
  "runId": envelope.run_id or "",
@@ -1131,3 +1702,70 @@ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
1131
1702
  if text:
1132
1703
  return "message.delta", {"delta": text}
1133
1704
  return None
1705
+
1706
+
1707
+ def _project_interaction_envelope_legacy(
1708
+ envelope,
1709
+ ) -> tuple[str, dict[str, Any]] | None:
1710
+ """Keep established Studio event names while Interaction/v1 owns truth."""
1711
+
1712
+ payload = envelope.payload or {}
1713
+ interaction_id = str(payload.get("interaction_id") or "")
1714
+ interaction_kind = str(payload.get("kind") or "")
1715
+ revision = payload.get("revision")
1716
+ request = payload.get("request")
1717
+ request = request if isinstance(request, Mapping) else {}
1718
+ presentation = request.get("presentation")
1719
+ presentation = presentation if isinstance(presentation, Mapping) else {}
1720
+ common: dict[str, Any] = {
1721
+ "runId": envelope.run_id or str(payload.get("run_id") or ""),
1722
+ "itemId": interaction_id,
1723
+ "interactionId": interaction_id,
1724
+ }
1725
+ # Expose the value as received for legacy/read-only diagnostics. Only the
1726
+ # typed ConversationItem validates it as an authoritative writable token.
1727
+ if revision is not None:
1728
+ common["revision"] = revision
1729
+
1730
+ if envelope.event_type == "interaction.requested":
1731
+ if interaction_kind == "approval":
1732
+ return "approval.requested", {
1733
+ **common,
1734
+ "approvalId": interaction_id,
1735
+ "callId": "",
1736
+ "kind": str(presentation.get("title") or interaction_kind),
1737
+ "detail": presentation.get("description"),
1738
+ }
1739
+ return "a2ui.interaction", {
1740
+ **common,
1741
+ "kind": interaction_kind or "form",
1742
+ "inputSchema": (
1743
+ dict(request.get("request_schema"))
1744
+ if isinstance(request.get("request_schema"), Mapping)
1745
+ else {}
1746
+ ),
1747
+ }
1748
+ if envelope.event_type in {
1749
+ "interaction.resolved",
1750
+ "interaction.cancelled",
1751
+ "interaction.expired",
1752
+ }:
1753
+ outcome = str(
1754
+ payload.get("outcome")
1755
+ or (
1756
+ "cancelled"
1757
+ if envelope.event_type == "interaction.cancelled"
1758
+ else "expired"
1759
+ if envelope.event_type == "interaction.expired"
1760
+ else ""
1761
+ )
1762
+ )
1763
+ if interaction_kind == "approval":
1764
+ return "approval.resolved", {
1765
+ **common,
1766
+ "approvalId": interaction_id,
1767
+ "callId": "",
1768
+ "decision": outcome,
1769
+ }
1770
+ return "a2ui.action", {**common, "name": outcome}
1771
+ return None