Skip to main content
Every interesting thing a Runnable does — start, end, error, LLM token, tool call, checkpoint — flows through the same Event enum. You attach consumers in two flavors:
  • Observer — a sync, broad-spectrum trait. Best for things like “log to stdout,” “push to a channel.”
  • CallbackHandler — strongly-typed, per-event hook methods (on_llm_start, on_tool_end, …). Best for things like “translate to Langfuse spans.”
The two compose: every CallbackHandler is also usable as an Observer via HandlerObserver.

Observer

Any Fn(&Event) + Send + Sync is also an Observer thanks to a blanket impl, so quick uses don’t need a struct:

Attach to a run

Observers travel with RunnableConfig. They can be attached on construction or pushed into the observers field:
Multiple observers are fine — they fan out, each receiving every event.

CallbackHandler

For richer integrations, implement CallbackHandler:
Each method has a default no-op, so you implement only what you care about. Wrap with HandlerObserver(your_handler) to attach to RunnableConfig.

Built-in observer

cognis::observers::TracingObserver is a tiny stdout-printing observer good for local debugging:
For production observability — token counts, USD cost, Langfuse export — see Observability → Trace with Langfuse.

Multiple consumers

Bundle handlers with CallbackManager (a HandlerBuilder builds one). The manager fans events out to every handler:
This is how you stack stdout + Langfuse + a custom audit log without writing your own multiplexer.

Event reference

run_id correlates events from the same invocation; nested chains use parent_run_id automatically.

How it works

  • Observers are sync, fan-out, and best-effort. A slow observer slows execution. Keep the work small or push to a channel.
  • CallbackHandlers are async. They can do real I/O without blocking the engine.
  • Order is propagation order, not strict global order. Two parallel branches emit events in their own threads; observers may see them interleaved.
  • run_id is set by RunnableConfig::default(). Reuse a RunnableConfig::with_parent_run_id(parent) to nest manually if needed.

See also

Trace with Langfuse

Wire the standard production exporter.

Cost tracking

Token counts and USD cost on every LLM call.

Streaming

Same events, returned as a stream instead.