package tools import ( "context" "encoding/json" "fmt" "sort" "strings" "codey/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 }