Hooks & Observability
Hooks & Observability
Hooks are middleware that intercept model calls, tool calls, and session events. They provide a composable way to add caching, retry logic, cost tracking, rate limiting, metrics, and logging without modifying agent code.
Hook Interface
Every hook implements the Hook interface:
type Hook interface {
Before(ctx context.Context, evt *Event) error
After(ctx context.Context, evt *Event) error
}
Hooks are composed into a Chain and fire in order: all Before handlers run before the operation, all After handlers run after.
Event Types
EventModelCallBefore // Before an LLM API call
EventModelCallAfter // After an LLM API call
EventToolCallBefore // Before a tool execution
EventToolCallAfter // After a tool execution
EventSessionStart // When a session begins
EventSessionEnd // When a session ends
Built-in Hooks
MetricsHook
Tracks latency, token usage, and error rates for all model and tool calls.
metrics := hooks.NewMetricsHook()
// After running the agent...
summary := metrics.GetSummary()
fmt.Printf("Model calls: %d, Avg latency: %v\n", summary.TotalModelCalls, summary.AvgModelLatency)
// Per-call breakdown
for _, m := range metrics.GetMetrics() {
fmt.Printf(" %s: %v (error=%v)\n", m.Name, m.Duration, m.Error)
}
// Reset for a new measurement window
metrics.Reset()
CostTracker
Estimates API costs using configurable per-token pricing. Supports budget limits.
tracker := hooks.NewCostTracker(map[string]hooks.ModelPrice{
"gpt-4o": {PromptPricePerToken: 0.0000025, CompletionPricePerToken: 0.00001},
"gpt-4o-mini": {PromptPricePerToken: 0.00000015, CompletionPricePerToken: 0.0000006},
})
tracker.Budget = 1.00 // $1.00 max spend — blocks calls when exceeded
// After running...
report := tracker.GetGlobalCost()
fmt.Printf("Total: $%.4f (%d tokens)\n", report.TotalCost, report.PromptTokens+report.CompletionTokens)
// Per-session tracking
sessionReport := tracker.GetSessionCost("session-123")
Pass nil for the price table to use built-in defaults covering GPT-4o, Claude, Gemini, Mistral, and o-series models.
CacheHook
Caches identical LLM requests to avoid duplicate API calls. Supports TTL and max entries.
cache := hooks.NewCacheHook(5 * time.Minute)
cache.MaxEntries = 1000
// After running...
hits, misses := cache.Stats()
fmt.Printf("Cache: %d hits, %d misses\n", hits, misses)
// Manual clear
cache.Clear()
Streaming requests and tool-call responses are automatically excluded from caching.
RetryHook
Retries failed model calls with exponential backoff and jitter. When the agent provides the model provider and request in event metadata, the hook performs actual retry calls.
retry := hooks.NewRetryHook(3) // max 3 retry attempts
retry.BaseDelay = 500 * time.Millisecond
retry.MaxDelay = 10 * time.Second
retry.OnRetry = func(attempt int, delay time.Duration) {
log.Printf("Retry attempt %d after %v", attempt, delay)
}
// Optional: classify which errors are retryable
retry.RetryableError = func(err error) bool {
return strings.Contains(err.Error(), "rate limit") ||
strings.Contains(err.Error(), "timeout")
}
RateLimitHook
Token-bucket rate limiter for model API calls.
limiter := hooks.NewRateLimitHook(
60, // 60 requests per minute
0, // no token-per-minute limit
)
limiter.WaitOnLimit = true // block until capacity is available (vs. return error)
LoggingHook
Simple structured logging for all events.
logger := &hooks.LoggingHook{}
Composing Hooks
Hooks are added to an agent via the builder and execute in registration order:
a, _ := agent.New("observed-agent", "Agent").
WithModel(provider).
AddHook(logger). // fires first
AddHook(metrics). // fires second
AddHook(tracker). // fires third
AddHook(limiter). // fires fourth
AddHook(cache). // fires fifth
AddHook(retry). // fires last
Build()
Order matters:
- Put
LoggingHookfirst for comprehensive logging - Put
RateLimitHookbeforeCacheHookso cached responses bypass the limiter - Put
RetryHooklast so it catches errors from all earlier hooks
Custom Hooks
Implement the Hook interface:
type AuditHook struct {
entries []AuditEntry
}
func (h *AuditHook) Before(_ context.Context, evt *hooks.Event) error {
if evt.Type == hooks.EventModelCallBefore {
h.entries = append(h.entries, AuditEntry{
Time: time.Now(),
Model: evt.Name,
Type: "request",
})
}
return nil
}
func (h *AuditHook) After(_ context.Context, evt *hooks.Event) error {
if evt.Type == hooks.EventModelCallAfter {
h.entries = append(h.entries, AuditEntry{
Time: time.Now(),
Model: evt.Name,
Type: "response",
Error: evt.Error != nil,
})
}
return nil
}
See the hooks_observability example for a complete runnable demonstration.