package agent import ( "encoding/json" "os" "path/filepath" "strings" "sync" "ollie/embedding" "ollie/skills" "ollie/util" ) // Skill matching configuration. const ( // skillMatchThreshold is the minimum cosine similarity to include a skill. skillMatchThreshold = 0.35 // skillMatchLimit is the maximum number of skills to inject per turn. skillMatchLimit = 3 // toolMatchThreshold is the minimum cosine similarity to include a tool hint. toolMatchThreshold = 0.35 // toolMatchLimit is the maximum number of tool hints to inject per turn. toolMatchLimit = 5 ) var ( skillIndex *skills.Index skillInitErr error skillOnce sync.Once toolIndex *skills.Index toolInitErr error toolOnce sync.Once ) // matchSkills finds skills relevant to the user's input and returns // their content formatted for injection. Initializes the skill index // on first call. func matchSkills(input string) string { skillOnce.Do(func() { idx, err := skills.NewIndex(skills.DefaultModelDir(), skills.DefaultSkillDirs()) if err != nil { skillInitErr = err return } skillIndex = idx }) if skillIndex == nil { return "" } results, err := skillIndex.Match(input, skillMatchThreshold, skillMatchLimit) if err != nil { return "" } if len(results) == 0 { return "" } var sb strings.Builder sb.WriteString("\n") for _, r := range results { sb.WriteString("\n") // Include full skill content (already includes frontmatter) sb.WriteString(r.Skill.Content) if !strings.HasSuffix(r.Skill.Content, "\n") { sb.WriteString("\n") } sb.WriteString("\n") } sb.WriteString("\n") return sb.String() } // matchTools finds tools relevant to the user's input and returns // a hint block telling the model to call them. func matchTools(input string) string { toolOnce.Do(func() { idx, err := newToolIndex() if err != nil { toolInitErr = err return } toolIndex = idx }) if toolIndex == nil { return "" } results, err := toolIndex.Match(input, toolMatchThreshold, toolMatchLimit) if err != nil { return "" } if len(results) == 0 { return "" } var sb strings.Builder sb.WriteString("\n") sb.WriteString("Relevant tools for this request:\n\n") for _, r := range results { sb.WriteString("→ `") sb.WriteString(r.Skill.Name) sb.WriteString("({...})` — ") sb.WriteString(r.Skill.Description) sb.WriteString("\n") } sb.WriteString("\nTo make one callable, use client_9p to load it through the agent ctl file:\n") sb.WriteString("client_9p(op=\"rdwr\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl\", data=\"tool_load \")\n") sb.WriteString("Then call the loaded tool.\n") sb.WriteString("\n") return sb.String() } func formatScore(s float32) string { // Format as percentage pct := int(s * 100) if pct > 99 { pct = 99 } return string([]byte{'0' + byte(pct/10), '0' + byte(pct%10), '%'}) } // newToolIndex creates a skill-compatible index from tool .meta files. func newToolIndex() (*skills.Index, error) { model, err := embedding.LoadModel(skills.DefaultModelDir()) if err != nil { return nil, err } toolsDir := filepath.Join(util.CfgDir(), "tools") entries, err := os.ReadDir(toolsDir) if err != nil { model.Close() return nil, err } var toolSkills []skills.Skill for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".meta") { continue } metaPath := filepath.Join(toolsDir, entry.Name()) data, err := os.ReadFile(metaPath) if err != nil { continue } var meta struct { Description string `json:"description"` } if json.Unmarshal(data, &meta) != nil || meta.Description == "" { continue } toolName := strings.TrimSuffix(entry.Name(), ".meta") toolSkills = append(toolSkills, skills.Skill{ Name: toolName, Description: meta.Description, }) } return skills.NewIndexFromSkills(model, toolSkills) }