package skillpack import ( "context" "errors" "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 { sorted := make([]*Pack, 0, len(packs)) for _, p := range packs { if p != nil && p.Manifest != nil { sorted = append(sorted, p) } } if len(sorted) == 0 { return "" } 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"` } // BundleStager makes a pack's bundled files available to the current run and // returns a short note the model can act on (e.g. where the files are and how to // reference them). It is called LAZILY, inside the skill_use tool, so a pack's // files are staged only when the model actually loads that pack — not for every // subscribed pack on every run. A host implements it over its own file plumbing // (mort saves the files to run-scoped storage and returns their file_ids). nil = // no staging: skill_use just lists the bundled file names. type BundleStager func(ctx context.Context, p *Pack) (string, error) // 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). // // stager, if non-nil, is invoked when skill_use loads a pack with bundled files; // its returned note is appended to the body so the model knows how to reach the // staged scripts/references. A stager error degrades gracefully (the // instructions still return, with a note that the files are unavailable). func Activate(packs []*Pack, stager BundleStager) mdagent.Skill { byName := make(map[string]*Pack, len(packs)) for _, p := range packs { if p != nil && p.Manifest != nil { byName[p.Manifest.Name] = p } } if len(byName) == 0 { return nil } 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, if it has bundled files, how to access them.", func(ctx 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 } body := renderPackBody(p) if stager != nil && len(p.Bundled) > 0 { note, err := stager(ctx, p) switch { case err != nil: body += "\n\n(bundled files could not be staged: " + err.Error() + ")" case note != "": body += "\n\n" + note } } return body, nil }) tb := llm.NewToolbox("skillpack", tool) return mdskill.New("skillpacks", mdskill.WithInstructions(Catalog(packs)), mdskill.WithToolbox(tb), ) } // renderPackBody is the base skill_use payload: the pack's instructions plus, if // it has any, a list of its bundled file names. A stager (see Activate) appends // the concrete access note. func renderPackBody(p *Pack) string { if p == nil || p.Manifest == nil { return "Error: invalid skill pack." } 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:\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) { if p == nil || p.Manifest == nil { return "", errors.New("skillpack: Stage requires a non-nil pack") } dir := baseDir + "/" + p.Manifest.Name if err := p.Tree.WriteTo(dir); err != nil { return "", err } return dir, nil }