Skip to main content

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:

CallbackInvoked WhenParameters
OnStepStartStep beginsstepNum int
OnStepFinishStep completesstep types.StepResult
OnToolCallTool is calledtoolCall types.ToolCall
OnToolResultTool execution completestoolResult types.ToolResult
OnFinishAgent completesresult *AgentResult

LangChain-Style Callbacks (v6.0.60+)​

These callbacks provide fine-grained control and align with LangChain's callback system:

Chain Lifecycle​

CallbackInvoked WhenParameters
OnChainStartAgent begins executioninput string, messages []types.Message
OnChainEndAgent completes successfullyresult *AgentResult
OnChainErrorAgent encounters an errorerr error

Agent Decisions​

CallbackInvoked WhenParameters
OnAgentActionAgent decides to take an actionaction AgentAction
OnAgentFinishAgent reaches a final answerfinish AgentFinish

Tool Lifecycle​

CallbackInvoked WhenParameters
OnToolStartTool execution beginstoolCall types.ToolCall
OnToolEndTool completes successfullytoolResult types.ToolResult
OnToolErrorTool execution failstoolCall 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 chain
  • ParentRunID: 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​


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