// 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") }