Events
Every run produces one ordered, typed log. Every managed invocation appends to it, whatever started the run, and a run’s status is folded from it rather than stored beside it, so there is no second source that can disagree.
Streaming a run as it happens
async for event in run.events(follow=True):
print(event.kind)run.started
tool.call.started
text.delta
message.completed
run.completedfollow=True is what makes this live. Without it, events() returns only what the log already
holds and stops, which for a run that has just started is a single event. That is the right default
for reading a finished run back and the wrong one for watching a live one.
The loop ends on a terminal event: run.completed, run.failed or run.cancelled.
Reading one back afterwards
run = await deck.runs.get(run_id)
async for event in run.events(): # everything, no waiting
...
async for event in run.events(from_seq=120): # only what is new to you
...from_seq resumes from a position you already have, so a consumer that crashed does not replay
what it already processed. seq is assigned by the store, not the producer, and counts from 1
within a run.
Streaming without a handle
When you want the events and the result in one pass and do not need the handle:
async for event in deck.stream("Jack", question):
if event.kind == "text.delta":
print(event.payload.text, end="")Switching on kind
The payload is typed per kind, so a consumer branches on event.kind and gets the right fields:
if event.kind == "tool.call.started":
print(event.payload.tool, event.payload.args)
elif event.kind == "text.delta":
print(event.payload.text, end="")An unknown kind is carried rather than rejected, so a reader written today keeps working against a log written by a newer release.
Related
- Event types - all 21 kinds, their payloads, and the envelope
- Runs - starting a run and rehydrating its handle
- Lifecycle & Control - which events set which status