agentengine-sdk-python 0.6.9__py3-none-any.whl → 0.8.0__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 (384) hide show
  1. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.8.0.dist-info}/METADATA +41 -23
  2. agentengine_sdk_python-0.8.0.dist-info/RECORD +578 -0
  3. ksadk/a2a/__init__.py +178 -11
  4. ksadk/a2a/bootstrap.py +334 -0
  5. ksadk/a2a/card.py +120 -0
  6. ksadk/a2a/context_store.py +203 -0
  7. ksadk/a2a/control_plane.py +818 -0
  8. ksadk/a2a/credential.py +99 -0
  9. ksadk/a2a/event_adapter.py +149 -0
  10. ksadk/a2a/executor.py +406 -89
  11. ksadk/a2a/external_transport.py +516 -0
  12. ksadk/a2a/identity.py +304 -0
  13. ksadk/a2a/ids.py +22 -0
  14. ksadk/a2a/langgraph.py +285 -0
  15. ksadk/a2a/resume_store.py +201 -0
  16. ksadk/a2a/routes.py +105 -0
  17. ksadk/a2a/server.py +107 -60
  18. ksadk/a2a/space_client.py +1101 -0
  19. ksadk/a2a/task_adapter.py +423 -0
  20. ksadk/a2a/task_event_dispatcher.py +167 -0
  21. ksadk/a2a/task_event_outbox.py +326 -0
  22. ksadk/a2a/task_store.py +206 -0
  23. ksadk/a2ui/__init__.py +27 -0
  24. ksadk/a2ui/core.py +186 -0
  25. ksadk/a2ui/models.py +166 -0
  26. ksadk/a2ui/renderer.py +198 -0
  27. ksadk/agui/__init__.py +9 -0
  28. ksadk/agui/a2ui_projection.py +92 -0
  29. ksadk/agui/agent.py +857 -0
  30. ksadk/agui/config.py +86 -0
  31. ksadk/agui/routes.py +55 -0
  32. ksadk/api/client.py +252 -191
  33. ksadk/builders/__init__.py +2 -0
  34. ksadk/builders/base.py +16 -14
  35. ksadk/builders/code_builder.py +318 -236
  36. ksadk/builders/container_builder.py +187 -167
  37. ksadk/builders/framework_requirements.py +29 -11
  38. ksadk/builders/ks3_uploader.py +58 -27
  39. ksadk/builders/managed_runtime_builder.py +145 -0
  40. ksadk/builders/mcp_builder.py +91 -92
  41. ksadk/builders/requirements_utils.py +1 -5
  42. ksadk/cli/__init__.py +89 -171
  43. ksadk/cli/cmd_a2a.py +254 -70
  44. ksadk/cli/cmd_agent.py +19 -5
  45. ksadk/cli/cmd_build.py +158 -29
  46. ksadk/cli/cmd_completion.py +47 -35
  47. ksadk/cli/cmd_config.py +267 -198
  48. ksadk/cli/cmd_create.py +533 -726
  49. ksadk/cli/cmd_dashboard.py +120 -45
  50. ksadk/cli/cmd_deploy.py +117 -58
  51. ksadk/cli/cmd_destroy.py +44 -14
  52. ksadk/cli/cmd_files.py +63 -24
  53. ksadk/cli/cmd_hermes.py +173 -90
  54. ksadk/cli/cmd_invoke.py +388 -159
  55. ksadk/cli/cmd_launch.py +64 -28
  56. ksadk/cli/cmd_mcp.py +103 -64
  57. ksadk/cli/cmd_model.py +14 -5
  58. ksadk/cli/cmd_openclaw.py +488 -228
  59. ksadk/cli/cmd_replay.py +71 -0
  60. ksadk/cli/cmd_run.py +44 -14
  61. ksadk/cli/cmd_status.py +80 -31
  62. ksadk/cli/cmd_version.py +20 -13
  63. ksadk/cli/cmd_web.py +89 -13
  64. ksadk/cli/deploy_utils.py +20 -20
  65. ksadk/cli/dry_run.py +5 -3
  66. ksadk/cli/error_utils.py +37 -12
  67. ksadk/cli/global_options.py +1 -2
  68. ksadk/cli/local_runtime.py +1 -2
  69. ksadk/cli/model_catalog.py +6 -2
  70. ksadk/cli/network_options.py +38 -16
  71. ksadk/cli/resource_common.py +6 -2
  72. ksadk/cli/storage.py +0 -1
  73. ksadk/cli/ui.py +5 -3
  74. ksadk/cli/workflow_common.py +51 -17
  75. ksadk/codex/__init__.py +12 -0
  76. ksadk/codex/client.py +408 -0
  77. ksadk/codex/phase.py +143 -0
  78. ksadk/codex/runtime.py +449 -0
  79. ksadk/common/aicp_env.py +0 -1
  80. ksadk/common/auth.py +2 -2
  81. ksadk/common/constants.py +1 -3
  82. ksadk/common/llm_utils.py +17 -13
  83. ksadk/compat/__init__.py +5 -0
  84. ksadk/compat/adk_compat.py +200 -0
  85. ksadk/compat/copilotkit_a2ui.py +330 -0
  86. ksadk/configs/__init__.py +12 -12
  87. ksadk/configs/env_registry.py +501 -100
  88. ksadk/configs/global_config.py +2 -1
  89. ksadk/configs/settings.py +151 -124
  90. ksadk/conversations/__init__.py +7 -5
  91. ksadk/conversations/attachment_storage.py +16 -11
  92. ksadk/conversations/attachments.py +44 -36
  93. ksadk/conversations/compaction_pipeline.py +12 -12
  94. ksadk/conversations/compaction_prompt.py +9 -5
  95. ksadk/conversations/context.py +221 -6
  96. ksadk/conversations/message_projection.py +656 -0
  97. ksadk/conversations/model_context.py +22 -6
  98. ksadk/conversations/model_options.py +0 -1
  99. ksadk/conversations/normalize.py +20 -16
  100. ksadk/conversations/reasoning_markup.py +1 -1
  101. ksadk/conversations/runtime.py +339 -5187
  102. ksadk/conversations/runtime_compaction.py +333 -0
  103. ksadk/conversations/runtime_constants.py +27 -0
  104. ksadk/conversations/runtime_governance.py +185 -0
  105. ksadk/conversations/runtime_input.py +652 -0
  106. ksadk/conversations/runtime_invocation.py +395 -0
  107. ksadk/conversations/runtime_metadata.py +475 -0
  108. ksadk/conversations/runtime_observability.py +355 -0
  109. ksadk/conversations/runtime_payloads.py +272 -0
  110. ksadk/conversations/runtime_persistence.py +440 -0
  111. ksadk/conversations/runtime_preparation.py +418 -0
  112. ksadk/conversations/runtime_resume.py +816 -0
  113. ksadk/conversations/runtime_stream_events.py +902 -0
  114. ksadk/conversations/runtime_streaming.py +547 -0
  115. ksadk/conversations/semantic_summary.py +17 -5
  116. ksadk/conversations/session_title.py +6 -3
  117. ksadk/deployment/__init__.py +15 -16
  118. ksadk/deployment/agent_access.py +12 -23
  119. ksadk/deployment/base.py +52 -39
  120. ksadk/deployment/manager.py +63 -65
  121. ksadk/deployment/providers/serverless.py +326 -209
  122. ksadk/deployment/registry.py +38 -32
  123. ksadk/deployment/state.py +11 -11
  124. ksadk/deployment/ui_config.py +1 -1
  125. ksadk/detection/__init__.py +1 -1
  126. ksadk/detection/detector.py +146 -83
  127. ksadk/detection/mcp_detector.py +9 -6
  128. ksadk/events/__init__.py +25 -0
  129. ksadk/events/parser.py +191 -0
  130. ksadk/events/replay.py +37 -0
  131. ksadk/events/runtime_event.py +264 -0
  132. ksadk/events/store.py +317 -0
  133. ksadk/harness/__init__.py +34 -0
  134. ksadk/harness/app.py +278 -0
  135. ksadk/harness/config.py +181 -0
  136. ksadk/harness/reasoner.py +105 -0
  137. ksadk/harness/runner.py +211 -0
  138. ksadk/harness/sandbox.py +141 -0
  139. ksadk/harness/tools.py +222 -0
  140. ksadk/hermes_terminal.py +23 -12
  141. ksadk/identity/resolver.py +15 -9
  142. ksadk/ids.py +32 -0
  143. ksadk/knowledge_base/adk_tool.py +1 -1
  144. ksadk/knowledge_base/client.py +23 -29
  145. ksadk/managed_runtime.py +188 -0
  146. ksadk/markdown.py +5 -2
  147. ksadk/mcp_runtime/__init__.py +9 -6
  148. ksadk/memory/__init__.py +1 -1
  149. ksadk/memory/adk/__init__.py +2 -0
  150. ksadk/memory/adk/backends/base_ltm_backend.py +2 -6
  151. ksadk/memory/adk/backends/http_ltm_backend.py +13 -21
  152. ksadk/memory/adk/backends/inmemory_ltm_backend.py +7 -13
  153. ksadk/memory/adk/backends/sdk_ltm_backend.py +26 -33
  154. ksadk/memory/adk/long_term_memory.py +31 -35
  155. ksadk/memory/adk/resilient_session_service.py +267 -0
  156. ksadk/memory/adk/short_term_memory.py +33 -27
  157. ksadk/memory/adk_tool.py +3 -2
  158. ksadk/memory/backends/base.py +19 -19
  159. ksadk/memory/backends/memory.py +24 -25
  160. ksadk/memory/backends/redis.py +31 -30
  161. ksadk/memory/langchain_tool.py +2 -1
  162. ksadk/memory/manager.py +52 -47
  163. ksadk/memory/service.py +10 -8
  164. ksadk/model_policy.py +15 -7
  165. ksadk/model_proxy/__init__.py +51 -0
  166. ksadk/model_proxy/bootstrap.py +80 -0
  167. ksadk/model_proxy/cache.py +165 -0
  168. ksadk/model_proxy/config.py +45 -0
  169. ksadk/model_proxy/demo_webui.py +115 -0
  170. ksadk/model_proxy/detect.py +145 -0
  171. ksadk/model_proxy/gate.py +68 -0
  172. ksadk/model_proxy/namespace.py +173 -0
  173. ksadk/model_proxy/protocol_proxy.py +22 -0
  174. ksadk/model_proxy/server.py +300 -0
  175. ksadk/model_proxy/transform.py +691 -0
  176. ksadk/openclaw_gateway.py +29 -14
  177. ksadk/runners/adk_runner.py +1248 -164
  178. ksadk/runners/base_runner.py +46 -29
  179. ksadk/runners/codex_runner.py +132 -0
  180. ksadk/runners/deepagents_runner.py +0 -1
  181. ksadk/runners/factory.py +17 -5
  182. ksadk/runners/langchain_runner.py +29 -530
  183. ksadk/runners/langgraph_runner.py +490 -95
  184. ksadk/runners/patch_langchain.py +16 -16
  185. ksadk/runners/remote_runner.py +264 -38
  186. ksadk/runners/usage_accumulator.py +1 -0
  187. ksadk/runners/utils/langfuse.py +3 -3
  188. ksadk/runners/utils/loader.py +7 -7
  189. ksadk/runtime/__init__.py +41 -0
  190. ksadk/runtime/adapter.py +345 -0
  191. ksadk/runtime/framework_adapters.py +124 -0
  192. ksadk/runtime/preprocessing.py +118 -0
  193. ksadk/runtime/runner_adapter.py +873 -0
  194. ksadk/runtime_context.py +18 -3
  195. ksadk/runtime_state.py +0 -1
  196. ksadk/sandbox/__init__.py +4 -1
  197. ksadk/sandbox/backends/__init__.py +4 -1
  198. ksadk/sandbox/backends/e2b.py +13 -5
  199. ksadk/sandbox/backends/local_process.py +11 -3
  200. ksadk/sandbox/base.py +10 -15
  201. ksadk/sandbox/factory.py +14 -4
  202. ksadk/sandbox/registry.py +57 -6
  203. ksadk/server/api_models.py +15 -5
  204. ksadk/server/app.py +455 -3743
  205. ksadk/server/composition.py +21 -0
  206. ksadk/server/factory.py +624 -0
  207. ksadk/server/routes/__init__.py +1 -0
  208. ksadk/server/routes/checkpoint_resolution.py +116 -0
  209. ksadk/server/routes/common.py +713 -0
  210. ksadk/server/routes/control.py +327 -0
  211. ksadk/server/routes/dependencies.py +98 -0
  212. ksadk/server/routes/misc.py +448 -0
  213. ksadk/server/routes/models.py +443 -0
  214. ksadk/server/routes/openai_compat.py +207 -0
  215. ksadk/server/routes/projection.py +795 -0
  216. ksadk/server/routes/routers.py +52 -0
  217. ksadk/server/routes/run.py +726 -0
  218. ksadk/server/routes/sessions.py +649 -0
  219. ksadk/server/routes/streaming.py +223 -0
  220. ksadk/server/routes/workspace.py +381 -0
  221. ksadk/server/static/assets/{ArtifactsPanel-C0meuOdi.js → ArtifactsPanel-Cjnqt-8W.js} +1 -1
  222. ksadk/server/static/assets/CodeBlock-BqyqZfuB.js +9 -0
  223. ksadk/server/static/assets/MathMessageMarkdown-BpH7jiFM.js +8 -0
  224. ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +303 -0
  225. ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +1 -0
  226. ksadk/server/static/assets/{arc-bj2rZnP5.js → arc-CFKwSk_B.js} +1 -1
  227. ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +1 -0
  228. ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-2u1ltDIj.js → architectureDiagram-3BPJPVTR-Dej8jX8c.js} +1 -1
  229. ksadk/server/static/assets/{blockDiagram-GPEHLZMM-D6fvdK20.js → blockDiagram-GPEHLZMM-1F64jqvy.js} +1 -1
  230. ksadk/server/static/assets/{c4Diagram-AAUBKEIU-BK82L5n5.js → c4Diagram-AAUBKEIU-9q7AUAKb.js} +1 -1
  231. ksadk/server/static/assets/channel-iUGv2N24.js +1 -0
  232. ksadk/server/static/assets/{chunk-2J33WTMH-DLVdkeyQ.js → chunk-2J33WTMH-6qgwJm7Q.js} +1 -1
  233. ksadk/server/static/assets/{chunk-4BX2VUAB-BbwsYcbW.js → chunk-4BX2VUAB-CsWIQRhc.js} +1 -1
  234. ksadk/server/static/assets/{chunk-55IACEB6-DOairxzw.js → chunk-55IACEB6-BK8paA20.js} +1 -1
  235. ksadk/server/static/assets/{chunk-727SXJPM-hCO487gs.js → chunk-727SXJPM-B-JEhy7h.js} +1 -1
  236. ksadk/server/static/assets/{chunk-AQP2D5EJ-XU8vDOuG.js → chunk-AQP2D5EJ-DYBvMHAa.js} +1 -1
  237. ksadk/server/static/assets/{chunk-FMBD7UC4-PrapyfMu.js → chunk-FMBD7UC4-BGhvzDYt.js} +1 -1
  238. ksadk/server/static/assets/{chunk-ND2GUHAM-CboKhTZ3.js → chunk-ND2GUHAM-D98vPd-E.js} +1 -1
  239. ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +1 -0
  240. ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +1 -0
  241. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +1 -0
  242. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-_N8bJ-EU.js → cose-bilkent-S5V4N54A-CX3yotHq.js} +1 -1
  243. ksadk/server/static/assets/{dagre-BM42HDAG-FoDlo5J2.js → dagre-BM42HDAG-C7TguAWo.js} +1 -1
  244. ksadk/server/static/assets/{diagram-2AECGRRQ-BBp6ebBw.js → diagram-2AECGRRQ-Dd17_kws.js} +1 -1
  245. ksadk/server/static/assets/{diagram-5GNKFQAL-CqOvG1PR.js → diagram-5GNKFQAL-DVIClIil.js} +1 -1
  246. ksadk/server/static/assets/{diagram-KO2AKTUF-O9T3t6ba.js → diagram-KO2AKTUF-Df_3gYQa.js} +1 -1
  247. ksadk/server/static/assets/{diagram-LMA3HP47-CGG8ekBO.js → diagram-LMA3HP47-BhLk6ZtA.js} +1 -1
  248. ksadk/server/static/assets/{diagram-OG6HWLK6-4BmH1EEg.js → diagram-OG6HWLK6-D4GNGvuu.js} +1 -1
  249. ksadk/server/static/assets/{dist-CiZHB6ZW.js → dist-6CVKuTAe.js} +1 -1
  250. ksadk/server/static/assets/{dist-BMKM66B4.js → dist-BUHRxvDG.js} +1 -1
  251. ksadk/server/static/assets/{dist-CZ6C3x8B.js → dist-BZ9n9AjO.js} +1 -1
  252. ksadk/server/static/assets/{dist-DbaH3p5h.js → dist-BibcG1V9.js} +1 -1
  253. ksadk/server/static/assets/{dist-BVzcWa6D.js → dist-Bs-jQcRX.js} +1 -1
  254. ksadk/server/static/assets/{dist-D99wf6_X.js → dist-C2xjE7cO.js} +1 -1
  255. ksadk/server/static/assets/{dist-DLMjL01k.js → dist-C7-3jInC.js} +1 -1
  256. ksadk/server/static/assets/{dist-DI-oVAXw.js → dist-C99THtIA.js} +1 -1
  257. ksadk/server/static/assets/{dist-BkJjNih3.js → dist-CGcp2ex8.js} +1 -1
  258. ksadk/server/static/assets/{dist-D5JGcxBx.js → dist-CVQRZjPu.js} +1 -1
  259. ksadk/server/static/assets/{dist-CWiTDN_V.js → dist-Cf23NY4L.js} +1 -1
  260. ksadk/server/static/assets/{dist-DLeM1OCK.js → dist-CgF3fj15.js} +1 -1
  261. ksadk/server/static/assets/{dist-BmuTWcm7.js → dist-Cwt7koer.js} +1 -1
  262. ksadk/server/static/assets/{dist-C2DKRFVK.js → dist-D82mtD2s.js} +1 -1
  263. ksadk/server/static/assets/{dist-B67uF2uK.js → dist-DF06ORQo.js} +1 -1
  264. ksadk/server/static/assets/{dist-Cwhk4lwI.js → dist-DHn6nBzr.js} +1 -1
  265. ksadk/server/static/assets/{dist-S4klkK5W.js → dist-Djgl2wVZ.js} +1 -1
  266. ksadk/server/static/assets/{dist-DCpZrjaK.js → dist-DlzCRy0S.js} +1 -1
  267. ksadk/server/static/assets/{dist-Cp1_jKwf.js → dist-DqNnNFrw.js} +1 -1
  268. ksadk/server/static/assets/{dist-6kSZvCZh.js → dist-DyvYLMW9.js} +1 -1
  269. ksadk/server/static/assets/{dist-DM0rEQPk.js → dist-eoW6tZXA.js} +1 -1
  270. ksadk/server/static/assets/{dist-Bk1l05Ni.js → dist-ivOya1LD.js} +1 -1
  271. ksadk/server/static/assets/{erDiagram-TEJ5UH35-oJhaci7S.js → erDiagram-TEJ5UH35-CnfRcY2M.js} +1 -1
  272. ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +1 -0
  273. ksadk/server/static/assets/{flowDiagram-I6XJVG4X-DtTmUan-.js → flowDiagram-I6XJVG4X-DGaJjGNJ.js} +1 -1
  274. ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BSsb_eij.js → ganttDiagram-6RSMTGT7-DpCoB2g9.js} +1 -1
  275. ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +1 -0
  276. ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-BR5s6Pws.js → gitGraphDiagram-PVQCEYII-9IKVFBVT.js} +1 -1
  277. ksadk/server/static/assets/index-QSwCHuB8.js +177 -0
  278. ksadk/server/static/assets/index-jOPmTOZH.css +2 -0
  279. ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +1 -0
  280. ksadk/server/static/assets/{infoDiagram-5YYISTIA-DYPc5cvh.js → infoDiagram-5YYISTIA-CYIIsb8y.js} +1 -1
  281. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-MoBdALDg.js → ishikawaDiagram-YF4QCWOH-DLOHGvWw.js} +1 -1
  282. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-DgLiQT8l.js → journeyDiagram-JHISSGLW-BIzKeejT.js} +1 -1
  283. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-CsFx112F.js → kanban-definition-UN3LZRKU-BWHoFSw2.js} +1 -1
  284. ksadk/server/static/assets/{katex-DRsiwpM2.js → katex-DATssjp1.js} +1 -1
  285. ksadk/server/static/assets/{linear-TNRQ5Orv.js → linear-CvMBw9cA.js} +1 -1
  286. ksadk/server/static/assets/{mermaid-parser.core-D2eMVOgo.js → mermaid-parser.core-mGAcEwcq.js} +2 -2
  287. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-BtByYFhL.js → mindmap-definition-RKZ34NQL-CSAQxEAe.js} +1 -1
  288. ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +1 -0
  289. ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +1 -0
  290. ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPnuCB8c.js → pieDiagram-4H26LBE5-By-ra07M.js} +1 -1
  291. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-miFpCPv4.js → quadrantDiagram-W4KKPZXB-CxIR4JXI.js} +1 -1
  292. ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +1 -0
  293. ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-mjB_yYkW.js → requirementDiagram-4Y6WPE33-DpW90iu3.js} +1 -1
  294. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-D7hSlZZZ.js → sankeyDiagram-5OEKKPKP-DpGa6EAV.js} +1 -1
  295. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-D9Cx6hC3.js → sequenceDiagram-3UESZ5HK-NTKNGGe5.js} +1 -1
  296. ksadk/server/static/assets/{stateDiagram-AJRCARHV-D_zrg47o.js → stateDiagram-AJRCARHV-DGpd9yFG.js} +1 -1
  297. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +1 -0
  298. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-CwSec0Wa.js → timeline-definition-PNZ67QCA-jqoYs3jQ.js} +1 -1
  299. ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +1 -0
  300. ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +1 -0
  301. ksadk/server/static/assets/{vennDiagram-CIIHVFJN-agCVobiF.js → vennDiagram-CIIHVFJN-l3Z7-wbP.js} +1 -1
  302. ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +1 -0
  303. ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-TmrlhWNt.js → wardleyDiagram-YWT4CUSO-BR54GPmr.js} +1 -1
  304. ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-DV71V-ah.js → xychartDiagram-2RQKCTM6-SkiH8GsK.js} +1 -1
  305. ksadk/server/static/index.html +2 -2
  306. ksadk/server/terminal_sessions.py +111 -58
  307. ksadk/sessions/__init__.py +86 -40
  308. ksadk/sessions/base.py +31 -11
  309. ksadk/sessions/continuity.py +28 -5
  310. ksadk/sessions/in_memory.py +46 -2
  311. ksadk/sessions/local_service.py +148 -17
  312. ksadk/sessions/postgres_service.py +216 -35
  313. ksadk/sessions/resilience.py +52 -0
  314. ksadk/sessions/resilient.py +388 -0
  315. ksadk/skills/models.py +17 -5
  316. ksadk/skills/package_store.py +5 -2
  317. ksadk/skills/runtime/agent.py +20 -5
  318. ksadk/skills/runtime/artifacts.py +1 -5
  319. ksadk/skills/runtime/backends/e2b.py +10 -4
  320. ksadk/skills/runtime/backends/local.py +15 -3
  321. ksadk/skills/runtime/base.py +1 -2
  322. ksadk/skills/runtime/executor.py +10 -3
  323. ksadk/skills/runtime/factory.py +4 -2
  324. ksadk/skills/runtime/loader.py +1 -2
  325. ksadk/skills/runtime/registry.py +15 -10
  326. ksadk/skills/runtime/request.py +3 -1
  327. ksadk/skills/service_client.py +38 -17
  328. ksadk/skills/tool_defs.py +15 -10
  329. ksadk/terminal_exec_policy.py +3 -4
  330. ksadk/tools/gateway.py +92 -15
  331. ksadk/tools/result_budget.py +12 -4
  332. ksadk/toolsets/__init__.py +136 -36
  333. ksadk/toolsets/platform.py +21 -5
  334. ksadk/toolsets/sandbox.py +82 -16
  335. ksadk/toolsets/skills.py +200 -39
  336. ksadk/toolsets/web.py +123 -26
  337. ksadk/toolsets/workspace.py +264 -114
  338. ksadk/tracing/exporters/inmemory_exporter.py +62 -51
  339. ksadk/tracing/exporters/langfuse_exporter.py +91 -76
  340. ksadk/tracing/setup.py +132 -53
  341. ksadk/tracing/span_utils.py +5 -2
  342. ksadk/tui/__init__.py +22 -7
  343. ksadk/tui/loop.py +1925 -0
  344. ksadk/tui/stream_render.py +69 -0
  345. ksadk/ui_config.py +5 -3
  346. ksadk/version.py +1 -1
  347. ksadk_runtime_common/schemas/runtime_event_v1.json +95 -0
  348. agentengine_sdk_python-0.6.9.dist-info/RECORD +0 -487
  349. ksadk/server/static/assets/CodeBlock-BbI4HPtJ.js +0 -9
  350. ksadk/server/static/assets/MathMessageMarkdown-DMLQYoLp.js +0 -8
  351. ksadk/server/static/assets/MermaidBlock-DCR8WZ17.js +0 -303
  352. ksadk/server/static/assets/NativeTerminalPanel-CKUvWhUz.js +0 -1
  353. ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +0 -1
  354. ksadk/server/static/assets/channel-BSSOwFIb.js +0 -1
  355. ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +0 -1
  356. ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +0 -1
  357. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +0 -1
  358. ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +0 -1
  359. ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +0 -1
  360. ksadk/server/static/assets/index-BH7-pC0B.js +0 -165
  361. ksadk/server/static/assets/index-DAURbCsP.css +0 -1
  362. ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +0 -1
  363. ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +0 -1
  364. ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +0 -1
  365. ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +0 -1
  366. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +0 -1
  367. ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +0 -1
  368. ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +0 -1
  369. ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +0 -1
  370. ksadk/tui/app.py +0 -400
  371. ksadk/tui/clipboard.py +0 -100
  372. ksadk/tui/widgets/__init__.py +0 -22
  373. ksadk/tui/widgets/assistant.py +0 -89
  374. ksadk/tui/widgets/base.py +0 -54
  375. ksadk/tui/widgets/chat_input.py +0 -246
  376. ksadk/tui/widgets/history.py +0 -156
  377. ksadk/tui/widgets/system.py +0 -87
  378. ksadk/tui/widgets/thinking.py +0 -110
  379. ksadk/tui/widgets/tool_call.py +0 -161
  380. ksadk/tui/widgets/user.py +0 -37
  381. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.8.0.dist-info}/WHEEL +0 -0
  382. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.8.0.dist-info}/entry_points.txt +0 -0
  383. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.8.0.dist-info}/licenses/LICENSE +0 -0
  384. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.8.0.dist-info}/top_level.txt +0 -0
ksadk/cli/cmd_invoke.py CHANGED
@@ -4,16 +4,18 @@ agentengine invoke - 与已部署的 Agent 进行交互
4
4
  支持 OpenAI 兼容格式调用,支持流式输出
5
5
  """
6
6
 
7
- import click
8
7
  import asyncio
9
8
  import io
10
9
  import json
11
10
  import os
11
+ import time
12
+ import uuid
12
13
  from datetime import datetime
13
14
  from pathlib import Path
14
15
  from typing import Any, Callable, Dict, Optional
15
- import time
16
- import uuid
16
+
17
+ import click
18
+
17
19
  from ksadk.api import AgentEngineAPIError, AgentEngineClient
18
20
  from ksadk.cli.agent_ref import merge_agent_inputs, resolve_agent_ref, resolve_openclaw_ref
19
21
  from ksadk.cli.cmd_files import (
@@ -24,20 +26,28 @@ from ksadk.cli.cmd_files import (
24
26
  _normalize_workspace_dir,
25
27
  _push_workspace_files,
26
28
  )
27
- from ksadk.cli.resource_common import CONTEXT_SETTINGS, CompatibilityAliasCommand, print_compatibility_hint
29
+ from ksadk.cli.resource_common import (
30
+ CONTEXT_SETTINGS,
31
+ CompatibilityAliasCommand,
32
+ print_compatibility_hint,
33
+ )
28
34
  from ksadk.hermes_terminal import run_hermes_terminal_session
29
35
  from ksadk.terminal_client import run_terminal_session
30
36
  from ksadk_runtime_common.workspace_files.constants import DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
31
37
 
38
+ console: Any = None
39
+ Markdown: Any = None
40
+ Live: Any = None
32
41
  try:
33
- from rich.console import Console
34
- from rich.markdown import Markdown
35
- from rich.live import Live
36
- console = Console()
42
+ from rich.console import Console as RichConsole
43
+ from rich.live import Live as RichLive
44
+ from rich.markdown import Markdown as RichMarkdown
45
+
46
+ console = RichConsole()
47
+ Markdown = RichMarkdown
48
+ Live = RichLive
37
49
  except ImportError:
38
- console = None
39
- Markdown = None
40
- Live = None
50
+ pass
41
51
 
42
52
 
43
53
  @click.command(
@@ -84,6 +94,20 @@ except ImportError:
84
94
  )
85
95
  @click.option("--model", help="指定模型名称")
86
96
  @click.option("--show-thinking", is_flag=True, help="显示模型思考过程")
97
+ @click.option(
98
+ "--api-format",
99
+ "api_format",
100
+ type=click.Choice(["auto", "responses", "chat_completions"], case_sensitive=False),
101
+ default="auto",
102
+ show_default=True,
103
+ help="HTTP 协议: auto(优先 responses,不通回退 chat), responses, chat_completions",
104
+ )
105
+ @click.option(
106
+ "--no-alt-screen",
107
+ "no_alt_screen",
108
+ is_flag=True,
109
+ help="兼容参数;TUI 已默认使用 inline viewport 并保留终端 scrollback",
110
+ )
87
111
  def invoke(
88
112
  agent_ref: str,
89
113
  agent_option: str,
@@ -101,6 +125,8 @@ def invoke(
101
125
  verbose_workspace_sync: bool,
102
126
  model: str,
103
127
  show_thinking: bool,
128
+ api_format: str,
129
+ no_alt_screen: bool,
104
130
  ):
105
131
  """与 Agent 进行交互 (本地或远程)。"""
106
132
  run_invoke_command(
@@ -121,6 +147,8 @@ def invoke(
121
147
  model=model,
122
148
  show_thinking=show_thinking,
123
149
  compatibility_alias=True,
150
+ api_format=None if (api_format or "auto").lower() == "auto" else api_format,
151
+ no_alt_screen=no_alt_screen,
124
152
  )
125
153
 
126
154
 
@@ -129,9 +157,9 @@ def _resolve_remote_workspace_seed_path(
129
157
  remote_workspace_path: str | None,
130
158
  ) -> str:
131
159
  if remote_workspace_path:
132
- return _normalize_workspace_dir(remote_workspace_path)
160
+ return str(_normalize_workspace_dir(remote_workspace_path))
133
161
  default_name = local_workspace.resolve().name or local_workspace.name or "workspace"
134
- return _normalize_workspace_dir(default_name)
162
+ return str(_normalize_workspace_dir(default_name))
135
163
 
136
164
 
137
165
  def _summarize_local_workspace_dir(
@@ -182,10 +210,12 @@ def _describe_workspace_sync_phase(event: dict[str, Any] | None) -> str:
182
210
  return "同步 workspace"
183
211
 
184
212
 
185
- def _render_workspace_sync_progress_bar(current: object | None, total: object | None, *, width: int = 20) -> str:
213
+ def _render_workspace_sync_progress_bar(
214
+ current: object | None, total: object | None, *, width: int = 20
215
+ ) -> str:
186
216
  try:
187
- current_value = int(current or 0)
188
- total_value = max(int(total or 0), 1)
217
+ current_value = int(str(current or 0))
218
+ total_value = max(int(str(total or 0)), 1)
189
219
  except (TypeError, ValueError):
190
220
  current_value = 0
191
221
  total_value = 1
@@ -196,8 +226,8 @@ def _render_workspace_sync_progress_bar(current: object | None, total: object |
196
226
 
197
227
  def _format_workspace_sync_percent(current: object | None, total: object | None) -> str:
198
228
  try:
199
- current_value = float(current or 0)
200
- total_value = max(float(total or 0), 1.0)
229
+ current_value = float(str(current or 0))
230
+ total_value = max(float(str(total or 0)), 1.0)
201
231
  except (TypeError, ValueError):
202
232
  current_value = 0.0
203
233
  total_value = 1.0
@@ -226,7 +256,8 @@ def _build_workspace_sync_progress_emitter(*, verbose: bool) -> Callable[[dict[s
226
256
  click.secho(
227
257
  "📂 准备同步 workspace: "
228
258
  f"{event.get('total_files', 0)} 个文件,"
229
- f"{_format_size(event.get('total_bytes', 0))} -> workspace:/{event.get('remote_path', '.')}",
259
+ f"{_format_size(event.get('total_bytes', 0))} -> "
260
+ f"workspace:/{event.get('remote_path', '.')}",
230
261
  fg="blue",
231
262
  )
232
263
  ignored_artifacts = list(event.get("ignored_artifacts") or [])
@@ -306,7 +337,7 @@ def _extract_workspace_upload_limit(bootstrap: dict[str, Any] | None) -> int | N
306
337
  return None
307
338
  raw_limit = workspace.get("max_upload_bytes") or workspace.get("MaxUploadBytes")
308
339
  try:
309
- limit = int(raw_limit)
340
+ limit = int(str(raw_limit))
310
341
  except (TypeError, ValueError):
311
342
  return None
312
343
  return limit if limit > 0 else None
@@ -318,7 +349,7 @@ async def _lookup_workspace_upload_limit(
318
349
  region: str,
319
350
  ) -> int:
320
351
  if not agent_ref:
321
- return DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
352
+ return int(DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES)
322
353
 
323
354
  try:
324
355
  async with AgentEngineClient(region=region) as client:
@@ -327,9 +358,9 @@ async def _lookup_workspace_upload_limit(
327
358
  else:
328
359
  payload = await client.get_agent_ui_bootstrap(name=agent_ref)
329
360
  except Exception:
330
- return DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
361
+ return int(DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES)
331
362
 
332
- return _extract_workspace_upload_limit(payload) or DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
363
+ return int(_extract_workspace_upload_limit(payload) or DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES)
333
364
 
334
365
 
335
366
  async def _sync_local_workspace_for_hermes_invoke(
@@ -376,7 +407,8 @@ async def _sync_local_workspace_for_hermes_invoke(
376
407
  if item["size_bytes"] > max_upload_bytes:
377
408
  raise click.ClickException(
378
409
  "本地目录中存在超过远端 workspace 上传上限的文件:"
379
- f"{item['path']}({_format_size(item['size_bytes'])} > {_format_size(max_upload_bytes)})"
410
+ f"{item['path']}({_format_size(item['size_bytes'])} > "
411
+ f"{_format_size(max_upload_bytes)})"
380
412
  )
381
413
 
382
414
  if summary["total_bytes"] > max_upload_bytes:
@@ -394,7 +426,7 @@ async def _sync_local_workspace_for_hermes_invoke(
394
426
  progress_callback(event)
395
427
 
396
428
  try:
397
- return await _push_workspace_files(
429
+ result = await _push_workspace_files(
398
430
  agent_ref=agent_ref,
399
431
  local_dir=local_dir,
400
432
  remote_path=remote_path,
@@ -406,11 +438,12 @@ async def _sync_local_workspace_for_hermes_invoke(
406
438
  ignore_dev_artifacts=True,
407
439
  ignore_git_artifacts=".git" in list(summary.get("ignored_artifacts") or []),
408
440
  )
441
+ if not isinstance(result, dict):
442
+ raise click.ClickException("同步远端 workspace 返回了无效响应")
443
+ return result
409
444
  except AgentEngineAPIError as exc:
410
445
  phase = _describe_workspace_sync_phase(last_progress)
411
- raise click.ClickException(
412
- f"同步远端 workspace 失败({phase}):{exc.message}"
413
- ) from exc
446
+ raise click.ClickException(f"同步远端 workspace 失败({phase}):{exc.message}") from exc
414
447
 
415
448
 
416
449
  def run_invoke_command(
@@ -432,6 +465,8 @@ def run_invoke_command(
432
465
  show_thinking: bool,
433
466
  openclaw_gateway_token: str | None = None,
434
467
  compatibility_alias: bool = False,
468
+ api_format: str | None = None,
469
+ no_alt_screen: bool = False,
435
470
  ):
436
471
  """与 Agent 进行交互 (本地或远程)。"""
437
472
  if compatibility_alias:
@@ -458,9 +493,8 @@ def run_invoke_command(
458
493
  # 加载本地状态
459
494
  state = _load_state()
460
495
  latest_access: dict[str, Any] = {}
461
- reuse_state_session = True
462
496
  target_agent: str | None = None
463
-
497
+
464
498
  normalized_transport = (transport or "auto").strip().lower() or "auto"
465
499
 
466
500
  # 确定 Endpoint
@@ -493,25 +527,24 @@ def run_invoke_command(
493
527
  state=state,
494
528
  persist=_state_matches_target(state, target_agent),
495
529
  )
496
- reuse_state_session = not agent_input or _state_matches_target(state, target_agent)
497
530
 
498
531
  # 优先使用 state 里的 endpoint (如果是对应的 agent)
499
532
  if latest_access.get("endpoint"):
500
533
  endpoint = latest_access["endpoint"]
501
534
  elif not agent_input or _state_matches_target(state, target_agent):
502
535
  endpoint = state.get("endpoint")
503
-
536
+
504
537
  if not endpoint:
505
538
  # 自动获取
506
539
  endpoint = _get_endpoint(target_agent, region)
507
540
 
508
541
  # API Key
509
542
  api_key = api_key or latest_access.get("api_key") or state.get("api_key")
510
- session_id = (
511
- session
512
- or (state.get("session_id") if reuse_state_session else None)
513
- or str(uuid.uuid4())[:8]
514
- )
543
+ # 单次 --message 调用默认开新 session,避免复用 .agentengine.state 里长上下文
544
+ # 默认每次调用开新 session(单次 -m 与交互 TUI 都是),不复用 .agentengine.state
545
+ # 里的上次 session,避免长上下文累积导致首包变慢。TUI 内多轮续聊由
546
+ # InteractionLoop.session_id 维持;跨次想续聊上次会话用显式 --session 指定。
547
+ session_id = session or str(uuid.uuid4())[:8]
515
548
 
516
549
  next_state = _load_state() or dict(state)
517
550
  for key in ("agent_id", "name", "endpoint", "api_key", "framework"):
@@ -530,7 +563,7 @@ def run_invoke_command(
530
563
  latest_access=latest_access,
531
564
  )
532
565
 
533
- click.secho(f"🤖 连接到 Agent", fg="blue", bold=True)
566
+ click.secho("🤖 连接到 Agent", fg="blue", bold=True)
534
567
  click.echo(f" Endpoint: {endpoint}")
535
568
  if _is_openclaw_target(next_state, latest_access):
536
569
  if runtime_api_key:
@@ -543,13 +576,13 @@ def run_invoke_command(
543
576
  click.echo(f" Auth: Bearer {_mask_secret(api_key)}")
544
577
  else:
545
578
  click.secho(" ⚠️ 未发现 API Key,尝试匿名调用", fg="yellow")
546
-
579
+
547
580
  if insecure:
548
581
  click.secho(" ⚠️ SSL 证书验证已禁用", fg="yellow")
549
582
 
550
583
  if message:
551
584
  # 单次调用模式
552
- api_format = asyncio.run(
585
+ api_format_resolved = asyncio.run(
553
586
  _resolve_remote_api_format(
554
587
  endpoint=endpoint,
555
588
  api_key=api_key,
@@ -557,12 +590,23 @@ def run_invoke_command(
557
590
  insecure=insecure,
558
591
  state=next_state,
559
592
  latest_access=latest_access,
593
+ api_format=api_format,
594
+ )
595
+ )
596
+ asyncio.run(
597
+ _invoke_once(
598
+ endpoint,
599
+ message,
600
+ runtime_api_key,
601
+ session_id,
602
+ True,
603
+ insecure,
604
+ model,
605
+ api_format_resolved,
560
606
  )
561
607
  )
562
- asyncio.run(_invoke_once(endpoint, message, runtime_api_key, session_id, True, insecure, model, api_format))
563
608
  else:
564
609
  is_hermes_target = _is_hermes_target(next_state, latest_access)
565
- is_openclaw_target = _is_openclaw_target(next_state, latest_access)
566
610
  if normalized_transport == "chat" and is_hermes_target:
567
611
  click.secho("❌ Hermes 不再支持 ksadk 通用 chat TUI。", fg="red")
568
612
  click.echo(" 浏览器聊天页请改用: agentengine hermes open --chat")
@@ -634,7 +678,7 @@ def run_invoke_command(
634
678
  insecure=insecure,
635
679
  )
636
680
  else:
637
- api_format = asyncio.run(
681
+ api_format_resolved = asyncio.run(
638
682
  _resolve_remote_api_format(
639
683
  endpoint=endpoint,
640
684
  api_key=api_key,
@@ -642,8 +686,16 @@ def run_invoke_command(
642
686
  insecure=insecure,
643
687
  state=next_state,
644
688
  latest_access=latest_access,
689
+ api_format=api_format,
645
690
  )
646
691
  )
692
+ tui_agent_id = (
693
+ latest_access.get("agent_id")
694
+ or next_state.get("agent_id")
695
+ or target_agent
696
+ or latest_access.get("name")
697
+ or next_state.get("name")
698
+ )
647
699
  _invoke_tui(
648
700
  endpoint,
649
701
  runtime_api_key,
@@ -651,28 +703,34 @@ def run_invoke_command(
651
703
  insecure,
652
704
  model,
653
705
  show_thinking,
654
- api_format=api_format,
706
+ api_format=api_format_resolved,
655
707
  responses_session_header=(
656
- "x-openclaw-session-key" if _is_openclaw_target(next_state, latest_access) else None
708
+ "x-openclaw-session-key"
709
+ if _is_openclaw_target(next_state, latest_access)
710
+ else None
657
711
  ),
712
+ no_alt_screen=no_alt_screen,
713
+ region=region,
714
+ agent_id=tui_agent_id,
658
715
  )
659
716
 
660
717
 
661
-
662
-
663
718
  def _invoke_tui(
664
719
  endpoint: str,
665
- api_key: str = None,
666
- session_id: str = None,
720
+ api_key: Optional[str] = None,
721
+ session_id: Optional[str] = None,
667
722
  insecure: bool = False,
668
- model: str = None,
723
+ model: Optional[str] = None,
669
724
  show_thinking: bool = False,
670
725
  api_format: str = "chat_completions",
671
726
  responses_session_header: str | None = None,
727
+ no_alt_screen: bool = False,
728
+ region: str = "cn-beijing-6",
729
+ agent_id: str | None = None,
672
730
  ):
673
731
  """使用 TUI 模式调用"""
674
732
  from ksadk.runners.remote_runner import RemoteRunner
675
- from ksadk.tui import AgentTUI
733
+ from ksadk.tui.loop import run_tui
676
734
 
677
735
  runner = RemoteRunner(
678
736
  endpoint=endpoint,
@@ -683,22 +741,102 @@ def _invoke_tui(
683
741
  api_format=api_format,
684
742
  responses_session_header=responses_session_header,
685
743
  )
744
+ # 优先用 ListAgentModels(控制面 action,server 侧封装 runtime catalog,
745
+ # 避开 CLI 直连 runtime /v1/models 对 openclaw/hermes 的鉴权坑)拿模型列表 +
746
+ # 当前模型 metadata。失败 fallback 到 _fetch_tui_model_metadata(直连 /v1/models)。
747
+ model_list = asyncio.run(_fetch_tui_model_list(region=region, agent_id=agent_id))
748
+ if model_list:
749
+ runner.available_models = model_list.get("models") or []
750
+ current_id = model_list.get("current")
751
+ matched = None
752
+ if current_id:
753
+ matched = next((m for m in runner.available_models if m.get("id") == current_id), None)
754
+ if matched is None and runner.available_models:
755
+ matched = runner.available_models[0]
756
+ if matched:
757
+ setattr(runner, "model_metadata", matched)
758
+ # 用户未指定 --model 时,用 ListAgentModels 的 current 作为 runner.model,
759
+ # 让 TUI 启动即显示真实模型名(而非 unknown)。
760
+ if not runner.model and matched.get("id"):
761
+ runner.model = str(matched["id"])
762
+ else:
763
+ # fallback:直连 runtime /v1/models(旧路径,托管 runtime 常失败但不阻断)
764
+ model_metadata = asyncio.run(
765
+ _fetch_tui_model_metadata(endpoint=endpoint, api_key=api_key, model=model)
766
+ )
767
+ if model_metadata:
768
+ setattr(runner, "model_metadata", model_metadata)
686
769
 
687
- app = AgentTUI(
688
- runner=runner,
689
- show_thinking=show_thinking,
690
- project_dir=".",
691
- )
692
- app.run()
770
+ run_tui(runner, show_thinking=show_thinking, project_dir=".", no_alt_screen=no_alt_screen)
771
+
772
+
773
+ async def _fetch_tui_model_list(
774
+ *,
775
+ region: str,
776
+ agent_id: str | None,
777
+ ) -> dict[str, Any] | None:
778
+ """通过控制面 ListAgentModels action 拿 Agent 可选模型列表(对标 hosted UI)。
779
+
780
+ 需 KSYUN access/secret key(env),与 _fetch_remote_access 同套鉴权。
781
+ 失败返回 None(不阻断 TUI,由 _fetch_tui_model_metadata fallback)。
782
+ """
783
+ if not agent_id:
784
+ return None
785
+ try:
786
+ from ksadk.api.client import AgentEngineClient
787
+
788
+ async with AgentEngineClient(region=region) as client:
789
+ payload = await client.list_agent_models(agent_id=agent_id)
790
+ return payload if isinstance(payload, dict) else None
791
+ except Exception:
792
+ return None
793
+
794
+
795
+ async def _fetch_tui_model_metadata(
796
+ *,
797
+ endpoint: str,
798
+ api_key: str | None,
799
+ model: str | None,
800
+ ) -> dict[str, Any] | None:
801
+ try:
802
+ from ksadk.cli.model_catalog import (
803
+ fetch_provider_model_catalog,
804
+ find_model_in_catalog,
805
+ )
806
+ from ksadk.conversations.model_context import normalize_model_metadata
807
+
808
+ catalog = await fetch_provider_model_catalog(
809
+ api_base=endpoint,
810
+ api_key=api_key,
811
+ timeout=3.0,
812
+ )
813
+ except Exception:
814
+ return None
815
+
816
+ if not catalog:
817
+ return None
818
+ raw_models = [item.get("_provider_raw_model") or item for item in catalog]
819
+ matched = find_model_in_catalog(raw_models, model)
820
+ if matched is not None:
821
+ metadata = normalize_model_metadata(matched)
822
+ return metadata if isinstance(metadata, dict) else None
823
+ if model:
824
+ return None
825
+ if len(catalog) == 1:
826
+ item = dict(catalog[0])
827
+ item.pop("_provider_raw_model", None)
828
+ return item
829
+ return None
693
830
 
694
831
 
695
832
  def _load_state() -> dict:
696
833
  """从 .agentengine.state 加载状态"""
697
834
  import yaml
835
+
698
836
  state_file = Path(".") / ".agentengine.state"
699
837
  if state_file.exists():
700
838
  try:
701
- with open(state_file, 'r', encoding='utf-8') as f:
839
+ with open(state_file, "r", encoding="utf-8") as f:
702
840
  return yaml.safe_load(f) or {}
703
841
  except Exception:
704
842
  pass
@@ -726,15 +864,13 @@ def _extract_remote_access(detail: Dict[str, Any]) -> Dict[str, Any]:
726
864
  if not isinstance(detail, dict):
727
865
  return {}
728
866
 
729
- basic = detail.get("basic") if isinstance(detail.get("basic"), dict) else {}
730
- quick = detail.get("quick_access") if isinstance(detail.get("quick_access"), dict) else {}
731
-
732
- deployment = detail.get("deployment") if isinstance(detail.get("deployment"), dict) else {}
733
- framework = (
734
- deployment.get("framework")
735
- or basic.get("framework")
736
- or detail.get("framework")
737
- )
867
+ basic_value = detail.get("basic")
868
+ quick_value = detail.get("quick_access")
869
+ deployment_value = detail.get("deployment")
870
+ basic: Dict[str, Any] = basic_value if isinstance(basic_value, dict) else {}
871
+ quick: Dict[str, Any] = quick_value if isinstance(quick_value, dict) else {}
872
+ deployment: Dict[str, Any] = deployment_value if isinstance(deployment_value, dict) else {}
873
+ framework = deployment.get("framework") or basic.get("framework") or detail.get("framework")
738
874
 
739
875
  return {
740
876
  "agent_id": basic.get("agent_id") or detail.get("agent_id"),
@@ -752,9 +888,13 @@ async def _fetch_remote_access(target_agent: str, region: str) -> Dict[str, Any]
752
888
 
753
889
  async with AgentEngineClient(region=region) as client:
754
890
  try:
755
- return _extract_remote_access(await client.get_agent(agent_id=target_agent, include_api_key=True))
891
+ return _extract_remote_access(
892
+ await client.get_agent(agent_id=target_agent, include_api_key=True)
893
+ )
756
894
  except Exception:
757
- return _extract_remote_access(await client.get_agent(name=target_agent, include_api_key=True))
895
+ return _extract_remote_access(
896
+ await client.get_agent(name=target_agent, include_api_key=True)
897
+ )
758
898
 
759
899
 
760
900
  def _refresh_remote_access(
@@ -783,22 +923,20 @@ def _refresh_remote_access(
783
923
 
784
924
 
785
925
  def _is_hermes_target(state: dict, latest_access: dict) -> bool:
786
- framework = str(
787
- latest_access.get("framework")
788
- or state.get("framework")
789
- or state.get("type")
790
- or ""
791
- ).strip().lower()
926
+ framework = (
927
+ str(latest_access.get("framework") or state.get("framework") or state.get("type") or "")
928
+ .strip()
929
+ .lower()
930
+ )
792
931
  return framework == "hermes"
793
932
 
794
933
 
795
934
  def _is_openclaw_target(state: dict, latest_access: dict) -> bool:
796
- framework = str(
797
- latest_access.get("framework")
798
- or state.get("framework")
799
- or state.get("type")
800
- or ""
801
- ).strip().lower()
935
+ framework = (
936
+ str(latest_access.get("framework") or state.get("framework") or state.get("type") or "")
937
+ .strip()
938
+ .lower()
939
+ )
802
940
  return framework == "openclaw"
803
941
 
804
942
 
@@ -812,13 +950,17 @@ def _mask_secret(secret: str | None) -> str:
812
950
 
813
951
 
814
952
  def _openclaw_auth_mode(state: dict, latest_access: dict) -> str:
815
- return str(
816
- latest_access.get("openclaw_auth_mode")
817
- or latest_access.get("gateway_auth_mode")
818
- or state.get("openclaw_auth_mode")
819
- or state.get("gateway_auth_mode")
820
- or ""
821
- ).strip().lower()
953
+ return (
954
+ str(
955
+ latest_access.get("openclaw_auth_mode")
956
+ or latest_access.get("gateway_auth_mode")
957
+ or state.get("openclaw_auth_mode")
958
+ or state.get("gateway_auth_mode")
959
+ or ""
960
+ )
961
+ .strip()
962
+ .lower()
963
+ )
822
964
 
823
965
 
824
966
  def _select_runtime_api_key(
@@ -846,26 +988,28 @@ def _select_runtime_api_key(
846
988
  auth_mode = _openclaw_auth_mode(state, latest_access)
847
989
  if auth_mode in {"token", "password"}:
848
990
  raise click.ClickException(
849
- "当前 OpenClaw Gateway 为 token/password 模式,agentengine invoke 需要 OpenClaw Gateway token。\n"
991
+ "当前 OpenClaw Gateway 为 token/password 模式,"
992
+ "agentengine invoke 需要 OpenClaw Gateway token。\n"
850
993
  "请使用: agentengine invoke --gateway-token <token>\n"
851
994
  "或设置: OPENCLAW_GATEWAY_TOKEN=<token> agentengine invoke\n"
852
- "如果部署时传过 OPENCLAW_GATEWAY_TOKEN,请重新运行部署让本地 .agentengine.state 记录该 token。\n"
853
- "注意:这里不是 AgentEngine API Key(ak-*),而是 OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD。"
995
+ "如果部署时传过 OPENCLAW_GATEWAY_TOKEN,请重新运行部署让本地 "
996
+ ".agentengine.state 记录该 token。\n"
997
+ "注意:这里不是 AgentEngine API Key(ak-*),而是 "
998
+ "OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD。"
854
999
  )
855
1000
 
856
1001
  return api_key
857
1002
 
858
1003
 
859
1004
  def _select_remote_api_format(state: dict, latest_access: dict) -> str:
860
- framework = str(
861
- latest_access.get("framework")
862
- or state.get("framework")
863
- or state.get("type")
864
- or ""
865
- ).strip().lower()
866
- if framework in {"openclaw", "hermes"}:
867
- return "responses"
868
- return "chat_completions"
1005
+ """auto 模式下所有框架首选 responses(probe 不通再回退 chat_completions)。"""
1006
+ return "responses"
1007
+
1008
+
1009
+ # probe 结果进程内短缓存,key=endpoint,value=(过期时间戳, 是否暴露 responses)。
1010
+ # 避免每次 invoke 都多打一跳 GET /v1/responses。
1011
+ _RESPONSES_ROUTE_CACHE: dict[tuple, tuple[float, bool]] = {}
1012
+ _RESPONSES_ROUTE_CACHE_TTL = 30.0
869
1013
 
870
1014
 
871
1015
  async def _resolve_remote_api_format(
@@ -876,23 +1020,65 @@ async def _resolve_remote_api_format(
876
1020
  insecure: bool,
877
1021
  state: dict,
878
1022
  latest_access: dict,
1023
+ api_format: str | None = None,
879
1024
  ) -> str:
880
- api_format = _select_remote_api_format(state, latest_access)
881
- if api_format != "responses" or not _is_openclaw_target(state, latest_access):
882
- return api_format
883
-
1025
+ # 显式指定 api_format(非 auto)时直接用,不 probe。
1026
+ explicit = str(api_format or "").strip().lower()
1027
+ if explicit in {"responses", "chat_completions"}:
1028
+ return explicit
1029
+
1030
+ # auto:按框架决定首选格式,probe /v1/responses 不通回退 chat_completions。
1031
+ preferred = _select_remote_api_format(state, latest_access)
1032
+ if preferred != "responses":
1033
+ return preferred
884
1034
  probe_api_key = runtime_api_key if runtime_api_key is not None else api_key
885
- if await _probe_openclaw_responses_route(endpoint=endpoint, api_key=probe_api_key, insecure=insecure):
886
- return api_format
1035
+ # openclaw/hermes 的 responses 用独立 gateway 鉴权头,GET 401 不代表 POST 401,
1036
+ # 保留 401=可用的既有行为;普通框架 401=鉴权不通,回退 chat。
1037
+ treat_401 = _is_openclaw_target(state, latest_access) or _is_hermes_target(state, latest_access)
1038
+ if await _probe_responses_route_cached(
1039
+ endpoint=endpoint,
1040
+ api_key=probe_api_key,
1041
+ insecure=insecure,
1042
+ treat_401_as_available=treat_401,
1043
+ ):
1044
+ return "responses"
1045
+ return "chat_completions"
1046
+
887
1047
 
888
- raise click.ClickException(
889
- "当前 OpenClaw endpoint 未暴露 /v1/responses,不能使用 agentengine invoke 的 HTTP TUI。\n"
890
- "可先使用: agentengine dashboard open\n"
891
- "如果要命令行交互,请升级/修复 OpenClaw 镜像或 bootstrap,使 Gateway 暴露 OpenResponses API。"
1048
+ # probe 结果进程内短缓存。cache key 含 treat_401:同一 endpoint 在 openclaw/hermes
1049
+ # 与普通框架下对 401 的解读不同,分开缓存避免串味。
1050
+ async def _probe_responses_route_cached(
1051
+ *,
1052
+ endpoint: str,
1053
+ api_key: str | None,
1054
+ insecure: bool,
1055
+ treat_401_as_available: bool = False,
1056
+ ) -> bool:
1057
+ import time as _time
1058
+
1059
+ cache_key = (endpoint.rstrip("/"), treat_401_as_available)
1060
+ now = _time.time()
1061
+ cached = _RESPONSES_ROUTE_CACHE.get(cache_key)
1062
+ if cached and cached[0] > now:
1063
+ return cached[1]
1064
+
1065
+ available = await _probe_responses_route(
1066
+ endpoint=endpoint,
1067
+ api_key=api_key,
1068
+ insecure=insecure,
1069
+ treat_401_as_available=treat_401_as_available,
892
1070
  )
1071
+ _RESPONSES_ROUTE_CACHE[cache_key] = (now + _RESPONSES_ROUTE_CACHE_TTL, available)
1072
+ return available
893
1073
 
894
1074
 
895
- async def _probe_openclaw_responses_route(*, endpoint: str, api_key: str | None, insecure: bool) -> bool:
1075
+ async def _probe_responses_route(
1076
+ *,
1077
+ endpoint: str,
1078
+ api_key: str | None,
1079
+ insecure: bool,
1080
+ treat_401_as_available: bool = False,
1081
+ ) -> bool:
896
1082
  try:
897
1083
  import httpx
898
1084
  except ImportError:
@@ -912,12 +1098,19 @@ async def _probe_openclaw_responses_route(*, endpoint: str, api_key: str | None,
912
1098
  except Exception:
913
1099
  return False
914
1100
 
915
- # POST-only API routes usually answer GET with 405/422/400. A 404 or HTML 200
916
- # means the request fell through to the OpenClaw UI/router instead of the API.
917
- return response.status_code in {400, 401, 405, 422}
1101
+ # POST-only API routes usually answer GET with 405/422/400 = 路由存在且鉴权通过。
1102
+ # 401 = 未鉴权:带 key 仍 401 说明该 key 不被 responses 路由接受,走 responses 也会 401,
1103
+ # 应回退 chat。openclaw/hermes 的 responses 用独立 gateway 鉴权头,GET 401 不代表 POST 401,
1104
+ # 由 treat_401_as_available 保留其既有行为。404/HTML 200 = 路由不存在。
1105
+ available = response.status_code in {400, 405, 422}
1106
+ if treat_401_as_available and response.status_code == 401:
1107
+ available = True
1108
+ return available
918
1109
 
919
1110
 
920
- def _should_use_hermes_native_tui(*, transport: str, local: bool, state: dict, latest_access: dict) -> bool:
1111
+ def _should_use_hermes_native_tui(
1112
+ *, transport: str, local: bool, state: dict, latest_access: dict
1113
+ ) -> bool:
921
1114
  if transport == "chat":
922
1115
  return False
923
1116
  if transport == "native":
@@ -927,7 +1120,9 @@ def _should_use_hermes_native_tui(*, transport: str, local: bool, state: dict, l
927
1120
  return _is_hermes_target(state, latest_access)
928
1121
 
929
1122
 
930
- def _should_use_openclaw_native_tui(*, transport: str, local: bool, state: dict, latest_access: dict) -> bool:
1123
+ def _should_use_openclaw_native_tui(
1124
+ *, transport: str, local: bool, state: dict, latest_access: dict
1125
+ ) -> bool:
931
1126
  if transport == "chat":
932
1127
  return False
933
1128
  if transport == "native":
@@ -939,14 +1134,16 @@ def _should_use_openclaw_native_tui(*, transport: str, local: bool, state: dict,
939
1134
 
940
1135
  def _invoke_hermes_terminal_tui(
941
1136
  endpoint: str,
942
- api_key: str = None,
943
- session_id: str = None,
1137
+ api_key: Optional[str] = None,
1138
+ session_id: Optional[str] = None,
944
1139
  insecure: bool = False,
945
1140
  cwd: str | None = None,
946
1141
  ):
947
1142
  click.secho("🖥️ Hermes Native Remote TUI", fg="blue", bold=True)
948
1143
  click.echo(" 退出: Ctrl-D 或 Ctrl-C")
949
- click.echo(" 新 Pod 首次启动会加载 Hermes tools/skills,可能需要几十秒到 1 分钟;后续进入会更快。")
1144
+ click.echo(
1145
+ " 新 Pod 首次启动会加载 Hermes tools/skills,可能需要几十秒到 1 分钟;后续进入会更快。"
1146
+ )
950
1147
  try:
951
1148
  _warmup_hermes_terminal(
952
1149
  endpoint=endpoint,
@@ -1005,8 +1202,8 @@ def _warmup_hermes_terminal(
1005
1202
 
1006
1203
  def _invoke_openclaw_terminal_tui(
1007
1204
  endpoint: str,
1008
- api_key: str = None,
1009
- session_id: str = None,
1205
+ api_key: Optional[str] = None,
1206
+ session_id: Optional[str] = None,
1010
1207
  insecure: bool = False,
1011
1208
  ):
1012
1209
  click.secho("🖥️ OpenClaw Native Remote TUI", fg="blue", bold=True)
@@ -1039,15 +1236,16 @@ def _get_api_key() -> Optional[str]:
1039
1236
 
1040
1237
  def _get_endpoint(agent_ref: str, region: str) -> str:
1041
1238
  """获取 Agent Endpoint(先按 ID,再按名称)"""
1042
- from ksadk.api import AgentEngineClient
1043
1239
  import asyncio
1044
1240
 
1045
- async def _get():
1241
+ from ksadk.api import AgentEngineClient
1242
+
1243
+ async def _get() -> str:
1046
1244
  async with AgentEngineClient(region=region) as client:
1047
1245
  # 1) 优先按 ID 查询
1048
1246
  try:
1049
1247
  res = await client.get_agent(agent_id=agent_ref)
1050
- endpoint = _extract_remote_access(res).get("endpoint", "")
1248
+ endpoint = str(_extract_remote_access(res).get("endpoint") or "").strip()
1051
1249
  if endpoint:
1052
1250
  return endpoint
1053
1251
  except Exception:
@@ -1055,13 +1253,14 @@ def _get_endpoint(agent_ref: str, region: str) -> str:
1055
1253
 
1056
1254
  # 2) 回退按名称查询
1057
1255
  res = await client.get_agent(name=agent_ref)
1058
- endpoint = _extract_remote_access(res).get("endpoint", "")
1256
+ endpoint = str(_extract_remote_access(res).get("endpoint") or "").strip()
1059
1257
  if endpoint:
1060
1258
  return endpoint
1061
1259
 
1062
1260
  # endpoint 为空时,尽量提取真实 ID 供默认域名拼接
1063
- basic = res.get("basic", {})
1064
- return basic.get("agent_id") or res.get("agent_id") or ""
1261
+ basic_value = res.get("basic")
1262
+ basic = basic_value if isinstance(basic_value, dict) else {}
1263
+ return str(basic.get("agent_id") or res.get("agent_id") or "")
1065
1264
 
1066
1265
  try:
1067
1266
  resolved = asyncio.run(_get())
@@ -1081,16 +1280,16 @@ def _get_endpoint(agent_ref: str, region: str) -> str:
1081
1280
  async def _invoke_once(
1082
1281
  endpoint: str,
1083
1282
  message: str,
1084
- api_key: str = None,
1085
- session_id: str = None,
1283
+ api_key: Optional[str] = None,
1284
+ session_id: Optional[str] = None,
1086
1285
  stream: bool = True,
1087
1286
  insecure: bool = False,
1088
- model: str = None,
1287
+ model: Optional[str] = None,
1089
1288
  api_format: str = "chat_completions",
1090
1289
  ):
1091
1290
  """单次调用"""
1092
1291
  click.echo(f"\n👤 你: {message}")
1093
- click.echo(f"🤖 Agent: ", nl=False)
1292
+ click.echo("🤖 Agent: ", nl=False)
1094
1293
 
1095
1294
  try:
1096
1295
  if stream:
@@ -1098,12 +1297,19 @@ async def _invoke_once(
1098
1297
  if Live and Markdown:
1099
1298
  # 降低刷新率减少闪烁,vertical_overflow="visible"防止回滚丢失
1100
1299
  # 手动控制刷新以减少闪烁
1101
- with Live(Markdown("", justify="left"), console=console, auto_refresh=False, vertical_overflow="visible") as live:
1102
- last_refresh_time = 0
1300
+ with Live(
1301
+ Markdown("", justify="left"),
1302
+ console=console,
1303
+ auto_refresh=False,
1304
+ vertical_overflow="visible",
1305
+ ) as live:
1306
+ last_refresh_time = 0.0
1103
1307
  full_reasoning = ""
1104
- async for chunk in _stream_chat(endpoint, message, api_key, session_id, True, insecure, model, api_format):
1308
+ async for chunk in _stream_chat(
1309
+ endpoint, message, api_key, session_id, True, insecure, model, api_format
1310
+ ):
1105
1311
  content, reasoning = _extract_content(chunk)
1106
-
1312
+
1107
1313
  updated = False
1108
1314
  if reasoning:
1109
1315
  full_reasoning += reasoning
@@ -1111,25 +1317,27 @@ async def _invoke_once(
1111
1317
  if content:
1112
1318
  full_response += content
1113
1319
  updated = True
1114
-
1320
+
1115
1321
  if updated:
1116
1322
  # 构造显示文本
1117
1323
  display_text = ""
1118
1324
  if full_reasoning:
1119
- formatted_reasoning = full_reasoning.replace('\n', '\n> ')
1325
+ formatted_reasoning = full_reasoning.replace("\n", "\n> ")
1120
1326
  display_text += f"> 🧠 **Thinking:**\n> {formatted_reasoning}\n\n"
1121
1327
  display_text += full_response
1122
-
1328
+
1123
1329
  live.update(Markdown(display_text, justify="left"))
1124
-
1330
+
1125
1331
  # 基于时间限流刷新 (每0.2秒一次 = 5 FPS)
1126
1332
  now = time.time()
1127
1333
  if now - last_refresh_time > 0.2:
1128
1334
  live.refresh()
1129
1335
  last_refresh_time = now
1130
- live.refresh() # 确保最后一次刷新
1336
+ live.refresh() # 确保最后一次刷新
1131
1337
  else:
1132
- async for chunk in _stream_chat(endpoint, message, api_key, session_id, True, insecure, model, api_format):
1338
+ async for chunk in _stream_chat(
1339
+ endpoint, message, api_key, session_id, True, insecure, model, api_format
1340
+ ):
1133
1341
  content, reasoning = _extract_content(chunk)
1134
1342
  if reasoning:
1135
1343
  click.secho(reasoning, fg="bright_black", nl=False)
@@ -1137,7 +1345,9 @@ async def _invoke_once(
1137
1345
  print(content, end="", flush=True)
1138
1346
  click.echo() # 换行
1139
1347
  else:
1140
- response = await _chat(endpoint, message, api_key, session_id, insecure, model, api_format)
1348
+ response = await _chat(
1349
+ endpoint, message, api_key, session_id, insecure, model, api_format
1350
+ )
1141
1351
  content = _extract_response_content(response)
1142
1352
  if console and Markdown:
1143
1353
  console.print(Markdown(content))
@@ -1147,16 +1357,15 @@ async def _invoke_once(
1147
1357
  click.secho(f"\n❌ 调用失败: {e}", fg="red")
1148
1358
 
1149
1359
 
1150
-
1151
1360
  async def _chat(
1152
1361
  endpoint: str,
1153
1362
  message: str,
1154
- api_key: str = None,
1155
- session_id: str = None,
1363
+ api_key: Optional[str] = None,
1364
+ session_id: Optional[str] = None,
1156
1365
  insecure: bool = False,
1157
- model: str = None,
1366
+ model: Optional[str] = None,
1158
1367
  api_format: str = "chat_completions",
1159
- ) -> dict:
1368
+ ) -> dict[str, Any]:
1160
1369
  """非流式调用 (OpenAI 兼容格式)"""
1161
1370
  try:
1162
1371
  import httpx
@@ -1167,10 +1376,16 @@ async def _chat(
1167
1376
  normalized_api_format = str(api_format or "chat_completions").strip().lower()
1168
1377
  if normalized_api_format == "responses":
1169
1378
  url = f"{endpoint.rstrip('/')}/v1/responses"
1170
- payload = {"input": [{"role": "user", "content": message}], "stream": False}
1379
+ payload: dict[str, Any] = {
1380
+ "input": [{"role": "user", "content": message}],
1381
+ "stream": False,
1382
+ }
1171
1383
  else:
1172
1384
  url = f"{endpoint.rstrip('/')}/v1/chat/completions"
1173
- payload = {"messages": [{"role": "user", "content": message}], "stream": False}
1385
+ payload = {
1386
+ "messages": [{"role": "user", "content": message}],
1387
+ "stream": False,
1388
+ }
1174
1389
 
1175
1390
  if session_id:
1176
1391
  payload["session_id"] = session_id
@@ -1184,31 +1399,37 @@ async def _chat(
1184
1399
  is_local = "localhost" in url or "127.0.0.1" in url or "0.0.0.0" in url
1185
1400
 
1186
1401
  # 构造 httpx client 配置
1187
- client_kwargs = {"timeout": 60, "trust_env": not is_local}
1402
+ client_kwargs: dict[str, Any] = {
1403
+ "timeout": 60,
1404
+ "trust_env": not is_local,
1405
+ }
1188
1406
 
1189
1407
  # 如果指定了 --insecure 参数,跳过 SSL 证书验证(类似 curl -k)
1190
1408
  if insecure:
1191
1409
  client_kwargs["verify"] = False
1192
1410
 
1193
1411
  # 构造 Headers
1194
- headers = {}
1412
+ headers: dict[str, str] = {}
1195
1413
  if api_key:
1196
1414
  headers["Authorization"] = f"Bearer {api_key}"
1197
1415
 
1198
1416
  async with httpx.AsyncClient(**client_kwargs) as client:
1199
1417
  response = await client.post(url, json=payload, headers=headers)
1200
1418
  response.raise_for_status()
1201
- return response.json()
1419
+ response_payload = response.json()
1420
+ if not isinstance(response_payload, dict):
1421
+ raise ValueError("Agent response must be a JSON object")
1422
+ return response_payload
1202
1423
 
1203
1424
 
1204
1425
  async def _stream_chat(
1205
1426
  endpoint: str,
1206
1427
  message: str,
1207
- api_key: str = None,
1208
- session_id: str = None,
1428
+ api_key: Optional[str] = None,
1429
+ session_id: Optional[str] = None,
1209
1430
  is_once: bool = False,
1210
1431
  insecure: bool = False,
1211
- model: str = None,
1432
+ model: Optional[str] = None,
1212
1433
  api_format: str = "chat_completions",
1213
1434
  ):
1214
1435
  """流式调用 (SSE)"""
@@ -1221,7 +1442,10 @@ async def _stream_chat(
1221
1442
  normalized_api_format = str(api_format or "chat_completions").strip().lower()
1222
1443
  if normalized_api_format == "responses":
1223
1444
  url = f"{endpoint.rstrip('/')}/v1/responses"
1224
- payload = {"input": [{"role": "user", "content": message}], "stream": True}
1445
+ payload: dict[str, Any] = {
1446
+ "input": [{"role": "user", "content": message}],
1447
+ "stream": True,
1448
+ }
1225
1449
  else:
1226
1450
  url = f"{endpoint.rstrip('/')}/v1/chat/completions"
1227
1451
  payload = {"messages": [{"role": "user", "content": message}], "stream": True}
@@ -1238,14 +1462,17 @@ async def _stream_chat(
1238
1462
  is_local = "localhost" in url or "127.0.0.1" in url or "0.0.0.0" in url
1239
1463
 
1240
1464
  # 构造 httpx client 配置
1241
- client_kwargs = {"timeout": 60, "trust_env": not is_local}
1465
+ client_kwargs: dict[str, Any] = {
1466
+ "timeout": 60,
1467
+ "trust_env": not is_local,
1468
+ }
1242
1469
 
1243
1470
  # 如果指定了 --insecure 参数,跳过 SSL 证书验证(类似 curl -k)
1244
1471
  if insecure:
1245
1472
  client_kwargs["verify"] = False
1246
1473
 
1247
1474
  # 构造 Headers
1248
- headers = {}
1475
+ headers: dict[str, str] = {}
1249
1476
  if api_key:
1250
1477
  headers["Authorization"] = f"Bearer {api_key}"
1251
1478
 
@@ -1270,6 +1497,8 @@ async def _stream_chat(
1270
1497
 
1271
1498
  try:
1272
1499
  data = json.loads(data_str)
1500
+ if not isinstance(data, dict):
1501
+ continue
1273
1502
  if current_event and isinstance(data, dict):
1274
1503
  data = {**data, "_event": current_event}
1275
1504
  # 直接 yield 解析后的 JSON 数据,让 _extract_content 处理
@@ -1330,7 +1559,7 @@ def _extract_response_content(response: dict) -> str:
1330
1559
  choices = response.get("choices", [])
1331
1560
  if choices:
1332
1561
  message = choices[0].get("message", {})
1333
- return message.get("content", "")
1562
+ return str(message.get("content") or "")
1334
1563
  except (KeyError, IndexError):
1335
1564
  pass
1336
1565
  return str(response)