Skill.md
Agent Tool Builder
Helps define agent tools using the fail-closed design pattern: a unified class that co-locates identity, schema, security properties, and execution logic, with fail-closed defaults so new tools are safe by default.
Why this pattern matters
Three things that ad-hoc tool definitions lack:
- Fail-closed defaults —
is_read_only,is_destructive,is_concurrency_safeall default to False. A tool that forgets to declare its properties is conservatively treated as write-capable. - Layered execution —
validate_semantics → check_permissions → _callare separate methods, so validation logic doesn't bleed into permission logic or business logic. - Self-contained definition — schema, description, security metadata, and execution all live in one place. No separate middleware to wire up.
Workflow
Step 1 — Identify the target framework
Ask which agent framework the tool will be registered in (e.g. hermes-agent, LangChain, plain Python). This determines the import path and registration method, but the design principles are identical.
Check if agent_tool_base.py exists in the project's utils/tools directory.
If not, copy it from references/agent_tool_base.py in this skill directory.
Tell the user where it was placed.
Step 2 — Interview the user
Collect answers to these questions. Defaults are shown — skip questions where the default is clearly fine.
Naming convention: use {service}_{action}_{resource} format with a service prefix so the tool stays unambiguous when multiple tool sets are loaded simultaneously (e.g. stock_get_price, stock_list_symbols, stock_search_news). Start with a verb: get, list, search, create, delete.
| Field | Question | Default |
|---|---|---|
name | Tool name (format: {service}_{action}_{resource}, e.g. stock_get_price) | — required |
description | One-sentence description for the LLM: precisely match actual functionality — vague descriptions cause the agent to misuse the tool | — required |
| Schema fields | What parameters does the tool accept? (field name, type, description; add example in Field description) | — required |
is_read_only | Does this tool only read data, with no writes or side effects? | False |
is_destructive | Does this tool perform irreversible operations (delete, overwrite)? | False |
is_concurrency_safe | Can this tool run simultaneously with other tools? | False |
response_format | Is the return data for agent programmatic processing (JSON) or user display (Markdown)? | Markdown by default |
| List tool? | If returning multiple records, support pagination? | Recommended for 50+ records |
_validate_input_semantics | Any semantic issues to catch before execution? (e.g. too-short query, wrong format) | Not needed |
_check_permissions | Any permissions to check? (e.g. required env var, caller identity restriction) | Not needed |
_call | What is the core execution logic of the tool? | — required |
You don't have to ask all questions upfront — infer reasonable answers from context.
For example, a "search" or "get" tool is almost certainly is_read_only=True, is_concurrency_safe=True.
Step 3 — Generate the tool file
Create a .py file for the tool. Follow this field order:
1. imports
2. Input schema (Pydantic BaseModel)
3. Tool class:
a. name, description, args_schema — identity
b. is_read_only, is_destructive, is_concurrency_safe, max_result_chars — security metadata
c. _validate_input_semantics() — semantic validation (omit if unneeded)
d. _check_permissions() — permission check (omit if unneeded)
e. _call() — actual logic
Suggest a file path consistent with the project's tool directory structure.
Step 4 — Show security property summary
After generating, print a one-line summary of the tool's security posture:
StockGetPriceTool: read_only=True destructive=False concurrency_safe=True max_result=10K
Output template
"""<tool_name>.py — <one-line description>"""
from typing import Optional
from pydantic import BaseModel, Field
from base.utils.agent_tool_base import AgentTool
# ---------------------------------------------------------------------------
# Input schema
# ---------------------------------------------------------------------------
class <ToolName>Input(BaseModel):
<field_name>: <type> = Field(description="<description>. e.g. '<example>'")
# ... more fields
# ---------------------------------------------------------------------------
# Tool class
# ---------------------------------------------------------------------------
class <ToolName>Tool(AgentTool):
# — identity —
name: str = "<tool_name>"
description: str = "<one-sentence description for the LLM>"
args_schema = <ToolName>Input
# — security metadata (fail-closed: only set True when verified) —
is_read_only: bool = <True/False>
is_destructive: bool = <True/False>
is_concurrency_safe: bool = <True/False>
max_result_chars: int = 10_000
# — semantic validation (omit if no input constraints needed) —
def _validate_input_semantics(self, <params>, **kwargs) -> tuple[bool, Optional[str]]:
if not <condition>:
return False, "<why invalid>. Try <concrete fix>"
return True, None
# — permission check (omit if no access control needed) —
def _check_permissions(self, <params>, **kwargs) -> tuple[bool, Optional[str]]:
if not <allowed>:
return False, "<why denied>. <suggested next step>"
return True, None
# — core logic —
def _call(self, <params>, **kwargs) -> str:
# ... implement tool logic here
return result
Common security property patterns
| Tool type | is_read_only | is_destructive | is_concurrency_safe |
|---|---|---|---|
| Search / query | True | False | True |
| File read | True | False | True |
| File write / modify | False | False | False |
| Delete operation | False | True | False |
| API call (GET) | True | False | True |
| API call (POST/DELETE) | False | depends | False |
| Database query | True | False | True |
| Database write | False | False | False |
Output design principles
Atomic tools — one tool, one responsibility
Keep each tool focused on a single operation. Let the agent compose multiple tools to complete complex tasks. A tool that does too much is harder for the agent to reuse and reason about.
Response format — JSON vs Markdown
| Format | When to use |
|---|---|
| JSON | Agent needs to parse/filter the result programmatically |
| Markdown | Result will be shown directly to a user |
Support both when uncertain — accept an optional response_format: str = "markdown" parameter and branch in _call. For JSON output use json.dumps(data, ensure_ascii=False, indent=2).
Pagination for list tools
Any tool that can return more than ~50 records should support pagination:
return json.dumps({
"items": [...],
"total": 150,
"count": 20,
"offset": 0,
"has_more": True,
"next_offset": 20,
}, ensure_ascii=False, indent=2)
Add offset: int = Field(default=0, description="Pagination offset") and limit: int = Field(default=20, description="Max items to return") to the input schema.
Actionable error messages
Error strings must guide the agent toward a fix — not just describe the failure:
# Bad: agent is stuck
return False, "Query too short."
# Good: agent knows exactly what to try next
return False, "Query too short (got 2 chars, need >= 3). Provide a more specific search term."
Reference files
references/agent_tool_base.py— Full AgentTool base class (pure Python, no framework dependency)