package skillpack import ( "context" "fmt" "sort" "strings" mdagent "gitea.stevedudenhoeffer.com/steve/majordomo/agent" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" mdskill "gitea.stevedudenhoeffer.com/steve/majordomo/skill" ) // Resolve loads the pinned Pack for each enabled subscription from the cache. It // is how a host turns "this agent subscribes to these packs" into activatable // packs at run time without touching the network. A pinned digest missing from // the cache is an error (the host should have cached it at pin/apply time). // Disabled subscriptions are skipped. func Resolve(ctx context.Context, cache PackCache, subs []Subscription) ([]*Pack, error) { out := make([]*Pack, 0, len(subs)) for i := range subs { s := &subs[i] if !s.Enabled { continue } tree, err := cache.Get(ctx, s.PinnedDigest) if err != nil { return nil, fmt.Errorf("skillpack: resolving %q: %w", s.Name, err) } pack, err := LoadPack(tree) if err != nil { return nil, fmt.Errorf("skillpack: loading %q: %w", s.Name, err) } out = append(out, pack) } return out, nil } // Catalog renders the always-in-prompt block for a set of packs: one line per // pack (name + description) plus how to load one. This is the whole prompt cost // of a subscription — the bodies stay out until skill_use is called. func Catalog(packs []*Pack) string { if len(packs) == 0 { return "" } sorted := append([]*Pack(nil), packs...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Manifest.Name < sorted[j].Manifest.Name }) var b strings.Builder b.WriteString("You have access to skills — packaged instructions for specific tasks. ") b.WriteString("When a task matches one, call skill_use with its name to load its full instructions before proceeding.\n\n") b.WriteString("Available skills:\n") for _, p := range sorted { fmt.Fprintf(&b, "- %s: %s\n", p.Manifest.Name, p.Manifest.Description) } return strings.TrimRight(b.String(), "\n") } type skillUseArgs struct { Name string `json:"name" description:"the exact name of the skill to load, from the Available skills list"` } // Activate turns a set of resolved packs into a majordomo agent.Skill: its // Instructions are the Catalog, and it contributes a single skill_use tool that // returns a named pack's full body (progressive disclosure). Attach the result // to an agent with agent.WithSkill. Returns nil when there are no packs, which // agent.WithSkill tolerates (a nil skill contributes nothing). // // stagedDir, if non-empty, is the directory a host has staged the packs' bundled // files into (see Stage); skill_use appends the concrete path so the model knows // where to read scripts/references with its file tools. Leave it empty when the // host has no staging. func Activate(packs []*Pack, stagedDir string) mdagent.Skill { if len(packs) == 0 { return nil } byName := make(map[string]*Pack, len(packs)) for _, p := range packs { byName[p.Manifest.Name] = p } tool := llm.DefineTool("skill_use", "Load the full instructions for a skill by name before doing a task it covers. Returns the skill's instructions and a list of any bundled files.", func(_ context.Context, args skillUseArgs) (any, error) { p, ok := byName[strings.TrimSpace(args.Name)] if !ok { return fmt.Sprintf("No skill named %q. Use one of the names from the Available skills list.", args.Name), nil } return renderPackBody(p, stagedDir), nil }) tb := llm.NewToolbox("skillpack", tool) return mdskill.New("skillpacks", mdskill.WithInstructions(Catalog(packs)), mdskill.WithToolbox(tb), ) } // renderPackBody is what skill_use returns: the pack's instructions plus a // pointer to its bundled files (with the staged path when known). func renderPackBody(p *Pack, stagedDir string) string { var b strings.Builder fmt.Fprintf(&b, "# Skill: %s\n\n%s\n", p.Manifest.Name, p.Manifest.Body) if len(p.Bundled) > 0 { b.WriteString("\nBundled files") if stagedDir != "" { fmt.Fprintf(&b, " (under %s)", strings.TrimRight(stagedDir, "/")+"/"+p.Manifest.Name) } b.WriteString(":\n") for _, f := range p.Bundled { fmt.Fprintf(&b, "- %s\n", f) } } return strings.TrimRight(b.String(), "\n") } // Stage materializes a pack's files under baseDir// so a host can // mount them (read-only is the host's concern) into a sandbox the agent's file // tools can read. Returns the pack's staged directory. func Stage(p *Pack, baseDir string) (string, error) { dir := baseDir + "/" + p.Manifest.Name if err := p.Tree.WriteTo(dir); err != nil { return "", err } return dir, nil }