agentengine-sdk-python 0.6.9__py3-none-any.whl → 0.7.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 (149) hide show
  1. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/METADATA +6 -4
  2. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/RECORD +123 -128
  3. ksadk/api/client.py +19 -0
  4. ksadk/cli/cmd_create.py +34 -7
  5. ksadk/cli/cmd_hermes.py +6 -2
  6. ksadk/cli/cmd_invoke.py +208 -41
  7. ksadk/cli/cmd_mcp.py +6 -1
  8. ksadk/cli/cmd_openclaw.py +6 -2
  9. ksadk/cli/cmd_run.py +13 -6
  10. ksadk/cli/network_options.py +5 -3
  11. ksadk/configs/env_registry.py +1 -0
  12. ksadk/conversations/message_projection.py +269 -0
  13. ksadk/conversations/model_context.py +11 -1
  14. ksadk/conversations/runtime.py +81 -13
  15. ksadk/deployment/base.py +1 -1
  16. ksadk/deployment/providers/serverless.py +18 -8
  17. ksadk/memory/adk/resilient_session_service.py +268 -0
  18. ksadk/memory/adk/short_term_memory.py +21 -6
  19. ksadk/runners/adk_runner.py +974 -115
  20. ksadk/runners/langchain_runner.py +219 -7
  21. ksadk/runners/langgraph_runner.py +145 -12
  22. ksadk/runners/remote_runner.py +143 -18
  23. ksadk/server/app.py +148 -6
  24. ksadk/server/static/assets/{ArtifactsPanel-C0meuOdi.js → ArtifactsPanel-D-SSn2ee.js} +1 -1
  25. ksadk/server/static/assets/{CodeBlock-BbI4HPtJ.js → CodeBlock-P16VOLYi.js} +1 -1
  26. ksadk/server/static/assets/{MathMessageMarkdown-DMLQYoLp.js → MathMessageMarkdown-CdGiQWHR.js} +1 -1
  27. ksadk/server/static/assets/{MermaidBlock-DCR8WZ17.js → MermaidBlock-IOcal8cm.js} +6 -6
  28. ksadk/server/static/assets/{NativeTerminalPanel-CKUvWhUz.js → NativeTerminalPanel-DGSDWsei.js} +1 -1
  29. ksadk/server/static/assets/{arc-bj2rZnP5.js → arc-BA13sHc9.js} +1 -1
  30. ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +1 -0
  31. ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-2u1ltDIj.js → architectureDiagram-3BPJPVTR-DzJzgVoL.js} +1 -1
  32. ksadk/server/static/assets/{blockDiagram-GPEHLZMM-D6fvdK20.js → blockDiagram-GPEHLZMM-UTdVcZhn.js} +1 -1
  33. ksadk/server/static/assets/{c4Diagram-AAUBKEIU-BK82L5n5.js → c4Diagram-AAUBKEIU-Dix0GD4m.js} +1 -1
  34. ksadk/server/static/assets/channel-CTuydARX.js +1 -0
  35. ksadk/server/static/assets/{chunk-2J33WTMH-DLVdkeyQ.js → chunk-2J33WTMH-C2JxuZ42.js} +1 -1
  36. ksadk/server/static/assets/{chunk-4BX2VUAB-BbwsYcbW.js → chunk-4BX2VUAB-Be57QPgD.js} +1 -1
  37. ksadk/server/static/assets/{chunk-55IACEB6-DOairxzw.js → chunk-55IACEB6-WjSuy4nv.js} +1 -1
  38. ksadk/server/static/assets/{chunk-727SXJPM-hCO487gs.js → chunk-727SXJPM-BOovP-b5.js} +1 -1
  39. ksadk/server/static/assets/{chunk-AQP2D5EJ-XU8vDOuG.js → chunk-AQP2D5EJ-DOmZNmWj.js} +1 -1
  40. ksadk/server/static/assets/{chunk-FMBD7UC4-PrapyfMu.js → chunk-FMBD7UC4-DzwlodkU.js} +1 -1
  41. ksadk/server/static/assets/{chunk-ND2GUHAM-CboKhTZ3.js → chunk-ND2GUHAM-DMqAikEU.js} +1 -1
  42. ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +1 -0
  43. ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +1 -0
  44. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +1 -0
  45. ksadk/server/static/assets/{cose-bilkent-S5V4N54A-_N8bJ-EU.js → cose-bilkent-S5V4N54A-Zhy2GBnP.js} +1 -1
  46. ksadk/server/static/assets/{dagre-BM42HDAG-FoDlo5J2.js → dagre-BM42HDAG-CbKrtIUz.js} +1 -1
  47. ksadk/server/static/assets/{diagram-2AECGRRQ-BBp6ebBw.js → diagram-2AECGRRQ-BRApgQRK.js} +1 -1
  48. ksadk/server/static/assets/{diagram-5GNKFQAL-CqOvG1PR.js → diagram-5GNKFQAL-D0xbnDiK.js} +1 -1
  49. ksadk/server/static/assets/{diagram-KO2AKTUF-O9T3t6ba.js → diagram-KO2AKTUF-CVugVI9L.js} +1 -1
  50. ksadk/server/static/assets/{diagram-LMA3HP47-CGG8ekBO.js → diagram-LMA3HP47-DozUGgee.js} +1 -1
  51. ksadk/server/static/assets/{diagram-OG6HWLK6-4BmH1EEg.js → diagram-OG6HWLK6-ZOYsKlru.js} +1 -1
  52. ksadk/server/static/assets/{dist-BmuTWcm7.js → dist-2hInLS_J.js} +1 -1
  53. ksadk/server/static/assets/{dist-BMKM66B4.js → dist-8EEyGgDx.js} +1 -1
  54. ksadk/server/static/assets/{dist-Cp1_jKwf.js → dist-8_SfK1IP.js} +1 -1
  55. ksadk/server/static/assets/{dist-CZ6C3x8B.js → dist-B0NP_Z0z.js} +1 -1
  56. ksadk/server/static/assets/{dist-DLeM1OCK.js → dist-BIJVDMuB.js} +1 -1
  57. ksadk/server/static/assets/{dist-Cwhk4lwI.js → dist-BmlMPsCc.js} +1 -1
  58. ksadk/server/static/assets/{dist-D99wf6_X.js → dist-BrCcQ47T.js} +1 -1
  59. ksadk/server/static/assets/{dist-CWiTDN_V.js → dist-BssVmjIo.js} +1 -1
  60. ksadk/server/static/assets/{dist-BVzcWa6D.js → dist-C3HIIaN5.js} +1 -1
  61. ksadk/server/static/assets/{dist-DM0rEQPk.js → dist-CPBtqRTW.js} +1 -1
  62. ksadk/server/static/assets/{dist-BkJjNih3.js → dist-CkEKJFtk.js} +1 -1
  63. ksadk/server/static/assets/{dist-B67uF2uK.js → dist-D-JMdWPQ.js} +1 -1
  64. ksadk/server/static/assets/{dist-DI-oVAXw.js → dist-D-xOD048.js} +1 -1
  65. ksadk/server/static/assets/{dist-6kSZvCZh.js → dist-D57wnGWd.js} +1 -1
  66. ksadk/server/static/assets/{dist-Bk1l05Ni.js → dist-DL_NKc_z.js} +1 -1
  67. ksadk/server/static/assets/{dist-DbaH3p5h.js → dist-DMezvBGJ.js} +1 -1
  68. ksadk/server/static/assets/{dist-CiZHB6ZW.js → dist-DmFaq_Iy.js} +1 -1
  69. ksadk/server/static/assets/{dist-DLMjL01k.js → dist-Dp0k7bWI.js} +1 -1
  70. ksadk/server/static/assets/{dist-DCpZrjaK.js → dist-DpKtt4Bo.js} +1 -1
  71. ksadk/server/static/assets/{dist-D5JGcxBx.js → dist-DyOkZmze.js} +1 -1
  72. ksadk/server/static/assets/{dist-C2DKRFVK.js → dist-DyWsb78Q.js} +1 -1
  73. ksadk/server/static/assets/{dist-S4klkK5W.js → dist-gX6GOLKq.js} +1 -1
  74. ksadk/server/static/assets/{erDiagram-TEJ5UH35-oJhaci7S.js → erDiagram-TEJ5UH35-Bk-9bgE5.js} +1 -1
  75. ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +1 -0
  76. ksadk/server/static/assets/{flowDiagram-I6XJVG4X-DtTmUan-.js → flowDiagram-I6XJVG4X-Dn9kZVuZ.js} +1 -1
  77. ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BSsb_eij.js → ganttDiagram-6RSMTGT7-BGxXlJqm.js} +1 -1
  78. ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +1 -0
  79. ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-BR5s6Pws.js → gitGraphDiagram-PVQCEYII-C7Q0X-wi.js} +1 -1
  80. ksadk/server/static/assets/{index-BH7-pC0B.js → index-C00Lwg3L.js} +6 -6
  81. ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +1 -0
  82. ksadk/server/static/assets/{infoDiagram-5YYISTIA-DYPc5cvh.js → infoDiagram-5YYISTIA-BO_CSqq5.js} +1 -1
  83. ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-MoBdALDg.js → ishikawaDiagram-YF4QCWOH-eYLPLd46.js} +1 -1
  84. ksadk/server/static/assets/{journeyDiagram-JHISSGLW-DgLiQT8l.js → journeyDiagram-JHISSGLW-De9geSb0.js} +1 -1
  85. ksadk/server/static/assets/{kanban-definition-UN3LZRKU-CsFx112F.js → kanban-definition-UN3LZRKU-Cs80kQto.js} +1 -1
  86. ksadk/server/static/assets/{katex-DRsiwpM2.js → katex-DSrCFD8n.js} +1 -1
  87. ksadk/server/static/assets/{linear-TNRQ5Orv.js → linear-CN9sbKTF.js} +1 -1
  88. ksadk/server/static/assets/{mermaid-parser.core-D2eMVOgo.js → mermaid-parser.core-DlgSBU8l.js} +2 -2
  89. ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-BtByYFhL.js → mindmap-definition-RKZ34NQL-9afz7Oug.js} +1 -1
  90. ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +1 -0
  91. ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +1 -0
  92. ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPnuCB8c.js → pieDiagram-4H26LBE5-DPv_VstN.js} +1 -1
  93. ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-miFpCPv4.js → quadrantDiagram-W4KKPZXB-o77wZ1XK.js} +1 -1
  94. ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +1 -0
  95. ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-mjB_yYkW.js → requirementDiagram-4Y6WPE33-BYVw81VC.js} +1 -1
  96. ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-D7hSlZZZ.js → sankeyDiagram-5OEKKPKP-B7nZFMKT.js} +1 -1
  97. ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-D9Cx6hC3.js → sequenceDiagram-3UESZ5HK-BGRRrFw5.js} +1 -1
  98. ksadk/server/static/assets/{stateDiagram-AJRCARHV-D_zrg47o.js → stateDiagram-AJRCARHV-BUfHCoPs.js} +1 -1
  99. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +1 -0
  100. ksadk/server/static/assets/{timeline-definition-PNZ67QCA-CwSec0Wa.js → timeline-definition-PNZ67QCA-FbCTGrf9.js} +1 -1
  101. ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +1 -0
  102. ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +1 -0
  103. ksadk/server/static/assets/{vennDiagram-CIIHVFJN-agCVobiF.js → vennDiagram-CIIHVFJN-DWgwtPsJ.js} +1 -1
  104. ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +1 -0
  105. ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-TmrlhWNt.js → wardleyDiagram-YWT4CUSO-2x8G-tTM.js} +1 -1
  106. ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-DV71V-ah.js → xychartDiagram-2RQKCTM6-Drma3AYl.js} +1 -1
  107. ksadk/server/static/index.html +1 -1
  108. ksadk/sessions/__init__.py +17 -21
  109. ksadk/sessions/continuity.py +28 -4
  110. ksadk/sessions/postgres_service.py +77 -14
  111. ksadk/sessions/resilience.py +52 -0
  112. ksadk/sessions/resilient.py +360 -0
  113. ksadk/toolsets/__init__.py +2 -0
  114. ksadk/toolsets/workspace.py +49 -10
  115. ksadk/tracing/setup.py +63 -0
  116. ksadk/tui/__init__.py +22 -7
  117. ksadk/tui/loop.py +1877 -0
  118. ksadk/tui/stream_render.py +68 -0
  119. ksadk/version.py +1 -1
  120. ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +0 -1
  121. ksadk/server/static/assets/channel-BSSOwFIb.js +0 -1
  122. ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +0 -1
  123. ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +0 -1
  124. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +0 -1
  125. ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +0 -1
  126. ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +0 -1
  127. ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +0 -1
  128. ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +0 -1
  129. ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +0 -1
  130. ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +0 -1
  131. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +0 -1
  132. ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +0 -1
  133. ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +0 -1
  134. ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +0 -1
  135. ksadk/tui/app.py +0 -400
  136. ksadk/tui/clipboard.py +0 -100
  137. ksadk/tui/widgets/__init__.py +0 -22
  138. ksadk/tui/widgets/assistant.py +0 -89
  139. ksadk/tui/widgets/base.py +0 -54
  140. ksadk/tui/widgets/chat_input.py +0 -246
  141. ksadk/tui/widgets/history.py +0 -156
  142. ksadk/tui/widgets/system.py +0 -87
  143. ksadk/tui/widgets/thinking.py +0 -110
  144. ksadk/tui/widgets/tool_call.py +0 -161
  145. ksadk/tui/widgets/user.py +0 -37
  146. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/WHEEL +0 -0
  147. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/entry_points.txt +0 -0
  148. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/licenses/LICENSE +0 -0
  149. {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/top_level.txt +0 -0
@@ -110,9 +110,66 @@ class LangChainRunner(BaseRunner):
110
110
 
111
111
  accumulated_text = ""
112
112
  last_chunk: Any = None
113
+ final_output_text = ""
114
+ emitted_non_text_event = False
115
+ message_snapshots: dict[str, str] = {}
113
116
 
114
117
  try:
115
- if hasattr(self._agent, "astream"):
118
+ if self._should_stream_events(payload):
119
+ kwargs = self._build_optional_call_kwargs(
120
+ self._agent.astream_events,
121
+ config=config,
122
+ context=native_context,
123
+ )
124
+ kwargs["version"] = "v2"
125
+ async for event in self._agent.astream_events(payload, **kwargs):
126
+ if not isinstance(event, dict):
127
+ continue
128
+ event_kind = event.get("event", "")
129
+ data = event.get("data") or {}
130
+
131
+ if event_kind == "on_chat_model_stream":
132
+ chunk = data.get("chunk") if isinstance(data, dict) else None
133
+ if chunk is None:
134
+ continue
135
+ last_chunk = chunk
136
+ delta, chunk_type = self._extract_chunk(chunk)
137
+ if delta:
138
+ if chunk_type == "text":
139
+ accumulated_text += delta
140
+ else:
141
+ emitted_non_text_event = True
142
+ yield {"delta": delta, "type": chunk_type}
143
+ elif event_kind == "on_tool_start":
144
+ emitted_non_text_event = True
145
+ yield {
146
+ "type": "tool_call",
147
+ "tool_name": event.get("name", "unknown"),
148
+ "tool_args": data.get("input", {}) if isinstance(data, dict) else {},
149
+ "run_id": event.get("run_id"),
150
+ }
151
+ elif event_kind == "on_tool_end":
152
+ emitted_non_text_event = True
153
+ tool_output = data.get("output", "") if isinstance(data, dict) else ""
154
+ yield {
155
+ "type": "tool_result",
156
+ "tool_name": event.get("name", "unknown"),
157
+ "tool_args": data.get("input", {}) if isinstance(data, dict) else {},
158
+ "tool_output": (
159
+ tool_output
160
+ if isinstance(tool_output, dict)
161
+ else (str(tool_output) if tool_output else "")
162
+ ),
163
+ "run_id": event.get("run_id"),
164
+ }
165
+ elif event_kind == "on_chain_end":
166
+ output = data.get("output") if isinstance(data, dict) else None
167
+ extracted_output = self._extract_recognized_output(output)
168
+ if extracted_output:
169
+ final_output_text = extracted_output
170
+ if self._extract_usage(output):
171
+ last_chunk = output
172
+ elif hasattr(self._agent, "astream"):
116
173
  kwargs = self._build_optional_call_kwargs(
117
174
  self._agent.astream,
118
175
  config=config,
@@ -120,7 +177,15 @@ class LangChainRunner(BaseRunner):
120
177
  )
121
178
  async for chunk in self._agent.astream(payload, **kwargs):
122
179
  last_chunk = chunk
123
- delta, chunk_type = self._extract_chunk(chunk)
180
+ message_state = self._extract_message_state(chunk)
181
+ if message_state:
182
+ content, message_key = message_state
183
+ previous = message_snapshots.get(message_key, "")
184
+ delta = self._snapshot_delta(content, previous)
185
+ message_snapshots[message_key] = content
186
+ chunk_type = "text"
187
+ else:
188
+ delta, chunk_type = self._extract_chunk(chunk)
124
189
  if delta:
125
190
  accumulated_text += delta
126
191
  yield {"delta": delta, "type": chunk_type}
@@ -132,14 +197,32 @@ class LangChainRunner(BaseRunner):
132
197
  )
133
198
  for chunk in self._agent.stream(payload, **kwargs):
134
199
  last_chunk = chunk
135
- delta, chunk_type = self._extract_chunk(chunk)
200
+ message_state = self._extract_message_state(chunk)
201
+ if message_state:
202
+ content, message_key = message_state
203
+ previous = message_snapshots.get(message_key, "")
204
+ delta = self._snapshot_delta(content, previous)
205
+ message_snapshots[message_key] = content
206
+ chunk_type = "text"
207
+ else:
208
+ delta, chunk_type = self._extract_chunk(chunk)
136
209
  if delta:
137
210
  accumulated_text += delta
138
211
  yield {"delta": delta, "type": chunk_type}
139
212
  except Exception as exc:
140
- print(f"\n⚠️ 流式调用失败: {exc},回退到同步模式")
213
+ logger.warning("LangChain stream failed: %s", exc)
141
214
 
142
215
  if not accumulated_text:
216
+ if final_output_text or emitted_non_text_event:
217
+ final_chunk = {"output": final_output_text, "type": "final"}
218
+ usage = self._extract_usage(last_chunk)
219
+ if usage:
220
+ final_chunk["usage"] = usage
221
+ last_usage = self._extract_last_usage(last_chunk)
222
+ if last_usage:
223
+ final_chunk.setdefault("metadata", {})["last_usage"] = last_usage
224
+ yield final_chunk
225
+ return
143
226
  result = await self.invoke(input_data)
144
227
  final_chunk = {"output": result.get("output", ""), "type": "final"}
145
228
  usage = self._extract_usage(result)
@@ -160,6 +243,12 @@ class LangChainRunner(BaseRunner):
160
243
  final_chunk.setdefault("metadata", {})["last_usage"] = last_usage
161
244
  yield final_chunk
162
245
 
246
+ def _should_stream_events(self, payload: dict[str, Any]) -> bool:
247
+ """Use LangGraph events for LangChain create_agent message-state agents."""
248
+ return isinstance(payload.get("messages"), list) and hasattr(
249
+ self._agent, "astream_events"
250
+ )
251
+
163
252
  def _resolve_request_path(self) -> str:
164
253
  module = getattr(self, "_module", None)
165
254
  if callable(getattr(module, "ksadk_prepare_input", None)):
@@ -168,7 +257,9 @@ class LangChainRunner(BaseRunner):
168
257
  return "runnable_with_message_history"
169
258
  return "replay"
170
259
 
171
- def _prepare_with_standard_hook(self, input_data: Dict[str, Any], session_id: str) -> dict[str, Any]:
260
+ def _prepare_with_standard_hook(
261
+ self, input_data: Dict[str, Any], session_id: str
262
+ ) -> dict[str, Any]:
172
263
  module = getattr(self, "_module", None)
173
264
  prepare_input = getattr(module, "ksadk_prepare_input", None)
174
265
  if not callable(prepare_input):
@@ -195,7 +286,11 @@ class LangChainRunner(BaseRunner):
195
286
  @staticmethod
196
287
  def _ksadk_builtin_tool_context() -> dict[str, Any]:
197
288
  try:
198
- from ksadk.toolsets import builtin_tool_descriptors_for_runtime, builtin_tools_mode, builtin_tools_profile
289
+ from ksadk.toolsets import (
290
+ builtin_tool_descriptors_for_runtime,
291
+ builtin_tools_mode,
292
+ builtin_tools_profile,
293
+ )
199
294
 
200
295
  mode = builtin_tools_mode(default="off")
201
296
  descriptors = builtin_tool_descriptors_for_runtime(mode=mode)
@@ -231,7 +326,11 @@ class LangChainRunner(BaseRunner):
231
326
  def _ambient_context_text(input_data: Dict[str, Any]) -> str:
232
327
  sections: list[str] = []
233
328
  kb_context = input_data.get("kb_context") or {}
234
- kb_text = str(kb_context.get("formatted_text") or "").strip() if isinstance(kb_context, dict) else ""
329
+ kb_text = (
330
+ str(kb_context.get("formatted_text") or "").strip()
331
+ if isinstance(kb_context, dict)
332
+ else ""
333
+ )
235
334
  if kb_text:
236
335
  sections.append(f"Knowledge base context:\n{kb_text}")
237
336
 
@@ -518,6 +617,119 @@ class LangChainRunner(BaseRunner):
518
617
  return str(content)
519
618
  return str(result)
520
619
 
620
+ @classmethod
621
+ def _extract_recognized_output(cls, result: Any) -> str:
622
+ if result is None:
623
+ return ""
624
+ if isinstance(result, str):
625
+ return result
626
+
627
+ if isinstance(result, dict):
628
+ for key in ("output", "text"):
629
+ if key not in result:
630
+ continue
631
+ value = result[key]
632
+ if isinstance(value, str):
633
+ return value
634
+ extracted = cls._extract_recognized_output(value)
635
+ if extracted:
636
+ return extracted
637
+ if value is not None and not isinstance(value, (dict, list, tuple)):
638
+ return str(value)
639
+
640
+ message_state = cls._extract_message_state(result)
641
+ return message_state[0] if message_state else ""
642
+
643
+ if isinstance(result, (list, tuple)):
644
+ for item in reversed(result):
645
+ extracted = cls._extract_recognized_output(item)
646
+ if extracted:
647
+ return extracted
648
+ return ""
649
+
650
+ command_update = getattr(result, "update", None)
651
+ if isinstance(command_update, dict):
652
+ return cls._extract_recognized_output(command_update)
653
+
654
+ content = cls._ai_message_content(result)
655
+ return content if content is not None else ""
656
+
657
+ @classmethod
658
+ def _extract_message_state(cls, chunk: Any) -> tuple[str, str] | None:
659
+ """Extract the newest AI message from LangGraph values/updates snapshots."""
660
+
661
+ def visit(value: Any, path: tuple[str, ...]) -> tuple[str, str] | None:
662
+ if not isinstance(value, dict):
663
+ return None
664
+
665
+ messages = value.get("messages")
666
+ if isinstance(messages, list):
667
+ for index in range(len(messages) - 1, -1, -1):
668
+ message = messages[index]
669
+ content = cls._ai_message_content(message)
670
+ if content is None:
671
+ continue
672
+ message_id = (
673
+ message.get("id")
674
+ if isinstance(message, dict)
675
+ else getattr(message, "id", None)
676
+ )
677
+ fallback_key = "/".join((*path, "messages", str(index)))
678
+ return content, str(message_id or fallback_key)
679
+
680
+ for key, nested in value.items():
681
+ if key == "messages" or not isinstance(nested, dict):
682
+ continue
683
+ result = visit(nested, (*path, str(key)))
684
+ if result:
685
+ return result
686
+ return None
687
+
688
+ return visit(chunk, ())
689
+
690
+ @staticmethod
691
+ def _ai_message_content(message: Any) -> str | None:
692
+ if isinstance(message, dict):
693
+ role = str(message.get("role") or message.get("type") or "").lower()
694
+ if role not in {"ai", "assistant", "model", "aimessage", "aimessagechunk"}:
695
+ return None
696
+ content = message.get("content")
697
+ else:
698
+ role = str(getattr(message, "type", "") or "").lower()
699
+ class_name = type(message).__name__.lower()
700
+ if role not in {
701
+ "ai",
702
+ "assistant",
703
+ "model",
704
+ "aimessage",
705
+ "aimessagechunk",
706
+ } and not class_name.startswith("aimessage"):
707
+ return None
708
+ content = getattr(message, "content", None)
709
+
710
+ if isinstance(content, str):
711
+ return content
712
+ if not isinstance(content, list):
713
+ return None
714
+
715
+ parts: list[str] = []
716
+ for part in content:
717
+ if isinstance(part, str):
718
+ parts.append(part)
719
+ elif isinstance(part, dict):
720
+ text = part.get("text")
721
+ if isinstance(text, str):
722
+ parts.append(text)
723
+ return "".join(parts)
724
+
725
+ @staticmethod
726
+ def _snapshot_delta(content: str, previous: str) -> str:
727
+ if content.startswith(previous):
728
+ return content[len(previous) :]
729
+ if previous.startswith(content):
730
+ return ""
731
+ return content
732
+
521
733
  def _extract_chunk(self, chunk: Any) -> tuple[Optional[str], Optional[str]]:
522
734
  if isinstance(chunk, dict):
523
735
  if "output" in chunk:
@@ -12,6 +12,7 @@ import base64
12
12
  from pathlib import Path
13
13
 
14
14
  from ksadk.runners.base_runner import BaseRunner
15
+ from ksadk.runners.usage_accumulator import accumulate_usage
15
16
  from ksadk.sessions.continuity import LangGraphSessionAdapter
16
17
  from ksadk.runners.utils import get_langfuse_callbacks, get_langfuse_metadata, load_agent_module
17
18
  from langgraph.types import Command
@@ -259,6 +260,15 @@ class LangGraphRunner(BaseRunner):
259
260
  return self._extract_usage(values)
260
261
  return {}
261
262
 
263
+ @staticmethod
264
+ def _latest_checkpoint_config(config: dict[str, Any]) -> dict[str, Any]:
265
+ """Select the newest state in a resumed thread, not the source checkpoint."""
266
+ latest_config = dict(config)
267
+ configurable = dict(latest_config.get("configurable") or {})
268
+ configurable.pop("checkpoint_id", None)
269
+ latest_config["configurable"] = configurable
270
+ return latest_config
271
+
262
272
  @staticmethod
263
273
  def _ambient_context_text(payload: Dict[str, Any]) -> str:
264
274
  sections: list[str] = []
@@ -479,6 +489,51 @@ class LangGraphRunner(BaseRunner):
479
489
  )
480
490
  return self._agent.invoke(payload, **kwargs)
481
491
 
492
+ async def _invoke_from_stream_events(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
493
+ chunks: list[dict[str, Any]] = []
494
+ accumulated_text = ""
495
+ final_chunk: dict[str, Any] | None = None
496
+ metadata: dict[str, Any] = {}
497
+
498
+ async for chunk in self.stream(input_data):
499
+ chunks.append(dict(chunk))
500
+ chunk_type = chunk.get("type")
501
+ if chunk_type == "text":
502
+ accumulated_text += str(chunk.get("delta") or "")
503
+ elif chunk_type == "final":
504
+ final_chunk = dict(chunk)
505
+ elif chunk_type == "checkpoint":
506
+ chunk_metadata = chunk.get("metadata")
507
+ if isinstance(chunk_metadata, Mapping):
508
+ metadata.update(dict(chunk_metadata))
509
+ elif chunk_type == "interrupt":
510
+ return {
511
+ "type": "interrupt",
512
+ "interrupt_info": chunk.get("interrupt_info"),
513
+ "session_id": chunk.get("session_id") or input_data.get("session_id"),
514
+ "output": (
515
+ chunk.get("interrupt_info", {}).get("message", "需要用户确认")
516
+ if isinstance(chunk.get("interrupt_info"), Mapping)
517
+ else "需要用户确认"
518
+ ),
519
+ "raw": {"chunks": chunks},
520
+ }
521
+
522
+ if final_chunk:
523
+ output_text = str(final_chunk.get("output") or accumulated_text)
524
+ final_metadata = final_chunk.get("metadata")
525
+ if isinstance(final_metadata, Mapping):
526
+ metadata = {**dict(final_metadata), **metadata}
527
+ result: dict[str, Any] = {"output": output_text, "raw": {"chunks": chunks}}
528
+ usage = final_chunk.get("usage")
529
+ if isinstance(usage, Mapping) and usage:
530
+ result["usage"] = dict(usage)
531
+ if metadata:
532
+ result["metadata"] = metadata
533
+ return result
534
+
535
+ return {"output": accumulated_text, "raw": {"chunks": chunks}}
536
+
482
537
  async def invoke(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
483
538
  """调用 LangGraph 图
484
539
 
@@ -487,6 +542,10 @@ class LangGraphRunner(BaseRunner):
487
542
  2. 原生格式: {"messages": [...]} 或自定义 State - 直接透传
488
543
  """
489
544
  payload = dict(input_data)
545
+ force_graph_invoke = bool(payload.pop("_ksadk_force_graph_invoke", False))
546
+ if not force_graph_invoke and hasattr(self._agent, "astream_events"):
547
+ return await self._invoke_from_stream_events(payload)
548
+
490
549
  session_id = payload.pop("session_id", None) or str(uuid.uuid4())[:8]
491
550
  is_resume = payload.pop("resume", False)
492
551
  is_checkpoint_resume = bool(payload.pop("checkpoint_resume", False))
@@ -618,14 +677,15 @@ class LangGraphRunner(BaseRunner):
618
677
  else:
619
678
  yield {"type": "graph_update", "output": update}
620
679
 
621
- state_usage = await self._latest_state_usage(config)
680
+ latest_config = self._latest_checkpoint_config(config)
681
+ state_usage = await self._latest_state_usage(latest_config)
622
682
  state_output = ""
623
683
  try:
624
684
  state = None
625
685
  if callable(getattr(self._agent, "aget_state", None)):
626
- state = await self._agent.aget_state(config)
686
+ state = await self._agent.aget_state(latest_config)
627
687
  elif callable(getattr(self._agent, "get_state", None)):
628
- state = self._agent.get_state(config)
688
+ state = self._agent.get_state(latest_config)
629
689
  values = getattr(state, "values", None)
630
690
  if values is not None:
631
691
  state_output = self._extract_output(values)
@@ -639,13 +699,14 @@ class LangGraphRunner(BaseRunner):
639
699
  **({"resume_noop": True} if not emitted_update else {}),
640
700
  }
641
701
 
642
- metadata = await self._latest_checkpoint_metadata(config)
702
+ metadata = await self._latest_checkpoint_metadata(latest_config)
643
703
  if metadata:
644
704
  yield {"type": "checkpoint", "metadata": metadata}
645
705
 
646
706
  async def stream(self, input_data: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
647
707
  """流式调用 LangGraph 图"""
648
708
  payload = dict(input_data)
709
+ payload.pop("_ksadk_force_graph_invoke", None)
649
710
  session_id = payload.pop("session_id", None) or str(uuid.uuid4())[:8]
650
711
  history = payload.pop("history", [])
651
712
  is_resume = payload.pop("resume", False)
@@ -689,6 +750,51 @@ class LangGraphRunner(BaseRunner):
689
750
  emitted_non_text_event = False
690
751
  final_output_text = ""
691
752
  final_output_usage: dict[str, Any] = {}
753
+ final_output_last_usage: dict[str, Any] = {}
754
+ model_run_usages: dict[str, dict[str, Any]] = {}
755
+ model_run_order: list[str] = []
756
+ stream_usage_run_keys: set[str] = set()
757
+ latest_stream_usage: dict[str, Any] = {}
758
+
759
+ def model_run_key(
760
+ event: Mapping[str, Any],
761
+ *,
762
+ fallback_key: str | None = None,
763
+ ) -> str:
764
+ raw_run_id = event.get("run_id")
765
+ return (
766
+ str(raw_run_id)
767
+ if raw_run_id
768
+ else fallback_key or f"model-event-{len(model_run_order)}"
769
+ )
770
+
771
+ def record_model_usage(
772
+ event: Mapping[str, Any],
773
+ usage: dict[str, Any],
774
+ *,
775
+ fallback_key: str | None = None,
776
+ ) -> None:
777
+ if not usage:
778
+ return
779
+ run_key = model_run_key(event, fallback_key=fallback_key)
780
+ if run_key not in model_run_usages:
781
+ model_run_order.append(run_key)
782
+ model_run_usages[run_key] = dict(usage)
783
+
784
+ def accumulated_model_usage() -> dict[str, Any]:
785
+ if len(model_run_order) == 1:
786
+ return dict(model_run_usages.get(model_run_order[0]) or {})
787
+ usage: dict[str, Any] = {}
788
+ for run_key in model_run_order:
789
+ usage = accumulate_usage(usage, model_run_usages.get(run_key) or {})
790
+ return usage
791
+
792
+ def latest_model_usage() -> dict[str, Any]:
793
+ for run_key in reversed(model_run_order):
794
+ usage = model_run_usages.get(run_key)
795
+ if usage:
796
+ return dict(usage)
797
+ return {}
692
798
 
693
799
  if is_checkpoint_resume and callable(getattr(self._agent, "astream", None)):
694
800
  try:
@@ -731,6 +837,19 @@ class LangGraphRunner(BaseRunner):
731
837
  chunk = event.get("data", {}).get("chunk")
732
838
  if not chunk:
733
839
  continue
840
+ chunk_usage = self._extract_usage(chunk)
841
+ if chunk_usage:
842
+ # Some LangChain providers attach cumulative usage to
843
+ # every stream chunk, and LangChain may then sum those
844
+ # cumulative snapshots into an inflated
845
+ # on_chat_model_end usage. For a concrete model run,
846
+ # keep the latest stream snapshot and ignore the later
847
+ # end usage for that same run_id.
848
+ latest_stream_usage = dict(chunk_usage)
849
+ if event.get("run_id"):
850
+ run_key = model_run_key(event)
851
+ stream_usage_run_keys.add(run_key)
852
+ record_model_usage(event, latest_stream_usage)
734
853
 
735
854
  # 推理内容
736
855
  reasoning = getattr(chunk, "reasoning_content", None)
@@ -760,6 +879,15 @@ class LangGraphRunner(BaseRunner):
760
879
  accumulated_text += part.text
761
880
  yield {"delta": part.text, "type": "text"}
762
881
 
882
+ elif event_kind == "on_chat_model_end":
883
+ data = event.get("data") or {}
884
+ output = data.get("output") if isinstance(data, Mapping) else None
885
+ usage = self._extract_usage(output) or self._extract_usage(data)
886
+ last_usage = self._extract_last_usage(output) or self._extract_last_usage(data)
887
+ run_key = model_run_key(event)
888
+ if run_key not in stream_usage_run_keys:
889
+ record_model_usage(event, last_usage or usage)
890
+
763
891
  elif event_kind == "on_tool_start":
764
892
  emitted_non_text_event = True
765
893
  yield {
@@ -811,13 +939,17 @@ class LangGraphRunner(BaseRunner):
811
939
  if not accumulated_text:
812
940
  if final_output_text:
813
941
  final_chunk = {"output": final_output_text, "type": "final"}
814
- if final_output_usage:
815
- final_chunk["usage"] = final_output_usage
816
- if final_output_last_usage:
817
- final_chunk.setdefault("metadata", {})["last_usage"] = final_output_last_usage
942
+ usage = accumulated_model_usage() or final_output_usage or latest_stream_usage
943
+ last_usage = latest_model_usage() or final_output_last_usage or latest_stream_usage or usage
944
+ if usage:
945
+ final_chunk["usage"] = usage
946
+ if last_usage:
947
+ final_chunk.setdefault("metadata", {})["last_usage"] = last_usage
818
948
  yield final_chunk
819
949
  elif not emitted_non_text_event:
820
- result = await self.invoke(invoke_payload)
950
+ result = await self.invoke(
951
+ {**invoke_payload, "_ksadk_force_graph_invoke": True}
952
+ )
821
953
  final_chunk = {"output": result.get("output", ""), "type": "final"}
822
954
  usage = self._extract_usage(result)
823
955
  if usage:
@@ -832,11 +964,12 @@ class LangGraphRunner(BaseRunner):
832
964
  return
833
965
  else:
834
966
  final_chunk = {"output": accumulated_text, "type": "final"}
835
- usage = await self._latest_state_usage(config)
967
+ state_usage = await self._latest_state_usage(config)
968
+ usage = accumulated_model_usage() or state_usage or final_output_usage or latest_stream_usage
836
969
  if usage:
837
970
  final_chunk["usage"] = usage
838
- # _latest_state_usage 返回末个 message usage,单次调用场景即 last_usage
839
- final_chunk.setdefault("metadata", {})["last_usage"] = usage
971
+ last_usage = latest_model_usage() or state_usage or final_output_last_usage or latest_stream_usage or usage
972
+ final_chunk.setdefault("metadata", {})["last_usage"] = last_usage
840
973
  yield final_chunk
841
974
 
842
975
  metadata = await self._latest_checkpoint_metadata(config)