agentengine-sdk-python 0.8.4__py3-none-any.whl → 0.8.6__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 (1221) hide show
  1. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/METADATA +17 -10
  2. agentengine_sdk_python-0.8.6.dist-info/RECORD +1731 -0
  3. ksadk/_async_iterators.py +79 -0
  4. ksadk/_build_provenance.json +1 -1
  5. ksadk/_model_anthropic.py +49 -0
  6. ksadk/_model_input.py +86 -0
  7. ksadk/_model_json.py +40 -0
  8. ksadk/_model_reasoning.py +17 -0
  9. ksadk/_model_refusal.py +20 -0
  10. ksadk/_model_sse.py +163 -0
  11. ksadk/_process.py +83 -0
  12. ksadk/_str_enum.py +17 -0
  13. ksadk/a2a/__init__.py +157 -167
  14. ksadk/a2a/_invocation_state.py +86 -0
  15. ksadk/a2a/_space_client_events.py +33 -22
  16. ksadk/a2a/adk.py +194 -0
  17. ksadk/a2a/adk_invocation_context.py +242 -0
  18. ksadk/a2a/adk_runtime.py +126 -0
  19. ksadk/a2a/adk_tools.py +319 -0
  20. ksadk/a2a/bootstrap.py +149 -23
  21. ksadk/a2a/build_validation.py +147 -0
  22. ksadk/a2a/card.py +23 -15
  23. ksadk/a2a/codex.py +160 -0
  24. ksadk/a2a/codex_checkpoint.py +111 -0
  25. ksadk/a2a/codex_recovery.py +96 -0
  26. ksadk/a2a/codex_recovery_events.py +123 -0
  27. ksadk/a2a/codex_runtime.py +198 -0
  28. ksadk/a2a/codex_session_checkpoint.py +162 -0
  29. ksadk/a2a/composition.py +88 -0
  30. ksadk/a2a/context_store.py +118 -19
  31. ksadk/a2a/control_plane.py +108 -106
  32. ksadk/a2a/event_adapter.py +92 -193
  33. ksadk/a2a/executor.py +257 -85
  34. ksadk/a2a/exposure.py +106 -0
  35. ksadk/a2a/external_transport.py +3 -4
  36. ksadk/a2a/identity.py +106 -12
  37. ksadk/a2a/ids.py +14 -3
  38. ksadk/a2a/inbound_kernel.py +133 -0
  39. ksadk/a2a/interaction_wire.py +76 -0
  40. ksadk/a2a/invocation_checkpoint.py +274 -0
  41. ksadk/a2a/invocation_policy.py +87 -0
  42. ksadk/a2a/invocation_publication.py +210 -0
  43. ksadk/a2a/invocation_service.py +972 -0
  44. ksadk/a2a/invocation_types.py +459 -0
  45. ksadk/a2a/kernel_task_adapter.py +259 -0
  46. ksadk/a2a/langgraph.py +20 -7
  47. ksadk/a2a/langgraph_invocation_context.py +136 -0
  48. ksadk/a2a/langgraph_managed.py +150 -0
  49. ksadk/a2a/langgraph_recovery.py +32 -0
  50. ksadk/a2a/langgraph_runtime.py +89 -0
  51. ksadk/a2a/langgraph_tool_runtime.py +132 -0
  52. ksadk/a2a/langgraph_tools.py +63 -0
  53. ksadk/a2a/message_digest.py +40 -0
  54. ksadk/a2a/platform.py +389 -0
  55. ksadk/a2a/policy_refs.py +13 -0
  56. ksadk/a2a/presentation.py +402 -0
  57. ksadk/a2a/product_kernel_facts.py +177 -0
  58. ksadk/a2a/product_key.py +99 -0
  59. ksadk/a2a/protocol_errors.py +116 -0
  60. ksadk/a2a/result_text.py +49 -0
  61. ksadk/a2a/resume_store.py +93 -3
  62. ksadk/a2a/routes.py +6 -1
  63. ksadk/a2a/runtime_approval.py +198 -0
  64. ksadk/a2a/runtime_input.py +151 -0
  65. ksadk/a2a/server.py +16 -7
  66. ksadk/a2a/space_client.py +101 -16
  67. ksadk/a2a/task_adapter.py +19 -5
  68. ksadk/a2a/task_event_dispatcher.py +39 -2
  69. ksadk/a2a/task_event_outbox.py +215 -0
  70. ksadk/a2a/task_event_outbox_owned.py +130 -0
  71. ksadk/a2a/task_store.py +37 -0
  72. ksadk/a2a/tool_activity.py +176 -0
  73. ksadk/a2a/workload_tokens.py +134 -0
  74. ksadk/a2ui/core.py +3 -3
  75. ksadk/api/client.py +38 -79
  76. ksadk/api/sse_stream.py +96 -0
  77. ksadk/builders/code_builder.py +121 -118
  78. ksadk/builders/container_builder.py +11 -29
  79. ksadk/builders/framework_requirements.py +22 -2
  80. ksadk/builders/managed_runtime_builder.py +12 -0
  81. ksadk/builders/source_provenance.py +87 -0
  82. ksadk/cli/__init__.py +36 -0
  83. ksadk/cli/cmd_a2a.py +43 -0
  84. ksadk/cli/cmd_build.py +9 -0
  85. ksadk/cli/cmd_config.py +4 -4
  86. ksadk/cli/cmd_create.py +12 -12
  87. ksadk/cli/cmd_deploy.py +27 -2
  88. ksadk/cli/cmd_hermes.py +24 -2
  89. ksadk/cli/cmd_invoke.py +64 -88
  90. ksadk/cli/cmd_openclaw.py +26 -1
  91. ksadk/cli/cmd_plugin.py +70 -12
  92. ksadk/cli/cmd_studio.py +69 -17
  93. ksadk/cli/cmd_web.py +2 -0
  94. ksadk/cli/hosted_invoke.py +206 -0
  95. ksadk/cli/invoke_content.py +60 -0
  96. ksadk/cli/openclaw_env.py +1 -1
  97. ksadk/cli/resource_common.py +26 -0
  98. ksadk/cli/runtime_bootstrap.py +27 -5
  99. ksadk/codex/_expert_request_reader.py +48 -0
  100. ksadk/codex/a2a_tools.py +140 -0
  101. ksadk/codex/async_approval.py +169 -0
  102. ksadk/codex/async_client.py +938 -0
  103. ksadk/codex/async_proxy.py +216 -0
  104. ksadk/codex/base_runtime.py +25 -0
  105. ksadk/codex/client.py +3 -1047
  106. ksadk/codex/client_payloads.py +85 -0
  107. ksadk/codex/rollout_checkpoint.py +264 -0
  108. ksadk/codex/runtime.py +290 -54
  109. ksadk/common/auth.py +2 -2
  110. ksadk/common/cloud_headers.py +36 -0
  111. ksadk/common/diagnostics.py +34 -0
  112. ksadk/common/kop_client.py +2 -1
  113. ksadk/configs/__init__.py +1 -1
  114. ksadk/configs/env_registry.py +216 -143
  115. ksadk/configs/env_registry_a2a.py +173 -0
  116. ksadk/configs/env_registry_studio.py +134 -0
  117. ksadk/configs/global_config.py +2 -1
  118. ksadk/configs/settings.py +9 -3
  119. ksadk/connector/__init__.py +17 -0
  120. ksadk/connector/channel.py +477 -0
  121. ksadk/context_engine/assembler.py +9 -0
  122. ksadk/context_engine/capabilities.py +52 -4
  123. ksadk/context_engine/contributors.py +80 -17
  124. ksadk/context_engine/instruction_authority.py +18 -0
  125. ksadk/conversations/attachment_storage.py +10 -3
  126. ksadk/conversations/attachments.py +69 -20
  127. ksadk/conversations/contracts.py +28 -9
  128. ksadk/conversations/message_projection.py +51 -155
  129. ksadk/conversations/message_projection_blocks.py +137 -0
  130. ksadk/conversations/model_context.py +11 -3
  131. ksadk/conversations/normalize.py +28 -10
  132. ksadk/conversations/projector.py +63 -0
  133. ksadk/conversations/runtime_baseline.py +37 -0
  134. ksadk/conversations/runtime_compaction.py +7 -2
  135. ksadk/conversations/runtime_input.py +16 -4
  136. ksadk/conversations/runtime_invocation.py +17 -4
  137. ksadk/conversations/runtime_observability.py +39 -10
  138. ksadk/conversations/runtime_payloads.py +12 -0
  139. ksadk/conversations/runtime_persistence.py +296 -32
  140. ksadk/conversations/runtime_preparation.py +98 -13
  141. ksadk/conversations/runtime_projection.py +217 -0
  142. ksadk/conversations/runtime_resume.py +141 -70
  143. ksadk/conversations/runtime_stream_events.py +29 -35
  144. ksadk/conversations/runtime_streaming.py +8 -0
  145. ksadk/conversations/tool_receipt_resume.py +387 -0
  146. ksadk/conversations/tool_receipts.py +32 -0
  147. ksadk/deployment/env_forward.py +5 -0
  148. ksadk/deployment/manager.py +15 -6
  149. ksadk/deployment/providers/serverless.py +76 -136
  150. ksadk/deployment/providers/serverless_payloads.py +122 -0
  151. ksadk/evaluation/agent_eval_client.py +2 -1
  152. ksadk/evaluation/contracts.py +78 -3
  153. ksadk/evaluation/evaluators.py +166 -1
  154. ksadk/evaluation/evidence.py +271 -4
  155. ksadk/evaluation/local_adapter.py +76 -12
  156. ksadk/evaluation/studio_build_adapter.py +73 -9
  157. ksadk/events/adapters/_a2a_checkpoint.py +151 -0
  158. ksadk/events/adapters/_a2a_snapshot.py +18 -14
  159. ksadk/events/adapters/_a2a_support.py +15 -3
  160. ksadk/events/adapters/_a2a_tool_activity.py +123 -0
  161. ksadk/events/adapters/_codex_errors.py +49 -0
  162. ksadk/events/adapters/_codex_items.py +16 -8
  163. ksadk/events/adapters/_langgraph_support.py +46 -1
  164. ksadk/events/adapters/a2a.py +54 -26
  165. ksadk/events/adapters/adk.py +188 -30
  166. ksadk/events/adapters/codex.py +33 -7
  167. ksadk/events/adapters/langgraph.py +16 -6
  168. ksadk/events/canonical.py +5 -0
  169. ksadk/events/canonical_store.py +200 -4
  170. ksadk/events/reducer.py +82 -0
  171. ksadk/events/run_authority.py +176 -0
  172. ksadk/events/session_event.py +2 -2
  173. ksadk/harness/__init__.py +18 -0
  174. ksadk/harness/app.py +11 -0
  175. ksadk/harness/artifact_adapters/__init__.py +1 -0
  176. ksadk/harness/artifact_adapters/native_page_budget.py +172 -0
  177. ksadk/harness/artifact_adapters/native_search.py +249 -0
  178. ksadk/harness/artifact_adapters/native_search_budget.py +134 -0
  179. ksadk/harness/artifact_adapters/native_store.py +115 -0
  180. ksadk/harness/artifact_adapters/native_tool.py +222 -0
  181. ksadk/harness/artifact_store.py +230 -0
  182. ksadk/harness/browser_adapters/__init__.py +6 -0
  183. ksadk/harness/browser_adapters/native_browser_tool.py +92 -0
  184. ksadk/harness/browser_adapters/native_dom_snapshot.py +187 -0
  185. ksadk/harness/browser_adapters/native_playwright.py +953 -0
  186. ksadk/harness/browser_adapters/native_vision.py +295 -0
  187. ksadk/harness/capabilities.py +115 -0
  188. ksadk/harness/capability_adapters/__init__.py +1 -0
  189. ksadk/harness/capability_adapters/native_dsh.py +289 -0
  190. ksadk/harness/capability_runtime.py +332 -0
  191. ksadk/harness/closure_contract.py +92 -0
  192. ksadk/harness/compaction_record.py +153 -0
  193. ksadk/harness/compiler.py +284 -0
  194. ksadk/harness/config.py +28 -5
  195. ksadk/harness/conformance/__init__.py +33 -0
  196. ksadk/harness/conformance/contract.py +504 -0
  197. ksadk/harness/conformance/fixtures.py +102 -0
  198. ksadk/harness/context_engine.py +388 -0
  199. ksadk/harness/context_maintenance.py +259 -0
  200. ksadk/harness/context_manifest.py +188 -0
  201. ksadk/harness/context_sections.py +397 -0
  202. ksadk/harness/context_tuning.py +168 -0
  203. ksadk/harness/diagnostic_adapters/__init__.py +1 -0
  204. ksadk/harness/diagnostic_adapters/contracts.py +70 -0
  205. ksadk/harness/diagnostic_adapters/lsp_rpc.py +210 -0
  206. ksadk/harness/diagnostic_adapters/native_lsp.py +286 -0
  207. ksadk/harness/draft_runtime.py +322 -0
  208. ksadk/harness/engine/__init__.py +40 -0
  209. ksadk/harness/engine/base.py +122 -0
  210. ksadk/harness/engine/budgets.py +108 -0
  211. ksadk/harness/engine/capability_declarations.py +59 -0
  212. ksadk/harness/engine/completion_guard.py +57 -0
  213. ksadk/harness/engine/context_pipeline.py +1351 -0
  214. ksadk/harness/engine/graph_builder.py +352 -0
  215. ksadk/harness/engine/hooked_reasoner.py +380 -0
  216. ksadk/harness/engine/langgraph.py +1049 -0
  217. ksadk/harness/engine/mcp_disclosure.py +701 -0
  218. ksadk/harness/engine/phases/__init__.py +6 -0
  219. ksadk/harness/engine/phases/context.py +48 -0
  220. ksadk/harness/engine/phases/interaction.py +31 -0
  221. ksadk/harness/engine/phases/reason.py +552 -0
  222. ksadk/harness/engine/phases/tools.py +638 -0
  223. ksadk/harness/engine/policy_runtime.py +81 -0
  224. ksadk/harness/engine/postgres_checkpointer.py +386 -0
  225. ksadk/harness/engine/run_control_bridge.py +100 -0
  226. ksadk/harness/engine/run_types.py +98 -0
  227. ksadk/harness/engine/skill_disclosure.py +275 -0
  228. ksadk/harness/engine/spans.py +41 -0
  229. ksadk/harness/engine/steering.py +177 -0
  230. ksadk/harness/engine/subagents.py +360 -0
  231. ksadk/harness/engine/thread_ids.py +112 -0
  232. ksadk/harness/engine/tool_dispatch.py +249 -0
  233. ksadk/harness/event_tree.py +212 -0
  234. ksadk/harness/events.py +498 -0
  235. ksadk/harness/execution_policy.py +104 -0
  236. ksadk/harness/hooks.py +259 -0
  237. ksadk/harness/insights.py +210 -0
  238. ksadk/harness/lifecycle.py +578 -0
  239. ksadk/harness/lifecycle_conformance.py +233 -0
  240. ksadk/harness/lifecycle_local_plane.py +104 -0
  241. ksadk/harness/loop/__init__.py +44 -0
  242. ksadk/harness/loop/reason.py +699 -0
  243. ksadk/harness/loop/tools.py +907 -0
  244. ksadk/harness/managed_runtime.py +1044 -0
  245. ksadk/harness/mcp_disclosure_contract.py +85 -0
  246. ksadk/harness/mcp_matrix.py +474 -0
  247. ksadk/harness/mcp_result_policy.py +44 -0
  248. ksadk/harness/mcp_runtime.py +504 -0
  249. ksadk/harness/memory_consolidation.py +157 -0
  250. ksadk/harness/memory_loop.py +156 -0
  251. ksadk/harness/memory_runtime.py +541 -0
  252. ksadk/harness/metrics.py +90 -0
  253. ksadk/harness/micro_compact.py +69 -0
  254. ksadk/harness/model_adapters/__init__.py +1 -0
  255. ksadk/harness/model_adapters/native_mcp_media.py +100 -0
  256. ksadk/harness/model_adapters/native_mcp_result.py +36 -0
  257. ksadk/harness/model_adapters/native_openai.py +703 -0
  258. ksadk/harness/model_capability.py +468 -0
  259. ksadk/harness/model_provider.py +430 -0
  260. ksadk/harness/multimodal.py +57 -0
  261. ksadk/harness/native/__init__.py +1 -0
  262. ksadk/harness/native/activation_contract.py +25 -0
  263. ksadk/harness/native/activation_recovery.py +29 -0
  264. ksadk/harness/native/adapters.py +291 -0
  265. ksadk/harness/native/artifact_contract.py +14 -0
  266. ksadk/harness/native/artifact_partial_read_capacity.py +171 -0
  267. ksadk/harness/native/artifact_read_capacity.py +182 -0
  268. ksadk/harness/native/artifact_read_contract.py +27 -0
  269. ksadk/harness/native/artifact_runtime.py +408 -0
  270. ksadk/harness/native/artifact_search_capacity.py +206 -0
  271. ksadk/harness/native/artifact_search_contract.py +40 -0
  272. ksadk/harness/native/artifact_single_read_capacity.py +170 -0
  273. ksadk/harness/native/browser_contract.py +229 -0
  274. ksadk/harness/native/browser_recovery.py +183 -0
  275. ksadk/harness/native/capabilities.py +204 -0
  276. ksadk/harness/native/child_approval.py +60 -0
  277. ksadk/harness/native/child_budget.py +136 -0
  278. ksadk/harness/native/child_cancellation.py +80 -0
  279. ksadk/harness/native/child_input.py +98 -0
  280. ksadk/harness/native/child_input_runtime.py +59 -0
  281. ksadk/harness/native/child_interruption.py +81 -0
  282. ksadk/harness/native/child_lifecycle.py +246 -0
  283. ksadk/harness/native/child_lifecycle_contract.py +91 -0
  284. ksadk/harness/native/child_notifications.py +75 -0
  285. ksadk/harness/native/child_promotion.py +66 -0
  286. ksadk/harness/native/command_runtime.py +99 -0
  287. ksadk/harness/native/completion.py +302 -0
  288. ksadk/harness/native/completion_text.py +116 -0
  289. ksadk/harness/native/context.py +358 -0
  290. ksadk/harness/native/context_artifact_page.py +75 -0
  291. ksadk/harness/native/context_compaction_projection.py +131 -0
  292. ksadk/harness/native/context_contributions.py +320 -0
  293. ksadk/harness/native/context_contributor_contract.py +133 -0
  294. ksadk/harness/native/context_driver.py +820 -0
  295. ksadk/harness/native/context_fresh_tools.py +49 -0
  296. ksadk/harness/native/context_history.py +272 -0
  297. ksadk/harness/native/context_microcompact.py +61 -0
  298. ksadk/harness/native/context_partial_summary.py +58 -0
  299. ksadk/harness/native/context_partition.py +492 -0
  300. ksadk/harness/native/context_pruning.py +275 -0
  301. ksadk/harness/native/context_reduction.py +60 -0
  302. ksadk/harness/native/context_replay.py +148 -0
  303. ksadk/harness/native/context_request_projection.py +526 -0
  304. ksadk/harness/native/context_runtime.py +976 -0
  305. ksadk/harness/native/context_runtime_contracts.py +409 -0
  306. ksadk/harness/native/context_search.py +112 -0
  307. ksadk/harness/native/context_source_fragments.py +188 -0
  308. ksadk/harness/native/context_source_progress.py +119 -0
  309. ksadk/harness/native/context_summary_planning.py +788 -0
  310. ksadk/harness/native/context_summary_recovery.py +105 -0
  311. ksadk/harness/native/delivery_review.py +331 -0
  312. ksadk/harness/native/delivery_review_contract.py +54 -0
  313. ksadk/harness/native/effect_admission.py +82 -0
  314. ksadk/harness/native/effect_control.py +318 -0
  315. ksadk/harness/native/effect_receipts.py +729 -0
  316. ksadk/harness/native/effect_runtime.py +303 -0
  317. ksadk/harness/native/effective_configuration.py +28 -0
  318. ksadk/harness/native/engine.py +945 -0
  319. ksadk/harness/native/final_output.py +34 -0
  320. ksadk/harness/native/final_submission.py +58 -0
  321. ksadk/harness/native/finalization_workspace.py +96 -0
  322. ksadk/harness/native/fork.py +151 -0
  323. ksadk/harness/native/guidance.py +91 -0
  324. ksadk/harness/native/history.py +381 -0
  325. ksadk/harness/native/hook_executor.py +205 -0
  326. ksadk/harness/native/hook_runtime_contract.py +282 -0
  327. ksadk/harness/native/interaction_observation.py +117 -0
  328. ksadk/harness/native/lease.py +103 -0
  329. ksadk/harness/native/machine.py +974 -0
  330. ksadk/harness/native/mcp_result_governance.py +156 -0
  331. ksadk/harness/native/model.py +570 -0
  332. ksadk/harness/native/model_feedback.py +63 -0
  333. ksadk/harness/native/model_input.py +30 -0
  334. ksadk/harness/native/model_recovery_contract.py +35 -0
  335. ksadk/harness/native/model_replay.py +128 -0
  336. ksadk/harness/native/model_request.py +133 -0
  337. ksadk/harness/native/model_runtime.py +196 -0
  338. ksadk/harness/native/model_takeover.py +419 -0
  339. ksadk/harness/native/model_tool_batch.py +40 -0
  340. ksadk/harness/native/observed_progress.py +46 -0
  341. ksadk/harness/native/operator_guidance.py +201 -0
  342. ksadk/harness/native/output_contract.py +80 -0
  343. ksadk/harness/native/output_recovery.py +24 -0
  344. ksadk/harness/native/ports.py +749 -0
  345. ksadk/harness/native/precompact_hook_runtime.py +171 -0
  346. ksadk/harness/native/replay_context.py +149 -0
  347. ksadk/harness/native/request_media.py +112 -0
  348. ksadk/harness/native/restore_contract.py +16 -0
  349. ksadk/harness/native/run_clock.py +120 -0
  350. ksadk/harness/native/state.py +436 -0
  351. ksadk/harness/native/steering.py +170 -0
  352. ksadk/harness/native/stop_hook_runtime.py +151 -0
  353. ksadk/harness/native/studio_admission.py +29 -0
  354. ksadk/harness/native/subagents.py +86 -0
  355. ksadk/harness/native/task_policy.py +99 -0
  356. ksadk/harness/native/task_progress.py +86 -0
  357. ksadk/harness/native/terminal_verification.py +43 -0
  358. ksadk/harness/native/tool_argument_feedback.py +178 -0
  359. ksadk/harness/native/tool_budget.py +253 -0
  360. ksadk/harness/native/tool_error_streak.py +89 -0
  361. ksadk/harness/native/tool_hook_runtime.py +343 -0
  362. ksadk/harness/native/tool_input_validation.py +58 -0
  363. ksadk/harness/native/tool_interruption.py +96 -0
  364. ksadk/harness/native/tool_observation.py +259 -0
  365. ksadk/harness/native/tool_progress.py +223 -0
  366. ksadk/harness/native/tool_recovery.py +25 -0
  367. ksadk/harness/native/tool_scheduling.py +33 -0
  368. ksadk/harness/native/tool_schema.py +126 -0
  369. ksadk/harness/native/transitions.py +151 -0
  370. ksadk/harness/native/turn_control.py +98 -0
  371. ksadk/harness/native/user_document.py +98 -0
  372. ksadk/harness/native/user_document_capture.py +49 -0
  373. ksadk/harness/native/user_input.py +87 -0
  374. ksadk/harness/native/verification.py +432 -0
  375. ksadk/harness/native/verification_binding.py +40 -0
  376. ksadk/harness/native/verification_runtime.py +233 -0
  377. ksadk/harness/native/verification_workspace.py +67 -0
  378. ksadk/harness/native_driver_failure.py +123 -0
  379. ksadk/harness/native_facade.py +977 -0
  380. ksadk/harness/native_factory.py +244 -0
  381. ksadk/harness/native_projection.py +503 -0
  382. ksadk/harness/native_runtime.py +621 -0
  383. ksadk/harness/native_shadow.py +250 -0
  384. ksadk/harness/native_shadow_inputs.py +146 -0
  385. ksadk/harness/observability/__init__.py +29 -0
  386. ksadk/harness/observability/inspection.py +528 -0
  387. ksadk/harness/observability/metrics.py +167 -0
  388. ksadk/harness/observability/otel_exporter.py +218 -0
  389. ksadk/harness/progress_adapters/__init__.py +5 -0
  390. ksadk/harness/progress_adapters/native_todo.py +75 -0
  391. ksadk/harness/prompt_cache.py +132 -0
  392. ksadk/harness/public_activity.py +213 -0
  393. ksadk/harness/public_output.py +30 -0
  394. ksadk/harness/public_summary.py +36 -0
  395. ksadk/harness/readiness.py +249 -0
  396. ksadk/harness/reasoner.py +779 -17
  397. ksadk/harness/release_readiness.py +140 -0
  398. ksadk/harness/resource_ref.py +69 -0
  399. ksadk/harness/rpc.py +200 -0
  400. ksadk/harness/run_control.py +773 -0
  401. ksadk/harness/runtime.py +157 -113
  402. ksadk/harness/runtime_capabilities.py +228 -0
  403. ksadk/harness/runtime_server.py +514 -0
  404. ksadk/harness/sandbox_adapters.py +739 -0
  405. ksadk/harness/sandbox_backend.py +733 -0
  406. ksadk/harness/sandbox_conformance.py +288 -0
  407. ksadk/harness/sandbox_lease.py +77 -0
  408. ksadk/harness/sandbox_lease_conformance.py +259 -0
  409. ksadk/harness/sandbox_matrix.py +170 -0
  410. ksadk/harness/sandbox_reconciliation.py +140 -0
  411. ksadk/harness/sandbox_recovery.py +586 -0
  412. ksadk/harness/sandbox_recovery_conformance.py +246 -0
  413. ksadk/harness/sdk.py +153 -0
  414. ksadk/harness/session_store.py +589 -0
  415. ksadk/harness/skill_adapters/__init__.py +1 -0
  416. ksadk/harness/skill_adapters/native_bundle.py +148 -0
  417. ksadk/harness/skill_adapters/native_disclosure.py +306 -0
  418. ksadk/harness/skill_composition.py +212 -0
  419. ksadk/harness/skill_feedback.py +123 -0
  420. ksadk/harness/skill_intent.py +47 -0
  421. ksadk/harness/skill_matrix.py +233 -0
  422. ksadk/harness/skill_runtime.py +317 -0
  423. ksadk/harness/spec.py +342 -0
  424. ksadk/harness/state.py +155 -0
  425. ksadk/harness/stores/__init__.py +14 -0
  426. ksadk/harness/stores/native_command_settlement.py +65 -0
  427. ksadk/harness/stores/native_contract.py +311 -0
  428. ksadk/harness/stores/native_memory.py +326 -0
  429. ksadk/harness/stores/native_memory_archive.py +70 -0
  430. ksadk/harness/stores/native_migration_cli.py +91 -0
  431. ksadk/harness/stores/native_outbox.py +56 -0
  432. ksadk/harness/stores/native_sqlite.py +782 -0
  433. ksadk/harness/stores/native_sqlite_audit.py +29 -0
  434. ksadk/harness/stores/native_sqlite_migration.py +252 -0
  435. ksadk/harness/strategies.py +149 -0
  436. ksadk/harness/subagent.py +263 -0
  437. ksadk/harness/subagent_adapters/checkpoint_recovery.py +120 -0
  438. ksadk/harness/subagent_adapters/native_background_child.py +803 -0
  439. ksadk/harness/subagent_adapters/native_child.py +819 -0
  440. ksadk/harness/subagent_adapters/native_child_approval.py +128 -0
  441. ksadk/harness/subagent_adapters/native_child_continuation.py +51 -0
  442. ksadk/harness/subagent_adapters/native_child_result.py +392 -0
  443. ksadk/harness/subagent_adapters/prepared_recovery.py +120 -0
  444. ksadk/harness/tool_display.py +75 -0
  445. ksadk/harness/tool_display_contract.py +134 -0
  446. ksadk/harness/tool_output.py +141 -0
  447. ksadk/harness/tool_policy.py +134 -0
  448. ksadk/harness/tool_receipts.py +96 -0
  449. ksadk/harness/tool_reliability.py +81 -0
  450. ksadk/harness/tool_result_policy.py +216 -0
  451. ksadk/harness/tools.py +52 -0
  452. ksadk/harness/user_errors.py +69 -0
  453. ksadk/harness/working_context.py +81 -0
  454. ksadk/harness/working_context_patch.py +99 -0
  455. ksadk/harness/workspace_adapters/__init__.py +6 -0
  456. ksadk/harness/workspace_adapters/native_directory.py +180 -0
  457. ksadk/harness/workspace_adapters/native_git.py +208 -0
  458. ksadk/harness/workspace_adapters/native_isolation.py +350 -0
  459. ksadk/identity/resolver.py +18 -0
  460. ksadk/interaction/__init__.py +1 -1
  461. ksadk/interaction/ledger.py +1 -1
  462. ksadk/interaction/provider.py +1 -1
  463. ksadk/interaction/providers/__init__.py +3 -1
  464. ksadk/interaction/providers/adk.py +1 -1
  465. ksadk/interaction/providers/codex.py +1 -1
  466. ksadk/interaction/providers/harness.py +50 -0
  467. ksadk/interaction/providers/langgraph.py +9 -2
  468. ksadk/kernel/__init__.py +10 -0
  469. ksadk/kernel/approval_presentation.py +91 -0
  470. ksadk/kernel/authorization.py +1 -1
  471. ksadk/kernel/bootstrap.py +220 -252
  472. ksadk/kernel/contract_fingerprints.py +45 -1
  473. ksadk/kernel/contracts.py +30 -0
  474. ksadk/kernel/control.py +180 -29
  475. ksadk/kernel/execution_grants.py +428 -0
  476. ksadk/kernel/execution_grants_memory.py +252 -0
  477. ksadk/kernel/execution_grants_postgres.py +395 -0
  478. ksadk/kernel/execution_grants_sqlite.py +435 -0
  479. ksadk/kernel/execution_host_ingress.py +1071 -0
  480. ksadk/kernel/history_projection.py +120 -0
  481. ksadk/kernel/ingress.py +139 -226
  482. ksadk/kernel/ingress_bootstrap.py +268 -0
  483. ksadk/kernel/legacy_history.py +179 -0
  484. ksadk/kernel/mapping.py +1 -1
  485. ksadk/kernel/memory_store.py +61 -58
  486. ksadk/kernel/memory_store_projection.py +63 -0
  487. ksadk/kernel/postgres_event_log.py +181 -0
  488. ksadk/kernel/postgres_store.py +230 -231
  489. ksadk/kernel/readiness.py +125 -0
  490. ksadk/kernel/recovery.py +166 -16
  491. ksadk/kernel/recovery_projection.py +124 -0
  492. ksadk/kernel/runtime_checkpoint.py +34 -0
  493. ksadk/kernel/sql/001_agent_kernel.sql +28 -0
  494. ksadk/kernel/sqlite_schema.py +100 -0
  495. ksadk/kernel/sqlite_store.py +410 -200
  496. ksadk/kernel/state.py +2 -3
  497. ksadk/kernel/store.py +104 -5
  498. ksadk/kernel/teams_artifact_tools.py +392 -0
  499. ksadk/kernel/teams_effects.py +851 -0
  500. ksadk/kernel/teams_execution_context.py +408 -0
  501. ksadk/kernel/teams_host_http.py +237 -0
  502. ksadk/kernel/teams_material_transport.py +245 -0
  503. ksadk/kernel/teams_materials.py +318 -0
  504. ksadk/kernel/teams_runtime_bootstrap.py +604 -0
  505. ksadk/kernel/teams_tool_runtime.py +273 -0
  506. ksadk/kernel/teams_workspace.py +259 -0
  507. ksadk/kernel/worker.py +369 -340
  508. ksadk/kernel/worker_controls.py +454 -0
  509. ksadk/kernel/worker_history.py +72 -0
  510. ksadk/kernel/worker_identity.py +74 -0
  511. ksadk/kernel/worker_interactions.py +197 -0
  512. ksadk/knowledge_base/client.py +15 -1
  513. ksadk/managed_runtime.py +29 -2
  514. ksadk/memory/adk/backends/http_ltm_backend.py +2 -1
  515. ksadk/memory/adk/backends/sdk_ltm_backend.py +15 -1
  516. ksadk/memory/adk/bounded_database_session_service.py +232 -0
  517. ksadk/memory/adk/resilient_session_service.py +115 -15
  518. ksadk/memory/adk/short_term_memory.py +8 -9
  519. ksadk/memory/coordinator.py +22 -6
  520. ksadk/memory/extraction.py +159 -7
  521. ksadk/memory/models.py +77 -1
  522. ksadk/memory/provider_resolver.py +5 -0
  523. ksadk/memory/providers/local_candidates.py +183 -0
  524. ksadk/memory/providers/local_postgres.py +200 -0
  525. ksadk/memory/providers/local_sqlite.py +280 -67
  526. ksadk/memory/resolved_policy.py +12 -7
  527. ksadk/memory/retrieval.py +189 -0
  528. ksadk/memory/store_adapter.py +150 -0
  529. ksadk/model_gateway/__init__.py +29 -0
  530. ksadk/model_gateway/client.py +253 -0
  531. ksadk/model_gateway/events.py +113 -0
  532. ksadk/model_gateway/matrix.py +297 -0
  533. ksadk/model_policy.py +2 -2
  534. ksadk/model_proxy/compat.py +670 -0
  535. ksadk/model_proxy/detect.py +254 -0
  536. ksadk/model_proxy/server.py +14 -9
  537. ksadk/model_proxy/transform.py +72 -6
  538. ksadk/plugins/__init__.py +1 -1
  539. ksadk/plugins/bridges/dsh.py +29 -20
  540. ksadk/plugins/bridges/dsh_profile_manifest.py +36 -0
  541. ksadk/plugins/builtin_resources.py +199 -0
  542. ksadk/plugins/builtins.py +417 -185
  543. ksadk/plugins/companion_artifacts.py +83 -0
  544. ksadk/plugins/companions.py +368 -0
  545. ksadk/plugins/context_contributor.py +15 -4
  546. ksadk/plugins/contracts.py +1 -1
  547. ksadk/plugins/dsh_channels.py +156 -0
  548. ksadk/plugins/dsh_dependency_policy.py +41 -0
  549. ksadk/plugins/dsh_home.py +140 -0
  550. ksadk/plugins/dsh_teams.py +105 -0
  551. ksadk/plugins/dsh_toolchain.py +92 -21
  552. ksadk/plugins/execution_host.py +107 -0
  553. ksadk/plugins/host.py +73 -5
  554. ksadk/plugins/kernel_adapter.py +44 -1
  555. ksadk/plugins/memory_provider.py +180 -0
  556. ksadk/plugins/providers/_process_group.py +50 -0
  557. ksadk/plugins/providers/bundles/dsh-channels-client/client.js +19 -0
  558. ksadk/plugins/providers/bundles/dsh-channels-client/cordis.patch.yml +3 -0
  559. ksadk/plugins/providers/bundles/dsh-channels-client/index.mjs +7 -0
  560. ksadk/plugins/providers/bundles/dsh-channels-client/package.json +12 -0
  561. ksadk/plugins/providers/bundles/dsh-knowledge/index.mjs +24 -2
  562. ksadk/plugins/providers/bundles/dsh-knowledge/package.json +1 -1
  563. ksadk/plugins/providers/bundles/dsh-memory/package.json +1 -1
  564. ksadk/plugins/providers/bundles/dsh-skill-center/package.json +1 -1
  565. ksadk/plugins/providers/bundles/dsh-teams-client/client.js +29 -0
  566. ksadk/plugins/providers/bundles/dsh-teams-client/cordis.patch.yml +3 -0
  567. ksadk/plugins/providers/bundles/dsh-teams-client/index.mjs +11 -0
  568. ksadk/plugins/providers/bundles/dsh-teams-client/package.json +35 -0
  569. ksadk/plugins/providers/bundles/dsh-teams-leader-policy/cordis.patch.yml +3 -0
  570. ksadk/plugins/providers/bundles/dsh-teams-leader-policy/index.mjs +13 -0
  571. ksadk/plugins/providers/bundles/dsh-teams-leader-policy/package.json +27 -0
  572. ksadk/plugins/providers/bundles/dsh-teams-profile/cordis.patch.yml +3 -0
  573. ksadk/plugins/providers/bundles/dsh-teams-profile/index.mjs +12 -0
  574. ksadk/plugins/providers/bundles/dsh-teams-profile/package.json +26 -0
  575. ksadk/plugins/providers/bundles/dsh-teams-router/cordis.patch.yml +3 -0
  576. ksadk/plugins/providers/bundles/dsh-teams-router/index.mjs +16 -0
  577. ksadk/plugins/providers/bundles/dsh-teams-router/package.json +27 -0
  578. ksadk/plugins/providers/bundles/dsh-teams-tasks/cordis.patch.yml +3 -0
  579. ksadk/plugins/providers/bundles/dsh-teams-tasks/index.mjs +16 -0
  580. ksadk/plugins/providers/bundles/dsh-teams-tasks/package.json +27 -0
  581. ksadk/plugins/providers/bundles/dsh-teams-tools/cordis.patch.yml +3 -0
  582. ksadk/plugins/providers/bundles/dsh-teams-tools/index.mjs +25 -0
  583. ksadk/plugins/providers/bundles/dsh-teams-tools/package.json +29 -0
  584. ksadk/plugins/providers/bundles/dsh-teams-tools/tools.json +235 -0
  585. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +1 -1
  586. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/index.mjs +51 -1
  587. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/package.json +2 -2
  588. ksadk/plugins/providers/bundles/ksadk-dsh-companion-host/client.mjs +133 -0
  589. ksadk/plugins/providers/bundles/ksadk-dsh-companion-host/index.mjs +14 -0
  590. ksadk/plugins/providers/bundles/ksadk-dsh-studio/client.js +27 -4
  591. ksadk/plugins/providers/bundles/ksadk-dsh-studio/index.mjs +9 -5
  592. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +1 -1
  593. ksadk/plugins/providers/codex.py +52 -40
  594. ksadk/plugins/providers/codex_native.py +20 -0
  595. ksadk/plugins/providers/codex_skills.py +46 -0
  596. ksadk/plugins/providers/dsh_capabilities.py +49 -143
  597. ksadk/plugins/providers/dsh_connector_lease.py +225 -0
  598. ksadk/plugins/providers/dsh_mcp.py +163 -1
  599. ksadk/plugins/providers/harness.py +413 -52
  600. ksadk/plugins/providers/harness_context.py +61 -0
  601. ksadk/plugins/providers/harness_contributions.py +241 -0
  602. ksadk/plugins/providers/harness_delegation.py +670 -0
  603. ksadk/plugins/providers/harness_hooks.py +121 -0
  604. ksadk/plugins/providers/harness_managed.py +550 -0
  605. ksadk/plugins/providers/harness_tools.py +455 -0
  606. ksadk/plugins/providers/legacy_catalog.py +1 -1
  607. ksadk/plugins/providers/mcp_projection.py +100 -3
  608. ksadk/plugins/providers/platform_resources.py +16 -6
  609. ksadk/plugins/providers/shipped_dsh.py +4 -1
  610. ksadk/plugins/subagent_providers/codex.py +28 -6
  611. ksadk/plugins/teams/__init__.py +5 -0
  612. ksadk/plugins/teams/api.py +405 -0
  613. ksadk/plugins/teams/application.py +540 -0
  614. ksadk/plugins/teams/artifacts.py +60 -0
  615. ksadk/plugins/teams/build_artifacts.py +460 -0
  616. ksadk/plugins/teams/cloud_contract_fingerprints.py +22 -0
  617. ksadk/plugins/teams/cloud_contracts.py +928 -0
  618. ksadk/plugins/teams/cloud_permits.py +250 -0
  619. ksadk/plugins/teams/context.py +96 -0
  620. ksadk/plugins/teams/contracts.py +136 -0
  621. ksadk/plugins/teams/domain.py +1480 -0
  622. ksadk/plugins/teams/domain_values.py +24 -0
  623. ksadk/plugins/teams/effect_contracts.py +255 -0
  624. ksadk/plugins/teams/errors.py +13 -0
  625. ksadk/plugins/teams/execution_projection.py +255 -0
  626. ksadk/plugins/teams/leader_decisions.py +106 -0
  627. ksadk/plugins/teams/migration.py +311 -0
  628. ksadk/plugins/teams/migration_cli.py +322 -0
  629. ksadk/plugins/teams/postgres_store.py +352 -0
  630. ksadk/plugins/teams/release_contracts.py +60 -0
  631. ksadk/plugins/teams/runtime.py +853 -0
  632. ksadk/plugins/teams/source_projection.py +84 -0
  633. ksadk/plugins/teams/store.py +519 -0
  634. ksadk/plugins/teams/task_decisions.py +436 -0
  635. ksadk/plugins/teams/tool_service.py +152 -0
  636. ksadk/plugins/teams/transport.py +96 -0
  637. ksadk/plugins/teams/workspaces.py +255 -0
  638. ksadk/resource_runtime/skill_runtime.py +3 -0
  639. ksadk/resource_runtime/socket_server.py +11 -0
  640. ksadk/resource_runtime/worker.py +28 -4
  641. ksadk/runners/_adk_a2a_stream.py +122 -0
  642. ksadk/runners/_adk_canonical_stream.py +474 -0
  643. ksadk/runners/_langgraph_canonical_streams.py +527 -0
  644. ksadk/runners/_langgraph_runner_streams.py +83 -401
  645. ksadk/runners/_langgraph_state_reader.py +106 -0
  646. ksadk/runners/_langgraph_tool_projection.py +86 -0
  647. ksadk/runners/_langgraph_tools_transformer.py +27 -0
  648. ksadk/runners/_session_identity.py +285 -0
  649. ksadk/runners/adk_runner.py +220 -350
  650. ksadk/runners/base_runner.py +6 -5
  651. ksadk/runners/langgraph_runner.py +208 -195
  652. ksadk/runners/patch_langchain.py +47 -0
  653. ksadk/runners/usage_accumulator.py +11 -3
  654. ksadk/runners/utils/loader.py +28 -14
  655. ksadk/runtime/_runner_adapter/a2ui_surface.py +145 -0
  656. ksadk/runtime/_runner_adapter/gateway_approvals.py +93 -0
  657. ksadk/runtime/_runner_adapter/stream_mapping.py +134 -129
  658. ksadk/runtime/adapter.py +40 -3
  659. ksadk/runtime/conversation_execution.py +29 -3
  660. ksadk/runtime/executor.py +105 -7
  661. ksadk/runtime/factory.py +45 -3
  662. ksadk/runtime/framework_adapters.py +184 -10
  663. ksadk/runtime/hosted_finalizer.py +4 -6
  664. ksadk/runtime/launch.py +2 -0
  665. ksadk/runtime/preprocessing.py +11 -3
  666. ksadk/runtime/runner_adapter.py +160 -21
  667. ksadk/runtime/skill_eval_result.py +36 -0
  668. ksadk/runtime/timing.py +46 -0
  669. ksadk/runtime/usage.py +49 -18
  670. ksadk/runtime_context.py +105 -1
  671. ksadk/sandbox/backends/e2b.py +272 -24
  672. ksadk/sandbox/backends/local_process.py +73 -39
  673. ksadk/sandbox/base.py +62 -1
  674. ksadk/sandbox/diagnostics.py +82 -0
  675. ksadk/sandbox/e2b_connection.py +9 -1
  676. ksadk/sandbox/local_controls.py +776 -0
  677. ksadk/sandbox/local_launcher.py +124 -0
  678. ksadk/scheduler/dispatcher.py +1 -1
  679. ksadk/server/factory.py +178 -25
  680. ksadk/server/history_deadline.py +158 -0
  681. ksadk/server/history_page_cache.py +129 -0
  682. ksadk/server/invocation_identity.py +118 -0
  683. ksadk/server/routes/checkpoint_audit.py +88 -0
  684. ksadk/server/routes/checkpoint_listing.py +294 -0
  685. ksadk/server/routes/checkpoint_resolution.py +73 -20
  686. ksadk/server/routes/common.py +32 -3
  687. ksadk/server/routes/control.py +112 -9
  688. ksadk/server/routes/dependencies.py +2 -1
  689. ksadk/server/routes/display_projection.py +154 -0
  690. ksadk/server/routes/kernel_ingress.py +26 -3
  691. ksadk/server/routes/misc.py +331 -33
  692. ksadk/server/routes/models.py +28 -4
  693. ksadk/server/routes/openai_compat.py +82 -13
  694. ksadk/server/routes/projection.py +159 -216
  695. ksadk/server/routes/run.py +112 -37
  696. ksadk/server/routes/sessions.py +493 -358
  697. ksadk/server/routes/streaming.py +68 -15
  698. ksadk/server/routes/tool_receipt_claim_routes.py +107 -0
  699. ksadk/server/routes/workspace.py +135 -23
  700. ksadk/server/static/assets/{ArtifactsPanel-CGbWlMNh.js → ArtifactsPanel-CILaW-fC.js} +1 -1
  701. ksadk/server/static/assets/{CodeBlock-b4C7upmO.js → CodeBlock-BVrNVtJC.js} +10 -7
  702. ksadk/server/static/assets/MathMessageMarkdown-Dp7c4wWD.js +8 -0
  703. ksadk/server/static/assets/{MermaidBlock-BEBN4Ksz.js → MermaidBlock-Dxop8Rd7.js} +9 -9
  704. ksadk/server/static/assets/{NativeTerminalPanel-zvL-WpAn.js → NativeTerminalPanel-Cjjz8B30.js} +1 -1
  705. ksadk/server/static/assets/{abnfDiagram-VCTEODGH-BYafW7Za.js → abnfDiagram-VCTEODGH-ckWDmOzw.js} +1 -1
  706. ksadk/server/static/assets/{arc-bTohInsS.js → arc-DqxJede-.js} +1 -1
  707. ksadk/server/static/assets/architecture-7GRP2DOG-pn_DuLy6.js +1 -0
  708. ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-Ch5Lbzsi.js → architectureDiagram-5GKGNRK7-SEQA4bVi.js} +1 -1
  709. ksadk/server/static/assets/{blockDiagram-NRAW4CY4-kqF4MDOG.js → blockDiagram-NRAW4CY4-pM0B-6NJ.js} +1 -1
  710. ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-8uSRIODv.js → c4Diagram-UCG6FXSJ-m2tuCNVq.js} +1 -1
  711. ksadk/server/static/assets/channel-fySsnkft.js +1 -0
  712. ksadk/server/static/assets/chunk-2Q5K7J3B-BheGxKRQ.js +1 -0
  713. ksadk/server/static/assets/{chunk-5VM5RSS4-DkhehZDr.js → chunk-5VM5RSS4-GJHDr9Ww.js} +1 -1
  714. ksadk/server/static/assets/{chunk-F27PBJKO-BdXhtPe9.js → chunk-F27PBJKO-CbAN45AV.js} +1 -1
  715. ksadk/server/static/assets/{chunk-G27WJ6UU-DPTpvpwi.js → chunk-G27WJ6UU-D5ngbbzF.js} +1 -1
  716. ksadk/server/static/assets/{chunk-JWPE2WC7-Pqoh38el.js → chunk-JWPE2WC7-Cc5IwuYC.js} +1 -1
  717. ksadk/server/static/assets/{chunk-LCL6LL3I-DeU00qC-.js → chunk-LCL6LL3I-CprgDDdJ.js} +1 -1
  718. ksadk/server/static/assets/{chunk-POPQ4Y6H-CzxScFkr.js → chunk-POPQ4Y6H-CDZhSy82.js} +1 -1
  719. ksadk/server/static/assets/{chunk-RHFEMEQ7-DpqMx-ux.js → chunk-RHFEMEQ7-CZchUNro.js} +1 -1
  720. ksadk/server/static/assets/{chunk-SVP7TREG-C6t0SypQ.js → chunk-SVP7TREG-BVruqk6T.js} +1 -1
  721. ksadk/server/static/assets/{chunk-XXDRQBXY-DVaeafZk.js → chunk-XXDRQBXY-4ld9SxPN.js} +1 -1
  722. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DSfLtNYm.js +1 -0
  723. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DSfLtNYm.js +1 -0
  724. ksadk/server/static/assets/{cose-bilkent-JH36ORCC-Bf9D3tzJ.js → cose-bilkent-JH36ORCC-Dmyyk_Vp.js} +1 -1
  725. ksadk/server/static/assets/cynefin-OW5HDTMX-PFWIGtvA.js +1 -0
  726. ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-B9cY3iTz.js → cynefinDiagram-5FMLGOSQ-BIyq3EJb.js} +1 -1
  727. ksadk/server/static/assets/{dagre-3AP2YEHR-CSwlx3CB.js → dagre-3AP2YEHR-eBw08Qdk.js} +1 -1
  728. ksadk/server/static/assets/{dagre-UBpgon5q.js → dagre-DfvnEf45.js} +1 -1
  729. ksadk/server/static/assets/{diagram-S7CK7UJ4-C9akhN9a.js → diagram-S7CK7UJ4-CUSrE3Mg.js} +1 -1
  730. ksadk/server/static/assets/{diagram-UQ7AKVKN-CgedhTPI.js → diagram-UQ7AKVKN-Dq5Zw2gy.js} +1 -1
  731. ksadk/server/static/assets/{diagram-VSXAHHWV-Cjucz5BY.js → diagram-VSXAHHWV-DyUHfNwT.js} +1 -1
  732. ksadk/server/static/assets/{diagram-VX7I27RA-Dd4Vleyn.js → diagram-VX7I27RA-CREmM89O.js} +1 -1
  733. ksadk/server/static/assets/{diagram-Z3DM3KII-CZ2jGtYW.js → diagram-Z3DM3KII-Dw9zhHUK.js} +1 -1
  734. ksadk/server/static/assets/{dist-Bj730Ib4.js → dist-0uJ4tdx6.js} +1 -1
  735. ksadk/server/static/assets/{dist-BnaTiyzN.js → dist-AEv-vXf-.js} +1 -1
  736. ksadk/server/static/assets/{dist-BgV_owcB.js → dist-B49GzZDb.js} +1 -1
  737. ksadk/server/static/assets/{dist-D_ctSS2i.js → dist-B8Myiet9.js} +1 -1
  738. ksadk/server/static/assets/{dist-ufY3F5p6.js → dist-BBsYarrI.js} +2 -2
  739. ksadk/server/static/assets/{dist-ClYUxfeK.js → dist-BQMrUwdI.js} +1 -1
  740. ksadk/server/static/assets/{dist-CFzsn6BG.js → dist-BX_-FENW.js} +1 -1
  741. ksadk/server/static/assets/{dist-B3FR1SKK.js → dist-CD4L6ymH.js} +1 -1
  742. ksadk/server/static/assets/{dist-Cf125P_m.js → dist-CHpW1BHj.js} +1 -1
  743. ksadk/server/static/assets/{dist-DVCOWtha.js → dist-CVPau3tn.js} +1 -1
  744. ksadk/server/static/assets/{dist-DFD6Qa6T.js → dist-C_qdxcSH.js} +1 -1
  745. ksadk/server/static/assets/{dist-BIr4DSRq.js → dist-Co68Apq4.js} +1 -1
  746. ksadk/server/static/assets/{dist-DiixqL0J.js → dist-Cp2PaOMY.js} +1 -1
  747. ksadk/server/static/assets/{dist-fBO2v5Kt.js → dist-CuR5Bgme.js} +1 -1
  748. ksadk/server/static/assets/{dist-DoVSwoDb.js → dist-DDSyLEOX.js} +1 -1
  749. ksadk/server/static/assets/{dist-C5_ZAZsG.js → dist-DUxO0R99.js} +1 -1
  750. ksadk/server/static/assets/{dist-C1NMAY5a.js → dist-QzbGrED_.js} +1 -1
  751. ksadk/server/static/assets/dist-bupkyEBZ.js +1 -0
  752. ksadk/server/static/assets/{dist-DvFK7p5S.js → dist-cxTGfdqx.js} +1 -1
  753. ksadk/server/static/assets/{dist-CB9iJrvg.js → dist-hL5p1til.js} +1 -1
  754. ksadk/server/static/assets/{dist-btbmmX5O.js → dist-lOy23y43.js} +1 -1
  755. ksadk/server/static/assets/{dist-QfkEHOlb.js → dist-pq-nD33R.js} +1 -1
  756. ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Ci8NqqZu.js → ebnfDiagram-PWID7BFC-BS5LdNoR.js} +1 -1
  757. ksadk/server/static/assets/{erDiagram-SSCWMZ5O-CCIOhLZG.js → erDiagram-SSCWMZ5O-Bv1QtiY-.js} +1 -1
  758. ksadk/server/static/assets/eventmodeling-NTZA5JFV-DsM4upQX.js +1 -0
  759. ksadk/server/static/assets/flowDiagram-A5DVABFB-D5NA16k9.js +1 -0
  760. ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-Cp_r9Hb6.js → ganttDiagram-EL5Y4UJY-musi-e0G.js} +1 -1
  761. ksadk/server/static/assets/gitGraph-4MIJSDKK-DUCVz-ih.js +1 -0
  762. ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-Dlhc8Wr8.js → gitGraphDiagram-WWUBYQGX-DH37EGkG.js} +1 -1
  763. ksadk/server/static/assets/index-CZdkAvsv.js +244 -0
  764. ksadk/server/static/assets/index-CvXdHgF3.css +2 -0
  765. ksadk/server/static/assets/info-A6RAGUB7-rw0ZNHZp.js +1 -0
  766. ksadk/server/static/assets/{infoDiagram-RXCK75RN-CO0rRTRA.js → infoDiagram-RXCK75RN-C1kEtv2y.js} +1 -1
  767. ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-CHiaedSS.js → ishikawaDiagram-5VMMS53U-BCNPyMka.js} +1 -1
  768. ksadk/server/static/assets/{journeyDiagram-EYS64GPL-CHcCIB09.js → journeyDiagram-EYS64GPL--cDbKnjP.js} +1 -1
  769. ksadk/server/static/assets/{kanban-definition-3QL26DDD-CJ_ebBZf.js → kanban-definition-3QL26DDD-R1rAVw2f.js} +1 -1
  770. ksadk/server/static/assets/{katex-B4weu383.js → katex-BT-X-V7U.js} +1 -1
  771. ksadk/server/static/assets/{linear-V-MgYHW1.js → linear-DTR631iG.js} +1 -1
  772. ksadk/server/static/assets/{mermaid-parser.core-CBijEX_c.js → mermaid-parser.core-DfFzJhX8.js} +2 -2
  773. ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-Bc-pQvpp.js → mindmap-definition-FBJOCRG2-D4by_rmk.js} +1 -1
  774. ksadk/server/static/assets/packet-AYTQ26CC-Ca4Hvki9.js +1 -0
  775. ksadk/server/static/assets/{pegDiagram-XKGWAZYB-D7genGLl.js → pegDiagram-XKGWAZYB-xWXPEh0c.js} +1 -1
  776. ksadk/server/static/assets/pie-WAS4IAKB-hwiz6fbw.js +1 -0
  777. ksadk/server/static/assets/{pieDiagram-E7YTZNPT-Bxj32zwD.js → pieDiagram-E7YTZNPT-BudBDlCI.js} +1 -1
  778. ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BURv2mGh.js → quadrantDiagram-AXDQQJYC-DXZ0PCJF.js} +1 -1
  779. ksadk/server/static/assets/radar-RG4KPBEZ-BR1oZXLM.js +1 -0
  780. ksadk/server/static/assets/railroad-74A4TZTK-DfgEISfX.js +1 -0
  781. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-CWOC3rdk.js +1 -0
  782. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CiRvHCLY.js +1 -0
  783. ksadk/server/static/assets/railroad-peg-WCYAUIDC-DSDDyJ2f.js +1 -0
  784. ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-CCsh3avW.js → railroadDiagram-O6MQD6OU-BdAYRYB8.js} +1 -1
  785. ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-_FRRBKEd.js → requirementDiagram-EFPCY7ZU-CBBmaL70.js} +1 -1
  786. ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-C-gr2o9M.js → sankeyDiagram-P5KCCOFB-Doc3rdwp.js} +1 -1
  787. ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-D97NmfKg.js → sequenceDiagram-WJ2MYXX4-0J8R8hWW.js} +1 -1
  788. ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-DjrPC5FX.js → sizeCapture-X5ZJPWSS-CfFLlWsK.js} +1 -1
  789. ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DzMNg1hh.js → stateDiagram-HBIQ2CUA-jBwkjtTy.js} +1 -1
  790. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-Cw1fPb5w.js +1 -0
  791. ksadk/server/static/assets/{swimlanes-XN3QIQJK-Cssf_CsU.js → swimlanes-XN3QIQJK-B0WxTQf8.js} +1 -1
  792. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-Cc3JMBAF.js +8 -0
  793. ksadk/server/static/assets/{timeline-definition-24CTP7MA-BWTHzyS1.js → timeline-definition-24CTP7MA-BfCT_75A.js} +1 -1
  794. ksadk/server/static/assets/treeView-Q6P3EWNA-C05SJHxo.js +1 -0
  795. ksadk/server/static/assets/treemap-WGGIJYW6-BqNLAhc3.js +1 -0
  796. ksadk/server/static/assets/{vennDiagram-4TSXK5OY-BSKgJsmR.js → vennDiagram-4TSXK5OY-9gX1VMDC.js} +1 -1
  797. ksadk/server/static/assets/wardley-WFR3VGLG-DokNktqB.js +1 -0
  798. ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-eflHNoaZ.js → wardleyDiagram-VM6X3IG4-sEs_BP6O.js} +1 -1
  799. ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-eYqrQ-v4.js → xychartDiagram-S5SC5T6Z-Ll4jvUN9.js} +1 -1
  800. ksadk/server/static/index.html +2 -2
  801. ksadk/server/terminal_sessions.py +34 -2
  802. ksadk/sessions/__init__.py +8 -1
  803. ksadk/sessions/_checkpoint_resume_audit.py +44 -0
  804. ksadk/sessions/_in_memory_query.py +112 -0
  805. ksadk/sessions/_local_service_sync.py +157 -156
  806. ksadk/sessions/_local_state_sync.py +152 -0
  807. ksadk/sessions/_local_tables.py +2 -0
  808. ksadk/sessions/_postgres_checkpoint_read.py +323 -0
  809. ksadk/sessions/_postgres_checkpoint_stats.py +330 -0
  810. ksadk/sessions/_postgres_history_read.py +678 -0
  811. ksadk/sessions/_postgres_schema.py +44 -2
  812. ksadk/sessions/_postgres_tables.py +2 -0
  813. ksadk/sessions/_postgres_tool_receipts.py +169 -0
  814. ksadk/sessions/_resilient_read.py +630 -0
  815. ksadk/sessions/_tool_receipt_sync.py +119 -0
  816. ksadk/sessions/agent_identity.py +16 -0
  817. ksadk/sessions/base.py +278 -0
  818. ksadk/sessions/db_budget.py +332 -0
  819. ksadk/sessions/errors.py +12 -0
  820. ksadk/sessions/history_budget.py +36 -0
  821. ksadk/sessions/history_identity.py +27 -0
  822. ksadk/sessions/in_memory.py +376 -113
  823. ksadk/sessions/invocation_identity.py +235 -0
  824. ksadk/sessions/local_service.py +345 -0
  825. ksadk/sessions/postgres_service.py +515 -549
  826. ksadk/sessions/resilience.py +21 -14
  827. ksadk/sessions/resilient.py +611 -504
  828. ksadk/skills/__init__.py +26 -0
  829. ksadk/skills/events.py +464 -0
  830. ksadk/skills/loader.py +44 -2
  831. ksadk/skills/manifest_cache.py +263 -0
  832. ksadk/skills/mcp_server/__init__.py +0 -0
  833. ksadk/skills/mcp_server/_registry.py +33 -0
  834. ksadk/skills/mcp_server/_routing.py +26 -0
  835. ksadk/skills/mcp_server/_workspace_files.py +160 -0
  836. ksadk/skills/mcp_server/register.py +415 -0
  837. ksadk/skills/mcp_server/server.py +218 -0
  838. ksadk/skills/models.py +11 -0
  839. ksadk/skills/observability.py +132 -0
  840. ksadk/skills/package_store.py +75 -2
  841. ksadk/skills/runtime/agent.py +38 -5
  842. ksadk/skills/runtime/artifacts.py +87 -0
  843. ksadk/skills/runtime/backends/e2b.py +400 -56
  844. ksadk/skills/runtime/backends/local.py +621 -119
  845. ksadk/skills/runtime/base.py +78 -7
  846. ksadk/skills/runtime/executor.py +266 -1
  847. ksadk/skills/runtime/loader.py +108 -3
  848. ksadk/skills/runtime/request.py +18 -0
  849. ksadk/skills/service_client.py +6 -1
  850. ksadk/skills/tool_defs.py +108 -3
  851. ksadk/studio/a2a_bindings.py +407 -0
  852. ksadk/studio/a2a_orchestration.py +178 -0
  853. ksadk/studio/active_run_output.py +92 -0
  854. ksadk/studio/activity_records.py +542 -0
  855. ksadk/studio/agent_configuration.py +173 -0
  856. ksadk/studio/api.py +784 -419
  857. ksadk/studio/api_a2a_routes.py +93 -0
  858. ksadk/studio/api_catalog_routes.py +13 -0
  859. ksadk/studio/api_cloud_chat_routes.py +382 -0
  860. ksadk/studio/api_contracts.py +31 -2
  861. ksadk/studio/api_plugin_routes.py +39 -12
  862. ksadk/studio/api_resource_connections.py +79 -41
  863. ksadk/studio/attachment_store.py +14 -2
  864. ksadk/studio/authoring.py +12 -6
  865. ksadk/studio/authoring_coordinator.py +4 -2
  866. ksadk/studio/build_resolution.py +117 -0
  867. ksadk/studio/build_singleflight.py +37 -0
  868. ksadk/studio/builder.py +32 -2
  869. ksadk/studio/capabilities.py +31 -9
  870. ksadk/studio/channel_connections.py +333 -0
  871. ksadk/studio/cloud.py +113 -387
  872. ksadk/studio/cloud_gateways.py +424 -0
  873. ksadk/studio/cloud_shared_web.py +82 -2
  874. ksadk/studio/codex_agent_service.py +19 -2
  875. ksadk/studio/codex_builder.py +1 -1
  876. ksadk/studio/codex_manifest.py +5 -1
  877. ksadk/studio/codex_provider_build.py +65 -0
  878. ksadk/studio/codex_run.py +6 -1
  879. ksadk/studio/compiler.py +112 -6
  880. ksadk/studio/configuration.py +182 -0
  881. ksadk/studio/contract_base.py +19 -0
  882. ksadk/studio/contracts.py +260 -202
  883. ksadk/studio/dsh_application.py +183 -27
  884. ksadk/studio/dsh_capability_service.py +302 -189
  885. ksadk/studio/dsh_lifecycle.py +58 -0
  886. ksadk/studio/dsh_mcp_transport.py +191 -0
  887. ksadk/studio/dsh_provider_registration.py +55 -16
  888. ksadk/studio/entry.py +53 -0
  889. ksadk/studio/event_store.py +53 -5
  890. ksadk/studio/execution_host.py +598 -0
  891. ksadk/studio/framework_run.py +50 -10
  892. ksadk/studio/hosted_kernel.py +1 -1
  893. ksadk/studio/interaction_submission.py +265 -0
  894. ksadk/studio/kernel_registry.py +429 -0
  895. ksadk/studio/kernel_sqlite_migration.py +181 -0
  896. ksadk/studio/lifecycle_status.py +43 -0
  897. ksadk/studio/model_buffered_wire.py +227 -0
  898. ksadk/studio/model_chat_tools.py +143 -0
  899. ksadk/studio/model_client.py +1939 -345
  900. ksadk/studio/model_content_stream.py +129 -0
  901. ksadk/studio/model_credentials.py +196 -0
  902. ksadk/studio/model_profile_service.py +89 -40
  903. ksadk/studio/model_request_projection.py +284 -0
  904. ksadk/studio/model_responses_content.py +296 -0
  905. ksadk/studio/model_usage.py +109 -0
  906. ksadk/studio/model_wire_failure.py +110 -0
  907. ksadk/studio/model_wire_identity.py +138 -0
  908. ksadk/studio/model_wire_snapshot.py +45 -0
  909. ksadk/studio/model_wire_tools.py +283 -0
  910. ksadk/studio/native_acceptance.py +68 -0
  911. ksadk/studio/native_approval.py +111 -0
  912. ksadk/studio/native_browser_port.py +861 -0
  913. ksadk/studio/native_child_mcp.py +87 -0
  914. ksadk/studio/native_child_publication.py +68 -0
  915. ksadk/studio/native_child_skills.py +72 -0
  916. ksadk/studio/native_child_workspace.py +176 -0
  917. ksadk/studio/native_command_policy.py +53 -0
  918. ksadk/studio/native_context_port.py +326 -0
  919. ksadk/studio/native_context_registration.py +54 -0
  920. ksadk/studio/native_document_input.py +88 -0
  921. ksadk/studio/native_documents.py +172 -0
  922. ksadk/studio/native_final_shape.py +852 -0
  923. ksadk/studio/native_gateway_stream.py +69 -0
  924. ksadk/studio/native_hook_port.py +145 -0
  925. ksadk/studio/native_hook_registration.py +70 -0
  926. ksadk/studio/native_inventory_disclosure.py +223 -0
  927. ksadk/studio/native_kernel_admission.py +84 -0
  928. ksadk/studio/native_language_diagnostics.py +320 -0
  929. ksadk/studio/native_mcp_disclosure.py +198 -0
  930. ksadk/studio/native_mcp_tool_port.py +219 -0
  931. ksadk/studio/native_memory_acceptance.py +362 -0
  932. ksadk/studio/native_memory_context.py +266 -0
  933. ksadk/studio/native_memory_flush.py +174 -0
  934. ksadk/studio/native_memory_sources.py +80 -0
  935. ksadk/studio/native_memory_tool_port.py +360 -0
  936. ksadk/studio/native_memory_turn.py +183 -0
  937. ksadk/studio/native_model_port.py +765 -0
  938. ksadk/studio/native_model_recovery.py +126 -0
  939. ksadk/studio/native_options.py +75 -0
  940. ksadk/studio/native_output_fields.py +58 -0
  941. ksadk/studio/native_planning.py +156 -0
  942. ksadk/studio/native_product.py +988 -0
  943. ksadk/studio/native_read_budget.py +426 -0
  944. ksadk/studio/native_recovery.py +171 -0
  945. ksadk/studio/native_scope.py +35 -0
  946. ksadk/studio/native_session_context_port.py +141 -0
  947. ksadk/studio/native_session_engine.py +14 -0
  948. ksadk/studio/native_skill_source.py +74 -0
  949. ksadk/studio/native_subagent_port.py +700 -0
  950. ksadk/studio/native_terminal_recovery.py +95 -0
  951. ksadk/studio/native_test_action.py +610 -0
  952. ksadk/studio/native_tool_port.py +477 -0
  953. ksadk/studio/native_user_input.py +60 -0
  954. ksadk/studio/native_verification_presentation.py +65 -0
  955. ksadk/studio/native_workspace_diagnostics.py +183 -0
  956. ksadk/studio/native_workspace_port.py +43 -0
  957. ksadk/studio/operations.py +34 -9
  958. ksadk/studio/plugin_composition.py +39 -12
  959. ksadk/studio/plugin_kernel_adapter.py +89 -0
  960. ksadk/studio/plugin_lifecycle.py +80 -0
  961. ksadk/studio/plugin_native_activation.py +163 -0
  962. ksadk/studio/plugin_runtime.py +832 -99
  963. ksadk/studio/plugin_runtime_admission.py +57 -0
  964. ksadk/studio/presentation.py +92 -0
  965. ksadk/studio/provider_recovery.py +59 -0
  966. ksadk/studio/public_output_store.py +69 -0
  967. ksadk/studio/recovery_page.py +70 -0
  968. ksadk/studio/resource_authority.py +69 -34
  969. ksadk/studio/resource_catalog.py +14 -12
  970. ksadk/studio/run_activity.py +848 -0
  971. ksadk/studio/run_controls.py +185 -0
  972. ksadk/studio/run_documents.py +127 -0
  973. ksadk/studio/run_service.py +1220 -335
  974. ksadk/studio/run_submission_receipts.py +68 -0
  975. ksadk/studio/runtime_catalog.py +73 -11
  976. ksadk/studio/runtime_prewarm.py +22 -0
  977. ksadk/studio/runtime_source.py +1 -1
  978. ksadk/studio/scheduler_assistant.py +4 -1
  979. ksadk/studio/scheduler_runtime.py +32 -289
  980. ksadk/studio/service.py +615 -246
  981. ksadk/studio/service_settings.py +229 -0
  982. ksadk/studio/shared_web.py +518 -181
  983. ksadk/studio/shared_web_response_events.py +180 -0
  984. ksadk/studio/shared_web_run_state.py +133 -0
  985. ksadk/studio/skill_discovery.py +305 -3
  986. ksadk/studio/static/assets/ChannelsPage-xX5HlMId.js +8 -0
  987. ksadk/studio/static/assets/CodeBlock-DMIf4h33-CkufcPYo.js +44 -0
  988. ksadk/studio/static/assets/ExecutionTree-v3HexvPo-Bk8wzGSy.js +1 -0
  989. ksadk/studio/static/assets/MathMessageMarkdown-BesSeCYl-DYG1Wzbx.js +8 -0
  990. ksadk/studio/static/assets/MermaidBlock-H_0qzQDf-BdXtxug_.js +11 -0
  991. ksadk/studio/static/assets/MessageMarkdown-CwMTvgFj-BeyLyQHu.js +59 -0
  992. ksadk/studio/static/assets/PrismRenderer-5pK6tUdb.js +3 -0
  993. ksadk/studio/static/assets/TeamsPage-CCaaGaVp.js +3 -0
  994. ksadk/studio/static/assets/abnfDiagram-VCTEODGH-Bnb1O3tA.js +1 -0
  995. ksadk/studio/static/assets/architecture-7GRP2DOG-BMOPAkUC.js +1 -0
  996. ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-5Zn1gqJv.js +36 -0
  997. ksadk/studio/static/assets/{blockDiagram-I7D4REHJ-CFmPl1de.js → blockDiagram-I7D4REHJ-CLB4L11u.js} +6 -6
  998. ksadk/studio/static/assets/{c4Diagram-7LVT6UL2-BRrGu8qt.js → c4Diagram-7LVT6UL2-BOma_Z_F.js} +7 -7
  999. ksadk/studio/static/assets/channel-r8itPEik.js +1 -0
  1000. ksadk/studio/static/assets/chunk-2E4U76K2-BFf_huql.js +2 -0
  1001. ksadk/studio/static/assets/chunk-2ZTRR5NV-C2qZJila.js +2 -0
  1002. ksadk/studio/static/assets/chunk-3Z5EZCMW-DMniOlPd.js +1 -0
  1003. ksadk/studio/static/assets/{chunk-4HAMMTFA-D220vUtq.js → chunk-4HAMMTFA-DVtDP4rm.js} +7 -7
  1004. ksadk/studio/static/assets/chunk-5V3GS4D5-i-ZDOEnP.js +1 -0
  1005. ksadk/studio/static/assets/chunk-6AZGARVD-BB6FqMl-.js +1 -0
  1006. ksadk/studio/static/assets/chunk-6EIED4P4-DdUJkP0f.js +1 -0
  1007. ksadk/studio/static/assets/chunk-6K3QC6MW-CqVsVn-y.js +1 -0
  1008. ksadk/studio/static/assets/chunk-6TQVIW2G-DKlLbNIP.js +1 -0
  1009. ksadk/studio/static/assets/chunk-747NJXEK-BFkZPlab.js +2 -0
  1010. ksadk/studio/static/assets/chunk-75Z2AOVW-BZNhM4vB.js +2 -0
  1011. ksadk/studio/static/assets/chunk-CLGD4ZFX-D-GTtyKs.js +1 -0
  1012. ksadk/studio/static/assets/{chunk-DU6HZSFF-C4DtVpCu.js → chunk-DU6HZSFF-CpEZCUgg.js} +1 -1
  1013. ksadk/studio/static/assets/chunk-F27PBJKO-D7pvKf0C.js +1 -0
  1014. ksadk/studio/static/assets/{chunk-GMAD6QVW-DiQA-u2N.js → chunk-GMAD6QVW-DE_8H717.js} +6 -6
  1015. ksadk/studio/static/assets/chunk-GVQU2GXP-BfEB0lNv.js +1 -0
  1016. ksadk/studio/static/assets/chunk-I5DQTOEV-Hm2LfiYI.js +1 -0
  1017. ksadk/studio/static/assets/chunk-ICYGCRZG-749o-pGJ.js +1 -0
  1018. ksadk/studio/static/assets/chunk-IH6LHLGP-CC6I9F3i.js +1 -0
  1019. ksadk/studio/static/assets/{chunk-IMKFNOWR-BSlbjT_b.js → chunk-IMKFNOWR-D38axYOR.js} +5 -5
  1020. ksadk/studio/static/assets/chunk-KI3K4JFJ-Rma-0k_M.js +1 -0
  1021. ksadk/studio/static/assets/chunk-L3NEJ4N5-Dq5xn7aL.js +1 -0
  1022. ksadk/studio/static/assets/chunk-LNGE3PJU-DWpGwHrW.js +34 -0
  1023. ksadk/studio/static/assets/{chunk-OSK3NFVY-M2m6G3j4.js → chunk-OSK3NFVY-DiCQjx9b.js} +5 -5
  1024. ksadk/studio/static/assets/chunk-OUJLGHUK-D0j2JRSg.js +2 -0
  1025. ksadk/studio/static/assets/{chunk-P2QGCYS3-DezaQxiK.js → chunk-P2QGCYS3-VdhKmfYq.js} +1 -1
  1026. ksadk/studio/static/assets/chunk-POPQ4Y6H-C1_rzO5M.js +1 -0
  1027. ksadk/studio/static/assets/chunk-PWAF6VOD-B45HvflI.js +1 -0
  1028. ksadk/studio/static/assets/{chunk-SHT3W25Y-DAyIVp5H.js → chunk-SHT3W25Y-DsCFnRVE.js} +9 -9
  1029. ksadk/studio/static/assets/{chunk-SVP7TREG-DulomOrS.js → chunk-SVP7TREG-DljApgRX.js} +1 -1
  1030. ksadk/studio/static/assets/{chunk-TICWLB2K-bHJYoflj.js → chunk-TICWLB2K-DWqRtyqa.js} +6 -6
  1031. ksadk/studio/static/assets/chunk-TLUHSLCS-DUeGgnD5.js +2 -0
  1032. ksadk/studio/static/assets/chunk-UY3FDG6J-CnmaAkG5.js +1 -0
  1033. ksadk/studio/static/assets/chunk-XHIXRSVI-BFQ6B8BE.js +1 -0
  1034. ksadk/studio/static/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
  1035. ksadk/studio/static/assets/classDiagram-ZZMXUADV-DjeMNJmu.js +1 -0
  1036. ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-DjeMNJmu.js +1 -0
  1037. ksadk/studio/static/assets/clsx-DB0hHKMi.js +1 -0
  1038. ksadk/studio/static/assets/cose-bilkent-JH36ORCC-om8AY0Mc.js +1 -0
  1039. ksadk/studio/static/assets/createLucideIcon-DR0LGTP5.js +1 -0
  1040. ksadk/studio/static/assets/cynefin-OW5HDTMX-7tP7QfM3.js +1 -0
  1041. ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-6BrmK52S.js +62 -0
  1042. ksadk/studio/static/assets/dagre-GXQ25YYZ-buZBRlwz.js +4 -0
  1043. ksadk/studio/static/assets/{diagram-S7CK7UJ4-0BZ98mya.js → diagram-S7CK7UJ4-C6aH78c4.js} +3 -3
  1044. ksadk/studio/static/assets/diagram-UQ7AKVKN-DY_tQ6C_.js +41 -0
  1045. ksadk/studio/static/assets/diagram-VSXAHHWV-DX9p5cQT.js +3 -0
  1046. ksadk/studio/static/assets/diagram-VX7I27RA-DB2WSH8X.js +24 -0
  1047. ksadk/studio/static/assets/diagram-Z3DM3KII-Ce1BJwHE.js +24 -0
  1048. ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-Daukxwqf.js +1 -0
  1049. ksadk/studio/static/assets/{erDiagram-RLTQ6QDP-DLGry2Ap.js → erDiagram-RLTQ6QDP-CGthTMmJ.js} +6 -6
  1050. ksadk/studio/static/assets/eventmodeling-NTZA5JFV-BR2FzK4R.js +1 -0
  1051. ksadk/studio/static/assets/flowDiagram-HODETNUW-CZ1tEenp.js +1 -0
  1052. ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BfoAZW9X.js +292 -0
  1053. ksadk/studio/static/assets/gitGraph-4MIJSDKK-DYlXv_yJ.js +1 -0
  1054. ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-S985OIJF.js +106 -0
  1055. ksadk/studio/static/assets/iconfont-4HN3C9Pg.woff2 +0 -0
  1056. ksadk/studio/static/assets/index-C7DNDik2.css +2 -0
  1057. ksadk/studio/static/assets/index-Cy68gTlH.js +265 -0
  1058. ksadk/studio/static/assets/index.dom-BQenBtFm.js +1 -0
  1059. ksadk/studio/static/assets/info-A6RAGUB7-BlU-8_xu.js +1 -0
  1060. ksadk/studio/static/assets/infoDiagram-27XIBGKW-CO3W8tFn.js +2 -0
  1061. ksadk/studio/static/assets/{ishikawaDiagram-5VMMS53U-CljI2wyf.js → ishikawaDiagram-5VMMS53U-BUGEl8cO.js} +2 -2
  1062. ksadk/studio/static/assets/{journeyDiagram-3NMN7TZE-D50Fo2sL.js → journeyDiagram-3NMN7TZE-Ch2ZYxvr.js} +6 -6
  1063. ksadk/studio/static/assets/{kanban-definition-UXKFOSKX-DimfOTam.js → kanban-definition-UXKFOSKX-Da-Fp4vC.js} +3 -3
  1064. ksadk/studio/static/assets/kingsoft-cloud-BOcLiQ3e.ico +0 -0
  1065. ksadk/studio/static/assets/{line-CX5BOOUP.js → line-BDaeq300.js} +1 -1
  1066. ksadk/studio/static/assets/linear-DmucvK78.js +1 -0
  1067. ksadk/studio/static/assets/mermaid-parser.core-Cj5mYdBi.js +4 -0
  1068. ksadk/studio/static/assets/{mindmap-definition-YA3MSWOX-Bizzqy5R.js → mindmap-definition-YA3MSWOX-CvlsVQ38.js} +1 -1
  1069. ksadk/studio/static/assets/packet-AYTQ26CC-BSJEH9Lb.js +1 -0
  1070. ksadk/studio/static/assets/pegDiagram-XKGWAZYB-BuHuNgT5.js +1 -0
  1071. ksadk/studio/static/assets/pie-WAS4IAKB-qxv01Y8W.js +1 -0
  1072. ksadk/studio/static/assets/{pieDiagram-E7YTZNPT-B9mrFSDF.js → pieDiagram-E7YTZNPT-DlrWiSOv.js} +1 -1
  1073. ksadk/studio/static/assets/preload-helper-CO68xz6u.js +1 -0
  1074. ksadk/studio/static/assets/presentation-yYw7xK60-BmcZriri.js +1 -0
  1075. ksadk/studio/static/assets/{quadrantDiagram-AXDQQJYC-NzUfCbMR.js → quadrantDiagram-AXDQQJYC-BA3hpUZM.js} +6 -6
  1076. ksadk/studio/static/assets/radar-RG4KPBEZ-B6h6pWOs.js +1 -0
  1077. ksadk/studio/static/assets/railroad-74A4TZTK-CGtTWdGc.js +1 -0
  1078. ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-Cq7y22IA.js +1 -0
  1079. ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-V0aQVv9m.js +1 -0
  1080. ksadk/studio/static/assets/railroad-peg-WCYAUIDC-BQiIsX_p.js +1 -0
  1081. ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-BxpAazym.js +1 -0
  1082. ksadk/studio/static/assets/{requirementDiagram-BXWQKSXE-BF3d103U.js → requirementDiagram-BXWQKSXE-E-Ge0FcV.js} +1 -1
  1083. ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-CA6YKhQ3.js +40 -0
  1084. ksadk/studio/static/assets/{sequenceDiagram-WJ2MYXX4-Dh4wZtUm.js → sequenceDiagram-WJ2MYXX4-CcElXGg4.js} +9 -9
  1085. ksadk/studio/static/assets/src-B6xuSHsQ.js +1 -0
  1086. ksadk/studio/static/assets/stateDiagram-D77RDMKH-DdXZOFqY.js +1 -0
  1087. ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-CJbOM-QK.js +1 -0
  1088. ksadk/studio/static/assets/swimlanes-42K2YHIH-NtgEEHLY.js +1 -0
  1089. ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-D-AVlW8B.js +8 -0
  1090. ksadk/studio/static/assets/{timeline-definition-24CTP7MA-DxmSSjRB.js → timeline-definition-24CTP7MA-CIuYhNy3.js} +7 -7
  1091. ksadk/studio/static/assets/treeView-Q6P3EWNA-BTTYcvrF.js +1 -0
  1092. ksadk/studio/static/assets/treemap-WGGIJYW6-Cg7tyfH8.js +1 -0
  1093. ksadk/studio/static/assets/vennDiagram-4TSXK5OY-7HbApbrv.js +34 -0
  1094. ksadk/studio/static/assets/wardley-WFR3VGLG-C6N1rULY.js +1 -0
  1095. ksadk/studio/static/assets/{wardleyDiagram-VM6X3IG4-Ba1AOWoe.js → wardleyDiagram-VM6X3IG4-CMJiaedy.js} +1 -1
  1096. ksadk/studio/static/assets/{xychartDiagram-S5SC5T6Z-Cr5pt5Y-.js → xychartDiagram-S5SC5T6Z-BdgtMvW2.js} +6 -6
  1097. ksadk/studio/static/index.html +8 -2
  1098. ksadk/studio/teams_catalog.py +166 -0
  1099. ksadk/studio/teams_installation.py +217 -0
  1100. ksadk/studio/teams_node.py +549 -0
  1101. ksadk/studio/teams_node_factory.py +558 -0
  1102. ksadk/studio/teams_node_runner.py +72 -0
  1103. ksadk/studio/teams_node_v1.py +935 -0
  1104. ksadk/studio/teams_remote.py +496 -0
  1105. ksadk/studio/templates.py +41 -19
  1106. ksadk/studio/validator.py +13 -0
  1107. ksadk/studio/workspace.py +19 -4
  1108. ksadk/studio/workspace_plugins.py +138 -0
  1109. ksadk/studio/workspace_registry.py +286 -0
  1110. ksadk/tools/a2a_policy.py +5 -0
  1111. ksadk/tools/artifact_observation.py +53 -0
  1112. ksadk/tools/captured_source_artifact.py +68 -0
  1113. ksadk/tools/schema.py +34 -0
  1114. ksadk/toolsets/__init__.py +93 -88
  1115. ksadk/toolsets/a2a.py +89 -89
  1116. ksadk/toolsets/sandbox.py +377 -6
  1117. ksadk/toolsets/skill_management.py +315 -0
  1118. ksadk/toolsets/skills.py +446 -378
  1119. ksadk/toolsets/tool_selection.py +94 -0
  1120. ksadk/toolsets/workspace.py +344 -326
  1121. ksadk/toolsets/workspace_editing.py +291 -0
  1122. ksadk/toolsets/workspace_identity.py +15 -0
  1123. ksadk/toolsets/workspace_model_observation.py +104 -0
  1124. ksadk/toolsets/workspace_mutation.py +75 -0
  1125. ksadk/toolsets/workspace_read_budget.py +216 -0
  1126. ksadk/toolsets/workspace_search.py +192 -0
  1127. ksadk/toolsets/workspace_search_process.py +129 -0
  1128. ksadk/toolsets/workspace_state.py +6 -0
  1129. ksadk/version.py +1 -1
  1130. ksadk_runtime_common/http_headers.py +18 -0
  1131. ksadk_runtime_common/memory_backend/providers/lancedb.py +56 -2
  1132. ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -148
  1133. ksadk_runtime_common/workspace_files/router.py +28 -14
  1134. agentengine_sdk_python-0.8.4.dist-info/RECORD +0 -1070
  1135. ksadk/server/static/assets/MathMessageMarkdown-DbdW5A4_.js +0 -8
  1136. ksadk/server/static/assets/architecture-7GRP2DOG-BW5UtSnx.js +0 -1
  1137. ksadk/server/static/assets/channel-CF5Up3hQ.js +0 -1
  1138. ksadk/server/static/assets/chunk-2Q5K7J3B-KkXX47WI.js +0 -1
  1139. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Cz81acRQ.js +0 -1
  1140. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Cz81acRQ.js +0 -1
  1141. ksadk/server/static/assets/cynefin-OW5HDTMX-DVWs8a2t.js +0 -1
  1142. ksadk/server/static/assets/dist-B2_CVgKv.js +0 -1
  1143. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CQqR2hg3.js +0 -1
  1144. ksadk/server/static/assets/flowDiagram-A5DVABFB-DDRRbGPm.js +0 -1
  1145. ksadk/server/static/assets/gitGraph-4MIJSDKK-fgWEf7UD.js +0 -1
  1146. ksadk/server/static/assets/index-DcTML1Up.css +0 -2
  1147. ksadk/server/static/assets/index-Dh2XHqPK.js +0 -244
  1148. ksadk/server/static/assets/info-A6RAGUB7-BUecLTdt.js +0 -1
  1149. ksadk/server/static/assets/packet-AYTQ26CC-BeW-btFv.js +0 -1
  1150. ksadk/server/static/assets/pie-WAS4IAKB-X0xh4SaT.js +0 -1
  1151. ksadk/server/static/assets/radar-RG4KPBEZ-DzRMSqjd.js +0 -1
  1152. ksadk/server/static/assets/railroad-74A4TZTK-Cc44icX-.js +0 -1
  1153. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-D6S4BdV-.js +0 -1
  1154. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-vD3aVE2o.js +0 -1
  1155. ksadk/server/static/assets/railroad-peg-WCYAUIDC-CYbW_b4T.js +0 -1
  1156. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BIcjKFhF.js +0 -1
  1157. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-tq3K3Ybl.js +0 -8
  1158. ksadk/server/static/assets/treeView-Q6P3EWNA-DBXt3GdU.js +0 -1
  1159. ksadk/server/static/assets/treemap-WGGIJYW6-DrCy4E_l.js +0 -1
  1160. ksadk/server/static/assets/wardley-WFR3VGLG-CefhOliM.js +0 -1
  1161. ksadk/studio/static/assets/CodeBlock-DCUUY0PL-BLjA7kbS.js +0 -41
  1162. ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--CUYIXwgI.js +0 -8
  1163. ksadk/studio/static/assets/MermaidBlock-DBFTztfQ-BTYKWEbD.js +0 -44
  1164. ksadk/studio/static/assets/PrismRenderer-wiXRHURw.js +0 -3
  1165. ksadk/studio/static/assets/abnfDiagram-VCTEODGH-CLETGjKq.js +0 -1
  1166. ksadk/studio/static/assets/architecture-7GRP2DOG-hXMiO_AU.js +0 -1
  1167. ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-B5EQysk6.js +0 -36
  1168. ksadk/studio/static/assets/channel-BqO-gtEw.js +0 -1
  1169. ksadk/studio/static/assets/chunk-75Z2AOVW-C8lLv_qc.js +0 -2
  1170. ksadk/studio/static/assets/chunk-F27PBJKO-Cpw14SNG.js +0 -1
  1171. ksadk/studio/static/assets/chunk-GVQU2GXP-DUdNBUCi.js +0 -1
  1172. ksadk/studio/static/assets/chunk-L3NEJ4N5-Db8Ab22W.js +0 -1
  1173. ksadk/studio/static/assets/chunk-POPQ4Y6H-DOpufJ3t.js +0 -1
  1174. ksadk/studio/static/assets/chunk-PWAF6VOD-CfLb1gSc.js +0 -1
  1175. ksadk/studio/static/assets/chunk-XXDRQBXY-7PcdR2B1.js +0 -1
  1176. ksadk/studio/static/assets/classDiagram-ZZMXUADV-D_bW8JjA.js +0 -1
  1177. ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-D_bW8JjA.js +0 -1
  1178. ksadk/studio/static/assets/cose-bilkent-JH36ORCC-snL88wkX.js +0 -1
  1179. ksadk/studio/static/assets/cynefin-OW5HDTMX-Bd1YhRo1.js +0 -1
  1180. ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-BBrIG_mI.js +0 -62
  1181. ksadk/studio/static/assets/dagre-GXQ25YYZ-DtF1nGdY.js +0 -4
  1182. ksadk/studio/static/assets/diagram-UQ7AKVKN-6mLKEo6L.js +0 -41
  1183. ksadk/studio/static/assets/diagram-VSXAHHWV-CJBIaiZ2.js +0 -3
  1184. ksadk/studio/static/assets/diagram-VX7I27RA-BohLEE1C.js +0 -24
  1185. ksadk/studio/static/assets/diagram-Z3DM3KII-D8KIGonG.js +0 -24
  1186. ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-C99HU61t.js +0 -1
  1187. ksadk/studio/static/assets/eventmodeling-NTZA5JFV-B1_5Ac8j.js +0 -1
  1188. ksadk/studio/static/assets/flowDiagram-HODETNUW-DD5hmicn.js +0 -1
  1189. ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BPJrVLSG.js +0 -292
  1190. ksadk/studio/static/assets/gitGraph-4MIJSDKK-Cy_ZQBGi.js +0 -1
  1191. ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-CZCUz4MT.js +0 -106
  1192. ksadk/studio/static/assets/index-CeHL9ARV.css +0 -2
  1193. ksadk/studio/static/assets/index-D9QEcVpL.js +0 -320
  1194. ksadk/studio/static/assets/info-A6RAGUB7-DBzDQB_I.js +0 -1
  1195. ksadk/studio/static/assets/infoDiagram-27XIBGKW-B-sq07Hi.js +0 -2
  1196. ksadk/studio/static/assets/linear-BCxN9JLv.js +0 -1
  1197. ksadk/studio/static/assets/mermaid-parser.core-BdCz8fkS.js +0 -7
  1198. ksadk/studio/static/assets/packet-AYTQ26CC-gPln-_kG.js +0 -1
  1199. ksadk/studio/static/assets/pegDiagram-XKGWAZYB-Dy3fB-gW.js +0 -1
  1200. ksadk/studio/static/assets/pie-WAS4IAKB-qZdHMr7B.js +0 -1
  1201. ksadk/studio/static/assets/radar-RG4KPBEZ-BZlnaea9.js +0 -1
  1202. ksadk/studio/static/assets/railroad-74A4TZTK-Ce3__H9c.js +0 -1
  1203. ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-DqQn7EzD.js +0 -1
  1204. ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-B55kCA8F.js +0 -1
  1205. ksadk/studio/static/assets/railroad-peg-WCYAUIDC-DfQnvkHd.js +0 -1
  1206. ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-DnhMjZCk.js +0 -1
  1207. ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-Bo-JTwt0.js +0 -40
  1208. ksadk/studio/static/assets/src-SFn9St9H.js +0 -1
  1209. ksadk/studio/static/assets/stateDiagram-D77RDMKH-DH774G6d.js +0 -1
  1210. ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-DZvVWPBo.js +0 -1
  1211. ksadk/studio/static/assets/swimlanes-42K2YHIH-Bl13OvTX.js +0 -1
  1212. ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-BnFOTALJ.js +0 -8
  1213. ksadk/studio/static/assets/treeView-Q6P3EWNA-D5Nn9Swz.js +0 -1
  1214. ksadk/studio/static/assets/treemap-WGGIJYW6-CgHgnQhE.js +0 -1
  1215. ksadk/studio/static/assets/vennDiagram-4TSXK5OY-B9QEXcJq.js +0 -34
  1216. ksadk/studio/static/assets/wardley-WFR3VGLG-DXrK2RRY.js +0 -1
  1217. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/WHEEL +0 -0
  1218. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/entry_points.txt +0 -0
  1219. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/licenses/LICENSE +0 -0
  1220. {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/top_level.txt +0 -0
  1221. /ksadk/studio/static/assets/{MathMessageMarkdown-CBcDrAw--BEOyPC3w.css → MathMessageMarkdown-BesSeCYl-BEOyPC3w.css} +0 -0
@@ -2,8 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ import weakref
5
6
  from collections.abc import AsyncIterator
6
- from contextvars import ContextVar
7
+ from contextvars import Context, ContextVar
7
8
  from typing import Any, Optional, cast
8
9
 
9
10
  from ksadk.sessions.base import (
@@ -13,22 +14,27 @@ from ksadk.sessions.base import (
13
14
  SessionEvent,
14
15
  SessionEventQuery,
15
16
  SessionState,
17
+ ToolReceiptClaim,
18
+ )
19
+ from ksadk.sessions.db_budget import DatabaseOperationDeadline, session_db_budget
20
+ from ksadk.sessions.errors import (
21
+ CheckpointScanRestartRequired,
22
+ HistoryReloadRequired,
23
+ SessionBackendUnavailable,
16
24
  )
17
- from ksadk.sessions.errors import CheckpointScanRestartRequired
18
25
  from ksadk.sessions.in_memory import InMemorySessionService
19
- from ksadk.sessions.resilience import is_session_backend_failure
26
+ from ksadk.sessions.resilience import is_session_backend_failure, is_session_connection_failure
27
+
28
+ from ksadk.sessions._resilient_read import _ResilientReadMixin
20
29
 
21
30
  logger = logging.getLogger(__name__)
22
31
 
23
32
 
24
- class ResilientSessionService(BaseSessionService):
33
+ class ResilientSessionService(_ResilientReadMixin, BaseSessionService):
25
34
  """Keep live agent sessions available when durable persistence is unavailable.
26
35
 
27
- The in-memory service is authoritative for the lifetime of this process. The
28
- configured durable service is used as a read-through source and a best-effort
29
- write-through sink. After its first failure it stays disabled until a
30
- background probe confirms the durable backend is reachable again, at which
31
- point it is re-enabled and an INFO log is emitted.
36
+ Clean sessions read and write through the durable backend. A session with
37
+ confirmed process-local writes stays live-authoritative after recovery.
32
38
  """
33
39
 
34
40
  _probe_interval_seconds: float = 30.0
@@ -41,10 +47,16 @@ class ResilientSessionService(BaseSessionService):
41
47
  self.primary = primary
42
48
  self.fallback = fallback or InMemorySessionService()
43
49
  self._primary_enabled = True
44
- self._hydrate_lock = asyncio.Lock()
50
+ self._hydrate_locks: weakref.WeakValueDictionary[str, asyncio.Lock] = weakref.WeakValueDictionary()
45
51
  self._primary_session_lock = asyncio.Lock()
46
52
  self._primary_session_ids: set[str] = set()
53
+ self._hydrated_session_ids: set[str] = set()
54
+ self._live_generation: dict[str, int] = {}
47
55
  self._dirty_session_ids: set[str] = set()
56
+ self._unsafe_live_session_ids: set[str] = set()
57
+ self._dirty_state_keys: set[tuple[str, str, str, str]] = set()
58
+ self._hydrated_state_keys: set[tuple[str, str, str, str]] = set()
59
+ self._deleted_session_ids: set[str] = set()
48
60
  self._checkpoint_partition: ContextVar[
49
61
  tuple[frozenset[str], frozenset[str]] | None
50
62
  ] = ContextVar(f"checkpoint_partition_{id(self)}", default=None)
@@ -54,20 +66,63 @@ class ResilientSessionService(BaseSessionService):
54
66
  def canonical_event_service(self) -> BaseSessionService:
55
67
  """Return the single durable authority for canonical runtime facts.
56
68
 
57
- Legacy session state remains live-first. Canonical event sequences cannot
69
+ Canonical event sequences cannot
58
70
  fail over to the independently sequenced fallback without splitting one
59
71
  logical log, so RuntimeEventStore binds directly to the primary service.
60
72
  """
61
73
 
62
74
  return self.primary
63
75
 
76
+ async def ensure_canonical_session_header(
77
+ self, agent_id: str, user_id: str, session_id: str
78
+ ) -> Session:
79
+ """Preserve deletion and recovery fencing without hydrating a transcript."""
80
+ if session_id in self._deleted_session_ids:
81
+ raise SessionBackendUnavailable("Session deletion is pending durable recovery")
82
+ if not self._primary_enabled:
83
+ self._clean_read_unavailable()
84
+ ensure = getattr(self.primary, "ensure_session_header", None)
85
+ if callable(ensure):
86
+ header = await ensure(agent_id, user_id, session_id)
87
+ else:
88
+ header = await self.primary.create_session(agent_id, user_id, session_id)
89
+ if session_id in self._deleted_session_ids:
90
+ raise SessionBackendUnavailable("Session deletion is pending durable recovery")
91
+ return header
92
+
64
93
  @property
65
94
  def degraded(self) -> bool:
66
95
  return not self._primary_enabled
67
96
 
68
- # This service is intentionally live-first and writes two independently
69
- # sequenced stores, so the wrapper itself does not claim canonical storage
70
- # capabilities.
97
+ def persistence_degraded(self, session_id: str | None = None) -> bool:
98
+ """Whether a read may contain process-local facts absent from durable storage."""
99
+ return not self._primary_enabled or (
100
+ bool(self._dirty_session_ids or self._dirty_state_keys or self._deleted_session_ids)
101
+ if session_id is None
102
+ else session_id in self._dirty_session_ids
103
+ )
104
+
105
+ def _mark_dirty(self, session_id: str | None) -> None:
106
+ if session_id:
107
+ if session_id in self._unsafe_live_session_ids:
108
+ self._clean_read_unavailable()
109
+ self._dirty_session_ids.add(session_id)
110
+
111
+ def _has_complete_live_session(self, session_id: str) -> bool:
112
+ return (
113
+ session_id in self._hydrated_session_ids
114
+ and session_id not in self._unsafe_live_session_ids
115
+ and session_id not in self._deleted_session_ids
116
+ )
117
+
118
+ @staticmethod
119
+ def _clean_read_unavailable() -> None:
120
+ raise SessionBackendUnavailable(
121
+ "Durable session history is unavailable; clean history cannot be inferred from a partial live cache"
122
+ )
123
+
124
+ # The fallback can acquire an independent sequence when persistence fails,
125
+ # so the wrapper itself does not claim canonical storage capabilities.
71
126
 
72
127
  async def _call_primary(self, method_name: str, *args: Any, **kwargs: Any) -> tuple[bool, Any]:
73
128
  if not self._primary_enabled:
@@ -76,8 +131,24 @@ class ResilientSessionService(BaseSessionService):
76
131
  method = getattr(self.primary, method_name)
77
132
  return True, await method(*args, **kwargs)
78
133
  except Exception as exc:
134
+ readonly = (getattr(self.primary, "_retry_safe_reads", False)
135
+ and (method_name == "has_events" or
136
+ method_name.startswith(("get_", "list_", "query_", "count_"))))
137
+ if readonly and isinstance(exc, TimeoutError):
138
+ raise # Request/SQL capacity timeout does not change the source.
139
+ if readonly and is_session_connection_failure(exc):
140
+ # A stale idle socket must not disable every clean session for
141
+ # the entire probe interval. Only pure reads may reacquire once;
142
+ # the caller's original deadline still owns the retry.
143
+ try:
144
+ async with asyncio.timeout(2.0):
145
+ return True, await method(*args, **kwargs)
146
+ except TimeoutError:
147
+ raise
148
+ except Exception as retry_error:
149
+ exc = retry_error
79
150
  if not is_session_backend_failure(exc):
80
- raise
151
+ raise exc
81
152
  self._disable_primary(exc)
82
153
  return False, None
83
154
 
@@ -98,7 +169,9 @@ class ResilientSessionService(BaseSessionService):
98
169
  def _start_probe(self) -> None:
99
170
  if self._probe_task is not None and not self._probe_task.done():
100
171
  return
101
- self._probe_task = asyncio.create_task(self._probe_loop())
172
+ # The recovery task outlives a request. Do not inherit request byte
173
+ # budgets, checkpoint snapshot connections or identity ContextVars.
174
+ self._probe_task = Context().run(asyncio.create_task, self._probe_loop())
102
175
 
103
176
  async def _probe_loop(self) -> None:
104
177
  while not self._primary_enabled:
@@ -106,7 +179,10 @@ class ResilientSessionService(BaseSessionService):
106
179
  if self._primary_enabled:
107
180
  break
108
181
  try:
109
- await self.primary.get_session("__ksadk_probe__")
182
+ probe = (self.primary.get_session_header if getattr(self.primary, "_retry_safe_reads", False)
183
+ else self.primary.get_session)
184
+ async with asyncio.timeout(2.0):
185
+ await probe("__ksadk_probe__")
110
186
  except Exception:
111
187
  continue
112
188
  self._primary_enabled = True
@@ -118,21 +194,56 @@ class ResilientSessionService(BaseSessionService):
118
194
  },
119
195
  )
120
196
 
121
- async def _hydrate(self, session: Session) -> Session:
122
- async with self._hydrate_lock:
197
+ async def _hydrate(self, session: Session, expected_generation: int) -> Optional[Session]:
198
+ lock = self._hydrate_locks.setdefault(session.id, asyncio.Lock())
199
+ async with lock:
200
+ if session.id in self._deleted_session_ids:
201
+ return None
202
+ if session.id in self._dirty_session_ids:
203
+ live = await self.fallback.get_session(session.id)
204
+ if live is not None:
205
+ return live
206
+ if self._live_generation.get(session.id, 0) != expected_generation:
207
+ # A local mutation completed after the durable snapshot was
208
+ # fetched but before this lock. Refresh under the same lock so
209
+ # an older state snapshot cannot undo that mutation.
210
+ ok, fresh = await self._call_primary("get_session", session.id)
211
+ if not ok:
212
+ if session.id in self._hydrated_session_ids:
213
+ return await self.fallback.get_session(session.id)
214
+ self._clean_read_unavailable()
215
+ if fresh is None:
216
+ return None
217
+ session = cast(Session, fresh)
123
218
  self._primary_session_ids.add(session.id)
124
- existing = await self.fallback.get_session(session.id)
219
+ existing = await self.fallback.get_session_header(session.id)
220
+ if existing is not None and (
221
+ existing.agent_id != session.agent_id
222
+ or existing.user_id != session.user_id
223
+ or existing.created_at != session.created_at
224
+ ):
225
+ # A different process deleted and recreated this ID. Never
226
+ # graft its facts onto the old owner's recovery mirror.
227
+ self._hydrated_session_ids.discard(session.id)
228
+ self._unsafe_live_session_ids.add(session.id)
229
+ await self.fallback.delete_session(session.id)
230
+ existing = None
125
231
  if existing is None:
126
232
  await self.fallback.create_session(
127
233
  session.agent_id,
128
234
  session.user_id,
129
235
  session_id=session.id,
130
236
  )
131
- existing_events = await self.fallback.get_events(session.id)
132
- existing_ids = {event.id for event in existing_events}
133
- for event in sorted(session.events, key=lambda item: item.seq_id):
237
+ await self.fallback.align_session_created_at(session.id, session.created_at)
238
+ existing_ids = await self.fallback.get_session_event_ids(session.id)
239
+ for index, event in enumerate(sorted(session.events, key=lambda item: item.seq_id)):
134
240
  if event.id not in existing_ids:
135
241
  await self.fallback.append_event(session.id, event)
242
+ if (index + 1) % 128 == 0:
243
+ # Uncontended in-memory awaits do not yield. Let history
244
+ # reads and driver cancellation run during a cold replay;
245
+ # the per-session hydrate lock still protects the merge.
246
+ await asyncio.sleep(0)
136
247
  await self.fallback.update_session_metadata(
137
248
  session.id,
138
249
  title=session.title,
@@ -141,7 +252,7 @@ class ResilientSessionService(BaseSessionService):
141
252
  first_prompt=session.first_prompt,
142
253
  last_prompt=session.last_prompt,
143
254
  )
144
- current = await self.fallback.get_session(session.id)
255
+ current = await self.fallback.get_session_metadata(session.id)
145
256
  if current is not None and session.state != current.state:
146
257
  await self.fallback.update_state(
147
258
  agent_id=session.agent_id,
@@ -153,6 +264,11 @@ class ResilientSessionService(BaseSessionService):
153
264
  hydrated = await self.fallback.get_session(session.id)
154
265
  if hydrated is None:
155
266
  raise RuntimeError(f"Failed to hydrate live session {session.id}")
267
+ # The clean durable row defines the incarnation. The fallback
268
+ # mirror may have been created a moment earlier with another clock.
269
+ hydrated.created_at = session.created_at
270
+ self._unsafe_live_session_ids.discard(session.id)
271
+ self._hydrated_session_ids.add(session.id)
156
272
  return hydrated
157
273
 
158
274
  async def create_session(
@@ -161,12 +277,23 @@ class ResilientSessionService(BaseSessionService):
161
277
  user_id: str,
162
278
  session_id: Optional[str] = None,
163
279
  ) -> Session:
280
+ if session_id in self._deleted_session_ids:
281
+ raise SessionBackendUnavailable("Session deletion is pending durable recovery")
164
282
  if session_id:
283
+ if session_id in self._dirty_session_ids:
284
+ existing = await self.fallback.get_session(session_id)
285
+ if existing is not None:
286
+ return existing
287
+ generation = self._live_generation.get(session_id, 0)
165
288
  ok, durable = await self._call_primary("get_session", session_id)
166
289
  if ok and durable is not None:
167
- return await self._hydrate(durable)
290
+ hydrated = await self._hydrate(durable, generation)
291
+ if hydrated is not None:
292
+ return hydrated
168
293
  existing = await self.fallback.get_session(session_id)
169
294
  if existing is not None:
295
+ if session_id in self._unsafe_live_session_ids:
296
+ self._clean_read_unavailable()
170
297
  return existing
171
298
 
172
299
  live = await self.fallback.create_session(agent_id, user_id, session_id=session_id)
@@ -178,21 +305,126 @@ class ResilientSessionService(BaseSessionService):
178
305
  )
179
306
  if ok and durable is not None:
180
307
  self._primary_session_ids.add(durable.id)
181
- return await self._hydrate(durable)
308
+ # create_session may have returned an existing row from a racing
309
+ # instance. Its metadata projection does not prove the event log
310
+ # is empty, so establish a complete live mirror before marking it
311
+ # safe for degraded writes.
312
+ generation = self._live_generation.get(durable.id, 0)
313
+ ok, complete = await self._call_primary("get_session", durable.id)
314
+ if not ok or complete is None:
315
+ self._clean_read_unavailable()
316
+ hydrated = await self._hydrate(complete, generation)
317
+ if hydrated is None:
318
+ self._clean_read_unavailable()
319
+ return hydrated
320
+ self._mark_dirty(live.id)
182
321
  return live
183
322
 
184
323
  async def get_session(self, session_id: str) -> Optional[Session]:
185
- live = await self.fallback.get_session(session_id)
324
+ if session_id in self._deleted_session_ids:
325
+ return None
326
+ if session_id in self._dirty_session_ids:
327
+ return await self.fallback.get_session(session_id)
328
+ if not self._primary_enabled:
329
+ if self._has_complete_live_session(session_id):
330
+ return await self.fallback.get_session(session_id)
331
+ self._clean_read_unavailable()
332
+ generation = self._live_generation.get(session_id, 0)
186
333
  ok, durable = await self._call_primary("get_session", session_id)
187
334
  if ok and durable is not None:
188
- return await self._hydrate(durable)
189
- return live
335
+ return await self._hydrate(durable, generation)
336
+ if not ok:
337
+ if self._has_complete_live_session(session_id):
338
+ return await self.fallback.get_session(session_id)
339
+ self._clean_read_unavailable()
340
+ return None
341
+
342
+ async def get_adk_session_snapshot(
343
+ self, session_id: str, expected_session: Session | None = None
344
+ ) -> Optional[Session]:
345
+ if session_id in self._deleted_session_ids:
346
+ raise HistoryReloadRequired("Session changed during legacy read")
347
+ live = session_id in self._dirty_session_ids or not self._primary_enabled
348
+ if (not self._primary_enabled and session_id not in self._dirty_session_ids
349
+ and not self._has_complete_live_session(session_id)):
350
+ self._clean_read_unavailable()
351
+ backend = self.fallback if live else self.primary
352
+ snapshot = getattr(backend, "get_adk_session_snapshot", None)
353
+ if callable(snapshot):
354
+ session = await snapshot(session_id, expected_session)
355
+ else:
356
+ session = await backend.get_session(session_id)
357
+ if expected_session is not None and (
358
+ session is None or session.agent_id != expected_session.agent_id
359
+ or session.user_id != expected_session.user_id
360
+ or session.created_at != expected_session.created_at
361
+ ):
362
+ raise HistoryReloadRequired("Session changed during legacy read")
363
+ if (session_id in self._deleted_session_ids
364
+ or (session_id in self._dirty_session_ids or not self._primary_enabled) != live):
365
+ raise HistoryReloadRequired("Session source changed during legacy read")
366
+ return session
190
367
 
191
368
  async def get_session_metadata(self, session_id: str) -> Optional[Session]:
369
+ if session_id in self._deleted_session_ids:
370
+ return None
371
+ if session_id in self._dirty_session_ids:
372
+ return await self.fallback.get_session_metadata(session_id)
373
+ if not self._primary_enabled:
374
+ if self._has_complete_live_session(session_id):
375
+ return await self.fallback.get_session_metadata(session_id)
376
+ self._clean_read_unavailable()
192
377
  ok, durable = await self._call_primary("get_session_metadata", session_id)
193
378
  if ok and durable is not None:
194
379
  return cast(Session, durable)
195
- return await self.fallback.get_session_metadata(session_id)
380
+ if not ok:
381
+ if self._has_complete_live_session(session_id):
382
+ return await self.fallback.get_session_metadata(session_id)
383
+ self._clean_read_unavailable()
384
+ return None
385
+
386
+ async def get_session_header(self, session_id: str) -> Optional[Session]:
387
+ if session_id in self._deleted_session_ids:
388
+ return None
389
+ if session_id in self._dirty_session_ids:
390
+ return await self.fallback.get_session_header(session_id)
391
+ if not self._primary_enabled:
392
+ if self._has_complete_live_session(session_id):
393
+ return await self.fallback.get_session_header(session_id)
394
+ self._clean_read_unavailable()
395
+ method = "get_session_header" if hasattr(self.primary, "get_session_header") else "get_session_metadata"
396
+ ok, header = await self._call_primary(method, session_id)
397
+ if not ok:
398
+ if self._has_complete_live_session(session_id):
399
+ return await self.fallback.get_session_header(session_id)
400
+ self._clean_read_unavailable()
401
+ return cast(Optional[Session], header)
402
+
403
+ async def get_session_summary_events(self, session_id: str) -> list[SessionEvent]:
404
+ if session_id in self._deleted_session_ids:
405
+ return []
406
+ if session_id in self._dirty_session_ids:
407
+ return await self.fallback.get_session_summary_events(session_id)
408
+ if not self._primary_enabled:
409
+ if self._has_complete_live_session(session_id):
410
+ return await self.fallback.get_session_summary_events(session_id)
411
+ self._clean_read_unavailable()
412
+ ok, events = await self._call_primary("get_session_summary_events", session_id)
413
+ if ok:
414
+ return cast(list[SessionEvent], events)
415
+ if self._has_complete_live_session(session_id):
416
+ return await self.fallback.get_session_summary_events(session_id)
417
+ self._clean_read_unavailable()
418
+
419
+ async def get_max_seq_id(self, session_id: str) -> int:
420
+ if session_id in self._dirty_session_ids:
421
+ return await self.fallback.get_max_seq_id(session_id)
422
+ if not self._primary_enabled:
423
+ self._clean_read_unavailable()
424
+ ok, value = await self._call_primary("get_max_seq_id", session_id)
425
+ if not ok:
426
+ self._clean_read_unavailable()
427
+ return int(value or 0)
196
428
 
197
429
  async def list_sessions(
198
430
  self,
@@ -201,28 +433,96 @@ class ResilientSessionService(BaseSessionService):
201
433
  offset: Optional[int] = None,
202
434
  limit: Optional[int] = None,
203
435
  ) -> list[Session]:
436
+ dirty = bool(self._dirty_session_ids or self._deleted_session_ids)
437
+ bounded = limit is not None
438
+ # Fetch enough durable rows to replace every locally changed/deleted
439
+ # row ahead of the requested page, without loading the whole agent.
440
+ durable_limit = (
441
+ (offset or 0) + limit + len(self._dirty_session_ids)
442
+ + len(self._deleted_session_ids)
443
+ if dirty and bounded else limit
444
+ )
204
445
  ok, durable_sessions = await self._call_primary(
205
446
  "list_sessions",
206
447
  agent_id,
207
448
  user_id,
208
- offset,
209
- limit,
449
+ 0 if dirty and bounded else (None if dirty else offset),
450
+ durable_limit,
210
451
  )
211
- if ok:
212
- return cast(list[Session], durable_sessions or [])
213
- return await self.fallback.list_sessions(agent_id, user_id, offset, limit)
452
+ if ok and not dirty:
453
+ return [s for s in cast(list[Session], durable_sessions or [])
454
+ if s.id not in self._deleted_session_ids]
455
+ if not ok:
456
+ self._clean_read_unavailable()
457
+ # Dirty rows may have an older durable counterpart. Merge metadata only.
458
+ durable = {s.id: s for s in (durable_sessions or [])} if ok else {}
459
+ if bounded and ok:
460
+ live = [session for session in await asyncio.gather(*(
461
+ self.fallback.get_session_metadata(session_id)
462
+ for session_id in self._dirty_session_ids
463
+ )) if session is not None]
464
+ else:
465
+ live = await self.fallback.list_session_metadata(agent_id, user_id)
466
+ durable.update({s.id: s for s in live
467
+ if s.agent_id == agent_id
468
+ and (user_id is None or s.user_id == user_id)
469
+ and (not ok or s.id in self._dirty_session_ids)})
470
+ ordered = sorted((s for s in durable.values() if s.id not in self._deleted_session_ids),
471
+ key=lambda s: (s.updated_at, s.created_at, s.id), reverse=True)
472
+ start = offset or 0
473
+ return ordered[start:None if limit is None else start + limit]
214
474
 
215
475
  async def count_sessions(self, agent_id: str, user_id: Optional[str] = None) -> int:
216
476
  ok, total = await self._call_primary("count_sessions", agent_id, user_id)
217
- if ok:
477
+ if ok and not (self._dirty_session_ids or self._deleted_session_ids):
218
478
  return int(total or 0)
219
- return await self.fallback.count_sessions(agent_id, user_id)
479
+ if not ok:
480
+ self._clean_read_unavailable()
481
+ count = int(total or 0)
482
+ for session_id in self._dirty_session_ids | self._deleted_session_ids:
483
+ ok, durable = await self._call_primary("get_session_header", session_id)
484
+ if not ok:
485
+ self._clean_read_unavailable()
486
+ live = (await self.fallback.get_session_header(session_id)
487
+ if session_id not in self._deleted_session_ids else None)
488
+ durable_in_scope = (durable is not None and durable.agent_id == agent_id
489
+ and (user_id is None or durable.user_id == user_id))
490
+ live_in_scope = (live is not None and live.agent_id == agent_id
491
+ and (user_id is None or live.user_id == user_id))
492
+ count += int(live_in_scope) - int(durable_in_scope)
493
+ return count
220
494
 
221
495
  async def delete_session(self, session_id: str) -> bool:
496
+ budget = getattr(self.primary, "db_budget", session_db_budget())
497
+ deadline = DatabaseOperationDeadline(budget)
498
+ lock = self._hydrate_locks.setdefault(session_id, asyncio.Lock())
499
+ await asyncio.wait_for(
500
+ lock.acquire(), timeout=min(budget.lock_seconds, deadline.remaining_seconds()),
501
+ )
502
+ try:
503
+ deleted = await deadline.run(self._delete_session_locked(session_id))
504
+ self._live_generation[session_id] = self._live_generation.get(session_id, 0) + 1
505
+ return deleted
506
+ finally:
507
+ lock.release()
508
+
509
+ async def _delete_session_locked(self, session_id: str) -> bool:
222
510
  deleted = await self.fallback.delete_session(session_id)
223
- ok, durable_deleted = await self._call_primary("delete_session", session_id)
511
+ try:
512
+ ok, durable_deleted = await self._call_primary("delete_session", session_id)
513
+ except BaseException:
514
+ # Cancellation after the local delete cannot let a recovered
515
+ # durable row reappear in this process.
516
+ self._deleted_session_ids.add(session_id)
517
+ raise
224
518
  if ok:
225
519
  self._primary_session_ids.discard(session_id)
520
+ self._dirty_session_ids.discard(session_id)
521
+ self._hydrated_session_ids.discard(session_id)
522
+ self._unsafe_live_session_ids.discard(session_id)
523
+ self._deleted_session_ids.discard(session_id)
524
+ else:
525
+ self._deleted_session_ids.add(session_id)
226
526
  return deleted or bool(durable_deleted) if ok else deleted
227
527
 
228
528
  async def update_session_metadata(
@@ -235,27 +535,39 @@ class ResilientSessionService(BaseSessionService):
235
535
  first_prompt: Optional[str] = None,
236
536
  last_prompt: Optional[str] = None,
237
537
  ) -> Session:
238
- if await self.fallback.get_session(session_id) is None:
239
- await self.get_session(session_id)
240
- live = await self.fallback.update_session_metadata(
241
- session_id,
242
- title=title,
243
- title_source=title_source,
244
- summary=summary,
245
- first_prompt=first_prompt,
246
- last_prompt=last_prompt,
538
+ budget = getattr(self.primary, "db_budget", session_db_budget())
539
+ deadline = DatabaseOperationDeadline(budget)
540
+ lock = self._hydrate_locks.setdefault(session_id, asyncio.Lock())
541
+ await asyncio.wait_for(
542
+ lock.acquire(), timeout=min(budget.lock_seconds, deadline.remaining_seconds()),
247
543
  )
248
- await self._ensure_primary_session(session_id)
249
- await self._call_primary(
250
- "update_session_metadata",
251
- session_id,
252
- title=title,
253
- title_source=title_source,
254
- summary=summary,
255
- first_prompt=first_prompt,
256
- last_prompt=last_prompt,
257
- )
258
- return live
544
+ try:
545
+ updated = await deadline.run(self._update_session_metadata_locked(
546
+ session_id, title=title, title_source=title_source,
547
+ summary=summary, first_prompt=first_prompt, last_prompt=last_prompt,
548
+ ))
549
+ self._live_generation[session_id] = self._live_generation.get(session_id, 0) + 1
550
+ return updated
551
+ finally:
552
+ lock.release()
553
+
554
+ async def _update_session_metadata_locked(
555
+ self, session_id: str, *, title: Optional[str] = None,
556
+ title_source: Optional[str] = None, summary: Optional[str] = None,
557
+ first_prompt: Optional[str] = None, last_prompt: Optional[str] = None,
558
+ ) -> Session:
559
+ kwargs = dict(title=title, title_source=title_source, summary=summary,
560
+ first_prompt=first_prompt, last_prompt=last_prompt)
561
+ if session_id not in self._dirty_session_ids:
562
+ ok, durable = await self._call_primary("update_session_metadata", session_id, **kwargs)
563
+ if ok:
564
+ if session_id in self._hydrated_session_ids:
565
+ await self.fallback.update_session_metadata(session_id, **kwargs)
566
+ return cast(Session, durable)
567
+ if session_id not in self._hydrated_session_ids or session_id in self._unsafe_live_session_ids:
568
+ self._clean_read_unavailable()
569
+ self._mark_dirty(session_id)
570
+ return await self.fallback.update_session_metadata(session_id, **kwargs)
259
571
 
260
572
  async def _ensure_primary_session(self, session_id: str) -> None:
261
573
  """Create the session in PG if it only exists in memory (degraded-era)."""
@@ -264,11 +576,11 @@ class ResilientSessionService(BaseSessionService):
264
576
  async with self._primary_session_lock:
265
577
  if not self._primary_enabled or session_id in self._primary_session_ids:
266
578
  return
267
- ok, durable = await self._call_primary("get_session", session_id)
579
+ ok, durable = await self._call_primary("get_session_metadata", session_id)
268
580
  if not ok:
269
581
  return
270
582
  if durable is None:
271
- live = await self.fallback.get_session(session_id)
583
+ live = await self.fallback.get_session_metadata(session_id)
272
584
  if live is None:
273
585
  return
274
586
  ok, durable = await self._call_primary(
@@ -282,431 +594,181 @@ class ResilientSessionService(BaseSessionService):
282
594
  self._primary_session_ids.add(session_id)
283
595
 
284
596
  async def append_event(self, session_id: str, event: SessionEvent) -> SessionEvent:
285
- if await self.fallback.get_session(session_id) is None:
286
- await self.get_session(session_id)
287
- live = await self.fallback.append_event(session_id, event)
288
- await self._ensure_primary_session(session_id)
289
- await self._call_primary("append_event", session_id, event)
290
- return live
291
-
292
- async def get_event_by_id(self, session_id: str, event_id: str) -> Optional[SessionEvent]:
293
- if await self.fallback.get_session_metadata(session_id) is None:
294
- await self.get_session(session_id)
295
- return await self.fallback.get_event_by_id(session_id, event_id)
296
-
297
- async def get_events_by_invocation_id(
298
- self,
299
- session_id: str,
300
- invocation_id: str,
301
- *,
302
- after_seq_id: Optional[int] = None,
303
- before_seq_id: Optional[int] = None,
304
- ) -> list[SessionEvent]:
305
- # ResilientSessionService is explicitly live-first: hydrate any durable
306
- # prefix, then read the indexed in-memory authority used by get_events.
307
- await self.get_session(session_id)
308
- return await self.fallback.get_events_by_invocation_id(
309
- session_id,
310
- invocation_id,
311
- after_seq_id=after_seq_id,
312
- before_seq_id=before_seq_id,
313
- )
314
-
315
- async def get_events(
316
- self,
317
- session_id: str,
318
- offset: Optional[int] = None,
319
- limit: Optional[int] = None,
320
- after_seq_id: Optional[int] = None,
321
- before_seq_id: Optional[int] = None,
322
- ) -> list[SessionEvent]:
323
- await self.get_session(session_id)
324
- return cast(
325
- list[SessionEvent],
326
- await self.fallback.get_events(
327
- session_id,
328
- offset,
329
- limit,
330
- after_seq_id,
331
- before_seq_id,
332
- ),
333
- )
334
-
335
- async def count_events(
336
- self,
337
- session_id: str,
338
- after_seq_id: Optional[int] = None,
339
- before_seq_id: Optional[int] = None,
340
- ) -> int:
341
- await self.get_session(session_id)
342
- return cast(
343
- int,
344
- await self.fallback.count_events(session_id, after_seq_id, before_seq_id),
345
- )
346
-
347
- async def get_sessions_by_ids(self, session_ids: list[str]) -> list[Session]:
348
- result: dict[str, Session] = {}
349
- ok, durable = await self._call_primary("get_sessions_by_ids", session_ids)
350
- if ok:
351
- result.update({session.id: session for session in durable or []})
352
- live = await self.fallback.get_sessions_by_ids(session_ids)
353
- result.update({session.id: session for session in live})
354
- return [result[session_id] for session_id in session_ids if session_id in result]
355
-
356
- async def list_session_metadata(
357
- self, agent_id: Optional[str] = None, user_id: Optional[str] = None
358
- ) -> list[Session]:
359
- result: dict[str, Session] = {}
360
- ok, durable = await self._call_primary("list_session_metadata", agent_id, user_id)
361
- if ok:
362
- result.update({session.id: session for session in durable or []})
363
- live = await self.fallback.list_session_metadata(agent_id, user_id)
364
- result.update({session.id: session for session in live})
365
- sessions = list(result.values())
366
- sessions.sort(key=lambda item: (item.updated_at, item.created_at, item.id), reverse=True)
367
- return sessions
368
-
369
- async def _partition_query_session_ids(
370
- self, session_ids: list[str] | None, agent_id: str | None
371
- ) -> tuple[list[str], list[str]]:
372
- if session_ids is None:
373
- sessions = await self.list_session_metadata(agent_id)
374
- ids = [session.id for session in sessions]
375
- else:
376
- ids = list(dict.fromkeys(session_ids))
377
- dirty = set(self._dirty_session_ids)
378
- durable_ids: set[str] = set()
379
- if ids:
380
- ok, durable_metadata = await self._call_primary("get_sessions_by_ids", ids)
381
- if ok:
382
- durable_ids = {session.id for session in durable_metadata or []}
383
- live_metadata = await self.fallback.get_sessions_by_ids(ids)
384
- live_ids = {session.id for session in live_metadata}
385
- clean_ids = [
386
- session_id
387
- for session_id in ids
388
- if session_id not in dirty and session_id in durable_ids
389
- ]
390
- fallback_ids = [
391
- session_id for session_id in ids if session_id in dirty or session_id in live_ids
392
- ]
393
- return clean_ids, fallback_ids
394
-
395
- async def query_events(self, query: SessionEventQuery) -> list[SessionEvent]:
396
- clean_ids, live_ids = await self._partition_query_session_ids(
397
- query.session_ids, query.agent_id
398
- )
399
- window = query.offset + query.limit
400
- durable_events: list[SessionEvent] = []
401
- if clean_ids:
402
- ok, result = await self._call_primary(
403
- "query_events",
404
- SessionEventQuery(
405
- **{**query.__dict__, "session_ids": clean_ids, "offset": 0, "limit": window}
406
- ),
407
- )
408
- if ok:
409
- durable_events = cast(list[SessionEvent], result or [])
410
- else:
411
- return await self.fallback.query_events(query)
412
- live_events = (
413
- await self.fallback.query_events(
414
- SessionEventQuery(
415
- **{**query.__dict__, "session_ids": live_ids, "offset": 0, "limit": window}
416
- )
417
- )
418
- if live_ids
419
- else []
597
+ return await self._append_event_entry(session_id, event, None)
598
+
599
+ async def append_event_if_session_matches(
600
+ self, session_id: str, event: SessionEvent, expected_session: Session
601
+ ) -> SessionEvent:
602
+ """Use normal recovery fencing with an incarnation check at each write."""
603
+ return await self._append_event_entry(session_id, event, expected_session)
604
+
605
+ async def _append_event_entry(
606
+ self, session_id: str, event: SessionEvent, expected_session: Session | None
607
+ ) -> SessionEvent:
608
+ budget = getattr(self.primary, "db_budget", session_db_budget())
609
+ deadline = DatabaseOperationDeadline(budget)
610
+ lock = self._hydrate_locks.setdefault(session_id, asyncio.Lock())
611
+ await asyncio.wait_for(
612
+ lock.acquire(), timeout=min(budget.lock_seconds, deadline.remaining_seconds()),
420
613
  )
421
- merged_by_id = {event.id: event for event in [*durable_events, *live_events]}
422
- merged = list(merged_by_id.values())
423
- if query.order_by_seq:
424
- merged.sort(key=lambda event: (event.session_id, event.seq_id, event.id))
425
- else:
426
- merged.sort(
427
- key=lambda event: (event.timestamp, event.session_id, event.seq_id, event.id)
428
- )
429
- if query.from_start:
430
- return merged[query.offset : query.offset + query.limit]
431
- end = max(len(merged) - query.offset, 0)
432
- return merged[max(end - query.limit, 0) : end]
433
-
434
- async def count_event_query(self, query: SessionEventQuery) -> int:
435
- # Deduplicate by physical event id because a hydrated clean session may
436
- # be present in both children during recovery.
437
- events = await self.query_events(
438
- SessionEventQuery(
439
- **{
440
- **query.__dict__,
441
- "offset": 0,
442
- "limit": 2**31 - 1,
443
- "from_start": True,
444
- }
445
- )
446
- )
447
- return len(events)
448
-
449
- async def get_checkpoint_lookup_stats(
450
- self, session_id: str, run_id: str, checkpoint_id: str
451
- ) -> dict[str, object]:
452
- if session_id not in self._dirty_session_ids:
453
- ok, stats = await self._call_primary(
454
- "get_checkpoint_lookup_stats", session_id, run_id, checkpoint_id
455
- )
456
- if ok:
457
- return cast(dict[str, object], stats)
458
- return await self.fallback.get_checkpoint_lookup_stats(
459
- session_id, run_id, checkpoint_id
460
- )
461
-
462
- async def scan_checkpoint_events(
463
- self, query: CheckpointEventQuery
464
- ) -> list[SessionEvent]:
465
- if query.limit < 1 or query.limit > 50:
466
- raise ValueError("checkpoint scan limit must be between 1 and 50")
467
- clean_ids, live_ids = await self._partition_query_session_ids(
468
- query.session_ids, query.agent_id
469
- )
470
- if clean_ids and not live_ids:
471
- ok, result = await self._call_primary(
472
- "scan_checkpoint_events",
473
- CheckpointEventQuery(**{**query.__dict__, "session_ids": clean_ids}),
474
- )
475
- if ok:
476
- return cast(list[SessionEvent], result or [])
477
- return await self.fallback.scan_checkpoint_events(query)
478
- if live_ids and not clean_ids:
479
- return await self.fallback.scan_checkpoint_events(
480
- CheckpointEventQuery(**{**query.__dict__, "session_ids": live_ids})
481
- )
482
- window = query.offset + query.limit
483
- durable: list[SessionEvent] = []
484
- if clean_ids:
485
- ok, result = await self._call_primary(
486
- "scan_checkpoint_events",
487
- CheckpointEventQuery(
488
- **{
489
- **query.__dict__,
490
- "session_ids": clean_ids,
491
- "offset": 0,
492
- "limit": min(window, 50),
493
- }
494
- ),
614
+ clean_write = session_id not in self._dirty_session_ids
615
+ try:
616
+ persisted = await deadline.run(
617
+ self._append_event_locked(session_id, event, deadline, expected_session)
495
618
  )
496
- if ok:
497
- durable = cast(list[SessionEvent], result or [])
498
- else:
499
- return await self.fallback.scan_checkpoint_events(query)
500
- live = (
501
- await self.fallback.scan_checkpoint_events(
502
- CheckpointEventQuery(
503
- **{
504
- **query.__dict__,
505
- "session_ids": live_ids,
506
- "offset": 0,
507
- "limit": min(window, 50),
508
- }
619
+ self._live_generation[session_id] = self._live_generation.get(session_id, 0) + 1
620
+ return persisted
621
+ except (TimeoutError, asyncio.CancelledError, SessionBackendUnavailable) as error:
622
+ if clean_write:
623
+ # The primary may have committed before its acknowledgement or
624
+ # stable-ID readback was interrupted. Fence the old mirror and
625
+ # any hydration that captured a snapshot before this write.
626
+ self._unsafe_live_session_ids.add(session_id)
627
+ self._hydrated_session_ids.discard(session_id)
628
+ self._hydrated_state_keys.difference_update(
629
+ key for key in tuple(self._hydrated_state_keys) if key[3] == session_id
509
630
  )
510
- )
511
- if live_ids
512
- else []
513
- )
514
- merged = list({event.id: event for event in [*durable, *live]}.values())
515
- merged.sort(key=lambda event: (event.timestamp, event.session_id, event.seq_id, event.id))
516
- return merged[query.offset : query.offset + query.limit]
517
-
518
- async def iter_checkpoint_event_chunks(
519
- self, query: CheckpointEventQuery
520
- ) -> AsyncIterator[list[SessionEvent]]:
521
- if query.limit < 1 or query.limit > 50:
522
- raise ValueError("checkpoint scan limit must be between 1 and 50")
523
- clean_ids, live_ids = await self._partition_query_session_ids(
524
- query.session_ids, query.agent_id
525
- )
526
- token = self._checkpoint_partition.set(
527
- (frozenset(clean_ids), frozenset(live_ids))
528
- )
529
- batches = self._iter_checkpoint_event_chunks_partitioned(
530
- query, clean_ids, live_ids
531
- ).__aiter__()
532
- try:
533
- async for batch in batches:
534
- yield batch
631
+ self._live_generation[session_id] = self._live_generation.get(session_id, 0) + 1
632
+ if isinstance(error, TimeoutError):
633
+ raise SessionBackendUnavailable(
634
+ "Session event commit outcome is unknown; stable ID confirmation required"
635
+ ) from error
636
+ raise
535
637
  finally:
536
- close = getattr(batches, "aclose", None)
537
- if callable(close):
538
- await close()
539
- self._checkpoint_partition.reset(token)
540
-
541
- async def _iter_checkpoint_event_chunks_partitioned(
542
- self,
543
- query: CheckpointEventQuery,
544
- clean_ids: list[str],
545
- live_ids: list[str],
546
- ) -> AsyncIterator[list[SessionEvent]]:
547
- if query.limit < 1 or query.limit > 50:
548
- raise ValueError("checkpoint scan limit must be between 1 and 50")
549
- if not clean_ids:
550
- batches = self.fallback.iter_checkpoint_event_chunks(
551
- CheckpointEventQuery(**{**query.__dict__, "session_ids": live_ids})
552
- ).__aiter__()
553
- try:
554
- async for batch in batches:
555
- yield batch
556
- finally:
557
- close = getattr(batches, "aclose", None)
558
- if callable(close):
559
- await close()
560
- return
561
- if not live_ids:
562
- batches = self.primary.iter_checkpoint_event_chunks(
563
- CheckpointEventQuery(**{**query.__dict__, "session_ids": clean_ids})
564
- ).__aiter__()
638
+ lock.release()
639
+
640
+ async def _append_event_locked(
641
+ self, session_id: str, event: SessionEvent, deadline: DatabaseOperationDeadline,
642
+ expected_session: Session | None = None,
643
+ ) -> SessionEvent:
644
+ if expected_session is not None and session_id in self._deleted_session_ids:
645
+ raise HistoryReloadRequired("Session changed during legacy import")
646
+
647
+ async def append_on(backend: BaseSessionService) -> SessionEvent:
648
+ if expected_session is None:
649
+ return await backend.append_event(session_id, event)
650
+ guarded = getattr(backend, "append_event_if_session_matches", None)
651
+ if callable(guarded):
652
+ return await guarded(session_id, event, expected_session)
653
+ current = await backend.get_session_metadata(session_id)
654
+ if (current is None or current.agent_id != expected_session.agent_id
655
+ or current.user_id != expected_session.user_id
656
+ or current.created_at != expected_session.created_at):
657
+ raise HistoryReloadRequired("Session changed during legacy import")
658
+ return await backend.append_event(session_id, event)
659
+
660
+ if session_id in self._dirty_session_ids:
661
+ return await append_on(self.fallback)
662
+ if not self._primary_enabled:
663
+ if not self._has_complete_live_session(session_id):
664
+ self._clean_read_unavailable()
665
+ self._mark_dirty(session_id)
666
+ return await append_on(self.fallback)
667
+ if expected_session is None:
668
+ ok, durable = await self._call_primary("append_event", session_id, event)
669
+ else:
565
670
  try:
566
- async for batch in batches:
567
- yield batch
671
+ durable = await append_on(self.primary)
672
+ ok = True
568
673
  except Exception as exc:
569
674
  if not is_session_backend_failure(exc):
570
675
  raise
571
676
  self._disable_primary(exc)
572
- raise CheckpointScanRestartRequired(
573
- "primary failed during clean checkpoint scan"
574
- ) from exc
575
- finally:
576
- close = getattr(batches, "aclose", None)
577
- if callable(close):
578
- await close()
579
- return
580
-
581
- iterators = {
582
- "clean": self.primary.iter_checkpoint_event_chunks(
583
- CheckpointEventQuery(
584
- **{**query.__dict__, "session_ids": clean_ids, "offset": 0, "limit": 50}
585
- )
586
- ).__aiter__(),
587
- "live": self.fallback.iter_checkpoint_event_chunks(
588
- CheckpointEventQuery(
589
- **{**query.__dict__, "session_ids": live_ids, "offset": 0, "limit": 50}
590
- )
591
- ).__aiter__(),
592
- }
593
- pages: dict[str, list[SessionEvent]] = {"clean": [], "live": []}
594
- indexes = {"clean": 0, "live": 0}
595
- exhausted = {"clean": False, "live": False}
596
- skipped = 0
597
-
598
- async def load(kind: str) -> None:
677
+ ok, durable = False, None
678
+ if not ok:
679
+ # The commit may have succeeded before the transport failed. A
680
+ # stable event ID is the only safe confirmation; never replay the
681
+ # append or its upstream tool/LLM side effect automatically.
599
682
  try:
600
- pages[kind] = await anext(iterators[kind])
601
- except StopAsyncIteration:
602
- pages[kind] = []
603
- exhausted[kind] = True
604
- except Exception as exc:
605
- if kind != "clean" or not is_session_backend_failure(exc):
606
- raise
607
- self._disable_primary(exc)
608
- raise CheckpointScanRestartRequired(
609
- "primary failed during mixed checkpoint scan"
610
- ) from exc
611
- indexes[kind] = 0
612
- if len(pages[kind]) < 50:
613
- exhausted[kind] = True
614
-
615
- try:
616
- await load("clean")
617
- await load("live")
618
- output: list[SessionEvent] = []
619
- while True:
620
- for kind in ("clean", "live"):
621
- if indexes[kind] >= len(pages[kind]) and not exhausted[kind]:
622
- await load(kind)
623
- candidates = [
624
- (event.timestamp, event.session_id, event.seq_id, event.id, kind, event)
625
- for kind in ("clean", "live")
626
- for event in pages[kind][indexes[kind] : indexes[kind] + 1]
627
- ]
628
- if not candidates:
629
- if output:
630
- yield output
631
- break
632
- *_, kind, event = min(candidates)
633
- indexes[kind] += 1
634
- if skipped < query.offset:
635
- skipped += 1
636
- continue
637
- output.append(event)
638
- if len(output) == query.limit:
639
- yield output
640
- output = []
641
- finally:
642
- for iterator in iterators.values():
643
- close = getattr(iterator, "aclose", None)
644
- if callable(close):
645
- await close()
646
-
647
- async def get_checkpoint_stats(
648
- self, keys: list[tuple[str, str, str]]
649
- ) -> dict[str, object]:
650
- if len(keys) > 50:
651
- raise ValueError("checkpoint stats batch cannot exceed 50 keys")
652
- unique_keys = list(dict.fromkeys(keys))
653
- active = self._checkpoint_partition.get()
654
- if active is None:
655
- clean_ids, live_ids = await self._partition_query_session_ids(
656
- list(dict.fromkeys(key[0] for key in unique_keys)), None
657
- )
658
- clean_set, live_set = set(clean_ids), set(live_ids)
683
+ if expected_session is not None:
684
+ guarded_read = getattr(
685
+ self.primary, "get_event_by_id_if_session_matches", None
686
+ )
687
+ if callable(guarded_read):
688
+ committed = await deadline.run(
689
+ guarded_read(session_id, event.id, expected_session)
690
+ )
691
+ else:
692
+ current = await deadline.run(
693
+ self.primary.get_session_metadata(session_id)
694
+ )
695
+ if (current is None or current.agent_id != expected_session.agent_id
696
+ or current.user_id != expected_session.user_id
697
+ or current.created_at != expected_session.created_at):
698
+ raise HistoryReloadRequired("Session changed during legacy import")
699
+ committed = await deadline.run(
700
+ self.primary.get_event_by_id(session_id, event.id)
701
+ )
702
+ else:
703
+ committed = await deadline.run(
704
+ self.primary.get_event_by_id(session_id, event.id)
705
+ )
706
+ except HistoryReloadRequired:
707
+ raise
708
+ except Exception as error:
709
+ raise SessionBackendUnavailable(
710
+ "Session event commit outcome is unknown; stable ID confirmation failed"
711
+ ) from error
712
+ if committed is not None:
713
+ self._primary_enabled = True
714
+ if self._probe_task is not None and not self._probe_task.done():
715
+ self._probe_task.cancel()
716
+ await self._mirror_persisted_event(session_id, event, committed)
717
+ return cast(SessionEvent, committed)
718
+ if session_id in self._unsafe_live_session_ids or session_id not in self._hydrated_session_ids:
719
+ self._clean_read_unavailable()
720
+ self._mark_dirty(session_id)
721
+ return await append_on(self.fallback)
722
+ persisted = cast(SessionEvent, durable)
723
+ await self._mirror_persisted_event(session_id, event, persisted)
724
+ return persisted
725
+
726
+ async def _mirror_persisted_event(
727
+ self, session_id: str, event: SessionEvent, persisted: SessionEvent,
728
+ ) -> None:
729
+ if session_id in self._hydrated_session_ids:
730
+ expected_seq = await self.fallback.count_events(session_id) + 1
659
731
  else:
660
- clean_set, live_set = map(set, active)
661
- clean_keys = [key for key in unique_keys if key[0] in clean_set]
662
- live_keys = [key for key in unique_keys if key[0] in live_set]
663
- durable: dict[str, object] = {"audits": {}, "latest_seq_ids": {}}
664
- if clean_keys:
665
- ok, value = await self._call_primary("get_checkpoint_stats", clean_keys)
666
- if not ok:
667
- raise CheckpointScanRestartRequired(
668
- "primary failed while reading checkpoint stats"
669
- )
670
- durable = cast(dict[str, object], value or durable)
671
- live = (
672
- await self.fallback.get_checkpoint_stats(live_keys)
673
- if live_keys
674
- else {"audits": {}, "latest_seq_ids": {}}
675
- )
676
- return {
677
- "audits": {
678
- **cast(dict, durable["audits"]),
679
- **cast(dict, live["audits"]),
680
- },
681
- "latest_seq_ids": {
682
- **cast(dict, durable["latest_seq_ids"]),
683
- **cast(dict, live["latest_seq_ids"]),
684
- },
685
- }
686
-
687
- async def get_events_for_agent(
688
- self,
689
- agent_id: str,
690
- user_id: Optional[str] = None,
691
- offset: Optional[int] = None,
692
- limit: Optional[int] = None,
693
- ) -> list[SessionEvent]:
694
- ok, events = await self._call_primary(
695
- "get_events_for_agent", agent_id, user_id, offset, limit
732
+ expected_seq = None
733
+ if expected_seq == persisted.seq_id:
734
+ if await self.fallback.get_event_by_id(session_id, event.id) is None:
735
+ await self.fallback.append_event(session_id, event)
736
+ elif expected_seq is not None:
737
+ self._unsafe_live_session_ids.add(session_id)
738
+
739
+ async def claim_tool_receipt(self, session_id: str, **kwargs: Any) -> ToolReceiptClaim:
740
+ # Falling back to the process-local live store here would re-open the
741
+ # cross-process duplicate window this claim closes. Fail closed while
742
+ # the durable primary is unavailable instead.
743
+ if (not self._primary_enabled or session_id in self._dirty_session_ids
744
+ or session_id in self._deleted_session_ids):
745
+ raise SessionBackendUnavailable("durable tool receipt store unavailable")
746
+ ok, result = await self._call_primary("claim_tool_receipt", session_id, **kwargs)
747
+ if not ok:
748
+ raise SessionBackendUnavailable("durable tool receipt store unavailable")
749
+ return cast(ToolReceiptClaim, result)
750
+
751
+ async def settle_tool_receipt(self, session_id: str, **kwargs: Any) -> ToolReceiptClaim:
752
+ if (not self._primary_enabled or session_id in self._dirty_session_ids
753
+ or session_id in self._deleted_session_ids):
754
+ raise SessionBackendUnavailable("durable tool receipt store unavailable")
755
+ ok, result = await self._call_primary("settle_tool_receipt", session_id, **kwargs)
756
+ if not ok:
757
+ raise SessionBackendUnavailable("durable tool receipt store unavailable")
758
+ return cast(ToolReceiptClaim, result)
759
+
760
+ async def get_tool_receipt_claim(
761
+ self, session_id: str, *, idempotency_key: str
762
+ ) -> ToolReceiptClaim | None:
763
+ if (not self._primary_enabled or session_id in self._dirty_session_ids
764
+ or session_id in self._deleted_session_ids):
765
+ raise SessionBackendUnavailable("durable tool receipt store unavailable")
766
+ ok, result = await self._call_primary(
767
+ "get_tool_receipt_claim", session_id, idempotency_key=idempotency_key
696
768
  )
697
- if ok:
698
- return cast(list[SessionEvent], events)
699
- return await self.fallback.get_events_for_agent(agent_id, user_id, offset, limit)
700
-
701
- async def count_events_for_agent(
702
- self,
703
- agent_id: str,
704
- user_id: Optional[str] = None,
705
- ) -> int:
706
- ok, total = await self._call_primary("count_events_for_agent", agent_id, user_id)
707
- if ok:
708
- return int(total or 0)
709
- return await self.fallback.count_events_for_agent(agent_id, user_id)
769
+ if not ok:
770
+ raise SessionBackendUnavailable("durable tool receipt store unavailable")
771
+ return cast(ToolReceiptClaim | None, result)
710
772
 
711
773
  async def get_state(
712
774
  self,
@@ -716,10 +778,31 @@ class ResilientSessionService(BaseSessionService):
716
778
  scope: str = "session",
717
779
  ) -> Optional[SessionState]:
718
780
  if session_id:
719
- await self.get_session(session_id)
720
- live = await self.fallback.get_state(agent_id, user_id, session_id, scope)
721
- if live is not None or not self._primary_enabled:
722
- return live
781
+ lock = self._hydrate_locks.setdefault(session_id, asyncio.Lock())
782
+ async with lock:
783
+ return await self._get_state_locked(agent_id, user_id, session_id, scope)
784
+ return await self._get_state_locked(agent_id, user_id, session_id, scope)
785
+
786
+ async def _get_state_locked(
787
+ self, agent_id: str, user_id: Optional[str], session_id: Optional[str],
788
+ scope: str,
789
+ ) -> Optional[SessionState]:
790
+ state_key = (scope, agent_id, user_id or "", session_id or "")
791
+ unsafe_live = session_id in self._unsafe_live_session_ids
792
+ if not unsafe_live and (
793
+ state_key in self._dirty_state_keys or session_id in self._dirty_session_ids
794
+ ):
795
+ return await self.fallback.get_state(agent_id, user_id, session_id, scope)
796
+ safe_live = not unsafe_live and (
797
+ state_key in self._hydrated_state_keys or (
798
+ scope == "session" and bool(session_id)
799
+ and self._has_complete_live_session(session_id)
800
+ )
801
+ )
802
+ if not self._primary_enabled:
803
+ if safe_live:
804
+ return await self.fallback.get_state(agent_id, user_id, session_id, scope)
805
+ self._clean_read_unavailable()
723
806
  ok, durable = await self._call_primary(
724
807
  "get_state",
725
808
  agent_id,
@@ -728,14 +811,14 @@ class ResilientSessionService(BaseSessionService):
728
811
  scope,
729
812
  )
730
813
  if ok and durable is not None:
731
- return await self.fallback.update_state(
732
- agent_id=agent_id,
733
- user_id=user_id,
734
- session_id=session_id,
735
- scope=scope,
736
- state_delta=durable.state,
737
- )
738
- return live
814
+ await self.fallback.mirror_state_snapshot(cast(SessionState, durable))
815
+ self._hydrated_state_keys.add(state_key)
816
+ return cast(SessionState, durable)
817
+ if not ok:
818
+ if safe_live:
819
+ return await self.fallback.get_state(agent_id, user_id, session_id, scope)
820
+ self._clean_read_unavailable()
821
+ return None
739
822
 
740
823
  async def update_state(
741
824
  self,
@@ -746,26 +829,50 @@ class ResilientSessionService(BaseSessionService):
746
829
  scope: str,
747
830
  state_delta: dict[str, Any],
748
831
  ) -> SessionState:
749
- if session_id and await self.fallback.get_session(session_id) is None:
750
- await self.get_session(session_id)
751
- live = await self.fallback.update_state(
752
- agent_id=agent_id,
753
- user_id=user_id,
754
- session_id=session_id,
755
- scope=scope,
756
- state_delta=state_delta,
757
- )
832
+ budget = getattr(self.primary, "db_budget", session_db_budget())
833
+ deadline = DatabaseOperationDeadline(budget)
758
834
  if session_id:
759
- await self._ensure_primary_session(session_id)
760
- await self._call_primary(
761
- "update_state",
762
- agent_id=agent_id,
763
- user_id=user_id,
764
- session_id=session_id,
765
- scope=scope,
766
- state_delta=state_delta,
767
- )
768
- return live
835
+ lock = self._hydrate_locks.setdefault(session_id, asyncio.Lock())
836
+ await asyncio.wait_for(
837
+ lock.acquire(), timeout=min(budget.lock_seconds, deadline.remaining_seconds()),
838
+ )
839
+ try:
840
+ updated = await deadline.run(self._update_state_locked(
841
+ agent_id=agent_id, user_id=user_id, session_id=session_id,
842
+ scope=scope, state_delta=state_delta,
843
+ ))
844
+ self._live_generation[session_id] = self._live_generation.get(session_id, 0) + 1
845
+ return updated
846
+ finally:
847
+ lock.release()
848
+ return await deadline.run(self._update_state_locked(
849
+ agent_id=agent_id, user_id=user_id, session_id=session_id,
850
+ scope=scope, state_delta=state_delta,
851
+ ))
852
+
853
+ async def _update_state_locked(
854
+ self, *, agent_id: str, user_id: Optional[str], session_id: Optional[str],
855
+ scope: str, state_delta: dict[str, Any],
856
+ ) -> SessionState:
857
+ kwargs = dict(agent_id=agent_id, user_id=user_id, session_id=session_id,
858
+ scope=scope, state_delta=state_delta)
859
+ state_key = (scope, agent_id, user_id or "", session_id or "")
860
+ if session_id not in self._dirty_session_ids and state_key not in self._dirty_state_keys:
861
+ ok, durable = await self._call_primary("update_state", **kwargs)
862
+ if ok:
863
+ await self.fallback.mirror_state_snapshot(cast(SessionState, durable))
864
+ self._hydrated_state_keys.add(state_key)
865
+ return cast(SessionState, durable)
866
+ if state_key not in self._hydrated_state_keys and not (
867
+ scope == "session" and bool(session_id)
868
+ and self._has_complete_live_session(session_id)
869
+ ):
870
+ self._clean_read_unavailable()
871
+ self._mark_dirty(session_id)
872
+ self._dirty_state_keys.add(state_key)
873
+ if session_id in self._unsafe_live_session_ids:
874
+ self._clean_read_unavailable()
875
+ return await self.fallback.update_state(**kwargs)
769
876
 
770
877
  async def aclose(self) -> None:
771
878
  if self._probe_task is not None and not self._probe_task.done():