Files
Cyrene_v2/internal/capability/skills/skill.go
T

135 lines
4.5 KiB
Go

// Package skills implements the M5 reusable-workflow capability: a Skill is a
// higher-order, persistable workflow — an ordered set of steps plus a base
// prompt, references, and guardrails — that builds on pkg/plugins/sdk and is
// more structured than a Tool (DevDocs/design/09 §0/§3).
//
// The package provides five pieces:
//
// - Skill: the concrete in-memory model implementing pkg/plugins/sdk.Skill.
// - Store: persistence of skills in a SQLite skills table.
// - Loader: loading Skill definitions from YAML (a single stream or a
// configs/skills/*.yaml directory).
// - Runner: expanding a Skill into an executable Workflow for the loop/agent
// executor (the actual execution itself lands in M2/M12).
// - Register: publishing skills to the global capability registry
// (internal/capability/registry) as KindSkill capabilities.
package skills
import (
"errors"
"fmt"
"io"
"log/slog"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// Sentinel errors returned by the skills package. Use errors.Is to test.
var (
// ErrInvalidSkill is returned when a skill is nil or has a blank name.
ErrInvalidSkill = errors.New("skills: invalid skill")
// ErrNotFound is returned by Store when a skill is absent.
ErrNotFound = errors.New("skills: skill not found")
// ErrNilRegistrar is returned by Register/NewRunner when no registry was
// supplied.
ErrNilRegistrar = errors.New("skills: nil registrar")
// ErrNilDB is returned by NewStore when no *sql.DB was supplied.
ErrNilDB = errors.New("skills: nil db")
// ErrRunPending is returned by Runner.Run, which awaits M12 wiring.
ErrRunPending = errors.New("skills: run pending")
)
// Skill is the concrete, persistable form of a skill (see pkg/plugins/sdk.Skill):
// a reusable workflow (ordered steps with dependencies), a base prompt,
// references, and guardrails. It is more structured than a Tool and is the
// higher-order capability described in DevDocs/design/09 §0.
//
// Skill is immutable after construction; its getters return copies of the slice
// fields, so a *Skill is safe for concurrent use as the sdk.Skill contract
// requires.
type Skill struct {
name string
description string
prompt string
steps []sdk.Step
refs []sdk.Ref
guardrails []sdk.Guardrail
}
// compile-time assertion that *Skill satisfies the SDK Skill interface.
var _ sdk.Skill = (*Skill)(nil)
// New builds a Skill. It returns ErrInvalidSkill when name is blank. The step,
// ref, and guardrail slices are copied, so the caller remains free to mutate
// them afterwards.
func New(name, description, prompt string, steps []sdk.Step, refs []sdk.Ref, guardrails []sdk.Guardrail) (*Skill, error) {
if strings.TrimSpace(name) == "" {
return nil, fmt.Errorf("skills: new: %w", ErrInvalidSkill)
}
return &Skill{
name: name,
description: description,
prompt: prompt,
steps: cloneSteps(steps),
refs: cloneRefs(refs),
guardrails: cloneGuardrails(guardrails),
}, nil
}
// Name returns the unique skill identifier.
func (s *Skill) Name() string { return s.name }
// Description explains what the Skill accomplishes; the model uses it to select
// a Skill for a task.
func (s *Skill) Description() string { return s.description }
// Prompt returns the base system prompt for the Skill.
func (s *Skill) Prompt() string { return s.prompt }
// Steps returns a copy of the ordered workflow steps.
func (s *Skill) Steps() []sdk.Step { return cloneSteps(s.steps) }
// Refs returns a copy of the references the Skill may draw on.
func (s *Skill) Refs() []sdk.Ref { return cloneRefs(s.refs) }
// Guardrails returns a copy of the constraints the Skill enforces.
func (s *Skill) Guardrails() []sdk.Guardrail { return cloneGuardrails(s.guardrails) }
func cloneSteps(in []sdk.Step) []sdk.Step {
if len(in) == 0 {
return nil
}
out := make([]sdk.Step, len(in))
copy(out, in)
return out
}
func cloneRefs(in []sdk.Ref) []sdk.Ref {
if len(in) == 0 {
return nil
}
out := make([]sdk.Ref, len(in))
copy(out, in)
return out
}
func cloneGuardrails(in []sdk.Guardrail) []sdk.Guardrail {
if len(in) == 0 {
return nil
}
out := make([]sdk.Guardrail, len(in))
copy(out, in)
return out
}
// sourcedLog returns a source-tagged logger suitable for this package, falling
// back to a discard logger when l is nil so callers can pass nil safely.
func sourcedLog(l *slog.Logger) *slog.Logger {
if l == nil {
l = slog.New(slog.NewTextHandler(io.Discard, nil))
}
return logger.WithSource(l, "capability/skills")
}