Skip to main content

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​

  1. Keep callbacks fast: Callbacks are called synchronously during agent execution. Avoid blocking operations.

  2. Handle errors gracefully: Don't panic in callbacks - log errors instead.

  3. 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 logs
    uploadStepData(step)
    }()
    }
  4. Thread safety: If callbacks share state, use proper synchronization:

    var mu sync.Mutex
    var sharedState []types.StepResult

    OnStepFinish: func(step types.StepResult) {
    mu.Lock()
    defer mu.Unlock()
    sharedState = append(sharedState, step)
    }
  5. Monitor token usage: Use OnStepFinish to track costs and set budgets.

  6. Debug with context: StepResult includes warnings, raw responses, and context management info.

Comparison with TypeScript AI SDK​

The Go implementation closely follows the TypeScript AI SDK's callback patterns:

TypeScriptGoNotes
onStepFinishOnStepFinishSame functionality
onFinishOnFinishCalled at completion
experimental_onToolCallOnToolCallStable in Go

The StepResult structure matches the TypeScript StreamStep with all the same fields and behavior.

See Also​