Agent Skills
Agent skills are reusable behaviors that can be registered with agents to extend their capabilities. Skills provide a clean way to encapsulate common operations and share them across multiple agents.
Overview
Skills in the Go-AI SDK allow you to:
- Encapsulate reusable behaviors: Define skills once and use them across multiple agents
- Extend agent capabilities: Add custom functionality beyond what tools provide
- Organize agent logic: Group related behaviors into named skills
- Share instructions: Include guidance on when and how to use each skill
Basic Usage
Creating a Skill
skill := &agent.Skill{
Name: "weather",
Description: "Get weather information for a location",
Instructions: "Use this skill when the user asks about weather or climate",
Handler: func(ctx context.Context, input string) (string, error) {
// Implement weather fetching logic
return fmt.Sprintf("Weather for %s: Sunny, 72°F", input), nil
},
}
Adding Skills to an Agent
agentInstance := agent.NewToolLoopAgent(config)
if err := agentInstance.AddSkill(skill); err != nil {
log.Fatalf("Failed to add skill: %v", err)
}
Executing Skills
result, err := agentInstance.ExecuteSkill(ctx, "weather", "San Francisco")
if err != nil {
log.Fatalf("Skill execution failed: %v", err)
}
fmt.Println(result) // "Weather for San Francisco: Sunny, 72°F"
Skill Structure
Required Fields
- Name (string): Unique identifier for the skill
- Description (string): Brief description of what the skill does
- Handler (SkillHandler): Function that executes the skill logic
Optional Fields
- Instructions (string): Guidance for agents on when to use the skill
- Metadata (map[string]interface{}): Additional information about the skill
Skill Registry
The SkillRegistry manages a collection of skills and provides methods for registration, retrieval, and execution.
Creating a Registry
registry := agent.NewSkillRegistry()
Registry Operations
// Register a skill
err := registry.Register(skill)
// Check if a skill exists
exists := registry.Has("weather")
// Get a skill
skill, found := registry.Get("weather")
// Execute a skill
result, err := registry.Execute(ctx, "weather", "New York")
// List all skills
skills := registry.List()
// Get skill names
names := registry.Names()
// Remove a skill
registry.Unregister("weather")
// Count skills
count := registry.Count()
// Clear all skills
registry.Clear()
Complete Example
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/digitallysavvy/go-ai/pkg/agent"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
func main() {
// Create agent
openaiProvider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := openaiProvider.LanguageModel("gpt-4o-mini")
config := agent.AgentConfig{
Model: model,
System: "You are a helpful assistant with text processing skills.",
MaxSteps: 5,
}
agentInstance := agent.NewToolLoopAgent(config)
// Define skills
uppercaseSkill := &agent.Skill{
Name: "uppercase",
Description: "Converts text to uppercase",
Instructions: "Use when the user wants text in all caps",
Handler: func(ctx context.Context, input string) (string, error) {
return strings.ToUpper(input), nil
},
}
wordCountSkill := &agent.Skill{
Name: "word_count",
Description: "Counts words in text",
Instructions: "Use when the user wants to know word count",
Handler: func(ctx context.Context, input string) (string, error) {
words := strings.Fields(input)
return fmt.Sprintf("Word count: %d", len(words)), nil
},
Metadata: map[string]interface{}{
"category": "text-analysis",
"version": "1.0",
},
}
// Add skills
agentInstance.AddSkill(uppercaseSkill)
agentInstance.AddSkill(wordCountSkill)
// Execute skills
result1, _ := agentInstance.ExecuteSkill(context.Background(), "uppercase", "hello world")
fmt.Println(result1) // "HELLO WORLD"
result2, _ := agentInstance.ExecuteSkill(context.Background(), "word_count", "hello world")
fmt.Println(result2) // "Word count: 2"
// List skills
for _, skill := range agentInstance.ListSkills() {
fmt.Printf("Skill: %s - %s\n", skill.Name, skill.Description)
}
}
Best Practices
1. Choose Clear Names
Use descriptive, action-oriented names for skills:
// Good
"format_text"
"analyze_sentiment"
"extract_keywords"
// Avoid
"skill1"
"helper"
"util"
2. Provide Detailed Instructions
Help agents understand when to use each skill:
skill := &agent.Skill{
Name: "summarize",
Description: "Creates a concise summary of text",
Instructions: "Use this skill when the user asks for a summary, synopsis, or brief overview of long text. Works best with text over 100 words.",
Handler: summarizeHandler,
}
3. Handle Errors Gracefully
Return descriptive errors from skill handlers:
Handler: func(ctx context.Context, input string) (string, error) {
if input == "" {
return "", fmt.Errorf("input cannot be empty")
}
result, err := processData(input)
if err != nil {
return "", fmt.Errorf("processing failed: %w", err)
}
return result, nil
}
4. Use Metadata for Organization
Add metadata to categorize and version skills:
skill := &agent.Skill{
Name: "analyze_sentiment",
Description: "Analyzes text sentiment",
Handler: sentimentHandler,
Metadata: map[string]interface{}{
"category": "text-analysis",
"version": "1.0.0",
"author": "team-ai",
"model": "sentiment-v2",
},
}
5. Keep Skills Focused
Each skill should do one thing well:
// Good: Focused skills
formatSkill // Just formats text
validateSkill // Just validates
transformSkill // Just transforms
// Avoid: Overly complex skills
formatAndValidateAndTransformSkill
Skills vs Tools
When to Use Skills
- Reusable agent behaviors: When multiple agents need the same capability
- Simple operations: When the operation doesn't need LLM-style tool calling
- Organizational clarity: When you want to group related agent behaviors
When to Use Tools
- LLM-driven operations: When the LLM should decide when to use it
- Complex parameters: When you need structured input schemas
- Tool calling: When using the model's native tool-calling capabilities
Skills + Tools
You can use both together:
// Tool for LLM to call
weatherTool := types.Tool{
Name: "get_weather",
Description: "Get current weather",
Execute: func(ctx context.Context, args map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
location := args["location"].(string)
// Fetch weather...
return weatherData, nil
},
}
// Skill for direct agent use
weatherSkill := &agent.Skill{
Name: "format_weather",
Description: "Formats weather data for display",
Handler: func(ctx context.Context, input string) (string, error) {
// Format weather...
return formatted, nil
},
}
config := agent.AgentConfig{
Tools: []types.Tool{weatherTool},
Skills: registry,
}
Advanced Patterns
Skill Composition
Combine multiple skills:
compositeSkill := &agent.Skill{
Name: "process_and_analyze",
Handler: func(ctx context.Context, input string) (string, error) {
// Use other skills
processed, err := agentInstance.ExecuteSkill(ctx, "process", input)
if err != nil {
return "", err
}
analyzed, err := agentInstance.ExecuteSkill(ctx, "analyze", processed)
return analyzed, err
},
}
Conditional Skills
Add/remove skills dynamically:
if userHasPremium {
agentInstance.AddSkill(premiumSkill)
}
if !userNeedsBasicFeatures {
agentInstance.RemoveSkill("basic_feature")
}
Skill Middleware
Wrap skill execution with common logic:
func withLogging(skill *agent.Skill) *agent.Skill {
originalHandler := skill.Handler
skill.Handler = func(ctx context.Context, input string) (string, error) {
log.Printf("Executing skill: %s with input: %s", skill.Name, input)
result, err := originalHandler(ctx, input)
if err != nil {
log.Printf("Skill %s failed: %v", skill.Name, err)
} else {
log.Printf("Skill %s succeeded", skill.Name)
}
return result, err
}
return skill
}
// Usage
agentInstance.AddSkill(withLogging(mySkill))
See Also
- Agent Subagents - Hierarchical agent delegation
- Tool Loop Agent - Core agent implementation
- Agent Configuration - Configuring agents
Examples
See the agent-skills example for a complete working example.