skill

package
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 6, 2026 License: MIT Imports: 3 Imported by: 0

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

View Source
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

type Guard interface {
	Validate(skill Skill) error
}

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

func AllowedToolIDsGuard(allowed []string) Guard

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

func MaxStepsGuard(max int) Guard

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 Plan

type Plan struct {
	Name  string
	Steps []Step
}

Plan is the compiled, deterministic execution plan.

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

func (*Planner) Plan

func (p *Planner) Plan(skill Skill) (Plan, error)

Plan validates and orders steps deterministically by ID.

type Runner

type Runner interface {
	Run(ctx context.Context, step Step) (any, error)
}

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

type Skill struct {
	Name  string
	Steps []Step
}

Skill represents a declarative workflow.

func (Skill) Validate

func (s Skill) Validate() error

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 Step

type Step struct {
	ID     string
	ToolID string
	Inputs map[string]any
}

Step references a tool and its bindings.

func (Step) Validate

func (s Step) Validate() error

Validate validates a step.

type StepResult

type StepResult struct {
	StepID string
	Value  any
	Err    error
}

StepResult captures execution results.

func Execute

func Execute(ctx context.Context, plan Plan, runner Runner) ([]StepResult, error)

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL