Events¶
Agent event stream¶
Typed, frozen Pydantic events yielded by agent.run(...). Consume
with async for event in agent.run(...) or pipe into a hook.
TulipEvent ¶
Bases: BaseModel
Base class for all Tulip events.
ThinkEvent ¶
Bases: TulipEvent
Agent produced reasoning and/or tool calls.
reasoning carries the assistant's ordinary prose for the turn — the
text you show a user — not hidden chain-of-thought. It is the streaming
path's answer to "where is the assistant's text?" (#165): interim text
arrives here, the final answer on TerminateEvent.final_message, and
token-by-token deltas on ModelChunkEvent (opt-in via
stream_tokens=True).
content
property
¶
The assistant's text for this turn — alias for reasoning.
content is the first guess from outside; reasoning reads like
chain-of-thought but is actually the visible prose. Both names work.
ToolStartEvent ¶
ToolCompleteEvent ¶
ReflectEvent ¶
GroundingEvent ¶
TerminateEvent ¶
Bases: TulipEvent
Agent execution terminated.
Not just a lifecycle signal: final_message is the payload — the
agent's final answer on the streaming path, the counterpart of
AgentResult.message (#165). Also readable as .content.
ModelChunkEvent ¶
Bases: TulipEvent
Streaming chunk from model.
Fires only with agent.run(prompt, stream_tokens=True) — without
the flag no chunk events arrive and nothing says why (#164). Assistant
text still arrives without it, batched per turn: interim prose on
ThinkEvent.reasoning/.content, the final answer on
TerminateEvent.final_message/.content.
ModelCompleteEvent ¶
InterruptEvent ¶
Bases: TulipEvent
Agent paused for user input.
When a tool calls interrupt() (e.g., ask_user), the agent yields this event and pauses. The caller should present the question to the user and call agent.resume(response) to continue.
In-process event bus (SSE)¶
The EventBus publishes StreamEvents for every meaningful step from
every framework layer. Opt-in via run_context() — zero cost when
unused.
See Observability for usage patterns and
the Server-Sent Events (SSE) event catalogue
for the full wire-format reference (40+ event_type strings across 9
prefixes).
EventBus ¶
EventBus(*, max_queue_size: int = _DEFAULT_QUEUE_SIZE, history_per_run: int = _DEFAULT_HISTORY_PER_RUN, max_runs_retained: int = _DEFAULT_MAX_RUNS_RETAINED)
In-process pub/sub bus.
Thread-safe under asyncio (a single asyncio.Lock guards all
queue mutations). Not safe across processes — for multi-worker
deployments, swap the implementation for one backed by Redis /
NATS / similar.
Source code in .sdk/src/tulip/observability/event_bus.py
publish
async
¶
Deliver event to every subscriber on its run + global.
Non-blocking on slow consumers: each queue.put is wrapped in a
wait_for with :data:_PUBLISH_TIMEOUT_SECONDS. If a
subscriber's queue is saturated past the timeout, the event is
dropped for that subscriber (counted in
:attr:_dropped_events) and the publish continues for
everyone else.
Source code in .sdk/src/tulip/observability/event_bus.py
subscribe
async
¶
Subscribe to a single run's events.
On connect, every event currently in history for the run is
replayed in order, then live events follow. The iterator
terminates when :meth:close_stream is called for the run.
Source code in .sdk/src/tulip/observability/event_bus.py
subscribe_global
async
¶
Subscribe to every event on the bus.
Useful for monitoring dashboards. Does not replay history —
only live events. Capped at :attr:_max_global_subscribers
concurrent subscribers; over-cap connections receive an
immediate sentinel.
Source code in .sdk/src/tulip/observability/event_bus.py
close_stream
async
¶
Signal end-of-stream to every active subscriber of run_id.
Idempotent. Keeps the history buffer — a late subscriber
joining within the retention window still sees the run's
events plus an immediate sentinel terminating the iterator.
Use :meth:drop_history to forcibly forget a run.
Source code in .sdk/src/tulip/observability/event_bus.py
drop_history
async
¶
Forget a run's history buffer + closed-state.
Late subscribers after this call see nothing for run_id.
Useful for explicit cleanup; not called automatically.
Source code in .sdk/src/tulip/observability/event_bus.py
close_global
async
¶
Terminate every global subscriber. Used at shutdown.
Source code in .sdk/src/tulip/observability/event_bus.py
stats ¶
Snapshot of subscriber counts + drop totals.
Cheap to call. Read-only — does not lock; treat as approximate when called concurrently with publish.
Source code in .sdk/src/tulip/observability/event_bus.py
StreamEvent
dataclass
¶
StreamEvent(run_id: str, event_type: str, data: dict[str, Any], timestamp: datetime = (lambda: datetime.now(UTC))(), event_id: str = (lambda: str(uuid4()))())
Single event flowing through the bus.
Fields are intentionally narrow — every consumer (CLI, SSE, JSON
log) sees the same shape, and data is the open extension
point.
to_dict ¶
JSON-serialisable shape used by the SSE wire format.
Source code in .sdk/src/tulip/observability/event_bus.py
run_context
async
¶
Bind a run_id to the current asyncio context.
Yields the bound id (generates a fresh uuid4 hex if run_id is
None) and restores the previous contextvar value on exit.
Example::
from tulip.observability import run_context, get_event_bus
async with run_context() as rid:
# any emission inside this block tags events with `rid`
await my_pipeline.run(task)
# downstream subscribers can now `bus.subscribe(rid)`
Note
The context manager does not call
:meth:EventBus.close_stream — callers decide when to close
based on their own lifecycle (the router does it at end of
dispatch; notebook subprocesses do it on exit).
Source code in .sdk/src/tulip/observability/context.py
current_run_id ¶
Return the run_id active on the current context, or None.
Every emission helper in :mod:tulip.observability.emit calls this
and bails when it returns None. SDK users who never enter a
run context pay nothing.
Source code in .sdk/src/tulip/observability/context.py
EventBusHook ¶
Bases: HookProvider
Republishes every agent-lifecycle hook onto the event bus.
The hook never mutates events — it's pure-observation. Tools and model calls retain whatever cancellation / retry / replacement behaviour the surrounding code dictates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run_id
|
str | None
|
The id every event is tagged with. Omit it to tag each event
with the run that produced it, read per call: the id of the
active :func: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in .sdk/src/tulip/observability/bus_hook.py
run_id
property
¶
The id an event fired now would carry, outside any agent run.
The fixed run_id when one was given; otherwise the active
run_context id, or None when there is none (an event fired
inside a run then falls back to that run's own id).
on_before_compaction
async
¶
Called before the agent compacts its context.
Set event.instructions to steer the summary; set event.cancel = True to skip this compaction. event.messages is the full conversation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
BeforeCompactionEvent
|
Write-protected event. Writable: instructions, cancel. |
required |
Source code in .sdk/src/tulip/hooks/provider.py
register_hooks ¶
Return which hooks this provider implements.
Returns:
| Type | Description |
|---|---|
dict[str, bool]
|
Dictionary mapping hook names to whether they are implemented. |
dict[str, bool]
|
Useful for optimization - registry can skip calling unimplemented hooks. |