76ecf0e49e
Phase 6: skill.New constructor satisfying the agent.Skill contract; instruction-only skills; ordered additive composition; skill/clock (injectable-clock time tools) and skill/calc (recursive-descent arithmetic evaluator) as ready-made examples with full test suites incl. an agent-loop round trip. ADR-0013; README skills section + matrix synced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
package skill_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/agent"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/skill"
|
|
)
|
|
|
|
// The skill package must satisfy the agent contract.
|
|
var _ agent.Skill = (*skill.Skill)(nil)
|
|
|
|
func TestSkillConstruction(t *testing.T) {
|
|
tool := llm.Tool{Name: "t1"}
|
|
sk := skill.New("research",
|
|
skill.WithInstructions("Cite sources."),
|
|
skill.WithTools(tool),
|
|
)
|
|
if sk.Name() != "research" || sk.Instructions() != "Cite sources." {
|
|
t.Errorf("skill = %q / %q", sk.Name(), sk.Instructions())
|
|
}
|
|
if sk.Tools() == nil || len(sk.Tools().Tools()) != 1 {
|
|
t.Errorf("tools = %+v", sk.Tools())
|
|
}
|
|
if sk.Tools().Name() != "research" {
|
|
t.Errorf("toolbox name = %q, want skill name", sk.Tools().Name())
|
|
}
|
|
}
|
|
|
|
func TestInstructionOnlySkill(t *testing.T) {
|
|
sk := skill.New("tone", skill.WithInstructions("Be kind."))
|
|
if sk.Tools() != nil {
|
|
t.Error("instruction-only skill must have nil toolbox")
|
|
}
|
|
}
|
|
|
|
// TestSkillsAttachToAnyAgent: the same skill instance layers onto multiple
|
|
// agents, on demand, extending prompt and toolset additively.
|
|
func TestSkillsAttachToAnyAgent(t *testing.T) {
|
|
echo := llm.Tool{
|
|
Name: "echo",
|
|
Handler: func(_ context.Context, args json.RawMessage) (any, error) {
|
|
return string(args), nil
|
|
},
|
|
}
|
|
sk := skill.New("echoer",
|
|
skill.WithInstructions("Echo when asked."),
|
|
skill.WithTools(echo),
|
|
)
|
|
|
|
for _, base := range []string{"Agent one.", "Agent two."} {
|
|
fp := fake.New("fp")
|
|
fp.Enqueue("m", fake.Reply("done"))
|
|
m, _ := fp.Model("m")
|
|
|
|
a := agent.New(m, base)
|
|
a.AddSkill(sk)
|
|
if _, err := a.Run(context.Background(), "hi"); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
|
|
req := fp.Calls()[0].Request
|
|
if !strings.HasPrefix(req.System, base) || !strings.Contains(req.System, "Echo when asked.") {
|
|
t.Errorf("system = %q", req.System)
|
|
}
|
|
if len(req.Tools) != 1 || req.Tools[0].Name != "echo" {
|
|
t.Errorf("tools = %+v", req.Tools)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMultipleSkillsCompose: instructions append in order; toolsets merge.
|
|
func TestMultipleSkillsCompose(t *testing.T) {
|
|
fp := fake.New("fp")
|
|
fp.Enqueue("m", fake.Reply("ok"))
|
|
m, _ := fp.Model("m")
|
|
|
|
a := agent.New(m, "Base.",
|
|
agent.WithSkill(skill.New("one", skill.WithInstructions("First."), skill.WithTools(llm.Tool{Name: "t1"}))),
|
|
agent.WithSkill(skill.New("two", skill.WithInstructions("Second."), skill.WithTools(llm.Tool{Name: "t2"}))),
|
|
)
|
|
if _, err := a.Run(context.Background(), "go"); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
req := fp.Calls()[0].Request
|
|
if req.System != "Base.\n\nFirst.\n\nSecond." {
|
|
t.Errorf("system = %q", req.System)
|
|
}
|
|
if len(req.Tools) != 2 {
|
|
t.Errorf("tools = %+v", req.Tools)
|
|
}
|
|
}
|