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.