Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content
Multi-agent memory

Event-driven agents

Agents that wake on memory changes instead of polling — the control event stream, what it emits, and when to reach for it.

An event-driven agent subscribes to a run's control event stream and acts when memory changes: a handoff lands, reflection finishes, a lesson promotes. Use this when an agent's job is to react — polling loops burn tokens and latency re-reading memory that hasn't changed, and the interesting moments (a verdict request, a new lesson) are exactly the ones the stream announces.

💡Tip

This page frames when to use the pattern. For the full walkthrough — wiring a consumer, filtering, rebuilding context after a wakeup — go to the event-driven agents recipe.

How it works

The control plane emits an event for every significant memory change in a run. Subscribe over SSE (GET /v2/control/events/subscribe) or gRPC (Subscribe); each SDK exposes the stream directly:

SDKCallReturns
Pythonclient.advanced.subscribe({"run_id": run_id})iterator of events
JS/TSclient.advanced.subscribe({ run_id: runId })async-iterable stream
Rustclient.subscribe(payload).await?ValueStream

Events carry a type, the run_id, the acting agent_id, and a JSON payload. The types fall into a few families (all namespaced context.*):

FamilyExample types
Memory writescontext.ingest_completed, context.working_memory_updated, context.checkpoint_created
Learningcontext.reflection_completed, context.lesson_promoted, context.lesson_validation_passed / _failed
Coordinationcontext.handoff_created, context.feedback_received, context.agent_registered
Outcomescontext.outcome_recorded, context.step_outcome_recorded

A reactive reviewer is a few lines — Rex sleeps until Ari asks for a verdict:

rex_wakes_on_handoff.py
for event in client.advanced.subscribe({"run_id": "nimbus:ticket-1088"}):
    if event.get("type") == "context.handoff_created":
        review_handoff(event)   # recall traces, then feedback() a verdict

When to use it

  • Reactive review agents. Rex shouldn't poll every run for pending handoffs — context.handoff_created wakes him exactly when a memory-backed handoff needs a verdict, and context.feedback_received wakes the sender.
  • Cache invalidation. If you cache assembled context or derived state, context.reflection_completed and context.working_memory_updated tell you precisely when the cache went stale — rebuild with get_context() on wakeup rather than on a timer.
  • Monitoring and audit. Sana can watch context.lesson_promoted and context.lesson_validation_failed across the fleet to track what's entering shared team memory — and flag a quarantined promotion before anyone asks.

When not to use it

Don't reach for the stream when a poll would do: a batch job that runs hourly and reads memory once doesn't need a persistent connection, and handoff(..., await_verdict=True) already packages the common "block until the verdict arrives" case without any subscription. The stream is per-run — a consumer watching many runs needs a subscription per run, which is a real cost at fleet scale.

ℹ️Note

The core data plane has its own pub-sub with semantic subscriptions — fire when a written node crosses a similarity threshold to a query embedding. It's a separate, lower-level surface, available only where the instance operator has enabled it; see Pub-sub & semantic subscriptions if you have access.

Related