Source code for jeevesagent.skills.tools
"""Tool factories that surface skills to the agent's model.
We inject ONE tool — ``load_skill(name)`` — into the agent's tool
host whenever a non-empty :class:`SkillRegistry` is configured. The
tool's input schema enumerates the registered skill names as an
``enum`` so:
* Strict-schema providers (Anthropic / OpenAI strict mode) reject
hallucinated skill names at the API boundary
* The model sees every available skill name in the schema docs
* Typos return a tool error with the valid set listed
The tool's *description* also lists every skill with its short
description, giving the model the full catalog at metadata cost
without loading any bodies.
When a skill ships pending Tools (Mode B from ``tools.py`` or
Mode C from frontmatter ``tools:`` manifest), ``load_skill`` ALSO
registers those Tools with the agent's tool host on the first call.
The model sees the new tools in its toolset on the next turn.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..tools.registry import InProcessToolHost, Tool
from .registry import SkillRegistry
from .skill import SkillError
if TYPE_CHECKING:
from ..core.protocols import ToolHost
[docs]
def make_load_skill_tool(
registry: SkillRegistry,
*,
host: ToolHost | None = None,
tool_name: str = "load_skill",
) -> Tool:
"""Build the ``load_skill`` tool for a given registry.
When ``host`` is provided, the tool will register a skill's
pending Tools (from Mode B / Mode C) with the host on first
load — making them callable on subsequent turns. Without a
host, ``load_skill`` only returns the body (skill brings no
tools, or the framework integration handles registration
elsewhere).
"""
skill_names = registry.names()
catalog_lines = "\n".join(
s.metadata.to_catalog_line() for s in registry
)
description = (
"Load the full instructions for a packaged skill. Call this "
"ONCE per task when the user's request matches one of the "
"available skills' descriptions. The tool returns the "
"skill's full markdown body — follow its instructions step "
"by step using the standard tools (read / write / bash / "
"etc.). When a skill brings its own tools (marked '+N "
"tools' in the catalog below), those tools also become "
"callable on subsequent turns."
)
if catalog_lines:
description += f"\n\nAvailable skills:\n{catalog_lines}"
async def _load(name: str) -> str:
try:
body, pending = registry.load_with_tools(name)
except SkillError as exc:
return f"Error: {exc}"
# Register any skill-shipped Tools so the model can use
# them on the next turn. Only fires once per skill —
# load_with_tools is idempotent.
if pending and host is not None:
for tool in pending:
if isinstance(host, InProcessToolHost):
host.register(tool)
elif hasattr(host, "register"):
host.register(tool) # type: ignore[attr-defined]
# Hosts without register() (e.g. an immutable MCP
# adapter) silently skip; we already validated the
# registration path during Agent construction by
# using ExtendedToolHost when needed.
if pending:
tool_list = ", ".join(t.name for t in pending)
footer = (
f"\n\n---\n_{len(pending)} tool(s) now available: "
f"{tool_list}_"
)
return body + footer
return body
return Tool(
name=tool_name,
description=description,
fn=_load,
input_schema={
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": skill_names,
"description": (
"The skill name to load. Must be one of: "
f"{', '.join(skill_names) or '(no skills registered)'}."
),
}
},
"required": ["name"],
},
)