Creating a Tool¶
A tool is a small program that agents download and run locally. You publish it once to the Tools Registry, and agents load it on demand with the Tools SDK.
Tools come in two runtimes:
| Runtime | tool_runtime_type |
What you ship |
|---|---|---|
| Python | "python" (or "") |
code/function.py with an AgentSpaceV1Tool class, plus an optional requirements.txt |
| Binary | any other value, e.g. "binary" |
A single executable in code/ that reads JSON from its arguments |
The overall flow:
write code -> test locally -> build bundle (spec.json + tool.zip + tool.md) -> POST /tools/upload -> AgentTools.add() + execute
Python tools¶
1. Layout¶
text-stats/
├── code/
│ ├── function.py # required: defines AgentSpaceV1Tool
│ └── requirements.txt # optional: pip dependencies
├── spec.json # registry metadata (see step 3)
└── tool.md # human-readable docs (stored as tool_man_page_doc)
2. The AgentSpaceV1Tool class¶
The runtime imports code/function.py and instantiates the class named exactly AgentSpaceV1Tool.
| Method | Required | Called with |
|---|---|---|
__init__(self, tool_id, tool_data) |
Yes | tool_id: the registry ID. tool_data: the full registry record of the tool, a dict with tool_id, tool_api_spec, tool_parameters, and so on. |
execute(self, input_data) |
Yes | The dict passed to execute_tool_by_id(). Return a JSON-serialisable value. |
execute_command(self, command_name, data) |
No | Called by AgentTools.execute_command() for management operations such as reconfiguring the tool. |
Where does my configuration come from?
Configuration lives in tool_data["tool_parameters"]. The value comes from the tool_parameters in spec.json, merged with any overrides passed to AgentTools.add(tool_id, parameters).
code/function.py: a word-statistics tool.
class AgentSpaceV1Tool:
def __init__(self, tool_id, tool_data):
# tool_data is the full registry record; your config lives in tool_parameters
self.tool_id = tool_id
params = tool_data.get("tool_parameters", {})
self.lowercase = params.get("lowercase", True)
self.top_n = params.get("top_n", 3)
def execute(self, input_data):
text = input_data["text"]
if self.lowercase:
text = text.lower()
words = text.split()
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[: self.top_n]
return {
"word_count": len(words),
"unique_words": len(counts),
"top_words": [w for w, _ in top],
}
def execute_command(self, command_name, data):
if command_name == "set_top_n":
self.top_n = int(data["top_n"])
return {"top_n": self.top_n}
raise NotImplementedError(f"Unknown command: {command_name}")
code/requirements.txt: list any pip packages. This tool needs none, so the file can be empty or omitted.
Warning
Dependencies are installed with pip install -r requirements.txt into the same Python environment as the agent. Pin versions so they don't conflict with the agent's own packages.
Test it locally before publishing¶
# test_tool.py (run from the text-stats/ directory)
import importlib.util
spec = importlib.util.spec_from_file_location("function", "code/function.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
tool = mod.AgentSpaceV1Tool("text-stats", {"tool_parameters": {"top_n": 2}})
print(tool.execute({"text": "the cat and the hat and the bat"}))
# {'word_count': 8, 'unique_words': 5, 'top_words': ['the', 'and']}
3. Write spec.json¶
spec.json becomes the tool's registry entry. Only fields that exist in the ToolEntry schema are stored; any other keys are dropped. Leave tool_source_code_link and tool_man_page_doc out, because the registry fills them in on upload.
{
"tool_id": "text-stats",
"tool_runtime_type": "python",
"tool_type": "text-processing",
"tool_search_description": "Counts words in a text and returns the most frequent ones",
"tool_tags": ["text", "nlp", "statistics"],
"tool_metadata": {
"author": "Docs Team",
"version": "1.0.0",
"license": "MIT"
},
"tool_parameters": {
"lowercase": true,
"top_n": 3
},
"tool_api_spec": {
"entrypoints": {
"execute": {
"input": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "Text to analyse" }
},
"required": ["text"]
},
"output": {
"type": "object",
"properties": {
"word_count": { "type": "integer" },
"unique_words": { "type": "integer" },
"top_words": { "type": "array", "items": { "type": "string" } }
}
}
}
}
}
}
Why entrypoints.execute.input?
LLM-based tool search (AgentTools.search_tool / search_and_execute_tool) sends tool_api_spec.entrypoints.execute.input to OpenAI as the function's parameter schema, so it must be a valid JSON Schema. tool_search_description and tool_tags become the function description. Tools without this block can still be executed directly; the LLM just won't know their parameters.
4. Build the bundle¶
The upload is a zip containing exactly three files at its root:
| File | Content |
|---|---|
spec.json |
Metadata from step 3 |
tool.zip |
Your code; must unpack to a code/ folder |
tool.md |
Markdown documentation, stored as tool_man_page_doc |
cd text-stats
printf "# text-stats\n\nCounts words and returns the most frequent ones.\n" > tool.md
zip -r tool.zip code # -> tool.zip/code/function.py
zip text-stats-bundle.zip spec.json tool.zip tool.md
5. Upload¶
curl -X POST http://<tools-registry>:6000/tools/upload \
-F "file=@text-stats-bundle.zip"
{ "message": "Tool uploaded successfully", "tool_id": "text-stats" }
The registry uploads tool.zip to the assets server, writes the resulting URL into tool_source_code_link, and stores the entry. Uploading a tool_id that already exists fails with 500 {"error": "Failed to process tool package"}. To replace a tool, delete it first or use a new tool_id.
Verify:
curl http://<tools-registry>:6000/tools/text-stats
6. Use it from an agent¶
from agents_tools import AgentTools
tools = AgentTools(tools_db_url="http://<tools-registry>:6000")
tools.add("text-stats", {"top_n": 2}) # download, pip install, instantiate
print(tools.execute_tool_by_id("text-stats", {"text": "the cat and the hat and the bat"}))
# {'word_count': 8, 'unique_words': 5, 'top_words': ['the', 'and']}
See the Tools SDK for the full API.
Binary tools¶
Binary tools let you ship a compiled executable (Go, Rust, C/C++, and so on) or any executable script.
Contract¶
The runtime calls your executable as:
code/<your-binary> '<request-json>' <output-file-path>
-
Argument 1 is a JSON string:
{ "tool_id": "binary-increment", "tool_data": { "...": "full registry record, including tool_parameters" }, "mode": "input", "input": { "value": 41 } } -
Argument 2 is a path. Write your result there as JSON.
- Exit with a non-zero code on failure; the runtime raises an error.
code/must contain only the executable, because the runtime runs the first file it finds there.execute_commandis not supported. It always returns{"success": false, "message": "management commands not available for binaries"}.
Example (Go)¶
main.go
package main
import (
"encoding/json"
"fmt"
"os"
)
type Request struct {
ToolID string `json:"tool_id"`
ToolData map[string]interface{} `json:"tool_data"`
Mode string `json:"mode"`
Input map[string]interface{} `json:"input"`
}
func fail(msg string) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
}
func main() {
if len(os.Args) < 3 {
fail("usage: increment <request_json> <output_file>")
}
var req Request
if err := json.Unmarshal([]byte(os.Args[1]), &req); err != nil {
fail("invalid request JSON: " + err.Error())
}
value, ok := req.Input["value"].(float64)
if !ok {
fail("missing or invalid 'value'")
}
// Optional config from the registry record's tool_parameters
step := 1.0
if params, ok := req.ToolData["tool_parameters"].(map[string]interface{}); ok {
if s, ok := params["step"].(float64); ok {
step = s
}
}
out, err := os.Create(os.Args[2])
if err != nil {
fail("cannot create output file: " + err.Error())
}
defer out.Close()
json.NewEncoder(out).Encode(map[string]interface{}{
"result": value + step,
"status": "success",
})
}
Build, test, and package
GOOS=linux GOARCH=amd64 go build -o increment main.go # match the agent's OS/arch
# quick local test
./increment '{"tool_id":"t","tool_data":{},"mode":"input","input":{"value":41}}' out.json && cat out.json
# {"result":42,"status":"success"}
mkdir -p binary-increment/code && cp increment binary-increment/code/
cd binary-increment
tar czf tool.zip code # a tar.gz archive, named tool.zip (see warning)
Use tar.gz, not zip, for binaries
Python's zip extraction drops the executable bit, so a binary extracted from a real .zip fails with Permission denied. tar archives keep permissions. The runtime detects the archive type from its content, so a tar.gz file named tool.zip works and still satisfies the registry's bundle layout.
spec.json
{
"tool_id": "binary-increment",
"tool_runtime_type": "binary",
"tool_type": "utility",
"tool_search_description": "Increments a number by a configurable step",
"tool_tags": ["math", "binary", "go"],
"tool_metadata": { "language": "go", "version": "1.0.0" },
"tool_parameters": { "step": 1 },
"tool_api_spec": {
"entrypoints": {
"execute": {
"input": {
"type": "object",
"properties": { "value": { "type": "number" } },
"required": ["value"]
}
}
}
}
}
Bundle and upload exactly as for Python tools (step 4 onwards).
Current limitation
In the current agents_tools release, AgentTools.execute_tool_by_id() cannot run binary tools: the execution manager calls evaluate(), which BinaryToolExecutor does not implement. Until that is fixed, run binary tools with the executor directly:
from agents_tools.runtimes.binary_executor import BinaryToolExecutor
record = {"tool_parameters": {"step": 2}} # or the record from GET /tools/<id>
ex = BinaryToolExecutor("./binary-increment/tool.zip", "binary-increment", record)
print(ex.execute({"value": 41})) # {'result': 43, 'status': 'success'}
BinaryToolExecutor also only works with local archive paths right now; remote URLs fail because the temp directory is not created before download.
Python vs binary at a glance¶
| Python | Binary | |
|---|---|---|
| Entry point | code/function.py → AgentSpaceV1Tool |
First file in code/ |
| Input | execute(input_data) dict |
JSON string in argv[1] |
| Output | Return value | JSON written to the argv[2] file |
| Config | tool_data["tool_parameters"] |
tool_data.tool_parameters inside argv[1] |
| Dependencies | requirements.txt (pip) |
Statically link or bundle everything |
| Custom commands | execute_command() |
Not supported |
| Archive | zip or tar.gz | tar.gz (keeps the executable bit) |
| Portability | Any OS with Python | Must match the agent's OS and architecture |