Source code for jeevesagent.architecture.tool_host_wrappers
"""Tool-host wrappers used by multi-agent architectures.
A :class:`~jeevesagent.core.protocols.ToolHost` is a black box from
the agent's point of view. Architectures that need to inject extra
tools per run (Supervisor's ``delegate``, Swarm's ``handoff``, the
``Agent.run(extra_tools=...)`` per-run kwarg) build an
:class:`ExtendedToolHost` that combines a base host with a fixed
list of extra :class:`Tool` instances.
Why a wrapper rather than mutating the base host?
* User-provided agents stay untouched — running an agent inside a
supervisor doesn't permanently add a ``delegate`` tool to that
agent's host.
* Additive only — extras coexist with the base's tools; conflicts
resolve in favour of the extras (the architecture's tool wins
over a same-named user tool, which is what the architecture wants).
* Same shape as the :class:`ToolHost` protocol — drop-in.
"""
from __future__ import annotations
from collections.abc import AsyncIterator, Mapping
from typing import TYPE_CHECKING, Any
from ..core.types import ToolDef, ToolEvent, ToolResult
if TYPE_CHECKING:
from ..core.protocols import ToolHost
from ..tools.registry import Tool
[docs]
class ExtendedToolHost:
"""Combine a base :class:`ToolHost` with N extra :class:`Tool`\\ s.
``list_tools`` returns the base's defs plus the extras' defs.
``call`` dispatches to the matching extra by name; falls through
to the base for everything else. Extras win on name conflict.
"""
def __init__(
self, base: ToolHost, extras: list[Tool]
) -> None:
self._base = base
self._extras = list(extras)
self._extras_by_name: dict[str, Tool] = {
t.name: t for t in extras
}
[docs]
def register(self, item: Tool) -> Tool:
"""Mutably append a Tool to the extras pool.
Mirrors :meth:`InProcessToolHost.register` so callers
(notably the skills system, which lazy-registers Tools when
``load_skill`` fires) can add to either host kind without
special-casing."""
self._extras.append(item)
self._extras_by_name[item.name] = item
return item
[docs]
async def list_tools(
self, *, query: str | None = None
) -> list[ToolDef]:
defs = list(await self._base.list_tools(query=query))
for t in self._extras:
d = t.to_def()
if query is None or (
query.lower() in d.name.lower()
or query.lower() in d.description.lower()
):
defs.append(d)
return defs
[docs]
async def call(
self,
tool: str,
args: Mapping[str, Any],
*,
call_id: str = "",
) -> ToolResult:
extra = self._extras_by_name.get(tool)
if extra is not None:
try:
output = await extra.execute(args)
except Exception as exc: # noqa: BLE001
return ToolResult.error_(
call_id=call_id, message=str(exc)
)
return ToolResult.success(call_id=call_id, output=output)
return await self._base.call(tool, args, call_id=call_id)
[docs]
async def watch(self) -> AsyncIterator[ToolEvent]:
async for ev in self._base.watch():
yield ev