AgentToolStore / plan /REFACTOR_PLAN.md
ToolStore Agent
refactor: remove api/docker types, add toolset type
2036bab
|
Raw
History Blame Contribute Delete
15.5 kB

ToolStore Refactor Plan β€” Toolset (Agent-Centric Managed Tools)

1. Overview & Motivation

Current State

Type Mechanism Managed By
api JSON definition β†’ HTTP call via httpx Us
mcp Discovered from external MCP servers Client
skill SKILL.md (YAML + Markdown), progressive disc. Client
docker Base64-encoded Python β†’ warm Docker container Us

Problem: api and docker are ad-hoc. An API "tool" is just a URL + method β€” no custom logic, no code reuse. A docker "tool" is a raw base64 blob β€” no structure, no metadata.

Skills are human-centric (progressive disclosure: load β†’ browse files β†’ read file β†’ run script). Agents need to do a multi-step dance just to use one. That friction is appropriate for human-authored instructional content, not for executable code.

The new toolset is agent-centric: the agent calls tool_store(action="execute", tool_name="weather", ...) and it just runs.

Goal

  1. Keep mcp (external MCP servers β€” client-managed) and skill (human-centric progressive disclosure β€” client-managed).
  2. Introduce toolset β€” a new type we manage. Agent-centric. 1 doc file + 1 code file. Code has @tool decorator bindings.
  3. Remove api and docker. Their functionality is refactored into toolsets.

2. New Type: toolset

2.1 The Core Idea

A toolset is a directory with exactly two files:

weather/                     # directory name = toolset name
β”œβ”€β”€ doc.md                   # agent-facing documentation
└── toolset.py               # code with @tool-decorated functions

That's it. No scripts/, no references/, no assets/, no progressive disclosure, no SKILL.md. The agent calls it and it runs.

2.2 doc.md β€” Agent-Facing Documentation

Plain markdown. Describes what the toolset does, what functions are available, what parameters they take, what they return. The agent reads this to understand the tool.

# Weather Toolset

Get weather data from public APIs.

## Functions

### get_weather
Get current weather for a location.

| Parameter | Type   | Required | Default | Description        |
|-----------|--------|----------|---------|--------------------|
| location  | string | yes      | β€”       | City name or coords|
| units     | string | no       | metric  | metric / imperial  |

Returns: JSON with temperature, humidity, wind speed.

### get_forecast
Get 5-day forecast.

| Parameter | Type    | Required | Default | Description  |
|-----------|---------|----------|---------|--------------|
| location  | string  | yes      | β€”       | City name    |
| days      | integer | no       | 5       | Number of days|

Returns: JSON array of daily forecasts.

2.3 toolset.py β€” Code with @tool Bindings

The code file uses a @tool decorator to mark functions as callable entry points:

from toolstore.toolset import tool

@tool
def get_weather(location: str, units: str = "metric"):
    """Get current weather for a location."""
    import httpx
    resp = httpx.get(
        "https://api.openweathermap.org/data/2.5/weather",
        params={"q": location, "units": units, "appid": "..."}
    )
    resp.raise_for_status()
    return resp.json()

@tool
def get_forecast(location: str, days: int = 5):
    """Get 5-day forecast for a location."""
    import httpx
    resp = httpx.get(
        "https://api.openweathermap.org/data/2.5/forecast",
        params={"q": location, "cnt": days * 8}
    )
    resp.raise_for_status()
    return resp.json()

@tool decorator rules:

  • Marks a function as an entry point callable by agents.
  • The function name becomes the binding name.
  • The function docstring is used as the binding description.
  • Function signature (params + type hints + defaults) defines the input schema.
  • Return value is JSON-serialized and sent back to the agent.

Only @tool-decorated functions are callable. Other functions/classes in toolset.py are private helpers (not exposed).

2.4 How The Agent Uses It

One call, no ceremony:

tool_store(
    action="execute",
    tool_name="weather",
    arguments={"function": "get_weather", "location": "London", "units": "metric"}
)

That's it. No load/files/file/run dance. The agent can also request the doc:

tool_store(action="info", tool_name="weather")

Which returns the toolset definition including the full doc.md body.

2.5 How It Subsumes api and docker

Old type New equivalent
api β€” {"endpoint": "...", "method": "GET", "parameters": {...}} A toolset.py with @tool functions that use httpx. Full Python β€” headers, auth, retries, response parsing, chaining calls β€” not just a rigid URL + method template.
docker β€” {"code": "<base64>", "function": "main"} A toolset.py with @tool functions. Same warm Docker worker executes it. But now it has proper structure, metadata, and a doc.md.

3. toolset Module (the @tool decorator)

A lightweight module (toolstore/toolset.py) that toolsets import:

# toolstore/toolset.py
"""
Decorator module imported by toolset code files.
Provides the @tool decorator that marks functions as agent-callable.
"""

from typing import Callable, Any

_REGISTRY: dict[str, Callable] = {}

def tool(fn: Callable) -> Callable:
    """Decorator: mark a function as a toolset entry point."""
    _REGISTRY[fn.__name__] = fn
    fn._is_toolset_tool = True
    return fn

def get_tool(name: str) -> Callable | None:
    """Get a registered tool function by name."""
    return _REGISTRY.get(name)

def get_tool_names() -> list[str]:
    """Get all registered tool names."""
    return list(_REGISTRY.keys())

def clear_registry() -> None:
    """Clear the registry (called between toolset loads)."""
    _REGISTRY.clear()

This module lives inside ToolStore itself (not a third-party dependency). Toolsets import it with from toolstore.toolset import tool.


4. Discovery & Registration

4.1 Toolset Discovery

A new ToolsetManager scans configured directories for toolset.py files:

toolsets/                    # configured toolset directory
β”œβ”€β”€ weather/toolset.py       β†’ name="weather", type="toolset"
β”œβ”€β”€ github/toolset.py        β†’ name="github", type="toolset"
└── calculator/toolset.py    β†’ name="calculator", type="toolset"

Each directory containing a toolset.py is a toolset. The directory name becomes the tool name.

4.2 Registration

On scan, ToolsetManager:

  1. Reads doc.md from the toolset directory.
  2. Imports toolset.py in a controlled way (parses @tool-decorated functions without executing them).
  3. Extracts function names, signatures, docstrings, and parameter info.
  4. Builds a tool definition with a proper JSON Schema input.

4.3 Tool Definition (index entry)

{
  "name": "weather",
  "type": "toolset",
  "description": "Get weather data from public APIs.",
  "source": "toolset",
  "toolset_dir": "/path/to/toolsets/weather",
  "doc": "<raw doc.md content>",
  "bindings": {
    "get_weather": {
      "description": "Get current weather for a location.",
      "parameters": {
        "location": {"type": "string", "required": true},
        "units": {"type": "string", "required": false, "default": "metric"}
      }
    },
    "get_forecast": {
      "description": "Get 5-day forecast for a location.",
      "parameters": {
        "location": {"type": "string", "required": true},
        "days": {"type": "integer", "required": false, "default": 5}
      }
    }
  },
  "schema": {
    "input_schema": {
      "type": "object",
      "properties": {
        "function": {
          "type": "string",
          "enum": ["get_weather", "get_forecast"],
          "description": "Which function to call in this toolset"
        },
        "location": {"type": "string", "description": "..."},
        "units": {"type": "string", "description": "..."},
        "days": {"type": "integer", "description": "..."}
      }
    }
  }
}

5. Execution Model

5.1 Flow

tool_store_tool(action="execute", tool_name="weather",
                function="get_weather", location="London", units="metric")
  β”‚
  β”œβ”€ index_manager.get_tool("weather") β†’ type="toolset"
  β”‚
  └─ _execute_toolset(tool, args)
       β”‚
       β”œβ”€ 1. Read toolset.py from disk
       β”œβ”€ 2. Extract function "get_weather" from args
       β”œβ”€ 3. Load module in warm Docker worker
       β”‚     docker_pool.get_worker().load_module(name, code)
       β”œβ”€ 4. Call the function
       β”‚     worker.call_function(name, "get_weather", fn_args, timeout)
       └─ 5. Return JSON result

5.2 Sandbox (reuses docker_pool.py)

Same warm Docker worker as today's docker tools. The worker:

  • Loads the Python module (with @tool decorator available in its environment)
  • Calls get_tool(name) to get the function
  • Calls the function with the agent's arguments
  • Serializes the return value as JSON

5.3 Argument Passing

The function argument selects which @tool function to call. All other arguments are passed as kwargs to the function:

agent calls:  arguments={"function": "get_weather", "location": "London", "units": "metric"}
becomes:      get_weather(location="London", units="metric")

If function is omitted and there's only one @tool function, it's called directly.


6. What Gets Removed

6.1 Client (toolstore/)

File Change
native_tool.py Remove _execute_api() (L200-227), _execute_docker() (L308-357), and their branches in _do_execute(). Add _execute_toolset().
mcp_server.py Remove _execute_api() (L176-197), add _execute_toolset(). Remove api/docker from dispatch.
cli.py Remove publish-api, publish-docker commands. Add publish-toolset command. Remove api/docker interactive sections.
schema_converter.py No changes needed (it already handles arbitrary tool defs by reading schema).
index_manager.py No changes. Type-agnostic.
config_manager.py Add get_toolset_dirs() alongside existing get_skill_dirs().
docker_pool.py Update docstrings: "executes toolsets" instead of "docker tools". Add @tool decorator stub to the warm worker environment.
transport.py No changes (transport is MCP-only).
management/server.py Remove api/docker UI and endpoints. Add toolset management.
skill_manager.py No changes. Skills stay as-is.

6.2 Server (server/app/)

File Change
models.py Remove APITool and DockerTool models. Add ToolsetTool model.
main.py Remove /tools/api and /tools/docker endpoints. Add /tools/toolset endpoint.

6.3 New Files

File Purpose
toolstore/toolset.py The @tool decorator module that toolsets import
toolstore/toolset_manager.py Discovery, parsing, registration (reads doc.md + inspects toolset.py)

7. Final Type Taxonomy

Type Managed By Description
mcp Client External MCP servers (unchanged)
skill Client Human-centric SKILL.md progressive disclosure (unchanged)
toolset Us Agent-centric: 1 doc + 1 code, @tool bindings, just runs

All future "managed by us" extensibility goes through toolset. No other custom types.


8. Key Design Decisions

  1. @tool is a Python decorator, not a markdown annotation. It lives in the code file, where code belongs. Simple, standard Python.
  2. 1 doc + 1 code. Exactly two files. No subdirectories, no scripts/, no assets/, no SKILL.md. Frictionless for agents.
  3. Agents just execute. No progressive disclosure, no load/files/file/run. One call: execute.
  4. Skills stay human-centric. Toolsets are the agent-centric sibling, not a replacement.
  5. Same warm Docker worker. docker_pool.py is repurposed, not rewritten.
  6. The @tool decorator IS the binding. Function name = binding name. Docstring = description. Signature = schema. No duplication.

9. Example: Converting API β†’ Toolset

Before (api JSON)

{
  "name": "weather-api",
  "type": "api",
  "endpoint": "https://api.weather.com/data",
  "method": "GET",
  "parameters": {
    "location": {"type": "string", "required": true},
    "units": {"type": "string", "required": false, "default": "metric"}
  }
}

After (toolset)

weather/doc.md:

# Weather Toolset

### get_weather
Get weather for a location.
- `location` (string): City name
- `units` (string, default "metric"): metric or imperial

weather/toolset.py:

from toolstore.toolset import tool

@tool
def get_weather(location: str, units: str = "metric"):
    """Get current weather for a location."""
    import httpx
    resp = httpx.get(
        "https://api.weather.com/data",
        params={"location": location, "units": units}
    )
    resp.raise_for_status()
    return resp.json()

Agent calls it:

tool_store(action="execute", tool_name="weather",
           arguments={"function": "get_weather", "location": "London"})

Before (docker JSON)

{
  "name": "calculator",
  "type": "docker",
  "code": "def main(a, b, op): ...",
  "function": "main"
}

After (toolset)

calculator/doc.md:

# Calculator Toolset

### compute
Basic arithmetic.
- `a` (number): First operand
- `b` (number): Second operand
- `op` (string): add, subtract, multiply, divide

calculator/toolset.py:

from toolstore.toolset import tool

@tool
def compute(a: float, b: float, op: str = "add"):
    """Basic arithmetic."""
    ops = {"add": a + b, "subtract": a - b,
           "multiply": a * b, "divide": a / b}
    return {"result": ops[op]}

10. Migration Path

Phase 1: Add toolset (zero breaking changes)

  1. Create toolstore/toolset.py (the @tool decorator).
  2. Create toolstore/toolset_manager.py.
  3. Add _execute_toolset() in native_tool.py.
  4. Add toolset dispatch in mcp_server.py.
  5. Add toolset to config_manager.py.
  6. Add server-side ToolsetTool model + endpoint.
  7. Add publish-toolset CLI command.

All four types coexist. No existing tool breaks.

Phase 2: Convert existing tools

  1. Migration script: api JSON β†’ toolset directory.
  2. Migration script: docker JSON β†’ toolset directory.
  3. Convert all examples and reference tools.

Phase 3: Remove api and docker

  1. Delete _execute_api() + _execute_docker() from native_tool.py and mcp_server.py.
  2. Remove APITool + DockerTool from server models.
  3. Remove api/docker CLI commands, endpoints, UI sections.
  4. Clean up stale references.

Phase 4: Polish

  1. Per-toolset timeout and env var config.
  2. Toolset validation CLI (toolstore validate-toolset <path>).
  3. Support for Node.js toolsets (runtime: node β†’ warm Node container).
  4. Package dependency declarations.