skill

package
v1.2.0-alpha.2 Latest Latest
Warning

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

Go to latest
Published: May 25, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package skill implements user-installed Markdown skills and the SKILL tool that invokes them. The package is public so downstream apps embedding the evva agent runtime can build their own skill catalogs — either by dropping SKILL.md files under a directory (the disk path) or by registering skills programmatically through Registry.Add (the SDK path).

Skills live in two directories. Both layouts are identical:

<root>/skills/
  <skill-name>/
    SKILL.md

LoadRegistry reads AppHome first then WorkDir, so a workdir-local skill transparently overrides a same-named home skill. The first line of every SKILL.md is parsed as `# <skill-name> <description>`; the body is whatever follows. The SKILL tool wraps the body as "Follow these instructions" when the model invokes a skill, so the file content is treated as opaque Markdown — the package does not impose structure beyond the title line.

Programmatic skills (added via Registry.Add) skip the filesystem entirely: they carry a BodyFunc the registry calls when the model dispatches the skill. This is the path SDK consumers use to bundle skills inside their binary via go:embed, fetch them over the network, or generate them on the fly.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Names

func Names() []tools.ToolName

Names lists every tool name this package contributes. Callers compose this into a profile's ActiveTools list the same way they do fs.Names() etc.

func ParseTitleLine

func ParseTitleLine(line string) (name, description string, err error)

ParseTitleLine parses a SKILL.md's first non-blank line — the canonical `# <name> [<description>]` shape — into its name token and optional description. It is the single source of truth both the disk loader (parseFirstLine) and the bundled loader (internal/skills/bundled) call, so a title that loads as a disk skill also loads as a bundled skill and vice versa. Errors carry no package prefix; callers wrap them with context.

Types

type Lookup

type Lookup func() *Registry

Lookup is the late-binding shape NewSkill accepts. The host loads the registry at startup (LoadRegistry or NewRegistry+Add) and installs it on the agent's ToolState; the SkillTool reads it through Lookup at Execute time so construction order between the tool and the registry doesn't matter — mirrors meta.SpawnerLookup and meta.DeferredLookup.

type Registry

type Registry struct {

	// Warnings collects non-fatal load issues (malformed first lines, name
	// mismatches, unreadable files). Callers may surface these at startup;
	// the loader never blocks boot on them.
	Warnings []string
	// contains filtered or unexported fields
}

Registry is the merged catalog of installed skills. Construct via LoadRegistry (disk) or NewRegistry (programmatic-only); methods are safe to call from any goroutine because the map is set once at construction and only mutated through Add — which runs at host bootstrap time before the agent starts dispatching tools.

func LoadRegistry

func LoadRegistry(homeSkillsDir, workdirSkillsDir string) (*Registry, error)

LoadRegistry scans homeSkillsDir then workdirSkillsDir and returns the merged registry. Missing directories are treated as empty — having no skills installed is the normal state, not an error.

The order matters: workdir skills overwrite home skills with the same folder name. The override is recorded as a warning so the user can spot surprise shadowing.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry for programmatic-only use. Hosts that want the on-disk override behavior should call LoadRegistry; hosts that bundle every skill in code can construct here and call Add.

Mixed use is fine: LoadRegistry first, then Add for any programmatic extras the host wants alongside the disk catalog.

Example

ExampleNewRegistry shows the SDK path for downstream apps that want a programmatic-only skill catalog: build an empty registry and Add each skill with a BodyFunc. Mixed disk+programmatic catalogs start from LoadRegistry and call Add for extras.

package main

import (
	"fmt"

	"github.com/johnny1110/evva/pkg/skill"
)

func main() {
	r := skill.NewRegistry()
	_ = r.Add(skill.SkillMeta{
		Name:        "commit",
		Description: "draft a conventional-commits message from the staged diff",
		BodyFunc: func() (string, error) {
			return "Generate a one-line conventional-commits message.", nil
		},
	})

	body, _ := r.LoadBody("commit")
	fmt.Println("names:", r.Names())
	fmt.Println("body:", body)
}
Output:
names: [commit]
body: Generate a one-line conventional-commits message.

func (*Registry) Add

func (r *Registry) Add(m SkillMeta) error

Add inserts a programmatic skill into the registry. The skill's Source field is force-set to SourceProgrammatic so the origin is always honest regardless of how the caller filled the struct.

Validation:

  • Name must be non-empty.
  • BodyFunc must be non-nil (the SKILL tool would have nothing to return).
  • A name already present in the registry is rejected. To override a disk skill the caller should use a different name or delete the disk entry before adding.
Example

ExampleRegistry_Add demonstrates the validation rules: name must be non-empty, BodyFunc must be set, duplicates are rejected. Add is typically called once per skill at host bootstrap before agent.New runs.

package main

import (
	"fmt"

	"github.com/johnny1110/evva/pkg/skill"
)

func main() {
	r := skill.NewRegistry()
	if err := r.Add(skill.SkillMeta{Name: ""}); err != nil {
		fmt.Println("empty-name:", err != nil)
	}
	if err := r.Add(skill.SkillMeta{Name: "no-body"}); err != nil {
		fmt.Println("no-body:", err != nil)
	}
	_ = r.Add(skill.SkillMeta{
		Name:     "review",
		BodyFunc: func() (string, error) { return "...", nil },
	})
	if err := r.Add(skill.SkillMeta{
		Name:     "review",
		BodyFunc: func() (string, error) { return "...", nil },
	}); err != nil {
		fmt.Println("duplicate:", err != nil)
	}
}
Output:
empty-name: true
no-body: true
duplicate: true

func (*Registry) AddBundled

func (r *Registry) AddBundled(m SkillMeta) error

AddBundled inserts a skill at the SourceBundled tier — evva's own first-party content. It differs from Add in two ways:

  1. If a skill with the same Name already exists (typically loaded from disk by LoadRegistry, or added programmatically), AddBundled silently skips the insert and returns nil. The existing entry wins WITHOUT a Warning — overriding a bundled body is the documented extension point, not surprise shadowing.
  2. Source is force-set to SourceBundled regardless of the caller's value, mirroring how Add force-sets SourceProgrammatic.

Validation matches Add: Name non-empty, BodyFunc non-nil, non-nil receiver. Callers live in internal/skills/bundled; external SDK consumers should use Add for content they ship.

func (*Registry) Get

func (r *Registry) Get(name string) (SkillMeta, bool)

Get returns the meta for a skill by name. ok=false when unknown.

func (*Registry) List

func (r *Registry) List() []SkillMeta

List returns every known skill sorted by name. Used by the sysprompt builder so the # Skills section is deterministic across runs.

func (*Registry) LoadBody

func (r *Registry) LoadBody(name string) (string, error)

LoadBody returns the full body content for the named skill. Disk-loaded skills are read from SkillMeta.Path; programmatic skills invoke SkillMeta.BodyFunc. The SKILL tool wraps the output before handing it back to the model.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns just the skill names, sorted. Convenience for callers that only need the catalog identifiers (e.g. the unknown-skill error message).

type SkillMeta

type SkillMeta struct {
	Name        string
	Description string
	Path        string // absolute path to SKILL.md; empty for programmatic skills
	Source      SkillSource
	// BodyFunc, when non-nil, is the lazy body loader the registry calls
	// instead of reading Path. Programmatic skills MUST set this; disk
	// skills MUST leave it nil.
	BodyFunc func() (string, error)
}

SkillMeta is the resolved metadata for a single skill. Body content is loaded on demand via Registry.LoadBody so the prompt path stays cheap.

For disk-loaded skills, Path points to SKILL.md and BodyFunc is nil — the registry reads the file at dispatch time. For programmatic skills, Path is empty and BodyFunc returns the body string when invoked; the host can back it with any source (embed.FS, network, generator, ...).

type SkillSource

type SkillSource string

SkillSource identifies where a skill was loaded from. Precedence on a name clash, highest to lowest: Programmatic (an explicit SDK choice the host made at startup) > WorkDir > Home > Bundled (evva's own first-party content, embedded in the binary). Bundled is intentionally lowest so a user's disk skill or a host's programmatic skill silently overrides it — overriding a bundled body is the documented extension point, not a surprise, so that override is NOT recorded as a Warning. The field is exposed mostly for logging and a future `/skills` slash command that wants to surface origin.

const (
	SourceHome         SkillSource = "home"
	SourceWorkDir      SkillSource = "workdir"
	SourceProgrammatic SkillSource = "programmatic"
	// SourceBundled is the lowest-precedence tier: a same-named disk skill
	// (Home or WorkDir) or a Programmatic skill wins silently. Registered
	// via Registry.AddBundled (see internal/skills/bundled).
	SourceBundled SkillSource = "bundled"
)

type SkillTool

type SkillTool struct {
	// contains filtered or unexported fields
}

SkillTool is the LLM-facing tool that invokes a user-installed skill by name. Execute returns the SKILL.md body (or BodyFunc output) wrapped as an instruction block so the model treats it as guidance to follow, not raw content to summarize.

func NewSkill

func NewSkill(lookup Lookup) *SkillTool

NewSkill constructs a SkillTool that reads its registry via lookup at Execute time. lookup may be nil (yields a clear runtime error if the model invokes the tool); it may also return nil (same outcome).

func (*SkillTool) Description

func (t *SkillTool) Description() string

func (*SkillTool) Execute

func (t *SkillTool) Execute(_ context.Context, logger *slog.Logger, input json.RawMessage) (tools.Result, error)

func (*SkillTool) Name

func (t *SkillTool) Name() string

func (*SkillTool) Schema

func (t *SkillTool) Schema() json.RawMessage

Jump to

Keyboard shortcuts

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