toolcompose Design Notes¶
Overview¶
toolcompose provides composition primitives for the ApertureStack ecosystem:
- set: build filtered, access-controlled tool collections
- skill: declare tool-based workflows and execute them deterministically
Ecosystem Position¶
flowchart TB
subgraph Foundation["toolfoundation"]
Model[model.Tool]
Adapter[adapter.CanonicalTool]
Backend[model.ToolBackend]
end
subgraph Discovery["tooldiscovery"]
Index[index.Index]
Search[search.BM25Searcher]
Docs[tooldoc.Store]
end
subgraph Exec["toolexec"]
Runner[run.Runner]
Code[code.Executor]
Runtime[runtime.Sandbox]
end
subgraph Compose["toolcompose"]
Set[set.Toolset]
Skill[skill.Skill]
Exposure[set.Exposure]
end
Model --> Adapter
Adapter --> Set
Index --> Set
Set --> Exposure
Runner --> Skill
Exposure --> Code Dependency Flow¶
| Package | Depends On | Provides To |
|---|---|---|
| toolfoundation | - | All packages (core types) |
| tooldiscovery | toolfoundation | toolcompose (registry source) |
| toolexec | toolfoundation, tooldiscovery | toolcompose (Runner impl) |
| toolcompose | toolfoundation | End users (composition API) |
set Package¶
Design Decisions¶
- Thread-safe Toolset: Toolsets wrap a map with RW locks for safe concurrent access.
- Deterministic Ordering:
IDs()andTools()return lexicographically sorted results. - Pure Composition: No I/O or execution; callers supply tools as input.
- Filter + Policy: Filters reduce candidates; policies enforce access rules.
- Export via Adapters:
Exposureconverts toolsets to protocol-specific shapes.
Filter + Policy Semantics¶
- Filters are AND-composed in builder order.
- Policies run after filters.
niltools are always rejected.
flowchart LR
Source[Canonical Tools] --> Filters
Filters --> Policy
Policy --> Toolset
Toolset --> Exposure Thread Safety Model¶
| Type | Safe For | Notes |
|---|---|---|
Toolset | Concurrent reads | Uses sync.RWMutex |
Builder | Single goroutine | Configure, then call Build() once |
Exposure | Concurrent reads | After construction only |
FilterFunc | Must be thread-safe | If Toolset is shared |
Error Handling¶
Sentinel errors enable programmatic handling:
ts, err := builder.Build()
if errors.Is(err, set.ErrNoSource) {
// No tools provided - call FromTools or FromRegistry
}
if errors.Is(err, set.ErrNilAdapter) {
// Adapter is nil in Exposure
}
skill Package¶
Design Decisions¶
- Declarative Skills: Skills are lightweight definitions of steps.
- Deterministic Planning: Planner sorts steps by ID to guarantee stable execution order.
- Runner Interface: Execution delegates to a
Runnerfor tool integration. - Guards: Optional validation hooks (max steps, allowed tool IDs).
- Fail-fast Execution: Execution stops on the first step error.
flowchart LR
Skill --> Planner
Planner --> Plan
Plan --> Execute
Execute --> Runner Execution Model¶
sequenceDiagram
participant User
participant Planner
participant Execute
participant Guard
participant Runner
User->>Planner: Plan(skill)
Planner->>Planner: Validate skill
Planner->>Planner: Apply guards
Planner-->>User: Plan
User->>Execute: Execute(ctx, plan, runner)
loop Each Step
Execute->>Runner: Run(ctx, toolID, inputs)
Runner-->>Execute: Result
alt Error
Execute-->>User: Partial results + error
end
end
Execute-->>User: All results Guard Contract¶
Guards must be: - Idempotent: Same skill → same result - Pure: No side effects during Check() - Thread-safe: If planner is shared
Thread Safety Model¶
| Type | Safe For | Notes |
|---|---|---|
Planner | Concurrent use | Stateless after construction |
Plan | Concurrent reads | Immutable after creation |
Runner | Implementation-dependent | Check implementation docs |
Guard | Must be thread-safe | If Planner is shared |
Error Handling¶
Sentinel errors for programmatic handling:
plan, err := planner.Plan(skill)
if errors.Is(err, skill.ErrNoSteps) {
// Skill has no steps defined
}
if errors.Is(err, skill.ErrMaxStepsExceeded) {
// MaxStepsGuard rejected the skill
}
if errors.Is(err, skill.ErrToolNotAllowed) {
// AllowedToolIDsGuard rejected a tool
}
Trade-offs¶
| Decision | Benefit | Cost |
|---|---|---|
| No implicit parallelism | Deterministic, easy to reason about | Sequential execution only |
| Explicit runner adapter | No hard dependency on execution engine | Requires integration work |
| Binary policy | Simple allow/deny semantics | Rich policies need wrappers |
| Fail-fast execution | Errors surface immediately | No partial recovery |
| Deterministic ordering | Reproducible results | Sorting overhead |
Integration Patterns¶
With tooldiscovery¶
// Build toolset from registry
ts, err := set.NewBuilder("github-tools").
FromRegistry(registry).
WithNamespace("github").
Build()
With toolexec¶
// Execute skill with toolexec runner
runner := exec.NewRunner(executor)
planner := skill.NewPlanner(skill.PlannerOptions{Runner: runner})
plan, _ := planner.Plan(mySkill)
results, _ := skill.Execute(ctx, plan, runner)
Protocol Export¶
// Export for MCP server
exposure := set.NewExposure(ts, adapter.NewMCPAdapter())
tools, warnings, errs := exposure.ExportWithWarnings()
for _, w := range warnings {
log.Printf("Feature %s lost converting %s → %s",
w.Feature, w.FromAdapter, w.ToAdapter)
}