feat: first-class skill packs on agents + ship gifsmith builtin
executus CI / test (push) Successful in 3m21s
executus CI / test (push) Successful in 3m21s
Lifts the 'an agent uses a SKILL.md pack' concept out of a host and into the harness: - run.Ports.SkillPacks (SkillPackActivator) — nil-safe port; the executor folds a loaded agent's pack catalog into the system prompt and adds a skill_use loader tool to the toolbox (uses the existing ra.SystemPrompt + toolbox seams) - run.RunnableAgent.SkillPacks + persona.Agent.SkillPacks (+ skill_packs YAML, extends-inherit, ToRunnable) — the Agent noun is now pack-aware - skillpack.Activator — the battery's default port impl (resolve names → packs → catalog + skill_use), with a per-run BundleStager factory the host plumbs; satisfies the port structurally (no import of run) - agentbuiltins: ships gifsmith, a portable focused GIF/MP4 render agent that uses the gif pack — references tool/tier/pack NAMES only, no host coupling A host now wires run.Ports.SkillPacks instead of carrying its own activation glue. Tests: Activator resolution + gifsmith loads through persona→RunnableAgent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package skillpack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
||||
)
|
||||
|
||||
// Activator adapts the battery to executus/run's SkillPackActivator port: given
|
||||
// an agent's subscribed pack names, it resolves them to their pinned packs and
|
||||
// returns the catalog instructions + the skill_use tool the run injects. It
|
||||
// satisfies run.SkillPackActivator structurally — no import of run — so the
|
||||
// battery stays run-agnostic (the same inversion as the other batteries).
|
||||
//
|
||||
// StagerFor, when set, builds the per-run BundleStager (a host plumbs bundled
|
||||
// files into its own run-scoped storage from the run + subject ids); nil means
|
||||
// skill_use lists a pack's bundled filenames without staging them.
|
||||
type Activator struct {
|
||||
Cache PackCache
|
||||
Subs Store
|
||||
StagerFor func(runID, subjectID string) BundleStager
|
||||
}
|
||||
|
||||
// ActivateSkillPacks implements run.SkillPackActivator. Unknown or disabled pack
|
||||
// names are skipped; it returns "" + nil when nothing resolves.
|
||||
func (a *Activator) ActivateSkillPacks(ctx context.Context, names []string, runID, subjectID string) (string, []llm.Tool, error) {
|
||||
if a == nil || a.Subs == nil || a.Cache == nil || len(names) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
chosen := make([]Subscription, 0, len(names))
|
||||
for _, n := range names {
|
||||
sub, err := a.Subs.GetByName(ctx, n)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !sub.Enabled {
|
||||
continue
|
||||
}
|
||||
chosen = append(chosen, *sub)
|
||||
}
|
||||
packs, err := Resolve(ctx, a.Cache, chosen)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
var stager BundleStager
|
||||
if a.StagerFor != nil {
|
||||
stager = a.StagerFor(runID, subjectID)
|
||||
}
|
||||
sk := Activate(packs, stager)
|
||||
if sk == nil {
|
||||
return "", nil, nil
|
||||
}
|
||||
return sk.Instructions(), sk.Tools().Tools(), nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package skillpack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestActivator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
src := &fakeSource{tree: packTree("alpha", "do alpha things"), ref: "r1"}
|
||||
y := newTestSyncer(src)
|
||||
if _, err := y.Subscribe(ctx, src, "main", "steve"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
staged := 0
|
||||
act := &Activator{
|
||||
Cache: y.Cache, Subs: y.Subs,
|
||||
StagerFor: func(runID, subjectID string) BundleStager {
|
||||
return func(context.Context, *Pack) (string, error) { staged++; return "", nil }
|
||||
},
|
||||
}
|
||||
|
||||
instr, tools, err := act.ActivateSkillPacks(ctx, []string{"alpha"}, "run1", "agent1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if instr == "" {
|
||||
t.Error("expected catalog instructions")
|
||||
}
|
||||
found := false
|
||||
for _, tl := range tools {
|
||||
if tl.Name == "skill_use" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected a skill_use tool, got %d tools", len(tools))
|
||||
}
|
||||
|
||||
// unknown name → nothing resolves (no error, no tools).
|
||||
if in, tl, err := act.ActivateSkillPacks(ctx, []string{"nope"}, "r", "a"); err != nil || in != "" || tl != nil {
|
||||
t.Fatalf("unknown pack should resolve to nothing: in=%q tools=%v err=%v", in, tl, err)
|
||||
}
|
||||
|
||||
// nil-safe: a zero Activator (or empty names) is inert.
|
||||
if in, tl, err := (&Activator{}).ActivateSkillPacks(ctx, []string{"alpha"}, "r", "a"); err != nil || in != "" || tl != nil {
|
||||
t.Fatalf("zero Activator should be inert: %q %v %v", in, tl, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user