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
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any
5
+ from urllib.parse import quote
6
+
7
+
8
+ def project_session_messages(
9
+ events: Sequence[Mapping[str, Any]],
10
+ *,
11
+ include_reasoning: bool = False,
12
+ include_tool_events: bool = False,
13
+ include_attachments: bool = True,
14
+ ) -> list[dict[str, Any]]:
15
+ """Project persisted runtime events into the chat history contract."""
16
+
17
+ groups: dict[str, list[Mapping[str, Any]]] = {}
18
+ for event in sorted(events, key=lambda item: int(item.get("SeqId") or 0)):
19
+ invocation_id = str(event.get("InvocationId") or f"seq:{event.get('SeqId')}")
20
+ groups.setdefault(invocation_id, []).append(event)
21
+
22
+ messages: list[dict[str, Any]] = []
23
+ for group in groups.values():
24
+ messages.extend(
25
+ _project_event_group(
26
+ group,
27
+ include_reasoning=include_reasoning,
28
+ include_tool_events=include_tool_events,
29
+ include_attachments=include_attachments,
30
+ )
31
+ )
32
+ return sorted(messages, key=lambda item: int(item.get("SeqId") or 0))
33
+
34
+
35
+ def _project_event_group(
36
+ events: Sequence[Mapping[str, Any]],
37
+ *,
38
+ include_reasoning: bool,
39
+ include_tool_events: bool,
40
+ include_attachments: bool,
41
+ ) -> list[dict[str, Any]]:
42
+ reasoning = [
43
+ {"text": _event_text(event), "SeqId": event.get("SeqId")}
44
+ for event in events
45
+ if event.get("EventType") == "reasoning" and _event_text(event)
46
+ ]
47
+ tool_events = _project_tool_events(events) if include_tool_events else []
48
+ projected: list[dict[str, Any]] = []
49
+ assistant_seen = False
50
+
51
+ for event in events:
52
+ event_type = str(event.get("EventType") or "")
53
+ if event_type == "user_message":
54
+ message = _base_message(event, "user")
55
+ if include_attachments:
56
+ attachments = _event_attachments(event)
57
+ if attachments:
58
+ message["Attachments"] = attachments
59
+ projected.append(message)
60
+ elif event_type == "assistant_message":
61
+ message = _base_message(event, "assistant")
62
+ if include_reasoning and reasoning:
63
+ message["Reasoning"] = reasoning
64
+ if tool_events:
65
+ message["ToolEvents"] = tool_events
66
+ projected.append(message)
67
+ assistant_seen = True
68
+
69
+ if not assistant_seen and (reasoning or tool_events):
70
+ anchor = next(
71
+ (
72
+ event
73
+ for event in reversed(events)
74
+ if event.get("EventType")
75
+ in {"assistant_stream_snapshot", "approval_request", "tool_call", "reasoning"}
76
+ ),
77
+ events[-1],
78
+ )
79
+ message = _base_message(anchor, "assistant", content="")
80
+ if include_reasoning and reasoning:
81
+ message["Reasoning"] = reasoning
82
+ if tool_events:
83
+ message["ToolEvents"] = tool_events
84
+ projected.append(message)
85
+
86
+ return projected
87
+
88
+
89
+ def _base_message(
90
+ event: Mapping[str, Any],
91
+ role: str,
92
+ *,
93
+ content: str | None = None,
94
+ ) -> dict[str, Any]:
95
+ metadata = event.get("Metadata") if isinstance(event.get("Metadata"), Mapping) else {}
96
+ message: dict[str, Any] = {
97
+ "MessageId": event.get("EventId"),
98
+ "Role": role,
99
+ "Content": {"text": _event_text(event) if content is None else content},
100
+ "Timestamp": event.get("Timestamp"),
101
+ "SeqId": event.get("SeqId"),
102
+ "InvocationId": event.get("InvocationId"),
103
+ }
104
+ for target, *sources in (
105
+ ("ResponseId", "response_id", "ResponseId"),
106
+ ("TraceId", "trace_id", "TraceId"),
107
+ ("RootSpanId", "root_span_id", "rootSpanId", "RootSpanId"),
108
+ ):
109
+ value = next((metadata.get(source) for source in sources if metadata.get(source)), None)
110
+ if value:
111
+ message[target] = str(value)
112
+ return message
113
+
114
+
115
+ def _event_text(event: Mapping[str, Any]) -> str:
116
+ content = event.get("Content")
117
+ if isinstance(content, str):
118
+ return content
119
+ if not isinstance(content, Mapping):
120
+ return ""
121
+ if content.get("text") is not None:
122
+ return str(content.get("text") or "")
123
+ return "".join(
124
+ str(part.get("text") or "")
125
+ for part in content.get("parts") or []
126
+ if isinstance(part, Mapping)
127
+ )
128
+
129
+
130
+ def _project_tool_events(events: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]:
131
+ projected: list[dict[str, Any]] = []
132
+ pending_calls: list[tuple[Mapping[str, Any], dict[str, Any]]] = []
133
+
134
+ for event in events:
135
+ event_type = str(event.get("EventType") or "")
136
+ metadata = event.get("Metadata") if isinstance(event.get("Metadata"), Mapping) else {}
137
+ if event_type == "tool_call":
138
+ entry = _tool_event_from_call(event, metadata)
139
+ projected.append(entry)
140
+ pending_calls.append((event, entry))
141
+ continue
142
+ if event_type != "tool_result":
143
+ continue
144
+
145
+ name = str(metadata.get("tool_name") or "tool")
146
+ match = next(
147
+ (
148
+ item
149
+ for item in reversed(pending_calls)
150
+ if item[1]["Name"] == name and item[1]["Status"] == "running"
151
+ ),
152
+ None,
153
+ )
154
+ if match is None:
155
+ entry = _tool_event_from_call(event, metadata)
156
+ projected.append(entry)
157
+ else:
158
+ entry = match[1]
159
+ output = metadata.get("tool_output", _event_text(event))
160
+ entry["Status"] = (
161
+ "failed" if isinstance(output, Mapping) and output.get("ok") is False else "completed"
162
+ )
163
+ entry["Result"] = output
164
+ entry["ResultSeqId"] = event.get("SeqId")
165
+
166
+ projected.extend(_project_approval_events(events))
167
+ return projected
168
+
169
+
170
+ def _tool_event_from_call(
171
+ event: Mapping[str, Any], metadata: Mapping[str, Any]
172
+ ) -> dict[str, Any]:
173
+ tool_receipt = metadata.get("tool_receipt")
174
+ call_id = metadata.get("call_id") or metadata.get("run_id")
175
+ if not call_id and isinstance(tool_receipt, Mapping):
176
+ call_id = tool_receipt.get("tool_call_id")
177
+ return {
178
+ "SeqId": event.get("SeqId"),
179
+ "Type": "tool_call",
180
+ "Name": str(metadata.get("tool_name") or "tool"),
181
+ "Args": metadata.get("tool_args"),
182
+ "Status": "running",
183
+ "ToolCallId": call_id,
184
+ }
185
+
186
+
187
+ def _project_approval_events(events: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]:
188
+ responses: dict[str, Mapping[str, Any]] = {}
189
+ for event in events:
190
+ if event.get("EventType") != "approval_response":
191
+ continue
192
+ metadata = event.get("Metadata") if isinstance(event.get("Metadata"), Mapping) else {}
193
+ resume_input = metadata.get("resume_input")
194
+ if not isinstance(resume_input, Mapping):
195
+ continue
196
+ request_id = str(
197
+ resume_input.get("approval_request_id")
198
+ or resume_input.get("interrupt_id")
199
+ or resume_input.get("id")
200
+ or ""
201
+ )
202
+ responses[request_id] = event
203
+
204
+ approvals: list[dict[str, Any]] = []
205
+ for request in events:
206
+ if request.get("EventType") != "approval_request":
207
+ continue
208
+ metadata = request.get("Metadata") if isinstance(request.get("Metadata"), Mapping) else {}
209
+ interrupt_info = metadata.get("interrupt_info")
210
+ if not isinstance(interrupt_info, Mapping):
211
+ interrupt_info = {}
212
+ request_id = str(
213
+ interrupt_info.get("approval_request_id") or interrupt_info.get("id") or ""
214
+ )
215
+ response = responses.get(request_id)
216
+ response_metadata = (
217
+ response.get("Metadata")
218
+ if response is not None and isinstance(response.get("Metadata"), Mapping)
219
+ else {}
220
+ )
221
+ resume_input = response_metadata.get("resume_input")
222
+ if not isinstance(resume_input, Mapping):
223
+ resume_input = {}
224
+ status = "paused" if response is None else (
225
+ "approved"
226
+ if resume_input.get("approve") or resume_input.get("approved")
227
+ else "denied"
228
+ )
229
+ entry: dict[str, Any] = {
230
+ "SeqId": request.get("SeqId"),
231
+ "Type": "approval",
232
+ "Name": str(interrupt_info.get("tool_name") or "approval"),
233
+ "Status": status,
234
+ "ApprovalRequestId": request_id or None,
235
+ }
236
+ arguments = (
237
+ interrupt_info.get("arguments")
238
+ or interrupt_info.get("tool_args")
239
+ or interrupt_info.get("args")
240
+ )
241
+ if arguments is not None:
242
+ entry["Args"] = arguments
243
+ approvals.append(entry)
244
+ return approvals
245
+
246
+
247
+ def _event_attachments(event: Mapping[str, Any]) -> list[dict[str, Any]]:
248
+ metadata = event.get("Metadata") if isinstance(event.get("Metadata"), Mapping) else {}
249
+ attachments: list[dict[str, Any]] = []
250
+ seen: set[str] = set()
251
+ for item in metadata.get("attachments") or []:
252
+ if not isinstance(item, Mapping):
253
+ continue
254
+ file_uri = str(item.get("file_uri") or item.get("fileUri") or "")
255
+ if not file_uri or file_uri in seen:
256
+ continue
257
+ mime = str(item.get("mime_type") or item.get("mimeType") or "")
258
+ attachments.append(
259
+ {
260
+ "file_uri": file_uri,
261
+ "name": str(item.get("display_name") or item.get("displayName") or ""),
262
+ "mime": mime,
263
+ "size": int(item.get("size_bytes") or item.get("sizeBytes") or 0),
264
+ "is_image": mime.startswith("image/"),
265
+ "url": "/agentengine/api/v1/AttachmentContent?FileUri=" + quote(file_uri, safe=""),
266
+ }
267
+ )
268
+ seen.add(file_uri)
269
+ return attachments
@@ -182,6 +182,16 @@ def estimate_text_tokens(text: str) -> int:
182
182
  return max(1, cjk_tokens + math.ceil(ascii_chars / 4))
183
183
 
184
184
 
185
+ def get_context_window_tokens(model_metadata: Mapping[str, Any] | None = None) -> int:
186
+ limits = dict(DEFAULT_MODEL_LIMITS)
187
+ if isinstance(model_metadata, Mapping):
188
+ limits.update(dict(model_metadata.get("limits") or {}))
189
+ if model_metadata.get("context_window_tokens"):
190
+ limits["context_window_tokens"] = model_metadata["context_window_tokens"]
191
+
192
+ return _coerce_positive_int(limits.get("context_window_tokens")) or DEFAULT_CONTEXT_WINDOW_TOKENS
193
+
194
+
185
195
  def get_effective_context_window_tokens(model_metadata: Mapping[str, Any] | None = None) -> int:
186
196
  limits = dict(DEFAULT_MODEL_LIMITS)
187
197
  if isinstance(model_metadata, Mapping):
@@ -191,7 +201,7 @@ def get_effective_context_window_tokens(model_metadata: Mapping[str, Any] | None
191
201
  if model_metadata.get("max_output_tokens"):
192
202
  limits["max_output_tokens"] = model_metadata["max_output_tokens"]
193
203
 
194
- context_window = _coerce_positive_int(limits.get("context_window_tokens")) or DEFAULT_CONTEXT_WINDOW_TOKENS
204
+ context_window = get_context_window_tokens(model_metadata)
195
205
  max_output_tokens = _coerce_positive_int(limits.get("max_output_tokens")) or DEFAULT_MAX_OUTPUT_TOKENS
196
206
  reserved_tokens = min(max_output_tokens, AUTOCOMPACT_SUMMARY_RESERVE_TOKENS)
197
207
  return max(1, context_window - reserved_tokens)
@@ -534,6 +534,59 @@ def _set_conversation_output_attributes(span: Any | None, output_text: str | Non
534
534
  _set_span_attribute(span, key, text)
535
535
 
536
536
 
537
+ def _set_conversation_usage_attributes(
538
+ span: Any | None,
539
+ usage: Mapping[str, Any] | None,
540
+ ) -> None:
541
+ normalized = _normalize_usage_payload(usage)
542
+ if not normalized:
543
+ return
544
+
545
+ def _usage_int(value: Any) -> int:
546
+ try:
547
+ return int(value or 0)
548
+ except (TypeError, ValueError):
549
+ return 0
550
+
551
+ input_tokens = _usage_int(normalized.get("input_tokens"))
552
+ output_tokens = _usage_int(normalized.get("output_tokens"))
553
+ total_tokens = _usage_int(normalized.get("total_tokens") or (input_tokens + output_tokens))
554
+ input_details = normalized.get("input_token_details")
555
+ output_details = normalized.get("output_token_details")
556
+ cache_read_tokens = 0
557
+ reasoning_tokens = 0
558
+ if isinstance(input_details, Mapping):
559
+ cache_read_tokens = _usage_int(
560
+ input_details.get("cache_read")
561
+ or input_details.get("cached")
562
+ or input_details.get("cached_tokens")
563
+ )
564
+ if isinstance(output_details, Mapping):
565
+ reasoning_tokens = _usage_int(
566
+ output_details.get("reasoning")
567
+ or output_details.get("reasoning_tokens")
568
+ )
569
+
570
+ attributes = {
571
+ "gen_ai.usage.input_tokens": input_tokens,
572
+ "gen_ai.usage.output_tokens": output_tokens,
573
+ "gen_ai.usage.total_tokens": total_tokens,
574
+ "llm.usage.prompt_tokens": input_tokens,
575
+ "llm.usage.completion_tokens": output_tokens,
576
+ "llm.usage.total_tokens": total_tokens,
577
+ }
578
+ if cache_read_tokens:
579
+ attributes["gen_ai.usage.cache_read.input_tokens"] = cache_read_tokens
580
+ attributes["llm.usage.cache_read.input_tokens"] = cache_read_tokens
581
+ if reasoning_tokens:
582
+ attributes["gen_ai.usage.reasoning.output_tokens"] = reasoning_tokens
583
+ attributes["llm.usage.reasoning_tokens"] = reasoning_tokens
584
+
585
+ for key, value in attributes.items():
586
+ if value:
587
+ _set_span_attribute(span, key, value)
588
+
589
+
537
590
  def _set_conversation_span_attributes(
538
591
  span: Any,
539
592
  *,
@@ -3862,6 +3915,7 @@ async def invoke_conversation_once(
3862
3915
  (result.get("metadata") or {}).get("last_usage")
3863
3916
  ) or (result_usage if result_usage else {})
3864
3917
  _set_conversation_output_attributes(span, output_text)
3918
+ _set_conversation_usage_attributes(span, result_usage)
3865
3919
  result_agentengine_metadata = _extract_agentengine_metadata(result)
3866
3920
  assistant_metadata: dict[str, Any] = {
3867
3921
  **trace_metadata,
@@ -4148,6 +4202,7 @@ async def _iter_conversation_turn_events(
4148
4202
  )
4149
4203
 
4150
4204
  accumulated_text = ""
4205
+ accumulated_reasoning_parts: list[str] = []
4151
4206
  emitted_anything = False
4152
4207
  emitted_response_artifacts = False
4153
4208
  saw_final_chunk = False
@@ -4157,6 +4212,20 @@ async def _iter_conversation_turn_events(
4157
4212
  stream_usage: dict[str, Any] = {}
4158
4213
  stream_last_usage: dict[str, Any] = {}
4159
4214
  reasoning_disabled = _model_options_disable_reasoning(prepared.model_options)
4215
+
4216
+ async def _persist_accumulated_reasoning() -> None:
4217
+ if not accumulated_reasoning_parts:
4218
+ return
4219
+ reasoning = "".join(accumulated_reasoning_parts)
4220
+ accumulated_reasoning_parts.clear()
4221
+ await append_reasoning_event(
4222
+ session_id=prepared.session_id,
4223
+ author=runner_name,
4224
+ text=reasoning,
4225
+ invocation_id=prepared.invocation_id,
4226
+ session_service_provider=provider,
4227
+ )
4228
+
4160
4229
  for attempt in range(2):
4161
4230
  try:
4162
4231
  runtime_context.history = list(prepared.history)
@@ -4237,12 +4306,8 @@ async def _iter_conversation_turn_events(
4237
4306
  responses_output
4238
4307
  ):
4239
4308
  if semantic_event.get("type") == "thinking":
4240
- await append_reasoning_event(
4241
- session_id=prepared.session_id,
4242
- author=runner_name,
4243
- text=str(semantic_event.get("delta") or ""),
4244
- invocation_id=prepared.invocation_id,
4245
- session_service_provider=provider,
4309
+ accumulated_reasoning_parts.append(
4310
+ str(semantic_event.get("delta") or "")
4246
4311
  )
4247
4312
  emitted_anything = True
4248
4313
  yield semantic_event
@@ -4252,13 +4317,7 @@ async def _iter_conversation_turn_events(
4252
4317
  continue
4253
4318
  delta = str(chunk.get("delta", ""))
4254
4319
  if delta:
4255
- await append_reasoning_event(
4256
- session_id=prepared.session_id,
4257
- author=runner_name,
4258
- text=delta,
4259
- invocation_id=prepared.invocation_id,
4260
- session_service_provider=provider,
4261
- )
4320
+ accumulated_reasoning_parts.append(delta)
4262
4321
  emitted_anything = True
4263
4322
  emitted_response_artifacts = True
4264
4323
  yield {"type": "thinking", "delta": delta}
@@ -4295,6 +4354,7 @@ async def _iter_conversation_turn_events(
4295
4354
  session_service_provider=provider,
4296
4355
  )
4297
4356
  emitted_anything = True
4357
+ await _persist_accumulated_reasoning()
4298
4358
  yield {
4299
4359
  "type": "tool_call",
4300
4360
  "name": chunk.get("tool_name"),
@@ -4391,6 +4451,7 @@ async def _iter_conversation_turn_events(
4391
4451
  run_trigger=run_trigger,
4392
4452
  )
4393
4453
  emitted_anything = True
4454
+ await _persist_accumulated_reasoning()
4394
4455
  yield {
4395
4456
  "type": "interrupt",
4396
4457
  "interrupt_info": approval_interrupt_info,
@@ -4448,6 +4509,7 @@ async def _iter_conversation_turn_events(
4448
4509
  governance, chunk.get("tool_output", "")
4449
4510
  )
4450
4511
  emitted_anything = True
4512
+ await _persist_accumulated_reasoning()
4451
4513
  yield {
4452
4514
  "type": "tool_result",
4453
4515
  "name": chunk.get("tool_name"),
@@ -4497,6 +4559,7 @@ async def _iter_conversation_turn_events(
4497
4559
  stream_last_usage = _normalize_usage_payload(chunk_last) or stream_usage
4498
4560
  break
4499
4561
  except asyncio.CancelledError:
4562
+ await _persist_accumulated_reasoning()
4500
4563
  await append_run_status_event(
4501
4564
  session_id=prepared.session_id,
4502
4565
  author=runner_name,
@@ -4541,6 +4604,7 @@ async def _iter_conversation_turn_events(
4541
4604
  run_mode=run_mode,
4542
4605
  run_trigger=run_trigger,
4543
4606
  )
4607
+ await _persist_accumulated_reasoning()
4544
4608
  yield {"type": "error", "message": str(circuit_exc) or "Agent 运行失败"}
4545
4609
  return
4546
4610
  if checkpoint:
@@ -4594,6 +4658,7 @@ async def _iter_conversation_turn_events(
4594
4658
  run_mode=run_mode,
4595
4659
  run_trigger=run_trigger,
4596
4660
  )
4661
+ await _persist_accumulated_reasoning()
4597
4662
  yield {"type": "error", "message": str(exc) or "Agent 运行失败"}
4598
4663
  return
4599
4664
 
@@ -4628,6 +4693,7 @@ async def _iter_conversation_turn_events(
4628
4693
  run_mode=run_mode,
4629
4694
  run_trigger=run_trigger,
4630
4695
  )
4696
+ await _persist_accumulated_reasoning()
4631
4697
  _finish_span()
4632
4698
  yield {
4633
4699
  "type": "error",
@@ -4641,6 +4707,7 @@ async def _iter_conversation_turn_events(
4641
4707
  return
4642
4708
  _set_conversation_output_attributes(span, accumulated_text)
4643
4709
 
4710
+ await _persist_accumulated_reasoning()
4644
4711
  await append_conversation_event(
4645
4712
  session_id=prepared.session_id,
4646
4713
  author=runner_name,
@@ -4674,6 +4741,7 @@ async def _iter_conversation_turn_events(
4674
4741
  run_mode=run_mode,
4675
4742
  run_trigger=run_trigger,
4676
4743
  )
4744
+ _set_conversation_usage_attributes(span, assistant_metadata.get("usage"))
4677
4745
  _finish_span()
4678
4746
  yield {
4679
4747
  "type": "completed",
ksadk/deployment/base.py CHANGED
@@ -41,7 +41,7 @@ class NetworkConfig(BaseModel):
41
41
  """网络配置"""
42
42
  access_type: str = "public" # public | private
43
43
  enable_https: bool = True
44
- enable_public_access: bool = False
44
+ enable_public_access: Optional[bool] = None
45
45
  enable_vpc_access: bool = False
46
46
  vpc_id: str = ""
47
47
  subnet_id: str = ""
@@ -261,22 +261,32 @@ class ServerlessProvider(BaseDeployProvider):
261
261
  json.dump(payload, f, indent=2, ensure_ascii=False)
262
262
 
263
263
  @staticmethod
264
- def _serialize_network_config(target: DeployTarget) -> Optional[Dict[str, Any]]:
264
+ def _serialize_network_config(
265
+ target: DeployTarget, *, is_update: bool = False
266
+ ) -> Optional[Dict[str, Any]]:
265
267
  network = getattr(target, "network", None)
266
268
  if network is None:
267
269
  return None
268
270
 
269
- payload: Dict[str, Any] = {
270
- "enable_public_access": bool(getattr(network, "enable_public_access", False)),
271
- "enable_vpc_access": bool(getattr(network, "enable_vpc_access", False)),
272
- }
271
+ public_access = getattr(network, "enable_public_access", None)
272
+ # 三态:None=未指定。create 默认开公网;update 未指定则不发 network 字段(保留服务端现有配置)。
273
+ if public_access is None:
274
+ public_value: Optional[bool] = None if is_update else True
275
+ else:
276
+ public_value = bool(public_access)
277
+
278
+ payload: Dict[str, Any] = {}
279
+ if public_value is not None:
280
+ payload["enable_public_access"] = public_value
281
+ if bool(getattr(network, "enable_vpc_access", False)):
282
+ payload["enable_vpc_access"] = True
273
283
 
274
284
  for field in ("vpc_id", "subnet_id", "security_group_id", "availability_zone"):
275
285
  value = str(getattr(network, field, "") or "").strip()
276
286
  if value:
277
287
  payload[field] = value
278
288
 
279
- if not payload["enable_public_access"] and not payload["enable_vpc_access"] and len(payload) == 2:
289
+ if not payload:
280
290
  return None
281
291
  return payload
282
292
 
@@ -710,7 +720,7 @@ class ServerlessProvider(BaseDeployProvider):
710
720
  else:
711
721
  click.echo(f" 📦 更新环境变量: {len(env_vars)} 项 from 全局配置")
712
722
 
713
- network_config = self._serialize_network_config(target)
723
+ network_config = self._serialize_network_config(target, is_update=True)
714
724
  if network_config:
715
725
  update_data["network"] = network_config
716
726
 
@@ -829,7 +839,7 @@ class ServerlessProvider(BaseDeployProvider):
829
839
  if env_vars:
830
840
  request_data["env_vars"] = env_vars
831
841
 
832
- network_config = self._serialize_network_config(target)
842
+ network_config = self._serialize_network_config(target, is_update=False)
833
843
  if network_config:
834
844
  request_data["network"] = network_config
835
845