agentengine-sdk-python 0.7.0__py3-none-any.whl → 0.8.1__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 (450) hide show
  1. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/METADATA +52 -20
  2. agentengine_sdk_python-0.8.1.dist-info/RECORD +644 -0
  3. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/WHEEL +1 -1
  4. ksadk/a2a/__init__.py +180 -11
  5. ksadk/a2a/bootstrap.py +332 -0
  6. ksadk/a2a/card.py +15 -0
  7. ksadk/a2a/context_store.py +203 -0
  8. ksadk/a2a/control_plane.py +1056 -0
  9. ksadk/a2a/credential.py +99 -0
  10. ksadk/a2a/event_adapter.py +149 -0
  11. ksadk/a2a/executor.py +361 -137
  12. ksadk/a2a/external_transport.py +516 -0
  13. ksadk/a2a/identity.py +304 -0
  14. ksadk/a2a/ids.py +22 -0
  15. ksadk/a2a/langgraph.py +285 -0
  16. ksadk/a2a/resume_store.py +201 -0
  17. ksadk/a2a/routes.py +112 -0
  18. ksadk/a2a/server.py +102 -62
  19. ksadk/a2a/service_env.py +33 -0
  20. ksadk/a2a/space_client.py +1122 -0
  21. ksadk/a2a/task_adapter.py +423 -0
  22. ksadk/a2a/task_event_dispatcher.py +167 -0
  23. ksadk/a2a/task_event_outbox.py +326 -0
  24. ksadk/a2a/task_store.py +206 -0
  25. ksadk/a2a_card.py +101 -0
  26. ksadk/a2ui/__init__.py +27 -0
  27. ksadk/a2ui/core.py +186 -0
  28. ksadk/a2ui/models.py +166 -0
  29. ksadk/a2ui/renderer.py +198 -0
  30. ksadk/agui/__init__.py +9 -0
  31. ksadk/agui/a2ui_projection.py +92 -0
  32. ksadk/agui/agent.py +872 -0
  33. ksadk/agui/config.py +92 -0
  34. ksadk/agui/routes.py +47 -0
  35. ksadk/api/client.py +238 -204
  36. ksadk/api/environment.py +27 -0
  37. ksadk/builders/__init__.py +2 -0
  38. ksadk/builders/base.py +16 -14
  39. ksadk/builders/code_builder.py +375 -270
  40. ksadk/builders/container_builder.py +240 -200
  41. ksadk/builders/framework_requirements.py +31 -12
  42. ksadk/builders/ks3_uploader.py +60 -29
  43. ksadk/builders/managed_runtime_builder.py +173 -0
  44. ksadk/builders/mcp_builder.py +91 -92
  45. ksadk/builders/requirements_utils.py +1 -5
  46. ksadk/cli/__init__.py +97 -171
  47. ksadk/cli/cmd_a2a.py +255 -84
  48. ksadk/cli/cmd_agent.py +19 -5
  49. ksadk/cli/cmd_build.py +158 -29
  50. ksadk/cli/cmd_completion.py +47 -35
  51. ksadk/cli/cmd_config.py +267 -198
  52. ksadk/cli/cmd_create.py +507 -740
  53. ksadk/cli/cmd_dashboard.py +120 -45
  54. ksadk/cli/cmd_deploy.py +135 -59
  55. ksadk/cli/cmd_destroy.py +44 -14
  56. ksadk/cli/cmd_eval.py +386 -0
  57. ksadk/cli/cmd_files.py +63 -24
  58. ksadk/cli/cmd_hermes.py +313 -135
  59. ksadk/cli/cmd_invoke.py +184 -122
  60. ksadk/cli/cmd_launch.py +76 -28
  61. ksadk/cli/cmd_mcp.py +98 -64
  62. ksadk/cli/cmd_model.py +14 -5
  63. ksadk/cli/cmd_openclaw.py +564 -268
  64. ksadk/cli/cmd_replay.py +71 -0
  65. ksadk/cli/cmd_run.py +58 -73
  66. ksadk/cli/cmd_status.py +80 -31
  67. ksadk/cli/cmd_studio.py +110 -0
  68. ksadk/cli/cmd_version.py +20 -13
  69. ksadk/cli/cmd_web.py +95 -18
  70. ksadk/cli/deploy_utils.py +20 -20
  71. ksadk/cli/dry_run.py +7 -4
  72. ksadk/cli/env_options.py +83 -0
  73. ksadk/cli/error_utils.py +37 -12
  74. ksadk/cli/global_options.py +4 -3
  75. ksadk/cli/local_runtime.py +1 -2
  76. ksadk/cli/model_catalog.py +6 -2
  77. ksadk/cli/network_options.py +41 -19
  78. ksadk/cli/resource_common.py +6 -2
  79. ksadk/cli/runtime_bootstrap.py +51 -0
  80. ksadk/cli/storage.py +0 -1
  81. ksadk/cli/ui.py +11 -3
  82. ksadk/cli/workflow_common.py +52 -18
  83. ksadk/codex/__init__.py +12 -0
  84. ksadk/codex/client.py +1042 -0
  85. ksadk/codex/phase.py +143 -0
  86. ksadk/codex/runtime.py +940 -0
  87. ksadk/common/aicp_env.py +0 -1
  88. ksadk/common/auth.py +2 -2
  89. ksadk/common/constants.py +1 -3
  90. ksadk/common/kop_client.py +232 -0
  91. ksadk/common/llm_utils.py +17 -13
  92. ksadk/compat/__init__.py +5 -0
  93. ksadk/compat/adk_compat.py +200 -0
  94. ksadk/compat/copilotkit_a2ui.py +330 -0
  95. ksadk/configs/__init__.py +12 -14
  96. ksadk/configs/env_registry.py +661 -116
  97. ksadk/configs/global_config.py +3 -2
  98. ksadk/configs/settings.py +133 -209
  99. ksadk/conversations/__init__.py +85 -92
  100. ksadk/conversations/attachment_storage.py +16 -11
  101. ksadk/conversations/attachments.py +44 -36
  102. ksadk/conversations/compaction_pipeline.py +12 -12
  103. ksadk/conversations/compaction_prompt.py +9 -5
  104. ksadk/conversations/context.py +221 -6
  105. ksadk/conversations/message_projection.py +438 -51
  106. ksadk/conversations/model_context.py +12 -6
  107. ksadk/conversations/model_options.py +0 -1
  108. ksadk/conversations/normalize.py +20 -16
  109. ksadk/conversations/reasoning_markup.py +1 -1
  110. ksadk/conversations/runtime.py +339 -5255
  111. ksadk/conversations/runtime_compaction.py +333 -0
  112. ksadk/conversations/runtime_constants.py +27 -0
  113. ksadk/conversations/runtime_governance.py +185 -0
  114. ksadk/conversations/runtime_input.py +660 -0
  115. ksadk/conversations/runtime_invocation.py +395 -0
  116. ksadk/conversations/runtime_metadata.py +475 -0
  117. ksadk/conversations/runtime_observability.py +367 -0
  118. ksadk/conversations/runtime_payloads.py +272 -0
  119. ksadk/conversations/runtime_persistence.py +440 -0
  120. ksadk/conversations/runtime_preparation.py +426 -0
  121. ksadk/conversations/runtime_resume.py +836 -0
  122. ksadk/conversations/runtime_stream_events.py +902 -0
  123. ksadk/conversations/runtime_streaming.py +700 -0
  124. ksadk/conversations/semantic_summary.py +17 -5
  125. ksadk/conversations/session_title.py +6 -3
  126. ksadk/deployment/__init__.py +15 -16
  127. ksadk/deployment/agent_access.py +12 -23
  128. ksadk/deployment/base.py +51 -38
  129. ksadk/deployment/managed_runtime.py +33 -0
  130. ksadk/deployment/manager.py +85 -71
  131. ksadk/deployment/providers/serverless.py +379 -210
  132. ksadk/deployment/registry.py +38 -32
  133. ksadk/deployment/state.py +11 -11
  134. ksadk/deployment/ui_config.py +1 -1
  135. ksadk/detection/__init__.py +1 -1
  136. ksadk/detection/detector.py +146 -83
  137. ksadk/detection/mcp_detector.py +9 -6
  138. ksadk/evaluation/__init__.py +84 -0
  139. ksadk/evaluation/a2a_adapter.py +444 -0
  140. ksadk/evaluation/adapters.py +54 -0
  141. ksadk/evaluation/contracts.py +409 -0
  142. ksadk/evaluation/evalset.py +351 -0
  143. ksadk/evaluation/evaluators.py +461 -0
  144. ksadk/evaluation/executor.py +118 -0
  145. ksadk/evaluation/storage.py +68 -0
  146. ksadk/evaluation/target.py +73 -0
  147. ksadk/events/__init__.py +25 -0
  148. ksadk/events/parser.py +191 -0
  149. ksadk/events/replay.py +37 -0
  150. ksadk/events/runtime_event.py +273 -0
  151. ksadk/events/store.py +317 -0
  152. ksadk/harness/__init__.py +35 -0
  153. ksadk/harness/app.py +269 -0
  154. ksadk/harness/config.py +181 -0
  155. ksadk/harness/reasoner.py +105 -0
  156. ksadk/harness/runtime.py +364 -0
  157. ksadk/harness/sandbox.py +141 -0
  158. ksadk/harness/tools.py +222 -0
  159. ksadk/hermes_terminal.py +28 -12
  160. ksadk/identity/resolver.py +15 -9
  161. ksadk/ids.py +32 -0
  162. ksadk/knowledge_base/adk_tool.py +1 -1
  163. ksadk/knowledge_base/client.py +38 -33
  164. ksadk/managed_a2a_card.py +160 -0
  165. ksadk/managed_runtime.py +195 -0
  166. ksadk/markdown.py +5 -2
  167. ksadk/mcp_runtime/__init__.py +9 -6
  168. ksadk/memory/__init__.py +1 -1
  169. ksadk/memory/adk/__init__.py +2 -0
  170. ksadk/memory/adk/backends/base_ltm_backend.py +2 -6
  171. ksadk/memory/adk/backends/http_ltm_backend.py +13 -21
  172. ksadk/memory/adk/backends/inmemory_ltm_backend.py +7 -13
  173. ksadk/memory/adk/backends/sdk_ltm_backend.py +36 -36
  174. ksadk/memory/adk/long_term_memory.py +34 -37
  175. ksadk/memory/adk/resilient_session_service.py +4 -5
  176. ksadk/memory/adk/short_term_memory.py +12 -21
  177. ksadk/memory/adk_tool.py +3 -2
  178. ksadk/memory/backends/base.py +19 -19
  179. ksadk/memory/backends/memory.py +24 -25
  180. ksadk/memory/backends/redis.py +31 -30
  181. ksadk/memory/langchain_tool.py +2 -1
  182. ksadk/memory/manager.py +52 -47
  183. ksadk/memory/service.py +16 -6
  184. ksadk/memory/tool.py +4 -0
  185. ksadk/model_policy.py +15 -7
  186. ksadk/model_proxy/__init__.py +51 -0
  187. ksadk/model_proxy/bootstrap.py +80 -0
  188. ksadk/model_proxy/cache.py +165 -0
  189. ksadk/model_proxy/config.py +66 -0
  190. ksadk/model_proxy/demo_webui.py +115 -0
  191. ksadk/model_proxy/detect.py +145 -0
  192. ksadk/model_proxy/gate.py +68 -0
  193. ksadk/model_proxy/namespace.py +201 -0
  194. ksadk/model_proxy/protocol_proxy.py +22 -0
  195. ksadk/model_proxy/server.py +443 -0
  196. ksadk/model_proxy/transform.py +735 -0
  197. ksadk/openclaw_gateway.py +29 -14
  198. ksadk/runners/adk_runner.py +354 -146
  199. ksadk/runners/base_runner.py +71 -29
  200. ksadk/runners/deepagents_runner.py +0 -1
  201. ksadk/runners/factory.py +18 -5
  202. ksadk/runners/langchain_runner.py +29 -742
  203. ksadk/runners/langgraph_runner.py +417 -97
  204. ksadk/runners/patch_langchain.py +16 -16
  205. ksadk/runners/remote_runner.py +134 -33
  206. ksadk/runners/usage_accumulator.py +1 -0
  207. ksadk/runners/utils/__init__.py +0 -10
  208. ksadk/runners/utils/loader.py +23 -7
  209. ksadk/runtime/__init__.py +67 -0
  210. ksadk/runtime/adapter.py +412 -0
  211. ksadk/runtime/conversation_execution.py +553 -0
  212. ksadk/runtime/executor.py +251 -0
  213. ksadk/runtime/factory.py +156 -0
  214. ksadk/runtime/framework_adapters.py +118 -0
  215. ksadk/runtime/launch.py +48 -0
  216. ksadk/runtime/preprocessing.py +131 -0
  217. ksadk/runtime/runner_adapter.py +1032 -0
  218. ksadk/runtime/runner_loading.py +36 -0
  219. ksadk/runtime_context.py +18 -3
  220. ksadk/runtime_state.py +0 -1
  221. ksadk/sandbox/__init__.py +4 -1
  222. ksadk/sandbox/backends/__init__.py +4 -1
  223. ksadk/sandbox/backends/e2b.py +13 -5
  224. ksadk/sandbox/backends/local_process.py +11 -3
  225. ksadk/sandbox/base.py +10 -15
  226. ksadk/sandbox/factory.py +14 -4
  227. ksadk/sandbox/registry.py +57 -6
  228. ksadk/server/__init__.py +39 -4
  229. ksadk/server/api_models.py +15 -5
  230. ksadk/server/app.py +24 -3928
  231. ksadk/server/composition.py +47 -0
  232. ksadk/server/factory.py +622 -0
  233. ksadk/server/routes/__init__.py +1 -0
  234. ksadk/server/routes/checkpoint_resolution.py +116 -0
  235. ksadk/server/routes/common.py +682 -0
  236. ksadk/server/routes/control.py +331 -0
  237. ksadk/server/routes/dependencies.py +98 -0
  238. ksadk/server/routes/misc.py +425 -0
  239. ksadk/server/routes/models.py +451 -0
  240. ksadk/server/routes/openai_compat.py +229 -0
  241. ksadk/server/routes/projection.py +848 -0
  242. ksadk/server/routes/routers.py +52 -0
  243. ksadk/server/routes/run.py +519 -0
  244. ksadk/server/routes/sessions.py +644 -0
  245. ksadk/server/routes/streaming.py +265 -0
  246. ksadk/server/routes/workspace.py +414 -0
  247. ksadk/server/static/assets/{ArtifactsPanel-D-SSn2ee.js → ArtifactsPanel-CakCqxEN.js} +1 -1
  248. ksadk/server/static/assets/CodeBlock-CynLknyO.js +9 -0
  249. ksadk/server/static/assets/MathMessageMarkdown-3sQdOysS.js +8 -0
  250. ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +303 -0
  251. ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +1 -0
  252. ksadk/server/static/assets/{arc-BA13sHc9.js → arc-DrgjcKhl.js} +1 -1
  253. ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +1 -0
  254. ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-DzJzgVoL.js → architectureDiagram-3BPJPVTR-BXoVRbQ6.js} +1 -1
  255. ksadk/server/static/assets/{blockDiagram-GPEHLZMM-UTdVcZhn.js → blockDiagram-GPEHLZMM-BnaB3Ywv.js} +1 -1
  256. ksadk/server/static/assets/{c4Diagram-AAUBKEIU-Dix0GD4m.js → c4Diagram-AAUBKEIU-Dw4weZcp.js} +1 -1
  257. ksadk/server/static/assets/channel-Dt3rQTFI.js +1 -0
  258. ksadk/server/static/assets/{chunk-2J33WTMH-C2JxuZ42.js → chunk-2J33WTMH-ChjpLK7j.js} +1 -1
  259. ksadk/server/static/assets/{chunk-4BX2VUAB-Be57QPgD.js → chunk-4BX2VUAB-B9drTgOW.js} +1 -1
  260. ksadk/server/static/assets/{chunk-55IACEB6-WjSuy4nv.js → chunk-55IACEB6-CK4ROATy.js} +1 -1
  261. ksadk/server/static/assets/{chunk-727SXJPM-BOovP-b5.js → chunk-727SXJPM-B9KtTTgw.js} +1 -1
  262. ksadk/server/static/assets/{chunk-AQP2D5EJ-DOmZNmWj.js → chunk-AQP2D5EJ-1O1mki3m.js} +1 -1
  263. ksadk/server/static/assets/{chunk-FMBD7UC4-DzwlodkU.js → chunk-FMBD7UC4-B_fH2YuH.js} +1 -1
  264. ksadk/server/static/assets/{chunk-ND2GUHAM-DMqAikEU.js → chunk-ND2GUHAM-D6rFLaVV.js} +1 -1
  265. ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +1 -0
  266. ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +1 -0
  267. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +1 -0
  268. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-Zhy2GBnP.js → cose-bilkent-S5V4N54A-ChNs_Mdk.js} +1 -1
  269. ksadk/server/static/assets/{dagre-BM42HDAG-CbKrtIUz.js → dagre-BM42HDAG-D0QXg_MQ.js} +1 -1
  270. ksadk/server/static/assets/{diagram-2AECGRRQ-BRApgQRK.js → diagram-2AECGRRQ-DBDDKwBQ.js} +1 -1
  271. ksadk/server/static/assets/{diagram-5GNKFQAL-D0xbnDiK.js → diagram-5GNKFQAL-BkwDOxmO.js} +1 -1
  272. ksadk/server/static/assets/{diagram-KO2AKTUF-CVugVI9L.js → diagram-KO2AKTUF-Bv8waYvY.js} +1 -1
  273. ksadk/server/static/assets/{diagram-LMA3HP47-DozUGgee.js → diagram-LMA3HP47-e1VECDdu.js} +1 -1
  274. ksadk/server/static/assets/{diagram-OG6HWLK6-ZOYsKlru.js → diagram-OG6HWLK6-Ds9adfRl.js} +1 -1
  275. ksadk/server/static/assets/{dist-DpKtt4Bo.js → dist-4N2NMVYg.js} +1 -1
  276. ksadk/server/static/assets/{dist-CkEKJFtk.js → dist-B-jNR6Vn.js} +1 -1
  277. ksadk/server/static/assets/{dist-D57wnGWd.js → dist-B5J2uhNk.js} +1 -1
  278. ksadk/server/static/assets/{dist-8_SfK1IP.js → dist-B5oS1zcj.js} +1 -1
  279. ksadk/server/static/assets/{dist-DMezvBGJ.js → dist-BB3-OwTQ.js} +1 -1
  280. ksadk/server/static/assets/{dist-2hInLS_J.js → dist-BIlKoW0k.js} +1 -1
  281. ksadk/server/static/assets/{dist-BrCcQ47T.js → dist-BjOdlDjj.js} +1 -1
  282. ksadk/server/static/assets/{dist-BmlMPsCc.js → dist-C1sz5Eo3.js} +1 -1
  283. ksadk/server/static/assets/{dist-B0NP_Z0z.js → dist-CWV4lDr1.js} +1 -1
  284. ksadk/server/static/assets/{dist-D-xOD048.js → dist-CmZHzlwZ.js} +1 -1
  285. ksadk/server/static/assets/{dist-DL_NKc_z.js → dist-D6MpCZtz.js} +1 -1
  286. ksadk/server/static/assets/{dist-DyOkZmze.js → dist-DJN_6Y39.js} +1 -1
  287. ksadk/server/static/assets/{dist-DmFaq_Iy.js → dist-DQU9EWw-.js} +1 -1
  288. ksadk/server/static/assets/{dist-gX6GOLKq.js → dist-DWKByxWV.js} +1 -1
  289. ksadk/server/static/assets/{dist-DyWsb78Q.js → dist-Dli7qW-B.js} +1 -1
  290. ksadk/server/static/assets/{dist-BIJVDMuB.js → dist-DsHPdk3W.js} +1 -1
  291. ksadk/server/static/assets/{dist-Dp0k7bWI.js → dist-X4TnqwOK.js} +1 -1
  292. ksadk/server/static/assets/{dist-8EEyGgDx.js → dist-_pHPTyZ-.js} +1 -1
  293. ksadk/server/static/assets/{dist-CPBtqRTW.js → dist-fhOuWds5.js} +1 -1
  294. ksadk/server/static/assets/{dist-D-JMdWPQ.js → dist-gLnrGsoU.js} +1 -1
  295. ksadk/server/static/assets/{dist-BssVmjIo.js → dist-j6Dxmf_j.js} +1 -1
  296. ksadk/server/static/assets/{dist-C3HIIaN5.js → dist-o2OVmKvt.js} +1 -1
  297. ksadk/server/static/assets/{erDiagram-TEJ5UH35-Bk-9bgE5.js → erDiagram-TEJ5UH35-BPItFKH6.js} +1 -1
  298. ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +1 -0
  299. ksadk/server/static/assets/{flowDiagram-I6XJVG4X-Dn9kZVuZ.js → flowDiagram-I6XJVG4X-BbYB9w5Y.js} +1 -1
  300. ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BGxXlJqm.js → ganttDiagram-6RSMTGT7-TQXa0KXv.js} +1 -1
  301. ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +1 -0
  302. ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-C7Q0X-wi.js → gitGraphDiagram-PVQCEYII-bKblCnpa.js} +1 -1
  303. ksadk/server/static/assets/index-R52pJJvg.js +177 -0
  304. ksadk/server/static/assets/index-jJr3_955.css +2 -0
  305. ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +1 -0
  306. ksadk/server/static/assets/{infoDiagram-5YYISTIA-BO_CSqq5.js → infoDiagram-5YYISTIA-BGVT7J_i.js} +1 -1
  307. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-eYLPLd46.js → ishikawaDiagram-YF4QCWOH-K0OOfVna.js} +1 -1
  308. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-De9geSb0.js → journeyDiagram-JHISSGLW-8LeOJaZp.js} +1 -1
  309. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-Cs80kQto.js → kanban-definition-UN3LZRKU-C6uCgZrW.js} +1 -1
  310. ksadk/server/static/assets/{katex-DSrCFD8n.js → katex-CYWseY5E.js} +1 -1
  311. ksadk/server/static/assets/{linear-CN9sbKTF.js → linear-hdBcYzM8.js} +1 -1
  312. ksadk/server/static/assets/{mermaid-parser.core-DlgSBU8l.js → mermaid-parser.core-BWrDjSZM.js} +2 -2
  313. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-9afz7Oug.js → mindmap-definition-RKZ34NQL-Bjy_2v4_.js} +1 -1
  314. ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +1 -0
  315. ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +1 -0
  316. ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPv_VstN.js → pieDiagram-4H26LBE5-GmmqNiVH.js} +1 -1
  317. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-o77wZ1XK.js → quadrantDiagram-W4KKPZXB-BmkRbwCg.js} +1 -1
  318. ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +1 -0
  319. ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-BYVw81VC.js → requirementDiagram-4Y6WPE33-DSWFqL7c.js} +1 -1
  320. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-B7nZFMKT.js → sankeyDiagram-5OEKKPKP-l6iXmU65.js} +1 -1
  321. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-BGRRrFw5.js → sequenceDiagram-3UESZ5HK-Cq6UBeQB.js} +1 -1
  322. ksadk/server/static/assets/{stateDiagram-AJRCARHV-BUfHCoPs.js → stateDiagram-AJRCARHV-qzTQpg3Y.js} +1 -1
  323. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +1 -0
  324. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-FbCTGrf9.js → timeline-definition-PNZ67QCA-DEz24vV0.js} +1 -1
  325. ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +1 -0
  326. ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +1 -0
  327. ksadk/server/static/assets/{vennDiagram-CIIHVFJN-DWgwtPsJ.js → vennDiagram-CIIHVFJN-CMCixki2.js} +1 -1
  328. ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +1 -0
  329. ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-2x8G-tTM.js → wardleyDiagram-YWT4CUSO-CVzB8F9K.js} +1 -1
  330. ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-Drma3AYl.js → xychartDiagram-2RQKCTM6-BFBqBLc1.js} +1 -1
  331. ksadk/server/static/index.html +2 -2
  332. ksadk/server/terminal_sessions.py +111 -58
  333. ksadk/sessions/__init__.py +69 -19
  334. ksadk/sessions/base.py +31 -11
  335. ksadk/sessions/continuity.py +2 -3
  336. ksadk/sessions/in_memory.py +46 -2
  337. ksadk/sessions/local_service.py +148 -17
  338. ksadk/sessions/postgres_service.py +143 -25
  339. ksadk/sessions/resilient.py +36 -8
  340. ksadk/skills/models.py +17 -5
  341. ksadk/skills/package_store.py +5 -2
  342. ksadk/skills/runtime/agent.py +20 -5
  343. ksadk/skills/runtime/artifacts.py +1 -5
  344. ksadk/skills/runtime/backends/e2b.py +10 -4
  345. ksadk/skills/runtime/backends/local.py +15 -3
  346. ksadk/skills/runtime/base.py +1 -2
  347. ksadk/skills/runtime/executor.py +10 -3
  348. ksadk/skills/runtime/factory.py +4 -2
  349. ksadk/skills/runtime/loader.py +1 -2
  350. ksadk/skills/runtime/registry.py +15 -10
  351. ksadk/skills/runtime/request.py +3 -1
  352. ksadk/skills/service_client.py +47 -16
  353. ksadk/skills/tool_defs.py +15 -10
  354. ksadk/studio/__init__.py +5 -0
  355. ksadk/studio/agent_avatar_assets.py +120 -0
  356. ksadk/studio/agent_lifecycle.py +51 -0
  357. ksadk/studio/api.py +1046 -0
  358. ksadk/studio/api_catalog_routes.py +324 -0
  359. ksadk/studio/api_contracts.py +177 -0
  360. ksadk/studio/api_helpers.py +132 -0
  361. ksadk/studio/authoring.py +524 -0
  362. ksadk/studio/authoring_coordinator.py +285 -0
  363. ksadk/studio/builder.py +320 -0
  364. ksadk/studio/capabilities.py +351 -0
  365. ksadk/studio/cloud.py +278 -0
  366. ksadk/studio/codex_agent_service.py +583 -0
  367. ksadk/studio/codex_builder.py +324 -0
  368. ksadk/studio/codex_manifest.py +278 -0
  369. ksadk/studio/codex_run.py +352 -0
  370. ksadk/studio/compiler.py +189 -0
  371. ksadk/studio/contracts.py +644 -0
  372. ksadk/studio/errors.py +47 -0
  373. ksadk/studio/evaluation.py +192 -0
  374. ksadk/studio/event_store.py +284 -0
  375. ksadk/studio/framework_run.py +114 -0
  376. ksadk/studio/identifiers.py +33 -0
  377. ksadk/studio/mcp_runtime.py +229 -0
  378. ksadk/studio/model_client.py +543 -0
  379. ksadk/studio/model_profile_service.py +214 -0
  380. ksadk/studio/operations.py +243 -0
  381. ksadk/studio/otel_trace.py +754 -0
  382. ksadk/studio/pagination.py +182 -0
  383. ksadk/studio/python_tool_inspection.py +183 -0
  384. ksadk/studio/repository.py +306 -0
  385. ksadk/studio/resource_catalog.py +1217 -0
  386. ksadk/studio/run_service.py +707 -0
  387. ksadk/studio/runtime_catalog.py +74 -0
  388. ksadk/studio/runtime_source.py +339 -0
  389. ksadk/studio/service.py +1141 -0
  390. ksadk/studio/shared_web.py +994 -0
  391. ksadk/studio/skill_discovery.py +628 -0
  392. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
  393. ksadk/studio/static/assets/index-CaEN0eTR.css +1 -0
  394. ksadk/studio/static/assets/index-DI2eY_NH.js +252 -0
  395. ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
  396. ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  397. ksadk/studio/static/index.html +15 -0
  398. ksadk/studio/templates.py +630 -0
  399. ksadk/studio/validator.py +246 -0
  400. ksadk/studio/workspace.py +137 -0
  401. ksadk/terminal_exec_policy.py +3 -4
  402. ksadk/tools/gateway.py +101 -15
  403. ksadk/tools/result_budget.py +12 -4
  404. ksadk/toolsets/__init__.py +161 -37
  405. ksadk/toolsets/a2a.py +426 -0
  406. ksadk/toolsets/platform.py +21 -5
  407. ksadk/toolsets/sandbox.py +82 -16
  408. ksadk/toolsets/skills.py +200 -39
  409. ksadk/toolsets/web.py +123 -26
  410. ksadk/toolsets/workspace.py +212 -101
  411. ksadk/tracing/__init__.py +19 -21
  412. ksadk/tracing/exporters/inmemory_exporter.py +62 -51
  413. ksadk/tracing/setup.py +135 -506
  414. ksadk/tracing/span_utils.py +0 -41
  415. ksadk/tui/loop.py +78 -30
  416. ksadk/tui/stream_render.py +1 -0
  417. ksadk/ui_config.py +5 -3
  418. ksadk/version.py +1 -1
  419. ksadk_runtime_common/memory_backend/registry.py +56 -56
  420. ksadk_runtime_common/schemas/runtime_event_v1.json +95 -0
  421. ksadk_runtime_common/workspace_files/router.py +8 -2
  422. agentengine_sdk_python-0.7.0.dist-info/RECORD +0 -482
  423. ksadk/a2a/card_builder.py +0 -64
  424. ksadk/a2a/client.py +0 -309
  425. ksadk/runners/utils/langfuse.py +0 -230
  426. ksadk/server/static/assets/CodeBlock-P16VOLYi.js +0 -9
  427. ksadk/server/static/assets/MathMessageMarkdown-CdGiQWHR.js +0 -8
  428. ksadk/server/static/assets/MermaidBlock-IOcal8cm.js +0 -303
  429. ksadk/server/static/assets/NativeTerminalPanel-DGSDWsei.js +0 -1
  430. ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +0 -1
  431. ksadk/server/static/assets/channel-CTuydARX.js +0 -1
  432. ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +0 -1
  433. ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +0 -1
  434. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +0 -1
  435. ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +0 -1
  436. ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +0 -1
  437. ksadk/server/static/assets/index-C00Lwg3L.js +0 -165
  438. ksadk/server/static/assets/index-DAURbCsP.css +0 -1
  439. ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +0 -1
  440. ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +0 -1
  441. ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +0 -1
  442. ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +0 -1
  443. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +0 -1
  444. ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +0 -1
  445. ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +0 -1
  446. ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +0 -1
  447. ksadk/tracing/exporters/langfuse_exporter.py +0 -445
  448. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/entry_points.txt +0 -0
  449. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/licenses/LICENSE +0 -0
  450. {agentengine_sdk_python-0.7.0.dist-info → agentengine_sdk_python-0.8.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1032 @@
1
+ """RunnerRuntimeAdapter — 把现有 ``BaseRunner`` 对齐到 G0.3 ``RuntimeAdapter`` (goal-07)。
2
+
3
+ 不推倒现有 runner(ADK/LangGraph 的 stream/invoke 已在跑),在它们之上实现平台六动词。
4
+ **cancel 不再是空壳**:实现冻结的 cancel 状态机——活跃 turn interrupt(关闭流)/
5
+ 无活跃 turn 记 pending / **级联丢弃 pending 工具审批** / 返回 :class:`CancelResult`。
6
+
7
+ 框架差异(resume 目标、checkpoint 粒度)由子类钩子 ``_resume_native`` 与
8
+ ``_checkpoint_capability`` 诚实声明(ADK forward-only vs LangGraph time-travel)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import copy
15
+ import inspect
16
+ import json
17
+ import logging
18
+ from collections.abc import Mapping
19
+ from contextlib import nullcontext
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, AsyncIterator, Dict, Optional, cast
22
+
23
+ from ksadk.conversations.runtime_input import _runner_name
24
+ from ksadk.conversations.runtime_observability import (
25
+ _conversation_span_scope,
26
+ _set_conversation_input_attributes,
27
+ _set_conversation_output_attributes,
28
+ _set_conversation_span_attributes,
29
+ _set_conversation_usage_attributes,
30
+ )
31
+ from ksadk.events.runtime_event import EventType, RuntimeEvent
32
+ from ksadk.runners.base_runner import BaseRunner
33
+ from ksadk.runtime.adapter import (
34
+ RESUME_START_REQUEST_NATIVE_KEY,
35
+ BaseRuntime,
36
+ CancelResult,
37
+ CheckpointCapability,
38
+ CheckpointDescriptor,
39
+ ResumePayload,
40
+ ResumeTarget,
41
+ RunHandle,
42
+ RuntimeAdapter,
43
+ StartRequest,
44
+ )
45
+ from ksadk.runtime.preprocessing import PreparedRuntimeStart, prepare_runtime_start
46
+ from ksadk.runtime.runner_loading import ensure_runner_loaded
47
+ from ksadk.runtime_context import platform_invocation_scope
48
+ from ksadk.tools.gateway import approval_interrupt_info_from_result
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+ _STREAM_STOP = object()
53
+ _ResumeKey = tuple[str, str, str, str, str]
54
+
55
+
56
+ async def _anext_or_stop(gen: AsyncIterator[Any]) -> Any:
57
+ """取下一个 chunk;流结束返回 _STREAM_STOP sentinel(便于竞速)。"""
58
+ try:
59
+ return await gen.__anext__()
60
+ except StopAsyncIteration:
61
+ return _STREAM_STOP
62
+
63
+
64
+ def _a2ui_surface_event(chunk: Any) -> tuple[str, dict[str, Any]] | None:
65
+ """Recognize a validated A2UI tool envelope and make it a first-class event.
66
+
67
+ The dynamic ``generate_a2ui`` tool returns official v0.9 operations as a
68
+ JSON tool result. Tool results are otherwise opaque to the runtime, which
69
+ would leave AG-UI with nothing to project until a page reload reconstructs
70
+ history. Convert exactly that envelope at the runtime boundary so it is
71
+ streamed, persisted, and replayed like every other A2UI surface.
72
+ """
73
+
74
+ if not isinstance(chunk, dict):
75
+ return None
76
+ value = chunk.get("tool_output", chunk.get("output"))
77
+ if value is not None and hasattr(value, "content"):
78
+ value = value.content
79
+ if isinstance(value, str):
80
+ try:
81
+ value = json.loads(value)
82
+ except (TypeError, ValueError):
83
+ return None
84
+ if not isinstance(value, Mapping):
85
+ return None
86
+ operations_raw = value.get("a2ui_operations")
87
+ if not isinstance(operations_raw, list) or not operations_raw:
88
+ return None
89
+ operations = [dict(operation) for operation in operations_raw if isinstance(operation, Mapping)]
90
+ if not operations:
91
+ return None
92
+
93
+ known: list[tuple[str, str]] = []
94
+ for operation in operations:
95
+ for key, event_type in (
96
+ ("createSurface", EventType.A2UI_SURFACE_BEGIN),
97
+ ("updateComponents", EventType.A2UI_SURFACE_UPDATE),
98
+ ("updateDataModel", EventType.A2UI_SURFACE_UPDATE),
99
+ ("deleteSurface", EventType.A2UI_SURFACE_END),
100
+ ):
101
+ detail = operation.get(key)
102
+ if isinstance(detail, Mapping) and isinstance(detail.get("surfaceId"), str):
103
+ surface_id = detail["surfaceId"].strip()
104
+ if surface_id:
105
+ known.append((surface_id, event_type))
106
+ break
107
+ if not known:
108
+ return None
109
+ surface_ids = {surface_id for surface_id, _event_type in known}
110
+ if len(surface_ids) != 1:
111
+ logger.warning("ignoring A2UI tool result with multiple surfaces")
112
+ return None
113
+ surface_id = known[0][0]
114
+ event_type = (
115
+ EventType.A2UI_SURFACE_BEGIN
116
+ if any(kind == EventType.A2UI_SURFACE_BEGIN for _surface_id, kind in known)
117
+ else known[0][1]
118
+ )
119
+ return event_type, {"surface_id": surface_id, "operations": operations}
120
+
121
+
122
+ def _coerce_literal(value: Any, allowed: tuple[str, ...], default: str) -> Any:
123
+ """把 value 收窄到 allowed 之一(不在则回退 default),供 Literal 字段使用。"""
124
+ return value if value in allowed else default
125
+
126
+
127
+ class _RunnerAsBaseRuntime(BaseRuntime):
128
+ """把 ``BaseRunner`` 包装为 G0.3 ``BaseRuntime``(原生能力面)。"""
129
+
130
+ def __init__(self, runner: BaseRunner, runtime_type: str) -> None:
131
+ self._runner = runner
132
+ self.runtime_type = runtime_type
133
+
134
+ @property
135
+ def runner(self) -> BaseRunner:
136
+ return self._runner
137
+
138
+ def native_capabilities(self) -> dict[str, Any]:
139
+ caps = getattr(self._runner, "get_runtime_capabilities", None)
140
+ if callable(caps):
141
+ try:
142
+ return dict(caps())
143
+ except Exception: # noqa: BLE001
144
+ pass
145
+ return {"Framework": self.runtime_type}
146
+
147
+
148
+ @dataclass
149
+ class _ActiveRun:
150
+ """一次进行中的 run(adapter 用于 cancel 追踪)。"""
151
+
152
+ invocation_id: str
153
+ session_id: str
154
+ stream: Optional[AsyncIterator[RuntimeEvent]] = None
155
+ pending_approvals: set[str] = field(default_factory=set)
156
+ interrupt_event: asyncio.Event = field(default_factory=asyncio.Event)
157
+ cancellation_ack: asyncio.Event = field(default_factory=asyncio.Event)
158
+ chunk_task: Optional[asyncio.Task[Any]] = None
159
+ resume_key: Optional[_ResumeKey] = None
160
+ resume_fingerprint: Any = None
161
+ skip_runner: bool = False
162
+ done: bool = False
163
+ completion_metrics: dict[str, Any] = field(default_factory=dict)
164
+
165
+
166
+ class RunnerRuntimeAdapter(RuntimeAdapter):
167
+ """把 ``BaseRunner`` 映射为平台六动词的通用 adapter。"""
168
+
169
+ def __init__(self, runner: BaseRunner, *, runtime_type: str) -> None:
170
+ super().__init__(_RunnerAsBaseRuntime(runner, runtime_type))
171
+ self._runner = runner
172
+ self._runtime_type = runtime_type
173
+ self._active_runs: dict[str, _ActiveRun] = {}
174
+ self._known_runs: set[str] = set()
175
+ self._run_sessions: dict[str, str] = {}
176
+ self._pending_cancels: set[str] = set()
177
+ self._resume_decisions: dict[_ResumeKey, Any] = {}
178
+ self._consumed_resumes: set[_ResumeKey] = set()
179
+ self._seq = 0
180
+ # 可观测:最近一次 cancel 级联丢弃的 pending 审批集(contract test 断言用)。
181
+ self.last_cancel_dropped_approvals: set[str] = set()
182
+
183
+ # ---- 框架钩子(子类按需 override) ----
184
+
185
+ def _checkpoint_capability(self) -> CheckpointCapability:
186
+ """诚实暴露 checkpoint 粒度。默认读 runner.describe_checkpoint_capability。"""
187
+ raw = {}
188
+ describe = getattr(self._runner, "describe_checkpoint_capability", None)
189
+ if callable(describe):
190
+ try:
191
+ raw = dict(describe())
192
+ except Exception: # noqa: BLE001
193
+ raw = {}
194
+ supported = bool(raw.get("Supported"))
195
+ granularity = _coerce_literal(
196
+ raw.get("Granularity"),
197
+ ("delta", "snapshot", "none"),
198
+ "snapshot" if supported else "none",
199
+ )
200
+ rollback_scope = _coerce_literal(
201
+ raw.get("RollbackScope"),
202
+ ("turn", "invocation", "none"),
203
+ "invocation" if supported else "none",
204
+ )
205
+ return CheckpointCapability(
206
+ supported=supported,
207
+ granularity=granularity,
208
+ rollback_scope=rollback_scope,
209
+ fork_supported=bool(raw.get("ForkSupported", False)),
210
+ durable=bool(raw.get("Durable", False)),
211
+ shared_across_pods=bool(raw.get("SharedAcrossPods", False)),
212
+ reason=str(raw.get("Reason") or ""),
213
+ )
214
+
215
+ async def _resume_native(
216
+ self,
217
+ handle: RunHandle,
218
+ target: ResumeTarget,
219
+ payload: Optional[ResumePayload],
220
+ ) -> Optional[dict]:
221
+ """框架原生 resume:返回注入下一次 ``stream()`` 的 runner_input 覆盖。
222
+
223
+ 返回的 dict 会被 :meth:`_build_runner_input` 合并,以框架 runner 真正消费的
224
+ ``checkpoint_resume`` + ``framework_ref`` 形式驱动真实恢复(而非仅写
225
+ ``native_ref`` 摆设)。默认通用形式;ADK/LangGraph 子类覆盖为各自框架结构。
226
+ 返回 ``None`` 表示本框架无可注入的 resume 输入(退化为普通 stream)。
227
+ """
228
+ return {
229
+ "checkpoint_resume": True,
230
+ "run_id": target.id,
231
+ "framework_ref": {self._runtime_type: {f"{target.kind}": target.id}},
232
+ "input": payload.data if payload else None,
233
+ }
234
+
235
+ # ---- 六动词 ----
236
+
237
+ async def preflight(self) -> None:
238
+ """Load the framework agent before a streaming HTTP response commits."""
239
+
240
+ ensure_runner_loaded(self._runner, runtime_type=self._runtime_type)
241
+
242
+ async def start(self, request: StartRequest) -> RunHandle:
243
+ ensure_runner_loaded(self._runner, runtime_type=self._runtime_type)
244
+ run_id = str(request.metadata.get("invocation_id") or self._next_invocation_id())
245
+ prepared_start = await prepare_runtime_start(request, self._runner)
246
+ handle = RunHandle(
247
+ run_id=run_id,
248
+ session_id=request.session_id,
249
+ runtime_type=self._runtime_type,
250
+ native_ref={"user_id": request.user_id, "agent_id": request.agent_id},
251
+ )
252
+ self._known_runs.add(run_id)
253
+ self._run_sessions[run_id] = request.session_id
254
+ self._active_runs[run_id] = _ActiveRun(invocation_id=run_id, session_id=request.session_id)
255
+ # 暂存 start 输入,供 stream() 使用。
256
+ self._active_runs[run_id].__dict__["_start_request"] = request
257
+ if prepared_start is not None:
258
+ self._active_runs[run_id].__dict__["_prepared_start"] = prepared_start
259
+ return handle
260
+
261
+ def stream(self, handle: RunHandle) -> AsyncIterator[RuntimeEvent]:
262
+ return self._stream_events(handle)
263
+
264
+ def is_handle_attached(self, handle: RunHandle) -> bool:
265
+ return (
266
+ handle.runtime_type == self._runtime_type
267
+ and handle.run_id in self._known_runs
268
+ and self._run_sessions.get(handle.run_id) == handle.session_id
269
+ )
270
+
271
+ async def attach(self, handle: RunHandle) -> RunHandle:
272
+ """Restore a persisted handle through an explicit native runner seam.
273
+
274
+ Merely repopulating ``_known_runs`` would make validation pass without
275
+ restoring framework state. A runner therefore has to expose
276
+ ``attach_runtime_handle(handle)`` and prove that its durable backend can
277
+ resolve the referenced session/checkpoint.
278
+ """
279
+ if self.is_handle_attached(handle):
280
+ return handle
281
+ if handle.runtime_type != self._runtime_type:
282
+ raise ValueError(
283
+ f"run handle runtime {handle.runtime_type!r} does not match "
284
+ f"adapter {self._runtime_type!r}"
285
+ )
286
+ attach = getattr(self._runner, "attach_runtime_handle", None)
287
+ if not callable(attach):
288
+ raise RuntimeError(
289
+ f"runner for {self._runtime_type!r} has no durable "
290
+ "attach_runtime_handle capability"
291
+ )
292
+ restored = attach(handle)
293
+ if inspect.isawaitable(restored):
294
+ restored = await restored
295
+ if restored is False or restored is None:
296
+ raise RuntimeError(
297
+ f"runner could not restore persisted run {handle.run_id!r} "
298
+ f"for session {handle.session_id!r}"
299
+ )
300
+ if isinstance(restored, RunHandle) and restored != handle:
301
+ raise ValueError("native runner attached a different run handle")
302
+ self._known_runs.add(handle.run_id)
303
+ self._run_sessions[handle.run_id] = handle.session_id
304
+ return handle
305
+
306
+ async def cancel(self, handle: RunHandle) -> CancelResult:
307
+ run_id = handle.run_id
308
+ run = self._active_runs.get(run_id)
309
+ # 活跃 turn = 正在 streaming(run.stream 非 None 且未 done);
310
+ # start 过但未 streaming / 已结束 → 无活跃 turn(记 pending 或 not_running)。
311
+ is_active = run is not None and not run.done and run.stream is not None
312
+ if not is_active:
313
+ if run_id in self._known_runs:
314
+ # 无活跃 turn 但 invocation 已知 → 记 pending,下个 turn 消费。
315
+ self._pending_cancels.add(run_id)
316
+ return CancelResult.PENDING_CANCEL_RECORDED
317
+ return CancelResult.NOT_RUNNING
318
+ # 有活跃 turn:interrupt + 级联丢弃 pending 审批。
319
+ assert run is not None # is_active 蕴含 run 非 None
320
+ try:
321
+ await self._interrupt_active_run(run)
322
+ # 级联丢弃该 turn 的 pending 工具审批(先快照供观测)。
323
+ self.last_cancel_dropped_approvals = set(run.pending_approvals)
324
+ run.pending_approvals.clear()
325
+ run.done = True
326
+ self._active_runs.pop(run_id, None)
327
+ self._pending_cancels.discard(run_id)
328
+ return CancelResult.INTERRUPTED_ACTIVE_TURN
329
+ except Exception: # noqa: BLE001
330
+ logger.exception("cancel active run %s 失败", run_id)
331
+ return CancelResult.FAILED
332
+
333
+ async def resume(
334
+ self,
335
+ handle: RunHandle,
336
+ target: ResumeTarget,
337
+ payload: Optional[ResumePayload],
338
+ ) -> RunHandle:
339
+ if (
340
+ handle.runtime_type != self._runtime_type
341
+ or handle.run_id not in self._known_runs
342
+ or self._run_sessions.get(handle.run_id) != handle.session_id
343
+ ):
344
+ raise ValueError(f"unknown run handle for {self._runtime_type}: {handle.run_id!r}")
345
+ if handle.run_id in self._pending_cancels:
346
+ return handle
347
+ self._require_native_checkpoint_capability()
348
+ self._known_runs.add(handle.run_id)
349
+ prepared_start: PreparedRuntimeStart | None = None
350
+ raw_start_request = handle.native_ref.pop(RESUME_START_REQUEST_NATIVE_KEY, None)
351
+ if isinstance(raw_start_request, Mapping):
352
+ prepared_start = await prepare_runtime_start(
353
+ StartRequest.model_validate(raw_start_request), self._runner
354
+ )
355
+ # _resume_native 返回的 runner_input 覆盖存到 run 上,下一次 stream() 经
356
+ # _build_runner_input 消费,以框架原生方式真驱动恢复。
357
+ override = await self._resume_native(handle, target, payload)
358
+ resume_key = (
359
+ handle.run_id,
360
+ handle.session_id,
361
+ target.kind,
362
+ target.id,
363
+ str(payload.call_id or "") if payload is not None else "",
364
+ )
365
+ resume_fingerprint = (
366
+ (payload.kind, copy.deepcopy(payload.data)) if payload is not None else None
367
+ )
368
+ existing_fingerprint = self._resume_decisions.get(resume_key, _STREAM_STOP)
369
+ if existing_fingerprint is not _STREAM_STOP:
370
+ if existing_fingerprint != resume_fingerprint:
371
+ raise ValueError(f"conflicting resume for checkpoint {target.id!r}")
372
+ current = self._active_runs.get(handle.run_id)
373
+ if current is not None and not current.done and current.resume_key == resume_key:
374
+ return handle
375
+ if resume_key in self._consumed_resumes:
376
+ self._active_runs[handle.run_id] = _ActiveRun(
377
+ invocation_id=handle.run_id,
378
+ session_id=handle.session_id,
379
+ resume_key=resume_key,
380
+ resume_fingerprint=resume_fingerprint,
381
+ skip_runner=True,
382
+ )
383
+ return handle
384
+
385
+ current = self._active_runs.get(handle.run_id)
386
+ if current is not None and not current.done and current.resume_key is not None:
387
+ if current.resume_key == resume_key:
388
+ if current.resume_fingerprint == resume_fingerprint:
389
+ return handle
390
+ raise ValueError(f"conflicting resume for checkpoint {target.id!r}")
391
+ raise ValueError(f"run {handle.run_id!r} already has an active or pending resume")
392
+
393
+ run = _ActiveRun(
394
+ invocation_id=handle.run_id,
395
+ session_id=handle.session_id,
396
+ resume_key=resume_key,
397
+ resume_fingerprint=resume_fingerprint,
398
+ )
399
+ if prepared_start is not None:
400
+ run.__dict__["_prepared_start"] = prepared_start
401
+ if override:
402
+ run.__dict__["_resume_input_override"] = override
403
+ self._resume_decisions[resume_key] = resume_fingerprint
404
+ self._active_runs[handle.run_id] = run
405
+ return handle
406
+
407
+ async def checkpoint(self, handle: RunHandle) -> CheckpointDescriptor:
408
+ capability = self._require_native_checkpoint_capability()
409
+ checkpoint_id = str(handle.native_ref.get("checkpoint_id") or "").strip()
410
+ if not checkpoint_id:
411
+ raise RuntimeError(
412
+ f"{self._runtime_type} runner has no native checkpoint for run "
413
+ f"{handle.run_id!r}"
414
+ )
415
+ return CheckpointDescriptor(
416
+ checkpoint_id=checkpoint_id,
417
+ invocation_id=handle.run_id,
418
+ capability=capability,
419
+ ref=dict(handle.native_ref),
420
+ )
421
+
422
+ def _require_native_checkpoint_capability(self) -> CheckpointCapability:
423
+ """Fail closed when a runner cannot create or resume a native checkpoint."""
424
+ capability = self._checkpoint_capability()
425
+ if capability.supported:
426
+ return capability
427
+ detail = capability.reason or "runner does not expose framework checkpoints"
428
+ raise RuntimeError(
429
+ f"{self._runtime_type} native checkpoint capability is unavailable: {detail}"
430
+ )
431
+
432
+ async def close(self, handle: RunHandle) -> None:
433
+ run = self._active_runs.pop(handle.run_id, None)
434
+ if run is not None:
435
+ await self._interrupt_active_run(run)
436
+ run.done = True
437
+ resume_keys = {
438
+ key
439
+ for key in self._resume_decisions
440
+ if key[0] == handle.run_id and key[1] == handle.session_id
441
+ }
442
+ for key in resume_keys:
443
+ self._resume_decisions.pop(key, None)
444
+ self._consumed_resumes.difference_update(resume_keys)
445
+ self._pending_cancels.discard(handle.run_id)
446
+ self._known_runs.discard(handle.run_id)
447
+ self._run_sessions.pop(handle.run_id, None)
448
+ close = getattr(self._runner, "close", None)
449
+ if callable(close):
450
+ result = close()
451
+ if inspect.isawaitable(result):
452
+ await result
453
+
454
+ # ---- 内部:cancel 中断 ----
455
+
456
+ async def _interrupt_active_run(self, run: _ActiveRun) -> None:
457
+ """中断活跃 turn:置 interrupt 事件,由 stream 消费侧的竞速循环取消在途 chunk task。
458
+
459
+ 机制链(经实证,见 test_adapter_contract.test_cancel_active_turn_returns_interrupted):
460
+ ``interrupt_event.set()`` → :meth:`_map_runner_stream` 的 ``asyncio.wait`` 竞速命中 →
461
+ ``chunk_task.cancel()`` → ``CancelledError`` 传入 runner 生成器正在 in-flight 的
462
+ ``__anext__`` await(ADK ``run_async`` 的 LLM/tool 调用就在其中),从而真实打断执行,
463
+ 而非仅在事件间停止消费。
464
+
465
+ 前提与边界(诚实声明):ADK / LangGraph 均**不暴露原生 cancel 方法**(Runner 只有
466
+ run/run_async/close 等),因此 asyncio 任务取消是**唯一且正确**的中断机制。它要求
467
+ runner 的执行发生在其 stream async generator 的 ``__anext__`` 内(ADK/LG 流式即如此);
468
+ 若某 runner 把实际工作放到与 ``__anext__`` 解耦的后台 task,则不在本机制覆盖范围。
469
+ 不在此跨 task 对正被另一 task 迭代的 async generator 直接 aclose(不可靠)。
470
+ """
471
+ run.interrupt_event.set()
472
+ if run.stream is None:
473
+ run.cancellation_ack.set()
474
+ return
475
+ if run.chunk_task is not None:
476
+ run.chunk_task.cancel()
477
+ await asyncio.wait_for(run.cancellation_ack.wait(), timeout=2)
478
+
479
+ # ---- 内部:stream → RuntimeEvent ----
480
+
481
+ def _next_invocation_id(self) -> str:
482
+ self._seq += 1
483
+ return f"inv_{self._runtime_type}_{self._seq}"
484
+
485
+ def _next_seq(self) -> int:
486
+ self._seq += 1
487
+ return self._seq
488
+
489
+ async def _stream_events(self, handle: RunHandle) -> AsyncIterator[RuntimeEvent]:
490
+ run = self._active_runs.get(handle.run_id)
491
+ if run is None:
492
+ run = _ActiveRun(invocation_id=handle.run_id, session_id=handle.session_id)
493
+ self._active_runs[handle.run_id] = run
494
+
495
+ # 消费 pending cancel:start 时若已记 pending,立即中断该 turn。
496
+ if handle.run_id in self._pending_cancels:
497
+ self._pending_cancels.discard(handle.run_id)
498
+ yield self._event(
499
+ handle,
500
+ EventType.RUN_CANCELED,
501
+ {
502
+ "status": "cancelled",
503
+ "cancel_result": CancelResult.PENDING_CANCEL_RECORDED.value,
504
+ },
505
+ )
506
+ return
507
+
508
+ if run.skip_runner:
509
+ run.done = True
510
+ self._active_runs.pop(handle.run_id, None)
511
+ yield self._event(
512
+ handle,
513
+ EventType.RUN_COMPLETED,
514
+ self._completion_payload(handle, status="already_resumed"),
515
+ )
516
+ return
517
+
518
+ if run.resume_key is not None:
519
+ self._consumed_resumes.add(run.resume_key)
520
+
521
+ yield self._event(handle, EventType.RUN_STARTED, {"status": "in_progress"})
522
+
523
+ request = run.__dict__.get("_start_request")
524
+ runner_input = self._build_runner_input(handle, request)
525
+ gen: Optional[AsyncIterator[RuntimeEvent]] = None
526
+ terminal_event_seen = False
527
+ approval_interrupted = False
528
+ try:
529
+ gen = self._map_runner_stream(handle, runner_input)
530
+ run.stream = gen
531
+ async for event in gen:
532
+ yield event
533
+ if event.event_type == EventType.APPROVAL_REQUESTED:
534
+ approval_interrupted = True
535
+ if event.event_type in {
536
+ EventType.RUN_COMPLETED,
537
+ EventType.RUN_FAILED,
538
+ EventType.RUN_CANCELED,
539
+ EventType.RUN_INTERRUPTED,
540
+ }:
541
+ terminal_event_seen = True
542
+ if event.event_type in {EventType.RUN_FAILED, EventType.RUN_CANCELED}:
543
+ return
544
+
545
+ if run.interrupt_event.is_set() and not terminal_event_seen:
546
+ yield self._event(
547
+ handle,
548
+ EventType.RUN_CANCELED,
549
+ {
550
+ "status": "cancelled",
551
+ "cancel_result": CancelResult.INTERRUPTED_ACTIVE_TURN.value,
552
+ },
553
+ )
554
+ elif approval_interrupted and not terminal_event_seen:
555
+ yield self._event(
556
+ handle,
557
+ EventType.RUN_INTERRUPTED,
558
+ {"status": "input_required"},
559
+ )
560
+ elif not terminal_event_seen:
561
+ yield self._event(
562
+ handle,
563
+ EventType.RUN_COMPLETED,
564
+ self._completion_payload(
565
+ handle,
566
+ status="completed",
567
+ metrics=run.completion_metrics,
568
+ ),
569
+ )
570
+ finally:
571
+ run.stream = None
572
+ run.done = True
573
+ self._active_runs.pop(handle.run_id, None)
574
+
575
+ def _build_runner_input(self, handle: RunHandle, request: Optional[StartRequest]) -> dict:
576
+ # resume 覆盖优先:_resume_native 注入的 checkpoint_resume + framework_ref
577
+ # 直接作为 runner 输入,驱动框架原生恢复。
578
+ run = self._active_runs.get(handle.run_id)
579
+ override = run.__dict__.get("_resume_input_override") if run is not None else None
580
+ prepared_start = run.__dict__.get("_prepared_start") if run is not None else None
581
+ if override is not None:
582
+ merged = (
583
+ dict(prepared_start.runner_input)
584
+ if isinstance(prepared_start, PreparedRuntimeStart)
585
+ else {}
586
+ )
587
+ base_metadata = merged.get("metadata")
588
+ merged.update({
589
+ "input": override.get("input"),
590
+ "session_id": handle.session_id,
591
+ "invocation_id": handle.run_id,
592
+ "metadata": {
593
+ **(dict(base_metadata) if isinstance(base_metadata, Mapping) else {}),
594
+ **dict(override.get("metadata") or {}),
595
+ },
596
+ })
597
+ for key, value in override.items():
598
+ if key not in ("input", "metadata"):
599
+ merged[key] = value
600
+ return merged
601
+
602
+ if isinstance(prepared_start, PreparedRuntimeStart):
603
+ return dict(prepared_start.runner_input)
604
+
605
+ metadata: Dict[str, Any] = {}
606
+ request_config: Dict[str, Any] = {}
607
+ input_value: Any = ""
608
+ if request is not None:
609
+ input_value = request.input
610
+ metadata = dict(request.metadata or {})
611
+ request_config = dict(request.config or {})
612
+ return {
613
+ **request_config,
614
+ "input": input_value,
615
+ "session_id": handle.session_id,
616
+ "invocation_id": handle.run_id,
617
+ "metadata": metadata,
618
+ }
619
+
620
+ async def _map_runner_stream(
621
+ self, handle: RunHandle, runner_input: dict
622
+ ) -> AsyncIterator[RuntimeEvent]:
623
+ run = self._active_runs.get(handle.run_id)
624
+ interrupt = run.interrupt_event if run is not None else None
625
+ prepared_start = run.__dict__.get("_prepared_start") if run is not None else None
626
+ invocation_context = (
627
+ prepared_start.context if isinstance(prepared_start, PreparedRuntimeStart) else None
628
+ )
629
+ scope = (
630
+ platform_invocation_scope(invocation_context)
631
+ if invocation_context is not None
632
+ else nullcontext()
633
+ )
634
+ runner_name = _runner_name(self._runner)
635
+ accumulated_output = ""
636
+ usage: dict[str, Any] = {}
637
+ runner_gen: Optional[AsyncIterator[Any]] = None
638
+ async with _conversation_span_scope(runner_name) as span:
639
+ if isinstance(prepared_start, PreparedRuntimeStart):
640
+ _set_conversation_span_attributes(
641
+ span,
642
+ agent_id=str(handle.native_ref.get("agent_id") or "agent"),
643
+ user_id=str(handle.native_ref.get("user_id") or "user"),
644
+ session_id=handle.session_id,
645
+ invocation_id=handle.run_id,
646
+ runner_name=runner_name,
647
+ model=prepared_start.context.model,
648
+ response_id=prepared_start.response_id,
649
+ )
650
+ _set_conversation_input_attributes(span, prepared_start.input_text)
651
+ try:
652
+ with scope:
653
+ canonical_stream = getattr(self._runner, "stream_runtime_events", None)
654
+ stream_result = (
655
+ canonical_stream(runner_input)
656
+ if callable(canonical_stream)
657
+ else self._runner.stream(runner_input)
658
+ )
659
+ if inspect.iscoroutine(stream_result):
660
+ # runner.stream 若声明为 async def -> AsyncIterator(非 async generator),
661
+ # 调用返回 coroutine,需 await 得到迭代器。
662
+ stream_result = await stream_result
663
+ runner_gen = cast(AsyncIterator[Any], stream_result)
664
+ while True:
665
+ # 竞速:下一个 runner chunk vs cancel 中断事件。
666
+ chunk_task = asyncio.ensure_future(_anext_or_stop(runner_gen))
667
+ if run is not None:
668
+ run.chunk_task = chunk_task
669
+ wait_set = {chunk_task}
670
+ interrupt_task = (
671
+ asyncio.ensure_future(interrupt.wait())
672
+ if interrupt is not None
673
+ else None
674
+ )
675
+ if interrupt_task is not None:
676
+ wait_set.add(interrupt_task)
677
+ done, pending = await asyncio.wait(
678
+ wait_set, return_when=asyncio.FIRST_COMPLETED
679
+ )
680
+ for task in pending:
681
+ task.cancel()
682
+ if pending:
683
+ await asyncio.gather(*pending, return_exceptions=True)
684
+ if interrupt_task is not None and interrupt_task in done:
685
+ # cancel 中断:安全关闭 runner 流(同一 task)并停止。
686
+ chunk_task.cancel()
687
+ await asyncio.gather(chunk_task, return_exceptions=True)
688
+ return
689
+ try:
690
+ chunk = chunk_task.result()
691
+ except asyncio.CancelledError:
692
+ if interrupt is not None and interrupt.is_set():
693
+ return
694
+ raise
695
+ finally:
696
+ if run is not None:
697
+ run.chunk_task = None
698
+ if chunk is _STREAM_STOP:
699
+ return
700
+ if isinstance(chunk, RuntimeEvent):
701
+ if chunk.event_type in {
702
+ EventType.TEXT_DELTA,
703
+ EventType.TEXT_COMPLETED,
704
+ } and chunk.phase == "final_answer":
705
+ text = self._coerce(chunk.payload.get("text"))
706
+ if chunk.event_type == EventType.TEXT_COMPLETED:
707
+ accumulated_output = text
708
+ else:
709
+ accumulated_output += text
710
+ elif chunk.event_type == EventType.USAGE_REPORTED:
711
+ usage.update(chunk.payload)
712
+ if isinstance(chunk, dict):
713
+ chunk_type = str(chunk.get("type") or "")
714
+ if chunk_type == "final" and run is not None:
715
+ for source_key, target_key in (
716
+ ("duration_ms", "duration_ms"),
717
+ ("started_at", "started_at"),
718
+ ("completed_at", "completed_at"),
719
+ ("metrics_source", "source"),
720
+ ):
721
+ if chunk.get(source_key) is not None:
722
+ run.completion_metrics[target_key] = chunk[source_key]
723
+ if chunk_type in {"final", "text", "text_delta"}:
724
+ text = self._coerce(
725
+ chunk.get("delta") or chunk.get("output") or chunk.get("data")
726
+ )
727
+ if text:
728
+ if chunk_type == "final" or chunk.get("replace"):
729
+ accumulated_output = text
730
+ else:
731
+ accumulated_output += text
732
+ raw_usage = chunk.get("usage")
733
+ if isinstance(raw_usage, dict):
734
+ usage.update(raw_usage)
735
+ event = self._chunk_to_event(handle, run, chunk)
736
+ if event is not None:
737
+ yield event
738
+ a2ui_surface = _a2ui_surface_event(chunk)
739
+ if a2ui_surface is not None:
740
+ event_type, payload = a2ui_surface
741
+ yield self._event(handle, event_type, payload)
742
+ finally:
743
+ if accumulated_output:
744
+ _set_conversation_output_attributes(span, accumulated_output)
745
+ _set_conversation_usage_attributes(span, usage)
746
+ if runner_gen is not None:
747
+ aclose = getattr(runner_gen, "aclose", None)
748
+ if callable(aclose):
749
+ try:
750
+ await aclose()
751
+ except Exception: # noqa: BLE001
752
+ pass
753
+ if run is not None:
754
+ run.cancellation_ack.set()
755
+
756
+ def _chunk_to_event(
757
+ self, handle: RunHandle, run: Optional[_ActiveRun], chunk: Any
758
+ ) -> Optional[RuntimeEvent]:
759
+ if isinstance(chunk, RuntimeEvent):
760
+ # Outer adapter owns the public lifecycle envelope. A native
761
+ # Runtime may emit its own RUN_STARTED with private identifiers;
762
+ # suppress that duplicate and rebind all other canonical events
763
+ # to the public handle without flattening their payloads.
764
+ if chunk.event_type == EventType.RUN_STARTED:
765
+ return None
766
+ return RuntimeEvent.create(
767
+ chunk.event_type,
768
+ agent_id=str(handle.native_ref.get("agent_id") or "agent"),
769
+ user_id=str(handle.native_ref.get("user_id") or "user"),
770
+ session_id=handle.session_id,
771
+ invocation_id=handle.run_id,
772
+ seq_id=self._next_seq(),
773
+ phase=chunk.phase,
774
+ payload=dict(chunk.payload),
775
+ event_id=chunk.event_id,
776
+ timestamp=chunk.timestamp,
777
+ )
778
+ if not isinstance(chunk, dict):
779
+ return self._event(
780
+ handle, EventType.TEXT_DELTA, {"text": str(chunk)}, phase="commentary"
781
+ )
782
+ chunk_type = chunk.get("type")
783
+ if chunk_type in ("reasoning", "reasoning_delta", "thinking"):
784
+ text = self._coerce(
785
+ chunk.get("delta")
786
+ or chunk.get("content")
787
+ or chunk.get("output")
788
+ or chunk.get("data")
789
+ )
790
+ if not text:
791
+ return None
792
+ event_type = (
793
+ EventType.REASONING_COMPLETED
794
+ if chunk.get("status") in ("completed", "done")
795
+ else EventType.REASONING_DELTA
796
+ )
797
+ return self._event(
798
+ handle,
799
+ event_type,
800
+ {"text": text},
801
+ phase="commentary",
802
+ )
803
+ if chunk_type in ("tool_call", "tool_start"):
804
+ call_id = str(
805
+ chunk.get("tool_call_id")
806
+ or chunk.get("call_id")
807
+ or chunk.get("run_id")
808
+ or chunk.get("id")
809
+ or ""
810
+ )
811
+ name = str(chunk.get("tool_name") or chunk.get("name") or "tool")
812
+ return self._event(
813
+ handle,
814
+ EventType.TOOL_CALL_BEGIN,
815
+ {
816
+ "call_id": call_id or name,
817
+ "name": name,
818
+ "args": chunk.get("tool_args", chunk.get("args")),
819
+ },
820
+ )
821
+ if chunk_type in ("tool_result", "tool_end"):
822
+ call_id = str(
823
+ chunk.get("tool_call_id")
824
+ or chunk.get("call_id")
825
+ or chunk.get("run_id")
826
+ or chunk.get("id")
827
+ or ""
828
+ )
829
+ name = str(chunk.get("tool_name") or chunk.get("name") or "tool")
830
+ tool_args = chunk.get("tool_args", chunk.get("args"))
831
+ result = chunk.get("tool_output", chunk.get("output"))
832
+ approval_detail = approval_interrupt_info_from_result(
833
+ result,
834
+ fallback_tool_name=name,
835
+ tool_args=tool_args,
836
+ run_id=call_id or None,
837
+ )
838
+ if approval_detail is not None:
839
+ return self._approval_requested_event(
840
+ handle,
841
+ run,
842
+ detail=approval_detail,
843
+ call_id=call_id,
844
+ )
845
+ return self._event(
846
+ handle,
847
+ EventType.TOOL_CALL_END,
848
+ {
849
+ "call_id": call_id or name,
850
+ "name": name,
851
+ "result": result,
852
+ "error": chunk.get("error"),
853
+ },
854
+ )
855
+ if chunk_type in ("interrupt", "approval", "approval_required"):
856
+ raw_detail = chunk.get("interrupt_info") or chunk.get("detail") or {}
857
+ detail = dict(raw_detail) if isinstance(raw_detail, Mapping) else {}
858
+ call_id = str(
859
+ chunk.get("call_id")
860
+ or chunk.get("approval_id")
861
+ or chunk.get("id")
862
+ or ""
863
+ )
864
+ return self._approval_requested_event(
865
+ handle,
866
+ run,
867
+ detail=detail,
868
+ call_id=call_id,
869
+ )
870
+ if chunk_type == "checkpoint":
871
+ raw_metadata = chunk.get("metadata")
872
+ metadata: dict[str, Any] = raw_metadata if isinstance(raw_metadata, dict) else {}
873
+ raw_agentengine = metadata.get("agentengine")
874
+ agentengine: dict[str, Any] = (
875
+ raw_agentengine if isinstance(raw_agentengine, dict) else {}
876
+ )
877
+ framework = str(agentengine.get("framework") or self._runtime_type)
878
+ framework_ref = agentengine.get("framework_ref") or {}
879
+ runtime_ref = (
880
+ framework_ref.get(framework) if isinstance(framework_ref, dict) else {}
881
+ ) or {}
882
+ checkpoint_id = str(
883
+ runtime_ref.get("checkpoint_id") if isinstance(runtime_ref, dict) else ""
884
+ )
885
+ if not checkpoint_id:
886
+ return None
887
+ handle.native_ref["checkpoint_id"] = checkpoint_id
888
+ known_checkpoint_ids = handle.native_ref.setdefault("known_checkpoint_ids", [])
889
+ if checkpoint_id not in known_checkpoint_ids:
890
+ known_checkpoint_ids.append(checkpoint_id)
891
+ handle.native_ref["framework_ref"] = framework_ref
892
+ if isinstance(runtime_ref, dict):
893
+ handle.native_ref.update(runtime_ref)
894
+ return self._event(
895
+ handle,
896
+ EventType.CHECKPOINT_CREATED,
897
+ {
898
+ "checkpoint_id": checkpoint_id,
899
+ "granularity": self._checkpoint_capability().granularity,
900
+ "run_id": str(agentengine.get("run_id") or handle.run_id),
901
+ "framework": framework,
902
+ "framework_ref": framework_ref,
903
+ "resume_target": framework_ref,
904
+ },
905
+ )
906
+ if chunk_type == "graph_update":
907
+ return self._event(
908
+ handle,
909
+ EventType.RUN_PROGRESS,
910
+ {
911
+ "status": "in_progress",
912
+ "node": str(chunk.get("node") or ""),
913
+ "state_update": self._coerce(chunk.get("output")),
914
+ },
915
+ )
916
+ if chunk_type == "usage":
917
+ raw_usage = chunk.get("usage")
918
+ usage: dict[str, Any] = raw_usage if isinstance(raw_usage, dict) else {}
919
+ return self._event(
920
+ handle,
921
+ EventType.USAGE_REPORTED,
922
+ {
923
+ "input_tokens": int(usage.get("input_tokens") or 0),
924
+ "output_tokens": int(usage.get("output_tokens") or 0),
925
+ "total_tokens": int(usage.get("total_tokens") or 0),
926
+ "cached_tokens": int(usage.get("cached_tokens") or 0),
927
+ "reasoning_tokens": int(usage.get("reasoning_tokens") or 0),
928
+ "source": str(usage.get("source") or self._runtime_type),
929
+ },
930
+ )
931
+ if chunk_type == "error":
932
+ error = self._coerce(chunk.get("message") or chunk.get("error"))
933
+ return self._event(
934
+ handle,
935
+ EventType.RUN_FAILED,
936
+ {
937
+ "status": "failed",
938
+ "error": error or "runner failed",
939
+ },
940
+ )
941
+ if chunk_type == "final":
942
+ return self._event(
943
+ handle,
944
+ EventType.TEXT_COMPLETED,
945
+ {"text": self._coerce(chunk.get("output"))},
946
+ phase="final_answer",
947
+ )
948
+ text = self._coerce(chunk.get("delta") or chunk.get("output") or chunk.get("data"))
949
+ if not text:
950
+ return None
951
+ payload: dict[str, Any] = {"text": text}
952
+ if chunk.get("replace"):
953
+ payload["replace"] = True
954
+ return self._event(handle, EventType.TEXT_DELTA, payload, phase="commentary")
955
+
956
+ def _approval_requested_event(
957
+ self,
958
+ handle: RunHandle,
959
+ run: Optional[_ActiveRun],
960
+ *,
961
+ detail: Mapping[str, Any],
962
+ call_id: str,
963
+ ) -> RuntimeEvent:
964
+ """Convert one framework/tool approval to the canonical runtime event."""
965
+
966
+ approval_id = str(
967
+ detail.get("approval_request_id") or detail.get("id") or call_id or ""
968
+ )
969
+ resolved_call_id = str(call_id or approval_id)
970
+ if run is not None and approval_id:
971
+ run.pending_approvals.add(approval_id)
972
+ if approval_id:
973
+ pending_approval_ids = handle.native_ref.setdefault("pending_approval_ids", [])
974
+ if approval_id not in pending_approval_ids:
975
+ pending_approval_ids.append(approval_id)
976
+ return self._event(
977
+ handle,
978
+ EventType.APPROVAL_REQUESTED,
979
+ {
980
+ "approval_id": approval_id,
981
+ "call_id": resolved_call_id,
982
+ "kind": "tool",
983
+ "detail": dict(detail),
984
+ },
985
+ )
986
+
987
+ def _event(
988
+ self,
989
+ handle: RunHandle,
990
+ event_type: str,
991
+ payload: dict,
992
+ *,
993
+ phase: Optional[str] = None,
994
+ ) -> RuntimeEvent:
995
+ return RuntimeEvent.create(
996
+ event_type,
997
+ agent_id=str(handle.native_ref.get("agent_id") or "agent"),
998
+ user_id=str(handle.native_ref.get("user_id") or "user"),
999
+ session_id=handle.session_id,
1000
+ invocation_id=handle.run_id,
1001
+ seq_id=self._next_seq(),
1002
+ phase=phase,
1003
+ payload=payload,
1004
+ )
1005
+
1006
+ def _completion_payload(
1007
+ self,
1008
+ handle: RunHandle,
1009
+ *,
1010
+ status: str,
1011
+ metrics: Mapping[str, Any] | None = None,
1012
+ ) -> dict[str, Any]:
1013
+ payload: dict[str, Any] = {"status": status, **dict(metrics or {})}
1014
+ framework_ref = handle.native_ref.get("framework_ref")
1015
+ if isinstance(framework_ref, Mapping) and framework_ref:
1016
+ payload["agentengine"] = {
1017
+ "run_id": handle.run_id,
1018
+ "framework": self._runtime_type,
1019
+ "framework_ref": dict(framework_ref),
1020
+ }
1021
+ return payload
1022
+
1023
+ @staticmethod
1024
+ def _coerce(value: Any) -> str:
1025
+ if value is None:
1026
+ return ""
1027
+ if isinstance(value, str):
1028
+ return value
1029
+ return str(value)
1030
+
1031
+
1032
+ __all__ = ["RunnerRuntimeAdapter", "_RunnerAsBaseRuntime"]