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

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