Skip to main content

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

TypeWhen Fired
tool_call.beforeBefore a tool is executed
tool_call.afterAfter a tool returns
model_call.beforeBefore an LLM chat request
model_call.afterAfter an LLM response
node.beforeBefore a graph node runs
node.afterAfter a graph node completes
context.overflowWhen context limit is exceeded (before summarization)
context.summarizeAfter summarization completes
session.startWhen a session chat begins
session.endWhen 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)
FieldTypeDescription
MaxRetriesintMaximum retry attempts (default 3)
BaseDelaytime.DurationInitial delay (default 500ms)
MaxDelaytime.DurationCap on delay (default 30s)
RetryableErrorfunc(error) boolClassify errors; nil = retry all
OnRetryfunc(int, time.Duration)Callback before each retry
RetriesintTotal 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)
FieldTypeDescription
RequestsPerMinuteintMax model calls per minute; 0 = unlimited
TokensPerMinuteintMax prompt tokens per minute; 0 = unlimited
WaitOnLimitboolBlock (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)
FieldTypeDescription
Budgetfloat64Max total spend (USD); 0 = unlimited

Built-in price table includes GPT-5.5, GPT-5, GPT-4o, o-series, Claude (Opus 4.8, Sonnet 5, Haiku 4.5, Fable 5), Gemini, and Mistral. Pass a custom map[string]ModelPrice to NewCostTracker to override. Prices track mid-2026 rates — override for current pricing.

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)
FieldTypeDescription
TTLtime.DurationHow long entries remain valid (default 5 min)
MaxEntriesintMax cache size; 0 = unlimited
HitsintCache hits
MissesintCache 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 FieldDescription
TotalModelCallsNumber of model calls
TotalToolCallsNumber of tool calls
TotalErrorsCalls that failed
TotalPromptTokensSum of prompt tokens
TotalCompTokensSum of completion tokens
AvgModelLatencyAverage model call duration
AvgToolLatencyAverage tool call duration
MaxModelLatencyLongest model call
MaxToolLatencyLongest 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)
}