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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +22 -2
  2. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/RECORD +356 -222
  3. ksadk/a2a/_space_client_events.py +381 -0
  4. ksadk/a2a/event_adapter.py +156 -64
  5. ksadk/a2a/executor.py +179 -63
  6. ksadk/a2a/space_client.py +6 -333
  7. ksadk/a2ui/core.py +222 -53
  8. ksadk/agui/_agent_helpers.py +243 -0
  9. ksadk/agui/a2ui_projection.py +12 -5
  10. ksadk/agui/agent.py +443 -259
  11. ksadk/api/client.py +525 -27
  12. ksadk/builders/code_builder.py +284 -8
  13. ksadk/builders/container_builder.py +8 -1
  14. ksadk/builders/managed_runtime_builder.py +39 -35
  15. ksadk/cli/__init__.py +17 -0
  16. ksadk/cli/cmd_dashboard.py +15 -1
  17. ksadk/cli/cmd_deploy.py +10 -2
  18. ksadk/cli/cmd_eval.py +66 -9
  19. ksadk/cli/cmd_evalset.py +284 -0
  20. ksadk/cli/cmd_files.py +5 -1
  21. ksadk/cli/cmd_hermes.py +1 -126
  22. ksadk/cli/cmd_invoke.py +28 -38
  23. ksadk/cli/cmd_managed_runtime.py +95 -0
  24. ksadk/cli/cmd_observe.py +85 -0
  25. ksadk/cli/cmd_openclaw.py +1 -435
  26. ksadk/cli/cmd_replay.py +28 -4
  27. ksadk/cli/cmd_studio.py +99 -8
  28. ksadk/cli/cmd_web.py +10 -2
  29. ksadk/cli/hermes_env.py +151 -0
  30. ksadk/cli/invoke_payload.py +49 -0
  31. ksadk/cli/openclaw_env.py +458 -0
  32. ksadk/codex/client.py +180 -30
  33. ksadk/codex/runtime.py +428 -326
  34. ksadk/configs/env_registry.py +66 -12
  35. ksadk/configs/env_registry_pcm.py +181 -0
  36. ksadk/configs/env_var_spec.py +18 -0
  37. ksadk/configs/global_config.py +5 -0
  38. ksadk/context_engine/__init__.py +98 -0
  39. ksadk/context_engine/assembler.py +176 -0
  40. ksadk/context_engine/baseline.py +419 -0
  41. ksadk/context_engine/cache_observability.py +228 -0
  42. ksadk/context_engine/capabilities.py +443 -0
  43. ksadk/context_engine/contributors.py +344 -0
  44. ksadk/context_engine/hosted_pipeline.py +417 -0
  45. ksadk/context_engine/models.py +117 -0
  46. ksadk/context_engine/planner.py +698 -0
  47. ksadk/context_engine/policies.py +319 -0
  48. ksadk/context_engine/projection.py +31 -0
  49. ksadk/context_engine/shadow_plan.py +266 -0
  50. ksadk/context_engine/tokenizer.py +164 -0
  51. ksadk/conversations/context.py +126 -6
  52. ksadk/conversations/message_projection.py +292 -28
  53. ksadk/conversations/model_context.py +49 -3
  54. ksadk/conversations/runtime_compaction.py +207 -5
  55. ksadk/conversations/runtime_input.py +184 -2
  56. ksadk/conversations/runtime_invocation.py +81 -3
  57. ksadk/conversations/runtime_observability.py +141 -0
  58. ksadk/conversations/runtime_payloads.py +42 -0
  59. ksadk/conversations/runtime_preparation.py +272 -2
  60. ksadk/conversations/runtime_resume.py +20 -1
  61. ksadk/conversations/runtime_stream_events.py +94 -9
  62. ksadk/conversations/semantic_summary.py +311 -1
  63. ksadk/conversations/session_lock.py +51 -0
  64. ksadk/deployment/env_forward.py +72 -0
  65. ksadk/deployment/managed_runtime.py +5 -0
  66. ksadk/deployment/providers/serverless.py +143 -69
  67. ksadk/evaluation/__init__.py +32 -0
  68. ksadk/evaluation/a2a_adapter.py +9 -1
  69. ksadk/evaluation/adapters.py +18 -5
  70. ksadk/evaluation/agent_eval_client.py +395 -0
  71. ksadk/evaluation/cloud_binding.py +80 -0
  72. ksadk/evaluation/cloud_converter.py +194 -0
  73. ksadk/evaluation/cloud_service.py +199 -0
  74. ksadk/evaluation/contracts.py +73 -4
  75. ksadk/evaluation/evaluators.py +183 -17
  76. ksadk/evaluation/evidence.py +258 -0
  77. ksadk/evaluation/executor.py +31 -6
  78. ksadk/evaluation/local_adapter.py +550 -0
  79. ksadk/evaluation/service_env.py +27 -0
  80. ksadk/evaluation/studio_build_adapter.py +253 -0
  81. ksadk/evaluation/target.py +15 -6
  82. ksadk/events/__init__.py +13 -13
  83. ksadk/events/_v1_compat/__init__.py +1 -0
  84. ksadk/events/_v1_compat/models.py +299 -0
  85. ksadk/events/_v1_compat/parser.py +247 -0
  86. ksadk/events/_v1_compat/projection.py +753 -0
  87. ksadk/events/adapters/__init__.py +34 -0
  88. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  89. ksadk/events/adapters/_a2a_support.py +275 -0
  90. ksadk/events/adapters/_codex_interactions.py +375 -0
  91. ksadk/events/adapters/_codex_items.py +801 -0
  92. ksadk/events/adapters/_codex_validators.py +311 -0
  93. ksadk/events/adapters/_langgraph_support.py +785 -0
  94. ksadk/events/adapters/a2a.py +841 -0
  95. ksadk/events/adapters/adk.py +503 -0
  96. ksadk/events/adapters/codex.py +717 -0
  97. ksadk/events/adapters/langgraph.py +745 -0
  98. ksadk/events/canonical.py +440 -0
  99. ksadk/events/canonical_replay.py +497 -0
  100. ksadk/events/canonical_store.py +468 -0
  101. ksadk/events/cold_recovery.py +257 -0
  102. ksadk/events/content.py +89 -0
  103. ksadk/events/identity.py +86 -0
  104. ksadk/events/pipeline.py +455 -0
  105. ksadk/events/projections.py +66 -0
  106. ksadk/events/reducer.py +661 -0
  107. ksadk/events/replay.py +3 -36
  108. ksadk/events/runtime_event.py +20 -265
  109. ksadk/events/session_event.py +324 -0
  110. ksadk/events/store.py +3 -316
  111. ksadk/events/v1_compat.py +41 -0
  112. ksadk/harness/runtime.py +160 -57
  113. ksadk/identity/resolver.py +4 -3
  114. ksadk/interaction/__init__.py +28 -0
  115. ksadk/interaction/contracts.py +136 -0
  116. ksadk/interaction/ledger.py +189 -0
  117. ksadk/interaction/provider.py +122 -0
  118. ksadk/interaction/providers/__init__.py +52 -0
  119. ksadk/interaction/providers/adk.py +40 -0
  120. ksadk/interaction/providers/codex.py +94 -0
  121. ksadk/interaction/providers/langgraph.py +58 -0
  122. ksadk/kernel/__init__.py +117 -0
  123. ksadk/kernel/authorization.py +258 -0
  124. ksadk/kernel/bootstrap.py +1074 -0
  125. ksadk/kernel/contract_fingerprints.py +61 -0
  126. ksadk/kernel/contracts.py +362 -0
  127. ksadk/kernel/control.py +219 -0
  128. ksadk/kernel/errors.py +79 -0
  129. ksadk/kernel/ingress.py +1102 -0
  130. ksadk/kernel/mapping.py +111 -0
  131. ksadk/kernel/memory_store.py +1133 -0
  132. ksadk/kernel/postgres_store.py +1756 -0
  133. ksadk/kernel/recovery.py +452 -0
  134. ksadk/kernel/runtime_identity.py +103 -0
  135. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  136. ksadk/kernel/sqlite_store.py +1410 -0
  137. ksadk/kernel/state.py +145 -0
  138. ksadk/kernel/store.py +232 -0
  139. ksadk/kernel/worker.py +953 -0
  140. ksadk/knowledge_base/client.py +7 -0
  141. ksadk/knowledge_base/service.py +32 -5
  142. ksadk/memory/__init__.py +61 -0
  143. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  144. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  145. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  146. ksadk/memory/adk/backends/sdk_ltm_backend.py +384 -2
  147. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  148. ksadk/memory/coordinator.py +461 -0
  149. ksadk/memory/events.py +202 -0
  150. ksadk/memory/extraction.py +228 -0
  151. ksadk/memory/ltm_backend_factory.py +16 -3
  152. ksadk/memory/models.py +316 -0
  153. ksadk/memory/policy.py +215 -0
  154. ksadk/memory/provider.py +47 -0
  155. ksadk/memory/provider_adapter.py +159 -0
  156. ksadk/memory/provider_resolver.py +71 -0
  157. ksadk/memory/providers/__init__.py +5 -0
  158. ksadk/memory/providers/local_sqlite.py +490 -0
  159. ksadk/memory/resolved_policy.py +145 -0
  160. ksadk/memory/service.py +188 -6
  161. ksadk/model_proxy/bootstrap.py +16 -9
  162. ksadk/model_proxy/detect.py +202 -9
  163. ksadk/model_proxy/namespace.py +39 -15
  164. ksadk/model_proxy/server.py +24 -5
  165. ksadk/model_proxy/transform.py +60 -2
  166. ksadk/observability/__init__.py +25 -0
  167. ksadk/observability/session_log.py +420 -0
  168. ksadk/observability/trajectory.py +105 -0
  169. ksadk/prompts/__init__.py +71 -0
  170. ksadk/prompts/compiler.py +215 -0
  171. ksadk/prompts/models.py +82 -0
  172. ksadk/prompts/projection.py +89 -0
  173. ksadk/prompts/resolved.py +137 -0
  174. ksadk/prompts/sources.py +233 -0
  175. ksadk/runners/_langgraph_runner_streams.py +822 -0
  176. ksadk/runners/adk_runner.py +5 -5
  177. ksadk/runners/base_runner.py +16 -0
  178. ksadk/runners/factory.py +1 -2
  179. ksadk/runners/langgraph_runner.py +198 -437
  180. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  181. ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
  182. ksadk/runtime/adapter.py +118 -2
  183. ksadk/runtime/conversation_execution.py +316 -182
  184. ksadk/runtime/executor.py +144 -3
  185. ksadk/runtime/factory.py +193 -7
  186. ksadk/runtime/hosted_finalizer.py +268 -0
  187. ksadk/runtime/launch.py +14 -1
  188. ksadk/runtime/preprocessing.py +25 -3
  189. ksadk/runtime/runner_adapter.py +288 -508
  190. ksadk/runtime/usage.py +33 -0
  191. ksadk/sandbox/backends/e2b.py +35 -2
  192. ksadk/server/composition.py +8 -0
  193. ksadk/server/factory.py +69 -6
  194. ksadk/server/routes/kernel_ingress.py +258 -0
  195. ksadk/server/routes/openai_compat.py +88 -8
  196. ksadk/server/routes/projection.py +35 -20
  197. ksadk/server/routes/run.py +94 -0
  198. ksadk/server/routes/sessions.py +2 -0
  199. ksadk/server/routes/streaming.py +5 -2
  200. ksadk/server/static/assets/{ArtifactsPanel-CakCqxEN.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
  201. ksadk/server/static/assets/{CodeBlock-CynLknyO.js → CodeBlock-DOH6MVcn.js} +4 -4
  202. ksadk/server/static/assets/{MathMessageMarkdown-3sQdOysS.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
  203. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  204. ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
  205. ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
  206. ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
  207. ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
  208. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
  209. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
  210. ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
  211. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
  212. ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
  213. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
  214. ksadk/server/static/assets/{chunk-FMBD7UC4-B_fH2YuH.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
  215. ksadk/server/static/assets/{chunk-ND2GUHAM-D6rFLaVV.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
  216. ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
  217. ksadk/server/static/assets/{chunk-4BX2VUAB-B9drTgOW.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
  218. ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
  219. ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
  220. ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
  221. ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
  222. ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
  223. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
  224. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
  225. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-ChNs_Mdk.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
  226. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
  227. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
  228. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  229. ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
  230. ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
  231. ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
  232. ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
  233. ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
  234. ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
  235. ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
  236. ksadk/server/static/assets/{dist-Dli7qW-B.js → dist-8hqcx0sU.js} +1 -1
  237. ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
  238. ksadk/server/static/assets/{dist-C1sz5Eo3.js → dist-BE7bu-DL.js} +1 -1
  239. ksadk/server/static/assets/{dist-fhOuWds5.js → dist-BVDffZ0U.js} +1 -1
  240. ksadk/server/static/assets/{dist-BjOdlDjj.js → dist-BX8z72IC.js} +1 -1
  241. ksadk/server/static/assets/{dist-DsHPdk3W.js → dist-Bf2M8m3N.js} +1 -1
  242. ksadk/server/static/assets/{dist-o2OVmKvt.js → dist-BjU6y-A2.js} +1 -1
  243. ksadk/server/static/assets/{dist-DWKByxWV.js → dist-BytlxIDT.js} +1 -1
  244. ksadk/server/static/assets/{dist-DJN_6Y39.js → dist-CSh_DE14.js} +1 -1
  245. ksadk/server/static/assets/{dist-DQU9EWw-.js → dist-CXYYBw3_.js} +1 -1
  246. ksadk/server/static/assets/{dist-4N2NMVYg.js → dist-C_wsv-Qd.js} +1 -1
  247. ksadk/server/static/assets/{dist-gLnrGsoU.js → dist-ClxGviKw.js} +1 -1
  248. ksadk/server/static/assets/{dist-j6Dxmf_j.js → dist-CttYUqzS.js} +1 -1
  249. ksadk/server/static/assets/{dist-D6MpCZtz.js → dist-DNp6rLRA.js} +1 -1
  250. ksadk/server/static/assets/{dist-BB3-OwTQ.js → dist-DUX-id_F.js} +1 -1
  251. ksadk/server/static/assets/{dist-_pHPTyZ-.js → dist-DmldrHd_.js} +1 -1
  252. ksadk/server/static/assets/{dist-CWV4lDr1.js → dist-DnfXs8Vn.js} +2 -2
  253. ksadk/server/static/assets/{dist-B5oS1zcj.js → dist-Dq9gLD7L.js} +1 -1
  254. ksadk/server/static/assets/{dist-B-jNR6Vn.js → dist-W-TIVntx.js} +1 -1
  255. ksadk/server/static/assets/{dist-BIlKoW0k.js → dist-dDdADKFA.js} +1 -1
  256. ksadk/server/static/assets/{dist-X4TnqwOK.js → dist-dydu68Fl.js} +1 -1
  257. ksadk/server/static/assets/{dist-CmZHzlwZ.js → dist-ngtN0DJB.js} +1 -1
  258. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
  259. ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
  260. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
  261. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
  262. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
  263. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
  264. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
  265. ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
  266. ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
  267. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
  268. ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
  269. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-K0OOfVna.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
  270. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-8LeOJaZp.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
  271. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-C6uCgZrW.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
  272. ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
  273. ksadk/server/static/assets/{linear-hdBcYzM8.js → linear-BHjG8b-J.js} +1 -1
  274. ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
  275. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-Bjy_2v4_.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
  276. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
  277. ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
  278. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
  279. ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
  280. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-BmkRbwCg.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
  281. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
  282. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
  283. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
  284. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
  285. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
  286. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
  287. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
  288. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-l6iXmU65.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
  289. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-Cq6UBeQB.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
  290. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
  291. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
  292. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
  293. ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
  294. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
  295. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-DEz24vV0.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
  296. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
  297. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
  298. ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
  299. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
  300. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
  301. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
  302. ksadk/server/static/index.html +2 -2
  303. ksadk/sessions/__init__.py +26 -9
  304. ksadk/sessions/_local_service_sync.py +688 -0
  305. ksadk/sessions/_local_tables.py +21 -0
  306. ksadk/sessions/_postgres_schema.py +305 -0
  307. ksadk/sessions/_postgres_tables.py +15 -0
  308. ksadk/sessions/base.py +80 -1
  309. ksadk/sessions/in_memory.py +53 -2
  310. ksadk/sessions/local_service.py +63 -604
  311. ksadk/sessions/postgres_service.py +58 -148
  312. ksadk/sessions/resilient.py +35 -1
  313. ksadk/studio/api.py +577 -25
  314. ksadk/studio/api_contracts.py +114 -5
  315. ksadk/studio/api_memory_routes.py +82 -0
  316. ksadk/studio/authoring.py +252 -27
  317. ksadk/studio/authoring_coordinator.py +799 -67
  318. ksadk/studio/builder.py +88 -11
  319. ksadk/studio/capabilities.py +17 -0
  320. ksadk/studio/cloud.py +1916 -95
  321. ksadk/studio/codex_agent_service.py +125 -14
  322. ksadk/studio/codex_authoring.py +469 -0
  323. ksadk/studio/codex_builder.py +138 -25
  324. ksadk/studio/codex_manifest.py +51 -22
  325. ksadk/studio/codex_run.py +46 -3
  326. ksadk/studio/compiler.py +1 -0
  327. ksadk/studio/contracts.py +114 -3
  328. ksadk/studio/event_store.py +25 -95
  329. ksadk/studio/framework_run.py +231 -3
  330. ksadk/studio/hosted_kernel.py +317 -0
  331. ksadk/studio/manifest_resolver.py +131 -0
  332. ksadk/studio/model_client.py +95 -9
  333. ksadk/studio/model_profile_service.py +1 -1
  334. ksadk/studio/operations.py +37 -5
  335. ksadk/studio/otel_trace.py +348 -42
  336. ksadk/studio/pcm_memory.py +85 -0
  337. ksadk/studio/repository.py +23 -2
  338. ksadk/studio/resource_catalog.py +43 -0
  339. ksadk/studio/run_service.py +563 -137
  340. ksadk/studio/runtime_source.py +42 -16
  341. ksadk/studio/service.py +1180 -72
  342. ksadk/studio/shared_web.py +32 -22
  343. ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
  344. ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
  345. ksadk/studio/static/index.html +2 -2
  346. ksadk/studio/templates.py +100 -8
  347. ksadk/studio/validator.py +19 -0
  348. ksadk/studio/workspace.py +30 -32
  349. ksadk/tracing/setup.py +304 -3
  350. ksadk/version.py +1 -1
  351. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  352. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  353. ksadk/events/parser.py +0 -191
  354. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  355. ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +0 -303
  356. ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +0 -1
  357. ksadk/server/static/assets/arc-DrgjcKhl.js +0 -1
  358. ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +0 -1
  359. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-BXoVRbQ6.js +0 -36
  360. ksadk/server/static/assets/blockDiagram-GPEHLZMM-BnaB3Ywv.js +0 -132
  361. ksadk/server/static/assets/c4Diagram-AAUBKEIU-Dw4weZcp.js +0 -10
  362. ksadk/server/static/assets/channel-Dt3rQTFI.js +0 -1
  363. ksadk/server/static/assets/chunk-2J33WTMH-ChjpLK7j.js +0 -1
  364. ksadk/server/static/assets/chunk-55IACEB6-CK4ROATy.js +0 -1
  365. ksadk/server/static/assets/chunk-727SXJPM-B9KtTTgw.js +0 -206
  366. ksadk/server/static/assets/chunk-AQP2D5EJ-1O1mki3m.js +0 -231
  367. ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +0 -1
  368. ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +0 -1
  369. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +0 -1
  370. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  371. ksadk/server/static/assets/dagre-BM42HDAG-D0QXg_MQ.js +0 -4
  372. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  373. ksadk/server/static/assets/diagram-2AECGRRQ-DBDDKwBQ.js +0 -43
  374. ksadk/server/static/assets/diagram-5GNKFQAL-BkwDOxmO.js +0 -10
  375. ksadk/server/static/assets/diagram-KO2AKTUF-Bv8waYvY.js +0 -3
  376. ksadk/server/static/assets/diagram-LMA3HP47-e1VECDdu.js +0 -24
  377. ksadk/server/static/assets/diagram-OG6HWLK6-Ds9adfRl.js +0 -24
  378. ksadk/server/static/assets/dist-B5J2uhNk.js +0 -1
  379. ksadk/server/static/assets/erDiagram-TEJ5UH35-BPItFKH6.js +0 -85
  380. ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +0 -1
  381. ksadk/server/static/assets/flowDiagram-I6XJVG4X-BbYB9w5Y.js +0 -162
  382. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-TQXa0KXv.js +0 -292
  383. ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +0 -1
  384. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-bKblCnpa.js +0 -106
  385. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  386. ksadk/server/static/assets/index-R52pJJvg.js +0 -177
  387. ksadk/server/static/assets/index-jJr3_955.css +0 -2
  388. ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +0 -1
  389. ksadk/server/static/assets/infoDiagram-5YYISTIA-BGVT7J_i.js +0 -2
  390. ksadk/server/static/assets/katex-CYWseY5E.js +0 -265
  391. ksadk/server/static/assets/mermaid-parser.core-BWrDjSZM.js +0 -161
  392. ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +0 -1
  393. ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +0 -1
  394. ksadk/server/static/assets/pieDiagram-4H26LBE5-GmmqNiVH.js +0 -30
  395. ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +0 -1
  396. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DSWFqL7c.js +0 -84
  397. ksadk/server/static/assets/stateDiagram-AJRCARHV-qzTQpg3Y.js +0 -1
  398. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +0 -1
  399. ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +0 -1
  400. ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +0 -1
  401. ksadk/server/static/assets/vennDiagram-CIIHVFJN-CMCixki2.js +0 -34
  402. ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +0 -1
  403. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-CVzB8F9K.js +0 -78
  404. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-BFBqBLc1.js +0 -7
  405. ksadk/studio/evaluation.py +0 -192
  406. ksadk/studio/static/assets/index-CaEN0eTR.css +0 -1
  407. ksadk/studio/static/assets/index-DI2eY_NH.js +0 -252
  408. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +0 -0
  409. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
  410. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
  411. {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
ksadk/kernel/worker.py ADDED
@@ -0,0 +1,953 @@
1
+ # -*- coding: utf-8 -*-
2
+ """per-session FIFO worker(Phase 1 Task 6 Step 6)。
3
+
4
+ - 持有 activation lease(fencing token 通过 Store 的 CAS 校验)才能 claim。
5
+ - 按 per-session accepted_seq 保序;active Run 存在时普通 enqueue 保持排队,
6
+ 只执行允许作用于该 Run 的控制命令(interrupt/pause/steer/...)。
7
+ - 异常分类:retryable kernel error(消息保持 claimed)、typed runtime
8
+ rejection(discarded + control.command_rejected)、terminal failure
9
+ (不 ack,消息保持 claimed 等待 takeover reclaim)。
10
+
11
+ Task 6:Activation 通过 :class:`ActiveExecution` 拥有 Adapter/RunHandle/
12
+ InteractionProvider——控制命令与 Interaction 回包永远作用于同一 live
13
+ execution(同一 client 实例);control lookup 永远按 durable run id。
14
+ ``submit_interaction`` 不再是静态 ``adapter.submit`` 映射:Worker 载入权威
15
+ ``InteractionRecord``,调用其绑定 provider 送达回包,provider 接受后才写
16
+ ``InteractionResolved``,同 fence 恢复 stream 消费。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import json
23
+ import logging
24
+ from collections.abc import Callable, Mapping
25
+ from dataclasses import dataclass, replace
26
+ from datetime import datetime, timezone
27
+ from typing import Literal
28
+
29
+ from ksadk.interaction.contracts import (
30
+ InteractionSubmission,
31
+ )
32
+ from ksadk.interaction.provider import (
33
+ RUNTIME_INTERACTION_UNAVAILABLE,
34
+ InteractionProvider,
35
+ InteractionResolveContext,
36
+ UnavailableInteractionProvider,
37
+ )
38
+ from ksadk.interaction.providers import default_interaction_providers
39
+ from ksadk.kernel.contracts import (
40
+ ActivationLease,
41
+ ActivationWriteGuard,
42
+ AgentControlCommand,
43
+ SubmitInteractionPayload,
44
+ )
45
+ from ksadk.kernel.contracts import (
46
+ InjectPayload as ContractInjectPayload,
47
+ )
48
+ from ksadk.kernel.contracts import (
49
+ SteerPayload as ContractSteerPayload,
50
+ )
51
+ from ksadk.kernel.errors import (
52
+ AgentKernelError,
53
+ InvalidCommandError,
54
+ StaleFenceError,
55
+ UnsupportedControlError,
56
+ )
57
+ from ksadk.kernel.mapping import COMMAND_HANDLERS, RESUME_TARGET_KINDS
58
+ from ksadk.kernel.state import RunState
59
+ from ksadk.kernel.store import (
60
+ AgentKernelStore,
61
+ RunRecord,
62
+ control_event,
63
+ new_message_id,
64
+ )
65
+ from ksadk.runtime.adapter import (
66
+ CancelResult,
67
+ PauseResult,
68
+ RunHandle,
69
+ RuntimeAdapter,
70
+ StartRequest,
71
+ )
72
+ from ksadk.runtime.adapter import (
73
+ ResumePayload as AdapterResumePayload,
74
+ )
75
+ from ksadk.runtime.adapter import (
76
+ ResumeTarget as AdapterResumeTarget,
77
+ )
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ WorkOutcome = Literal["idle", "claimed", "completed", "retryable_failure", "terminal_failure"]
83
+
84
+
85
+ @dataclass
86
+ class ActiveExecution:
87
+ """一个 activation 拥有的 live execution(Task 6 Step 4)。
88
+
89
+ owner 真相仍在 Store 的 RunRecord(durable run id);本结构只是当前
90
+ 进程持有 lease 期间的运行期句柄集合——adapter(含框架 client)、
91
+ live handle 与回包送达 provider 必须同源,否则回包会打到另一个
92
+ client 实例上静默丢失。
93
+ """
94
+
95
+ durable_run_id: str
96
+ runtime_run_id: str
97
+ adapter: RuntimeAdapter
98
+ handle: RunHandle
99
+ interaction_provider: InteractionProvider
100
+ stream_task: asyncio.Task[None] | None = None
101
+ stream_guard: ActivationWriteGuard | None = None
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class WorkResult:
106
+ """进程内调度结果,不是公网协议。idle 时后三项可为空。"""
107
+
108
+ outcome: WorkOutcome
109
+ message_id: str | None = None
110
+ run_id: str | None = None
111
+ last_seq: int | None = None
112
+
113
+
114
+ class AgentKernelWorker:
115
+ def __init__(
116
+ self,
117
+ store: AgentKernelStore,
118
+ *,
119
+ adapter_factory: Callable[[], RuntimeAdapter],
120
+ session_events: object | None = None,
121
+ interaction_providers: Mapping[str, InteractionProvider] | None = None,
122
+ start_request_defaults: Mapping[str, object] | None = None,
123
+ ) -> None:
124
+ self._store = store
125
+ self._adapter_factory = adapter_factory
126
+ # SessionEventStore(typed RuntimeEventStore 的 envelope 写路径)。
127
+ # 缺省时不落 runtime 事件,仅保证 stream 被消费到自然结束。
128
+ self._session_events = session_events
129
+ # Deployment-owned defaults (model, prompt and sandbox) come from the
130
+ # admitted immutable manifest. Server may attach a bounded per-turn
131
+ # model/approval selector to the signed command; the worker validates
132
+ # the model allow-list and never lets that selector replace sandbox.
133
+ self._start_request_defaults = dict(start_request_defaults or {})
134
+ # Task 6:activation 拥有 Adapter/RunHandle/Provider 的 live 表。
135
+ # key 永远是 durable run id;cache miss 不能等价于 Run 不存在
136
+ # (只能说明本进程未 attach,takeover 后由 adopt_execution 重建)。
137
+ self._executions: dict[str, ActiveExecution] = {}
138
+ self._providers: dict[str, InteractionProvider] = (
139
+ dict(interaction_providers)
140
+ if interaction_providers is not None
141
+ else default_interaction_providers()
142
+ )
143
+ # A stream may fail after its enqueue has been durably completed. Keep
144
+ # the exception observable to diagnostics without leaving an unhandled
145
+ # Task warning; the durable run remains open for recovery/takeover.
146
+ self._background_stream_errors: dict[str, Exception] = {}
147
+ # ``ActivationLease`` is a frozen wire contract and intentionally does
148
+ # not carry ``session_id``. Production composition roots therefore
149
+ # pass the session scope to ``run_once`` explicitly. Serialize that
150
+ # scope in-process as well: two scheduler ticks for the same Session
151
+ # must never both list/claim the Inbox head before either claim has
152
+ # completed. The durable activation/fence remains the cross-process
153
+ # authority; this lock closes the same-owner re-entrancy window.
154
+ self._session_locks: dict[tuple[str, str], asyncio.Lock] = {}
155
+
156
+ def attach_handle(
157
+ self,
158
+ run_id: str,
159
+ handle: RunHandle,
160
+ *,
161
+ adapter: RuntimeAdapter | None = None,
162
+ provider: InteractionProvider | None = None,
163
+ ) -> None:
164
+ """把 live handle 注册为当前 activation 的 ActiveExecution。"""
165
+
166
+ self.adopt_execution(
167
+ durable_run_id=run_id,
168
+ runtime_run_id=handle.run_id,
169
+ adapter=adapter if adapter is not None else self._adapter_factory(),
170
+ handle=handle,
171
+ provider=provider,
172
+ )
173
+
174
+ def adopt_execution(
175
+ self,
176
+ *,
177
+ durable_run_id: str,
178
+ runtime_run_id: str,
179
+ adapter: RuntimeAdapter,
180
+ handle: RunHandle,
181
+ provider: InteractionProvider | None = None,
182
+ ) -> ActiveExecution:
183
+ """takeover 后重建 ActiveExecution(仅在 lease 获取 + attach/resume
184
+ 成功后由 RecoveryCoordinator 调用;provider 按 runtime_type 解析)。"""
185
+
186
+ if provider is None:
187
+ provider = self._providers.get(handle.runtime_type, UnavailableInteractionProvider())
188
+ execution = ActiveExecution(
189
+ durable_run_id=durable_run_id,
190
+ runtime_run_id=runtime_run_id,
191
+ adapter=adapter,
192
+ handle=handle,
193
+ interaction_provider=provider,
194
+ )
195
+ self._executions[durable_run_id] = execution
196
+ return execution
197
+
198
+ def execution_for(self, durable_run_id: str) -> ActiveExecution | None:
199
+ """control lookup 入口:永远按 durable run id 查 live execution。"""
200
+
201
+ return self._executions.get(durable_run_id)
202
+
203
+ def active_session_ids(self) -> set[str]:
204
+ """Sessions whose activation must stay alive after Inbox ack.
205
+
206
+ An enqueue is acknowledged once ``adapter.start`` returns, while its
207
+ RuntimeEvent stream may continue for minutes. The composition root
208
+ uses this set to renew the lease during that interval; relying only on
209
+ accepted/claimed Inbox messages opens a stale-fence window mid-stream.
210
+ """
211
+
212
+ return {execution.handle.session_id for execution in self._executions.values()}
213
+
214
+ async def run_once(
215
+ self,
216
+ agent_instance_id: str,
217
+ activation: ActivationLease,
218
+ *,
219
+ session_id: str | None = None,
220
+ ) -> WorkResult:
221
+ if session_id is not None:
222
+ key = (agent_instance_id, session_id)
223
+ lock = self._session_locks.setdefault(key, asyncio.Lock())
224
+ async with lock:
225
+ return await self._run_once(agent_instance_id, activation, session_id=session_id)
226
+ # Compatibility for direct/test callers written before the internal
227
+ # scheduler API became session-scoped. Production callers below all
228
+ # provide ``session_id``; the frozen ActivationLease JSON is unchanged.
229
+ return await self._run_once(agent_instance_id, activation, session_id=None)
230
+
231
+ async def _run_once(
232
+ self,
233
+ agent_instance_id: str,
234
+ activation: ActivationLease,
235
+ *,
236
+ session_id: str | None,
237
+ ) -> WorkResult:
238
+ fence = activation.fencing_token
239
+ pending = await self._store.list_pending(
240
+ agent_instance_id, session_id=session_id, fencing_token=fence
241
+ )
242
+ if not pending:
243
+ return WorkResult(outcome="idle")
244
+
245
+ # per-session FIFO:通常按 accepted_seq 执行;但 active Run 会挡住
246
+ # enqueue,此时其后的 interrupt/pause/steer 等控制命令必须能越过
247
+ # 该 enqueue 作用于 active Run。只处理当前 activation 持有 lease 的
248
+ # session,避免跨 session 抢占。
249
+ eligible = None
250
+ for message in sorted(pending, key=lambda m: m.accepted_seq):
251
+ if session_id is not None and message.session_id != session_id:
252
+ continue
253
+ lease = await self._store.current_lease(agent_instance_id, message.session_id)
254
+ if lease is None or lease.activation_id != activation.activation_id:
255
+ continue # 该 session 归其它 activation(或无人)持有
256
+ if message.command is None: # pragma: no cover - defensive
257
+ continue
258
+ if message.command.command_type == "enqueue":
259
+ active = await self._store.find_active_run(agent_instance_id, message.session_id)
260
+ if active is not None:
261
+ continue # enqueue 保持排队
262
+ eligible = message
263
+ break
264
+ if eligible is None:
265
+ return WorkResult(outcome="idle")
266
+
267
+ claimed = await self._store.claim_message(eligible.message_id, fence)
268
+ result = await self._execute_claim(claimed.command, activation)
269
+ return result
270
+
271
+ # ------------------------------------------------------------- execution
272
+
273
+ async def _execute_claim(
274
+ self, command: AgentControlCommand, activation: ActivationLease
275
+ ) -> WorkResult:
276
+ fence = activation.fencing_token
277
+ message_id = await self._message_id_for(command)
278
+ try:
279
+ run_id = await self._dispatch(command, activation)
280
+ except (UnsupportedControlError, InvalidCommandError) as error:
281
+ # typed rejection:确定性收口,不重试。
282
+ await self._store.append_event(
283
+ control_event(
284
+ session_id=command.session_id,
285
+ event_type="control.command_rejected",
286
+ payload={
287
+ "command_id": str(command.command_id),
288
+ "status": "rejected",
289
+ "reason": getattr(error, "code", "unsupported"),
290
+ },
291
+ ),
292
+ expected_fence=fence,
293
+ agent_instance_id=command.agent_instance_id,
294
+ )
295
+ await self._store.discard_claim(message_id, expected_fence=fence)
296
+ return WorkResult(outcome="completed", message_id=message_id)
297
+ except StaleFenceError:
298
+ return WorkResult(outcome="terminal_failure", message_id=message_id)
299
+ except AgentKernelError as error:
300
+ if error.code == RUNTIME_INTERACTION_UNAVAILABLE:
301
+ # typed rejection:provider 诚实声明无法原生送达回包,
302
+ # Interaction 绝不标 resolved。
303
+ await self._store.append_event(
304
+ control_event(
305
+ session_id=command.session_id,
306
+ event_type="control.command_rejected",
307
+ payload={
308
+ "command_id": str(command.command_id),
309
+ "status": "rejected",
310
+ "reason": error.code,
311
+ },
312
+ ),
313
+ expected_fence=fence,
314
+ agent_instance_id=command.agent_instance_id,
315
+ )
316
+ await self._store.discard_claim(message_id, expected_fence=fence)
317
+ return WorkResult(outcome="completed", message_id=message_id)
318
+ if error.retryable:
319
+ return WorkResult(outcome="retryable_failure", message_id=message_id)
320
+ return WorkResult(outcome="terminal_failure", message_id=message_id)
321
+ except Exception:
322
+ # 未知异常绝不 ack 为成功:消息保持 claimed。
323
+ return WorkResult(outcome="terminal_failure", message_id=message_id)
324
+
325
+ try:
326
+ await self._store.complete_claim(message_id, expected_fence=fence)
327
+ except StaleFenceError:
328
+ return WorkResult(outcome="terminal_failure", message_id=message_id)
329
+ return WorkResult(outcome="completed", message_id=message_id, run_id=run_id)
330
+
331
+ async def _message_id_for(self, command: AgentControlCommand) -> str:
332
+ message = await self._store.load_by_idempotency(command.session_id, command.idempotency_key)
333
+ assert message is not None # claim 刚发生
334
+ return message.message_id
335
+
336
+ async def _dispatch(
337
+ self, command: AgentControlCommand, activation: ActivationLease
338
+ ) -> str | None:
339
+ handler = COMMAND_HANDLERS[command.command_type]
340
+ if handler == "start":
341
+ return await self._start_run(command, activation)
342
+ if handler == "submit_interaction":
343
+ return await self._control_active_run(command, activation)
344
+ return await self._control_active_run(command, activation)
345
+
346
+ # enqueue -> adapter.start,仅在没有 active Run 时到达这里。
347
+ async def _start_run(self, command: AgentControlCommand, activation: ActivationLease) -> str:
348
+ from ksadk.runtime.executor import handle_digest
349
+
350
+ fence = activation.fencing_token
351
+ guard = ActivationWriteGuard(activation_id=activation.activation_id, fencing_token=fence)
352
+ run_id = new_message_id()
353
+ adapter = self._adapter_factory()
354
+ pending = RunRecord(
355
+ run_id=run_id,
356
+ agent_instance_id=command.agent_instance_id,
357
+ session_id=command.session_id,
358
+ state=RunState.PENDING,
359
+ )
360
+ created = await self._store.save_run_transition(pending, expected_fence=fence)
361
+ continuation_metadata = await self._session_continuation_metadata(command.session_id)
362
+ defaults = self._start_request_defaults
363
+ runtime_options = command.payload.get("runtime_options")
364
+ if not isinstance(runtime_options, Mapping):
365
+ runtime_options = {}
366
+ default_model = str(defaults["model"]) if defaults.get("model") is not None else None
367
+ requested_model = str(runtime_options.get("model") or "").strip()
368
+ allowed_models = {
369
+ str(item).strip()
370
+ for item in (defaults.get("allowed_models") or [])
371
+ if str(item).strip()
372
+ }
373
+ # 显式 allowed_models 是收紧边界(不在名单的请求回落默认);
374
+ # 未声明名单 = 未设限制,run 级 model 覆盖直接生效(RunAgent Model 透传)。
375
+ selected_model = (
376
+ requested_model
377
+ if requested_model and (not allowed_models or requested_model in allowed_models)
378
+ else default_model
379
+ )
380
+ request_config = dict(defaults.get("config") or {})
381
+ approval_mode = str(runtime_options.get("tool_approval_mode") or "").strip().lower()
382
+ approval_overrides = {
383
+ "ask": "manual",
384
+ "risk": "auto_review",
385
+ }
386
+ if approval_mode in approval_overrides:
387
+ request_config["approval_mode"] = approval_overrides[approval_mode]
388
+ handle = await adapter.start(
389
+ StartRequest(
390
+ input=command.payload.get("content"),
391
+ user_id=str(command.tenant_id or "agent-kernel"),
392
+ session_id=command.session_id,
393
+ agent_id=str(defaults.get("agent_id") or command.agent_instance_id),
394
+ model=selected_model,
395
+ config=request_config,
396
+ # durable run_id 优先传给 adapter;adapter 不认时以
397
+ # runtime_run_id 映射显式记录两个 ID 的对应关系。
398
+ metadata={
399
+ "command_id": str(command.command_id),
400
+ "run_id": run_id,
401
+ **continuation_metadata,
402
+ },
403
+ )
404
+ )
405
+ running_update: dict = {
406
+ "state": RunState.RUNNING,
407
+ "handle": handle.model_dump(mode="json"),
408
+ "handle_digest": handle_digest(handle),
409
+ "tenant_id": command.tenant_id,
410
+ }
411
+ if handle.run_id != run_id:
412
+ running_update["runtime_run_id"] = handle.run_id
413
+ running = created.model_copy(update=running_update)
414
+ await self._store.save_run_transition(running, expected_fence=fence)
415
+ # 控制面始终用 durable RunRecord.run_id 查询;adapter 可以拒绝调用方
416
+ # 指定的 run id,因此绝不能以 runtime 私有 id 作为 cache key。
417
+ # Task 6:Activation 拥有 adapter + handle + provider。
418
+ execution = self.adopt_execution(
419
+ durable_run_id=run_id,
420
+ runtime_run_id=handle.run_id,
421
+ adapter=adapter,
422
+ handle=handle,
423
+ )
424
+ execution = self._start_stream(execution, running, guard)
425
+ # Keep the historical synchronous result for immediately exhausted
426
+ # streams (including deterministic test adapters), while a genuinely
427
+ # live stream runs in the background so it cannot block Inbox polling.
428
+ await asyncio.sleep(0)
429
+ if execution.stream_task is not None and execution.stream_task.done():
430
+ execution.stream_task.result()
431
+ return run_id
432
+
433
+ async def _session_continuation_metadata(self, session_id: str) -> dict[str, str]:
434
+ """Recover the latest native thread identity for a follow-up turn.
435
+
436
+ A durable Session owns multiple terminal Runs. Starting each enqueue
437
+ without the previous ``thread_resume`` continuation silently creates a
438
+ fresh provider conversation, so the UI appears multi-turn while the
439
+ model has no prior context. The canonical SessionEvent log is the
440
+ authority for this mapping and survives worker/process replacement.
441
+ """
442
+
443
+ if self._session_events is None:
444
+ return {}
445
+ from ksadk.events.canonical import ContinuationCreated, ContinuationResumed
446
+ from ksadk.events.canonical_store import RuntimeEventStore
447
+
448
+ events = await RuntimeEventStore(self._session_events).list(session_id, limit=256)
449
+ for event in reversed(events):
450
+ if not isinstance(event, (ContinuationCreated, ContinuationResumed)):
451
+ continue
452
+ if event.continuation_kind != "thread_resume":
453
+ continue
454
+ ref = getattr(event, "ref", None)
455
+ thread_id = str(ref.get("thread_id") or "").strip() if isinstance(ref, dict) else ""
456
+ if not thread_id:
457
+ thread_id = str(event.source.metadata.get("thread_id") or "").strip()
458
+ if thread_id:
459
+ return {"thread_id": thread_id}
460
+ return {}
461
+
462
+ def _start_stream(
463
+ self,
464
+ execution: ActiveExecution,
465
+ run: RunRecord,
466
+ guard: ActivationWriteGuard,
467
+ ) -> ActiveExecution:
468
+ """Start one non-blocking stream task owned by the current activation."""
469
+
470
+ current = self._executions.get(execution.durable_run_id)
471
+ if current is not None and current.stream_task is not None:
472
+ if not current.stream_task.done():
473
+ return current
474
+ execution = replace(current, stream_task=None, stream_guard=None)
475
+ task = asyncio.create_task(self._consume_stream(execution, run, guard))
476
+ updated = replace(execution, stream_task=task, stream_guard=guard)
477
+ self._executions[updated.durable_run_id] = updated
478
+ task.add_done_callback(
479
+ lambda done, run_id=updated.durable_run_id: self._observe_stream_task(run_id, done)
480
+ )
481
+ return updated
482
+
483
+ def _observe_stream_task(self, run_id: str, task: asyncio.Task[None]) -> None:
484
+ if task.cancelled():
485
+ return
486
+ try:
487
+ error = task.exception()
488
+ except asyncio.CancelledError: # pragma: no cover - defensive
489
+ return
490
+ if error is not None:
491
+ logger.error(
492
+ "background stream for run %s failed: %s: %s",
493
+ run_id,
494
+ type(error).__name__,
495
+ error,
496
+ )
497
+ self._background_stream_errors[run_id] = error
498
+ # A background stream has already left the Inbox claim path. Do
499
+ # not turn its failure into an invisible hung UI; keep the full
500
+ # traceback in workload logs while recovery turns the durable run
501
+ # into a terminal fact.
502
+ logger.exception(
503
+ "agent-kernel runtime stream failed for durable run %s (details=%s)",
504
+ run_id,
505
+ getattr(error, "details", {}),
506
+ exc_info=error,
507
+ )
508
+ # The failure happened after the Inbox claim had already been
509
+ # acknowledged, so no foreground owner remains to close this
510
+ # adapter. Leaving it in the live table leaks provider processes
511
+ # (notably one Codex app-server per failed turn) and makes later
512
+ # sessions stall behind stale transports. Preserve the durable
513
+ # open Run for recovery, but release this failed process-local
514
+ # attachment immediately.
515
+ execution = self._executions.get(run_id)
516
+ if execution is not None and execution.stream_task is task:
517
+ self._executions.pop(run_id, None)
518
+ cleanup = asyncio.create_task(
519
+ self._close_failed_execution(execution),
520
+ name=f"kernel-stream-cleanup:{run_id}",
521
+ )
522
+ cleanup.add_done_callback(self._observe_cleanup_task)
523
+
524
+ async def _close_failed_execution(self, execution: ActiveExecution) -> None:
525
+ try:
526
+ await execution.adapter.close(execution.handle)
527
+ except Exception: # noqa: BLE001
528
+ logger.exception(
529
+ "failed to close adapter after runtime stream error for durable run %s",
530
+ execution.durable_run_id,
531
+ )
532
+
533
+ @staticmethod
534
+ def _observe_cleanup_task(task: asyncio.Task[None]) -> None:
535
+ if task.cancelled():
536
+ return
537
+ try:
538
+ task.exception()
539
+ except asyncio.CancelledError: # pragma: no cover - defensive
540
+ return
541
+
542
+ async def _consume_stream(
543
+ self,
544
+ execution: ActiveExecution,
545
+ run: RunRecord,
546
+ guard: ActivationWriteGuard,
547
+ ) -> None:
548
+ """消费 run 的事件流并把每个事实落为 family=runtime/v2 事件。
549
+
550
+ 事件 run_id 统一改写为 durable run_id(adapter 私有 run_id 通过
551
+ RunRecord.metadata.runtime_run_id 记录映射)。stream 自然结束后
552
+ 才把 run 收口为 COMPLETED;任何异常原样上抛。
553
+ """
554
+
555
+ from ksadk.events.canonical import (
556
+ InteractionRequested,
557
+ InteractionResolved,
558
+ RunCompleted,
559
+ RunInterrupted,
560
+ SourceRef,
561
+ )
562
+
563
+ runtime_store = None
564
+ if self._session_events is not None:
565
+ # 延迟导入:ksadk.events 反向依赖 kernel.contracts,避免模块环。
566
+ from ksadk.events.canonical_store import RuntimeEventStore
567
+
568
+ runtime_store = RuntimeEventStore(self._session_events, session_id=run.session_id)
569
+ current_run = run
570
+ terminal_state: RunState | None = None
571
+ last_source: SourceRef | None = None
572
+ async for event in execution.adapter.stream(execution.handle):
573
+ if isinstance(event, InteractionRequested):
574
+ current_run = await self._record_interaction_request(
575
+ execution, current_run, event, guard
576
+ )
577
+ # The ledger's interaction/v1 SessionEvent is the single fact
578
+ # for this request. Do not append a second runtime/v2 copy.
579
+ continue
580
+ if isinstance(event, InteractionResolved):
581
+ # The submitted command's ledger transition is first-wins and
582
+ # already emitted interaction.resolved; ignore framework echo.
583
+ continue
584
+ if (
585
+ isinstance(event, RunInterrupted)
586
+ and event.interaction_id
587
+ and current_run.state is RunState.WAITING
588
+ ):
589
+ # Codex emits this immediately after InteractionRequested to
590
+ # describe a *temporarily blocked native turn*. The durable
591
+ # Kernel state for that condition is WAITING and the
592
+ # Interaction/v1 ledger is its authority. Treating the
593
+ # companion run.interrupted event as a terminal fact closes the
594
+ # process-local adapter before a human can answer, so the later
595
+ # SubmitInteraction receipt can never resolve. Do not publish
596
+ # a contradictory terminal runtime event; preserve the live
597
+ # execution until InteractionResolved resumes the same stream.
598
+ continue
599
+ if runtime_store is None:
600
+ continue
601
+ if event.run_id != current_run.run_id:
602
+ update: dict = {"run_id": run.run_id}
603
+ if getattr(event, "scope_id", None) == f"run:{execution.handle.run_id}":
604
+ update["scope_id"] = f"run:{run.run_id}"
605
+ event = event.model_copy(update=update)
606
+ last_source = event.source
607
+ await runtime_store.append(event, guard=guard)
608
+ terminal_state = {
609
+ "run.completed": RunState.COMPLETED,
610
+ "run.failed": RunState.FAILED,
611
+ "run.canceled": RunState.CANCELLED,
612
+ "run.interrupted": RunState.INTERRUPTED,
613
+ }.get(event.event_type)
614
+ if terminal_state is not None:
615
+ # App-server style providers keep their notification channel
616
+ # open across turns. A canonical terminal RuntimeEvent closes
617
+ # this run even when the transport itself does not produce
618
+ # EOF; waiting for EOF here leaves the durable run RUNNING and
619
+ # every later FIFO command queued forever.
620
+ break
621
+
622
+ # ``submit_interaction`` may resolve a live provider while this task is
623
+ # blocked in the framework stream. It transitions the durable run
624
+ # WAITING -> RUNNING, but ``current_run`` above is deliberately a
625
+ # local snapshot used to preserve event ordering. Refresh it before
626
+ # deciding whether natural stream exhaustion can settle the run;
627
+ # otherwise a Codex approval continuation finishes successfully but
628
+ # remains permanently WAITING because this task still sees its stale
629
+ # pre-response snapshot.
630
+ latest_run = await self._store.find_active_run(run.agent_instance_id, run.session_id)
631
+ if latest_run is not None and latest_run.run_id == current_run.run_id:
632
+ current_run = latest_run
633
+
634
+ # ``RuntimeAdapter`` is expected to emit a terminal RuntimeEvent, but
635
+ # a number of framework streams naturally exhaust after their last
636
+ # progress/item event. The Kernel is the lifecycle owner, so it must
637
+ # publish a fenced ``run.completed`` fact before recording COMPLETED.
638
+ # Otherwise foreground callers and Studio SSE wait forever even though
639
+ # the durable RunRecord says completion succeeded.
640
+ if terminal_state is None and current_run.state is not RunState.WAITING:
641
+ terminal_state = RunState.COMPLETED
642
+ if runtime_store is not None:
643
+ source = last_source or SourceRef(
644
+ framework="ksadk",
645
+ native_run_id=execution.runtime_run_id,
646
+ )
647
+ await runtime_store.append(
648
+ RunCompleted(
649
+ schema_version=2,
650
+ event_id=f"{current_run.run_id}:kernel-completed",
651
+ seq=0,
652
+ timestamp=datetime.now(timezone.utc).timestamp(),
653
+ run_id=current_run.run_id,
654
+ scope_id=f"run:{current_run.run_id}",
655
+ source=source,
656
+ status="completed",
657
+ output_refs=(),
658
+ ),
659
+ guard=guard,
660
+ )
661
+ if terminal_state is not None:
662
+ current_run = await self._store.save_run_transition(
663
+ current_run.model_copy(update={"state": terminal_state}),
664
+ expected_fence=guard.fencing_token,
665
+ )
666
+ current = self._executions.get(execution.durable_run_id)
667
+ if (
668
+ terminal_state is not None
669
+ and current is not None
670
+ and current.handle == execution.handle
671
+ ):
672
+ self._executions.pop(execution.durable_run_id, None)
673
+ if terminal_state is not None:
674
+ # Each enqueue owns the adapter instance created in ``_start_run``.
675
+ # Once its stream is terminal there is no live interaction left to
676
+ # preserve, so release the provider transport as part of that same
677
+ # lifecycle. Codex otherwise leaves one app-server child alive per
678
+ # turn; a later process trying to resume the persisted thread can
679
+ # then block behind the stale owner indefinitely.
680
+ try:
681
+ await execution.adapter.close(execution.handle)
682
+ except Exception: # noqa: BLE001
683
+ # The durable terminal event and RunRecord are already fenced
684
+ # and committed. A transport cleanup failure is observable but
685
+ # must not rewrite a successful run into a retryable command.
686
+ logger.exception(
687
+ "failed to close terminal runtime transport for durable run %s",
688
+ execution.durable_run_id,
689
+ )
690
+
691
+ async def _record_interaction_request(
692
+ self,
693
+ execution: ActiveExecution,
694
+ run: RunRecord,
695
+ event: object,
696
+ guard: ActivationWriteGuard,
697
+ ) -> RunRecord:
698
+ """Persist one framework interaction as the durable ledger authority."""
699
+
700
+ from ksadk.events.canonical import ApprovalRequest, InteractionRequested
701
+ from ksadk.interaction.contracts import InteractionPresentation, InteractionRecord
702
+
703
+ assert isinstance(event, InteractionRequested)
704
+ presentation = None
705
+ if isinstance(event.request, ApprovalRequest):
706
+ request_schema = {
707
+ "type": "object",
708
+ "properties": {
709
+ "decision": {
710
+ "type": "string",
711
+ "enum": ["approve", "reject"],
712
+ }
713
+ },
714
+ "required": ["decision"],
715
+ }
716
+ native_target = {"call_id": event.request.call_id or event.interaction_id}
717
+ detail = event.request.detail if isinstance(event.request.detail, Mapping) else {}
718
+ visible_arguments = {
719
+ key: detail[key]
720
+ for key in ("command", "cwd", "reason", "grantRoot", "proposedExecpolicyAmendment")
721
+ if key in detail and detail[key] is not None
722
+ }
723
+ presentation = InteractionPresentation(
724
+ title={
725
+ "command_execution": "run_command",
726
+ "file_change": "apply_patch",
727
+ "permissions": "request_permission",
728
+ "dynamic_tool_call": "tool_call",
729
+ }.get(event.request.kind, event.request.kind),
730
+ description=json.dumps(
731
+ {"arguments": visible_arguments},
732
+ ensure_ascii=False,
733
+ separators=(",", ":"),
734
+ ),
735
+ )
736
+ else:
737
+ request_schema = dict(event.request.schema_)
738
+ native_target = {"call_id": event.interaction_id}
739
+ for key in ("checkpoint_id", "thread_id"):
740
+ value = execution.handle.native_ref.get(key)
741
+ if value is not None:
742
+ native_target[key] = str(value)
743
+ provider_id = execution.interaction_provider.provider_id or execution.handle.runtime_type
744
+ record = InteractionRecord(
745
+ interaction_id=event.interaction_id,
746
+ tenant_id=str(run.metadata.get("tenant_id") or ""),
747
+ agent_instance_id=run.agent_instance_id,
748
+ session_id=run.session_id,
749
+ run_id=run.run_id,
750
+ kind=event.interaction_kind,
751
+ request_schema=request_schema,
752
+ created_at=datetime.fromtimestamp(event.timestamp, timezone.utc).isoformat(),
753
+ presentation=presentation,
754
+ provider_id=provider_id,
755
+ native_target=native_target,
756
+ continuation_metadata={"runtime_run_id": execution.runtime_run_id},
757
+ )
758
+ await self._store.request(record, guard=guard) # type: ignore[attr-defined]
759
+ if run.state is RunState.WAITING:
760
+ return run
761
+ return await self._store.save_run_transition(
762
+ run.model_copy(update={"state": RunState.WAITING}),
763
+ expected_fence=guard.fencing_token,
764
+ )
765
+
766
+ # 控制命令必须作用于 active Run 且本进程持有 live execution。
767
+ async def _control_active_run(
768
+ self, command: AgentControlCommand, activation: ActivationLease
769
+ ) -> str | None:
770
+ fence = activation.fencing_token
771
+ active = await self._store.find_active_run(command.agent_instance_id, command.session_id)
772
+ if active is None:
773
+ raise UnsupportedControlError(
774
+ "runtime_no_active_run",
775
+ details={"command_type": command.command_type},
776
+ )
777
+ execution = self._executions.get(active.run_id)
778
+ if execution is None:
779
+ raise UnsupportedControlError(
780
+ "runtime_not_attached",
781
+ details={"run_id": active.run_id},
782
+ )
783
+ # Task 6:控制命令作用在 activation 拥有的同一 adapter/handle 上,
784
+ # 绝不新建 adapter(那会把命令打到没有 live 状态的实例上)。
785
+ adapter = execution.adapter
786
+ handle = execution.handle
787
+ verb = COMMAND_HANDLERS[command.command_type]
788
+
789
+ if verb == "cancel":
790
+ result = await adapter.cancel(handle)
791
+ if result == CancelResult.INTERRUPTED_ACTIVE_TURN:
792
+ await self._transition_run(active, RunState.CANCELLED, fence)
793
+ self._executions.pop(active.run_id, None)
794
+ elif verb == "pause":
795
+ result = await adapter.pause(handle)
796
+ if result == PauseResult.PAUSED_ACTIVE_TURN:
797
+ await self._transition_run(active, RunState.PAUSED, fence)
798
+ elif verb == "resume":
799
+ target_dict = dict(command.payload.get("target") or {})
800
+ target = AdapterResumeTarget(
801
+ kind=RESUME_TARGET_KINDS[target_dict["kind"]],
802
+ id=str(target_dict["id"]),
803
+ )
804
+ resumed = await adapter.resume(handle, target, AdapterResumePayload(kind="free_text"))
805
+ execution = self._replace_handle(execution, resumed)
806
+ self._start_stream(
807
+ execution,
808
+ active,
809
+ ActivationWriteGuard(
810
+ activation_id=activation.activation_id,
811
+ fencing_token=fence,
812
+ ),
813
+ )
814
+ elif verb == "submit_interaction":
815
+ await self._submit_interaction(command, activation, active, execution)
816
+ elif verb == "steer":
817
+ await adapter.steer(handle, ContractSteerPayload.model_validate(dict(command.payload)))
818
+ elif verb == "inject":
819
+ await adapter.inject(
820
+ handle, ContractInjectPayload.model_validate(dict(command.payload))
821
+ )
822
+ else: # pragma: no cover - mapping 冻结
823
+ raise UnsupportedControlError(f"unknown handler {verb!r}")
824
+ return active.run_id
825
+
826
+ # ------------------------------------------------- Task 6: interaction 回包
827
+
828
+ async def _submit_interaction(
829
+ self,
830
+ command: AgentControlCommand,
831
+ activation: ActivationLease,
832
+ active: RunRecord,
833
+ execution: ActiveExecution,
834
+ ) -> None:
835
+ """权威 record -> 绑定 provider -> provider 接受后才写 resolved。
836
+
837
+ 顺序是合同:provider 拒绝(含 unavailable)时 Interaction 保持
838
+ pending,绝不提前标 resolved;ledger ``resolve`` 本身做 revision CAS
839
+ first-wins。
840
+ """
841
+
842
+ fence = activation.fencing_token
843
+ payload = SubmitInteractionPayload.model_validate(dict(command.payload))
844
+ record = await self._store.get( # type: ignore[attr-defined]
845
+ payload.interaction_id,
846
+ tenant_id=command.tenant_id,
847
+ agent_instance_id=command.agent_instance_id,
848
+ session_id=command.session_id,
849
+ run_id=active.run_id,
850
+ )
851
+ if record is None:
852
+ raise InvalidCommandError(
853
+ f"unknown interaction_id {payload.interaction_id!r}",
854
+ details={"interaction_id": payload.interaction_id},
855
+ )
856
+ if record.run_id != active.run_id:
857
+ raise InvalidCommandError(
858
+ "interaction does not belong to the active run",
859
+ details={
860
+ "interaction_id": record.interaction_id,
861
+ "interaction_run_id": record.run_id,
862
+ "active_run_id": active.run_id,
863
+ },
864
+ )
865
+ # ``ActiveExecution`` owns the adapter, live handle *and* provider for
866
+ # this activation. The durable record tells us what was requested,
867
+ # but it must not redirect a response into another framework provider:
868
+ # e.g. calling LangGraph checkpoint resume with a Codex live handle
869
+ # would acknowledge a response that can never reach the original run.
870
+ provider = execution.interaction_provider
871
+ if (
872
+ provider.provider_id != record.provider_id
873
+ or provider.mode == "unavailable"
874
+ ):
875
+ raise AgentKernelError(
876
+ RUNTIME_INTERACTION_UNAVAILABLE,
877
+ f"interaction provider {record.provider_id!r} cannot deliver "
878
+ "the response through the active execution's native framework "
879
+ "identity",
880
+ retryable=False,
881
+ details={
882
+ "provider_id": record.provider_id,
883
+ "active_provider_id": provider.provider_id,
884
+ "mode": provider.mode,
885
+ "interaction_id": record.interaction_id,
886
+ },
887
+ )
888
+ submission = InteractionSubmission(
889
+ interaction_id=record.interaction_id,
890
+ expected_revision=int(
891
+ payload.expected_revision
892
+ if payload.expected_revision is not None
893
+ else record.revision
894
+ ),
895
+ action=payload.action or "submit", # type: ignore[arg-type]
896
+ response=payload.response,
897
+ idempotency_key=payload.idempotency_key or command.idempotency_key,
898
+ )
899
+ context = InteractionResolveContext(
900
+ adapter=execution.adapter,
901
+ handle=execution.handle,
902
+ activation_id=activation.activation_id,
903
+ fencing_token=fence,
904
+ )
905
+ # provider 接受(typed 异常原样上抛 -> command_rejected,不标 resolved)。
906
+ resumed = await provider.resolve(context, record, submission)
907
+ execution = self._replace_handle(execution, resumed)
908
+ # provider 已接受,才在 ledger 收口 InteractionResolved(同一 fence)。
909
+ await self._store.resolve( # type: ignore[attr-defined]
910
+ submission,
911
+ guard=ActivationWriteGuard(activation_id=activation.activation_id, fencing_token=fence),
912
+ )
913
+ # A durable response returns a waiting run to execution. The old live
914
+ # stream usually remains open (Codex); checkpoint providers normally
915
+ # returned a fresh handle and need a new background stream. RUNNING is
916
+ # already the active-execution state (RUNNING -> RUNNING is not a legal
917
+ # transition), so only WAITING/PAUSED runs move back to RUNNING.
918
+ resumed_run = active
919
+ if active.state != RunState.RUNNING:
920
+ resumed_run = await self._transition_run(active, RunState.RUNNING, fence)
921
+ task = execution.stream_task
922
+ if task is None or task.done():
923
+ self._start_stream(
924
+ execution,
925
+ resumed_run,
926
+ ActivationWriteGuard(
927
+ activation_id=activation.activation_id,
928
+ fencing_token=fence,
929
+ ),
930
+ )
931
+
932
+ def _replace_handle(self, execution: ActiveExecution, handle: RunHandle) -> ActiveExecution:
933
+ if handle is execution.handle or handle == execution.handle:
934
+ return execution
935
+ if execution.stream_task is not None and not execution.stream_task.done():
936
+ execution.stream_task.cancel()
937
+ updated = replace(
938
+ execution,
939
+ handle=handle,
940
+ runtime_run_id=handle.run_id,
941
+ stream_task=None,
942
+ stream_guard=None,
943
+ )
944
+ self._executions[execution.durable_run_id] = updated
945
+ return updated
946
+
947
+ async def _transition_run(self, run: RunRecord, state: RunState, fence: int) -> RunRecord:
948
+ return await self._store.save_run_transition(
949
+ run.model_copy(update={"state": state}), expected_fence=fence
950
+ )
951
+
952
+
953
+ __all__ = ["ActiveExecution", "AgentKernelWorker", "WorkResult", "WorkOutcome"]