Events¶
Every observable step of an agent run is a typed Pydantic event. Not
a dict, not a callback, not a string — a frozen class with named
fields you can match on.
This is the reference page. For the how (consuming the stream, SSE, hooks), see Streaming. For the why (frozen, typed, write-protected), see Agent loop.
from locus.core.events import (
ThinkEvent, ToolStartEvent, ToolCompleteEvent, TerminateEvent,
)
async for event in agent.run("Plan a trip"):
match event:
case ThinkEvent(reasoning=r) if r:
print("💭", r)
case ToolStartEvent(tool_name=n, arguments=a):
print(f"🔧 {n}({a})")
case ToolCompleteEvent(tool_name=n, result=r, error=e):
print(f" ↳ {e or r}")
case TerminateEvent(reason=r, final_message=m):
print(f"[{r}] {m}")
Common fields¶
Every event inherits from LocusEvent and carries:
| Field | Type | Meaning |
|---|---|---|
event_type |
Literal[...] |
Discriminator string — "think", "tool_start", etc. |
timestamp |
datetime |
UTC, populated at emit time. |
Events are frozen Pydantic models. A hook can read every field;
it cannot mutate one. To steer a run, use the explicit method on the
event (event.cancel(), event.replace_arguments(...)) — the intent
is visible in code review.
Core events¶
ThinkEvent¶
The model emitted reasoning, optionally with tool calls.
| Field | Meaning |
|---|---|
iteration |
ReAct turn index (0-based) |
reasoning |
The model's chain-of-thought, if the provider exposed it |
tool_calls |
Tool calls the model decided to make this turn |
Render this as a "thinking…" bubble. Most providers return None
unless extended thinking is enabled (Claude 4 / o-series).
ToolStartEvent¶
The agent is about to invoke a tool.
| Field | Meaning |
|---|---|
tool_name |
Tool registered with @tool |
tool_call_id |
Provider-issued id, used to correlate with the matching ToolCompleteEvent |
arguments |
The validated arguments dict |
Show a "calling X" indicator.
ToolCompleteEvent¶
A tool returned, errored, or was cancelled.
| Field | Meaning |
|---|---|
tool_name |
Same name as the matching start event |
tool_call_id |
Pairs with ToolStartEvent.tool_call_id |
result |
The serialised return value, or None on error |
error |
Exception message, or None on success |
duration_ms |
How long the body actually ran |
Always check error first — a non-None error means result is
None.
ModelChunkEvent¶
One streamed chunk from the LLM provider — the granularity that drives token-by-token rendering.
| Field | Meaning |
|---|---|
content |
Text delta (may be None for tool-call-only chunks) |
tool_calls |
Tool-call deltas, if the provider streams those |
done |
True on the final chunk of a turn |
None-guard before printing: if e.content: print(e.content, end="").
ModelCompleteEvent¶
A full model response was received (paired with the chunks above).
| Field | Meaning |
|---|---|
content |
The complete text |
tool_calls |
All tool calls in this turn |
usage |
{"input_tokens": ..., "output_tokens": ...} |
stop_reason |
Provider-specific stop reason |
Telemetry hooks key off usage for cost tracking.
ReflectEvent¶
Reflexion emitted a self-evaluation.
| Field | Meaning |
|---|---|
iteration |
Which turn this reflection concerns |
assessment |
"on_track", "stuck", "new_findings", or "loop_detected" |
confidence_delta |
Change vs the previous turn |
new_confidence |
Current value, 0.0–1.0 |
guidance |
Free-text steering for the next turn |
Pair new_confidence with ConfidenceMet for early
stopping.
GroundingEvent¶
Grounding finished evaluating claims.
| Field | Meaning |
|---|---|
score |
0.0–1.0, fraction of claims supported |
claims_evaluated |
How many claims the judge looked at |
ungrounded_claims |
The text of every unsupported claim |
requires_replan |
True if the run should re-research |
InterruptEvent¶
A tool requested human-in-the-loop input. The run pauses; resume by calling the agent with the user's reply.
| Field | Meaning |
|---|---|
question |
What to ask the human |
options |
If multiple-choice, the allowed answers |
interrupt_id |
Pass back to resume |
metadata |
Free-form context for the UI |
See Interrupts.
TerminateEvent¶
The run finished.
| Field | Meaning |
|---|---|
reason |
Which termination condition fired (its repr) |
iterations_used |
How many ReAct turns ran |
final_confidence |
Reflexion confidence at end of run |
total_tool_calls |
Distinct tool invocations |
final_message |
The assistant's last text, if any |
Always emitted exactly once per run.
Multi-agent events¶
These appear when an Orchestrator, Swarm, or StateGraph is
running.
| Event | Fired when |
|---|---|
SpecialistStartEvent |
Orchestrator dispatched to a specialist |
SpecialistCompleteEvent |
Specialist returned a result |
OrchestratorDecisionEvent |
Orchestrator picked its next step (invoke_specialist, correlate, summarize, finalize) |
See Multi-agent.
Causal-reasoning events¶
When causal=True, the agent emits node and edge events as the graph
grows.
| Event | Fired when |
|---|---|
CausalNodeEvent |
A new entity entered the cause-effect graph (root cause / symptom / intermediate) |
CausalEdgeEvent |
A causal link was added between two nodes |
Hook events¶
BeforeInvocationEvent, AfterInvocationEvent, BeforeToolCallEvent,
AfterToolCallEvent — emitted to hooks around the same lifecycle
points the user-visible events come from. See Hooks.
Common gotchas¶
| Symptom | Likely cause |
|---|---|
match is non-exhaustive at the type checker |
Add a case _: pass fallthrough or handle the missing variant. |
ModelChunkEvent.content is None |
Tool-call-only chunk. Guard with if event.content:. |
TerminateEvent never arrives |
Generator was cancelled mid-stream. Check the consumer for exceptions. |
Hook tried to mutate event.tool_name and got ValidationError |
Frozen by design — use event.replace_arguments(...) or event.cancel() instead. |
Source¶
locus.core.events— every event class.
See also¶
- Streaming — how to consume the event stream.
- Hooks — observe the same events from inside the loop.
- Agent server — re-emit events over Server-Sent Events.