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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (494) hide show
  1. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
  2. agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
  3. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +2 -0
  5. ksadk/a2a/_space_client_events.py +381 -0
  6. ksadk/a2a/bootstrap.py +0 -2
  7. ksadk/a2a/card.py +6 -111
  8. ksadk/a2a/control_plane.py +239 -1
  9. ksadk/a2a/event_adapter.py +156 -64
  10. ksadk/a2a/executor.py +195 -172
  11. ksadk/a2a/routes.py +12 -5
  12. ksadk/a2a/server.py +1 -8
  13. ksadk/a2a/service_env.py +33 -0
  14. ksadk/a2a/space_client.py +43 -349
  15. ksadk/a2a_card.py +101 -0
  16. ksadk/a2ui/core.py +222 -53
  17. ksadk/agui/_agent_helpers.py +243 -0
  18. ksadk/agui/a2ui_projection.py +12 -5
  19. ksadk/agui/agent.py +474 -275
  20. ksadk/agui/config.py +11 -5
  21. ksadk/agui/routes.py +7 -15
  22. ksadk/api/client.py +530 -40
  23. ksadk/api/environment.py +27 -0
  24. ksadk/builders/code_builder.py +352 -53
  25. ksadk/builders/container_builder.py +71 -44
  26. ksadk/builders/framework_requirements.py +2 -1
  27. ksadk/builders/ks3_uploader.py +2 -2
  28. ksadk/builders/managed_runtime_builder.py +60 -28
  29. ksadk/cli/__init__.py +25 -0
  30. ksadk/cli/cmd_a2a.py +18 -31
  31. ksadk/cli/cmd_create.py +13 -26
  32. ksadk/cli/cmd_dashboard.py +15 -1
  33. ksadk/cli/cmd_deploy.py +28 -3
  34. ksadk/cli/cmd_eval.py +443 -0
  35. ksadk/cli/cmd_evalset.py +284 -0
  36. ksadk/cli/cmd_files.py +5 -1
  37. ksadk/cli/cmd_hermes.py +137 -163
  38. ksadk/cli/cmd_invoke.py +28 -38
  39. ksadk/cli/cmd_launch.py +12 -0
  40. ksadk/cli/cmd_managed_runtime.py +95 -0
  41. ksadk/cli/cmd_observe.py +85 -0
  42. ksadk/cli/cmd_openclaw.py +85 -479
  43. ksadk/cli/cmd_replay.py +28 -4
  44. ksadk/cli/cmd_run.py +29 -67
  45. ksadk/cli/cmd_studio.py +201 -0
  46. ksadk/cli/cmd_web.py +15 -6
  47. ksadk/cli/dry_run.py +2 -1
  48. ksadk/cli/env_options.py +83 -0
  49. ksadk/cli/global_options.py +3 -1
  50. ksadk/cli/hermes_env.py +151 -0
  51. ksadk/cli/invoke_payload.py +49 -0
  52. ksadk/cli/network_options.py +7 -5
  53. ksadk/cli/openclaw_env.py +458 -0
  54. ksadk/cli/runtime_bootstrap.py +51 -0
  55. ksadk/cli/ui.py +6 -0
  56. ksadk/cli/workflow_common.py +5 -5
  57. ksadk/codex/__init__.py +3 -3
  58. ksadk/codex/client.py +833 -49
  59. ksadk/codex/phase.py +1 -1
  60. ksadk/codex/runtime.py +720 -127
  61. ksadk/common/kop_client.py +232 -0
  62. ksadk/configs/__init__.py +1 -3
  63. ksadk/configs/env_registry.py +244 -45
  64. ksadk/configs/env_registry_pcm.py +181 -0
  65. ksadk/configs/env_var_spec.py +18 -0
  66. ksadk/configs/global_config.py +6 -1
  67. ksadk/configs/settings.py +5 -108
  68. ksadk/context_engine/__init__.py +98 -0
  69. ksadk/context_engine/assembler.py +176 -0
  70. ksadk/context_engine/baseline.py +419 -0
  71. ksadk/context_engine/cache_observability.py +228 -0
  72. ksadk/context_engine/capabilities.py +443 -0
  73. ksadk/context_engine/contributors.py +344 -0
  74. ksadk/context_engine/hosted_pipeline.py +417 -0
  75. ksadk/context_engine/models.py +117 -0
  76. ksadk/context_engine/planner.py +698 -0
  77. ksadk/context_engine/policies.py +319 -0
  78. ksadk/context_engine/projection.py +31 -0
  79. ksadk/context_engine/shadow_plan.py +266 -0
  80. ksadk/context_engine/tokenizer.py +164 -0
  81. ksadk/conversations/__init__.py +85 -94
  82. ksadk/conversations/context.py +126 -6
  83. ksadk/conversations/message_projection.py +292 -28
  84. ksadk/conversations/model_context.py +49 -3
  85. ksadk/conversations/runtime_compaction.py +207 -5
  86. ksadk/conversations/runtime_input.py +193 -3
  87. ksadk/conversations/runtime_invocation.py +81 -3
  88. ksadk/conversations/runtime_observability.py +157 -4
  89. ksadk/conversations/runtime_payloads.py +42 -0
  90. ksadk/conversations/runtime_preparation.py +280 -2
  91. ksadk/conversations/runtime_resume.py +71 -32
  92. ksadk/conversations/runtime_stream_events.py +94 -9
  93. ksadk/conversations/runtime_streaming.py +175 -22
  94. ksadk/conversations/semantic_summary.py +311 -1
  95. ksadk/conversations/session_lock.py +51 -0
  96. ksadk/deployment/env_forward.py +72 -0
  97. ksadk/deployment/managed_runtime.py +38 -0
  98. ksadk/deployment/manager.py +24 -8
  99. ksadk/deployment/providers/serverless.py +228 -92
  100. ksadk/evaluation/__init__.py +116 -0
  101. ksadk/evaluation/a2a_adapter.py +452 -0
  102. ksadk/evaluation/adapters.py +67 -0
  103. ksadk/evaluation/agent_eval_client.py +395 -0
  104. ksadk/evaluation/cloud_binding.py +80 -0
  105. ksadk/evaluation/cloud_converter.py +194 -0
  106. ksadk/evaluation/cloud_service.py +199 -0
  107. ksadk/evaluation/contracts.py +478 -0
  108. ksadk/evaluation/evalset.py +351 -0
  109. ksadk/evaluation/evaluators.py +627 -0
  110. ksadk/evaluation/evidence.py +258 -0
  111. ksadk/evaluation/executor.py +143 -0
  112. ksadk/evaluation/local_adapter.py +550 -0
  113. ksadk/evaluation/service_env.py +27 -0
  114. ksadk/evaluation/storage.py +68 -0
  115. ksadk/evaluation/studio_build_adapter.py +253 -0
  116. ksadk/evaluation/target.py +82 -0
  117. ksadk/events/__init__.py +13 -13
  118. ksadk/events/_v1_compat/__init__.py +1 -0
  119. ksadk/events/_v1_compat/models.py +299 -0
  120. ksadk/events/_v1_compat/parser.py +247 -0
  121. ksadk/events/_v1_compat/projection.py +753 -0
  122. ksadk/events/adapters/__init__.py +34 -0
  123. ksadk/events/adapters/_a2a_snapshot.py +622 -0
  124. ksadk/events/adapters/_a2a_support.py +275 -0
  125. ksadk/events/adapters/_codex_interactions.py +375 -0
  126. ksadk/events/adapters/_codex_items.py +801 -0
  127. ksadk/events/adapters/_codex_validators.py +311 -0
  128. ksadk/events/adapters/_langgraph_support.py +785 -0
  129. ksadk/events/adapters/a2a.py +841 -0
  130. ksadk/events/adapters/adk.py +503 -0
  131. ksadk/events/adapters/codex.py +717 -0
  132. ksadk/events/adapters/langgraph.py +745 -0
  133. ksadk/events/canonical.py +440 -0
  134. ksadk/events/canonical_replay.py +497 -0
  135. ksadk/events/canonical_store.py +468 -0
  136. ksadk/events/cold_recovery.py +257 -0
  137. ksadk/events/content.py +89 -0
  138. ksadk/events/identity.py +86 -0
  139. ksadk/events/pipeline.py +455 -0
  140. ksadk/events/projections.py +66 -0
  141. ksadk/events/reducer.py +661 -0
  142. ksadk/events/replay.py +3 -36
  143. ksadk/events/runtime_event.py +20 -256
  144. ksadk/events/session_event.py +324 -0
  145. ksadk/events/store.py +3 -316
  146. ksadk/events/v1_compat.py +41 -0
  147. ksadk/harness/__init__.py +3 -2
  148. ksadk/harness/app.py +114 -123
  149. ksadk/harness/config.py +1 -1
  150. ksadk/harness/runtime.py +467 -0
  151. ksadk/hermes_terminal.py +5 -0
  152. ksadk/identity/resolver.py +4 -3
  153. ksadk/interaction/__init__.py +28 -0
  154. ksadk/interaction/contracts.py +136 -0
  155. ksadk/interaction/ledger.py +189 -0
  156. ksadk/interaction/provider.py +122 -0
  157. ksadk/interaction/providers/__init__.py +52 -0
  158. ksadk/interaction/providers/adk.py +40 -0
  159. ksadk/interaction/providers/codex.py +94 -0
  160. ksadk/interaction/providers/langgraph.py +58 -0
  161. ksadk/kernel/__init__.py +117 -0
  162. ksadk/kernel/authorization.py +258 -0
  163. ksadk/kernel/bootstrap.py +1074 -0
  164. ksadk/kernel/contract_fingerprints.py +61 -0
  165. ksadk/kernel/contracts.py +362 -0
  166. ksadk/kernel/control.py +219 -0
  167. ksadk/kernel/errors.py +79 -0
  168. ksadk/kernel/ingress.py +1102 -0
  169. ksadk/kernel/mapping.py +111 -0
  170. ksadk/kernel/memory_store.py +1133 -0
  171. ksadk/kernel/postgres_store.py +1756 -0
  172. ksadk/kernel/recovery.py +452 -0
  173. ksadk/kernel/runtime_identity.py +103 -0
  174. ksadk/kernel/sql/001_agent_kernel.sql +120 -0
  175. ksadk/kernel/sqlite_store.py +1410 -0
  176. ksadk/kernel/state.py +145 -0
  177. ksadk/kernel/store.py +232 -0
  178. ksadk/kernel/worker.py +953 -0
  179. ksadk/knowledge_base/client.py +22 -4
  180. ksadk/knowledge_base/service.py +32 -5
  181. ksadk/managed_a2a_card.py +160 -0
  182. ksadk/managed_runtime.py +16 -9
  183. ksadk/memory/__init__.py +61 -0
  184. ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
  185. ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
  186. ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
  187. ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
  188. ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
  189. ksadk/memory/adk/long_term_memory.py +3 -2
  190. ksadk/memory/coordinator.py +461 -0
  191. ksadk/memory/events.py +202 -0
  192. ksadk/memory/extraction.py +228 -0
  193. ksadk/memory/ltm_backend_factory.py +16 -3
  194. ksadk/memory/models.py +316 -0
  195. ksadk/memory/policy.py +215 -0
  196. ksadk/memory/provider.py +47 -0
  197. ksadk/memory/provider_adapter.py +159 -0
  198. ksadk/memory/provider_resolver.py +71 -0
  199. ksadk/memory/providers/__init__.py +5 -0
  200. ksadk/memory/providers/local_sqlite.py +490 -0
  201. ksadk/memory/resolved_policy.py +145 -0
  202. ksadk/memory/service.py +198 -8
  203. ksadk/memory/tool.py +4 -0
  204. ksadk/model_proxy/bootstrap.py +16 -9
  205. ksadk/model_proxy/config.py +22 -1
  206. ksadk/model_proxy/detect.py +202 -9
  207. ksadk/model_proxy/namespace.py +67 -15
  208. ksadk/model_proxy/server.py +168 -6
  209. ksadk/model_proxy/transform.py +105 -3
  210. ksadk/observability/__init__.py +25 -0
  211. ksadk/observability/session_log.py +420 -0
  212. ksadk/observability/trajectory.py +105 -0
  213. ksadk/prompts/__init__.py +71 -0
  214. ksadk/prompts/compiler.py +215 -0
  215. ksadk/prompts/models.py +82 -0
  216. ksadk/prompts/projection.py +89 -0
  217. ksadk/prompts/resolved.py +137 -0
  218. ksadk/prompts/sources.py +233 -0
  219. ksadk/runners/_langgraph_runner_streams.py +822 -0
  220. ksadk/runners/adk_runner.py +14 -31
  221. ksadk/runners/base_runner.py +44 -3
  222. ksadk/runners/factory.py +3 -3
  223. ksadk/runners/langgraph_runner.py +252 -433
  224. ksadk/runners/utils/__init__.py +0 -10
  225. ksadk/runners/utils/loader.py +16 -0
  226. ksadk/runtime/__init__.py +67 -41
  227. ksadk/runtime/_runner_adapter/__init__.py +1 -0
  228. ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
  229. ksadk/runtime/adapter.py +199 -16
  230. ksadk/runtime/conversation_execution.py +687 -0
  231. ksadk/runtime/executor.py +392 -0
  232. ksadk/runtime/factory.py +342 -0
  233. ksadk/runtime/framework_adapters.py +6 -12
  234. ksadk/runtime/hosted_finalizer.py +268 -0
  235. ksadk/runtime/launch.py +61 -0
  236. ksadk/runtime/preprocessing.py +49 -14
  237. ksadk/runtime/runner_adapter.py +330 -391
  238. ksadk/runtime/runner_loading.py +36 -0
  239. ksadk/runtime/usage.py +33 -0
  240. ksadk/sandbox/backends/e2b.py +36 -3
  241. ksadk/server/__init__.py +39 -4
  242. ksadk/server/app.py +18 -492
  243. ksadk/server/composition.py +36 -2
  244. ksadk/server/factory.py +210 -149
  245. ksadk/server/routes/common.py +15 -46
  246. ksadk/server/routes/control.py +13 -9
  247. ksadk/server/routes/kernel_ingress.py +258 -0
  248. ksadk/server/routes/misc.py +10 -33
  249. ksadk/server/routes/models.py +14 -6
  250. ksadk/server/routes/openai_compat.py +129 -27
  251. ksadk/server/routes/projection.py +84 -16
  252. ksadk/server/routes/run.py +347 -460
  253. ksadk/server/routes/sessions.py +37 -40
  254. ksadk/server/routes/streaming.py +52 -7
  255. ksadk/server/routes/workspace.py +54 -21
  256. ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
  257. ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
  258. ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
  259. ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
  260. ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
  261. ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
  262. ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
  263. ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
  264. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
  265. ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
  266. ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
  267. ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
  268. ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
  269. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
  270. ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
  271. ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
  272. ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
  273. ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
  274. ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
  275. ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
  276. ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
  277. ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
  278. ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
  279. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
  280. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
  281. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
  282. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
  283. ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
  284. ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
  285. ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
  286. ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
  287. ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
  288. ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
  289. ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
  290. ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
  291. ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
  292. ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
  293. ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
  294. ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
  295. ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
  296. ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
  297. ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
  298. ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
  299. ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
  300. ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
  301. ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
  302. ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
  303. ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
  304. ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
  305. ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
  306. ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
  307. ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
  308. ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
  309. ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
  310. ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
  311. ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
  312. ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
  313. ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
  314. ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
  315. ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
  316. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
  317. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
  318. ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
  319. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
  320. ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
  321. ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
  322. ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
  323. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
  324. ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
  325. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
  326. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
  327. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
  328. ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
  329. ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
  330. ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
  331. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
  332. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
  333. ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
  334. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
  335. ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
  336. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
  337. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
  338. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
  339. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
  340. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
  341. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
  342. ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
  343. ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
  344. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
  345. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
  346. ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
  347. ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
  348. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
  349. ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
  350. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
  351. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
  352. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
  353. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
  354. ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
  355. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
  356. ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
  357. ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
  358. ksadk/server/static/index.html +2 -2
  359. ksadk/sessions/__init__.py +26 -9
  360. ksadk/sessions/_local_service_sync.py +688 -0
  361. ksadk/sessions/_local_tables.py +21 -0
  362. ksadk/sessions/_postgres_schema.py +305 -0
  363. ksadk/sessions/_postgres_tables.py +15 -0
  364. ksadk/sessions/base.py +80 -1
  365. ksadk/sessions/in_memory.py +53 -2
  366. ksadk/sessions/local_service.py +63 -604
  367. ksadk/sessions/postgres_service.py +58 -148
  368. ksadk/sessions/resilient.py +35 -1
  369. ksadk/skills/runtime/backends/e2b.py +1 -1
  370. ksadk/skills/service_client.py +13 -3
  371. ksadk/studio/__init__.py +5 -0
  372. ksadk/studio/agent_avatar_assets.py +120 -0
  373. ksadk/studio/agent_lifecycle.py +51 -0
  374. ksadk/studio/api.py +1598 -0
  375. ksadk/studio/api_catalog_routes.py +324 -0
  376. ksadk/studio/api_contracts.py +286 -0
  377. ksadk/studio/api_helpers.py +132 -0
  378. ksadk/studio/api_memory_routes.py +82 -0
  379. ksadk/studio/authoring.py +749 -0
  380. ksadk/studio/authoring_coordinator.py +1017 -0
  381. ksadk/studio/builder.py +397 -0
  382. ksadk/studio/capabilities.py +368 -0
  383. ksadk/studio/cloud.py +2099 -0
  384. ksadk/studio/codex_agent_service.py +694 -0
  385. ksadk/studio/codex_authoring.py +469 -0
  386. ksadk/studio/codex_builder.py +437 -0
  387. ksadk/studio/codex_manifest.py +307 -0
  388. ksadk/studio/codex_run.py +395 -0
  389. ksadk/studio/compiler.py +190 -0
  390. ksadk/studio/contracts.py +755 -0
  391. ksadk/studio/errors.py +47 -0
  392. ksadk/studio/event_store.py +214 -0
  393. ksadk/studio/framework_run.py +342 -0
  394. ksadk/studio/hosted_kernel.py +317 -0
  395. ksadk/studio/identifiers.py +33 -0
  396. ksadk/studio/manifest_resolver.py +131 -0
  397. ksadk/studio/mcp_runtime.py +229 -0
  398. ksadk/studio/model_client.py +629 -0
  399. ksadk/studio/model_profile_service.py +214 -0
  400. ksadk/studio/operations.py +275 -0
  401. ksadk/studio/otel_trace.py +1060 -0
  402. ksadk/studio/pagination.py +182 -0
  403. ksadk/studio/pcm_memory.py +85 -0
  404. ksadk/studio/python_tool_inspection.py +183 -0
  405. ksadk/studio/repository.py +327 -0
  406. ksadk/studio/resource_catalog.py +1260 -0
  407. ksadk/studio/run_service.py +1133 -0
  408. ksadk/studio/runtime_catalog.py +74 -0
  409. ksadk/studio/runtime_source.py +365 -0
  410. ksadk/studio/service.py +2249 -0
  411. ksadk/studio/shared_web.py +1004 -0
  412. ksadk/studio/skill_discovery.py +628 -0
  413. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  414. ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
  415. ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
  416. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  417. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  418. ksadk/studio/static/index.html +15 -0
  419. ksadk/studio/templates.py +722 -0
  420. ksadk/studio/validator.py +265 -0
  421. ksadk/studio/workspace.py +135 -0
  422. ksadk/tools/gateway.py +9 -0
  423. ksadk/toolsets/__init__.py +27 -1
  424. ksadk/toolsets/a2a.py +426 -0
  425. ksadk/tracing/__init__.py +19 -21
  426. ksadk/tracing/setup.py +317 -403
  427. ksadk/tracing/span_utils.py +0 -44
  428. ksadk/version.py +1 -1
  429. ksadk_runtime_common/memory_backend/registry.py +56 -56
  430. ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
  431. ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
  432. ksadk_runtime_common/workspace_files/router.py +8 -2
  433. agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
  434. ksadk/a2a/card_builder.py +0 -64
  435. ksadk/a2a/client.py +0 -309
  436. ksadk/events/parser.py +0 -191
  437. ksadk/harness/runner.py +0 -211
  438. ksadk/runners/codex_runner.py +0 -132
  439. ksadk/runners/utils/langfuse.py +0 -230
  440. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
  441. ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
  442. ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
  443. ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
  444. ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
  445. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
  446. ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
  447. ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
  448. ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
  449. ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
  450. ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
  451. ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
  452. ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
  453. ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
  454. ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
  455. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
  456. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
  457. ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
  458. ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
  459. ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
  460. ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
  461. ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
  462. ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
  463. ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
  464. ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
  465. ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
  466. ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
  467. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
  468. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
  469. ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
  470. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
  471. ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
  472. ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
  473. ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
  474. ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
  475. ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
  476. ksadk/server/static/assets/katex-DATssjp1.js +0 -265
  477. ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
  478. ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
  479. ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
  480. ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
  481. ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
  482. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
  483. ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
  484. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
  485. ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
  486. ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
  487. ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
  488. ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
  489. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
  490. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
  491. ksadk/tracing/exporters/langfuse_exporter.py +0 -460
  492. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
  493. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
  494. {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1133 @@
1
+ # -*- coding: utf-8 -*-
2
+ """InMemory ``AgentKernelStore``(Phase 1 Task 3 Step 4)。
3
+
4
+ 只用于单进程开发与 conformance 测试,不宣称跨进程 durable。
5
+ 以 per-(agent, session) asyncio lock 保证 accept/claim/transition 的原子语义;
6
+ 所有 mutation 与对应 ``ControlEvent/v1`` 的追加在同一个临界区内完成。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import time
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+ from uuid import uuid4
16
+
17
+ from ksadk.events.session_event import SessionEventStore
18
+ from ksadk.interaction.contracts import (
19
+ InteractionRecord,
20
+ InteractionReceipt,
21
+ InteractionSubmission,
22
+ is_terminal,
23
+ )
24
+ from ksadk.interaction.ledger import (
25
+ ALREADY_RESOLVED,
26
+ REVISION_MISMATCH,
27
+ REQUEST_CONFLICT,
28
+ interaction_event,
29
+ request_digest,
30
+ requested_event_payload,
31
+ resolve_outcome,
32
+ submission_digest,
33
+ )
34
+ from ksadk.kernel.contracts import (
35
+ ActivationLease,
36
+ ActivationWriteGuard,
37
+ AdmissionWriteGuard,
38
+ AgentControlCommand,
39
+ AgentControlReceipt,
40
+ ControlError,
41
+ SessionEventEnvelope,
42
+ )
43
+ from ksadk.kernel.errors import InvalidCommandError, StaleFenceError
44
+ from ksadk.kernel.state import (
45
+ InboxState,
46
+ assert_inbox_transition,
47
+ assert_run_transition,
48
+ is_active_run,
49
+ )
50
+ from ksadk.kernel.store import (
51
+ ActivationLeaseRequest,
52
+ InboxMessage,
53
+ RunRecord,
54
+ command_digest,
55
+ control_event,
56
+ new_message_id,
57
+ now_iso,
58
+ )
59
+
60
+
61
+ class InMemoryAgentKernelStore:
62
+ def __init__(self, session_event_store: SessionEventStore) -> None:
63
+ self._events = session_event_store
64
+ self._locks: dict[tuple[str, str], asyncio.Lock] = {}
65
+ self._messages: dict[str, dict[str, Any]] = {}
66
+ self._idempotency: dict[tuple[str, str], str] = {} # (session, key) -> message_id
67
+ self._accepted_seq: dict[str, int] = {}
68
+ self._activations: dict[tuple[str, str], dict[str, Any]] = {}
69
+ self._runs: dict[str, RunRecord] = {}
70
+ # Interaction ledger(Task 5):(tenant_id, interaction_id) -> row。
71
+ self._interactions: dict[tuple[str, str], dict[str, Any]] = {}
72
+ self._interaction_submissions: dict[tuple[str, str, str], dict[str, Any]] = {}
73
+
74
+ # ------------------------------------------------------------------ locks
75
+
76
+ def _lock(self, agent_instance_id: str, session_id: str) -> asyncio.Lock:
77
+ key = (agent_instance_id, session_id)
78
+ return self._locks.setdefault(key, asyncio.Lock())
79
+
80
+ # ---------------------------------------------------------------- helpers
81
+
82
+ def _activation_row(
83
+ self, agent_instance_id: str, session_id: str
84
+ ) -> dict[str, Any] | None:
85
+ row = self._activations.get((agent_instance_id, session_id))
86
+ if row is None or row["released"]:
87
+ return None
88
+ return row
89
+
90
+ @staticmethod
91
+ def _lease_expired(row: dict[str, Any]) -> bool:
92
+ return row["lease_expires_at"] <= time.time()
93
+
94
+ def _check_fence(self, agent_instance_id: str, session_id: str, expected_fence: int) -> dict[str, Any]:
95
+ row = self._activation_row(agent_instance_id, session_id)
96
+ if (
97
+ row is None
98
+ or self._lease_expired(row)
99
+ or row["fencing_token"] != int(expected_fence)
100
+ ):
101
+ raise StaleFenceError(
102
+ "activation lease does not match expected fence",
103
+ details={
104
+ "agent_instance_id": agent_instance_id,
105
+ "session_id": session_id,
106
+ "expected_fence": int(expected_fence),
107
+ },
108
+ )
109
+ return row
110
+
111
+ async def _emit(
112
+ self,
113
+ envelope: SessionEventEnvelope,
114
+ *,
115
+ activation_row: dict[str, Any] | None,
116
+ admission_command: AgentControlCommand | None = None,
117
+ ) -> SessionEventEnvelope:
118
+ if activation_row is not None:
119
+ guard = ActivationWriteGuard(
120
+ activation_id=activation_row["activation_id"],
121
+ fencing_token=activation_row["fencing_token"],
122
+ )
123
+ elif admission_command is not None:
124
+ # admission 事实的 guard 绑定提交方 permit 引用与 command_id。
125
+ guard = AdmissionWriteGuard(
126
+ authorization_ref=admission_command.authorization_ref,
127
+ command_id=admission_command.command_id,
128
+ )
129
+ else:
130
+ guard = AdmissionWriteGuard(
131
+ authorization_ref="agent-kernel", command_id=uuid4()
132
+ )
133
+ return await self._events.append(envelope, guard=guard)
134
+
135
+ async def _emit_admission(
136
+ self, envelope: SessionEventEnvelope, command: AgentControlCommand
137
+ ) -> None:
138
+ # admission 事实的 guard 绑定提交方 permit 引用与 command_id。
139
+ await self._events.append(
140
+ envelope,
141
+ guard=AdmissionWriteGuard(
142
+ authorization_ref=command.authorization_ref,
143
+ command_id=command.command_id,
144
+ ),
145
+ )
146
+
147
+ @staticmethod
148
+ def _receipt(
149
+ command: AgentControlCommand,
150
+ status: str,
151
+ *,
152
+ message_id: str | None = None,
153
+ accepted_seq: int | None = None,
154
+ error: ControlError | None = None,
155
+ ) -> AgentControlReceipt:
156
+ return AgentControlReceipt(
157
+ command_id=command.command_id,
158
+ status=status, # type: ignore[arg-type]
159
+ message_id=message_id,
160
+ accepted_seq=accepted_seq,
161
+ error=error,
162
+ )
163
+
164
+ # --------------------------------------------------------------- commands
165
+
166
+ async def accept_command(
167
+ self, command: AgentControlCommand, *, queue_limit: int
168
+ ) -> AgentControlReceipt:
169
+ if queue_limit < 1:
170
+ raise InvalidCommandError("queue_limit must be positive")
171
+ async with self._lock(command.agent_instance_id, command.session_id):
172
+ existing_id = self._idempotency.get(
173
+ (command.session_id, command.idempotency_key)
174
+ )
175
+ if existing_id is not None:
176
+ existing = self._messages[existing_id]
177
+ if existing["request_digest"] != command_digest(command):
178
+ await self._emit_admission(
179
+ control_event(
180
+ session_id=command.session_id,
181
+ event_type="control.command_rejected",
182
+ payload={
183
+ "command_id": str(command.command_id),
184
+ "status": "rejected",
185
+ "reason": "idempotency_conflict",
186
+ },
187
+ causation_id=str(command.command_id),
188
+ ),
189
+ command,
190
+ )
191
+ return self._receipt(
192
+ command,
193
+ "rejected",
194
+ error=ControlError(
195
+ code="idempotency_conflict",
196
+ message="idempotency key reused with a different request digest",
197
+ retryable=False,
198
+ ),
199
+ )
200
+ return self._receipt(
201
+ command,
202
+ "duplicate",
203
+ message_id=existing["message_id"],
204
+ accepted_seq=existing["accepted_seq"],
205
+ )
206
+
207
+ depth = sum(
208
+ 1
209
+ for row in self._messages.values()
210
+ if row["agent_instance_id"] == command.agent_instance_id
211
+ and row["session_id"] == command.session_id
212
+ and row["status"] == InboxState.ACCEPTED
213
+ )
214
+ if depth >= queue_limit:
215
+ await self._emit_admission(
216
+ control_event(
217
+ session_id=command.session_id,
218
+ event_type="control.command_rejected",
219
+ payload={
220
+ "command_id": str(command.command_id),
221
+ "status": "queue_full",
222
+ "queue_limit": queue_limit,
223
+ },
224
+ causation_id=str(command.command_id),
225
+ ),
226
+ command,
227
+ )
228
+ return self._receipt(
229
+ command,
230
+ "queue_full",
231
+ error=ControlError(
232
+ code="queue_full",
233
+ message=f"inbox reached queue_limit={queue_limit}",
234
+ retryable=True,
235
+ ),
236
+ )
237
+
238
+ accepted_seq = self._accepted_seq.get(command.session_id, 0) + 1
239
+ self._accepted_seq[command.session_id] = accepted_seq
240
+ message_id = new_message_id()
241
+ self._messages[message_id] = {
242
+ "message_id": message_id,
243
+ "agent_instance_id": command.agent_instance_id,
244
+ "session_id": command.session_id,
245
+ "idempotency_key": command.idempotency_key,
246
+ "request_digest": command_digest(command),
247
+ "accepted_seq": accepted_seq,
248
+ "status": InboxState.ACCEPTED,
249
+ "claimed_fence": None,
250
+ "command": command,
251
+ }
252
+ self._idempotency[(command.session_id, command.idempotency_key)] = message_id
253
+ await self._emit_admission(
254
+ control_event(
255
+ session_id=command.session_id,
256
+ event_type="control.command_accepted",
257
+ payload={
258
+ "command_id": str(command.command_id),
259
+ "status": "accepted",
260
+ "message_id": message_id,
261
+ "accepted_seq": accepted_seq,
262
+ "command_type": command.command_type,
263
+ },
264
+ causation_id=str(command.command_id),
265
+ ),
266
+ command,
267
+ )
268
+ return self._receipt(
269
+ command,
270
+ "accepted",
271
+ message_id=message_id,
272
+ accepted_seq=accepted_seq,
273
+ )
274
+
275
+ async def load_message(self, message_id: str) -> InboxMessage | None:
276
+ row = self._messages.get(str(message_id))
277
+ return self._to_message(row) if row is not None else None
278
+
279
+ async def load_by_idempotency(
280
+ self, session_id: str, idempotency_key: str
281
+ ) -> InboxMessage | None:
282
+ message_id = self._idempotency.get((session_id, idempotency_key))
283
+ if message_id is None:
284
+ return None
285
+ return await self.load_message(message_id)
286
+
287
+ async def reject_command(
288
+ self,
289
+ command: AgentControlCommand,
290
+ *,
291
+ status: str,
292
+ code: str,
293
+ message: str,
294
+ retryable: bool = False,
295
+ ) -> AgentControlReceipt:
296
+ """admission 拒绝(invalid_permit / unsupported / ...)的脱敏审计 + receipt。
297
+
298
+ 只在 SessionEventStore 里追加 ``control.command_rejected`` 事实,
299
+ 不写 Inbox 行;payload 仅含 command_id/status/reason。
300
+ """
301
+ await self._emit_admission(
302
+ control_event(
303
+ session_id=command.session_id,
304
+ event_type="control.command_rejected",
305
+ payload={
306
+ "command_id": str(command.command_id),
307
+ "status": status,
308
+ "reason": code,
309
+ },
310
+ causation_id=str(command.command_id),
311
+ ),
312
+ command,
313
+ )
314
+ return self._receipt(
315
+ command,
316
+ status,
317
+ error=ControlError(code=code, message=message, retryable=retryable),
318
+ )
319
+
320
+ def _session_rows(
321
+ self, agent_instance_id: str, session_id: str | None
322
+ ) -> list[dict[str, Any]]:
323
+ return [
324
+ row
325
+ for row in self._messages.values()
326
+ if row["agent_instance_id"] == agent_instance_id
327
+ and (session_id is None or row["session_id"] == session_id)
328
+ ]
329
+
330
+ async def list_messages(
331
+ self, agent_instance_id: str, session_id: str | None = None
332
+ ) -> list[InboxMessage]:
333
+ """全部状态的 Inbox 行(审计/测试视角),按 accepted_seq 排序。"""
334
+ rows = sorted(
335
+ self._session_rows(agent_instance_id, session_id),
336
+ key=lambda row: row["accepted_seq"],
337
+ )
338
+ return [self._to_message(row) for row in rows]
339
+
340
+ async def list_pending(
341
+ self,
342
+ agent_instance_id: str,
343
+ session_id: str | None = None,
344
+ *,
345
+ fencing_token: int | None = None,
346
+ ) -> list[InboxMessage]:
347
+ """按 accepted_seq 排序的待处理消息。
348
+
349
+ ACCEPTED 总是 pending;CLAIMED 只在传入相同 fencing_token(本 owner
350
+ 自我重试视角)时可见,用于 retryable failure 后的恢复。
351
+ """
352
+ rows = []
353
+ for row in self._session_rows(agent_instance_id, session_id):
354
+ if row["status"] == InboxState.ACCEPTED:
355
+ rows.append(row)
356
+ elif (
357
+ fencing_token is not None
358
+ and row["status"] == InboxState.CLAIMED
359
+ and row["claimed_fence"] == int(fencing_token)
360
+ ):
361
+ rows.append(row)
362
+ rows.sort(key=lambda row: row["accepted_seq"])
363
+ return [self._to_message(row) for row in rows]
364
+
365
+ async def claim_message(
366
+ self, message_id: str, fencing_token: int
367
+ ) -> InboxMessage:
368
+ """按 message_id 认领(worker 选择性 FIFO 使用)。同 fence 重复认领幂等。"""
369
+ row = self._messages.get(str(message_id))
370
+ if row is None:
371
+ raise InvalidCommandError(f"unknown message_id {message_id!r}")
372
+ async with self._lock(row["agent_instance_id"], row["session_id"]):
373
+ fresh = self._messages[str(message_id)]
374
+ if (
375
+ fresh["status"] == InboxState.CLAIMED
376
+ and fresh["claimed_fence"] == int(fencing_token)
377
+ ):
378
+ return self._to_message(fresh)
379
+ activation = self._check_fence(
380
+ fresh["agent_instance_id"], fresh["session_id"], fencing_token
381
+ )
382
+ if fresh["status"] != InboxState.ACCEPTED:
383
+ raise InvalidCommandError(
384
+ f"message {message_id!r} is not claimable at status {fresh['status']}"
385
+ )
386
+ assert_inbox_transition(InboxState(fresh["status"]), InboxState.CLAIMED)
387
+ fresh["status"] = InboxState.CLAIMED
388
+ fresh["claimed_fence"] = int(fencing_token)
389
+ await self._emit(
390
+ control_event(
391
+ session_id=fresh["session_id"],
392
+ event_type="control.message_claimed",
393
+ payload={
394
+ "message_id": fresh["message_id"],
395
+ "fencing_token": int(fencing_token),
396
+ },
397
+ ),
398
+ activation_row=activation,
399
+ )
400
+ return self._to_message(fresh)
401
+
402
+ async def discard_claim(self, message_id: str, *, expected_fence: int) -> None:
403
+ """typed rejection 的确定性收口:CLAIMED -> DISCARDED。"""
404
+ message_id = str(message_id)
405
+ row = self._messages.get(message_id)
406
+ if row is None:
407
+ raise InvalidCommandError(f"unknown message_id {message_id!r}")
408
+ async with self._lock(row["agent_instance_id"], row["session_id"]):
409
+ fresh = self._messages[message_id]
410
+ activation = self._check_fence(
411
+ fresh["agent_instance_id"], fresh["session_id"], expected_fence
412
+ )
413
+ if (
414
+ fresh["status"] != InboxState.CLAIMED
415
+ or fresh["claimed_fence"] != int(expected_fence)
416
+ ):
417
+ raise StaleFenceError(
418
+ f"message {message_id!r} is not claimed at fence {expected_fence}"
419
+ )
420
+ assert_inbox_transition(InboxState(fresh["status"]), InboxState.DISCARDED)
421
+ fresh["status"] = InboxState.DISCARDED
422
+ await self._emit(
423
+ control_event(
424
+ session_id=fresh["session_id"],
425
+ event_type="control.message_discarded",
426
+ payload={
427
+ "message_id": message_id,
428
+ "fencing_token": int(expected_fence),
429
+ },
430
+ ),
431
+ activation_row=activation,
432
+ )
433
+
434
+ async def inbox_depth(
435
+ self, agent_instance_id: str, session_id: str | None = None
436
+ ) -> int:
437
+ return sum(
438
+ 1
439
+ for row in self._session_rows(agent_instance_id, session_id)
440
+ if row["status"] == InboxState.ACCEPTED
441
+ )
442
+
443
+ async def find_active_run(
444
+ self, agent_instance_id: str, session_id: str | None = None
445
+ ) -> RunRecord | None:
446
+ for run in self._runs.values():
447
+ if run.agent_instance_id != agent_instance_id:
448
+ continue
449
+ if session_id is not None and run.session_id != session_id:
450
+ continue
451
+ if is_active_run(run.state):
452
+ return run
453
+ return None
454
+
455
+ async def current_lease(
456
+ self, agent_instance_id: str, session_id: str | None = None
457
+ ) -> ActivationLease | None:
458
+ for (agent, session), row in self._activations.items():
459
+ if agent != agent_instance_id:
460
+ continue
461
+ if session_id is not None and session != session_id:
462
+ continue
463
+ if row.get("released") or self._lease_expired(row):
464
+ continue
465
+ request = ActivationLeaseRequest(
466
+ agent_instance_id=agent,
467
+ session_id=session,
468
+ activation_id=row["activation_id"],
469
+ runtime_type=row["runtime_type"],
470
+ bundle_digest=row["bundle_digest"],
471
+ capability_digest=row["capability_digest"],
472
+ )
473
+ return self._lease(request, row)
474
+ return None
475
+
476
+ @staticmethod
477
+ def _to_message(row: dict[str, Any]) -> InboxMessage:
478
+ return InboxMessage(
479
+ message_id=row["message_id"],
480
+ agent_instance_id=row["agent_instance_id"],
481
+ session_id=row["session_id"],
482
+ idempotency_key=row["idempotency_key"],
483
+ request_digest=row["request_digest"],
484
+ accepted_seq=row["accepted_seq"],
485
+ status=InboxState(row["status"]),
486
+ claimed_fence=row["claimed_fence"],
487
+ command=row.get("command"),
488
+ )
489
+
490
+ async def claim_next(
491
+ self, agent_instance_id: str, session_id: str, fencing_token: int
492
+ ) -> InboxMessage | None:
493
+ async with self._lock(agent_instance_id, session_id):
494
+ activation = self._check_fence(agent_instance_id, session_id, fencing_token)
495
+ def _claimable(row: dict[str, Any]) -> bool:
496
+ if row["status"] == InboxState.ACCEPTED:
497
+ return True
498
+ # 过期/被 takeover 的 claim 只能被更高 fence 的 owner reclaim。
499
+ return row["status"] == InboxState.CLAIMED and row[
500
+ "claimed_fence"
501
+ ] != int(fencing_token)
502
+
503
+ candidates = sorted(
504
+ (
505
+ row
506
+ for row in self._messages.values()
507
+ if row["agent_instance_id"] == agent_instance_id
508
+ and row["session_id"] == session_id
509
+ and _claimable(row)
510
+ ),
511
+ key=lambda row: row["accepted_seq"],
512
+ )
513
+ if not candidates:
514
+ return None
515
+ row = candidates[0]
516
+ if row["status"] == InboxState.ACCEPTED:
517
+ assert_inbox_transition(InboxState(row["status"]), InboxState.CLAIMED)
518
+ row["status"] = InboxState.CLAIMED
519
+ row["claimed_fence"] = int(fencing_token)
520
+ await self._emit(
521
+ control_event(
522
+ session_id=session_id,
523
+ event_type="control.message_claimed",
524
+ payload={
525
+ "message_id": row["message_id"],
526
+ "fencing_token": int(fencing_token),
527
+ },
528
+ ),
529
+ activation_row=activation,
530
+ )
531
+ return self._to_message(row)
532
+
533
+ async def complete_claim(self, message_id: str, *, expected_fence: int) -> None:
534
+ message_id = str(message_id)
535
+ row = self._messages.get(message_id)
536
+ if row is None:
537
+ raise InvalidCommandError(f"unknown message_id {message_id!r}")
538
+ async with self._lock(row["agent_instance_id"], row["session_id"]):
539
+ fresh = self._messages[message_id]
540
+ activation = self._check_fence(
541
+ fresh["agent_instance_id"], fresh["session_id"], expected_fence
542
+ )
543
+ if (
544
+ fresh["status"] != InboxState.CLAIMED
545
+ or fresh["claimed_fence"] != int(expected_fence)
546
+ ):
547
+ raise StaleFenceError(
548
+ f"message {message_id!r} is not claimed at fence {expected_fence}"
549
+ )
550
+ assert_inbox_transition(InboxState(fresh["status"]), InboxState.COMPLETED)
551
+ fresh["status"] = InboxState.COMPLETED
552
+ await self._emit(
553
+ control_event(
554
+ session_id=fresh["session_id"],
555
+ event_type="control.message_completed",
556
+ payload={"message_id": message_id, "fencing_token": int(expected_fence)},
557
+ ),
558
+ activation_row=activation,
559
+ )
560
+
561
+ # -------------------------------------------------------------- interactions
562
+
563
+ def _check_interaction_guard(
564
+ self, agent_instance_id: str, session_id: str, guard: ActivationWriteGuard
565
+ ) -> dict[str, Any]:
566
+ """Interaction 台账的 fence CAS:guard 必须命中当前未过期 lease。"""
567
+
568
+ row = next(
569
+ (
570
+ candidate
571
+ for candidate in self._activations.values()
572
+ if candidate["activation_id"] == guard.activation_id
573
+ and candidate["agent_instance_id"] == agent_instance_id
574
+ and candidate["session_id"] == session_id
575
+ ),
576
+ None,
577
+ )
578
+ if (
579
+ row is None
580
+ or row.get("released")
581
+ or self._lease_expired(row)
582
+ or row["fencing_token"] != int(guard.fencing_token)
583
+ or row["agent_instance_id"] != agent_instance_id
584
+ or row["session_id"] != session_id
585
+ ):
586
+ raise StaleFenceError(
587
+ "interaction write guard does not match the current lease",
588
+ details={
589
+ "activation_id": guard.activation_id,
590
+ "fencing_token": int(guard.fencing_token),
591
+ "session_id": session_id,
592
+ },
593
+ )
594
+ return row
595
+
596
+ def _find_interaction(
597
+ self,
598
+ interaction_id: str,
599
+ *,
600
+ agent_instance_id: str | None = None,
601
+ session_id: str | None = None,
602
+ ) -> dict[str, Any] | None:
603
+ """Resolve an opaque public id only inside an activation-owned scope.
604
+
605
+ ``kernel_interactions`` is tenant-keyed, while a public submission
606
+ intentionally does not carry a tenant. The trusted activation guard
607
+ is consequently the lookup boundary for mutations. An unscoped read
608
+ is permitted only when the id is globally unambiguous; it must never
609
+ return an arbitrary tenant's row.
610
+ """
611
+
612
+ matches = [
613
+ row
614
+ for (_, key), row in self._interactions.items()
615
+ if key == interaction_id
616
+ and (agent_instance_id is None or row["record"].agent_instance_id == agent_instance_id)
617
+ and (session_id is None or row["record"].session_id == session_id)
618
+ ]
619
+ if len(matches) > 1:
620
+ raise InvalidCommandError(
621
+ f"interaction_id {interaction_id!r} is ambiguous without trusted scope",
622
+ details={"reason": REQUEST_CONFLICT, "interaction_id": interaction_id},
623
+ )
624
+ return matches[0] if matches else None
625
+
626
+ def _interaction_scope_for_guard(
627
+ self, guard: ActivationWriteGuard
628
+ ) -> tuple[str, str]:
629
+ row = next(
630
+ (
631
+ candidate
632
+ for candidate in self._activations.values()
633
+ if candidate["activation_id"] == guard.activation_id
634
+ ),
635
+ None,
636
+ )
637
+ if (
638
+ row is None
639
+ or row.get("released")
640
+ or self._lease_expired(row)
641
+ or row["fencing_token"] != int(guard.fencing_token)
642
+ ):
643
+ raise StaleFenceError(
644
+ "interaction write guard does not match the current lease",
645
+ details={
646
+ "activation_id": guard.activation_id,
647
+ "fencing_token": int(guard.fencing_token),
648
+ },
649
+ )
650
+ return str(row["agent_instance_id"]), str(row["session_id"])
651
+
652
+ @staticmethod
653
+ def _terminal_conflict(interaction_id: str, status: str) -> InvalidCommandError:
654
+ return InvalidCommandError(
655
+ f"interaction {interaction_id!r} already reached terminal status {status!r}",
656
+ details={"reason": ALREADY_RESOLVED, "interaction_id": interaction_id},
657
+ )
658
+
659
+ async def request(
660
+ self, record: InteractionRecord, *, guard: ActivationWriteGuard
661
+ ) -> InteractionRecord:
662
+ async with self._lock(record.agent_instance_id, record.session_id):
663
+ self._check_interaction_guard(
664
+ record.agent_instance_id, record.session_id, guard
665
+ )
666
+ key = (record.tenant_id, record.interaction_id)
667
+ existing = self._interactions.get(key)
668
+ digest = request_digest(record)
669
+ if existing is not None:
670
+ if existing["request_digest"] != digest:
671
+ raise InvalidCommandError(
672
+ "interaction_id reused with a different request digest",
673
+ details={
674
+ "reason": REQUEST_CONFLICT,
675
+ "interaction_id": record.interaction_id,
676
+ },
677
+ )
678
+ return existing["record"]
679
+ # persist-before-ack:事件追加失败时不留下 pending 行。
680
+ await self._events.append(
681
+ requested_event_payload(record, now_iso()), guard=guard
682
+ )
683
+ self._interactions[key] = {"record": record, "request_digest": digest}
684
+ return record
685
+
686
+ async def resolve(
687
+ self, submission: InteractionSubmission, *, guard: ActivationWriteGuard
688
+ ) -> InteractionReceipt:
689
+ agent_instance_id, session_id = self._interaction_scope_for_guard(guard)
690
+ row = self._find_interaction(
691
+ submission.interaction_id,
692
+ agent_instance_id=agent_instance_id,
693
+ session_id=session_id,
694
+ )
695
+ if row is None:
696
+ raise InvalidCommandError(
697
+ f"unknown interaction_id {submission.interaction_id!r}"
698
+ )
699
+ record = row["record"]
700
+ async with self._lock(record.agent_instance_id, record.session_id):
701
+ self._check_interaction_guard(
702
+ record.agent_instance_id, record.session_id, guard
703
+ )
704
+ fresh = self._interactions[(record.tenant_id, record.interaction_id)]
705
+ current: InteractionRecord = fresh["record"]
706
+ if is_terminal(current.status):
707
+ sub_key = (
708
+ current.tenant_id,
709
+ current.interaction_id,
710
+ submission.idempotency_key,
711
+ )
712
+ existing_sub = self._interaction_submissions.get(sub_key)
713
+ if (
714
+ existing_sub is not None
715
+ and existing_sub["digest"] == submission_digest(submission)
716
+ ):
717
+ return existing_sub["receipt"]
718
+ raise self._terminal_conflict(
719
+ current.interaction_id, current.status
720
+ )
721
+ if current.revision != submission.expected_revision:
722
+ raise InvalidCommandError(
723
+ "interaction revision does not match expected_revision",
724
+ details={
725
+ "reason": REVISION_MISMATCH,
726
+ "interaction_id": current.interaction_id,
727
+ "expected_revision": submission.expected_revision,
728
+ "current_revision": current.revision,
729
+ },
730
+ )
731
+ outcome = resolve_outcome(submission.action)
732
+ updated = current.model_copy(
733
+ update={
734
+ "status": "resolved",
735
+ "revision": current.revision + 1,
736
+ }
737
+ )
738
+ stored = await self._events.append(
739
+ interaction_event(
740
+ updated,
741
+ event_type="interaction.resolved",
742
+ timestamp=now_iso(),
743
+ outcome=outcome,
744
+ response=submission.response,
745
+ actor_ref="user",
746
+ ),
747
+ guard=guard,
748
+ )
749
+ receipt = InteractionReceipt(
750
+ interaction_id=updated.interaction_id,
751
+ revision=updated.revision,
752
+ status="resolved",
753
+ outcome=outcome, # type: ignore[arg-type]
754
+ event_id=stored.event_id,
755
+ accepted_seq=stored.seq,
756
+ )
757
+ fresh["record"] = updated
758
+ self._interaction_submissions[
759
+ (updated.tenant_id, updated.interaction_id, submission.idempotency_key)
760
+ ] = {"digest": submission_digest(submission), "receipt": receipt}
761
+ return receipt
762
+
763
+ async def _terminal_command(
764
+ self,
765
+ interaction_id: str,
766
+ expected_revision: int,
767
+ *,
768
+ guard: ActivationWriteGuard,
769
+ status: str,
770
+ reason: str,
771
+ ) -> InteractionReceipt:
772
+ agent_instance_id, session_id = self._interaction_scope_for_guard(guard)
773
+ row = self._find_interaction(
774
+ interaction_id,
775
+ agent_instance_id=agent_instance_id,
776
+ session_id=session_id,
777
+ )
778
+ if row is None:
779
+ raise InvalidCommandError(f"unknown interaction_id {interaction_id!r}")
780
+ record = row["record"]
781
+ async with self._lock(record.agent_instance_id, record.session_id):
782
+ self._check_interaction_guard(
783
+ record.agent_instance_id, record.session_id, guard
784
+ )
785
+ fresh = self._interactions[(record.tenant_id, record.interaction_id)]
786
+ current: InteractionRecord = fresh["record"]
787
+ if is_terminal(current.status):
788
+ raise self._terminal_conflict(current.interaction_id, current.status)
789
+ if current.revision != expected_revision:
790
+ raise InvalidCommandError(
791
+ "interaction revision does not match expected_revision",
792
+ details={
793
+ "reason": REVISION_MISMATCH,
794
+ "interaction_id": current.interaction_id,
795
+ "expected_revision": expected_revision,
796
+ "current_revision": current.revision,
797
+ },
798
+ )
799
+ updated = current.model_copy(
800
+ update={"status": status, "revision": current.revision + 1}
801
+ )
802
+ event_type = (
803
+ "interaction.cancelled" if status == "cancelled" else "interaction.expired"
804
+ )
805
+ stored = await self._events.append(
806
+ interaction_event(
807
+ updated,
808
+ event_type=event_type,
809
+ timestamp=now_iso(),
810
+ reason=reason,
811
+ ),
812
+ guard=guard,
813
+ )
814
+ receipt = InteractionReceipt(
815
+ interaction_id=updated.interaction_id,
816
+ revision=updated.revision,
817
+ status=updated.status, # type: ignore[arg-type]
818
+ outcome=updated.status, # type: ignore[arg-type]
819
+ event_id=stored.event_id,
820
+ accepted_seq=stored.seq,
821
+ )
822
+ fresh["record"] = updated
823
+ return receipt
824
+
825
+ async def cancel(
826
+ self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
827
+ ) -> InteractionReceipt:
828
+ return await self._terminal_command(
829
+ interaction_id,
830
+ expected_revision,
831
+ guard=guard,
832
+ status="cancelled",
833
+ reason="cancelled by owner",
834
+ )
835
+
836
+ async def expire(
837
+ self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
838
+ ) -> InteractionReceipt:
839
+ return await self._terminal_command(
840
+ interaction_id,
841
+ expected_revision,
842
+ guard=guard,
843
+ status="expired",
844
+ reason="interaction expired",
845
+ )
846
+
847
+ async def get(
848
+ self,
849
+ interaction_id: str,
850
+ *,
851
+ tenant_id: str | None = None,
852
+ agent_instance_id: str | None = None,
853
+ session_id: str | None = None,
854
+ run_id: str | None = None,
855
+ ) -> InteractionRecord | None:
856
+ """Read an opaque id only when it is unique or fully trusted-scoped.
857
+
858
+ Public interaction ids are not tenant grants. The worker always has
859
+ the Server-admitted command scope and must pass all four dimensions;
860
+ legacy local callers may omit all dimensions only while the id is
861
+ globally unambiguous.
862
+ """
863
+
864
+ scope = (tenant_id, agent_instance_id, session_id, run_id)
865
+ if any(value is not None for value in scope):
866
+ if not all(value is not None for value in scope):
867
+ raise InvalidCommandError(
868
+ "interaction lookup requires a complete trusted scope",
869
+ details={"interaction_id": interaction_id},
870
+ )
871
+ matches = [
872
+ row
873
+ for (_, key), row in self._interactions.items()
874
+ if key == interaction_id
875
+ and row["record"].tenant_id == tenant_id
876
+ and row["record"].agent_instance_id == agent_instance_id
877
+ and row["record"].session_id == session_id
878
+ and row["record"].run_id == run_id
879
+ ]
880
+ if len(matches) > 1: # pragma: no cover - backend key prevents it
881
+ raise InvalidCommandError(
882
+ f"interaction_id {interaction_id!r} is ambiguous in trusted scope",
883
+ details={"reason": REQUEST_CONFLICT, "interaction_id": interaction_id},
884
+ )
885
+ return matches[0]["record"] if matches else None
886
+ row = self._find_interaction(interaction_id)
887
+ return row["record"] if row is not None else None
888
+
889
+ async def list_pending_interactions(
890
+ self, tenant_id: str, session_id: str
891
+ ) -> list[InteractionRecord]:
892
+ return [
893
+ row["record"]
894
+ for (tenant, _), row in sorted(self._interactions.items())
895
+ if tenant == tenant_id
896
+ and row["record"].session_id == session_id
897
+ and row["record"].status == "pending"
898
+ ]
899
+
900
+ # ------------------------------------------------------------- activations
901
+
902
+ async def acquire_activation(self, request: ActivationLeaseRequest) -> ActivationLease:
903
+ key = (request.agent_instance_id, request.session_id)
904
+ async with self._lock(*key):
905
+ row = self._activations.get(key)
906
+ expires_at = time.time() + request.lease_ttl_seconds
907
+ if row is None:
908
+ token = 1
909
+ elif row["released"] or self._lease_expired(row):
910
+ token = row["fencing_token"] + 1
911
+ elif row["activation_id"] == request.activation_id:
912
+ token = row["fencing_token"]
913
+ else:
914
+ raise InvalidCommandError(
915
+ "activation lease is still held by another owner",
916
+ details={
917
+ "holder": row["activation_id"],
918
+ "lease_expires_at": row["lease_expires_at_iso"],
919
+ },
920
+ )
921
+ new_row = {
922
+ "agent_instance_id": request.agent_instance_id,
923
+ "session_id": request.session_id,
924
+ "activation_id": request.activation_id,
925
+ "fencing_token": token,
926
+ "lease_expires_at": expires_at,
927
+ "lease_expires_at_iso": datetime.fromtimestamp(
928
+ expires_at, tz=timezone.utc
929
+ ).isoformat(),
930
+ "released": False,
931
+ "runtime_type": request.runtime_type,
932
+ "bundle_digest": request.bundle_digest,
933
+ "capability_digest": request.capability_digest,
934
+ }
935
+ self._activations[key] = new_row
936
+ return self._lease(request, new_row)
937
+
938
+ @staticmethod
939
+ def _lease(request: ActivationLeaseRequest, row: dict[str, Any]) -> ActivationLease:
940
+ return ActivationLease(
941
+ agent_instance_id=request.agent_instance_id,
942
+ activation_id=row["activation_id"],
943
+ fencing_token=row["fencing_token"],
944
+ lease_expires_at=row["lease_expires_at_iso"],
945
+ bundle_digest=row["bundle_digest"],
946
+ runtime_type=row["runtime_type"],
947
+ capability_digest=row["capability_digest"],
948
+ )
949
+
950
+ def _find_activation(self, activation_id: str) -> dict[str, Any]:
951
+ for row in self._activations.values():
952
+ if row["activation_id"] == activation_id:
953
+ return row
954
+ raise InvalidCommandError(f"unknown activation_id {activation_id!r}")
955
+
956
+ async def renew_activation(
957
+ self, activation_id: str, *, expected_fence: int, lease_ttl_seconds: float
958
+ ) -> ActivationLease:
959
+ row = self._find_activation(activation_id)
960
+ key = self._activation_key(row)
961
+ async with self._lock(*key):
962
+ fresh = self._find_activation(activation_id)
963
+ if (
964
+ fresh["released"]
965
+ or self._lease_expired(fresh)
966
+ or fresh["fencing_token"] != int(expected_fence)
967
+ ):
968
+ raise StaleFenceError(
969
+ f"cannot renew activation {activation_id!r} at fence {expected_fence}"
970
+ )
971
+ fresh["lease_expires_at"] = time.time() + lease_ttl_seconds
972
+ fresh["lease_expires_at_iso"] = now_iso()
973
+ request = ActivationLeaseRequest(
974
+ agent_instance_id=key[0],
975
+ session_id=key[1],
976
+ activation_id=fresh["activation_id"],
977
+ runtime_type=fresh["runtime_type"],
978
+ bundle_digest=fresh["bundle_digest"],
979
+ capability_digest=fresh["capability_digest"],
980
+ )
981
+ return self._lease(request, fresh)
982
+
983
+ async def release_activation(self, activation_id: str, *, expected_fence: int) -> None:
984
+ row = self._find_activation(activation_id)
985
+ key = self._activation_key(row)
986
+ async with self._lock(*key):
987
+ fresh = self._find_activation(activation_id)
988
+ if (
989
+ fresh["released"]
990
+ or fresh["fencing_token"] != int(expected_fence)
991
+ ):
992
+ raise StaleFenceError(
993
+ f"cannot release activation {activation_id!r} at fence {expected_fence}"
994
+ )
995
+ fresh["released"] = True
996
+ fresh["lease_expires_at"] = time.time()
997
+
998
+ @staticmethod
999
+ def _activation_key(row: dict[str, Any]) -> tuple[str, str]:
1000
+ return (row["agent_instance_id"], row["session_id"])
1001
+
1002
+ # ------------------------------------------------------------------ events
1003
+
1004
+ async def validate_write_fence(
1005
+ self,
1006
+ envelope: SessionEventEnvelope,
1007
+ guard: ActivationWriteGuard,
1008
+ ) -> None:
1009
+ """SessionEventStore fence seam:guard 必须是当前未过期 lease 的 owner。
1010
+
1011
+ 被 takeover(activation 行被替换/释放)或 token 滞后的旧 owner 得到
1012
+ :class:`StaleFenceError`;不做任何写入。
1013
+ """
1014
+
1015
+ row = next(
1016
+ (
1017
+ candidate
1018
+ for candidate in self._activations.values()
1019
+ if candidate["activation_id"] == guard.activation_id
1020
+ ),
1021
+ None,
1022
+ )
1023
+ if (
1024
+ row is None
1025
+ or row.get("released")
1026
+ or self._lease_expired(row)
1027
+ or row["fencing_token"] != int(guard.fencing_token)
1028
+ ):
1029
+ raise StaleFenceError(
1030
+ "activation write guard does not match the current lease",
1031
+ details={
1032
+ "activation_id": guard.activation_id,
1033
+ "fencing_token": int(guard.fencing_token),
1034
+ "session_id": envelope.session_id,
1035
+ },
1036
+ )
1037
+
1038
+ async def append_event(
1039
+ self,
1040
+ envelope: SessionEventEnvelope,
1041
+ *,
1042
+ expected_fence: int,
1043
+ agent_instance_id: str | None = None,
1044
+ ) -> SessionEventEnvelope:
1045
+ row = self._resolve_activation(envelope.session_id, agent_instance_id)
1046
+ async with self._lock(row["agent_instance_id"], envelope.session_id):
1047
+ fresh = self._resolve_activation(envelope.session_id, agent_instance_id)
1048
+ activation = self._check_fence(
1049
+ fresh["agent_instance_id"], envelope.session_id, expected_fence
1050
+ )
1051
+ return await self._events.append(
1052
+ envelope, guard=ActivationWriteGuard(
1053
+ activation_id=activation["activation_id"],
1054
+ fencing_token=int(expected_fence),
1055
+ )
1056
+ )
1057
+
1058
+ def _resolve_activation(
1059
+ self, session_id: str, agent_instance_id: str | None
1060
+ ) -> dict[str, Any]:
1061
+ if agent_instance_id is not None:
1062
+ row = self._activation_row(agent_instance_id, session_id)
1063
+ if row is None:
1064
+ raise StaleFenceError(
1065
+ "no active activation lease",
1066
+ details={"agent_instance_id": agent_instance_id, "session_id": session_id},
1067
+ )
1068
+ return row
1069
+ matches = [
1070
+ row
1071
+ for (agent, session), row in self._activations.items()
1072
+ if session == session_id and row.get("released") is not True
1073
+ ]
1074
+ if len(matches) != 1:
1075
+ raise StaleFenceError(
1076
+ "cannot resolve a single activation lease for session",
1077
+ details={"session_id": session_id, "matches": len(matches)},
1078
+ )
1079
+ return matches[0]
1080
+
1081
+ # -------------------------------------------------------------------- runs
1082
+
1083
+ async def load_run(self, run_id: str) -> RunRecord | None:
1084
+ return self._runs.get(run_id)
1085
+
1086
+ async def save_run_transition(
1087
+ self, run: RunRecord, *, expected_fence: int
1088
+ ) -> RunRecord:
1089
+ async with self._lock(run.agent_instance_id, run.session_id):
1090
+ activation = self._check_fence(
1091
+ run.agent_instance_id, run.session_id, expected_fence
1092
+ )
1093
+ existing = self._runs.get(run.run_id)
1094
+ assert_run_transition(existing.state if existing else None, run.state)
1095
+ if is_active_run(run.state):
1096
+ for other in self._runs.values():
1097
+ if (
1098
+ other.run_id != run.run_id
1099
+ and other.session_id == run.session_id
1100
+ and is_active_run(other.state)
1101
+ ):
1102
+ raise InvalidCommandError(
1103
+ "session already has an active run",
1104
+ details={
1105
+ "session_id": run.session_id,
1106
+ "active_run_id": other.run_id,
1107
+ },
1108
+ )
1109
+ stored = run.model_copy(
1110
+ update={
1111
+ "activation_fence": int(expected_fence),
1112
+ "created_at": existing.created_at if existing else now_iso(),
1113
+ "updated_at": now_iso(),
1114
+ }
1115
+ )
1116
+ self._runs[run.run_id] = stored
1117
+ await self._emit(
1118
+ control_event(
1119
+ session_id=run.session_id,
1120
+ event_type="control.run_transition",
1121
+ payload={
1122
+ "run_id": run.run_id,
1123
+ "state": run.state.value,
1124
+ "fencing_token": int(expected_fence),
1125
+ },
1126
+ run_id=run.run_id,
1127
+ ),
1128
+ activation_row=activation,
1129
+ )
1130
+ return stored
1131
+
1132
+
1133
+ __all__ = ["InMemoryAgentKernelStore"]