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,360 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any, Optional, cast
6
+
7
+ from ksadk.sessions.base import BaseSessionService, Session, SessionEvent, SessionState
8
+ from ksadk.sessions.in_memory import InMemorySessionService
9
+ from ksadk.sessions.resilience import is_session_backend_failure
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ResilientSessionService(BaseSessionService):
15
+ """Keep live agent sessions available when durable persistence is unavailable.
16
+
17
+ The in-memory service is authoritative for the lifetime of this process. The
18
+ configured durable service is used as a read-through source and a best-effort
19
+ write-through sink. After its first failure it stays disabled until a
20
+ background probe confirms the durable backend is reachable again, at which
21
+ point it is re-enabled and an INFO log is emitted.
22
+ """
23
+
24
+ _probe_interval_seconds: float = 30.0
25
+
26
+ def __init__(
27
+ self,
28
+ primary: BaseSessionService,
29
+ fallback: InMemorySessionService | None = None,
30
+ ) -> None:
31
+ self.primary = primary
32
+ self.fallback = fallback or InMemorySessionService()
33
+ self._primary_enabled = True
34
+ self._hydrate_lock = asyncio.Lock()
35
+ self._primary_session_lock = asyncio.Lock()
36
+ self._primary_session_ids: set[str] = set()
37
+ self._probe_task: asyncio.Task[None] | None = None
38
+
39
+ @property
40
+ def degraded(self) -> bool:
41
+ return not self._primary_enabled
42
+
43
+ async def _call_primary(self, method_name: str, *args: Any, **kwargs: Any) -> tuple[bool, Any]:
44
+ if not self._primary_enabled:
45
+ return False, None
46
+ try:
47
+ method = getattr(self.primary, method_name)
48
+ return True, await method(*args, **kwargs)
49
+ except Exception as exc:
50
+ if not is_session_backend_failure(exc):
51
+ raise
52
+ self._disable_primary(exc)
53
+ return False, None
54
+
55
+ def _disable_primary(self, exc: Exception) -> None:
56
+ if not self._primary_enabled:
57
+ return
58
+ self._primary_enabled = False
59
+ logger.error(
60
+ "KSADK session persistence degraded; using in-memory live session: %s",
61
+ exc,
62
+ extra={
63
+ "session_backend_state": "degraded",
64
+ "session_backend": type(self.primary).__name__,
65
+ },
66
+ )
67
+ self._start_probe()
68
+
69
+ def _start_probe(self) -> None:
70
+ if self._probe_task is not None and not self._probe_task.done():
71
+ return
72
+ self._probe_task = asyncio.create_task(self._probe_loop())
73
+
74
+ async def _probe_loop(self) -> None:
75
+ while not self._primary_enabled:
76
+ await asyncio.sleep(self._probe_interval_seconds)
77
+ if self._primary_enabled:
78
+ break
79
+ try:
80
+ await self.primary.get_session("__ksadk_probe__")
81
+ except Exception:
82
+ continue
83
+ self._primary_enabled = True
84
+ logger.info(
85
+ "KSADK session persistence recovered; durable backend re-enabled",
86
+ extra={
87
+ "session_backend_state": "recovered",
88
+ "session_backend": type(self.primary).__name__,
89
+ },
90
+ )
91
+
92
+ async def _hydrate(self, session: Session) -> Session:
93
+ async with self._hydrate_lock:
94
+ self._primary_session_ids.add(session.id)
95
+ existing = await self.fallback.get_session(session.id)
96
+ if existing is None:
97
+ await self.fallback.create_session(
98
+ session.agent_id,
99
+ session.user_id,
100
+ session_id=session.id,
101
+ )
102
+ existing_events = await self.fallback.get_events(session.id)
103
+ existing_ids = {event.id for event in existing_events}
104
+ for event in sorted(session.events, key=lambda item: item.seq_id):
105
+ if event.id not in existing_ids:
106
+ await self.fallback.append_event(session.id, event)
107
+ await self.fallback.update_session_metadata(
108
+ session.id,
109
+ title=session.title,
110
+ title_source=session.title_source,
111
+ summary=session.summary,
112
+ first_prompt=session.first_prompt,
113
+ last_prompt=session.last_prompt,
114
+ )
115
+ current = await self.fallback.get_session(session.id)
116
+ if current is not None and session.state != current.state:
117
+ await self.fallback.update_state(
118
+ agent_id=session.agent_id,
119
+ user_id=session.user_id,
120
+ session_id=session.id,
121
+ scope="session",
122
+ state_delta=session.state,
123
+ )
124
+ hydrated = await self.fallback.get_session(session.id)
125
+ if hydrated is None:
126
+ raise RuntimeError(f"Failed to hydrate live session {session.id}")
127
+ return hydrated
128
+
129
+ async def create_session(
130
+ self,
131
+ agent_id: str,
132
+ user_id: str,
133
+ session_id: Optional[str] = None,
134
+ ) -> Session:
135
+ if session_id:
136
+ ok, durable = await self._call_primary("get_session", session_id)
137
+ if ok and durable is not None:
138
+ return await self._hydrate(durable)
139
+ existing = await self.fallback.get_session(session_id)
140
+ if existing is not None:
141
+ return existing
142
+
143
+ live = await self.fallback.create_session(agent_id, user_id, session_id=session_id)
144
+ ok, durable = await self._call_primary(
145
+ "create_session",
146
+ agent_id,
147
+ user_id,
148
+ session_id=live.id,
149
+ )
150
+ if ok and durable is not None:
151
+ self._primary_session_ids.add(durable.id)
152
+ return await self._hydrate(durable)
153
+ return live
154
+
155
+ async def get_session(self, session_id: str) -> Optional[Session]:
156
+ live = await self.fallback.get_session(session_id)
157
+ ok, durable = await self._call_primary("get_session", session_id)
158
+ if ok and durable is not None:
159
+ return await self._hydrate(durable)
160
+ return live
161
+
162
+ async def list_sessions(
163
+ self,
164
+ agent_id: str,
165
+ user_id: Optional[str] = None,
166
+ offset: Optional[int] = None,
167
+ limit: Optional[int] = None,
168
+ ) -> list[Session]:
169
+ ok, durable_sessions = await self._call_primary(
170
+ "list_sessions",
171
+ agent_id,
172
+ user_id,
173
+ offset,
174
+ limit,
175
+ )
176
+ if ok:
177
+ for session in durable_sessions or []:
178
+ await self._hydrate(session)
179
+ return cast(
180
+ list[Session],
181
+ await self.fallback.list_sessions(agent_id, user_id, offset, limit),
182
+ )
183
+
184
+ async def count_sessions(self, agent_id: str, user_id: Optional[str] = None) -> int:
185
+ sessions = await self.list_sessions(agent_id, user_id)
186
+ return len(sessions)
187
+
188
+ async def delete_session(self, session_id: str) -> bool:
189
+ deleted = await self.fallback.delete_session(session_id)
190
+ ok, durable_deleted = await self._call_primary("delete_session", session_id)
191
+ if ok:
192
+ self._primary_session_ids.discard(session_id)
193
+ return deleted or bool(durable_deleted) if ok else deleted
194
+
195
+ async def update_session_metadata(
196
+ self,
197
+ session_id: str,
198
+ *,
199
+ title: Optional[str] = None,
200
+ title_source: Optional[str] = None,
201
+ summary: Optional[str] = None,
202
+ first_prompt: Optional[str] = None,
203
+ last_prompt: Optional[str] = None,
204
+ ) -> Session:
205
+ if await self.fallback.get_session(session_id) is None:
206
+ await self.get_session(session_id)
207
+ live = await self.fallback.update_session_metadata(
208
+ session_id,
209
+ title=title,
210
+ title_source=title_source,
211
+ summary=summary,
212
+ first_prompt=first_prompt,
213
+ last_prompt=last_prompt,
214
+ )
215
+ await self._ensure_primary_session(session_id)
216
+ await self._call_primary(
217
+ "update_session_metadata",
218
+ session_id,
219
+ title=title,
220
+ title_source=title_source,
221
+ summary=summary,
222
+ first_prompt=first_prompt,
223
+ last_prompt=last_prompt,
224
+ )
225
+ return live
226
+
227
+ async def _ensure_primary_session(self, session_id: str) -> None:
228
+ """Create the session in PG if it only exists in memory (degraded-era)."""
229
+ if not self._primary_enabled or session_id in self._primary_session_ids:
230
+ return
231
+ async with self._primary_session_lock:
232
+ if not self._primary_enabled or session_id in self._primary_session_ids:
233
+ return
234
+ ok, durable = await self._call_primary("get_session", session_id)
235
+ if not ok:
236
+ return
237
+ if durable is None:
238
+ live = await self.fallback.get_session(session_id)
239
+ if live is None:
240
+ return
241
+ ok, durable = await self._call_primary(
242
+ "create_session",
243
+ live.agent_id,
244
+ live.user_id,
245
+ session_id=live.id,
246
+ )
247
+ if not ok or durable is None:
248
+ return
249
+ self._primary_session_ids.add(session_id)
250
+
251
+ async def append_event(self, session_id: str, event: SessionEvent) -> SessionEvent:
252
+ if await self.fallback.get_session(session_id) is None:
253
+ await self.get_session(session_id)
254
+ live = await self.fallback.append_event(session_id, event)
255
+ await self._ensure_primary_session(session_id)
256
+ await self._call_primary("append_event", session_id, event)
257
+ return live
258
+
259
+ async def get_events(
260
+ self,
261
+ session_id: str,
262
+ offset: Optional[int] = None,
263
+ limit: Optional[int] = None,
264
+ after_seq_id: Optional[int] = None,
265
+ before_seq_id: Optional[int] = None,
266
+ ) -> list[SessionEvent]:
267
+ await self.get_session(session_id)
268
+ return cast(
269
+ list[SessionEvent],
270
+ await self.fallback.get_events(
271
+ session_id,
272
+ offset,
273
+ limit,
274
+ after_seq_id,
275
+ before_seq_id,
276
+ ),
277
+ )
278
+
279
+ async def count_events(
280
+ self,
281
+ session_id: str,
282
+ after_seq_id: Optional[int] = None,
283
+ before_seq_id: Optional[int] = None,
284
+ ) -> int:
285
+ await self.get_session(session_id)
286
+ return cast(
287
+ int,
288
+ await self.fallback.count_events(session_id, after_seq_id, before_seq_id),
289
+ )
290
+
291
+ async def get_state(
292
+ self,
293
+ agent_id: str,
294
+ user_id: Optional[str],
295
+ session_id: Optional[str],
296
+ scope: str = "session",
297
+ ) -> Optional[SessionState]:
298
+ if session_id:
299
+ await self.get_session(session_id)
300
+ live = await self.fallback.get_state(agent_id, user_id, session_id, scope)
301
+ if live is not None or not self._primary_enabled:
302
+ return live
303
+ ok, durable = await self._call_primary(
304
+ "get_state",
305
+ agent_id,
306
+ user_id,
307
+ session_id,
308
+ scope,
309
+ )
310
+ if ok and durable is not None:
311
+ return await self.fallback.update_state(
312
+ agent_id=agent_id,
313
+ user_id=user_id,
314
+ session_id=session_id,
315
+ scope=scope,
316
+ state_delta=durable.state,
317
+ )
318
+ return live
319
+
320
+ async def update_state(
321
+ self,
322
+ *,
323
+ agent_id: str,
324
+ user_id: Optional[str],
325
+ session_id: Optional[str],
326
+ scope: str,
327
+ state_delta: dict[str, Any],
328
+ ) -> SessionState:
329
+ if session_id and await self.fallback.get_session(session_id) is None:
330
+ await self.get_session(session_id)
331
+ live = await self.fallback.update_state(
332
+ agent_id=agent_id,
333
+ user_id=user_id,
334
+ session_id=session_id,
335
+ scope=scope,
336
+ state_delta=state_delta,
337
+ )
338
+ if session_id:
339
+ await self._ensure_primary_session(session_id)
340
+ await self._call_primary(
341
+ "update_state",
342
+ agent_id=agent_id,
343
+ user_id=user_id,
344
+ session_id=session_id,
345
+ scope=scope,
346
+ state_delta=state_delta,
347
+ )
348
+ return live
349
+
350
+ async def aclose(self) -> None:
351
+ if self._probe_task is not None and not self._probe_task.done():
352
+ self._probe_task.cancel()
353
+ try:
354
+ await self._probe_task
355
+ except asyncio.CancelledError:
356
+ pass
357
+ for service in (self.primary, self.fallback):
358
+ close = getattr(service, "aclose", None)
359
+ if close is not None:
360
+ await close()
@@ -53,6 +53,8 @@ _FOCUSED_TOOL_NAMES = (
53
53
  "workspace_status",
54
54
  "list_workspace_files",
55
55
  "read_workspace_file",
56
+ "write_workspace_file",
57
+ "write_workspace_files",
56
58
  "search_workspace_files",
57
59
  "edit_workspace_file",
58
60
  "multi_edit_workspace_file",
@@ -150,7 +150,10 @@ def read_workspace_file(
150
150
  max_chars: int | None = None,
151
151
  include_line_numbers: bool = True,
152
152
  ) -> dict[str, Any]:
153
- """Read a UTF-8 text file from the AgentEngine workspace."""
153
+ """Read a UTF-8 workspace file. Inspect the result before editing it.
154
+
155
+ Do not issue a dependent read and edit in the same parallel tool-call batch.
156
+ """
154
157
 
155
158
  return _gateway().invoke(
156
159
  "read_workspace_file",
@@ -313,7 +316,10 @@ def edit_workspace_file(
313
316
  replace_all: bool = False,
314
317
  approval: dict[str, Any] | None = None,
315
318
  ) -> dict[str, Any]:
316
- """Replace an exact text snippet inside a UTF-8 workspace file."""
319
+ """Replace exact text after reading the file in a prior model step.
320
+
321
+ The read must complete before this call; do not batch a dependent read and edit.
322
+ """
317
323
 
318
324
  return _gateway().invoke(
319
325
  "edit_workspace_file",
@@ -336,6 +342,9 @@ def _edit_workspace_file_impl(
336
342
  ) -> dict[str, Any]:
337
343
  target = resolve_workspace_path(path)
338
344
  relative = workspace_relative(target)
345
+ request_error = _validate_single_edit_request(old_text, relative=relative)
346
+ if request_error is not None:
347
+ return request_error
339
348
  read_error = _validate_read_state(target, relative)
340
349
  if read_error is not None:
341
350
  return read_error
@@ -376,7 +385,10 @@ def multi_edit_workspace_file(
376
385
  replace_all: bool = False,
377
386
  approval: dict[str, Any] | None = None,
378
387
  ) -> dict[str, Any]:
379
- """Apply multiple exact snippet edits atomically inside one workspace file."""
388
+ """Apply atomic exact edits after reading the file in a prior model step.
389
+
390
+ The read must complete before this call; do not batch a dependent read and edit.
391
+ """
380
392
 
381
393
  return _gateway().invoke(
382
394
  "multi_edit_workspace_file",
@@ -395,21 +407,36 @@ def _multi_edit_workspace_file_impl(
395
407
  ) -> dict[str, Any]:
396
408
  target = resolve_workspace_path(path)
397
409
  relative = workspace_relative(target)
410
+ if not isinstance(edits, list) or not edits:
411
+ return {
412
+ "ok": False,
413
+ "path": relative,
414
+ "error_type": "invalid_edits",
415
+ "error_message": "edits must be a non-empty list",
416
+ }
417
+ for index, edit in enumerate(edits):
418
+ if not isinstance(edit, dict):
419
+ return {
420
+ "ok": False,
421
+ "path": relative,
422
+ "error_type": "invalid_edit",
423
+ "error_message": f"edit at index {index} must be an object",
424
+ "failed_edit_index": index,
425
+ }
426
+ request_error = _validate_single_edit_request(edit.get("old_text"), relative=relative)
427
+ if request_error is not None:
428
+ return {**request_error, "failed_edit_index": index, "edit_count": len(edits)}
398
429
  read_error = _validate_read_state(target, relative)
399
430
  if read_error is not None:
400
431
  return read_error
401
432
  text, error = _read_workspace_text(target)
402
433
  if error is not None:
403
434
  return {**error, "path": path}
404
- if not isinstance(edits, list) or not edits:
405
- return {"ok": False, "path": relative, "error_type": "invalid_edits", "error_message": "edits must be a non-empty list"}
406
435
  original = text or ""
407
436
  updated = original
408
437
  diagnostics: list[dict[str, Any]] = []
409
438
  total_replacements = 0
410
439
  for index, edit in enumerate(edits):
411
- if not isinstance(edit, dict):
412
- return {"ok": False, "path": relative, "error_type": "invalid_edit", "error_message": f"edit at index {index} must be an object", "failed_edit_index": index}
413
440
  edit_result = _apply_single_edit(
414
441
  updated,
415
442
  edit.get("old_text"),
@@ -718,9 +745,10 @@ def _apply_single_edit(
718
745
  replace_all: bool = False,
719
746
  relative: str,
720
747
  ) -> dict[str, Any]:
721
- old = str(old_text or "")
722
- if not old:
723
- return {"ok": False, "path": relative, "error_type": "missing_old_text", "error_message": "old_text is required"}
748
+ request_error = _validate_single_edit_request(old_text, relative=relative)
749
+ if request_error is not None:
750
+ return request_error
751
+ old = str(old_text)
724
752
  expected = max(1, int(expected_replacements or 1))
725
753
  actual_old, used_quote_normalization = _find_actual_string(text, old)
726
754
  matches = _match_previews(text, actual_old or old) if actual_old else []
@@ -756,6 +784,17 @@ def _apply_single_edit(
756
784
  }
757
785
 
758
786
 
787
+ def _validate_single_edit_request(old_text: Any, *, relative: str) -> dict[str, Any] | None:
788
+ if not str(old_text or ""):
789
+ return {
790
+ "ok": False,
791
+ "path": relative,
792
+ "error_type": "missing_old_text",
793
+ "error_message": "old_text is required",
794
+ }
795
+ return None
796
+
797
+
759
798
  def _match_previews(text: str, needle: str, limit: int = 20) -> list[dict[str, Any]]:
760
799
  if not needle:
761
800
  return []
ksadk/tracing/setup.py CHANGED
@@ -174,6 +174,16 @@ def _has_nonzero_token_usage(usage: dict[str, int]) -> bool:
174
174
  return any(value > 0 for value in usage.values())
175
175
 
176
176
 
177
+ def _instrumentation_scope_name(span: Any) -> str:
178
+ scope = getattr(span, "instrumentation_scope", None)
179
+ name = getattr(scope, "name", None)
180
+ if name:
181
+ return str(name)
182
+ info = getattr(span, "instrumentation_info", None)
183
+ name = getattr(info, "name", None)
184
+ return str(name or "")
185
+
186
+
177
187
  def _clone_span_with_attributes(span: Any, attributes: dict[str, Any]) -> Any:
178
188
  try:
179
189
  from opentelemetry.sdk.trace import ReadableSpan
@@ -198,6 +208,53 @@ def _clone_span_with_attributes(span: Any, attributes: dict[str, Any]) -> Any:
198
208
  return span
199
209
 
200
210
 
211
+ def _prepare_langfuse_spans(spans: Any) -> Any:
212
+ """Keep ksadk response usage authoritative for Langfuse exports.
213
+
214
+ OpenInference LangChain instrumentation can emit llm.token_count.* on nested
215
+ ChatOpenAI spans. In LangGraph streaming these counts can be cumulative and
216
+ disagree with the usage returned by ksadk to RunAgent/session state. When a
217
+ trace already has a ksadk.conversations span with gen_ai.usage.*, strip token
218
+ attrs from nested OpenInference LangChain spans before exporting to
219
+ Langfuse. The child spans remain useful for timing and structure, but cannot
220
+ pollute token accounting.
221
+ """
222
+ if not spans:
223
+ return spans
224
+ span_list = list(spans)
225
+ authoritative_trace_ids = {
226
+ _trace_id(span)
227
+ for span in span_list
228
+ if _trace_id(span) is not None
229
+ and _instrumentation_scope_name(span) == "ksadk.conversations"
230
+ and _has_nonzero_token_usage(_span_token_usage(span))
231
+ }
232
+ if not authoritative_trace_ids:
233
+ return spans
234
+
235
+ strip_prefixes = ("llm.token_count.", "llm.usage.", "gen_ai.usage.")
236
+ replacements: dict[int, Any] = {}
237
+ for span in span_list:
238
+ if _trace_id(span) not in authoritative_trace_ids:
239
+ continue
240
+ if _instrumentation_scope_name(span) != "openinference.instrumentation.langchain":
241
+ continue
242
+ attributes = dict(getattr(span, "attributes", None) or {})
243
+ stripped = {
244
+ key: value
245
+ for key, value in attributes.items()
246
+ if not any(str(key).startswith(prefix) for prefix in strip_prefixes)
247
+ }
248
+ if len(stripped) != len(attributes):
249
+ if str(stripped.get("openinference.span.kind") or "").upper() == "LLM":
250
+ stripped["openinference.span.kind"] = "CHAIN"
251
+ replacements[id(span)] = _clone_span_with_attributes(span, stripped)
252
+
253
+ if not replacements:
254
+ return spans
255
+ return [replacements.get(id(span), span) for span in span_list]
256
+
257
+
201
258
  def _prepare_cloud_monitor_spans(spans: Any) -> Any:
202
259
  """Add root token rollups for CloudMonitor without changing other exporters.
203
260
 
@@ -634,6 +691,11 @@ def setup_tracing(
634
691
  endpoint=generic_otlp_config["endpoint"],
635
692
  service_name=_get_service_name(),
636
693
  header_keys=sorted(generic_otlp_config["headers"]),
694
+ span_transform=(
695
+ _prepare_langfuse_spans
696
+ if _is_langfuse_otlp_endpoint(generic_otlp_config["endpoint"])
697
+ else None
698
+ ),
637
699
  )
638
700
  provider.add_span_processor(
639
701
  BatchSpanProcessor(
@@ -721,6 +783,7 @@ def setup_tracing(
721
783
  endpoint=config["endpoint"],
722
784
  service_name=_get_service_name(),
723
785
  header_keys=sorted(config["headers"]),
786
+ span_transform=_prepare_langfuse_spans,
724
787
  )
725
788
  provider.add_span_processor(
726
789
  BatchSpanProcessor(
ksadk/tui/__init__.py CHANGED
@@ -1,12 +1,27 @@
1
1
  """
2
- KsADK TUI - Textual Terminal User Interface
2
+ KsADK TUI - prompt_toolkit 全屏交互(对标 Codex CLI)
3
3
 
4
- 提供简洁的交互体验:
5
- - 流式 Markdown 输出
6
- - 历史记录支持
7
- - 思考过程显示
4
+ - 全屏 alternate screen,transcript 区(FormattedTextControl + ANSI 保留 rich 颜色)
5
+ + 底部输入框 + footer 状态栏
6
+ - 滚动:PageUp/PgDn/↑↓(cursor 跟随视口顶 + 手动 vertical_scroll)
7
+ - 流式:streaming 期动态区累积,turn 结束落定整条 rich 渲染(历史 ANSI 缓存)
8
+ - 命令:/new /clear /session /model(模型热切 picker)? exit;Ctrl-C 取消流式/退出
8
9
  """
9
10
 
10
- from ksadk.tui.app import AgentTUI
11
+ from ksadk.tui.loop import (
12
+ InteractionLoop,
13
+ InterruptPending,
14
+ RichLiveRenderer,
15
+ TranscriptEntry,
16
+ render_stream,
17
+ run_tui,
18
+ )
11
19
 
12
- __all__ = ["AgentTUI"]
20
+ __all__ = [
21
+ "InteractionLoop",
22
+ "InterruptPending",
23
+ "RichLiveRenderer",
24
+ "TranscriptEntry",
25
+ "render_stream",
26
+ "run_tui",
27
+ ]