ares_callbacks
The internal/ares_callbacks package (package ares_callbacks) provides
lifecycle event hooks for LLM calls, agent execution, and tool invocations. It
exposes a thread-safe Registry that implements both the Emitter and
CallbackRegistrar interfaces, a Context carrying event metadata, ten
canonical Event types, and a BridgeEventStore that converts callback events
into persisted ares_events.EventStore entries so instrumentation consumers
only need to watch one stream.
Responsibility
- Define the
Eventenum (llm.start,llm.end,llm.error,llm.token,agent.start,agent.end,agent.error,tool.start,tool.end,tool.error) and theContextstruct carryingAgentID,ToolName,Model,Input,Output,Error,Duration,TokenCount,Extra, and aGoCtxfor trace propagation. - Provide
Registry(NewRegistry,On,Emit,Clear,Count) that registers multiple handlers per event and dispatches them sequentially in registration order. - Decouple producers from the concrete registry via the
Emitterinterface (Emit) and consumers via theCallbackRegistrarinterface (On,Count). - Guarantee concurrency safety:
Emitsnapshots the handler slice under a read lock and invokes handlers outside the lock, each wrapped inrecover()so a panicking handler never crashes the caller. - Bridge callback events into the event-sourcing system via
BridgeEventStore, which mapsEventconstants toares_events.EventTypeand appends them to anEventStorewith a 5-second timeout.
Architecture
flowchart TD
LLM["llm.Client (Generate / Chat)"] -->|"emitCallback"| EM["Emitter interface"]
AG["leader / sub agents"] -->|"emitCallback"| EM
TE["sub.TaskExecutor"] -->|"SetCallbacks"| EM
EM --> RG["Registry.Emit"]
RG -->|"RLock + copy handlers"| SEQ["Sequential dispatch (registration order)"]
SEQ --> H1["Handler 1 (recover-wrapped)"]
SEQ --> H2["Handler 2"]
SEQ --> HN["Handler N"]
RG --> BE["BridgeEventStore.Emit"]
BE --> ME["mapEventType -> ares_events.EventType"]
ME --> ES["ares_events.EventStore.Append"]
EV["ares_evolution.Scheduler"] -->|"On(EventAgentEnd)"| RG
External interfaces
// Event types (callbacks.go)
type Event string
const (
EventLLMStart Event = "llm.start"
EventLLMEnd Event = "llm.end"
EventLLMError Event = "llm.error"
EventLLMToken Event = "llm.token"
EventAgentStart Event = "agent.start"
EventAgentEnd Event = "agent.end"
EventAgentError Event = "agent.error"
EventToolStart Event = "tool.start"
EventToolEnd Event = "tool.end"
EventToolError Event = "tool.error"
)
// Event context (callbacks.go)
type Context struct {
Event Event
AgentID string
ToolName string
Model string
Input string
Output string
Error error
Duration time.Duration
TokenCount int
Extra map[string]any
GoCtx context.Context // trace propagation; used by BridgeEventStore
}
type Handler func(ctx *Context)
// Abstraction interfaces (callbacks.go)
type Emitter interface {
Emit(ctx *Context)
}
type CallbackRegistrar interface {
On(event Event, handler Handler)
Count(event Event) int
}
// Registry (callbacks.go)
type Registry struct {
handlers map[Event][]Handler
mu sync.RWMutex
}
func NewRegistry() *Registry
func (r *Registry) On(event Event, handler Handler)
func (r *Registry) Emit(ctx *Context)
func (r *Registry) Clear()
func (r *Registry) Count(event Event) int
// EventStore bridge (callback_bridge.go)
type BridgeEventStore struct {
store ares_events.EventStore
agentID string
}
func NewBridge(store ares_events.EventStore, agentID string) *BridgeEventStore
func (b *BridgeEventStore) Emit(ctx *Context)
// Public API wrapper (api/service/callbacks/service.go)
type Registry struct{ inner *internal.Registry }
type Event = internal.Event
type Context = internal.Context
type Handler = internal.Handler
func New() *Registry
func (r *Registry) On(event Event, handler Handler)
func (r *Registry) Emit(ctx *Context)
func (r *Registry) Clear()
func (r *Registry) Count(event Event) int
Key types and methods
| Type / Method | Purpose |
|---|---|
Event | String enum for the ten lifecycle event types across LLM, agent, and tool layers. |
Context | Metadata carrier passed to handlers; GoCtx enables trace propagation into the bridge. |
Handler | Function signature func(ctx *Context) invoked on event emission. |
Emitter | Producer-facing interface with a single Emit method; decouples emitters from Registry. |
CallbackRegistrar | Consumer-facing interface with On and Count; decouples subscribers from Registry. |
Registry | Thread-safe handler store; implements both Emitter and CallbackRegistrar (compile-time asserted). |
Registry.On | Appends a handler for an event; multiple handlers per event are allowed. |
Registry.Emit | Snapshots handlers under RLock, dispatches sequentially in registration order, each recover-wrapped; nil context is a no-op. |
Registry.Clear | Removes all handlers for all event types. |
Registry.Count | Returns the number of handlers registered for a given event. |
BridgeEventStore | Emitter implementation that translates callback events into ares_events.EventStore appends. |
BridgeEventStore.Emit | Maps Event to ares_events.EventType, builds a payload, and appends with a 5s timeout using ctx.GoCtx when set. |
NewBridge | Constructs a BridgeEventStore bound to an EventStore and stream agentID. |
Module collaboration
ares_callbacks->internal/ares_eventsforEventStoreandEventType(the bridge mapsEventLLMStart/End/ErrortoEventLLMCall,EventToolStart/End/ErrortoEventToolCallStarted/Completed, andEventAgentStart/End/ErrortoEventAgentStarted/Stopped).ares_callbacks->internal/loggervia the package-levellogvariable (logger.Module("ares_callbacks")).internal/llminjects anEmitterthroughllm.WithCallbacksand emitsEventLLMStart/EventLLMEnd/EventLLMErrorfromGenerate,GenerateStream, andChat.internal/agents/leaderandinternal/agents/subaccept anEmitterviaWithCallbacks/SetCallbacksand emit agent and tool lifecycle events.internal/ares_bootstrapwires one sharedRegistryinto the LLM client, task executor, and leader agent throughNewCallbackRegistry,NewLLMClientWithCallbacks,WireTaskExecutorCallbacks, andWireLeaderAgentCallbacks.internal/ares_evolutionregisters anEventAgentEndhandler on the shared registry to trigger evolution cycles after an agent stops.api/service/callbacksre-exports the registry as a thin wrapper for public consumers, aliasingEvent,Context, andHandlerto the internal types.
Extension points
- Subscribe to a lifecycle event by obtaining the shared
*Registry(or anyCallbackRegistrar) and callingOn(event, handler). Multiple handlers for the same event execute in registration order. - Emit events from a new component by depending on the
Emitterinterface and callingEmit(&Context{...})at the relevant lifecycle points; populateGoCtxso the bridge can propagate traces. - Persist callback events into the event-sourcing stream by constructing
NewBridge(store, agentID)and registering it as a handler (or passing it as theEmitter); unmapped event types are silently skipped. - Add a new event type by extending the
Eventconstants and, if bridging is required, adding a case toBridgeEventStore.mapEventType. - Reset state between tests or runs with
Registry.Clear(); verify wiring withRegistry.Count(event)in assertions. - Expose callbacks to external SDK users through
api/service/callbacks.New(), which returns a wrappedRegistrysharing the sameEvent/Context/Handlertypes.
Bilingual status
This page is the English reference. A Chinese translation with identical
structure and technical content is published as ares_callbacks.zh.md. All code
identifiers, type names, and signatures are kept in English in both files;
only the prose differs.
Maturity
Beta. The package is covered by callbacks_test.go and integration_test.go
(handler registration, dispatch order, nil-context safety, concurrent
emit/registration, all-field propagation, interface verification) and is wired
into the LLM client, leader/sub agents, and the evolution scheduler. The API is
functional but still evolving: the Event enum and Context fields may grow
as new instrumentation surfaces are added, and the bridge mapping is
incomplete for EventLLMToken.