Agent Callbacks
Callbacks provide hooks into agent execution, allowing you to monitor progress, log activities, handle errors, and integrate with external systems. The Go-AI SDK supports both legacy callbacks and LangChain-style callbacks for enhanced interoperability.
Overview
Agent callbacks enable you to:
- Monitor execution: Track agent progress and decisions
- Log activities: Record tool calls, actions, and results
- Handle errors: Implement custom error recovery logic
- Measure performance: Track token usage and execution time
- Integrate with observability: Connect to logging and monitoring platforms
- Debug agent behavior: Understand decision-making processes
Callback Types
Legacy Callbacks
These callbacks provide basic step-level tracking:
| Callback | Invoked When | Parameters |
|---|---|---|
OnStepStart | Step begins | stepNum int |
OnStepFinish | Step completes | step types.StepResult |
OnToolCall | Tool is called | toolCall types.ToolCall |
OnToolResult | Tool execution completes | toolResult types.ToolResult |
OnFinish | Agent completes | result *AgentResult |
LangChain-Style Callbacks (v6.0.60+)
These callbacks provide fine-grained control and align with LangChain's callback system:
Chain Lifecycle
| Callback | Invoked When | Parameters |
|---|---|---|
OnChainStart | Agent begins execution | input string, messages []types.Message |
OnChainEnd | Agent completes successfully | result *AgentResult |
OnChainError | Agent encounters an error | err error |
Agent Decisions
| Callback | Invoked When | Parameters |
|---|---|---|
OnAgentAction | Agent decides to take an action | action AgentAction |
OnAgentFinish | Agent reaches a final answer | finish AgentFinish |
Tool Lifecycle
| Callback | Invoked When | Parameters |
|---|---|---|
OnToolStart | Tool execution begins | toolCall types.ToolCall |
OnToolEnd | Tool completes successfully | toolResult types.ToolResult |
OnToolError | Tool execution fails | toolCall types.ToolCall, err error |
Basic Usage
import (
"context"
"log"
"github.com/digitallysavvy/go-ai/pkg/agent"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
)
myAgent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
System: "You are a helpful assistant.",
Tools: tools,
// Legacy callbacks
OnStepStart: func(stepNum int) {
log.Printf("Starting step %d", stepNum)
},
OnStepFinish: func(step types.StepResult) {
log.Printf("Step %d finished: %s", step.StepNumber, step.FinishReason)
},
// LangChain-style callbacks
OnChainStart: func(input string, messages []types.Message) {
log.Printf("Agent starting with input: %s", input)
},
OnChainEnd: func(result *agent.AgentResult) {
log.Printf("Agent completed in %d steps", len(result.Steps))
},
OnChainError: func(err error) {
log.Printf("Agent error: %v", err)
},
})
Common Use Cases
Logging Agent Execution
Track the complete execution flow:
import (
"fmt"
"time"
)
var startTime time.Time
config := agent.AgentConfig{
Model: model,
Tools: tools,
OnChainStart: func(input string, messages []types.Message) {
startTime = time.Now()
fmt.Printf("\n=== Agent Started ===\n")
fmt.Printf("Input: %s\n", input)
},
OnAgentAction: func(action agent.AgentAction) {
fmt.Printf("\n[Step %d] Agent Action\n", action.StepNumber)
fmt.Printf(" Tool: %s\n", action.ToolCall.ToolName)
if action.Reasoning != "" {
fmt.Printf(" Reasoning: %s\n", action.Reasoning)
}
},
OnAgentFinish: func(finish agent.AgentFinish) {
fmt.Printf("\n[Step %d] Agent Finished\n", finish.StepNumber)
fmt.Printf(" Output: %s\n", finish.Output)
fmt.Printf(" Reason: %s\n", finish.FinishReason)
},
OnChainEnd: func(result *agent.AgentResult) {
duration := time.Since(startTime)
fmt.Printf("\n=== Agent Completed ===\n")
fmt.Printf("Duration: %s\n", duration)
fmt.Printf("Steps: %d\n", len(result.Steps))
fmt.Printf("Tool calls: %d\n", len(result.ToolResults))
fmt.Printf("Total tokens: %d\n", result.Usage.GetTotalTokens())
},
}
Tracking Token Usage and Costs
Monitor API usage for cost tracking:
type UsageTracker struct {
totalTokens int64
totalCost float64
toolCallCount int
costPerToken float64 // Cost per 1000 tokens
}
func (t *UsageTracker) addUsage(tokens int64) {
t.totalTokens += tokens
t.totalCost += float64(tokens) * t.costPerToken / 1000.0
}
tracker := &UsageTracker{
costPerToken: 0.002, // $0.002 per 1K tokens (example rate)
}
config := agent.AgentConfig{
Model: model,
Tools: tools,
OnStepFinish: func(step types.StepResult) {
if step.Usage.GetTotalTokens() > 0 {
tracker.addUsage(step.Usage.GetTotalTokens())
}
},
OnToolEnd: func(toolResult types.ToolResult) {
tracker.toolCallCount++
},
OnChainEnd: func(result *agent.AgentResult) {
fmt.Printf("\n=== Usage Summary ===\n")
fmt.Printf("Total tokens: %d\n", tracker.totalTokens)
fmt.Printf("Estimated cost: $%.4f\n", tracker.totalCost)
fmt.Printf("Tool calls: %d\n", tracker.toolCallCount)
fmt.Printf("Average tokens per step: %d\n",
tracker.totalTokens/int64(len(result.Steps)))
},
}
Error Handling and Recovery
Implement custom error handling:
config := agent.AgentConfig{
Model: model,
Tools: tools,
OnChainError: func(err error) {
log.Printf("Chain error occurred: %v", err)
// Send alert
sendAlert("Agent execution failed", err.Error())
// Log to external service
logToObservability("agent.error", map[string]interface{}{
"error": err.Error(),
"timestamp": time.Now(),
})
},
OnToolError: func(toolCall types.ToolCall, err error) {
log.Printf("Tool %s failed: %v", toolCall.ToolName, err)
// Track tool failures
metrics.IncrementCounter("tool.failures", map[string]string{
"tool_name": toolCall.ToolName,
})
// Implement retry logic elsewhere if needed
// (Note: callbacks should not modify execution flow directly)
},
}
Performance Monitoring
Track execution time for each component:
type PerformanceMonitor struct {
toolTimes map[string]time.Duration
toolStarts map[string]time.Time
stepTimes []time.Duration
stepStart time.Time
}
func NewPerformanceMonitor() *PerformanceMonitor {
return &PerformanceMonitor{
toolTimes: make(map[string]time.Duration),
toolStarts: make(map[string]time.Time),
stepTimes: make([]time.Duration, 0),
}
}
monitor := NewPerformanceMonitor()
config := agent.AgentConfig{
Model: model,
Tools: tools,
OnStepStart: func(stepNum int) {
monitor.stepStart = time.Now()
},
OnStepFinish: func(step types.StepResult) {
duration := time.Since(monitor.stepStart)
monitor.stepTimes = append(monitor.stepTimes, duration)
},
OnToolStart: func(toolCall types.ToolCall) {
monitor.toolStarts[toolCall.ID] = time.Now()
},
OnToolEnd: func(toolResult types.ToolResult) {
if startTime, ok := monitor.toolStarts[toolResult.ToolCallID]; ok {
duration := time.Since(startTime)
monitor.toolTimes[toolResult.ToolName] += duration
delete(monitor.toolStarts, toolResult.ToolCallID)
}
},
OnChainEnd: func(result *agent.AgentResult) {
fmt.Printf("\n=== Performance Report ===\n")
// Step timings
var totalStepTime time.Duration
for i, duration := range monitor.stepTimes {
fmt.Printf("Step %d: %s\n", i+1, duration)
totalStepTime += duration
}
fmt.Printf("Average step time: %s\n",
totalStepTime/time.Duration(len(monitor.stepTimes)))
// Tool timings
fmt.Printf("\nTool execution times:\n")
for toolName, duration := range monitor.toolTimes {
fmt.Printf(" %s: %s\n", toolName, duration)
}
},
}
Integration with Observability Platforms
Connect to external monitoring systems:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
tracer := otel.Tracer("agent-execution")
config := agent.AgentConfig{
Model: model,
Tools: tools,
OnChainStart: func(input string, messages []types.Message) {
ctx, span := tracer.Start(context.Background(), "agent.execute")
span.SetAttributes(attribute.String("input", input))
// Store ctx for later use
},
OnAgentAction: func(action agent.AgentAction) {
_, span := tracer.Start(context.Background(), "agent.action")
span.SetAttributes(
attribute.String("tool", action.ToolCall.ToolName),
attribute.Int("step", action.StepNumber),
)
defer span.End()
},
OnToolStart: func(toolCall types.ToolCall) {
_, span := tracer.Start(context.Background(), "tool.execute")
span.SetAttributes(attribute.String("tool", toolCall.ToolName))
// Store span for completion in OnToolEnd
},
OnChainEnd: func(result *agent.AgentResult) {
// Complete the main span
// span.SetAttributes(...)
// span.End()
},
}
Data Types
AgentAction
Represents an action the agent has decided to take:
type AgentAction struct {
ToolCall types.ToolCall // The tool being called
StepNumber int // Step when action was decided
Reasoning string // Agent's reasoning for the action
RunID string // Unique identifier for this agent execution chain
ParentRunID string // ID of the parent run (empty for top-level runs)
Tags []string // User-defined labels for categorizing runs
}
AgentFinish
Represents the agent's final decision:
type AgentFinish struct {
Output string // Final output text
StepNumber int // Step when agent finished
FinishReason types.FinishReason // Why the agent finished
Metadata map[string]interface{} // Additional metadata
RunID string // Unique identifier for this agent execution chain
ParentRunID string // ID of the parent run (empty for top-level runs)
Tags []string // User-defined labels for categorizing runs
}
Best Practices
1. Keep Callbacks Fast
Callbacks are invoked during execution. Long-running operations can slow down the agent:
// ❌ Bad: Slow synchronous operation
OnToolEnd: func(toolResult types.ToolResult) {
saveToDatabaseSync(toolResult) // Blocks execution
},
// ✅ Good: Async logging
OnToolEnd: func(toolResult types.ToolResult) {
go saveToDatabaseAsync(toolResult) // Non-blocking
},
2. Handle Errors in Callbacks
Panics in callbacks can crash your application:
OnChainEnd: func(result *agent.AgentResult) {
defer func() {
if r := recover(); r != nil {
log.Printf("Callback panic: %v", r)
}
}()
// Your callback code
},
3. Don't Modify Agent State
Callbacks should observe, not modify:
// ❌ Bad: Trying to modify execution
OnAgentAction: func(action agent.AgentAction) {
// Don't try to cancel or modify the action
},
// ✅ Good: Just observe and log
OnAgentAction: func(action agent.AgentAction) {
log.Printf("Agent is calling: %s", action.ToolCall.ToolName)
},
4. Use Appropriate Callback Level
Choose between legacy and LangChain-style callbacks based on your needs:
- Legacy callbacks: Simple step tracking, basic monitoring
- LangChain-style callbacks: Detailed lifecycle management, LangChain compatibility, fine-grained control
5. Thread Safety
If callbacks access shared state, ensure thread safety:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
counter := &SafeCounter{}
config := agent.AgentConfig{
OnToolEnd: func(toolResult types.ToolResult) {
counter.Increment() // Thread-safe
},
}
Combining Callbacks
You can use both legacy and LangChain-style callbacks together:
config := agent.AgentConfig{
Model: model,
Tools: tools,
// Legacy - for backward compatibility
OnStepFinish: func(step types.StepResult) {
// Your existing logic
},
// LangChain-style - for new features
OnChainStart: func(input string, messages []types.Message) {
// Enhanced tracking
},
OnAgentAction: func(action agent.AgentAction) {
// Fine-grained action monitoring
},
}
Run Tracking (v6.0.61+)
Run tracking enables you to correlate all callbacks, actions, and events from a single agent execution using unique identifiers. This is particularly useful for observability, debugging, and distributed tracing.
Run Tracking Fields
Each AgentAction and AgentFinish includes:
RunID: Unique identifier for this agent execution chainParentRunID: ID of the parent run (for subagents/nested executions)Tags: User-defined labels for categorizing runs
Automatic RunID Generation
Run IDs are automatically generated when an agent starts:
agent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
OnAgentAction: func(action agent.AgentAction) {
// RunID is automatically populated
log.Printf("Action in run %s: %s", action.RunID, action.ToolCall.ToolName)
},
})
result, err := agent.Execute(context.Background(), "question")
// RunID is automatically generated and propagated
Custom Run IDs and Tags
Provide your own run ID and tags using context helpers:
import "github.com/digitallysavvy/go-ai/pkg/agent"
ctx := context.Background()
// Option 1: Add tags only (RunID auto-generated)
ctx = agent.WithTags(ctx, []string{"production", "user:123", "session:abc"})
// Option 2: Custom RunID
ctx = agent.WithRunID(ctx, "my-custom-run-id-123")
ctx = agent.WithTags(ctx, []string{"experiment", "variant:A"})
// Option 3: Nested execution (subagent tracking)
parentCtx := agent.WithRunID(ctx, "parent-run-456")
subagentCtx := agent.WithParentRunID(parentCtx, "parent-run-456")
subagentCtx = agent.WithRunID(subagentCtx, "subagent-run-789")
result, err := agent.Execute(ctx, "question")
Retrieving Run Tracking Info
Extract run tracking information from context:
runID := agent.GetRunID(ctx)
parentRunID := agent.GetParentRunID(ctx)
tags := agent.GetTags(ctx)
log.Printf("Executing run %s with tags: %v", runID, tags)
Use Cases for Run Tracking
1. Distributed Tracing
Correlate agent execution with other services:
import "go.opentelemetry.io/otel/trace"
// Get trace ID from OpenTelemetry
span := trace.SpanFromContext(ctx)
traceID := span.SpanContext().TraceID().String()
// Use trace ID as run ID for correlation
ctx = agent.WithRunID(ctx, traceID)
ctx = agent.WithTags(ctx, []string{"service:agent", "env:production"})
config := agent.AgentConfig{
Model: model,
OnAgentAction: func(action agent.AgentAction) {
// Log with trace ID for correlation
log.Printf("[trace=%s] Agent action: %s",
action.RunID, action.ToolCall.ToolName)
},
}
2. Multi-Tenant Observability
Track executions by user or session:
// Tag runs with user/session info
ctx = agent.WithTags(ctx, []string{
fmt.Sprintf("user:%s", userID),
fmt.Sprintf("session:%s", sessionID),
"plan:premium",
})
config := agent.AgentConfig{
OnAgentFinish: func(finish agent.AgentFinish) {
// Store metrics tagged by user
metrics.RecordAgentCompletion(
finish.RunID,
finish.Tags,
len(finish.Metadata),
)
},
}
3. A/B Testing and Experiments
Track different agent configurations:
experimentID := "exp-123-variant-A"
ctx = agent.WithRunID(ctx, experimentID)
ctx = agent.WithTags(ctx, []string{
"experiment:prompt-variations",
"variant:A",
"temperature:0.7",
})
config := agent.AgentConfig{
OnChainEnd: func(result *agent.AgentResult) {
// Log results for experiment analysis
experimentTracker.LogResult(experimentID, result)
},
}
4. Subagent Hierarchies
Track parent-child relationships in delegations:
func executeWithSubagent(parentCtx context.Context) {
parentRunID := agent.GetRunID(parentCtx)
// Create subagent context with parent tracking
subagentCtx := agent.WithParentRunID(context.Background(), parentRunID)
subagentCtx = agent.WithTags(subagentCtx, []string{"type:subagent", "role:researcher"})
subagent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
OnChainStart: func(input string, messages []types.Message) {
runID := agent.GetRunID(subagentCtx)
log.Printf("Subagent %s starting (parent: %s)", runID, parentRunID)
},
})
result, err := subagent.Execute(subagentCtx, "research question")
}
5. Performance Analysis
Group and analyze runs by tags:
ctx = agent.WithTags(ctx, []string{
"tool:search",
"complexity:high",
"priority:p0",
})
config := agent.AgentConfig{
OnChainEnd: func(result *agent.AgentResult) {
// Store metrics for later analysis
analytics.RecordExecution(analytics.Execution{
RunID: agent.GetRunID(ctx),
Tags: agent.GetTags(ctx),
Duration: time.Since(startTime),
TokensUsed: result.Usage.GetTotalTokens(),
StepCount: len(result.Steps),
})
},
}
// Later: Query executions by tags
highComplexityRuns := analytics.FindByTag("complexity:high")
Examples
See complete examples in examples/agents/callbacks/:
- onstepfinish: Basic step tracking with OnStepFinish
- early-stopping: Token limit monitoring with callbacks
- langchain-style: Comprehensive example of all LangChain-style callbacks (includes run tracking demo)
Next Steps
- Explore Agent Workflows for complex agent patterns
- Learn about Loop Control for execution management
- See Agent Configuration for more options
Introduced in v6.0.60: LangChain-style callbacks (OnChainStart, OnChainEnd, OnChainError, OnAgentAction, OnAgentFinish, OnToolStart, OnToolEnd, OnToolError)
Introduced in v6.0.61: Run tracking with RunID, ParentRunID, and Tags fields in AgentAction and AgentFinish