Documentation
¶
Overview ¶
Package skill provides skill composition primitives built on tool execution.
A skill is a reusable workflow composed of tool calls with pre/post conditions and validation guards. This library defines the orchestration model; actual tool execution is delegated to a Runner implementation.
Core Concepts ¶
- Skill: Declarative workflow definition with named steps
- Step: Individual tool invocation with ID, ToolID, and Inputs
- Plan: Compiled, deterministically-ordered execution plan
- Planner: Compiles Skills into Plans (validates and sorts by step ID)
- Runner: Executes individual steps (user-provided implementation)
- Guard: Validates skills before execution (max steps, allowed tools)
Basic Usage ¶
// Define a skill
s := skill.Skill{
Name: "create-and-label-issue",
Steps: []skill.Step{
{ID: "1", ToolID: "github:create_issue", Inputs: map[string]any{"title": "Bug"}},
{ID: "2", ToolID: "github:add_labels", Inputs: map[string]any{"labels": []string{"bug"}}},
},
}
// Create a plan (validates and sorts steps)
planner := skill.NewPlanner()
plan, err := planner.Plan(s)
if err != nil {
log.Fatal(err)
}
// Execute the plan
results, err := skill.Execute(ctx, plan, myRunner)
Guards ¶
Guards validate skills before execution:
// Limit maximum steps
guard := skill.MaxStepsGuard(10)
if err := guard.Validate(mySkill); err != nil {
// Handle validation failure
}
// Restrict to allowed tools
guard := skill.AllowedToolIDsGuard([]string{"github:create_issue", "github:list_repos"})
Runner Interface ¶
Implement Runner to execute steps:
type myRunner struct {
exec *toolexec.Exec
}
func (r *myRunner) Run(ctx context.Context, step skill.Step) (any, error) {
result, err := r.exec.RunTool(ctx, step.ToolID, step.Inputs)
if err != nil {
return nil, err
}
return result.Value, nil
}
Execution Model ¶
Execute runs steps sequentially in plan order:
- Steps are executed one at a time
- Execution stops on first error (fail-fast)
- Partial results are returned on error
- Context cancellation is respected
Determinism ¶
Plans are deterministic: steps are sorted by ID (lexicographically). This ensures reproducible execution order regardless of definition order.
Thread Safety ¶
- Skill, Step, Plan: Immutable after creation (safe for concurrent read)
- Planner: Safe for concurrent use
- Runner: Must be safe for concurrent use (per contract)
- Guard: Safe after construction (immutable closures)
Error Handling ¶
Sentinel errors for programmatic handling:
- ErrInvalidRunner: Execute called with nil runner
- ErrInvalidSkillName: Skill has empty name
- ErrInvalidStepID: Step has empty ID
- ErrInvalidToolID: Step has empty ToolID
- ErrNoSteps: Skill has no steps
- ErrMaxStepsExceeded: MaxStepsGuard limit exceeded
- ErrToolNotAllowed: AllowedToolIDsGuard rejected tool
Use errors.Is() for reliable error checking:
if errors.Is(err, skill.ErrNoSteps) {
// Handle empty skill
}
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidRunner is returned when Execute is called with a nil runner. ErrInvalidRunner = errors.New("skill: runner is required") // ErrInvalidSkillName is returned when a Skill has an empty name. ErrInvalidSkillName = errors.New("skill: skill name is required") // ErrInvalidStepID is returned when a Step has an empty ID. ErrInvalidStepID = errors.New("skill: step id is required") // ErrInvalidToolID is returned when a Step has an empty ToolID. ErrInvalidToolID = errors.New("skill: tool id is required") // ErrNoSteps is returned when planning a Skill with no steps. ErrNoSteps = errors.New("skill: skill has no steps") // ErrMaxStepsExceeded is returned by MaxStepsGuard when limit is exceeded. ErrMaxStepsExceeded = errors.New("skill: max steps exceeded") // ErrToolNotAllowed is returned by AllowedToolIDsGuard for unlisted tools. ErrToolNotAllowed = errors.New("skill: tool id not allowed") )
Sentinel errors for the skill package.
Functions ¶
This section is empty.
Types ¶
type Guard ¶
Guard validates a skill or step.
Contract:
- Concurrency: implementations must be safe for concurrent use.
- Errors: validation failures must return non-nil error; must not panic.
- Determinism: same skill should yield same result.
func AllowedToolIDsGuard ¶
AllowedToolIDsGuard restricts steps to a set of tool IDs.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/skill"
)
func main() {
guard := skill.AllowedToolIDsGuard([]string{"safe:read", "safe:list"})
safeSkill := skill.Skill{
Name: "safe",
Steps: []skill.Step{{ID: "1", ToolID: "safe:read"}},
}
unsafeSkill := skill.Skill{
Name: "unsafe",
Steps: []skill.Step{{ID: "1", ToolID: "dangerous:delete"}},
}
fmt.Println("Safe skill valid:", guard.Validate(safeSkill) == nil)
fmt.Println("Unsafe skill valid:", guard.Validate(unsafeSkill) == nil)
}
Output: Safe skill valid: true Unsafe skill valid: false
func MaxStepsGuard ¶
MaxStepsGuard enforces a maximum number of steps.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/skill"
)
func main() {
guard := skill.MaxStepsGuard(2)
smallSkill := skill.Skill{
Name: "small",
Steps: []skill.Step{{ID: "1", ToolID: "ns:tool"}},
}
largeSkill := skill.Skill{
Name: "large",
Steps: []skill.Step{
{ID: "1", ToolID: "ns:tool"},
{ID: "2", ToolID: "ns:tool"},
{ID: "3", ToolID: "ns:tool"},
},
}
fmt.Println("Small skill valid:", guard.Validate(smallSkill) == nil)
fmt.Println("Large skill valid:", guard.Validate(largeSkill) == nil)
}
Output: Small skill valid: true Large skill valid: false
type Planner ¶
type Planner struct{}
Planner produces deterministic plans from skills.
func NewPlanner ¶
func NewPlanner() *Planner
NewPlanner creates a Planner.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/skill"
)
func main() {
s := skill.Skill{
Name: "greet-workflow",
Steps: []skill.Step{
{ID: "step-b", ToolID: "ns:greet", Inputs: map[string]any{"name": "World"}},
{ID: "step-a", ToolID: "ns:log", Inputs: map[string]any{"msg": "starting"}},
},
}
planner := skill.NewPlanner()
plan, err := planner.Plan(s)
if err != nil {
panic(err)
}
// Steps are sorted by ID
fmt.Println("Plan name:", plan.Name)
fmt.Println("First step:", plan.Steps[0].ID)
fmt.Println("Second step:", plan.Steps[1].ID)
}
Output: Plan name: greet-workflow First step: step-a Second step: step-b
type Runner ¶
Runner executes a single step.
Contract: - Concurrency: implementations must be safe for concurrent use. - Context: must honor cancellation/deadlines and return ctx.Err() when canceled. - Errors: execution failures should be returned directly; callers may wrap.
type Skill ¶
Skill represents a declarative workflow.
func (Skill) Validate ¶
Validate validates the skill definition.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/skill"
)
func main() {
validSkill := skill.Skill{
Name: "valid",
Steps: []skill.Step{{ID: "1", ToolID: "ns:tool"}},
}
invalidSkill := skill.Skill{
Name: "", // Empty name
Steps: []skill.Step{{ID: "1", ToolID: "ns:tool"}},
}
fmt.Println("Valid skill:", validSkill.Validate() == nil)
fmt.Println("Invalid skill:", invalidSkill.Validate() == nil)
}
Output: Valid skill: true Invalid skill: false
type StepResult ¶
StepResult captures execution results.
func Execute ¶
Execute runs the plan in order using the provided runner.
Example ¶
package main
import (
"context"
"fmt"
"github.com/jonwraymond/toolcompose/skill"
)
// mockRunner implements skill.Runner for examples.
type mockRunner struct {
results map[string]any
}
func (r *mockRunner) Run(ctx context.Context, step skill.Step) (any, error) {
if result, ok := r.results[step.ID]; ok {
return result, nil
}
return fmt.Sprintf("executed %s", step.ToolID), nil
}
func main() {
plan := skill.Plan{
Name: "demo",
Steps: []skill.Step{
{ID: "1", ToolID: "ns:tool1"},
{ID: "2", ToolID: "ns:tool2"},
},
}
runner := &mockRunner{
results: map[string]any{
"1": "result-1",
"2": "result-2",
},
}
results, err := skill.Execute(context.Background(), plan, runner)
if err != nil {
panic(err)
}
fmt.Println("Results count:", len(results))
fmt.Println("Step 1 result:", results[0].Value)
fmt.Println("Step 2 result:", results[1].Value)
}
Output: Results count: 2 Step 1 result: result-1 Step 2 result: result-2