toolexec User Journey¶
Overview¶
This guide walks through using toolexec to execute tools, from simple single-tool calls to complex orchestrated workflows.
1. Installation¶
go get github.com/jonwraymond/toolexec@latest
2. Quick Start with the Unified Facade (Recommended)¶
The exec package provides a unified facade combining discovery and execution:
import (
"github.com/jonwraymond/toolexec/exec"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/tooldiscovery/tooldoc"
)
// Setup discovery infrastructure
idx := index.NewInMemoryIndex()
docs := tooldoc.NewInMemoryStore(tooldoc.StoreOptions{Index: idx})
// Create executor with local handlers
executor, err := exec.New(exec.Options{
Index: idx,
Docs: docs,
LocalHandlers: map[string]exec.Handler{
"calculator-add": func(ctx context.Context, args map[string]any) (any, error) {
a, b := args["a"].(float64), args["b"].(float64)
return a + b, nil
},
},
})
// Execute a tool
result, err := executor.RunTool(ctx, "math:add", map[string]any{"a": 5, "b": 3})
fmt.Println(result.Value) // 8
// Search for tools
results, _ := executor.SearchTools(ctx, "calculator", 10)
// Get tool documentation
doc, _ := executor.GetToolDoc(ctx, "math:add", tooldoc.DetailFull)
3. Execute a Single Tool¶
result, err := executor.RunTool(ctx, "github:create_issue", map[string]any{
"owner": "jonwraymond",
"repo": "toolexec",
"title": "Bug report",
"body": "Description here",
})
if err != nil {
log.Fatalf("Execution failed: %v", err)
}
fmt.Printf("Created issue: %v\n", result.Value)
fmt.Printf("Duration: %v\n", result.Duration)
4. Chain Tool Calls¶
Use UsePrevious to pass results between steps:
result, steps, err := executor.RunChain(ctx, []exec.Step{
{ToolID: "github:create_issue", Args: map[string]any{
"title": "Bug report",
}},
{ToolID: "github:add_labels", Args: map[string]any{
"labels": []string{"bug"},
}, UsePrevious: true}, // Injects previous result as "previous" arg
})
fmt.Printf("Final result: %v\n", result.Value)
for i, step := range steps {
fmt.Printf("Step %d: %s → %v\n", i+1, step.ToolID, step.Value)
}
5. Register Local Tools¶
Register tools using the backend/local package:
import "github.com/jonwraymond/toolexec/backend/local"
backend := local.New("calculator")
backend.RegisterHandler("add", local.ToolDef{
Name: "add",
Description: "Adds two numbers",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"a": map[string]any{"type": "number"},
"b": map[string]any{"type": "number"},
},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
a, b := args["a"].(float64), args["b"].(float64)
return a + b, nil
},
})
6. Using the Run Package Directly (Advanced)¶
For more control, use the run package directly:
import "github.com/jonwraymond/toolexec/run"
runner := run.NewRunner(
run.WithIndex(idx),
run.WithLocalRegistry(localReg),
run.WithValidation(true, true),
)
result, err := runner.Run(ctx, "ns:tool", args)
7. Code Orchestration (Sandboxed Execution)¶
The code package enables executing user-provided code that can call tools:
import "github.com/jonwraymond/toolexec/code"
// Create code executor with limits
codeExec := code.NewExecutor(code.Config{
Index: idx,
Docs: docs,
Run: runner,
MaxToolCalls: 50,
Timeout: 30 * time.Second,
})
result, err := codeExec.Execute(ctx, code.ExecuteParams{
Language: "go",
Code: `
// Access tools via the tools interface
result, _ := tools.RunTool(ctx, "math:add", map[string]any{"a": 1, "b": 2})
return result.Structured
`,
})
8. Runtime Isolation (Security Profiles)¶
toolexec supports three security profiles for different isolation levels:
| Profile | Isolation | Use Case |
|---|---|---|
ProfileDev | None | Development/testing |
ProfileStandard | Container | Production |
ProfileHardened | VM/gVisor | Untrusted code |
import (
"github.com/jonwraymond/toolexec/runtime"
"github.com/jonwraymond/toolexec/runtime/backend/unsafe"
"github.com/jonwraymond/toolexec/runtime/gateway/direct"
)
// Gateway exposes tool discovery + execution to sandboxed code
gateway := direct.New(direct.Config{
Index: idx,
Docs: docs,
Runner: runner,
})
// Runtime with security profile selection
rt := runtime.NewDefaultRuntime(runtime.RuntimeConfig{
Backends: map[runtime.SecurityProfile]runtime.Backend{
runtime.ProfileDev: unsafe.New(unsafe.Config{RequireOptIn: true}),
},
DefaultProfile: runtime.ProfileDev,
})
// Execute code in the runtime
result, err := rt.Execute(ctx, runtime.ExecuteRequest{
Language: "go",
Code: `__out = 1 + 1`,
Profile: runtime.ProfileDev,
Gateway: gateway,
Limits: runtime.Limits{
MaxToolCalls: 100,
MemoryBytes: 512 * 1024 * 1024, // 512MB
CPUQuotaMillis: 60000, // 60s
},
})
For container isolation, use runtime/backend/docker or runtime/backend/containerd with ProfileStandard.
For maximum isolation, use runtime/backend/gvisor, runtime/backend/kata, or runtime/backend/firecracker with ProfileHardened.
WASM Execution Input¶
The WASM backend expects a compiled module provided via ExecuteRequest.Metadata:
req := runtime.ExecuteRequest{
Language: "wasm",
Code: "ignored for wasm",
Gateway: gateway,
Metadata: map[string]any{
"wasm_module": []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00},
// or "wasm_module_b64": "<base64-encoded module bytes>"
},
}
Execution Flow¶
Client exec.Exec run.Runner Backend
| | | |
|-- RunTool(id) ------| | |
| |-- Run(id, args) ----| |
| | |-- Validate --------|
| | |-- Resolve ---------|
| | |-- Execute ---------|
| | |<-- Result ---------|
| |<-- RunResult -------| |
|<-- Result ----------| | |
Examples¶
See the examples page for runnable examples (source lives in toolexec/examples/):
basic/- Simple tool executionchain/- Sequential tool chainingdiscovery/- Search and execute workflowstreaming/- Streaming execution eventsruntime/- Security profile configurationfull/- Complete integration example
Next Steps¶
- Add observability with toolops/observe
- Expose via MCP with metatools-mcp