Agent Callbacks
The Go AI SDK provides comprehensive callback support for monitoring and controlling agent execution. Callbacks allow you to track progress, log intermediate results, monitor resource usage, and build real-time UI updates.
OnStepFinish Callback
The OnStepFinish callback is triggered after each step in a multi-step agent execution. This is particularly useful for:
- Progress tracking: Monitor how many steps have been completed
- Token usage monitoring: Track costs in real-time
- Debugging: Inspect tool calls and results at each step
- UI updates: Build progress indicators and status displays
- Logging: Record detailed execution traces
Signature
OnStepFinish func(step types.StepResult)
StepResult Structure
The StepResult passed to the callback contains complete information about the step:
type StepResult struct {
// Step number (0-indexed)
StepNumber int
// Text generated in this step
Text string
// Tool calls made in this step
ToolCalls []ToolCall
// Tool results from this step
ToolResults []ToolResult
// Finish reason for this step
FinishReason FinishReason
// Raw finish reason from the provider
RawFinishReason string
// Usage for this step (token counts)
Usage Usage
// Context management information (Anthropic-specific)
ContextManagement interface{}
// Warnings from this step
Warnings []Warning
// Response messages generated in this step
// Deprecated: use Response.Messages instead.
ResponseMessages []Message
}
Basic Example
agent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
System: "You are a helpful assistant.",
Tools: tools,
OnStepFinish: func(step types.StepResult) {
fmt.Printf("Step %d complete: %s\n", step.StepNumber, step.Text)
if len(step.ToolCalls) > 0 {
fmt.Printf("Called %d tools\n", len(step.ToolCalls))
}
if step.Usage.InputTokens != nil {
fmt.Printf("Used %d input tokens\n", *step.Usage.InputTokens)
}
},
})
Advanced Example: Token Usage Tracking
var totalCost float64
const INPUT_COST_PER_1K = 0.01 // $0.01 per 1K input tokens
const OUTPUT_COST_PER_1K = 0.03 // $0.03 per 1K output tokens
agent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
Tools: tools,
OnStepFinish: func(step types.StepResult) {
// Calculate cost for this step
var stepCost float64
if step.Usage.InputTokens != nil {
inputCost := float64(*step.Usage.InputTokens) / 1000.0 * INPUT_COST_PER_1K
stepCost += inputCost
}
if step.Usage.OutputTokens != nil {
outputCost := float64(*step.Usage.OutputTokens) / 1000.0 * OUTPUT_COST_PER_1K
stepCost += outputCost
}
totalCost += stepCost
fmt.Printf("Step %d cost: $%.4f (Total: $%.4f)\n",
step.StepNumber, stepCost, totalCost)
},
})
Example: Building a Progress UI
type ProgressTracker struct {
steps []types.StepResult
currentStep int
totalTokens int64
mu sync.Mutex
}
func (p *ProgressTracker) OnStepFinish(step types.StepResult) {
p.mu.Lock()
defer p.mu.Unlock()
p.steps = append(p.steps, step)
p.currentStep = step.StepNumber
if step.Usage.GetTotalTokens() > 0 {
p.totalTokens += step.Usage.GetTotalTokens()
}
// Update UI (simplified example)
fmt.Printf("\r[%d steps] Processing... %d tokens used",
p.currentStep, p.totalTokens)
}
tracker := &ProgressTracker{}
agent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
OnStepFinish: tracker.OnStepFinish,
})
Example: Logging to File
logFile, _ := os.Create("agent-execution.log")
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)
agent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
Tools: tools,
OnStepFinish: func(step types.StepResult) {
// Log structured data as JSON
data, _ := json.MarshalIndent(step, "", " ")
logger.Printf("Step %d:\n%s\n", step.StepNumber, string(data))
},
})
Example: Early Termination Based on Steps
const MAX_ALLOWED_STEPS = 5
stepCount := 0
agent := agent.NewToolLoopAgent(agent.AgentConfig{
Model: model,
MaxSteps: 10, // hard limit for this agent (plain int, no SDK-imposed ceiling)
OnStepFinish: func(step types.StepResult) {
stepCount++
if stepCount >= MAX_ALLOWED_STEPS {
fmt.Println("Warning: Approaching step limit")
}
// Note: In current implementation, OnStepFinish cannot stop execution
// Use MaxSteps in AgentConfig for hard limits
},
})
Other Callbacks
OnStepStart
Called before each step begins:
OnStepStart func(stepNum int)
OnToolCall
Called when the agent decides to call a tool:
OnToolCall func(toolCall types.ToolCall)
OnToolResult
Called when a tool execution completes:
OnToolResult func(toolResult types.ToolResult)
OnFinish
Called when the agent completes execution:
OnFinish func(result *agent.AgentResult)
LangChain-Style Callbacks
For compatibility with LangChain patterns:
OnChainStart func(input string, messages []types.Message)
OnChainEnd func(result *agent.AgentResult)
OnChainError func(err error)
OnAgentAction func(action agent.AgentAction)
OnAgentFinish func(finish agent.AgentFinish)
Complete Example
See /examples/agent/on-step-finish/main.go for a complete working example demonstrating all features of the OnStepFinish callback.
Best Practices
-
Keep callbacks fast: Callbacks are called synchronously during agent execution. Avoid blocking operations.
-
Handle errors gracefully: Don't panic in callbacks - log errors instead.
-
Use goroutines for expensive operations: If you need to do expensive work, spawn a goroutine:
OnStepFinish: func(step types.StepResult) {go func() {// Expensive operation like uploading logsuploadStepData(step)}()} -
Thread safety: If callbacks share state, use proper synchronization:
var mu sync.Mutexvar sharedState []types.StepResultOnStepFinish: func(step types.StepResult) {mu.Lock()defer mu.Unlock()sharedState = append(sharedState, step)} -
Monitor token usage: Use
OnStepFinishto track costs and set budgets. -
Debug with context:
StepResultincludes warnings, raw responses, and context management info.
Comparison with TypeScript AI SDK
The Go implementation closely follows the TypeScript AI SDK's callback patterns:
| TypeScript | Go | Notes |
|---|---|---|
onStepFinish | OnStepFinish | Same functionality |
onFinish | OnFinish | Called at completion |
experimental_onToolCall | OnToolCall | Stable in Go |
The StepResult structure matches the TypeScript StreamStep with all the same fields and behavior.