43ea2e562d
Plan A from docs/superpowers/plans/2026-05-19-post-slm-unlock.md. Small local SLMs (<=16k context) waste ~1500 tokens per turn on the full tool catalogue. Two-stage routing replaces round-1 tools with a single synthetic select_category schema; round-2+ sends only the selected category's real tool schemas plus select_category for re-selection. - internal/tool/category.go: Category type, optional Categorized interface, CategoryOf() with meta fallback. fs.read/fs.ls -> read, fs.write/fs.edit -> write, fs.glob/fs.grep -> search, bash -> exec. - internal/engine/twostage.go: synthetic select_category tool, intercept helper, per-turn selectedCategory state under e.mu. - Engine round 1 forces ToolChoiceRequired so SLMs don't fall back to prose. State resets at the top and end of every runLoop. - Activates automatically on a forced local arm with ContextWindow <=16384, or via [router].force_two_stage TOML key. - Integration test drives a 3-round trip and asserts: round 1 emits exactly one schema (synthetic) with ToolChoiceRequired, round 2 contains only write-category schemas + select_category, real fs.write executes. Invalid-category fallback round-trips back to round-1 mode.
71 lines
1.7 KiB
Go
71 lines
1.7 KiB
Go
package tool
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
type categorizedStub struct {
|
|
stubTool
|
|
cat Category
|
|
}
|
|
|
|
func (c *categorizedStub) Category() Category { return c.cat }
|
|
|
|
func TestCategoryOf_DefaultIsMeta(t *testing.T) {
|
|
plain := &stubTool{name: "plain"}
|
|
if got := CategoryOf(plain); got != CategoryMeta {
|
|
t.Errorf("CategoryOf(plain) = %q, want %q", got, CategoryMeta)
|
|
}
|
|
}
|
|
|
|
func TestCategoryOf_DeclaredCategory(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
cat Category
|
|
}{
|
|
{"read", CategoryRead},
|
|
{"write", CategoryWrite},
|
|
{"search", CategorySearch},
|
|
{"exec", CategoryExec},
|
|
{"meta", CategoryMeta},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := &categorizedStub{stubTool: stubTool{name: tc.name}, cat: tc.cat}
|
|
if got := CategoryOf(s); got != tc.cat {
|
|
t.Errorf("CategoryOf(%s) = %q, want %q", tc.name, got, tc.cat)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCategoryOf_InvalidFallsBackToMeta(t *testing.T) {
|
|
s := &categorizedStub{stubTool: stubTool{name: "bogus"}, cat: Category("not-a-real-category")}
|
|
if got := CategoryOf(s); got != CategoryMeta {
|
|
t.Errorf("CategoryOf(invalid) = %q, want %q", got, CategoryMeta)
|
|
}
|
|
}
|
|
|
|
func TestIsValidCategory(t *testing.T) {
|
|
for _, c := range AllCategories() {
|
|
if !IsValidCategory(c) {
|
|
t.Errorf("IsValidCategory(%q) = false, want true", c)
|
|
}
|
|
}
|
|
if IsValidCategory(Category("")) {
|
|
t.Error("empty category should be invalid")
|
|
}
|
|
if IsValidCategory(Category("nope")) {
|
|
t.Error("unknown category should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestAllCategoriesStable(t *testing.T) {
|
|
want := []Category{CategoryRead, CategoryWrite, CategorySearch, CategoryExec, CategoryMeta}
|
|
got := AllCategories()
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("AllCategories() = %v, want %v", got, want)
|
|
}
|
|
}
|