250 lines
6.6 KiB
Markdown
250 lines
6.6 KiB
Markdown
# Meta-Tools: Tools That Call Other Tools
|
|
|
|
Meta-tools are CmdForge tools that can invoke other tools as steps in their workflow. This enables powerful composition and reuse of existing tools.
|
|
|
|
## Step Type: `tool`
|
|
|
|
A new step type `tool` allows calling another CmdForge tool from within a tool's workflow.
|
|
|
|
### Manifest Format
|
|
|
|
```yaml
|
|
name: summarize-and-translate
|
|
description: Summarize text then translate the summary
|
|
version: 1.0.0
|
|
category: Text
|
|
|
|
steps:
|
|
- type: tool
|
|
tool: official/summarize # Tool to call (owner/name or just name for local)
|
|
input: "{input}" # What to pass as input (supports variable substitution)
|
|
args: # Optional arguments to pass to the tool
|
|
max_words: "100"
|
|
output_var: summary # Variable to store the output
|
|
|
|
- type: tool
|
|
tool: official/translate
|
|
input: "{summary}" # Use output from previous step
|
|
args:
|
|
target_language: "{language}" # Can use tool's own arguments
|
|
output_var: translated
|
|
|
|
output: "{translated}"
|
|
|
|
arguments:
|
|
- flag: "--language"
|
|
variable: language
|
|
default: "Spanish"
|
|
description: "Target language for translation"
|
|
```
|
|
|
|
### Resolution Order
|
|
|
|
When resolving a tool reference in a `tool` step:
|
|
|
|
1. **Fully qualified**: `owner/name` - looks up in registry or installed tools
|
|
2. **Local tool**: Just `name` - checks local `~/.cmdforge/<name>/config.yaml` first
|
|
3. **Registry fallback**: If not found locally, checks installed registry tools
|
|
|
|
### Tool Step Properties
|
|
|
|
| Property | Required | Description |
|
|
|----------|----------|-------------|
|
|
| `type` | Yes | Must be `"tool"` |
|
|
| `tool` | Yes | Tool reference (owner/name or name) |
|
|
| `input` | No | Input to pass to the tool (default: current `{input}`) |
|
|
| `args` | No | Dictionary of arguments to pass |
|
|
| `output_var` | Yes | Variable name to store the tool's output |
|
|
| `provider` | No | Override provider for the called tool |
|
|
|
|
### Execution
|
|
|
|
When a `tool` step executes:
|
|
|
|
1. Resolve the tool reference to find the tool definition
|
|
2. Substitute variables in `input` and `args` values
|
|
3. Execute the tool with the resolved input and arguments
|
|
4. Capture the output in the specified `output_var`
|
|
|
|
### Error Handling
|
|
|
|
- If a called tool fails, the parent tool fails with an appropriate error
|
|
- Tool not found errors include helpful installation suggestions
|
|
- Circular dependencies are detected and prevented
|
|
|
|
## Dependency Resolution
|
|
|
|
### Declaring Dependencies
|
|
|
|
Tools can declare their dependencies in the config:
|
|
|
|
```yaml
|
|
name: my-meta-tool
|
|
dependencies:
|
|
- official/summarize
|
|
- official/translate@^1.0.0 # With version constraint
|
|
```
|
|
|
|
### CLI Integration
|
|
|
|
```bash
|
|
# Install a tool and its dependencies
|
|
cmdforge install official/summarize-and-translate
|
|
|
|
# Check if dependencies are satisfied
|
|
cmdforge check my-meta-tool
|
|
|
|
# Install missing dependencies
|
|
cmdforge install --deps my-meta-tool
|
|
```
|
|
|
|
### Dependency Checking
|
|
|
|
Before running a meta-tool, the runner verifies all dependencies are installed:
|
|
|
|
```python
|
|
def check_dependencies(tool: Tool) -> List[str]:
|
|
"""Returns list of missing dependencies."""
|
|
missing = []
|
|
for dep in tool.dependencies:
|
|
if not is_tool_installed(dep):
|
|
missing.append(dep)
|
|
return missing
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
1. **Sandboxing**: Called tools run in the same process context as the parent
|
|
2. **Input validation**: All inputs are treated as untrusted strings
|
|
3. **No shell execution**: Tool references cannot contain shell commands
|
|
4. **Depth limit**: Maximum nesting depth (default: 10) prevents runaway recursion
|
|
|
|
## Example: Multi-Step Analysis Pipeline
|
|
|
|
```yaml
|
|
name: code-review-pipeline
|
|
description: Comprehensive code review using multiple specialized tools
|
|
version: 1.0.0
|
|
category: Developer
|
|
|
|
dependencies:
|
|
- official/analyze-complexity
|
|
- official/find-bugs
|
|
- official/suggest-improvements
|
|
|
|
steps:
|
|
# Step 1: Analyze code complexity
|
|
- type: tool
|
|
tool: official/analyze-complexity
|
|
input: "{input}"
|
|
output_var: complexity_report
|
|
|
|
# Step 2: Find potential bugs
|
|
- type: tool
|
|
tool: official/find-bugs
|
|
input: "{input}"
|
|
output_var: bug_report
|
|
|
|
# Step 3: Generate improvement suggestions based on findings
|
|
- type: tool
|
|
tool: official/suggest-improvements
|
|
input: |
|
|
## Code:
|
|
{input}
|
|
|
|
## Complexity Analysis:
|
|
{complexity_report}
|
|
|
|
## Bug Report:
|
|
{bug_report}
|
|
output_var: suggestions
|
|
|
|
output: |
|
|
# Code Review Results
|
|
|
|
## Complexity Analysis
|
|
{complexity_report}
|
|
|
|
## Potential Issues
|
|
{bug_report}
|
|
|
|
## Improvement Suggestions
|
|
{suggestions}
|
|
```
|
|
|
|
## Implementation Notes
|
|
|
|
### ToolStep Dataclass
|
|
|
|
```python
|
|
@dataclass
|
|
class ToolStep:
|
|
"""A step that calls another tool."""
|
|
tool: str # Tool reference (owner/name or name)
|
|
output_var: str # Variable to store output
|
|
input_template: str = "{input}" # Input template
|
|
args: Dict[str, str] = field(default_factory=dict)
|
|
provider: Optional[str] = None # Provider override
|
|
|
|
def to_dict(self) -> dict:
|
|
d = {
|
|
"type": "tool",
|
|
"tool": self.tool,
|
|
"output_var": self.output_var,
|
|
}
|
|
if self.input_template != "{input}":
|
|
d["input"] = self.input_template
|
|
if self.args:
|
|
d["args"] = self.args
|
|
if self.provider:
|
|
d["provider"] = self.provider
|
|
return d
|
|
```
|
|
|
|
### Execution Function
|
|
|
|
```python
|
|
def execute_tool_step(
|
|
step: ToolStep,
|
|
variables: dict,
|
|
depth: int = 0,
|
|
max_depth: int = 10
|
|
) -> tuple[str, bool]:
|
|
"""Execute a tool step by calling another tool."""
|
|
if depth >= max_depth:
|
|
return "", False # Prevent infinite recursion
|
|
|
|
# Resolve the tool
|
|
try:
|
|
resolved = resolve_tool(step.tool)
|
|
except ToolNotFoundError:
|
|
print(f"Error: Tool '{step.tool}' not found", file=sys.stderr)
|
|
return "", False
|
|
|
|
# Prepare input
|
|
input_text = substitute_variables(step.input_template, variables)
|
|
|
|
# Prepare arguments
|
|
args = {}
|
|
for key, value in step.args.items():
|
|
args[key] = substitute_variables(value, variables)
|
|
|
|
# Run the tool
|
|
output, exit_code = run_tool(
|
|
tool=resolved.tool,
|
|
input_text=input_text,
|
|
custom_args=args,
|
|
provider_override=step.provider,
|
|
_depth=depth + 1 # Track nesting depth
|
|
)
|
|
|
|
return output, exit_code == 0
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
- **Parallel tool execution**: Run independent tool steps concurrently
|
|
- **Conditional execution**: Skip steps based on conditions
|
|
- **Tool aliases**: Define shorthand names for frequently used tools
|
|
- **Caching**: Cache tool outputs for identical inputs
|