Skip to main content

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​

Examples​

See the agent-skills example for a complete working example.