AgenticGoKit is currently in Beta. APIs may change before the stable v1.0 release.
Skip to content

Custom Handlers

Learn how to create custom agent logic with handlers that give you full control over agent behavior while still leveraging LLM, tools, and memory capabilities.


🎯 Overview

Custom handlers in v1beta allow you to:

  • Control Execution Flow - Define exactly how your agent processes input
  • Access Capabilities - Use LLM, tools, and memory within your logic
  • Compose Handlers - Chain, parallelize, and combine handler logic
  • Add Middleware - Apply cross-cutting concerns like logging and retries

🚀 Quick Start

Basic Handler

go
package main

import (
    "context"
    "github.com/agenticgokit/agenticgokit/v1beta"
)

func main() {
    // Define custom handler
    handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
        return "You said: " + input, nil
    }
    
    // Create agent with custom handler
    agent, _ := v1beta.NewBuilder("CustomAgent").
        WithLLM("openai", "gpt-4").
        WithHandler(handler).
        Build()
    
    result, _ := agent.Run(context.Background(), "Hello")
    // Output: "You said: Hello"
}

📋 Handler Signature

HandlerFunc Type

go
type HandlerFunc func(ctx context.Context, input string, capabilities *Capabilities) (string, error)

Parameters:

  • ctx - Context for cancellation and deadlines
  • input - User input string
  • capabilities - Access to LLM, tools, and memory

Returns:

  • string - Agent response
  • error - Error if processing fails

Capabilities Structure

go
type Capabilities struct {
    LLM    func(system, user string) (string, error)
    Tools  ToolManager
    Memory Memory
}

Fields:

  • LLM - Function to call the configured language model
  • Tools - Interface to discover and execute tools
  • Memory - Interface to store and query memory

🎨 Handler Patterns

Pattern 1: LLM-Only Handler

Direct LLM calls with custom prompts:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    systemPrompt := "You are a helpful coding assistant specializing in Go."
    return capabilities.LLM(systemPrompt, input)
}

agent, _ := v1beta.NewBuilder("CodingAssistant").
    WithLLM("openai", "gpt-4").
    WithHandler(handler).
    Build()

Pattern 2: Tool-Augmented Handler

Execute tools based on input analysis:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // Check if input requires calculation
    if strings.Contains(strings.ToLower(input), "calculate") {
        // Execute calculator tool
        result, err := capabilities.Tools.Execute(ctx, "calculator", map[string]interface{}{
            "expression": extractExpression(input),
        })
        if err != nil {
            return "", err
        }
        
        return fmt.Sprintf("The result is: %v", result.Content), nil
    }
    
    // Otherwise use LLM
    return capabilities.LLM("You are a helpful assistant.", input)
}

Pattern 3: Memory-Aware Handler

Store and retrieve context from memory:

go
import "github.com/agenticgokit/agenticgokit/core"

handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    if capabilities.Memory == nil {
        return capabilities.LLM("You are a helpful assistant.", input)
    }
    
    // Query relevant memories
    memories, err := capabilities.Memory.Query(ctx, input, 5)
    if err != nil {
        return "", err
    }
    
    // Build context from memories
    var context strings.Builder
    for _, mem := range memories {
        context.WriteString(fmt.Sprintf("- %s\n", mem.Content))
    }
    
    // Call LLM with memory context
    systemPrompt := fmt.Sprintf("You are a helpful assistant. Context:\n%s", context.String())
    response, err := capabilities.LLM(systemPrompt, input)
    if err != nil {
        return "", err
    }
    
    // Store interaction in memory
    interaction := fmt.Sprintf("User: %s\nAssistant: %s", input, response)
    capabilities.Memory.Store(ctx, interaction)
    
    return response, nil
}

Pattern 4: Multi-Step Processing

Chain multiple processing steps:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // Step 1: Analyze intent
    intent, err := capabilities.LLM(
        "Classify the user's intent: question, command, or statement",
        input,
    )
    if err != nil {
        return "", err
    }
    
    // Step 2: Process based on intent
    var response string
    switch strings.TrimSpace(intent) {
    case "question":
        response, err = handleQuestion(ctx, input, capabilities)
    case "command":
        response, err = handleCommand(ctx, input, capabilities)
    default:
        response, err = handleStatement(ctx, input, capabilities)
    }
    
    return response, err
}

Pattern 5: Hybrid Tool + LLM

Intelligently route between tools and LLM:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // First, ask LLM if tools are needed
    decision, err := capabilities.LLM(
        "You are a routing assistant. Reply ONLY 'TOOL:toolname' if a tool is needed, or 'LLM' otherwise.",
        input,
    )
    if err != nil {
        return "", err
    }
    
    // Route based on decision
    if strings.HasPrefix(decision, "TOOL:") {
        toolName := strings.TrimPrefix(decision, "TOOL:")
        result, err := capabilities.Tools.Execute(ctx, strings.TrimSpace(toolName), map[string]interface{}{
            "query": input,
        })
        if err != nil {
            return "", err
        }
        return fmt.Sprintf("%v", result.Content), nil
    }
    
    // Use LLM for general queries
    return capabilities.LLM("You are a helpful assistant.", input)
}

🔧 Handler Augmentation

v1beta provides pre-built augmentations to enhance handlers:

WithToolAugmentation

Automatically adds tool-calling capability:

go
import "github.com/agenticgokit/agenticgokit/v1beta"

// Base handler
baseHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are a helpful assistant.", input)
}

// Augment with automatic tool discovery and calling
handler := v1beta.WithToolAugmentation(baseHandler)

agent, _ := v1beta.NewBuilder("ToolAgent").
    WithLLM("openai", "gpt-4").
    WithTools(
        v1beta.WithMCP(mcpServers...),
    ).
    WithHandler(handler).
    Build()

WithMemoryAugmentation

Automatically adds memory storage and retrieval:

go
// Base handler
baseHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are a helpful assistant.", input)
}

// Augment with automatic memory integration
handler := v1beta.WithMemoryAugmentation(baseHandler)

agent, _ := v1beta.NewBuilder("MemoryAgent").
    WithLLM("openai", "gpt-4").
    WithMemory(
        v1beta.WithMemoryProvider("memory"),
    ).
    WithHandler(handler).
    Build()

WithRAGAugmentation

Automatically adds RAG knowledge retrieval:

go
// Base handler
baseHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("Answer based on the provided knowledge.", input)
}

// Augment with RAG - retrieves top 5 relevant documents
handler := v1beta.WithRAGAugmentation(baseHandler, "knowledge_base", 5)

agent, _ := v1beta.NewBuilder("RAGAgent").
    WithLLM("openai", "gpt-4").
    WithMemory(
        v1beta.WithMemoryProvider("pgvector"),
        v1beta.WithRAG(4000, 0.3, 0.7),
    ).
    WithHandler(handler).
    Build()

WithLLMAugmentation

Adds retry logic and error handling to LLM calls:

go
baseHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are a helpful assistant.", input)
}

// Augment with retry logic (max 3 retries)
handler := v1beta.WithLLMAugmentation(baseHandler, 3)

🔗 Handler Composition

Combine multiple handlers using composition functions:

Chain

Execute handlers in sequence:

go
import "github.com/agenticgokit/agenticgokit/v1beta"

preprocessHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return strings.ToLower(input), nil
}

processHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are a helpful assistant.", input)
}

postprocessHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return strings.ToUpper(input), nil
}

// Chain handlers: preprocess -> process -> postprocess
handler := v1beta.Chain(preprocessHandler, processHandler, postprocessHandler)

agent, _ := v1beta.NewBuilder("ChainedAgent").
    WithLLM("openai", "gpt-4").
    WithHandler(handler).
    Build()

ParallelHandlers

Run handlers in parallel and combine results:

go
summaryHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("Summarize this in one sentence.", input)
}

keywordsHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("Extract 5 keywords.", input)
}

sentimentHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("Analyze sentiment: positive, negative, or neutral.", input)
}

// Run all handlers in parallel, combine with separator
handler := v1beta.ParallelHandlers("\n---\n", summaryHandler, keywordsHandler, sentimentHandler)

agent, _ := v1beta.NewBuilder("AnalysisAgent").
    WithLLM("openai", "gpt-4").
    WithHandler(handler).
    Build()

Conditional

Execute handler only if condition is met:

go
isQuestion := func(ctx context.Context, input string) bool {
    return strings.HasSuffix(strings.TrimSpace(input), "?")
}

questionHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are an expert at answering questions.", input)
}

// Only run questionHandler if input is a question
handler := v1beta.Conditional(isQuestion, questionHandler)

Fallback

Try primary handler, fall back if it fails:

go
primaryHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // Try using expensive GPT-4
    return capabilities.LLM("You are a helpful assistant.", input)
}

fallbackHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // Fall back to cheaper GPT-3.5
    // In practice, you'd need to switch models here
    return "I'm experiencing high load. Here's a basic response: " + input, nil
}

handler := v1beta.Fallback(primaryHandler, fallbackHandler)

Retry

Add retry logic to handlers:

go
unreliableHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // Handler that might fail occasionally
    return capabilities.LLM("You are a helpful assistant.", input)
}

// Retry up to 3 times with exponential backoff
handler := v1beta.Retry(unreliableHandler, 3)

WithTimeout

Add timeout to handler execution:

go
slowHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are a helpful assistant.", input)
}

// Timeout after 30 seconds
handler := v1beta.WithTimeout(slowHandler, 30*time.Second)

WithLogging

Add logging to handlers:

go
import "log"

businessLogicHandler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    return capabilities.LLM("You are a helpful assistant.", input)
}

// Add logging
handler := v1beta.WithLogging(businessLogicHandler, func(format string, args ...interface{}) {
    log.Printf(format, args...)
})

🎯 Complete Examples

Example 1: Research Assistant

go
package main

import (
    "context"
    "fmt"
    "strings"
    "github.com/agenticgokit/agenticgokit/v1beta"
    "github.com/agenticgokit/agenticgokit/v1beta"
)

func createResearchAssistant() (v1beta.Agent, error) {
    handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
        // Step 1: Analyze if web search is needed
        decision, err := capabilities.LLM(
            "Determine if this query requires web search. Reply 'YES' or 'NO'.",
            input,
        )
        if err != nil {
            return "", err
        }
        
        var context string
        if strings.TrimSpace(decision) == "YES" {
            // Step 2: Execute web search tool
            searchResult, err := capabilities.Tools.Execute(ctx, "web_search", map[string]interface{}{
                "query": input,
            })
            if err == nil {
                context = fmt.Sprintf("Search results: %v", searchResult.Content)
            }
        }
        
        // Step 3: Generate response with context
        systemPrompt := "You are a research assistant. Use the provided context to answer."
        if context != "" {
            systemPrompt += "\n\nContext: " + context
        }
        
        response, err := capabilities.LLM(systemPrompt, input)
        if err != nil {
            return "", err
        }
        
        // Step 4: Store in memory for future reference
        if capabilities.Memory != nil {
            capabilities.Memory.Store(ctx, fmt.Sprintf("Q: %s\nA: %s", input, response))
        }
        
        return response, nil
    }
    
    return v1beta.NewBuilder("ResearchAssistant").
        WithLLM("openai", "gpt-4").
        WithTools(
            v1beta.WithMCP(/* web search server */),
        ).
        WithMemory(
            v1beta.WithMemoryProvider("memory"),
        ).
        WithHandler(handler).
        Build()
}

Example 2: Code Review Agent

go
func createCodeReviewAgent() (v1beta.Agent, error) {
    handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
        // Parallel analysis
        analysisHandlers := v1beta.ParallelHandlers("\n\n",
            // Security analysis
            func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
                return capabilities.LLM(
                    "Analyze this code for security vulnerabilities.",
                    input,
                )
            },
            // Performance analysis
            func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
                return capabilities.LLM(
                    "Analyze this code for performance issues.",
                    input,
                )
            },
            // Best practices
            func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
                return capabilities.LLM(
                    "Review this code for best practices and style.",
                    input,
                )
            },
        )
        
        // Run parallel analysis with timeout and retry
        robustHandler := v1beta.WithTimeout(
            v1beta.Retry(analysisHandlers, 2),
            60*time.Second,
        )
        
        return robustHandler(ctx, input, capabilities)
    }
    
    return v1beta.NewBuilder("CodeReviewer").
        WithLLM("openai", "gpt-4").
        WithHandler(handler).
        Build()
}

Example 3: Customer Support Bot

go
func createSupportBot() (v1beta.Agent, error) {
    handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
        // Check memory for previous interactions
        var customerHistory string
        if capabilities.Memory != nil {
            memories, err := capabilities.Memory.Query(ctx, "customer history", 5)
            if err == nil && len(memories) > 0 {
                customerHistory = "Previous interactions:\n"
                for _, mem := range memories {
                    customerHistory += "- " + mem.Content + "\n"
                }
            }
        }
        
        // Check knowledge base for relevant documentation
        var knowledgeContext string
        if capabilities.Memory != nil {
            // Query knowledge base (assuming documents were ingested)
            docs, err := capabilities.Memory.Query(ctx, input, 3)
            if err == nil && len(docs) > 0 {
                knowledgeContext = "Relevant documentation:\n"
                for _, doc := range docs {
                    knowledgeContext += "- " + doc.Content + "\n"
                }
            }
        }
        
        // Build context-aware prompt
        systemPrompt := "You are a helpful customer support agent."
        if customerHistory != "" {
            systemPrompt += "\n\n" + customerHistory
        }
        if knowledgeContext != "" {
            systemPrompt += "\n\n" + knowledgeContext
        }
        
        // Generate response
        response, err := capabilities.LLM(systemPrompt, input)
        if err != nil {
            return "", err
        }
        
        // Store interaction
        if capabilities.Memory != nil {
            capabilities.Memory.Store(ctx, fmt.Sprintf("User: %s\nAgent: %s", input, response))
        }
        
        return response, nil
    }
    
    // Add memory augmentation for automatic context handling
    augmentedHandler := v1beta.WithMemoryAugmentation(handler)
    
    return v1beta.NewBuilder("SupportBot").
        WithLLM("openai", "gpt-4").
        WithMemory(
            v1beta.WithMemoryProvider("pgvector"),
            v1beta.WithRAG(4000, 0.3, 0.7),
            v1beta.WithSessionScoped(),
        ).
        WithHandler(augmentedHandler).
        Build()
}

🎯 Best Practices

1. Error Handling

Always handle errors gracefully:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    response, err := capabilities.LLM("You are a helpful assistant.", input)
    if err != nil {
        // Log error
        log.Printf("LLM error: %v", err)
        
        // Return fallback response
        return "I'm experiencing technical difficulties. Please try again.", nil
    }
    return response, nil
}

2. Context Timeout

Respect context deadlines:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    // Check if context is already cancelled
    select {
    case <-ctx.Done():
        return "", ctx.Err()
    default:
    }
    
    // Perform work...
    return capabilities.LLM("You are a helpful assistant.", input)
}

3. Capability Checks

Verify capabilities before using them:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    if capabilities.Memory == nil {
        return capabilities.LLM("You are a helpful assistant.", input)
    }
    
    // Use memory...
    memories, _ := capabilities.Memory.Query(ctx, input, 3)
    // ...
}

4. Structured Output

Return well-formatted responses:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    response, err := capabilities.LLM("You are a helpful assistant.", input)
    if err != nil {
        return "", err
    }
    
    // Format output consistently
    return fmt.Sprintf("Response: %s\nTimestamp: %s", response, time.Now().Format(time.RFC3339)), nil
}

5. Logging and Observability

Add logging for debugging:

go
handler := func(ctx context.Context, input string, capabilities *v1beta.Capabilities) (string, error) {
    log.Printf("Processing input: %s", input)
    
    start := time.Now()
    response, err := capabilities.LLM("You are a helpful assistant.", input)
    duration := time.Since(start)
    
    if err != nil {
        log.Printf("LLM call failed after %v: %v", duration, err)
        return "", err
    }
    
    log.Printf("LLM call completed in %v", duration)
    return response, nil
}

📚 Next Steps


Ready to integrate tools? Continue to Tool Integration

Released under the Apache 2.0 License.