Vectrum
Sign In
98 lines · 3.1 KB · Go
package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"harness/internal/skills"
)

// SkillArgs is the skill input. The name is constrained to the discovered
// slugs by an enum patched into the schema, so a hallucinated name is a schema
// violation the model sees at once rather than a lookup failure it has to
// reason about.
type SkillArgs struct {
	Name string `json:"name" jsonschema:"description=Which skill to load"`
	Args string `json:"args,omitempty" jsonschema:"description=Arguments for the skill; whatever followed the skill name"`
}

// NewSkill builds the skill tool over the skills discovered at startup. It
// returns nil when there are none, so the caller registers nothing and the
// prompt is byte-identical to a harness without the feature.
func NewSkill(found []skills.Skill) Tool {
	if len(found) == 0 {
		return nil
	}
	byName := make(map[string]skills.Skill, len(found))
	names := make([]string, 0, len(found))
	var listing strings.Builder
	for _, s := range found {
		byName[s.Name] = s
		names = append(names, s.Name)
		fmt.Fprintf(&listing, "\n  %s: %s", s.Name, s.Description)
	}
	sort.Strings(names)

	desc := strings.TrimSpace(`
Load a skill: a set of instructions for one kind of work. Invoke a skill when
its description matches what you are about to do — before starting, not after.
The instructions come back as the tool result; follow them.

Available skills:`) + listing.String()

	t := New("skill", desc, func(_ context.Context, in SkillArgs) (string, error) {
		s, ok := byName[in.Name]
		if !ok {
			return "", fmt.Errorf("no skill named %q; available: %s", in.Name, strings.Join(names, ", "))
		}
		var b strings.Builder
		fmt.Fprintf(&b, "Skill: %s\nDirectory: %s\n", s.Name, s.Dir)
		// Not "relative paths resolve against that directory" — read_file
		// anchors a relative path to the working root, so promising
		// otherwise sends the model into a "no such file" it cannot diagnose.
		b.WriteString("Join relative paths onto the directory above before reading them.\n\n")
		b.WriteString(s.Body)
		if a := strings.TrimSpace(in.Args); a != "" {
			b.WriteString("\n\nARGUMENTS: " + a)
		}
		return b.String(), nil
	})
	return withEnum(t, "name", names)
}

// withEnum patches an enum onto one string property of a reflected schema.
// invopop/jsonschema derives the schema from struct tags, which cannot carry a
// list only known at startup.
func withEnum(t Tool, property string, values []string) Tool {
	var m map[string]any
	if err := json.Unmarshal(t.InputSchema(), &m); err != nil {
		return t
	}
	props, ok := m["properties"].(map[string]any)
	if !ok {
		return t
	}
	p, ok := props[property].(map[string]any)
	if !ok {
		return t
	}
	p["enum"] = values
	patched, err := json.Marshal(m)
	if err != nil {
		return t
	}
	return patchedSchema{Tool: t, schema: patched}
}

// patchedSchema wraps a Tool to override only its input schema.
type patchedSchema struct {
	Tool
	schema json.RawMessage
}

// InputSchema returns the schema with the patched enum, overriding the
// embedded Tool's reflected one.
func (p patchedSchema) InputSchema() json.RawMessage { return p.schema }