feat(tools): introduce plugin-based tool discovery and runtime context protocol
This commit implements a progressive refactoring of the tool system to support plugin discovery, scoped loading, and protocol-driven runtime context injection. Key changes: - Add Tool ABC metadata (tool_name, _scopes) and ToolContext dataclass for dependency injection. - Introduce ToolLoader with pkgutil-based builtin discovery and entry_points-based third-party plugin loading. - Add scope filtering (core/subagent/memory) so different contexts load appropriate tool sets. - Introduce ContextAware protocol and RequestContext dataclass to replace hardcoded per-tool context injection in AgentLoop. - Add RuntimeState / MutableRuntimeState protocols to decouple MyTool from AgentLoop. - Migrate all built-in tools to declare scopes and implement create()/enabled() hooks. - Migrate MessageTool, SpawnTool, CronTool, and MyTool to ContextAware. - Refactor AgentLoop to use ToolLoader and protocol-driven context injection. - Refactor SubagentManager to use ToolLoader(scope="subagent") with per-run FileStates isolation. - Register all built-in tools via pyproject.toml entry_points. - Add comprehensive tests for loader scopes, entry_points, ContextAware, subagent tools, and runtime state sync.
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
"""Base class for agent tools."""
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from typing import Any, TypeVar
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nanobot.agent.tools.context import ToolContext
|
||||
|
||||
_ToolT = TypeVar("_ToolT", bound="Tool")
|
||||
|
||||
# Matches :meth:`Tool._cast_value` / :meth:`Schema.validate_json_schema_value` behavior
|
||||
@@ -117,14 +124,7 @@ class Schema(ABC):
|
||||
class Tool(ABC):
|
||||
"""Agent capability: read files, run commands, etc."""
|
||||
|
||||
_TYPE_MAP = {
|
||||
"string": str,
|
||||
"integer": int,
|
||||
"number": (int, float),
|
||||
"boolean": bool,
|
||||
"array": list,
|
||||
"object": dict,
|
||||
}
|
||||
_TYPE_MAP = _JSON_TYPE_MAP
|
||||
_BOOL_TRUE = frozenset(("true", "1", "yes"))
|
||||
_BOOL_FALSE = frozenset(("false", "0", "no"))
|
||||
|
||||
@@ -166,6 +166,24 @@ class Tool(ABC):
|
||||
"""Whether this tool should run alone even if concurrency is enabled."""
|
||||
return False
|
||||
|
||||
# --- Plugin metadata ---
|
||||
|
||||
config_key: str = ""
|
||||
_plugin_discoverable: bool = True
|
||||
_scopes: set[str] = {"core"}
|
||||
|
||||
@classmethod
|
||||
def config_cls(cls) -> type[BaseModel] | None:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def enabled(cls, ctx: ToolContext) -> bool:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def create(cls, ctx: ToolContext) -> Tool:
|
||||
return cls()
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, **kwargs: Any) -> Any:
|
||||
"""Run the tool; returns a string or list of content blocks."""
|
||||
|
||||
Reference in New Issue
Block a user