@@ -61,10 +61,11 @@ def register(ctx):
6161import sys
6262import threading
6363import time
64+ from contextlib import contextmanager
6465from dataclasses import dataclass
6566from enum import Enum
6667from pathlib import Path
67- from typing import Any , Callable , Iterable
68+ from typing import Any , Callable , Iterable , Iterator , Protocol
6869
6970__all__ = [
7071 "tool" ,
@@ -75,6 +76,13 @@ def register(ctx):
7576 "register_plugin" ,
7677 "log_registration_summary" ,
7778 "invoke_host_tool" ,
79+ "open_session_db" ,
80+ "read_session" ,
81+ "list_sessions" ,
82+ "read_session_messages" ,
83+ "append_session_message" ,
84+ "SessionDBLike" ,
85+ "SessionDBCompatibilityError" ,
7886 "deliver_media" ,
7987 "resolve_delivery_target" ,
8088 "transform_media_delivery_output" ,
@@ -126,6 +134,183 @@ def register(ctx):
126134_TELEGRAM_SPOILER_IMAGE_EXTENSIONS = {".jpg" , ".jpeg" , ".png" , ".webp" }
127135
128136
137+ class SessionDBCompatibilityError (RuntimeError ):
138+ """Hermes does not expose the SessionDB contract required by the kit."""
139+
140+
141+ class SessionDBLike (Protocol ):
142+ """Public SessionDB methods used by the kit's state helpers."""
143+
144+ def get_session (self , session_id : str ) -> dict [str , Any ] | None : ...
145+
146+ def list_sessions_rich (self , ** kwargs : Any ) -> list [dict [str , Any ]]: ...
147+
148+ def get_messages (self , session_id : str , ** kwargs : Any ) -> list [dict [str , Any ]]: ...
149+
150+ def append_message (
151+ self , session_id : str , role : str , content : Any = None , ** kwargs : Any
152+ ) -> int : ...
153+
154+ def close (self ) -> None : ...
155+
156+
157+ def _session_db_method (db : Any , method_name : str ) -> Callable [..., Any ]:
158+ method = getattr (db , method_name , None )
159+ if not callable (method ):
160+ raise SessionDBCompatibilityError (
161+ "hermes-agent SessionDB is incompatible: required public method "
162+ f"{ method_name } () is unavailable"
163+ )
164+ return method
165+
166+
167+ def _call_session_db (
168+ db : Any , method_name : str , * args : Any , ** kwargs : Any
169+ ) -> Any :
170+ method = _session_db_method (db , method_name )
171+ try :
172+ inspect .signature (method ).bind (* args , ** kwargs )
173+ except (TypeError , ValueError ) as exc :
174+ raise SessionDBCompatibilityError (
175+ "hermes-agent SessionDB is incompatible: public method "
176+ f"{ method_name } () does not accept the required arguments: { exc } "
177+ ) from exc
178+ return method (* args , ** kwargs )
179+
180+
181+ @contextmanager
182+ def open_session_db (
183+ db_path : str | Path | None = None ,
184+ * ,
185+ db : SessionDBLike | None = None ,
186+ ) -> Iterator [SessionDBLike ]:
187+ """Yield an injected or lazily opened Hermes ``SessionDB``.
188+
189+ An injected handle remains caller-owned and is never closed here. When the
190+ kit constructs the handle, it closes it on context exit. Constructing a
191+ real ``SessionDB`` may migrate the selected database, so tests should
192+ always provide a path in temporary storage.
193+ """
194+ if db is not None and db_path is not None :
195+ raise ValueError ("db and db_path are mutually exclusive" )
196+ if db is not None :
197+ yield db
198+ return
199+ try :
200+ from hermes_state import SessionDB
201+ except (ImportError , AttributeError ) as exc :
202+ raise SessionDBCompatibilityError (
203+ "hermes-agent SessionDB is unavailable; run inside a compatible "
204+ "Hermes runtime or inject a SessionDB-compatible handle"
205+ ) from exc
206+
207+ if db_path is None :
208+ owned_db = SessionDB ()
209+ else :
210+ try :
211+ inspect .signature (SessionDB ).bind (db_path = Path (db_path ))
212+ except (TypeError , ValueError ) as exc :
213+ raise SessionDBCompatibilityError (
214+ "hermes-agent SessionDB does not support the required db_path contract"
215+ ) from exc
216+ owned_db = SessionDB (db_path = Path (db_path ))
217+ try :
218+ yield owned_db
219+ finally :
220+ _session_db_method (owned_db , "close" )()
221+
222+
223+ def read_session (db : SessionDBLike , session_id : str ) -> dict [str , Any ] | None :
224+ """Read one session through Hermes' public SessionDB API."""
225+ return _call_session_db (db , "get_session" , session_id )
226+
227+
228+ def list_sessions (
229+ db : SessionDBLike ,
230+ * ,
231+ source : str | None = None ,
232+ sources : list [str ] | None = None ,
233+ exclude_sources : list [str ] | None = None ,
234+ cwd_prefix : str | None = None ,
235+ limit : int = 20 ,
236+ offset : int = 0 ,
237+ include_children : bool = False ,
238+ min_message_count : int = 0 ,
239+ project_compression_tips : bool = True ,
240+ order_by_last_active : bool = False ,
241+ include_archived : bool = False ,
242+ archived_only : bool = False ,
243+ id_query : str | None = None ,
244+ search_query : str | None = None ,
245+ compact_rows : bool = False ,
246+ include_pinned : bool = False ,
247+ session_key : str | None = None ,
248+ ) -> list [dict [str , Any ]]:
249+ """List rich session rows using Hermes-supported filters and pagination."""
250+ return _call_session_db (
251+ db ,
252+ "list_sessions_rich" ,
253+ source = source ,
254+ sources = sources ,
255+ exclude_sources = exclude_sources ,
256+ cwd_prefix = cwd_prefix ,
257+ limit = limit ,
258+ offset = offset ,
259+ include_children = include_children ,
260+ min_message_count = min_message_count ,
261+ project_compression_tips = project_compression_tips ,
262+ order_by_last_active = order_by_last_active ,
263+ include_archived = include_archived ,
264+ archived_only = archived_only ,
265+ id_query = id_query ,
266+ search_query = search_query ,
267+ compact_rows = compact_rows ,
268+ include_pinned = include_pinned ,
269+ session_key = session_key ,
270+ )
271+
272+
273+ def read_session_messages (
274+ db : SessionDBLike ,
275+ session_id : str ,
276+ * ,
277+ include_inactive : bool = False ,
278+ limit : int | None = None ,
279+ offset : int = 0 ,
280+ latest : bool = False ,
281+ after_id : int | None = None ,
282+ ) -> list [dict [str , Any ]]:
283+ """Read a session transcript using Hermes' ordering and paging rules."""
284+ return _call_session_db (
285+ db ,
286+ "get_messages" ,
287+ session_id ,
288+ include_inactive = include_inactive ,
289+ limit = limit ,
290+ offset = offset ,
291+ latest = latest ,
292+ after_id = after_id ,
293+ )
294+
295+
296+ def append_session_message (
297+ db : SessionDBLike ,
298+ session_id : str ,
299+ role : str ,
300+ content : Any = None ,
301+ ** message_fields : Any ,
302+ ) -> int :
303+ """Append a message, forwarding structured fields to Hermes unchanged."""
304+ return _call_session_db (
305+ db ,
306+ "append_message" ,
307+ session_id ,
308+ role ,
309+ content ,
310+ ** message_fields ,
311+ )
312+
313+
129314@dataclass (frozen = True )
130315class PluginSkill :
131316 """Validated declaration for a plugin-owned, read-only Hermes skill."""
0 commit comments