Middleware & Hooks
Chronos uses a hook-based middleware system to intercept execution events. Hooks run before and after model calls, tool calls, graph nodes, and session lifecycle events. Use them for logging, metrics, retries, rate limiting, cost tracking, and caching.
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
}
- Before: Called before the event executes. Return an error to abort the operation.
- After: Called after the event executes. Runs even when the operation fails.
Chain
Hooks are composed in a Chain (a slice of Hook). The chain runs Before in order and After in reverse (middleware unwinding pattern).
chain := hooks.Chain{hook1, hook2, hook3}
if err := chain.Before(ctx, evt); err != nil {
return err // hook1 or hook2 or hook3 aborted
}
// ... execute operation ...
chain.After(ctx, evt) // hook3, hook2, hook1
Event Struct
Each hook receives an Event:
type Event struct {
Type EventType // kind of event
Name string // tool name, model name, or node ID
Input any // request or input state
Output any // response or output state
Error error // error if operation failed
Metadata map[string]any // extensible metadata
}
Event Types
| Type | When Fired |
|---|---|
tool_call.before |
Before a tool is executed |
tool_call.after |
After a tool returns |
model_call.before |
Before an LLM chat request |
model_call.after |
After an LLM response |
node.before |
Before a graph node runs |
node.after |
After a graph node completes |
context.overflow |
When context limit is exceeded (before summarization) |
context.summarize |
After summarization completes |
session.start |
When a session chat begins |
session.end |
When a session chat ends |
Adding Hooks
Attach hooks via the agent builder:
a, err := agent.New("my-agent", "My Agent").
WithModel(provider).
AddHook(loggingHook).
AddHook(metricsHook).
Build()
Built-in Hooks
LoggingHook
Records events to a slice for inspection or logging. Useful for debugging and audit trails.
loggingHook := &hooks.LoggingHook{}
a.AddHook(loggingHook)
// After execution
for _, evt := range loggingHook.Events {
fmt.Printf("%s: %s\n", evt.Type, evt.Name)
}
RetryHook
Retries failed model calls with exponential backoff and jitter. The hook signals retry via event metadata; the caller (agent) must implement the actual retry loop.
retry := hooks.NewRetryHook(3)
retry.BaseDelay = 500 * time.Millisecond
retry.MaxDelay = 30 * time.Second
retry.RetryableError = func(err error) bool {
return strings.Contains(err.Error(), "rate limit")
}
retry.OnRetry = func(attempt int, delay time.Duration) {
log.Printf("Retry attempt %d after %v", attempt, delay)
}
a.AddHook(retry)
| Field | Type | Description |
|---|---|---|
MaxRetries |
int | Maximum retry attempts (default 3) |
BaseDelay |
time.Duration | Initial delay (default 500ms) |
MaxDelay |
time.Duration | Cap on delay (default 30s) |
RetryableError |
func(error) bool | Classify errors; nil = retry all |
OnRetry |
func(int, time.Duration) | Callback before each retry |
Retries |
int | Total retries performed (observability) |
When a retry is signaled, the hook sets evt.Metadata["retry"] = true, retry_attempt, and retry_delay.
RateLimitHook
Enforces per-provider rate limits using a token-bucket algorithm. Blocks on model_call.before when the limit would be exceeded (or returns an error if WaitOnLimit is false).
rl := hooks.NewRateLimitHook(60, 100000) // 60 req/min, 100K tokens/min
rl.WaitOnLimit = true // block until capacity available (default)
a.AddHook(rl)
| Field | Type | Description |
|---|---|---|
RequestsPerMinute |
int | Max model calls per minute; 0 = unlimited |
TokensPerMinute |
int | Max prompt tokens per minute; 0 = unlimited |
WaitOnLimit |
bool | Block (true) or return error (false); default true |
CostTracker
Tracks LLM API costs per session and globally. Enforces an optional budget by blocking model calls when the limit is exceeded.
tracker := hooks.NewCostTracker(nil) // nil = use built-in price table
tracker.Budget = 10.0 // $10 max spend; 0 = unlimited
a.AddHook(tracker)
// After execution
global := tracker.GetGlobalCost()
sessionCost := tracker.GetSessionCost(sessionID)
fmt.Printf("Total: $%.4f, Session: $%.4f\n", global.TotalCost, sessionCost.TotalCost)
| Field | Type | Description |
|---|---|---|
Budget |
float64 | Max total spend (USD); 0 = unlimited |
Built-in price table includes GPT-4o, GPT-4o-mini, GPT-4-turbo, o1, o1-mini, o3, o3-mini, Claude models, Gemini, and Mistral. Pass a custom map[string]ModelPrice to NewCostTracker to override.
CacheHook
Caches LLM responses for identical requests. Uses SHA-256 of the serialized request as the cache key. Skips streaming and tool-call responses.
cache := hooks.NewCacheHook(5 * time.Minute)
cache.MaxEntries = 1000 // LRU eviction when exceeded; 0 = unlimited
a.AddHook(cache)
hits, misses := cache.Stats()
fmt.Printf("Cache: %d hits, %d misses\n", hits, misses)
| Field | Type | Description |
|---|---|---|
TTL |
time.Duration | How long entries remain valid (default 5 min) |
MaxEntries |
int | Max cache size; 0 = unlimited |
Hits |
int | Cache hits |
Misses |
int | Cache misses |
MetricsHook
Tracks latency, token usage, and error rates for model and tool calls.
metrics := hooks.NewMetricsHook()
a.AddHook(metrics)
// Raw metrics
calls := metrics.GetMetrics()
for _, c := range calls {
fmt.Printf("%s %s: %v\n", c.Type, c.Name, c.Duration)
}
// Aggregated summary
summary := metrics.GetSummary()
fmt.Printf("Model calls: %d, avg latency: %v, errors: %d\n",
summary.TotalModelCalls, summary.AvgModelLatency, summary.TotalErrors)
| MetricsSummary Field | Description |
|---|---|
TotalModelCalls |
Number of model calls |
TotalToolCalls |
Number of tool calls |
TotalErrors |
Calls that failed |
TotalPromptTokens |
Sum of prompt tokens |
TotalCompTokens |
Sum of completion tokens |
AvgModelLatency |
Average model call duration |
AvgToolLatency |
Average tool call duration |
MaxModelLatency |
Longest model call |
MaxToolLatency |
Longest tool call |
Complete Example: Combining Hooks
package main
import (
"context"
"log"
"os"
"time"
"github.com/spawn08/chronos/engine/hooks"
"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/sdk/agent"
)
func main() {
ctx := context.Background()
provider := model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))
logging := &hooks.LoggingHook{}
metrics := hooks.NewMetricsHook()
cost := hooks.NewCostTracker(nil)
cost.Budget = 5.0
cache := hooks.NewCacheHook(10 * time.Minute)
rl := hooks.NewRateLimitHook(30, 50000)
a, err := agent.New("monitored-agent", "Monitored Agent").
WithModel(provider).
WithSystemPrompt("You are a helpful assistant.").
AddHook(logging).
AddHook(metrics).
AddHook(cost).
AddHook(cache).
AddHook(rl).
Build()
if err != nil {
log.Fatal(err)
}
resp, err := a.Chat(ctx, "What is 2 + 2?")
if err != nil {
log.Fatal(err)
}
log.Println(resp.Content)
summary := metrics.GetSummary()
log.Printf("Model calls: %d, latency: %v, cost: $%.4f",
summary.TotalModelCalls, summary.AvgModelLatency, cost.GetGlobalCost().TotalCost)
hits, misses := cache.Stats()
log.Printf("Cache: %d hits, %d misses", hits, misses)
}