Files
vikingowl 43ea2e562d feat(engine): two-stage tool routing for small local arms
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.
2026-05-19 20:53:21 +02:00

53 lines
1.6 KiB
Go

package tool
// Category groups tools by what they do. Used by the two-stage tool routing
// path for small local models: round 1 picks a category, round 2 only sees
// schemas in that category.
type Category string
const (
// CategoryRead — read filesystem state (fs.read, fs.ls).
CategoryRead Category = "read"
// CategoryWrite — modify filesystem state (fs.write, fs.edit).
CategoryWrite Category = "write"
// CategorySearch — search filesystem content (fs.grep, fs.glob).
CategorySearch Category = "search"
// CategoryExec — execute external commands (bash).
CategoryExec Category = "exec"
// CategoryMeta — agent orchestration, introspection, and result handling.
// Default for tools that don't declare a category.
CategoryMeta Category = "meta"
)
// AllCategories returns the canonical category list in stable order.
func AllCategories() []Category {
return []Category{CategoryRead, CategoryWrite, CategorySearch, CategoryExec, CategoryMeta}
}
// IsValidCategory reports whether c is one of the known categories.
func IsValidCategory(c Category) bool {
switch c {
case CategoryRead, CategoryWrite, CategorySearch, CategoryExec, CategoryMeta:
return true
}
return false
}
// Categorized is the optional interface a tool implements to declare its
// category. Tools that don't implement it fall back to CategoryMeta.
type Categorized interface {
Category() Category
}
// CategoryOf returns the tool's declared category, or CategoryMeta if the
// tool does not implement Categorized.
func CategoryOf(t Tool) Category {
if c, ok := t.(Categorized); ok {
cat := c.Category()
if IsValidCategory(cat) {
return cat
}
}
return CategoryMeta
}