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.
```markdown
# 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:
```python
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:
```python
# 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)
```json
{
"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:
```python
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)
```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`:**
```markdown
# Weather Toolset
### get_weather
Get weather for a location.
- `location` (string): City name
- `units` (string, default "metric"): metric or imperial
```
**`weather/toolset.py`:**
```python
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)
```json
{
"name": "calculator",
"type": "docker",
"code": "def main(a, b, op): ...",
"function": "main"
}
```
### After (toolset)
**`calculator/doc.md`:**
```markdown
# Calculator Toolset
### compute
Basic arithmetic.
- `a` (number): First operand
- `b` (number): Second operand
- `op` (string): add, subtract, multiply, divide
```
**`calculator/toolset.py`:**
```python
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.