feat(skillpack): lazy BundleStager for bundled files in skill_use
executus CI / test (pull_request) Successful in 2m19s
executus CI / test (pull_request) Successful in 2m19s
Replace Activate's stagedDir string with a BundleStager callback invoked lazily inside skill_use: when the model loads a pack with bundled files, the host stages them (mort: into run-scoped file storage) and the returned note is appended to the body so the model knows how to reach them. A nil stager (or a stager error) degrades gracefully to just listing the file names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-16
@@ -66,17 +66,26 @@ 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).
|
||||
//
|
||||
// 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 {
|
||||
// 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 {
|
||||
@@ -88,13 +97,23 @@ func Activate(packs []*Pack, stagedDir string) mdagent.Skill {
|
||||
}
|
||||
|
||||
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) {
|
||||
"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
|
||||
}
|
||||
return renderPackBody(p, stagedDir), 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)
|
||||
@@ -104,20 +123,17 @@ func Activate(packs []*Pack, stagedDir string) mdagent.Skill {
|
||||
)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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")
|
||||
if stagedDir != "" {
|
||||
fmt.Fprintf(&b, " (under %s)", strings.TrimRight(stagedDir, "/")+"/"+p.Manifest.Name)
|
||||
}
|
||||
b.WriteString(":\n")
|
||||
b.WriteString("\nBundled files:\n")
|
||||
for _, f := range p.Bundled {
|
||||
fmt.Fprintf(&b, "- %s\n", f)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,12 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
||||
packs := []*Pack{
|
||||
mustPack(t, "pdf", "Use pdfplumber.", map[string]string{"scripts/x.py": "print()"}),
|
||||
}
|
||||
sk := Activate(packs, "/stage")
|
||||
staged := 0
|
||||
stager := func(_ context.Context, p *Pack) (string, error) {
|
||||
staged++
|
||||
return "staged " + p.Manifest.Name + " (file_id=abc)", nil
|
||||
}
|
||||
sk := Activate(packs, stager)
|
||||
if sk == nil {
|
||||
t.Fatal("expected a non-nil skill")
|
||||
}
|
||||
@@ -56,6 +61,9 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("skill_use tool missing from toolbox")
|
||||
}
|
||||
if staged != 0 {
|
||||
t.Error("stager must be lazy — not called until skill_use runs")
|
||||
}
|
||||
|
||||
// load an existing pack
|
||||
out, err := tool.Handler(ctx, json.RawMessage(`{"name":"pdf"}`))
|
||||
@@ -66,8 +74,11 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
||||
if !strings.Contains(body, "Use pdfplumber.") {
|
||||
t.Errorf("skill_use body missing instructions: %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "scripts/x.py") || !strings.Contains(body, "/stage/pdf") {
|
||||
t.Errorf("skill_use should list bundled files under the staged dir: %q", body)
|
||||
if !strings.Contains(body, "scripts/x.py") {
|
||||
t.Errorf("skill_use should list bundled files: %q", body)
|
||||
}
|
||||
if staged != 1 || !strings.Contains(body, "file_id=abc") {
|
||||
t.Errorf("stager should run on load and its note append to the body: staged=%d body=%q", staged, body)
|
||||
}
|
||||
|
||||
// unknown pack returns guidance, not an error
|
||||
@@ -81,7 +92,7 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestActivate_Empty(t *testing.T) {
|
||||
if Activate(nil, "") != nil {
|
||||
if Activate(nil, nil) != nil {
|
||||
t.Error("no packs should activate to a nil skill")
|
||||
}
|
||||
}
|
||||
@@ -92,7 +103,7 @@ func TestNilPackElementsAreSafe(t *testing.T) {
|
||||
if got := Catalog(packs); !strings.Contains(got, "real") {
|
||||
t.Errorf("catalog should include the valid pack and skip nils: %q", got)
|
||||
}
|
||||
sk := Activate(packs, "")
|
||||
sk := Activate(packs, nil)
|
||||
if sk == nil {
|
||||
t.Fatal("a valid pack among nils should still activate")
|
||||
}
|
||||
@@ -100,7 +111,7 @@ func TestNilPackElementsAreSafe(t *testing.T) {
|
||||
t.Error("skill_use missing")
|
||||
}
|
||||
// All-nil activates to nothing rather than panicking.
|
||||
if Activate([]*Pack{nil, {Manifest: nil}}, "") != nil {
|
||||
if Activate([]*Pack{nil, {Manifest: nil}}, nil) != nil {
|
||||
t.Error("only-nil packs should activate to nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user