Tool Group Routing (Dynamic Tool Loading)
Tool Group Routing (Dynamic Tool Loading)
Feature Information
- Feature ID: FEAT-040
- Created: 2026-03-01
- Last Updated: 2026-03-01
- Status: Draft
- Priority: P1 (Should Have)
- Owner: TBD
- Related RFC: RFC-040 (pending)
User Story
As a user of OneClaw, I want the AI agent to only load the tools it actually needs for each task, so that conversations use fewer tokens, respond faster, and are not cluttered with irrelevant tool schemas.
Typical Scenarios
- User says “Check my email” – the agent calls
load_tool_group("google_gmail"), which returns the list of Gmail tools. The agent then usesgmail_searchto find recent emails. - User says “Create a spreadsheet with quarterly data” – the agent calls
load_tool_group("google_sheets")to load sheet tools, then usessheets_createandsheets_update_values. - User says “What time is it?” – the agent calls
get_current_timedirectly (core tool, always available). No tool group loading needed. - User says “Schedule a task for tomorrow” – the agent calls
load_tool_group("scheduled_tasks")to load task management tools, then usesschedule_task. - User says “Load the PDF tools” – the agent calls
load_tool_group("pdf"), which returns the list of PDF tools:pdf_info,pdf_extract_text,pdf_render_page. - User says “Add a new OpenAI provider” – the agent calls
load_tool_group("config")to load configuration tools, then usescreate_provider. - User says “Help me write a JS tool” – the agent calls
load_tool_group("js_tool_management")to load JS tool CRUD tools, then usescreate_js_tool.
Feature Description
Overview
OneClaw currently sends ALL registered tool schemas to the LLM on every message turn. With 37+ Kotlin tools, 60+ JS Google Workspace tools, and potentially many user-created JS tools, this wastes significant tokens (each tool schema consumes ~200-500 tokens). The total tool schema payload can exceed 20,000 tokens per turn.
The Skill system (FEAT-014) already demonstrates a lazy-loading pattern: only skill names and descriptions appear in the system prompt, and the full skill content is loaded on demand via load_skill. This feature applies the same pattern to tools: group them by domain, list group summaries in the system prompt, and load full tool schemas on demand via a load_tool_group meta-tool.
How It Works
Before (current):
System prompt: ...
Tools: [get_current_time, read_file, write_file, http_request,
gmail_search, gmail_read, gmail_send, gmail_draft, ...(60+ more),
list_providers, create_provider, ...(17 more),
pdf_info, pdf_extract_text, pdf_render_page,
schedule_task, list_scheduled_tasks, ...,
create_js_tool, list_user_tools, ...]
Token cost: ~20,000+ tokens for tool schemas alone
After (with tool group routing):
System prompt:
...
## Available Tool Groups
Use `load_tool_group` to load tools from a group before using them.
- config: Manage providers, models, agents, app settings, environment variables, and tool states
- pdf: Extract text, get info, and render pages from PDF files
- scheduled_tasks: Create, list, run, update, and delete scheduled tasks
- js_tool_management: Create, list, update, and delete user JavaScript tools
- google_gmail: Email: search, read, send, draft, label, manage messages
- google_drive: File storage: list, search, upload, download, manage Drive files
...
Tools: [get_current_time, read_file, write_file, http_request,
load_skill, load_tool_group, save_memory, search_history,
exec, js_eval, webfetch, browser, create_agent]
Token cost: ~3,000 tokens for core tool schemas + ~200 tokens for group listing
Tool Classification
Core Tools (always available, never grouped)
These tools are sent to the LLM on every turn:
| Tool | Purpose |
|---|---|
load_skill |
Meta-tool: load skill content |
load_tool_group |
Meta-tool: load tool group |
save_memory |
Save information to memory |
search_history |
Search conversation history |
exec |
Execute shell commands |
js_eval |
Evaluate JavaScript code |
webfetch |
Fetch web content |
browser |
Browser automation |
create_agent |
Create sub-agents |
read_file |
Read files (JS built-in) |
write_file |
Write files (JS built-in) |
get_current_time |
Get current time (JS built-in) |
http_request |
HTTP requests (JS built-in) |
Grouped Kotlin Tools
| Group | Tools | Count |
|---|---|---|
config |
list_providers, create_provider, update_provider, delete_provider, list_models, fetch_models, set_default_model, add_model, delete_model, list_agents, update_agent, delete_agent, get_config, set_config, manage_env_var, list_tool_states, set_tool_enabled | 17 |
pdf |
pdf_info, pdf_extract_text, pdf_render_page | 3 |
scheduled_tasks |
schedule_task, list_scheduled_tasks, run_scheduled_task, update_scheduled_task, delete_scheduled_task | 5 |
js_tool_management |
create_js_tool, list_user_tools, update_js_tool, delete_js_tool | 4 |
Grouped JS Tools (from assets)
Each Google Workspace JSON manifest becomes its own group:
| Group | Source File | Approx. Tools |
|---|---|---|
google_gmail |
google_gmail.json | 8 |
google_gmail_settings |
google_gmail_settings.json | 4 |
google_drive |
google_drive.json | 8 |
google_calendar |
google_calendar.json | 7 |
google_contacts |
google_contacts.json | 5 |
google_docs |
google_docs.json | 6 |
google_sheets |
google_sheets.json | 8 |
google_slides |
google_slides.json | 6 |
google_forms |
google_forms.json | 5 |
google_tasks |
google_tasks.json | 5 |
User Interaction Flow
1. User: "Check my latest emails"
2. System prompt includes:
## Available Tool Groups
- google_gmail: Email management: search, read, send, draft, label, archive, and manage Gmail messages
...
3. Agent calls load_tool_group(group_name="google_gmail")
4. Tool returns: "Loaded 8 tools from group 'google_gmail':
- gmail_search: Search Gmail messages
- gmail_read: Read a Gmail message
- gmail_send: Send a Gmail message
..."
5. Gmail tools are now included in the active tool list for subsequent turns
6. Agent calls gmail_search(query="is:unread newer_than:1d")
7. Agent returns results to user
Acceptance Criteria
Core Behavior
TEST-040-01: Core Tools Always Available
- Given a new conversation starts
- When the agent receives the first message
- Then only core tools (load_skill, load_tool_group, save_memory, search_history, exec, js_eval, webfetch, browser, create_agent, and single-file JS built-ins) are sent to the LLM
TEST-040-02: Tool Group Listing in System Prompt
- Given tool groups are registered
- When the system prompt is assembled
- Then an “Available Tool Groups” section is appended listing each group’s name and description
TEST-040-03: Load Tool Group Success
- Given a valid tool group name
- When the agent calls
load_tool_group(group_name="google_gmail") - Then all tools in the “google_gmail” group become available for subsequent turns and the tool returns a list of loaded tool names and descriptions
TEST-040-04: Load Tool Group Invalid Name
- Given a non-existent group name
- When the agent calls
load_tool_group(group_name="nonexistent") - Then the tool returns an error listing all available group names
TEST-040-05: Loaded Tools Persist Across Turns
- Given a tool group has been loaded
- When the agent sends subsequent messages in the same conversation
- Then the loaded tools remain in the active tool list
TEST-040-06: Multiple Groups Can Be Loaded
- Given two different groups (“google_gmail” and “google_drive”)
- When the agent loads both groups
- Then tools from both groups are available simultaneously
TEST-040-07: Duplicate Load Is Idempotent
- Given a group has already been loaded
- When the agent calls
load_tool_groupwith the same group name again - Then the tool returns success without duplicating tool definitions
Token Efficiency
TEST-040-08: Reduced Initial Token Count
- Given 100+ tools are registered (core + grouped)
- When a new conversation starts
- Then only ~13 core tool schemas are sent to the LLM (not all 100+)
TEST-040-09: Group Tool Schemas Sent After Load
- Given the agent loads the “config” group (17 tools)
- When the next message is sent to the LLM
- Then the tool list includes all 13 core tools + 17 config tools = 30 tools
JS Tool Group Integration
TEST-040-10: JS Group Manifests with _meta
- Given a JS group manifest JSON file with a
_metaentry - When the manifest is loaded
- Then the
_metaentry is used for group display_name and description, and is not registered as a tool
TEST-040-11: JS Group Manifests without _meta
- Given a JS group manifest JSON file without a
_metaentry - When the manifest is loaded
- Then display_name is auto-generated from filename and description is auto-generated from tool names
Tool Execution
TEST-040-12: Grouped Tool Execution Blocked Before Load
- Given the “pdf” group has NOT been loaded
- When the agent attempts to call
pdf_info - Then the tool call fails because
pdf_infois not in the available tool names list
TEST-040-13: Grouped Tool Execution After Load
- Given the “pdf” group HAS been loaded
- When the agent calls
pdf_info - Then the tool executes normally and returns results
UI/UX Requirements
This feature has no new UI. The tool group routing is entirely transparent to the user – it operates within the existing chat interface and tool call display. The only user-visible change is that the agent may call load_tool_group before using domain-specific tools, which appears as a normal tool call in the chat.
Feature Boundary
Included
ToolGroupDefinitiondata class for group metadataload_tool_groupmeta-tool mirroringload_skillpattern- Dynamic tool list management in
SendMessageUseCase - System prompt injection for tool group listing
- Group registration in
ToolModulefor Kotlin tools _metaentry support in JS group manifests- Group metadata exposure from
JsToolLoader
Not Included (V1)
- Automatic tool group detection (agent must explicitly call
load_tool_group) - Per-agent tool group configuration (all agents see all groups)
- Persistent group loading across sessions (groups reset per conversation)
- Tool group enable/disable (covered by existing FEAT-017 tool management)
- Tool group creation/deletion by users (groups are defined by registration)
- Streaming tool group loading (groups load synchronously)
Business Rules
- Core tools are always available regardless of group loading state
- Grouped tools are only available after their group is explicitly loaded via
load_tool_group - Tool group loading is scoped to the current conversation (resets on new conversation)
- Loading a group that is already loaded is a no-op (idempotent)
- The
load_tool_grouptool itself is always a core tool (never grouped) - JS group manifests may optionally include a
_metaentry for human-readable group metadata - If a
_metaentry is absent, display_name and description are auto-generated - Tool groups registered via
ToolSourceInfo(type = TOOL_GROUP, groupName = ...)are automatically excluded from core tool definitions - Kotlin tools registered with
groupNameset in theirToolSourceInfoare treated as grouped regardless ofToolSourceType
Non-Functional Requirements
Performance
load_tool_groupexecution: < 10ms (in-memory registry lookup)- System prompt group listing assembly: < 5ms
- No network calls required for group loading
Token Savings
- Estimated 15,000-20,000 token reduction per turn when groups are not loaded
- Each group listing entry costs ~20-30 tokens (name + one-line description)
- Full group listing (14 groups) costs ~300-400 tokens vs ~20,000+ for all tool schemas
Memory
- Minimal memory overhead:
ToolGroupDefinitioninstances (~100 bytes each) - Tool definitions remain in
ToolRegistryregardless – group routing only controls which are sent to LLM
Compatibility
- Backward compatible: existing tools continue to work
- No database changes required
- No new permissions required
Dependencies
Depends On
- FEAT-004 (Tool System): Tool interface, ToolRegistry, ToolExecutionEngine, ToolSourceInfo
- FEAT-014 (Agent Skill): LoadSkillTool pattern, SkillRegistry pattern
- FEAT-018 (JS Tool Group): JS group manifest format, JsToolLoader
- FEAT-036 (Config Tools): Config tools to be grouped
Depended On By
- None currently
External Dependencies
- None (all operations use existing internal components)
Error Handling
Error Scenarios
- Group not found
- Cause: Agent calls
load_tool_groupwith non-existent group name - Handling: Return
ToolResult.error("not_found", "Tool group 'xyz' not found. Available groups: config, pdf, scheduled_tasks, ...")with full list of available groups
- Cause: Agent calls
- Missing parameter
- Cause: Agent calls
load_tool_groupwithoutgroup_name - Handling: Return
ToolResult.error("missing_parameter", "Required parameter 'group_name' is missing.")
- Cause: Agent calls
- Empty group
- Cause: A registered group has no tools (all tools unregistered or disabled)
- Handling: Return
ToolResult.error("empty_group", "Tool group 'xyz' has no available tools.")
Test Points
Functional Tests
- Verify core tools are always included in tool definitions
- Verify grouped tools are excluded from initial tool definitions
- Verify
load_tool_groupreturns correct tool list - Verify loaded tools become available for execution
- Verify multiple groups can be loaded simultaneously
- Verify duplicate load is idempotent
- Verify error response for invalid group name includes available groups
- Verify system prompt includes tool group listing
- Verify JS
_metaentries are parsed correctly - Verify JS
_metaentries are not registered as tools
Edge Cases
- Load all groups simultaneously
- Load group with a single tool
- Load group then disable a tool within it via
set_tool_enabled - JS manifest with
_metaentry only (no actual tools) - JS manifest with
_metaentry containing special characters in description - Concurrent
load_tool_groupcalls in parallel tool execution - Group name collision between Kotlin and JS groups
- Very long group description truncation
Change History
| Date | Version | Changes | Owner |
|---|---|---|---|
| 2026-03-01 | 0.1 | Initial version | - |