SubAgent Events
Filtering and tracking events from nested agents
For Beginners: If you're building a single-agent app (most common), you can skip this entirely. This guide is only needed when using SubAgents (nested agents).
When Do I Need This?
You only need to understand ExecutionContext if you're building applications with nested agents (SubAgents). This includes scenarios like:
- Agent orchestrator that spawns specialized sub-agents
- Multi-agent systems with hierarchical delegation
- Toolkit systems where Toolkits spawn their own agents
For single-agent applications: Ignore ExecutionContext - it will be null or Depth 0, and you don't need to filter anything.
Understanding ExecutionContext
Every event includes an ExecutionContext property that identifies which agent emitted it:
public record AgentExecutionContext
{
public required string AgentName { get; init; } // Name of the agent
public required string AgentId { get; init; } // Unique instance ID
public string? ParentAgentId { get; init; } // Parent agent (if subagent)
public IReadOnlyList<string> AgentChain { get; init; } // Full chain from root
public int Depth { get; init; } // Nesting level (0 = root)
public bool IsSubAgent => Depth > 0; // True if not root
}Depth Levels
Root Agent (Depth = 0)
└── SubAgent 1 (Depth = 1)
└── SubAgent 2 (Depth = 2)
└── SubAgent 3 (Depth = 3)Common Filtering Patterns
Pattern 1: Show Only Root Agent Events
Most UIs only want to show the root agent's output, hiding internal SubAgent chatter:
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
// Filter out SubAgent events
if (evt.ExecutionContext?.IsSubAgent == true) continue;
// Only root agent events reach here
HandleEvent(evt);
}Pattern 2: Indent by Depth
Show all events with indentation based on nesting level:
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
var depth = evt.ExecutionContext?.Depth ?? 0;
var indent = new string(' ', depth * 2);
switch (evt)
{
case TextDeltaEvent delta:
Console.Write($"{indent}{delta.Text}");
break;
case ToolCallStartEvent toolStart:
var agentName = evt.ExecutionContext?.AgentName ?? "Agent";
Console.WriteLine($"{indent}[{agentName}] Calling: {toolStart.Name}");
break;
}
}Pattern 3: Filter by Specific Agent
Show events only from a specific agent in the hierarchy:
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
// Only show events from "WeatherExpert" agent
if (evt.ExecutionContext?.AgentName != "WeatherExpert") continue;
HandleEvent(evt);
}Pattern 4: Track Events by Agent
Group events by which agent emitted them:
var eventsByAgent = new Dictionary<string, List<AgentEvent>>();
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
var agentId = evt.ExecutionContext?.AgentId ?? "root";
if (!eventsByAgent.ContainsKey(agentId))
eventsByAgent[agentId] = new List<AgentEvent>();
eventsByAgent[agentId].Add(evt);
}
// Analyze events per agent
foreach (var (agentId, events) in eventsByAgent)
{
Console.WriteLine($"Agent {agentId}: {events.Count} events");
}Hierarchical UI Display
Show the agent hierarchy in your UI:
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
var ctx = evt.ExecutionContext;
if (ctx == null) continue;
switch (evt)
{
case MessageTurnStartedEvent:
// Show agent hierarchy
var chain = string.Join(" → ", ctx.AgentChain);
Console.WriteLine($"\n[Agent Chain: {chain}]");
break;
case ToolCallStartEvent toolStart:
Console.WriteLine($"[{ctx.AgentName} @ Depth {ctx.Depth}] Calling: {toolStart.Name}");
break;
case TextDeltaEvent delta:
// Prefix with agent name if subagent
if (ctx.IsSubAgent)
Console.Write($"[{ctx.AgentName}] {delta.Text}");
else
Console.Write(delta.Text);
break;
}
}Performance Considerations
Don't Check ExecutionContext Every Time
// INEFFICIENT: Checks context for every event
await foreach (var evt in agent.RunAsync(messages))
{
if (evt.ExecutionContext?.Depth > 2) continue;
if (evt.ExecutionContext?.IsSubAgent == false) continue;
// ...
}Filter Once at the Top
// EFFICIENT: Filter early
await foreach (var evt in agent.RunAsync(messages))
{
// Fast checks first
if (evt is IObservabilityEvent) continue;
if (evt.ExecutionContext?.IsSubAgent == true) continue;
// Only root agent events reach here
HandleEvent(evt);
}Debugging Multi-Agent Systems
Log Agent Activity
await foreach (var evt in agent.RunAsync(messages))
{
var ctx = evt.ExecutionContext;
var prefix = ctx != null
? $"[{ctx.AgentName} D{ctx.Depth}]"
: "[Root]";
_logger.LogDebug("{Prefix} {EventType}", prefix, evt.GetType().Name);
}Track SubAgent Spawning
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is NestedAgentInvokedEvent nested)
{
Console.WriteLine($"SubAgent spawned: {nested.AgentName}");
Console.WriteLine($" Parent: {evt.ExecutionContext?.ParentAgentId}");
Console.WriteLine($" Depth: {evt.ExecutionContext?.Depth}");
}
}When ExecutionContext is Null
ExecutionContext may be null in these scenarios:
- Single-agent applications - No nesting, so context is optional
- Legacy events - Events emitted before context was added
- Custom events - If you don't set ExecutionContext manually
Safe access pattern:
var isSubAgent = evt.ExecutionContext?.IsSubAgent ?? false;
var agentName = evt.ExecutionContext?.AgentName ?? "Unknown";
var depth = evt.ExecutionContext?.Depth ?? 0;Example: Multi-Agent Orchestrator
Complete example showing orchestrator + subagents:
// Orchestrator spawns specialized agents
var orchestrator = new AgentBuilder()
.WithName("Orchestrator")
.WithSubAgent("WeatherExpert")
.WithSubAgent("NewsExpert")
.Build();
await foreach (var evt in orchestrator.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
var ctx = evt.ExecutionContext;
var agentName = ctx?.AgentName ?? "Orchestrator";
var indent = new string(' ', (ctx?.Depth ?? 0) * 2);
switch (evt)
{
case TextDeltaEvent delta:
// Show which agent is speaking
if (ctx?.IsSubAgent == true)
Console.Write($"{indent}[{agentName}] {delta.Text}");
else
Console.Write(delta.Text);
break;
case ToolCallStartEvent toolStart:
Console.WriteLine($"{indent}[{agentName}] Calling: {toolStart.Name}");
break;
case MessageTurnFinishedEvent when ctx?.IsSubAgent == true:
Console.WriteLine($"{indent}[{agentName}] Done");
break;
case MessageTurnFinishedEvent:
Console.WriteLine("\n✓ All agents finished");
break;
}
}Common Patterns
Pattern: Collapse SubAgent Details
Only show SubAgent results, hide intermediate steps:
var subAgentResults = new Dictionary<string, string>();
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
var ctx = evt.ExecutionContext;
if (ctx?.IsSubAgent != true) continue;
switch (evt)
{
case MessageTurnFinishedEvent:
// SubAgent finished - show accumulated result
if (subAgentResults.TryGetValue(ctx.AgentId, out var result))
{
Console.WriteLine($"\n[{ctx.AgentName} Result]");
Console.WriteLine(result);
subAgentResults.Remove(ctx.AgentId);
}
break;
case TextDeltaEvent delta:
// Accumulate subagent output silently
var key = ctx.AgentId;
subAgentResults[key] = (subAgentResults.GetValueOrDefault(key) ?? "") + delta.Text;
break;
}
}See Also
- Events Overview - Event lifecycle basics
- Consuming Events - Event handling patterns
- Event Types Reference - All event types