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"`
|
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
|
// 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
|
// 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
|
// 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
|
// to an agent with agent.WithSkill. Returns nil when there are no packs, which
|
||||||
// agent.WithSkill tolerates (a nil skill contributes nothing).
|
// agent.WithSkill tolerates (a nil skill contributes nothing).
|
||||||
//
|
//
|
||||||
// stagedDir, if non-empty, is the directory a host has staged the packs' bundled
|
// stager, if non-nil, is invoked when skill_use loads a pack with bundled files;
|
||||||
// files into (see Stage); skill_use appends the concrete path so the model knows
|
// its returned note is appended to the body so the model knows how to reach the
|
||||||
// where to read scripts/references with its file tools. Leave it empty when the
|
// staged scripts/references. A stager error degrades gracefully (the
|
||||||
// host has no staging.
|
// instructions still return, with a note that the files are unavailable).
|
||||||
func Activate(packs []*Pack, stagedDir string) mdagent.Skill {
|
func Activate(packs []*Pack, stager BundleStager) mdagent.Skill {
|
||||||
byName := make(map[string]*Pack, len(packs))
|
byName := make(map[string]*Pack, len(packs))
|
||||||
for _, p := range packs {
|
for _, p := range packs {
|
||||||
if p != nil && p.Manifest != nil {
|
if p != nil && p.Manifest != nil {
|
||||||
@@ -88,13 +97,23 @@ func Activate(packs []*Pack, stagedDir string) mdagent.Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tool := llm.DefineTool("skill_use",
|
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.",
|
"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(_ context.Context, args skillUseArgs) (any, error) {
|
func(ctx context.Context, args skillUseArgs) (any, error) {
|
||||||
p, ok := byName[strings.TrimSpace(args.Name)]
|
p, ok := byName[strings.TrimSpace(args.Name)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Sprintf("No skill named %q. Use one of the names from the Available skills list.", args.Name), nil
|
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)
|
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
|
// renderPackBody is the base skill_use payload: the pack's instructions plus, if
|
||||||
// pointer to its bundled files (with the staged path when known).
|
// it has any, a list of its bundled file names. A stager (see Activate) appends
|
||||||
func renderPackBody(p *Pack, stagedDir string) string {
|
// the concrete access note.
|
||||||
|
func renderPackBody(p *Pack) string {
|
||||||
if p == nil || p.Manifest == nil {
|
if p == nil || p.Manifest == nil {
|
||||||
return "Error: invalid skill pack."
|
return "Error: invalid skill pack."
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
fmt.Fprintf(&b, "# Skill: %s\n\n%s\n", p.Manifest.Name, p.Manifest.Body)
|
fmt.Fprintf(&b, "# Skill: %s\n\n%s\n", p.Manifest.Name, p.Manifest.Body)
|
||||||
if len(p.Bundled) > 0 {
|
if len(p.Bundled) > 0 {
|
||||||
b.WriteString("\nBundled files")
|
b.WriteString("\nBundled files:\n")
|
||||||
if stagedDir != "" {
|
|
||||||
fmt.Fprintf(&b, " (under %s)", strings.TrimRight(stagedDir, "/")+"/"+p.Manifest.Name)
|
|
||||||
}
|
|
||||||
b.WriteString(":\n")
|
|
||||||
for _, f := range p.Bundled {
|
for _, f := range p.Bundled {
|
||||||
fmt.Fprintf(&b, "- %s\n", f)
|
fmt.Fprintf(&b, "- %s\n", f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
|||||||
packs := []*Pack{
|
packs := []*Pack{
|
||||||
mustPack(t, "pdf", "Use pdfplumber.", map[string]string{"scripts/x.py": "print()"}),
|
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 {
|
if sk == nil {
|
||||||
t.Fatal("expected a non-nil skill")
|
t.Fatal("expected a non-nil skill")
|
||||||
}
|
}
|
||||||
@@ -56,6 +61,9 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("skill_use tool missing from toolbox")
|
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
|
// load an existing pack
|
||||||
out, err := tool.Handler(ctx, json.RawMessage(`{"name":"pdf"}`))
|
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.") {
|
if !strings.Contains(body, "Use pdfplumber.") {
|
||||||
t.Errorf("skill_use body missing instructions: %q", body)
|
t.Errorf("skill_use body missing instructions: %q", body)
|
||||||
}
|
}
|
||||||
if !strings.Contains(body, "scripts/x.py") || !strings.Contains(body, "/stage/pdf") {
|
if !strings.Contains(body, "scripts/x.py") {
|
||||||
t.Errorf("skill_use should list bundled files under the staged dir: %q", body)
|
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
|
// unknown pack returns guidance, not an error
|
||||||
@@ -81,7 +92,7 @@ func TestActivate_SkillUseTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestActivate_Empty(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")
|
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") {
|
if got := Catalog(packs); !strings.Contains(got, "real") {
|
||||||
t.Errorf("catalog should include the valid pack and skip nils: %q", got)
|
t.Errorf("catalog should include the valid pack and skip nils: %q", got)
|
||||||
}
|
}
|
||||||
sk := Activate(packs, "")
|
sk := Activate(packs, nil)
|
||||||
if sk == nil {
|
if sk == nil {
|
||||||
t.Fatal("a valid pack among nils should still activate")
|
t.Fatal("a valid pack among nils should still activate")
|
||||||
}
|
}
|
||||||
@@ -100,7 +111,7 @@ func TestNilPackElementsAreSafe(t *testing.T) {
|
|||||||
t.Error("skill_use missing")
|
t.Error("skill_use missing")
|
||||||
}
|
}
|
||||||
// All-nil activates to nothing rather than panicking.
|
// 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")
|
t.Error("only-nil packs should activate to nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user