Events Overview
Understanding the event lifecycle and when the agent is done
Every agent interaction emits a stream of events that describe exactly what's happening: when the agent starts thinking, calls tools, generates text, and finishes. Understanding this event system is essential for building responsive applications.
The Two-Level Turn Hierarchy
** CRITICAL CONCEPT:** There are TWO levels of turns, and knowing the difference is essential for building correct UIs.
Message Turn vs Agent Turn
MESSAGE TURN (entire user interaction)
├── MessageTurnStartedEvent
├── User: "What's the weather in Paris?"
│
├── AGENT TURN 1 (first LLM call)
│ ├── AgentTurnStartedEvent
│ ├── LLM responds: "I'll check the weather"
│ ├── ToolCallStartEvent: get_weather
│ └── AgentTurnFinishedEvent
│
├── Tool executes: get_weather("Paris") → "22°C, sunny"
│ └── ToolCallResultEvent
│
├── AGENT TURN 2 (second LLM call)
│ ├── AgentTurnStartedEvent
│ ├── TextDeltaEvent: "The"
│ ├── TextDeltaEvent: " weather"
│ ├── TextDeltaEvent: " is 22°C and sunny"
│ └── AgentTurnFinishedEvent
│
└── MessageTurnFinishedEvent ← THIS is when the agent is DONE!Key Insights
Message Turn: The ENTIRE conversation from user input to final response
- Starts:
MessageTurnStartedEvent - Ends:
MessageTurnFinishedEvent← Use this for UI state!
- Starts:
Agent Turn: A single LLM API call (there can be many per message turn)
- Starts:
AgentTurnStartedEvent - Ends:
AgentTurnFinishedEvent← Usually ignore these
- Starts:
** Common Mistake:** Using AgentTurnFinishedEvent to stop loading spinner causes the UI to show "done" too early while the agent is still working!
** Correct:** Always use MessageTurnFinishedEvent to know when the agent is completely finished.
Event Flow
All events flow through the Agent.RunAsync() async stream:
await foreach (var evt in agent.RunAsync(messages))
{
// Events arrive here in real-time
// Process them with pattern matching
}The stream delivers events in chronological order as they occur, enabling real-time UI updates.
Event Categories
Events are organized into these categories:
1. Turn Lifecycle Events
Control the conversation flow:
MessageTurnStartedEvent- User message processing beginsMessageTurnFinishedEvent- Agent is done ← Use this!MessageTurnErrorEvent- Unrecoverable error occurredAgentTurnStartedEvent- Internal LLM call begins (usually ignore)AgentTurnFinishedEvent- Internal LLM call ends (usually ignore)
2. Content Events
The agent's text response:
TextDeltaEvent- Streaming text chunks (accumulate these to build the response)TextMessageStartEvent- Message boundary (optional, nice for polish)TextMessageEndEvent- Message boundary (optional, nice for polish)
3. Reasoning Events
Extended thinking (Claude's internal reasoning):
ReasoningDeltaEvent- Streaming reasoning contentReasoningMessageStartEvent- Reasoning beginsReasoningMessageEndEvent- Reasoning ends
4. Tool Events
When the agent calls functions:
ToolCallStartEvent- Tool invocation begins (show "Calling calculator..." in UI)ToolCallResultEvent- Tool execution complete (show result or error)ToolCallArgsEvent- Streaming tool arguments (advanced)ToolCallEndEvent- Arguments complete (advanced)
5. Bidirectional Events
Events that require a response from the user:
PermissionRequestEvent→PermissionResponseEvent- Ask user to approve tool executionClarificationRequestEvent→ClarificationResponseEvent- Ask user for more infoClientToolInvokeRequestEvent→ClientToolInvokeResponseEvent- Execute tool on client
** CRITICAL:** These events require calling agent.SendResponseAsync() or the agent will hang until timeout!
6. Observability Events
Internal diagnostics (filter these out!):
- 25+ internal events like
MiddlewareProgressEvent,CircuitBreakerTriggeredEvent, etc. - All implement
IObservabilityEventmarker interface - Always filter these out in user-facing code to prevent console spam
Basic Event Handling Pattern
Here's the fundamental pattern every application needs:
await foreach (var evt in agent.RunAsync(messages))
{
switch (evt)
{
// Content streaming
case TextDeltaEvent delta:
Console.Write(delta.Text);
break;
// Reasoning
case ReasoningDeltaEvent reasoning:
Console.Write($"[Thinking: {reasoning.Text}]");
break;
// Tool execution
case ToolCallStartEvent toolStart:
Console.WriteLine($"\n[Calling: {toolStart.ToolName}]");
break;
case ToolCallResultEvent toolResult:
Console.WriteLine($"[Result: {toolResult.Result}]");
break;
// THIS is when to stop your loading spinner!
case MessageTurnFinishedEvent:
Console.WriteLine("\n✓ Agent finished");
// In a web UI: setIsLoading(false), enableInput()
break;
// Error handling
case MessageTurnErrorEvent error:
Console.WriteLine($"\n✗ Error: {error.ErrorMessage}");
break;
// Permissions (requires SendResponseAsync!)
case PermissionRequestEvent permission:
var approved = PromptUser($"Allow {permission.FunctionName}?");
// THIS LINE IS MANDATORY - don't forget it!
await agent.SendResponseAsync(permission.PermissionId,
new PermissionResponseEvent
{
PermissionId = permission.PermissionId,
Approved = approved
});
break;
}
}Event Properties
All events inherit from AgentEvent and share these core properties:
public abstract record AgentEvent
{
// Event routing
public EventPriority Priority { get; init; } // Immediate/Control/Normal/Background
public EventDirection Direction { get; init; } // Downstream/Upstream
public string? StreamId { get; init; } // For stream interruption
// Nested agent tracking
public AgentExecutionContext? ExecutionContext { get; init; }
// Stream control
public bool CanInterrupt { get; init; } // Can be dropped on cancellation
public long SequenceNumber { get; internal set; } // Order of emission
}Most applications only need to care about the event type itself. Advanced scenarios may use:
ExecutionContext- Filter events from nested agents (see SubAgent Events)Priority- Event routing in advanced streaming setups (see Streaming & Cancellation)
Common Beginner Mistakes
- Using
AgentTurnFinishedEventinstead ofMessageTurnFinishedEvent
- Result: UI shows "done" while agent is still working
- Fix: Always use
MessageTurnFinishedEventfor UI state
- Handling
PermissionRequestEventbut not callingSendResponseAsync()
- Result: Agent hangs for ~30 seconds until timeout
- Fix: Always call
SendResponseAsync()for bidirectional events
- Forgetting to handle
MessageTurnFinishedEvent
- Result: Loading spinner never stops, input stays disabled
- Fix: Always handle
MessageTurnFinishedEventto update UI state
What's Next
This overview covers the core concepts. For detailed guides:
Event Documentation
- Event Types Reference - Complete listing of all 50+ event types
- Consuming Events - Advanced patterns, filtering, error handling
- SubAgent Events - Nested agents, filtering by depth
- Streaming & Cancellation - Interruption, graceful shutdown, priority channels
- Bidirectional Events - Request/response patterns in depth
- Custom Events - Creating your own event types
Getting Started
- Event Handling - Quick start guide
- Building Console Apps - Console patterns
- Building Web Apps - SSE streaming setup