tool-creator
GitHub用于创建 AnimaWorks 外部 Python 工具模块的元技能,涵盖核心、共享及个人工具的编写、权限管理及调用流程。
Trigger Scenarios
Install
npx skills add xuiltul/animaworks --skill tool-creator -g -y
SKILL.md
Frontmatter
{
"name": "tool-creator",
"description": "A meta-skill for creating Python external tool modules for AnimaWorks.core\/integrationsHandles integrations, get_credential, and permissions. Use when: core\/integrationsWhen you need to add a new module, implement a Web API wrapper, or develop a custom tool called by animaworks-tool."
}
tool-creator
Overview
AnimaWorks tools are divided into three types:
| Type | Location | Discovery method |
|---|---|---|
| Core tools | core/integrations/*.py (files with the _ prefix are excluded) |
discover_core_tools() → TOOL_MODULES (package import) |
| Shared tools | {data_dir}/common_tools/*.py |
discover_common_tools() |
| Personal tools | {anima_dir}/tools/*.py |
discover_personal_tools() |
{data_dir} is usually ~/.animaworks/.
- Dispatch:
ExternalToolDispatcherhas deprecated_DISPATCH_TABLEand standardized ondispatch(name, args)in each module (or a function with the same name as the schema) (core/tooling/dispatch.py). - Merging: At startup, both
_discover_personal_tools()andrefresh_toolsforAgentCoremerge in the order shared → personal, with personal tools overriding entries with the same name ({**common, **personal}). The merged result is held in_personal_toolsofExternalToolDispatcher(the name is historical, but it also includes shared tools). - Core conflicts: Files with the same name as core
TOOL_MODULESare skipped when discovering shared and personal tools (warning log only). - Writing tool files: When writing to
tools/*.pyviawrite_memory_file, the tool_creation.personal requirement ofpermissions(permissions.jsontakes precedence) must be met (core/tooling/handler_memory.py).
Execution Path (How the LLM Calls It)
| Mode | Typical Path |
|---|---|
| A (LiteLLM, etc.) | Integrated tool use_tool(tool_name, action, args) → module's dispatch (core/tooling/handler.py). Details are designed so that each tool's skill is read via read_memory_file (USE_TOOL of core/tooling/schemas/skill.py). |
| S (Agent SDK) | Via Claude Code's built-in Bash with animaworks-tool <ツール> …, or via MCP (only a curated subset is exposed to MCP; see "Adding Core Tools to the Repository" below). |
| Anthropic fallback, etc. | include_use_tool=False configuration may exist in build_tool_list → externally, this assumes Bash + animaworks-tool or skills. |
At startup, the merged map above is passed to ToolHandler, so common and personal tools placed before process startup are available from the beginning via use_tool / ExternalToolDispatcher. For .py newly added during a session, use_tool will not recognize the tool name unless a rescan is performed via refresh_tools (because the map is not updated).
Procedure
Step 1: Tool Design
- Decide the tool name (module name) (snake_case, e.g.,
my_api_tool). This becomes the first argument ofanimaworks-tool my_api_tool …. - Decide the actions (subcommands). Schema names should generally follow
{tool_name}_{action}(e.g.,myapi_query). Inuse_tool, usetool_name="myapi",action="query". - Define parameters using JSON Schema (
input_schemaorparameters).
Step 2: Creating the Module File
Single-Action Example
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
def get_tool_schemas() -> list[dict]:
"""ツールスキーマを返す。個人・共有ツールでは必須推奨(スキーマ読み込み・ログ用)。"""
return [
{
"name": "my_tool_action",
"description": "このツールが何をするかの説明",
"input_schema": {
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "パラメータの説明",
},
"param2": {
"type": "integer",
"description": "オプションパラメータ",
"default": 10,
},
},
"required": ["param1"],
},
}
]
def dispatch(name: str, args: dict[str, Any]) -> Any:
"""スキーマ名に応じた処理を実行する(推奨)。"""
args.pop("anima_dir", None) # フレームワークから注入。必要なら Path(anima_dir) で利用
if name == "my_tool_action":
return _do_action(
param1=args["param1"],
param2=args.get("param2", 10),
)
raise ValueError(f"Unknown tool: {name}")
def _do_action(param1: str, param2: int = 10) -> dict[str, Any]:
return {"result": f"Processed {param1} with {param2}"}
When calling from animaworks-tool, cli_main must be implemented afterward (see the "cli_main" section below).
Multiple Actions + Authentication (API Integration)
The resolution order for get_credential(credential_name, tool_name, key_name="api_key", env_var=...) is config.json's credentials.{credential_name} (api_key or keys[key_name]) → the shared section of vault.json (the key name is the string passed as argument env_var) → shared/credentials.json (legacy, key is env_var) → environment variable env_var (core/credentials.py).
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
def get_tool_schemas() -> list[dict]:
return [
{
"name": "myapi_query",
"description": "APIにクエリを送信して結果を取得する",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "検索クエリ"},
"limit": {"type": "integer", "description": "最大件数", "default": 10},
},
"required": ["query"],
},
},
{
"name": "myapi_post",
"description": "APIにデータを送信する",
"input_schema": {
"type": "object",
"properties": {
"data": {"type": "string", "description": "送信データ"},
},
"required": ["data"],
},
},
]
class MyAPIClient:
def __init__(self) -> None:
from core.credentials import get_credential
self._api_key = get_credential(
"myapi",
"myapi_tool",
env_var="MYAPI_KEY",
)
def query(self, query: str, limit: int = 10) -> list[dict]:
import httpx
resp = httpx.get(
"https://api.example.com/search",
params={"q": query, "limit": limit},
headers={"Authorization": f"Bearer {self._api_key}"},
timeout=30.0,
)
resp.raise_for_status()
return resp.json()["results"]
def post(self, data: str) -> dict:
import httpx
resp = httpx.post(
"https://api.example.com/data",
json={"data": data},
headers={"Authorization": f"Bearer {self._api_key}"},
timeout=30.0,
)
resp.raise_for_status()
return resp.json()
def dispatch(name: str, args: dict[str, Any]) -> Any:
args.pop("anima_dir", None)
client = MyAPIClient()
if name == "myapi_query":
return client.query(query=args["query"], limit=args.get("limit", 10))
if name == "myapi_post":
return client.post(data=args["data"])
raise ValueError(f"Unknown tool: {name}")
Per-Anima authentication (Chatwork, etc.): There is a pattern where the Anima name is taken from args.get("anima_dir") and an Anima-specific key like CHATWORK_API_TOKEN__{anima_name} is resolved via resolve_env_style_credential(...) (e.g., resolve_identity of core/integrations/_chatwork_identity.py. If not registered, raise an error without falling back). The same key naming can be used in custom tools.
cli_main (for animaworks-tool)
animaworks-tool <tool_name> … cannot run the CLI for core, common, or personal modules unless the module has cli_main (see cli_dispatch in cli/tool_dispatch.py). The typical pattern is to parse subcommands with argparse and call dispatch(f"{tool}_{action}", args_dict) internally. If you want to generate usage from a schema, also refer to auto_cli_guide in core/integrations/_base.py.
Step 3: Saving the File
Personal tools:
write_memory_file(path="tools/my_tool.py", content=<コード>)
tool_creation.personal must be permitted.
Step 4: Tool Activation (Hot Reload)
Only when adding or changing tools/*.py or common_tools/*.py after process startup:
refresh_tools()
The file-based map of ExternalToolDispatcher within the same session is rescanned, and new module names are resolved from use_tool (usually unnecessary for files that existed before startup).
Step 5: Sharing (Optional)
share_tool(tool_name="my_tool")
Copied to ~/.animaworks/common_tools/. tool_creation.shared is required. Other Animas each need their own refresh_tools (or automatic discovery at restart).
Required Interfaces
| Function / Constant | Required | Description |
|---|---|---|
get_tool_schemas() |
Strongly recommended for personal and shared | For schema loading and guide generation. Even in core, some modules have empty lists (e.g., web_search is []). Important: ExternalToolDispatcher.dispatch (the path that passes the schema name directly via tool_use) matches modules only for schemas included in the name list of get_tool_schemas() in core. Empty modules are not hit on the core side via that path. On the other hand, use_tool imports the module directly from TOOL_MODULES and calls dispatch, so even with an empty schema list, execution is possible if dispatch exists. Custom tools should be aware of both paths; defining the schema is usually safer. |
dispatch(name, args) |
Recommended | ExternalToolDispatcher._call_module takes priority. |
| Function with the same name as the schema name | Alternative | Used when dispatch is absent: getattr(mod, name)(**args). |
cli_main(argv) |
Required for CLI use | animaworks-tool entry. |
EXECUTION_PROFILE |
Optional | expected_seconds, background_eligible; in core tools, gated: True can require an allowlist for send-type operations (core/tooling/permissions.py). |
Invocation and schema names
use_tool: Passed to the module’sdispatch(or a function with the same name) byschema_name = f"{tool_name}_{action}". Authorization checks: core:tool_registry(tool_namemust be included in the result ofget_permitted_tools); file-based (shared and personal): the merged_personal_toolsmust containtool_name(_handle_use_toolofcore/tooling/handler.py).animaworks-tool: If the first token issubmit, it is submitted as a background job (see below). Core imports fromTOOL_MODULESandcli_main; shared and personal load from a file andcli_main. An unknown first argument may fall back to the main CLI (animaworks) (cli/tool_dispatch.py).- Gated subcommands (core only): If the relevant action in
EXECUTION_PROFILEhas"gated": True, it is blocked in both the CLI and dispatch unless{tool_name}_{action}(e.g.,gmail_send) is included in the allowed set ofpermissions. File-based personal and shared tools are not inTOOL_MODULES, so they are not subject to this gate mechanism.
Schema Normalization
_normalise_schema of core/tooling/schemas/loader.py receives input_schema / parameters and normalizes them to parameters in the internal representation.
permissions (tool_creation, external tools)
- Loading:
load_permissions(anima_dir)(core/config/schemas.py).permissions.jsontakes priority. Only when absent, parsepermissions.mdto generate JSON and migrate (migrate_permissions_md_to_json). - Tool creation (JSON example):
{
"version": 1,
"tool_creation": {
"personal": true,
"shared": false
}
}
The Markdown "Tool Creation" section (個人ツール / 共有ツール lines) also becomes the same structure during migration.
- External tools (core):
external_toolscollects, viaget_permitted_tools, the module names of coreTOOL_MODULESand the{tool}_{action}strings for gate release (e.g.,gmail_send). Inuse_tool, core tools use names in this set on thetool_registryside; personal and shared tools are executed even outside the core set if the name exists in_personal_toolsafter startup merge orrefresh_tools(files with the same name as core are skipped during discovery, so no conflict).
EXECUTION_PROFILE
background_eligible: True:animaworks-tool submit <tool> <subcommand> …registers the input fromtask_type="command"into the TaskStore, andPendingTaskExecutorretrieves and executes the attempt (see_handle_submitincli/tool_dispatch.py). Execution results are also saved tostate/background_tasks/{task_id}.json. Profile references are only applied to importable core modules (file tools are less likely to be flagged as warnings at submit time).gated: True: For the corresponding core tool actions, explicit permission fortool_actionmust be granted via permissions.
EXECUTION_PROFILE: dict[str, dict[str, object]] = {
"pipeline": {"expected_seconds": 1800, "background_eligible": True},
"send": {"expected_seconds": 15, "background_eligible": False, "gated": True},
}
Adding Core Tools to the Repository
- Add
core/integrations/{name}.py(items starting with_are excluded from scanning). TOOL_MODULESis automatically registered viadiscover_core_tools(). No manual list forcore/integrations/__init__.pyis needed.- The curated allowlist for MCP is
MCP_TOOL_NAMESincore/tooling/surface.py.resolve_tool_surfaceperforms filtering based on trigger and role in MCP usage modes (S / C / D / G / X). Examples:search_memory,read_memory_file,write_memory_file,archive_memory_file,send_message,post_channel,call_human,delegate_task,submit_tasks,update_task,create_skill. External service core tools such as Slack / Gmail /web_searchdo not appear in MCP — they typically go throughuse_tool/ Bash (animaworks-tool) / skills. - Add tests to
tests/. If schemas or reference documents are auto-generated, also check the targets inscripts/generate_reference.py. - For destructive operations, consider updating
gated: Trueand the permission descriptions.
Validation Checklist
- File name: snake_case,
.py, no leading_(to be included in scanning) - Add
from __future__ import annotationsat the top (project convention) -
get_tool_schemas()returns the correct schema name (personal, shared) - Process all schemas via
dispatchor the schema-name function - If
anima_diris not used, avoid side effects withargs.pop("anima_dir", None) - Implement
cli_mainand verify operation withanimaworks-tool - Add
timeout=for external HTTP - Authentication uses
get_credential(or per-anima resolution of the same type as core) - Use
logging.getLogger(__name__)for logging (recommended)
Security
- Do not embed secrets in code. Use
get_credential/ vault / config. - Do not touch other Animas' directories.
- In core, design "write/send" operations with
gatedand permissions as a set.
Reference Implementations
- Thin entry +
_client/_clisplit:core/integrations/chatwork.py,slack.py,discord.py - Authentication and API:
core/integrations/gmail.py,github.py,notion.py,google_calendar.py,google_tasks.py - Long-running and pipeline:
core/integrations/image_gen.py(facade,image/subpackage +EXECUTION_PROFILE) - Search and local LLM:
core/integrations/web_search.py(ifget_tool_schemasis empty, it will not match via the core path inExternalToolDispatcher.dispatch;use_toolis possible withdispatch),x_search.py,local_llm.py - Dispatcher and CLI entry:
core/tooling/dispatch.py,cli/tool_dispatch.py(cli_dispatch/_handle_submit)
Notes
- Tools are executable Python. They are distinct from skills (Markdown).
- Only tools added after startup require
refresh_tools(files that existed before startup are already scanned at startup). - Personal and shared files with the same name as core files are not adopted.
Version History
- 0f2a80e Current 2026-10-19 11:11
-
edd4a93
2026-09-28 03:35
重构:将 core/tools 重命名为 core/integrations,并更新所有引用及迁移逻辑。
- 050238c 2026-08-20 03:19
- 140e43b 2026-07-25 09:12


