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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (494) hide show
  1. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
  2. agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
  3. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +2 -0
  5. ksadk/a2a/_space_client_events.py +381 -0
  6. ksadk/a2a/bootstrap.py +0 -2
  7. ksadk/a2a/card.py +6 -111
  8. ksadk/a2a/control_plane.py +239 -1
  9. ksadk/a2a/event_adapter.py +156 -64
  10. ksadk/a2a/executor.py +195 -172
  11. ksadk/a2a/routes.py +12 -5
  12. ksadk/a2a/server.py +1 -8
  13. ksadk/a2a/service_env.py +33 -0
  14. ksadk/a2a/space_client.py +43 -349
  15. ksadk/a2a_card.py +101 -0
  16. ksadk/a2ui/core.py +222 -53
  17. ksadk/agui/_agent_helpers.py +243 -0
  18. ksadk/agui/a2ui_projection.py +12 -5
  19. ksadk/agui/agent.py +474 -275
  20. ksadk/agui/config.py +11 -5
  21. ksadk/agui/routes.py +7 -15
  22. ksadk/api/client.py +530 -40
  23. ksadk/api/environment.py +27 -0
  24. ksadk/builders/code_builder.py +352 -53
  25. ksadk/builders/container_builder.py +71 -44
  26. ksadk/builders/framework_requirements.py +2 -1
  27. ksadk/builders/ks3_uploader.py +2 -2
  28. ksadk/builders/managed_runtime_builder.py +60 -28
  29. ksadk/cli/__init__.py +25 -0
  30. ksadk/cli/cmd_a2a.py +18 -31
  31. ksadk/cli/cmd_create.py +13 -26
  32. ksadk/cli/cmd_dashboard.py +15 -1
  33. ksadk/cli/cmd_deploy.py +28 -3
  34. ksadk/cli/cmd_eval.py +443 -0
  35. ksadk/cli/cmd_evalset.py +284 -0
  36. ksadk/cli/cmd_files.py +5 -1
  37. ksadk/cli/cmd_hermes.py +137 -163
  38. ksadk/cli/cmd_invoke.py +28 -38
  39. ksadk/cli/cmd_launch.py +12 -0
  40. ksadk/cli/cmd_managed_runtime.py +95 -0
  41. ksadk/cli/cmd_observe.py +85 -0
  42. ksadk/cli/cmd_openclaw.py +85 -479
  43. ksadk/cli/cmd_replay.py +28 -4
  44. ksadk/cli/cmd_run.py +29 -67
  45. ksadk/cli/cmd_studio.py +201 -0
  46. ksadk/cli/cmd_web.py +15 -6
  47. ksadk/cli/dry_run.py +2 -1
  48. ksadk/cli/env_options.py +83 -0
  49. ksadk/cli/global_options.py +3 -1
  50. ksadk/cli/hermes_env.py +151 -0
  51. ksadk/cli/invoke_payload.py +49 -0
  52. ksadk/cli/network_options.py +7 -5
  53. ksadk/cli/openclaw_env.py +458 -0
  54. ksadk/cli/runtime_bootstrap.py +51 -0
  55. ksadk/cli/ui.py +6 -0
  56. ksadk/cli/workflow_common.py +5 -5
  57. ksadk/codex/__init__.py +3 -3
  58. ksadk/codex/client.py +833 -49
  59. ksadk/codex/phase.py +1 -1
  60. ksadk/codex/runtime.py +720 -127
  61. ksadk/common/kop_client.py +232 -0
  62. ksadk/configs/__init__.py +1 -3
  63. ksadk/configs/env_registry.py +244 -45
  64. ksadk/configs/env_registry_pcm.py +181 -0
  65. ksadk/configs/env_var_spec.py +18 -0
  66. ksadk/configs/global_config.py +6 -1
  67. ksadk/configs/settings.py +5 -108
  68. ksadk/context_engine/__init__.py +98 -0
  69. ksadk/context_engine/assembler.py +176 -0
  70. ksadk/context_engine/baseline.py +419 -0
  71. ksadk/context_engine/cache_observability.py +228 -0
  72. ksadk/context_engine/capabilities.py +443 -0
  73. ksadk/context_engine/contributors.py +344 -0
  74. ksadk/context_engine/hosted_pipeline.py +417 -0
  75. ksadk/context_engine/models.py +117 -0
  76. ksadk/context_engine/planner.py +698 -0
  77. ksadk/context_engine/policies.py +319 -0
  78. ksadk/context_engine/projection.py +31 -0
  79. ksadk/context_engine/shadow_plan.py +266 -0
  80. ksadk/context_engine/tokenizer.py +164 -0
  81. ksadk/conversations/__init__.py +85 -94
  82. ksadk/conversations/context.py +126 -6
  83. ksadk/conversations/message_projection.py +292 -28
  84. ksadk/conversations/model_context.py +49 -3
  85. ksadk/conversations/runtime_compaction.py +207 -5
  86. ksadk/conversations/runtime_input.py +193 -3
  87. ksadk/conversations/runtime_invocation.py +81 -3
  88. ksadk/conversations/runtime_observability.py +157 -4
  89. ksadk/conversations/runtime_payloads.py +42 -0
  90. ksadk/conversations/runtime_preparation.py +280 -2
  91. ksadk/conversations/runtime_resume.py +71 -32
  92. ksadk/conversations/runtime_stream_events.py +94 -9
  93. ksadk/conversations/runtime_streaming.py +175 -22
  94. ksadk/conversations/semantic_summary.py +311 -1
  95. ksadk/conversations/session_lock.py +51 -0
  96. ksadk/deployment/env_forward.py +72 -0
  97. ksadk/deployment/managed_runtime.py +38 -0
  98. ksadk/deployment/manager.py +24 -8
  99. ksadk/deployment/providers/serverless.py +228 -92
  100. ksadk/evaluation/__init__.py +116 -0
  101. ksadk/evaluation/a2a_adapter.py +452 -0
  102. ksadk/evaluation/adapters.py +67 -0
  103. ksadk/evaluation/agent_eval_client.py +395 -0
  104. ksadk/evaluation/cloud_binding.py +80 -0
  105. ksadk/evaluation/cloud_converter.py +194 -0
  106. ksadk/evaluation/cloud_service.py +199 -0
  107. ksadk/evaluation/contracts.py +478 -0
  108. ksadk/evaluation/evalset.py +351 -0
  109. ksadk/evaluation/evaluators.py +627 -0
  110. ksadk/evaluation/evidence.py +258 -0
  111. ksadk/evaluation/executor.py +143 -0
  112. ksadk/evaluation/local_adapter.py +550 -0
  113. ksadk/evaluation/service_env.py +27 -0
  114. ksadk/evaluation/storage.py +68 -0
  115. ksadk/evaluation/studio_build_adapter.py +253 -0
  116. ksadk/evaluation/target.py +82 -0
  117. ksadk/events/__init__.py +13 -13
  118. ksadk/events/_v1_compat/__init__.py +1 -0
  119. ksadk/events/_v1_compat/models.py +299 -0
  120. ksadk/events/_v1_compat/parser.py +247 -0
  121. ksadk/events/_v1_compat/projection.py +753 -0
  122. ksadk/events/adapters/__init__.py +34 -0
  123. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  124. ksadk/events/adapters/_a2a_support.py +275 -0
  125. ksadk/events/adapters/_codex_interactions.py +375 -0
  126. ksadk/events/adapters/_codex_items.py +801 -0
  127. ksadk/events/adapters/_codex_validators.py +311 -0
  128. ksadk/events/adapters/_langgraph_support.py +785 -0
  129. ksadk/events/adapters/a2a.py +841 -0
  130. ksadk/events/adapters/adk.py +503 -0
  131. ksadk/events/adapters/codex.py +717 -0
  132. ksadk/events/adapters/langgraph.py +745 -0
  133. ksadk/events/canonical.py +440 -0
  134. ksadk/events/canonical_replay.py +497 -0
  135. ksadk/events/canonical_store.py +468 -0
  136. ksadk/events/cold_recovery.py +257 -0
  137. ksadk/events/content.py +89 -0
  138. ksadk/events/identity.py +86 -0
  139. ksadk/events/pipeline.py +455 -0
  140. ksadk/events/projections.py +66 -0
  141. ksadk/events/reducer.py +661 -0
  142. ksadk/events/replay.py +3 -36
  143. ksadk/events/runtime_event.py +20 -256
  144. ksadk/events/session_event.py +324 -0
  145. ksadk/events/store.py +3 -316
  146. ksadk/events/v1_compat.py +41 -0
  147. ksadk/harness/__init__.py +3 -2
  148. ksadk/harness/app.py +114 -123
  149. ksadk/harness/config.py +1 -1
  150. ksadk/harness/runtime.py +467 -0
  151. ksadk/hermes_terminal.py +5 -0
  152. ksadk/identity/resolver.py +4 -3
  153. ksadk/interaction/__init__.py +28 -0
  154. ksadk/interaction/contracts.py +136 -0
  155. ksadk/interaction/ledger.py +189 -0
  156. ksadk/interaction/provider.py +122 -0
  157. ksadk/interaction/providers/__init__.py +52 -0
  158. ksadk/interaction/providers/adk.py +40 -0
  159. ksadk/interaction/providers/codex.py +94 -0
  160. ksadk/interaction/providers/langgraph.py +58 -0
  161. ksadk/kernel/__init__.py +117 -0
  162. ksadk/kernel/authorization.py +258 -0
  163. ksadk/kernel/bootstrap.py +1074 -0
  164. ksadk/kernel/contract_fingerprints.py +61 -0
  165. ksadk/kernel/contracts.py +362 -0
  166. ksadk/kernel/control.py +219 -0
  167. ksadk/kernel/errors.py +79 -0
  168. ksadk/kernel/ingress.py +1102 -0
  169. ksadk/kernel/mapping.py +111 -0
  170. ksadk/kernel/memory_store.py +1133 -0
  171. ksadk/kernel/postgres_store.py +1756 -0
  172. ksadk/kernel/recovery.py +452 -0
  173. ksadk/kernel/runtime_identity.py +103 -0
  174. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  175. ksadk/kernel/sqlite_store.py +1410 -0
  176. ksadk/kernel/state.py +145 -0
  177. ksadk/kernel/store.py +232 -0
  178. ksadk/kernel/worker.py +953 -0
  179. ksadk/knowledge_base/client.py +22 -4
  180. ksadk/knowledge_base/service.py +32 -5
  181. ksadk/managed_a2a_card.py +160 -0
  182. ksadk/managed_runtime.py +16 -9
  183. ksadk/memory/__init__.py +61 -0
  184. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  185. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  186. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  187. ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
  188. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  189. ksadk/memory/adk/long_term_memory.py +3 -2
  190. ksadk/memory/coordinator.py +461 -0
  191. ksadk/memory/events.py +202 -0
  192. ksadk/memory/extraction.py +228 -0
  193. ksadk/memory/ltm_backend_factory.py +16 -3
  194. ksadk/memory/models.py +316 -0
  195. ksadk/memory/policy.py +215 -0
  196. ksadk/memory/provider.py +47 -0
  197. ksadk/memory/provider_adapter.py +159 -0
  198. ksadk/memory/provider_resolver.py +71 -0
  199. ksadk/memory/providers/__init__.py +5 -0
  200. ksadk/memory/providers/local_sqlite.py +490 -0
  201. ksadk/memory/resolved_policy.py +145 -0
  202. ksadk/memory/service.py +198 -8
  203. ksadk/memory/tool.py +4 -0
  204. ksadk/model_proxy/bootstrap.py +16 -9
  205. ksadk/model_proxy/config.py +22 -1
  206. ksadk/model_proxy/detect.py +202 -9
  207. ksadk/model_proxy/namespace.py +67 -15
  208. ksadk/model_proxy/server.py +168 -6
  209. ksadk/model_proxy/transform.py +105 -3
  210. ksadk/observability/__init__.py +25 -0
  211. ksadk/observability/session_log.py +420 -0
  212. ksadk/observability/trajectory.py +105 -0
  213. ksadk/prompts/__init__.py +71 -0
  214. ksadk/prompts/compiler.py +215 -0
  215. ksadk/prompts/models.py +82 -0
  216. ksadk/prompts/projection.py +89 -0
  217. ksadk/prompts/resolved.py +137 -0
  218. ksadk/prompts/sources.py +233 -0
  219. ksadk/runners/_langgraph_runner_streams.py +822 -0
  220. ksadk/runners/adk_runner.py +14 -31
  221. ksadk/runners/base_runner.py +44 -3
  222. ksadk/runners/factory.py +3 -3
  223. ksadk/runners/langgraph_runner.py +252 -433
  224. ksadk/runners/utils/__init__.py +0 -10
  225. ksadk/runners/utils/loader.py +16 -0
  226. ksadk/runtime/__init__.py +67 -41
  227. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  228. ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
  229. ksadk/runtime/adapter.py +199 -16
  230. ksadk/runtime/conversation_execution.py +687 -0
  231. ksadk/runtime/executor.py +392 -0
  232. ksadk/runtime/factory.py +342 -0
  233. ksadk/runtime/framework_adapters.py +6 -12
  234. ksadk/runtime/hosted_finalizer.py +268 -0
  235. ksadk/runtime/launch.py +61 -0
  236. ksadk/runtime/preprocessing.py +49 -14
  237. ksadk/runtime/runner_adapter.py +330 -391
  238. ksadk/runtime/runner_loading.py +36 -0
  239. ksadk/runtime/usage.py +33 -0
  240. ksadk/sandbox/backends/e2b.py +36 -3
  241. ksadk/server/__init__.py +39 -4
  242. ksadk/server/app.py +18 -492
  243. ksadk/server/composition.py +36 -2
  244. ksadk/server/factory.py +210 -149
  245. ksadk/server/routes/common.py +15 -46
  246. ksadk/server/routes/control.py +13 -9
  247. ksadk/server/routes/kernel_ingress.py +258 -0
  248. ksadk/server/routes/misc.py +10 -33
  249. ksadk/server/routes/models.py +14 -6
  250. ksadk/server/routes/openai_compat.py +129 -27
  251. ksadk/server/routes/projection.py +84 -16
  252. ksadk/server/routes/run.py +347 -460
  253. ksadk/server/routes/sessions.py +37 -40
  254. ksadk/server/routes/streaming.py +52 -7
  255. ksadk/server/routes/workspace.py +54 -21
  256. ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
  257. ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
  258. ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
  259. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  260. ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
  261. ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
  262. ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
  263. ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
  264. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
  265. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
  266. ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
  267. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
  268. ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
  269. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
  270. ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
  271. ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
  272. ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
  273. ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
  274. ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
  275. ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
  276. ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
  277. ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
  278. ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
  279. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
  280. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
  281. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
  282. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
  283. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
  284. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  285. ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
  286. ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
  287. ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
  288. ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
  289. ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
  290. ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
  291. ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
  292. ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
  293. ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
  294. ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
  295. ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
  296. ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
  297. ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
  298. ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
  299. ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
  300. ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
  301. ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
  302. ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
  303. ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
  304. ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
  305. ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
  306. ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
  307. ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
  308. ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
  309. ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
  310. ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
  311. ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
  312. ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
  313. ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
  314. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
  315. ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
  316. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
  317. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
  318. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
  319. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
  320. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
  321. ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
  322. ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
  323. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
  324. ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
  325. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
  326. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
  327. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
  328. ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
  329. ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
  330. ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
  331. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
  332. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
  333. ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
  334. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
  335. ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
  336. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
  337. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
  338. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
  339. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
  340. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
  341. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
  342. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
  343. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
  344. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
  345. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
  346. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
  347. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
  348. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
  349. ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
  350. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
  351. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
  352. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
  353. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
  354. ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
  355. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
  356. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
  357. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
  358. ksadk/server/static/index.html +2 -2
  359. ksadk/sessions/__init__.py +26 -9
  360. ksadk/sessions/_local_service_sync.py +688 -0
  361. ksadk/sessions/_local_tables.py +21 -0
  362. ksadk/sessions/_postgres_schema.py +305 -0
  363. ksadk/sessions/_postgres_tables.py +15 -0
  364. ksadk/sessions/base.py +80 -1
  365. ksadk/sessions/in_memory.py +53 -2
  366. ksadk/sessions/local_service.py +63 -604
  367. ksadk/sessions/postgres_service.py +58 -148
  368. ksadk/sessions/resilient.py +35 -1
  369. ksadk/skills/runtime/backends/e2b.py +1 -1
  370. ksadk/skills/service_client.py +13 -3
  371. ksadk/studio/__init__.py +5 -0
  372. ksadk/studio/agent_avatar_assets.py +120 -0
  373. ksadk/studio/agent_lifecycle.py +51 -0
  374. ksadk/studio/api.py +1598 -0
  375. ksadk/studio/api_catalog_routes.py +324 -0
  376. ksadk/studio/api_contracts.py +286 -0
  377. ksadk/studio/api_helpers.py +132 -0
  378. ksadk/studio/api_memory_routes.py +82 -0
  379. ksadk/studio/authoring.py +749 -0
  380. ksadk/studio/authoring_coordinator.py +1017 -0
  381. ksadk/studio/builder.py +397 -0
  382. ksadk/studio/capabilities.py +368 -0
  383. ksadk/studio/cloud.py +2099 -0
  384. ksadk/studio/codex_agent_service.py +694 -0
  385. ksadk/studio/codex_authoring.py +469 -0
  386. ksadk/studio/codex_builder.py +437 -0
  387. ksadk/studio/codex_manifest.py +307 -0
  388. ksadk/studio/codex_run.py +395 -0
  389. ksadk/studio/compiler.py +190 -0
  390. ksadk/studio/contracts.py +755 -0
  391. ksadk/studio/errors.py +47 -0
  392. ksadk/studio/event_store.py +214 -0
  393. ksadk/studio/framework_run.py +342 -0
  394. ksadk/studio/hosted_kernel.py +317 -0
  395. ksadk/studio/identifiers.py +33 -0
  396. ksadk/studio/manifest_resolver.py +131 -0
  397. ksadk/studio/mcp_runtime.py +229 -0
  398. ksadk/studio/model_client.py +629 -0
  399. ksadk/studio/model_profile_service.py +214 -0
  400. ksadk/studio/operations.py +275 -0
  401. ksadk/studio/otel_trace.py +1060 -0
  402. ksadk/studio/pagination.py +182 -0
  403. ksadk/studio/pcm_memory.py +85 -0
  404. ksadk/studio/python_tool_inspection.py +183 -0
  405. ksadk/studio/repository.py +327 -0
  406. ksadk/studio/resource_catalog.py +1260 -0
  407. ksadk/studio/run_service.py +1133 -0
  408. ksadk/studio/runtime_catalog.py +74 -0
  409. ksadk/studio/runtime_source.py +365 -0
  410. ksadk/studio/service.py +2249 -0
  411. ksadk/studio/shared_web.py +1004 -0
  412. ksadk/studio/skill_discovery.py +628 -0
  413. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  414. ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
  415. ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
  416. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  417. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  418. ksadk/studio/static/index.html +15 -0
  419. ksadk/studio/templates.py +722 -0
  420. ksadk/studio/validator.py +265 -0
  421. ksadk/studio/workspace.py +135 -0
  422. ksadk/tools/gateway.py +9 -0
  423. ksadk/toolsets/__init__.py +27 -1
  424. ksadk/toolsets/a2a.py +426 -0
  425. ksadk/tracing/__init__.py +19 -21
  426. ksadk/tracing/setup.py +317 -403
  427. ksadk/tracing/span_utils.py +0 -44
  428. ksadk/version.py +1 -1
  429. ksadk_runtime_common/memory_backend/registry.py +56 -56
  430. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  431. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  432. ksadk_runtime_common/workspace_files/router.py +8 -2
  433. agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
  434. ksadk/a2a/card_builder.py +0 -64
  435. ksadk/a2a/client.py +0 -309
  436. ksadk/events/parser.py +0 -191
  437. ksadk/harness/runner.py +0 -211
  438. ksadk/runners/codex_runner.py +0 -132
  439. ksadk/runners/utils/langfuse.py +0 -230
  440. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  441. ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
  442. ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
  443. ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
  444. ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
  445. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
  446. ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
  447. ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
  448. ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
  449. ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
  450. ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
  451. ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
  452. ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
  453. ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
  454. ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
  455. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
  456. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  457. ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
  458. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  459. ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
  460. ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
  461. ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
  462. ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
  463. ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
  464. ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
  465. ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
  466. ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
  467. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
  468. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
  469. ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
  470. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
  471. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  472. ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
  473. ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
  474. ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
  475. ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
  476. ksadk/server/static/assets/katex-DATssjp1.js +0 -265
  477. ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
  478. ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
  479. ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
  480. ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
  481. ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
  482. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
  483. ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
  484. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
  485. ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
  486. ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
  487. ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
  488. ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
  489. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
  490. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
  491. ksadk/tracing/exporters/langfuse_exporter.py +0 -460
  492. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
  493. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
  494. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1410 @@
1
+ # -*- coding: utf-8 -*-
2
+ """SQLite ``AgentKernelStore``(Phase 1 Task 3 Step 5)。
3
+
4
+ 单文件 durable Inbox / Run / ActivationLease 存储:
5
+ - WAL journal + 每次 mutation ``BEGIN IMMEDIATE`` 做跨进程 CAS;
6
+ - schema migration 用 ``PRAGMA user_version`` 整数版本,重复启动幂等;
7
+ - 所有 fence 比较都发生在同一个写事务内,不匹配抛 :class:`StaleFenceError`;
8
+ - ControlEvent/v1 经注入的 SessionEventStore 追加;accepted 事件在 kernel
9
+ 事务 commit 之前追加(persist-before-ack,见 :meth:`accept_command`),
10
+ 事件写入失败时回滚 Inbox。
11
+
12
+ 只面向单机本地部署(local dev / serverless pod 单写者场景);预发多写者
13
+ 场景由 Task 4 的 PostgreSQL 适配器承接。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import json
20
+ import time
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from uuid import uuid4
26
+
27
+ import aiosqlite
28
+
29
+ from ksadk.events.session_event import (
30
+ SessionEventStore,
31
+ SessionServiceEventStore,
32
+ envelope_to_session_event,
33
+ session_event_storage_id,
34
+ session_event_to_envelope,
35
+ validate_write_guard,
36
+ )
37
+ from ksadk.interaction.contracts import (
38
+ InteractionRecord,
39
+ InteractionReceipt,
40
+ InteractionSubmission,
41
+ is_terminal,
42
+ )
43
+ from ksadk.interaction.ledger import (
44
+ ALREADY_RESOLVED,
45
+ REVISION_MISMATCH,
46
+ REQUEST_CONFLICT,
47
+ interaction_event,
48
+ request_digest,
49
+ requested_event_payload,
50
+ resolve_outcome,
51
+ submission_digest,
52
+ )
53
+ from ksadk.kernel.contracts import (
54
+ ActivationLease,
55
+ ActivationWriteGuard,
56
+ AdmissionWriteGuard,
57
+ AgentControlCommand,
58
+ AgentControlReceipt,
59
+ ControlError,
60
+ SessionEventEnvelope,
61
+ )
62
+ from ksadk.kernel.errors import InvalidCommandError, StaleFenceError
63
+ from ksadk.kernel.state import (
64
+ InboxState,
65
+ assert_inbox_transition,
66
+ assert_run_transition,
67
+ is_active_run,
68
+ )
69
+ from ksadk.kernel.store import (
70
+ ActivationLeaseRequest,
71
+ InboxMessage,
72
+ RunRecord,
73
+ command_digest,
74
+ control_event,
75
+ new_message_id,
76
+ now_iso,
77
+ )
78
+ from ksadk.sessions._local_tables import KSADK_EVENTS_TABLE, KSADK_SESSIONS_TABLE
79
+ from ksadk.sessions.base import SessionEvent
80
+ from ksadk.sessions.local_service import LocalSessionService
81
+
82
+ SCHEMA_VERSION = 2
83
+
84
+ _SCHEMA = """
85
+ CREATE TABLE IF NOT EXISTS kernel_inbox (
86
+ message_id TEXT PRIMARY KEY,
87
+ agent_instance_id TEXT NOT NULL,
88
+ session_id TEXT NOT NULL,
89
+ idempotency_key TEXT NOT NULL,
90
+ request_digest TEXT NOT NULL,
91
+ accepted_seq INTEGER NOT NULL,
92
+ status TEXT NOT NULL CHECK (status IN ('accepted','claimed','completed','discarded')),
93
+ claimed_fence INTEGER,
94
+ payload_json TEXT NOT NULL,
95
+ UNIQUE(session_id, idempotency_key)
96
+ );
97
+ CREATE INDEX IF NOT EXISTS idx_kernel_inbox_claim
98
+ ON kernel_inbox (agent_instance_id, session_id, status, accepted_seq);
99
+ CREATE INDEX IF NOT EXISTS idx_kernel_inbox_idempotency
100
+ ON kernel_inbox (session_id, idempotency_key);
101
+
102
+ CREATE TABLE IF NOT EXISTS kernel_runs (
103
+ run_id TEXT PRIMARY KEY,
104
+ agent_instance_id TEXT NOT NULL,
105
+ session_id TEXT NOT NULL,
106
+ state TEXT NOT NULL CHECK (state IN (
107
+ 'pending','running','paused','waiting','completed','failed','cancelled','interrupted'
108
+ )),
109
+ activation_fence INTEGER NOT NULL DEFAULT 0,
110
+ created_at TEXT NOT NULL,
111
+ updated_at TEXT NOT NULL,
112
+ metadata_json TEXT NOT NULL DEFAULT '{}'
113
+ );
114
+ CREATE INDEX IF NOT EXISTS idx_kernel_runs_session_state
115
+ ON kernel_runs (session_id, state);
116
+
117
+ CREATE TABLE IF NOT EXISTS kernel_activations (
118
+ agent_instance_id TEXT NOT NULL,
119
+ session_id TEXT NOT NULL,
120
+ activation_id TEXT NOT NULL,
121
+ fencing_token INTEGER NOT NULL,
122
+ lease_expires_at REAL NOT NULL,
123
+ lease_expires_at_iso TEXT NOT NULL,
124
+ released INTEGER NOT NULL DEFAULT 0,
125
+ runtime_type TEXT NOT NULL DEFAULT 'ksadk',
126
+ bundle_digest TEXT NOT NULL DEFAULT '',
127
+ capability_digest TEXT NOT NULL DEFAULT '',
128
+ PRIMARY KEY (agent_instance_id, session_id)
129
+ );
130
+ CREATE INDEX IF NOT EXISTS idx_kernel_activations_expiry
131
+ ON kernel_activations (lease_expires_at);
132
+
133
+ CREATE TABLE IF NOT EXISTS kernel_accepted_seq (
134
+ session_id TEXT PRIMARY KEY,
135
+ last_seq INTEGER NOT NULL DEFAULT 0
136
+ );
137
+
138
+ CREATE TABLE IF NOT EXISTS kernel_interactions (
139
+ interaction_id TEXT NOT NULL,
140
+ tenant_id TEXT NOT NULL,
141
+ agent_instance_id TEXT NOT NULL,
142
+ session_id TEXT NOT NULL,
143
+ run_id TEXT NOT NULL,
144
+ kind TEXT NOT NULL CHECK (kind IN ('approval','structured_input','plan_review','custom')),
145
+ request_schema_json TEXT NOT NULL,
146
+ presentation_json TEXT,
147
+ revision INTEGER NOT NULL,
148
+ status TEXT NOT NULL CHECK (status IN (
149
+ 'pending','resolving','resolved','cancelled','expired'
150
+ )),
151
+ created_at TEXT NOT NULL,
152
+ expires_at TEXT,
153
+ provider_id TEXT NOT NULL DEFAULT '',
154
+ native_target_json TEXT,
155
+ continuation_json TEXT,
156
+ request_digest TEXT NOT NULL,
157
+ response_json TEXT,
158
+ outcome TEXT,
159
+ actor TEXT,
160
+ event_id TEXT,
161
+ accepted_seq INTEGER,
162
+ fencing_token INTEGER,
163
+ updated_at TEXT,
164
+ PRIMARY KEY (tenant_id, interaction_id)
165
+ );
166
+ CREATE INDEX IF NOT EXISTS idx_kernel_interactions_pending
167
+ ON kernel_interactions (tenant_id, session_id, status);
168
+
169
+ CREATE TABLE IF NOT EXISTS kernel_interaction_submissions (
170
+ tenant_id TEXT NOT NULL,
171
+ interaction_id TEXT NOT NULL,
172
+ idempotency_key TEXT NOT NULL,
173
+ submission_digest TEXT NOT NULL,
174
+ receipt_json TEXT NOT NULL,
175
+ PRIMARY KEY (tenant_id, interaction_id, idempotency_key)
176
+ );
177
+ """
178
+
179
+
180
+ class SQLiteAgentKernelStore:
181
+ def __init__(
182
+ self,
183
+ db_path: str | Path,
184
+ session_event_store: SessionEventStore,
185
+ ) -> None:
186
+ self.db_path = Path(db_path).expanduser().resolve()
187
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
188
+ self._events = session_event_store
189
+ self._write_lock = asyncio.Lock()
190
+ self._connection: aiosqlite.Connection | None = None
191
+ self._ready: asyncio.Future[None] | None = None
192
+
193
+ # ------------------------------------------------------------- lifecycle
194
+
195
+ async def _connect(self) -> aiosqlite.Connection:
196
+ if self._connection is None:
197
+ self._connection = await aiosqlite.connect(str(self.db_path))
198
+ self._connection.row_factory = aiosqlite.Row
199
+ await self._connection.execute("PRAGMA journal_mode=WAL")
200
+ await self._connection.execute("PRAGMA synchronous=FULL")
201
+ return self._connection
202
+
203
+ async def ensure_schema(self) -> None:
204
+ connection = await self._connect()
205
+ async with self._write_lock:
206
+ # CREATE ... IF NOT EXISTS + 整数 user_version,重复启动幂等。
207
+ await connection.executescript(_SCHEMA)
208
+ await connection.execute(f"PRAGMA user_version={SCHEMA_VERSION}")
209
+ await connection.commit()
210
+
211
+ async def close(self) -> None:
212
+ if self._connection is not None:
213
+ await self._connection.close()
214
+ self._connection = None
215
+
216
+ # ---------------------------------------------------------------- helpers
217
+
218
+ async def _begin(self) -> aiosqlite.Connection:
219
+ connection = await self._connect()
220
+ await connection.execute("BEGIN IMMEDIATE")
221
+ return connection
222
+
223
+ @staticmethod
224
+ async def _fetchone(connection: aiosqlite.Connection, sql: str, params: tuple) -> Any:
225
+ cursor = await connection.execute(sql, params)
226
+ row = await cursor.fetchone()
227
+ await cursor.close()
228
+ return row
229
+
230
+ @staticmethod
231
+ def _activation_row(row: aiosqlite.Row | None) -> dict[str, Any] | None:
232
+ if row is None or row["released"]:
233
+ return None
234
+ return dict(row)
235
+
236
+ async def _check_fence(
237
+ self, connection: aiosqlite.Connection, agent_instance_id: str, session_id: str,
238
+ expected_fence: int,
239
+ ) -> dict[str, Any]:
240
+ row = await self._fetchone(
241
+ connection,
242
+ "SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
243
+ (agent_instance_id, session_id),
244
+ )
245
+ activation = self._activation_row(row)
246
+ if (
247
+ activation is None
248
+ or activation["lease_expires_at"] <= time.time()
249
+ or activation["fencing_token"] != int(expected_fence)
250
+ ):
251
+ raise StaleFenceError(
252
+ "activation lease does not match expected fence",
253
+ details={
254
+ "agent_instance_id": agent_instance_id,
255
+ "session_id": session_id,
256
+ "expected_fence": int(expected_fence),
257
+ },
258
+ )
259
+ return activation
260
+
261
+ async def _emit_admission(
262
+ self, envelope: SessionEventEnvelope, command: AgentControlCommand
263
+ ) -> None:
264
+ # admission 事实的 guard 绑定提交方 permit 引用与 command_id。
265
+ await self._events.append(
266
+ envelope,
267
+ guard=AdmissionWriteGuard(
268
+ authorization_ref=command.authorization_ref,
269
+ command_id=command.command_id,
270
+ ),
271
+ )
272
+
273
+ async def _emit_activation(
274
+ self, envelope: SessionEventEnvelope, activation: dict[str, Any], fence: int
275
+ ) -> SessionEventEnvelope:
276
+ return await self._events.append(
277
+ envelope,
278
+ guard=ActivationWriteGuard(
279
+ activation_id=activation["activation_id"], fencing_token=int(fence)
280
+ ),
281
+ )
282
+
283
+ @staticmethod
284
+ def _receipt(
285
+ command: AgentControlCommand,
286
+ status: str,
287
+ *,
288
+ message_id: str | None = None,
289
+ accepted_seq: int | None = None,
290
+ error: ControlError | None = None,
291
+ ) -> AgentControlReceipt:
292
+ return AgentControlReceipt(
293
+ command_id=command.command_id,
294
+ status=status, # type: ignore[arg-type]
295
+ message_id=message_id,
296
+ accepted_seq=accepted_seq,
297
+ error=error,
298
+ )
299
+
300
+ # --------------------------------------------------------------- commands
301
+
302
+ async def accept_command(
303
+ self, command: AgentControlCommand, *, queue_limit: int
304
+ ) -> AgentControlReceipt:
305
+ if queue_limit < 1:
306
+ raise InvalidCommandError("queue_limit must be positive")
307
+ async with self._write_lock:
308
+ connection = await self._begin()
309
+ try:
310
+ existing = await self._fetchone(
311
+ connection,
312
+ "SELECT * FROM kernel_inbox WHERE session_id=? AND idempotency_key=?",
313
+ (command.session_id, command.idempotency_key),
314
+ )
315
+ if existing is not None:
316
+ if existing["request_digest"] != command_digest(command):
317
+ await connection.commit()
318
+ await self._emit_admission(
319
+ control_event(
320
+ session_id=command.session_id,
321
+ event_type="control.command_rejected",
322
+ payload={
323
+ "command_id": str(command.command_id),
324
+ "status": "rejected",
325
+ "reason": "idempotency_conflict",
326
+ },
327
+ causation_id=str(command.command_id),
328
+ ),
329
+ command,
330
+ )
331
+ return self._receipt(
332
+ command,
333
+ "rejected",
334
+ error=ControlError(
335
+ code="idempotency_conflict",
336
+ message=(
337
+ "idempotency key reused with a different request digest"
338
+ ),
339
+ retryable=False,
340
+ ),
341
+ )
342
+ await connection.commit()
343
+ return self._receipt(
344
+ command,
345
+ "duplicate",
346
+ message_id=existing["message_id"],
347
+ accepted_seq=existing["accepted_seq"],
348
+ )
349
+
350
+ depth_row = await self._fetchone(
351
+ connection,
352
+ "SELECT COUNT(*) AS depth FROM kernel_inbox "
353
+ "WHERE agent_instance_id=? AND session_id=? AND status='accepted'",
354
+ (command.agent_instance_id, command.session_id),
355
+ )
356
+ if depth_row["depth"] >= queue_limit:
357
+ await connection.commit()
358
+ await self._emit_admission(
359
+ control_event(
360
+ session_id=command.session_id,
361
+ event_type="control.command_rejected",
362
+ payload={
363
+ "command_id": str(command.command_id),
364
+ "status": "queue_full",
365
+ "queue_limit": queue_limit,
366
+ },
367
+ causation_id=str(command.command_id),
368
+ ),
369
+ command,
370
+ )
371
+ return self._receipt(
372
+ command,
373
+ "queue_full",
374
+ error=ControlError(
375
+ code="queue_full",
376
+ message=f"inbox reached queue_limit={queue_limit}",
377
+ retryable=True,
378
+ ),
379
+ )
380
+
381
+ seq_row = await self._fetchone(
382
+ connection,
383
+ "SELECT last_seq FROM kernel_accepted_seq WHERE session_id=?",
384
+ (command.session_id,),
385
+ )
386
+ accepted_seq = (seq_row["last_seq"] if seq_row else 0) + 1
387
+ message_id = new_message_id()
388
+ await connection.execute(
389
+ "INSERT INTO kernel_accepted_seq (session_id, last_seq) VALUES (?, ?) "
390
+ "ON CONFLICT(session_id) DO UPDATE SET last_seq=excluded.last_seq",
391
+ (command.session_id, accepted_seq),
392
+ )
393
+ await connection.execute(
394
+ "INSERT INTO kernel_inbox (message_id, agent_instance_id, session_id,"
395
+ " idempotency_key, request_digest, accepted_seq, status, claimed_fence,"
396
+ " payload_json) VALUES (?,?,?,?,?,?,?,?,?)",
397
+ (
398
+ message_id,
399
+ command.agent_instance_id,
400
+ command.session_id,
401
+ command.idempotency_key,
402
+ command_digest(command),
403
+ accepted_seq,
404
+ InboxState.ACCEPTED.value,
405
+ None,
406
+ command.model_dump_json(),
407
+ ),
408
+ )
409
+ # persist-before-ack:session 事件库与 kernel 库是两个独立
410
+ # SQLite 文件,无法共享一个事务。诚实取舍是在 kernel 事务
411
+ # commit 之前追加 accepted 事件:事件写入失败 -> 回滚 Inbox,
412
+ # 不产生 "persisted-but-untracked" 半状态,客户端可安全重试。
413
+ # 残余窗口:事件已追加但 kernel commit 崩溃 -> 出现一条孤儿
414
+ # accepted 事件而无 Inbox 行;该窗口不返回 ack,重试会重新
415
+ # 走完整路径(seq 单调,可能产生一条重复 accepted 事件),
416
+ # 不存在已 ack 但未持久化的状态。
417
+ await self._emit_admission(
418
+ control_event(
419
+ session_id=command.session_id,
420
+ event_type="control.command_accepted",
421
+ payload={
422
+ "command_id": str(command.command_id),
423
+ "status": "accepted",
424
+ "message_id": message_id,
425
+ "accepted_seq": accepted_seq,
426
+ "command_type": command.command_type,
427
+ },
428
+ causation_id=str(command.command_id),
429
+ ),
430
+ command,
431
+ )
432
+ await connection.commit()
433
+ except BaseException:
434
+ await connection.rollback()
435
+ raise
436
+ return self._receipt(
437
+ command, "accepted", message_id=message_id, accepted_seq=accepted_seq
438
+ )
439
+
440
+ async def load_message(self, message_id: str) -> InboxMessage | None:
441
+ connection = await self._connect()
442
+ row = await self._fetchone(
443
+ connection, "SELECT * FROM kernel_inbox WHERE message_id=?", (str(message_id),)
444
+ )
445
+ if row is None:
446
+ return None
447
+ return InboxMessage(
448
+ message_id=row["message_id"],
449
+ agent_instance_id=row["agent_instance_id"],
450
+ session_id=row["session_id"],
451
+ idempotency_key=row["idempotency_key"],
452
+ request_digest=row["request_digest"],
453
+ accepted_seq=row["accepted_seq"],
454
+ status=InboxState(row["status"]),
455
+ claimed_fence=row["claimed_fence"],
456
+ command=AgentControlCommand.model_validate_json(row["payload_json"]),
457
+ )
458
+
459
+ async def claim_next(
460
+ self, agent_instance_id: str, session_id: str, fencing_token: int
461
+ ) -> InboxMessage | None:
462
+ async with self._write_lock:
463
+ connection = await self._begin()
464
+ try:
465
+ activation = self._activation_row(await self._fetchone(
466
+ connection,
467
+ "SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
468
+ (agent_instance_id, session_id),
469
+ ))
470
+ if (
471
+ activation is None
472
+ or activation["lease_expires_at"] <= time.time()
473
+ or activation["fencing_token"] != int(fencing_token)
474
+ ):
475
+ raise StaleFenceError(
476
+ "activation lease does not match expected fence",
477
+ details={
478
+ "agent_instance_id": agent_instance_id,
479
+ "session_id": session_id,
480
+ "expected_fence": int(fencing_token),
481
+ },
482
+ )
483
+ row = await self._fetchone(
484
+ connection,
485
+ "SELECT * FROM kernel_inbox WHERE agent_instance_id=? AND session_id=? "
486
+ "AND (status='accepted' OR (status='claimed' AND claimed_fence != ?)) "
487
+ "ORDER BY accepted_seq LIMIT 1",
488
+ (agent_instance_id, session_id, int(fencing_token)),
489
+ )
490
+ if row is None:
491
+ await connection.commit()
492
+ return None
493
+ if row["status"] == InboxState.ACCEPTED.value:
494
+ assert_inbox_transition(InboxState(row["status"]), InboxState.CLAIMED)
495
+ await connection.execute(
496
+ "UPDATE kernel_inbox SET status='claimed', claimed_fence=? WHERE message_id=?",
497
+ (int(fencing_token), row["message_id"]),
498
+ )
499
+ await connection.commit()
500
+ except BaseException:
501
+ await connection.rollback()
502
+ raise
503
+ await self._emit_activation(
504
+ control_event(
505
+ session_id=session_id,
506
+ event_type="control.message_claimed",
507
+ payload={"message_id": row["message_id"], "fencing_token": int(fencing_token)},
508
+ ),
509
+ activation,
510
+ fencing_token,
511
+ )
512
+ return await self.load_message(row["message_id"])
513
+
514
+ async def complete_claim(self, message_id: str, *, expected_fence: int) -> None:
515
+ message_id = str(message_id)
516
+ async with self._write_lock:
517
+ connection = await self._begin()
518
+ try:
519
+ row = await self._fetchone(
520
+ connection, "SELECT * FROM kernel_inbox WHERE message_id=?", (message_id,)
521
+ )
522
+ if row is None:
523
+ raise InvalidCommandError(f"unknown message_id {message_id!r}")
524
+ activation = await self._check_fence(
525
+ connection, row["agent_instance_id"], row["session_id"], expected_fence
526
+ )
527
+ if (
528
+ row["status"] != InboxState.CLAIMED.value
529
+ or row["claimed_fence"] != int(expected_fence)
530
+ ):
531
+ raise StaleFenceError(
532
+ f"message {message_id!r} is not claimed at fence {expected_fence}"
533
+ )
534
+ assert_inbox_transition(InboxState(row["status"]), InboxState.COMPLETED)
535
+ await connection.execute(
536
+ "UPDATE kernel_inbox SET status='completed' WHERE message_id=?",
537
+ (message_id,),
538
+ )
539
+ await connection.commit()
540
+ except BaseException:
541
+ await connection.rollback()
542
+ raise
543
+ await self._emit_activation(
544
+ control_event(
545
+ session_id=row["session_id"],
546
+ event_type="control.message_completed",
547
+ payload={"message_id": message_id, "fencing_token": int(expected_fence)},
548
+ ),
549
+ activation,
550
+ expected_fence,
551
+ )
552
+
553
+ # -------------------------------------------------------------- interactions
554
+
555
+ async def _check_interaction_guard(
556
+ self,
557
+ connection: aiosqlite.Connection,
558
+ agent_instance_id: str,
559
+ session_id: str,
560
+ guard: ActivationWriteGuard,
561
+ ) -> dict[str, Any]:
562
+ row = await self._fetchone(
563
+ connection,
564
+ "SELECT * FROM kernel_activations WHERE activation_id=?",
565
+ (guard.activation_id,),
566
+ )
567
+ activation = self._activation_row(row)
568
+ if (
569
+ activation is None
570
+ or activation["lease_expires_at"] <= time.time()
571
+ or activation["fencing_token"] != int(guard.fencing_token)
572
+ or activation["agent_instance_id"] != agent_instance_id
573
+ or activation["session_id"] != session_id
574
+ ):
575
+ raise StaleFenceError(
576
+ "interaction write guard does not match the current lease",
577
+ details={
578
+ "activation_id": guard.activation_id,
579
+ "fencing_token": int(guard.fencing_token),
580
+ "session_id": session_id,
581
+ },
582
+ )
583
+ return activation
584
+
585
+ def _require_local_transactional_event_store(self) -> None:
586
+ """Ensure an Interaction fact shares this store's SQLite transaction.
587
+
588
+ ``SessionServiceEventStore(LocalSessionService)`` normally owns the
589
+ canonical local SessionEvent log. Giving the kernel a different file
590
+ would make a ledger row and its event independently committable, so it
591
+ is an invalid Interaction/v1 configuration rather than a best-effort
592
+ fallback. Other session backends remain usable for the pre-existing
593
+ non-transactional local control path, but not for durable interactions.
594
+ """
595
+
596
+ service = (
597
+ self._events.session_service
598
+ if isinstance(self._events, SessionServiceEventStore)
599
+ else None
600
+ )
601
+ if not isinstance(service, LocalSessionService) or service.db_path != self.db_path.resolve():
602
+ raise RuntimeError(
603
+ "SQLite InteractionLedger requires SessionEventStore backed by the "
604
+ "same SQLite database"
605
+ )
606
+
607
+ async def _append_interaction_event_on(
608
+ self,
609
+ connection: aiosqlite.Connection,
610
+ envelope: SessionEventEnvelope,
611
+ guard: ActivationWriteGuard,
612
+ ) -> SessionEventEnvelope:
613
+ """Append the canonical SessionEvent in the ledger writer transaction."""
614
+
615
+ self._require_local_transactional_event_store()
616
+ validate_write_guard(envelope, guard)
617
+ packed = envelope_to_session_event(envelope)
618
+ storage_id = session_event_storage_id(envelope.session_id, str(envelope.event_id))
619
+ session_row = await self._fetchone(
620
+ connection,
621
+ f"SELECT id FROM {KSADK_SESSIONS_TABLE} WHERE id=?",
622
+ (envelope.session_id,),
623
+ )
624
+ if session_row is None:
625
+ raise InvalidCommandError(
626
+ f"session {envelope.session_id!r} does not exist in the shared event log"
627
+ )
628
+ existing = await self._fetchone(
629
+ connection,
630
+ f"SELECT id, author, event_type, content_json, timestamp, seq_id,"
631
+ f" invocation_id, metadata_json FROM {KSADK_EVENTS_TABLE}"
632
+ " WHERE session_id=? AND id=?",
633
+ (envelope.session_id, storage_id),
634
+ )
635
+ if existing is not None:
636
+ stored = SessionEvent(
637
+ id=existing["id"],
638
+ session_id=envelope.session_id,
639
+ author=existing["author"],
640
+ event_type=existing["event_type"],
641
+ content=json.loads(existing["content_json"]),
642
+ timestamp=float(existing["timestamp"]),
643
+ seq_id=int(existing["seq_id"]),
644
+ invocation_id=existing["invocation_id"],
645
+ metadata=json.loads(existing["metadata_json"]),
646
+ )
647
+ persisted = session_event_to_envelope(stored)
648
+ if persisted is None: # pragma: no cover - only our packed rows use this id
649
+ raise RuntimeError("kernel interaction event lost its envelope marker")
650
+ SessionServiceEventStore._assert_same_fact(persisted, envelope)
651
+ return persisted
652
+
653
+ next_seq_row = await self._fetchone(
654
+ connection,
655
+ f"SELECT COALESCE(MAX(seq_id), 0) + 1 AS next_seq FROM {KSADK_EVENTS_TABLE}"
656
+ " WHERE session_id=?",
657
+ (envelope.session_id,),
658
+ )
659
+ next_seq = int(next_seq_row["next_seq"])
660
+ packed.bind_seq_id(next_seq)
661
+ await connection.execute(
662
+ f"INSERT INTO {KSADK_EVENTS_TABLE} ("
663
+ "id, session_id, author, event_type, content_json, timestamp, "
664
+ "state_delta_json, seq_id, invocation_id, metadata_json"
665
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
666
+ (
667
+ storage_id,
668
+ envelope.session_id,
669
+ packed.author,
670
+ packed.event_type,
671
+ json.dumps(packed.content, ensure_ascii=False),
672
+ packed.timestamp,
673
+ json.dumps(packed.state_delta, ensure_ascii=False),
674
+ next_seq,
675
+ packed.invocation_id,
676
+ json.dumps(packed.metadata, ensure_ascii=False),
677
+ ),
678
+ )
679
+ await connection.execute(
680
+ f"UPDATE {KSADK_SESSIONS_TABLE} SET updated_at=? WHERE id=?",
681
+ (time.time(), envelope.session_id),
682
+ )
683
+ persisted = session_event_to_envelope(packed)
684
+ if persisted is None: # pragma: no cover - packed by this method
685
+ raise RuntimeError("kernel interaction event lost its envelope marker")
686
+ return persisted
687
+
688
+ async def _interaction_row_for_guard(
689
+ self,
690
+ connection: aiosqlite.Connection,
691
+ interaction_id: str,
692
+ guard: ActivationWriteGuard,
693
+ ) -> aiosqlite.Row | None:
694
+ """Find a public id through the trusted activation scope, not by id alone."""
695
+
696
+ activation_row = await self._fetchone(
697
+ connection,
698
+ "SELECT * FROM kernel_activations WHERE activation_id=?",
699
+ (guard.activation_id,),
700
+ )
701
+ activation = self._activation_row(activation_row)
702
+ if (
703
+ activation is None
704
+ or activation["released"]
705
+ or activation["lease_expires_at"] <= time.time()
706
+ or activation["fencing_token"] != int(guard.fencing_token)
707
+ ):
708
+ raise StaleFenceError(
709
+ "interaction write guard does not match the current lease",
710
+ details={
711
+ "activation_id": guard.activation_id,
712
+ "fencing_token": int(guard.fencing_token),
713
+ },
714
+ )
715
+ return await self._fetchone(
716
+ connection,
717
+ "SELECT * FROM kernel_interactions WHERE interaction_id=?"
718
+ " AND agent_instance_id=? AND session_id=?",
719
+ (
720
+ interaction_id,
721
+ activation["agent_instance_id"],
722
+ activation["session_id"],
723
+ ),
724
+ )
725
+
726
+ @staticmethod
727
+ def _row_to_record(row: aiosqlite.Row | None) -> InteractionRecord | None:
728
+ if row is None:
729
+ return None
730
+ from ksadk.interaction.contracts import InteractionPresentation
731
+
732
+ presentation = None
733
+ if row["presentation_json"]:
734
+ presentation = InteractionPresentation.model_validate_json(
735
+ row["presentation_json"]
736
+ )
737
+ return InteractionRecord(
738
+ interaction_id=row["interaction_id"],
739
+ tenant_id=row["tenant_id"],
740
+ agent_instance_id=row["agent_instance_id"],
741
+ session_id=row["session_id"],
742
+ run_id=row["run_id"],
743
+ kind=row["kind"],
744
+ request_schema=json.loads(row["request_schema_json"]),
745
+ revision=int(row["revision"]),
746
+ status=row["status"],
747
+ created_at=row["created_at"],
748
+ expires_at=row["expires_at"],
749
+ presentation=presentation,
750
+ provider_id=row["provider_id"] or "",
751
+ native_target=(
752
+ json.loads(row["native_target_json"])
753
+ if row["native_target_json"]
754
+ else None
755
+ ),
756
+ continuation_metadata=(
757
+ json.loads(row["continuation_json"])
758
+ if row["continuation_json"]
759
+ else None
760
+ ),
761
+ )
762
+
763
+ def _record_values(self, record: InteractionRecord, *, request_digest_: str) -> tuple:
764
+ return (
765
+ record.interaction_id,
766
+ record.tenant_id,
767
+ record.agent_instance_id,
768
+ record.session_id,
769
+ record.run_id,
770
+ record.kind,
771
+ json.dumps(record.request_schema, ensure_ascii=False),
772
+ (
773
+ record.presentation.model_dump_json()
774
+ if record.presentation is not None
775
+ else None
776
+ ),
777
+ record.revision,
778
+ record.status,
779
+ record.created_at,
780
+ record.expires_at,
781
+ record.provider_id,
782
+ (
783
+ json.dumps(record.native_target, ensure_ascii=False)
784
+ if record.native_target is not None
785
+ else None
786
+ ),
787
+ (
788
+ json.dumps(record.continuation_metadata, ensure_ascii=False)
789
+ if record.continuation_metadata is not None
790
+ else None
791
+ ),
792
+ request_digest_,
793
+ now_iso(),
794
+ )
795
+
796
+ async def request(
797
+ self, record: InteractionRecord, *, guard: ActivationWriteGuard
798
+ ) -> InteractionRecord:
799
+ digest = request_digest(record)
800
+ async with self._write_lock:
801
+ connection = await self._begin()
802
+ try:
803
+ await self._check_interaction_guard(
804
+ connection, record.agent_instance_id, record.session_id, guard
805
+ )
806
+ existing = await self._fetchone(
807
+ connection,
808
+ "SELECT * FROM kernel_interactions WHERE tenant_id=? AND interaction_id=?",
809
+ (record.tenant_id, record.interaction_id),
810
+ )
811
+ if existing is not None:
812
+ if existing["request_digest"] != digest:
813
+ raise InvalidCommandError(
814
+ "interaction_id reused with a different request digest",
815
+ details={
816
+ "reason": REQUEST_CONFLICT,
817
+ "interaction_id": record.interaction_id,
818
+ },
819
+ )
820
+ await connection.commit()
821
+ stored = self._row_to_record(existing)
822
+ assert stored is not None
823
+ return stored
824
+ await connection.execute(
825
+ "INSERT INTO kernel_interactions (interaction_id, tenant_id,"
826
+ " agent_instance_id, session_id, run_id, kind, request_schema_json,"
827
+ " presentation_json, revision, status, created_at, expires_at,"
828
+ " provider_id, native_target_json, continuation_json,"
829
+ " request_digest, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
830
+ self._record_values(record, request_digest_=digest),
831
+ )
832
+ # pending 行与 canonical requested 事实共用同一 SQLite commit。
833
+ await self._append_interaction_event_on(
834
+ connection, requested_event_payload(record, now_iso()), guard
835
+ )
836
+ await connection.commit()
837
+ except BaseException:
838
+ await connection.rollback()
839
+ raise
840
+ return record
841
+
842
+ async def resolve(
843
+ self, submission: InteractionSubmission, *, guard: ActivationWriteGuard
844
+ ) -> InteractionReceipt:
845
+ sub_digest = submission_digest(submission)
846
+ async with self._write_lock:
847
+ connection = await self._begin()
848
+ try:
849
+ row = await self._interaction_row_for_guard(
850
+ connection, submission.interaction_id, guard
851
+ )
852
+ if row is None:
853
+ raise InvalidCommandError(
854
+ f"unknown interaction_id {submission.interaction_id!r}"
855
+ )
856
+ await self._check_interaction_guard(
857
+ connection, row["agent_instance_id"], row["session_id"], guard
858
+ )
859
+ current = self._row_to_record(row)
860
+ assert current is not None
861
+ if is_terminal(current.status):
862
+ existing_sub = await self._fetchone(
863
+ connection,
864
+ "SELECT * FROM kernel_interaction_submissions WHERE tenant_id=?"
865
+ " AND interaction_id=? AND idempotency_key=?",
866
+ (
867
+ current.tenant_id,
868
+ current.interaction_id,
869
+ submission.idempotency_key,
870
+ ),
871
+ )
872
+ if (
873
+ existing_sub is not None
874
+ and existing_sub["submission_digest"] == sub_digest
875
+ ):
876
+ await connection.commit()
877
+ return InteractionReceipt.model_validate_json(
878
+ existing_sub["receipt_json"]
879
+ )
880
+ raise InvalidCommandError(
881
+ "interaction already reached terminal status"
882
+ f" {current.status!r}",
883
+ details={
884
+ "reason": ALREADY_RESOLVED,
885
+ "interaction_id": current.interaction_id,
886
+ },
887
+ )
888
+ if current.revision != submission.expected_revision:
889
+ raise InvalidCommandError(
890
+ "interaction revision does not match expected_revision",
891
+ details={
892
+ "reason": REVISION_MISMATCH,
893
+ "interaction_id": current.interaction_id,
894
+ "expected_revision": submission.expected_revision,
895
+ "current_revision": current.revision,
896
+ },
897
+ )
898
+ outcome = resolve_outcome(submission.action)
899
+ updated = current.model_copy(
900
+ update={"status": "resolved", "revision": current.revision + 1}
901
+ )
902
+ stored = await self._append_interaction_event_on(
903
+ connection,
904
+ interaction_event(
905
+ updated,
906
+ event_type="interaction.resolved",
907
+ timestamp=now_iso(),
908
+ outcome=outcome,
909
+ response=submission.response,
910
+ actor_ref="user",
911
+ ),
912
+ guard,
913
+ )
914
+ receipt = InteractionReceipt(
915
+ interaction_id=updated.interaction_id,
916
+ revision=updated.revision,
917
+ status="resolved",
918
+ outcome=outcome, # type: ignore[arg-type]
919
+ event_id=str(stored.event_id),
920
+ accepted_seq=stored.seq,
921
+ )
922
+ await connection.execute(
923
+ "UPDATE kernel_interactions SET revision=?, status=?,"
924
+ " response_json=?, outcome=?, actor=?, event_id=?, accepted_seq=?,"
925
+ " fencing_token=?, updated_at=? WHERE tenant_id=? AND interaction_id=?",
926
+ (
927
+ updated.revision,
928
+ "resolved",
929
+ json.dumps(submission.response, ensure_ascii=False),
930
+ outcome,
931
+ "user",
932
+ str(stored.event_id),
933
+ stored.seq,
934
+ int(guard.fencing_token),
935
+ now_iso(),
936
+ updated.tenant_id,
937
+ updated.interaction_id,
938
+ ),
939
+ )
940
+ await connection.execute(
941
+ "INSERT INTO kernel_interaction_submissions (tenant_id,"
942
+ " interaction_id, idempotency_key, submission_digest, receipt_json)"
943
+ " VALUES (?,?,?,?,?) ON CONFLICT DO NOTHING",
944
+ (
945
+ updated.tenant_id,
946
+ updated.interaction_id,
947
+ submission.idempotency_key,
948
+ sub_digest,
949
+ receipt.model_dump_json(),
950
+ ),
951
+ )
952
+ await connection.commit()
953
+ except BaseException:
954
+ await connection.rollback()
955
+ raise
956
+ return receipt
957
+
958
+ async def _terminal_command(
959
+ self,
960
+ interaction_id: str,
961
+ expected_revision: int,
962
+ *,
963
+ guard: ActivationWriteGuard,
964
+ status: str,
965
+ reason: str,
966
+ ) -> InteractionReceipt:
967
+ async with self._write_lock:
968
+ connection = await self._begin()
969
+ try:
970
+ row = await self._interaction_row_for_guard(
971
+ connection, interaction_id, guard
972
+ )
973
+ if row is None:
974
+ raise InvalidCommandError(
975
+ f"unknown interaction_id {interaction_id!r}"
976
+ )
977
+ await self._check_interaction_guard(
978
+ connection, row["agent_instance_id"], row["session_id"], guard
979
+ )
980
+ current = self._row_to_record(row)
981
+ assert current is not None
982
+ if is_terminal(current.status):
983
+ raise InvalidCommandError(
984
+ f"interaction already reached terminal status"
985
+ f" {current.status!r}",
986
+ details={
987
+ "reason": ALREADY_RESOLVED,
988
+ "interaction_id": current.interaction_id,
989
+ },
990
+ )
991
+ if current.revision != expected_revision:
992
+ raise InvalidCommandError(
993
+ "interaction revision does not match expected_revision",
994
+ details={
995
+ "reason": REVISION_MISMATCH,
996
+ "interaction_id": current.interaction_id,
997
+ "expected_revision": expected_revision,
998
+ "current_revision": current.revision,
999
+ },
1000
+ )
1001
+ updated = current.model_copy(
1002
+ update={"status": status, "revision": current.revision + 1}
1003
+ )
1004
+ event_type = (
1005
+ "interaction.cancelled"
1006
+ if status == "cancelled"
1007
+ else "interaction.expired"
1008
+ )
1009
+ stored = await self._append_interaction_event_on(
1010
+ connection,
1011
+ interaction_event(
1012
+ updated,
1013
+ event_type=event_type,
1014
+ timestamp=now_iso(),
1015
+ reason=reason,
1016
+ ),
1017
+ guard,
1018
+ )
1019
+ receipt = InteractionReceipt(
1020
+ interaction_id=updated.interaction_id,
1021
+ revision=updated.revision,
1022
+ status=updated.status, # type: ignore[arg-type]
1023
+ outcome=updated.status, # type: ignore[arg-type]
1024
+ event_id=str(stored.event_id),
1025
+ accepted_seq=stored.seq,
1026
+ )
1027
+ await connection.execute(
1028
+ "UPDATE kernel_interactions SET revision=?, status=?, outcome=?,"
1029
+ " event_id=?, accepted_seq=?, fencing_token=?, updated_at=?"
1030
+ " WHERE tenant_id=? AND interaction_id=?",
1031
+ (
1032
+ updated.revision,
1033
+ status,
1034
+ status,
1035
+ str(stored.event_id),
1036
+ stored.seq,
1037
+ int(guard.fencing_token),
1038
+ now_iso(),
1039
+ updated.tenant_id,
1040
+ updated.interaction_id,
1041
+ ),
1042
+ )
1043
+ await connection.commit()
1044
+ except BaseException:
1045
+ await connection.rollback()
1046
+ raise
1047
+ return receipt
1048
+
1049
+ async def cancel(
1050
+ self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
1051
+ ) -> InteractionReceipt:
1052
+ return await self._terminal_command(
1053
+ interaction_id,
1054
+ expected_revision,
1055
+ guard=guard,
1056
+ status="cancelled",
1057
+ reason="cancelled by owner",
1058
+ )
1059
+
1060
+ async def expire(
1061
+ self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
1062
+ ) -> InteractionReceipt:
1063
+ return await self._terminal_command(
1064
+ interaction_id,
1065
+ expected_revision,
1066
+ guard=guard,
1067
+ status="expired",
1068
+ reason="interaction expired",
1069
+ )
1070
+
1071
+ async def get(
1072
+ self,
1073
+ interaction_id: str,
1074
+ *,
1075
+ tenant_id: str | None = None,
1076
+ agent_instance_id: str | None = None,
1077
+ session_id: str | None = None,
1078
+ run_id: str | None = None,
1079
+ ) -> InteractionRecord | None:
1080
+ """Return an interaction only inside a complete trusted scope.
1081
+
1082
+ An omitted scope is retained for local compatibility but is fail-closed
1083
+ when the opaque id exists in more than one tenant. Partial scope is
1084
+ never sufficient for a security-sensitive worker lookup.
1085
+ """
1086
+
1087
+ scope = (tenant_id, agent_instance_id, session_id, run_id)
1088
+ connection = await self._connect()
1089
+ if any(value is not None for value in scope):
1090
+ if not all(value is not None for value in scope):
1091
+ raise InvalidCommandError(
1092
+ "interaction lookup requires a complete trusted scope",
1093
+ details={"interaction_id": interaction_id},
1094
+ )
1095
+ row = await self._fetchone(
1096
+ connection,
1097
+ "SELECT * FROM kernel_interactions WHERE interaction_id=?"
1098
+ " AND tenant_id=? AND agent_instance_id=? AND session_id=? AND run_id=?",
1099
+ (interaction_id, tenant_id, agent_instance_id, session_id, run_id),
1100
+ )
1101
+ return self._row_to_record(row)
1102
+ cursor = await connection.execute(
1103
+ "SELECT * FROM kernel_interactions WHERE interaction_id=? LIMIT 2",
1104
+ (interaction_id,),
1105
+ )
1106
+ rows = await cursor.fetchall()
1107
+ await cursor.close()
1108
+ if len(rows) > 1:
1109
+ raise InvalidCommandError(
1110
+ f"interaction_id {interaction_id!r} is ambiguous without trusted scope",
1111
+ details={"reason": REQUEST_CONFLICT, "interaction_id": interaction_id},
1112
+ )
1113
+ return self._row_to_record(rows[0]) if rows else None
1114
+
1115
+ async def list_pending_interactions(
1116
+ self, tenant_id: str, session_id: str
1117
+ ) -> list[InteractionRecord]:
1118
+ connection = await self._connect()
1119
+ cursor = await connection.execute(
1120
+ "SELECT * FROM kernel_interactions WHERE tenant_id=? AND session_id=?"
1121
+ " AND status='pending' ORDER BY created_at",
1122
+ (tenant_id, session_id),
1123
+ )
1124
+ rows = await cursor.fetchall()
1125
+ await cursor.close()
1126
+ records = [self._row_to_record(row) for row in rows]
1127
+ return [r for r in records if r is not None]
1128
+
1129
+ # ------------------------------------------------------------- activations
1130
+
1131
+ async def acquire_activation(self, request: ActivationLeaseRequest) -> ActivationLease:
1132
+ async with self._write_lock:
1133
+ connection = await self._begin()
1134
+ try:
1135
+ row = await self._fetchone(
1136
+ connection,
1137
+ "SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
1138
+ (request.agent_instance_id, request.session_id),
1139
+ )
1140
+ expires_at = time.time() + request.lease_ttl_seconds
1141
+ if row is None:
1142
+ token = 1
1143
+ elif row["released"] or row["lease_expires_at"] <= time.time():
1144
+ token = row["fencing_token"] + 1
1145
+ elif row["activation_id"] == request.activation_id:
1146
+ token = row["fencing_token"]
1147
+ else:
1148
+ raise InvalidCommandError(
1149
+ "activation lease is still held by another owner",
1150
+ details={
1151
+ "holder": row["activation_id"],
1152
+ "lease_expires_at": row["lease_expires_at_iso"],
1153
+ },
1154
+ )
1155
+ expires_iso = datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat()
1156
+ await connection.execute(
1157
+ "INSERT INTO kernel_activations (agent_instance_id, session_id,"
1158
+ " activation_id, fencing_token, lease_expires_at, lease_expires_at_iso,"
1159
+ " released, runtime_type, bundle_digest, capability_digest)"
1160
+ " VALUES (?,?,?,?,?,?,0,?,?,?)"
1161
+ " ON CONFLICT(agent_instance_id, session_id) DO UPDATE SET"
1162
+ " activation_id=excluded.activation_id,"
1163
+ " fencing_token=excluded.fencing_token,"
1164
+ " lease_expires_at=excluded.lease_expires_at,"
1165
+ " lease_expires_at_iso=excluded.lease_expires_at_iso,"
1166
+ " released=0, runtime_type=excluded.runtime_type,"
1167
+ " bundle_digest=excluded.bundle_digest,"
1168
+ " capability_digest=excluded.capability_digest",
1169
+ (
1170
+ request.agent_instance_id,
1171
+ request.session_id,
1172
+ request.activation_id,
1173
+ token,
1174
+ expires_at,
1175
+ expires_iso,
1176
+ request.runtime_type,
1177
+ request.bundle_digest,
1178
+ request.capability_digest,
1179
+ ),
1180
+ )
1181
+ await connection.commit()
1182
+ except BaseException:
1183
+ await connection.rollback()
1184
+ raise
1185
+ return ActivationLease(
1186
+ agent_instance_id=request.agent_instance_id,
1187
+ activation_id=request.activation_id,
1188
+ fencing_token=token,
1189
+ lease_expires_at=expires_iso,
1190
+ bundle_digest=request.bundle_digest,
1191
+ runtime_type=request.runtime_type,
1192
+ capability_digest=request.capability_digest,
1193
+ )
1194
+
1195
+ async def renew_activation(
1196
+ self, activation_id: str, *, expected_fence: int, lease_ttl_seconds: float
1197
+ ) -> ActivationLease:
1198
+ async with self._write_lock:
1199
+ connection = await self._begin()
1200
+ try:
1201
+ row = await self._fetchone(
1202
+ connection,
1203
+ "SELECT * FROM kernel_activations WHERE activation_id=?",
1204
+ (activation_id,),
1205
+ )
1206
+ if row is None:
1207
+ raise InvalidCommandError(f"unknown activation_id {activation_id!r}")
1208
+ if (
1209
+ row["released"]
1210
+ or row["lease_expires_at"] <= time.time()
1211
+ or row["fencing_token"] != int(expected_fence)
1212
+ ):
1213
+ raise StaleFenceError(
1214
+ f"cannot renew activation {activation_id!r} at fence {expected_fence}"
1215
+ )
1216
+ expires_at = time.time() + lease_ttl_seconds
1217
+ expires_iso = datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat()
1218
+ await connection.execute(
1219
+ "UPDATE kernel_activations SET lease_expires_at=?, lease_expires_at_iso=?"
1220
+ " WHERE activation_id=?",
1221
+ (expires_at, expires_iso, activation_id),
1222
+ )
1223
+ await connection.commit()
1224
+ except BaseException:
1225
+ await connection.rollback()
1226
+ raise
1227
+ return ActivationLease(
1228
+ agent_instance_id=row["agent_instance_id"],
1229
+ activation_id=activation_id,
1230
+ fencing_token=row["fencing_token"],
1231
+ lease_expires_at=expires_iso,
1232
+ bundle_digest=row["bundle_digest"],
1233
+ runtime_type=row["runtime_type"],
1234
+ capability_digest=row["capability_digest"],
1235
+ )
1236
+
1237
+ async def release_activation(self, activation_id: str, *, expected_fence: int) -> None:
1238
+ async with self._write_lock:
1239
+ connection = await self._begin()
1240
+ try:
1241
+ row = await self._fetchone(
1242
+ connection,
1243
+ "SELECT * FROM kernel_activations WHERE activation_id=?",
1244
+ (activation_id,),
1245
+ )
1246
+ if row is None:
1247
+ raise InvalidCommandError(f"unknown activation_id {activation_id!r}")
1248
+ if row["released"] or row["fencing_token"] != int(expected_fence):
1249
+ raise StaleFenceError(
1250
+ f"cannot release activation {activation_id!r} at fence {expected_fence}"
1251
+ )
1252
+ await connection.execute(
1253
+ "UPDATE kernel_activations SET released=1, lease_expires_at=?"
1254
+ " WHERE activation_id=?",
1255
+ (time.time(), activation_id),
1256
+ )
1257
+ await connection.commit()
1258
+ except BaseException:
1259
+ await connection.rollback()
1260
+ raise
1261
+
1262
+ # ------------------------------------------------------------------ events
1263
+
1264
+ async def append_event(
1265
+ self,
1266
+ envelope: SessionEventEnvelope,
1267
+ *,
1268
+ expected_fence: int,
1269
+ agent_instance_id: str | None = None,
1270
+ ) -> SessionEventEnvelope:
1271
+ activation = await self._resolve_activation(envelope.session_id, agent_instance_id)
1272
+ await self._check_fence(
1273
+ await self._connect(),
1274
+ activation["agent_instance_id"],
1275
+ envelope.session_id,
1276
+ expected_fence,
1277
+ )
1278
+ return await self._events.append(
1279
+ envelope,
1280
+ guard=ActivationWriteGuard(
1281
+ activation_id=activation["activation_id"],
1282
+ fencing_token=int(expected_fence),
1283
+ ),
1284
+ )
1285
+
1286
+ async def _resolve_activation(
1287
+ self, session_id: str, agent_instance_id: str | None
1288
+ ) -> dict[str, Any]:
1289
+ connection = await self._connect()
1290
+ if agent_instance_id is not None:
1291
+ row = self._activation_row(await self._fetchone(
1292
+ connection,
1293
+ "SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
1294
+ (agent_instance_id, session_id),
1295
+ ))
1296
+ if row is None:
1297
+ raise StaleFenceError(
1298
+ "no active activation lease",
1299
+ details={"agent_instance_id": agent_instance_id, "session_id": session_id},
1300
+ )
1301
+ return row
1302
+ cursor = await connection.execute(
1303
+ "SELECT * FROM kernel_activations WHERE session_id=? AND released=0",
1304
+ (session_id,),
1305
+ )
1306
+ rows = [self._activation_row(row) for row in await cursor.fetchall()]
1307
+ await cursor.close()
1308
+ rows = [row for row in rows if row is not None]
1309
+ if len(rows) != 1:
1310
+ raise StaleFenceError(
1311
+ "cannot resolve a single activation lease for session",
1312
+ details={"session_id": session_id, "matches": len(rows)},
1313
+ )
1314
+ return rows[0]
1315
+
1316
+ # -------------------------------------------------------------------- runs
1317
+
1318
+ async def load_run(self, run_id: str) -> RunRecord | None:
1319
+ row = await self._fetchone(
1320
+ await self._connect(), "SELECT * FROM kernel_runs WHERE run_id=?", (run_id,)
1321
+ )
1322
+ if row is None:
1323
+ return None
1324
+ return RunRecord(
1325
+ run_id=row["run_id"],
1326
+ agent_instance_id=row["agent_instance_id"],
1327
+ session_id=row["session_id"],
1328
+ state=row["state"],
1329
+ activation_fence=row["activation_fence"],
1330
+ created_at=row["created_at"],
1331
+ updated_at=row["updated_at"],
1332
+ metadata=json.loads(row["metadata_json"]),
1333
+ )
1334
+
1335
+ async def save_run_transition(
1336
+ self, run: RunRecord, *, expected_fence: int
1337
+ ) -> RunRecord:
1338
+ async with self._write_lock:
1339
+ connection = await self._begin()
1340
+ try:
1341
+ activation = await self._check_fence(
1342
+ connection, run.agent_instance_id, run.session_id, expected_fence
1343
+ )
1344
+ existing = await self.load_run(run.run_id)
1345
+ assert_run_transition(existing.state if existing else None, run.state)
1346
+ if is_active_run(run.state):
1347
+ cursor = await connection.execute(
1348
+ "SELECT run_id FROM kernel_runs WHERE session_id=? AND run_id != ?"
1349
+ " AND state IN ('running','paused','waiting')",
1350
+ (run.session_id, run.run_id),
1351
+ )
1352
+ clash = await cursor.fetchone()
1353
+ await cursor.close()
1354
+ if clash is not None:
1355
+ raise InvalidCommandError(
1356
+ "session already has an active run",
1357
+ details={
1358
+ "session_id": run.session_id,
1359
+ "active_run_id": clash["run_id"],
1360
+ },
1361
+ )
1362
+ timestamp = now_iso()
1363
+ stored = run.model_copy(
1364
+ update={
1365
+ "activation_fence": int(expected_fence),
1366
+ "created_at": existing.created_at if existing else timestamp,
1367
+ "updated_at": timestamp,
1368
+ }
1369
+ )
1370
+ await connection.execute(
1371
+ "INSERT INTO kernel_runs (run_id, agent_instance_id, session_id, state,"
1372
+ " activation_fence, created_at, updated_at, metadata_json)"
1373
+ " VALUES (?,?,?,?,?,?,?,?)"
1374
+ " ON CONFLICT(run_id) DO UPDATE SET state=excluded.state,"
1375
+ " activation_fence=excluded.activation_fence,"
1376
+ " updated_at=excluded.updated_at,"
1377
+ " metadata_json=excluded.metadata_json",
1378
+ (
1379
+ stored.run_id,
1380
+ stored.agent_instance_id,
1381
+ stored.session_id,
1382
+ stored.state.value,
1383
+ stored.activation_fence,
1384
+ stored.created_at,
1385
+ stored.updated_at,
1386
+ json.dumps(stored.metadata, ensure_ascii=False),
1387
+ ),
1388
+ )
1389
+ await connection.commit()
1390
+ except BaseException:
1391
+ await connection.rollback()
1392
+ raise
1393
+ await self._emit_activation(
1394
+ control_event(
1395
+ session_id=run.session_id,
1396
+ event_type="control.run_transition",
1397
+ payload={
1398
+ "run_id": run.run_id,
1399
+ "state": run.state.value,
1400
+ "fencing_token": int(expected_fence),
1401
+ },
1402
+ run_id=run.run_id,
1403
+ ),
1404
+ activation,
1405
+ expected_fence,
1406
+ )
1407
+ return stored
1408
+
1409
+
1410
+ __all__ = ["SQLiteAgentKernelStore"]