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.
- {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/METADATA +6 -4
- {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/RECORD +123 -128
- ksadk/api/client.py +19 -0
- ksadk/cli/cmd_create.py +34 -7
- ksadk/cli/cmd_hermes.py +6 -2
- ksadk/cli/cmd_invoke.py +208 -41
- ksadk/cli/cmd_mcp.py +6 -1
- ksadk/cli/cmd_openclaw.py +6 -2
- ksadk/cli/cmd_run.py +13 -6
- ksadk/cli/network_options.py +5 -3
- ksadk/configs/env_registry.py +1 -0
- ksadk/conversations/message_projection.py +269 -0
- ksadk/conversations/model_context.py +11 -1
- ksadk/conversations/runtime.py +81 -13
- ksadk/deployment/base.py +1 -1
- ksadk/deployment/providers/serverless.py +18 -8
- ksadk/memory/adk/resilient_session_service.py +268 -0
- ksadk/memory/adk/short_term_memory.py +21 -6
- ksadk/runners/adk_runner.py +974 -115
- ksadk/runners/langchain_runner.py +219 -7
- ksadk/runners/langgraph_runner.py +145 -12
- ksadk/runners/remote_runner.py +143 -18
- ksadk/server/app.py +148 -6
- ksadk/server/static/assets/{ArtifactsPanel-C0meuOdi.js → ArtifactsPanel-D-SSn2ee.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-BbI4HPtJ.js → CodeBlock-P16VOLYi.js} +1 -1
- ksadk/server/static/assets/{MathMessageMarkdown-DMLQYoLp.js → MathMessageMarkdown-CdGiQWHR.js} +1 -1
- ksadk/server/static/assets/{MermaidBlock-DCR8WZ17.js → MermaidBlock-IOcal8cm.js} +6 -6
- ksadk/server/static/assets/{NativeTerminalPanel-CKUvWhUz.js → NativeTerminalPanel-DGSDWsei.js} +1 -1
- ksadk/server/static/assets/{arc-bj2rZnP5.js → arc-BA13sHc9.js} +1 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-3BPJPVTR-2u1ltDIj.js → architectureDiagram-3BPJPVTR-DzJzgVoL.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-GPEHLZMM-D6fvdK20.js → blockDiagram-GPEHLZMM-UTdVcZhn.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-AAUBKEIU-BK82L5n5.js → c4Diagram-AAUBKEIU-Dix0GD4m.js} +1 -1
- ksadk/server/static/assets/channel-CTuydARX.js +1 -0
- ksadk/server/static/assets/{chunk-2J33WTMH-DLVdkeyQ.js → chunk-2J33WTMH-C2JxuZ42.js} +1 -1
- ksadk/server/static/assets/{chunk-4BX2VUAB-BbwsYcbW.js → chunk-4BX2VUAB-Be57QPgD.js} +1 -1
- ksadk/server/static/assets/{chunk-55IACEB6-DOairxzw.js → chunk-55IACEB6-WjSuy4nv.js} +1 -1
- ksadk/server/static/assets/{chunk-727SXJPM-hCO487gs.js → chunk-727SXJPM-BOovP-b5.js} +1 -1
- ksadk/server/static/assets/{chunk-AQP2D5EJ-XU8vDOuG.js → chunk-AQP2D5EJ-DOmZNmWj.js} +1 -1
- ksadk/server/static/assets/{chunk-FMBD7UC4-PrapyfMu.js → chunk-FMBD7UC4-DzwlodkU.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-CboKhTZ3.js → chunk-ND2GUHAM-DMqAikEU.js} +1 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-_N8bJ-EU.js → cose-bilkent-S5V4N54A-Zhy2GBnP.js} +1 -1
- ksadk/server/static/assets/{dagre-BM42HDAG-FoDlo5J2.js → dagre-BM42HDAG-CbKrtIUz.js} +1 -1
- ksadk/server/static/assets/{diagram-2AECGRRQ-BBp6ebBw.js → diagram-2AECGRRQ-BRApgQRK.js} +1 -1
- ksadk/server/static/assets/{diagram-5GNKFQAL-CqOvG1PR.js → diagram-5GNKFQAL-D0xbnDiK.js} +1 -1
- ksadk/server/static/assets/{diagram-KO2AKTUF-O9T3t6ba.js → diagram-KO2AKTUF-CVugVI9L.js} +1 -1
- ksadk/server/static/assets/{diagram-LMA3HP47-CGG8ekBO.js → diagram-LMA3HP47-DozUGgee.js} +1 -1
- ksadk/server/static/assets/{diagram-OG6HWLK6-4BmH1EEg.js → diagram-OG6HWLK6-ZOYsKlru.js} +1 -1
- ksadk/server/static/assets/{dist-BmuTWcm7.js → dist-2hInLS_J.js} +1 -1
- ksadk/server/static/assets/{dist-BMKM66B4.js → dist-8EEyGgDx.js} +1 -1
- ksadk/server/static/assets/{dist-Cp1_jKwf.js → dist-8_SfK1IP.js} +1 -1
- ksadk/server/static/assets/{dist-CZ6C3x8B.js → dist-B0NP_Z0z.js} +1 -1
- ksadk/server/static/assets/{dist-DLeM1OCK.js → dist-BIJVDMuB.js} +1 -1
- ksadk/server/static/assets/{dist-Cwhk4lwI.js → dist-BmlMPsCc.js} +1 -1
- ksadk/server/static/assets/{dist-D99wf6_X.js → dist-BrCcQ47T.js} +1 -1
- ksadk/server/static/assets/{dist-CWiTDN_V.js → dist-BssVmjIo.js} +1 -1
- ksadk/server/static/assets/{dist-BVzcWa6D.js → dist-C3HIIaN5.js} +1 -1
- ksadk/server/static/assets/{dist-DM0rEQPk.js → dist-CPBtqRTW.js} +1 -1
- ksadk/server/static/assets/{dist-BkJjNih3.js → dist-CkEKJFtk.js} +1 -1
- ksadk/server/static/assets/{dist-B67uF2uK.js → dist-D-JMdWPQ.js} +1 -1
- ksadk/server/static/assets/{dist-DI-oVAXw.js → dist-D-xOD048.js} +1 -1
- ksadk/server/static/assets/{dist-6kSZvCZh.js → dist-D57wnGWd.js} +1 -1
- ksadk/server/static/assets/{dist-Bk1l05Ni.js → dist-DL_NKc_z.js} +1 -1
- ksadk/server/static/assets/{dist-DbaH3p5h.js → dist-DMezvBGJ.js} +1 -1
- ksadk/server/static/assets/{dist-CiZHB6ZW.js → dist-DmFaq_Iy.js} +1 -1
- ksadk/server/static/assets/{dist-DLMjL01k.js → dist-Dp0k7bWI.js} +1 -1
- ksadk/server/static/assets/{dist-DCpZrjaK.js → dist-DpKtt4Bo.js} +1 -1
- ksadk/server/static/assets/{dist-D5JGcxBx.js → dist-DyOkZmze.js} +1 -1
- ksadk/server/static/assets/{dist-C2DKRFVK.js → dist-DyWsb78Q.js} +1 -1
- ksadk/server/static/assets/{dist-S4klkK5W.js → dist-gX6GOLKq.js} +1 -1
- ksadk/server/static/assets/{erDiagram-TEJ5UH35-oJhaci7S.js → erDiagram-TEJ5UH35-Bk-9bgE5.js} +1 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +1 -0
- ksadk/server/static/assets/{flowDiagram-I6XJVG4X-DtTmUan-.js → flowDiagram-I6XJVG4X-Dn9kZVuZ.js} +1 -1
- ksadk/server/static/assets/{ganttDiagram-6RSMTGT7-BSsb_eij.js → ganttDiagram-6RSMTGT7-BGxXlJqm.js} +1 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-PVQCEYII-BR5s6Pws.js → gitGraphDiagram-PVQCEYII-C7Q0X-wi.js} +1 -1
- ksadk/server/static/assets/{index-BH7-pC0B.js → index-C00Lwg3L.js} +6 -6
- ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +1 -0
- ksadk/server/static/assets/{infoDiagram-5YYISTIA-DYPc5cvh.js → infoDiagram-5YYISTIA-BO_CSqq5.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-MoBdALDg.js → ishikawaDiagram-YF4QCWOH-eYLPLd46.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-DgLiQT8l.js → journeyDiagram-JHISSGLW-De9geSb0.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-CsFx112F.js → kanban-definition-UN3LZRKU-Cs80kQto.js} +1 -1
- ksadk/server/static/assets/{katex-DRsiwpM2.js → katex-DSrCFD8n.js} +1 -1
- ksadk/server/static/assets/{linear-TNRQ5Orv.js → linear-CN9sbKTF.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-D2eMVOgo.js → mermaid-parser.core-DlgSBU8l.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-BtByYFhL.js → mindmap-definition-RKZ34NQL-9afz7Oug.js} +1 -1
- ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +1 -0
- ksadk/server/static/assets/{pieDiagram-4H26LBE5-DPnuCB8c.js → pieDiagram-4H26LBE5-DPv_VstN.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-miFpCPv4.js → quadrantDiagram-W4KKPZXB-o77wZ1XK.js} +1 -1
- ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +1 -0
- ksadk/server/static/assets/{requirementDiagram-4Y6WPE33-mjB_yYkW.js → requirementDiagram-4Y6WPE33-BYVw81VC.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-D7hSlZZZ.js → sankeyDiagram-5OEKKPKP-B7nZFMKT.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-D9Cx6hC3.js → sequenceDiagram-3UESZ5HK-BGRRrFw5.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-AJRCARHV-D_zrg47o.js → stateDiagram-AJRCARHV-BUfHCoPs.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +1 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-CwSec0Wa.js → timeline-definition-PNZ67QCA-FbCTGrf9.js} +1 -1
- ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +1 -0
- ksadk/server/static/assets/{vennDiagram-CIIHVFJN-agCVobiF.js → vennDiagram-CIIHVFJN-DWgwtPsJ.js} +1 -1
- ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-YWT4CUSO-TmrlhWNt.js → wardleyDiagram-YWT4CUSO-2x8G-tTM.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-2RQKCTM6-DV71V-ah.js → xychartDiagram-2RQKCTM6-Drma3AYl.js} +1 -1
- ksadk/server/static/index.html +1 -1
- ksadk/sessions/__init__.py +17 -21
- ksadk/sessions/continuity.py +28 -4
- ksadk/sessions/postgres_service.py +77 -14
- ksadk/sessions/resilience.py +52 -0
- ksadk/sessions/resilient.py +360 -0
- ksadk/toolsets/__init__.py +2 -0
- ksadk/toolsets/workspace.py +49 -10
- ksadk/tracing/setup.py +63 -0
- ksadk/tui/__init__.py +22 -7
- ksadk/tui/loop.py +1877 -0
- ksadk/tui/stream_render.py +68 -0
- ksadk/version.py +1 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +0 -1
- ksadk/server/static/assets/channel-BSSOwFIb.js +0 -1
- ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +0 -1
- ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +0 -1
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +0 -1
- ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +0 -1
- ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +0 -1
- ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +0 -1
- ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +0 -1
- ksadk/tui/app.py +0 -400
- ksadk/tui/clipboard.py +0 -100
- ksadk/tui/widgets/__init__.py +0 -22
- ksadk/tui/widgets/assistant.py +0 -89
- ksadk/tui/widgets/base.py +0 -54
- ksadk/tui/widgets/chat_input.py +0 -246
- ksadk/tui/widgets/history.py +0 -156
- ksadk/tui/widgets/system.py +0 -87
- ksadk/tui/widgets/thinking.py +0 -110
- ksadk/tui/widgets/tool_call.py +0 -161
- ksadk/tui/widgets/user.py +0 -37
- {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.6.9.dist-info → agentengine_sdk_python-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {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()
|
ksadk/toolsets/__init__.py
CHANGED
ksadk/toolsets/workspace.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
722
|
-
if not
|
|
723
|
-
return
|
|
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 -
|
|
2
|
+
KsADK TUI - prompt_toolkit 全屏交互(对标 Codex CLI)
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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__ = [
|
|
20
|
+
__all__ = [
|
|
21
|
+
"InteractionLoop",
|
|
22
|
+
"InterruptPending",
|
|
23
|
+
"RichLiveRenderer",
|
|
24
|
+
"TranscriptEntry",
|
|
25
|
+
"render_stream",
|
|
26
|
+
"run_tui",
|
|
27
|
+
]
|