Skip to content
Closed
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
13 changes: 12 additions & 1 deletion hermes_cli/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -1462,8 +1462,19 @@ def _discover_and_load_inner(self) -> None:
# is imported only when the gateway / cron / setup / send_message
# path actually asks for that platform. Every platform Hermes ships
# remains available out of the box — it just loads on first use.
#
# Exception: a platform plugin that also provides outbound agent
# tools (declared via ``provides_tools`` in plugin.yaml) must load
# eagerly. Those tools need to be visible in CLI/TUI sessions,
# not only in gateway/web processes where the deferred loader
# fires — otherwise the toolset is unreachable via ``hermes tools``
# and invisible to the agent (#78050). Pure inbound adapters leave
# ``provides_tools`` empty and stay deferred.
if manifest.source == "bundled" and manifest.kind == "platform":
self._register_deferred_platform(manifest)
if manifest.provides_tools:
self._load_plugin(manifest)
else:
self._register_deferred_platform(manifest)
continue

# Everything else (standalone, user-installed backends,
Expand Down
11 changes: 11 additions & 0 deletions plugins/platforms/a2a/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ name: a2a-platform
label: A2A
kind: platform
version: 1.0.0
# Declares the outbound client tools this plugin provides. A platform plugin
# with non-empty provides_tools is eagerly loaded (not deferred) so its tools
# are available in CLI/TUI sessions, not only in gateway/web processes where
# the deferred platform loader fires. Safe for a2a because the entire module
# is stdlib-only (no heavy platform SDK to defer).
provides_tools:
- a2a_call
- a2a_discover
- a2a_list
- a2a_history
- a2a_orchestrate
description: >
A2A (Agent-to-Agent) protocol v1.0 support for Hermes Agent — both directions
of the open Linux Foundation standard for inter-agent communication.
Expand Down
84 changes: 84 additions & 0 deletions tests/plugins/test_a2a_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1618,3 +1618,87 @@ def test_forward_to_profile_first_contact_creates_then_resumes_fake_hermes(self,
title = con.execute("SELECT title FROM sessions WHERE id='sess-1'").fetchone()[0]
con.close()
assert title == "a2a-dev-ctx-unsafe-value"


# --------------------------------------------------------------------------
# #78050 — A2A client tools must be visible in CLI/TUI sessions
#
# A platform plugin that declares ``provides_tools`` in its manifest loads
# eagerly so its outbound agent tools register in every process — not only in
# gateway/web processes where the deferred platform loader fires. Pure inbound
# adapters (telegram, discord, …) leave ``provides_tools`` empty and stay
# deferred to avoid importing ~20 heavy platform SDKs on every CLI start.
# --------------------------------------------------------------------------

_A2A_TOOL_NAMES = {
"a2a_call",
"a2a_discover",
"a2a_list",
"a2a_history",
"a2a_orchestrate",
}


class TestA2APluginEagerToolRegistration:
"""The a2a platform plugin provides outbound client tools, so it must be
eagerly loaded — not deferred — for those tools to appear in CLI/TUI."""

@pytest.fixture(autouse=True)
def _isolate_home(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

@staticmethod
def _discover():
from hermes_cli import plugins as pmod

mgr = pmod.PluginManager()
mgr.discover_and_load()
return mgr

def test_a2a_platform_eagerly_loaded_not_deferred(self):
"""The a2a plugin loads eagerly; its module is imported and tools
are registered immediately at discovery time."""
mgr = self._discover()
loaded = mgr._plugins.get("a2a-platform")
assert loaded is not None, "a2a-platform not discovered"
assert not loaded.deferred, "a2a-platform should load eagerly, not defer"
assert loaded.module is not None, "module should be imported eagerly"
assert loaded.enabled

def test_a2a_manifest_declares_provides_tools(self):
"""The manifest carries the provides_tools declaration that triggers
eager loading."""
mgr = self._discover()
loaded = mgr._plugins.get("a2a-platform")
assert loaded is not None
assert set(loaded.manifest.provides_tools) == _A2A_TOOL_NAMES

def test_a2a_tools_registered_after_discovery(self):
"""All five outbound tools are registered after plugin discovery."""
mgr = self._discover()
loaded = mgr._plugins.get("a2a-platform")
assert loaded is not None
assert set(loaded.tools_registered) == _A2A_TOOL_NAMES

def test_a2a_toolset_resolvable_after_discovery(self):
"""resolve_toolset('a2a') returns the five tools — the opt-in path
via ``hermes tools`` can now see them."""
self._discover()
from toolsets import resolve_toolset

resolved = resolve_toolset("a2a")
assert set(resolved) == _A2A_TOOL_NAMES

def test_pure_inbound_adapter_stays_deferred(self):
"""A platform plugin without provides_tools (e.g. telegram) must
still be deferred — the eager-load exception is scoped to plugins
that declare outbound tools."""
mgr = self._discover()
telegram = mgr._plugins.get("telegram-platform")
if telegram is None:
pytest.skip("telegram-platform plugin not present in this checkout")
assert telegram.deferred, (
"telegram has no provides_tools — it must stay deferred"
)
Loading