package tools import ( "context" "encoding/json" "fmt" "strings" "ollie/skills" ) // SetSkillsRegistry attaches a skills registry to the execute server. // The server uses it to serve skill_list, skill_load, and skill_active. func (e *Server) SetSkillsRegistry(r *skills.Registry) { e.skillsRegistry = r } // ListSkillsTools returns ToolInfo entries for the skill_* built-ins. // These are included alongside the standard tool_* tools. func ListSkillsTools(skillsReg *skills.Registry, sessionID string) []ToolInfo { if skillsReg == nil { return nil } tools := []ToolInfo{ { Name: "skill_list", Description: `List available skill modules with name and description. Usage: {"name": "skillname"} — if name is provided, loads that skill. Otherwise lists all skills with descriptions.`, InputSchema: json.RawMessage(`{ "type": "object", "properties": {} }`), }, { Name: "skill_load", Description: `Load a skill module into the current session's context. Usage: {"name": "skillname"} The skill content (SKILL.md body) is injected into the agent's context and persists across turns.`, InputSchema: json.RawMessage(`{ "type": "object", "required": ["name"], "properties": { "name": {"type": "string", "description": "Skill name to load."} } }`), }, { Name: "skill_active", Description: `List skills currently loaded in this session. Usage: (no arguments) Returns loaded skill names, one per line.`, InputSchema: json.RawMessage(`{ "type": "object", "properties": {} }`), }, } return tools } // dispatchSkillList lists all available skills from the global registry. func dispatchSkillList(ctx context.Context, e *Server, args json.RawMessage) (string, error) { if e.skillsRegistry == nil { return "", fmt.Errorf("skill_list: no skills registry available") } summaries := e.skillsRegistry.Summaries() if len(summaries) == 0 { return "(no skills found)", nil } var out strings.Builder for _, s := range summaries { out.WriteString(s.Name) if s.Description != "" { out.WriteString(" — ") out.WriteString(s.Description) } out.WriteString("\n") } return strings.TrimRight(out.String(), "\n"), nil } // dispatchSkillLoad loads a skill into the current session. func dispatchSkillLoad(ctx context.Context, e *Server, args json.RawMessage) (string, error) { var a struct { Name string `json:"name"` } if err := json.Unmarshal(args, &a); err != nil { return "", fmt.Errorf("skill_load: bad args: %w", err) } if a.Name == "" { return "", fmt.Errorf("skill_load: name is required") } if e.skillsRegistry == nil || e.sessionID == "" { return "", fmt.Errorf("skill_load: no session registry") } if err := e.skillsRegistry.Load(e.sessionID, a.Name); err != nil { return "", fmt.Errorf("skill_load: %w", err) } content, err := e.skillsRegistry.SkillContent(a.Name) if err != nil { return "", fmt.Errorf("skill_load: %w", err) } // Inject skill content into the agent context. // Skill content is sent as a system message so it persists across turns. injectMsg := fmt.Sprintf("[system: loaded skill %s]\n\n%s", a.Name, content) if e.OnInjection != nil { e.OnInjection(injectMsg) } return fmt.Sprintf("loaded: %s (%d bytes)", a.Name, len(content)), nil } // dispatchSkillActive lists skills currently loaded in this session. func dispatchSkillActive(ctx context.Context, e *Server, args json.RawMessage) (string, error) { if e.skillsRegistry == nil || e.sessionID == "" { return "(no skills loaded)", nil } loaded := e.skillsRegistry.Loaded(e.sessionID) if len(loaded) == 0 { return "(no skills loaded)", nil } var out strings.Builder for _, s := range loaded { out.WriteString(s.Name) if s.Description != "" { out.WriteString(" — ") out.WriteString(s.Description) } out.WriteString("\n") } return strings.TrimRight(out.String(), "\n"), nil }