Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,9 @@ live `~/.hermes/state.db`. Production helpers issue no raw SQL and delegate
ordering, pagination, structured message encoding, locking, and migration to
Hermes itself. An incompatible Hermes build raises
`SessionDBCompatibilityError` naming the missing contract.
Default-valued options added by newer Hermes releases are omitted when an
older public method signature does not accept them. Requesting a non-default
option that the running Hermes does not support still fails explicitly.

## Calling host-managed capabilities

Expand Down
62 changes: 60 additions & 2 deletions hermes_plugin_kit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,38 @@ def _call_session_db(
return method(*args, **kwargs)


def _call_session_db_evolving(
db: Any,
method_name: str,
*args: Any,
optional_defaults: dict[str, Any],
**kwargs: Any,
) -> Any:
"""Call an evolving Hermes API without forwarding absent optional features."""
method = _session_db_method(db, method_name)
try:
signature = inspect.signature(method)
except (TypeError, ValueError) as exc:
raise SessionDBCompatibilityError(
f"hermes-agent SessionDB is incompatible: cannot inspect public method {method_name}()"
) from exc
accepted = set(signature.parameters)
accepts_kwargs = any(
parameter.kind is inspect.Parameter.VAR_KEYWORD
for parameter in signature.parameters.values()
)
forwarded: dict[str, Any] = {}
for name, value in kwargs.items():
if name in accepted or accepts_kwargs:
forwarded[name] = value
elif name not in optional_defaults or value != optional_defaults[name]:
raise SessionDBCompatibilityError(
"hermes-agent SessionDB is incompatible: public method "
f"{method_name}() does not accept requested argument {name}={value!r}"
)
return _call_session_db(db, method_name, *args, **forwarded)


@contextmanager
def open_session_db(
db_path: str | Path | None = None,
Expand Down Expand Up @@ -250,9 +282,28 @@ def list_sessions(
session_key: str | None = None,
) -> list[dict[str, Any]]:
"""List rich session rows using Hermes-supported filters and pagination."""
return _call_session_db(
return _call_session_db_evolving(
db,
"list_sessions_rich",
optional_defaults={
"source": None,
"sources": None,
"exclude_sources": None,
"cwd_prefix": None,
"limit": 20,
"offset": 0,
"include_children": False,
"min_message_count": 0,
"project_compression_tips": True,
"order_by_last_active": False,
"include_archived": False,
"archived_only": False,
"id_query": None,
"search_query": None,
"compact_rows": False,
"include_pinned": False,
"session_key": None,
},
source=source,
sources=sources,
exclude_sources=exclude_sources,
Expand Down Expand Up @@ -284,10 +335,17 @@ def read_session_messages(
after_id: int | None = None,
) -> list[dict[str, Any]]:
"""Read a session transcript using Hermes' ordering and paging rules."""
return _call_session_db(
return _call_session_db_evolving(
db,
"get_messages",
session_id,
optional_defaults={
"include_inactive": False,
"limit": None,
"offset": 0,
"latest": False,
"after_id": None,
},
include_inactive=include_inactive,
limit=limit,
offset=offset,
Expand Down
17 changes: 16 additions & 1 deletion tests/test_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,25 @@ def test_incompatible_public_signature_has_instructive_error(self) -> None:
db = types.SimpleNamespace(get_messages=lambda session_id: [])
with self.assertRaisesRegex(
hpk.SessionDBCompatibilityError,
r"get_messages\(\) does not accept the required arguments",
r"get_messages\(\) does not accept requested argument limit=1",
):
hpk.read_session_messages(db, "s1", limit=1)

def test_older_public_signature_ignores_default_newer_options(self) -> None:
calls = []

def get_messages(session_id, include_inactive=False, limit=None, offset=0):
calls.append((session_id, include_inactive, limit, offset))
return []

db = types.SimpleNamespace(get_messages=get_messages)
self.assertEqual(hpk.read_session_messages(db, "s1"), [])
self.assertEqual(calls, [("s1", False, None, 0)])
with self.assertRaisesRegex(
hpk.SessionDBCompatibilityError, "does not accept requested argument latest=True"
):
hpk.read_session_messages(db, "s1", latest=True)

def test_missing_hermes_import_has_instructive_error(self) -> None:
real_import = __import__

Expand Down