Custom Events
Creating your own event types with automatic serialization
HPD-Agent lets you create custom events by simply extending AgentEvent. The source generator automatically handles serialization, registration, and Native AOT support.
Quick Start
Creating a custom event is as simple as defining a record:
csharp
// That's it! Source generator handles everything else
public record AnalysisProgressEvent(
string Stage,
int PercentComplete
) : AgentEvent;The source generator automatically:
- Creates
EventTypes.Custom.ANALYSIS_PROGRESSconstant - Registers the event with
AgentEventSerializer - Adds
[JsonSerializable]attributes for Native AOT
Basic Custom Events
Simple Progress Event
csharp
public record AnalysisProgressEvent(
string Stage,
int PercentComplete
) : AgentEvent;
// Usage in middleware
coordinator.Emit(new AnalysisProgressEvent("Loading data", 25));
// Consuming
await foreach (var evt in agent.RunAsync(messages))
{
switch (evt)
{
case AnalysisProgressEvent progress:
UpdateProgressBar(progress.PercentComplete);
Console.WriteLine($"{progress.Stage}: {progress.PercentComplete}%");
break;
}
}Custom Notification Event
csharp
public record WorkflowStepCompletedEvent(
string StepName,
bool Success,
string? ErrorMessage = null,
TimeSpan Duration = default
) : AgentEvent;
// Usage
coordinator.Emit(new WorkflowStepCompletedEvent(
"Data validation",
Success: true,
Duration: TimeSpan.FromSeconds(2.5)
));Custom Event Type Names
By default, the event type name is auto-generated from the class name:
csharp
AnalysisProgressEvent → "ANALYSIS_PROGRESS"
WorkflowStepCompletedEvent → "WORKFLOW_STEP_COMPLETED"
MyCustomEvent → "MY_CUSTOM"Override with [EventType] Attribute
csharp
[EventType("CUSTOM_WORKFLOW_STEP")]
public record WorkflowStepEvent(
string StepName,
bool Success
) : AgentEvent;
// Serialized as: {"type": "CUSTOM_WORKFLOW_STEP", ...}What the Source Generator Provides
1. EventTypes Constants
csharp
// Generated: CustomEventTypes.g.cs
public static partial class EventTypes
{
public static class Custom
{
public const string ANALYSIS_PROGRESS = "ANALYSIS_PROGRESS";
public const string WORKFLOW_STEP_COMPLETED = "WORKFLOW_STEP_COMPLETED";
public const string CUSTOM_WORKFLOW_STEP = "CUSTOM_WORKFLOW_STEP";
}
}Usage:
csharp
if (eventType == EventTypes.Custom.ANALYSIS_PROGRESS)
{
// Handle analysis progress
}2. Automatic Serialization
csharp
var evt = new AnalysisProgressEvent("Processing", 50);
var json = AgentEventSerializer.ToJson(evt);
// Output:
// {
// "version": "1.0",
// "type": "ANALYSIS_PROGRESS",
// "stage": "Processing",
// "percentComplete": 50
// }3. Automatic Deserialization
csharp
var json = """
{
"version": "1.0",
"type": "ANALYSIS_PROGRESS",
"stage": "Loading",
"percentComplete": 75
}
""";
var evt = AgentEventSerializer.FromJson<AnalysisProgressEvent>(json);
Console.WriteLine($"{evt.Stage}: {evt.PercentComplete}%");Advanced Patterns
Custom Bidirectional Events
For request/response patterns:
csharp
public interface ICustomBidirectionalEvent : IBidirectionalEvent
{
string CustomRequestId { get; }
}
public record DataValidationRequestEvent(
string CustomRequestId,
string SourceName,
string DataToValidate
) : AgentEvent, ICustomBidirectionalEvent;
public record DataValidationResponseEvent(
string CustomRequestId,
string SourceName,
bool IsValid,
string? ValidationMessage = null
) : AgentEvent, ICustomBidirectionalEvent;Usage in middleware:
csharp
var requestId = Guid.NewGuid().ToString();
coordinator.Emit(new DataValidationRequestEvent
{
CustomRequestId = requestId,
SourceName = "ValidationMiddleware",
DataToValidate = inputData
});
var response = await coordinator.WaitForResponseAsync<DataValidationResponseEvent>(
requestId,
timeout: TimeSpan.FromSeconds(30),
cancellationToken: ct);
if (!response.IsValid)
{
throw new ValidationException(response.ValidationMessage);
}Custom Observability Events
For internal diagnostics:
csharp
public record PerformanceMetricEvent(
string OperationName,
TimeSpan Duration,
long MemoryUsed
) : AgentEvent, IObservabilityEvent;
// This will be automatically filtered by:
// if (evt is IObservabilityEvent) continue;Custom Priority Events
For urgent events:
csharp
public record EmergencyStopEvent(
string Reason
) : AgentEvent
{
public EmergencyStopEvent(string Reason) : this()
{
this.Reason = Reason;
Priority = EventPriority.Immediate; // Bypass queued events
}
}Event Properties
All custom events inherit these from AgentEvent:
csharp
public record MyCustomEvent(string Data) : AgentEvent
{
public MyCustomEvent(string Data) : this()
{
this.Data = Data;
// Optional: Set properties
Priority = EventPriority.Control;
CanInterrupt = false; // Don't drop on cancellation
Direction = EventDirection.Upstream;
StreamId = "my-stream";
}
}Available Properties
ExecutionContext- Agent that emitted this eventPriority-Immediate,Control,Normal,BackgroundSequenceNumber- Auto-assigned by coordinatorDirection-DownstreamorUpstreamStreamId- For grouping related eventsCanInterrupt- Can be dropped on stream interruption
Emitting Custom Events
From Middleware
csharp
public class AnalysisMiddleware : IAgentMiddleware
{
public async Task<ModelResponse> WrapModelCallAsync(
ModelRequest request,
IAgentContext context,
Func<ModelRequest, Task<ModelResponse>> next)
{
var coordinator = context.EventCoordinator;
// Emit custom progress events
coordinator.Emit(new AnalysisProgressEvent("Analyzing", 0));
// Do work...
await Task.Delay(1000);
coordinator.Emit(new AnalysisProgressEvent("Processing", 50));
var response = await next(request);
coordinator.Emit(new AnalysisProgressEvent("Complete", 100));
return response;
}
}From Tools/Toolkits
csharp
public class MyTools
{
private readonly IEventCoordinator _coordinator;
public MyTools(IEventCoordinator coordinator)
{
_coordinator = coordinator;
}
[AIFunction]
public async Task<string> ProcessData(string data)
{
_coordinator.Emit(new AnalysisProgressEvent("Starting", 0));
// Process...
await Task.Delay(1000);
_coordinator.Emit(new AnalysisProgressEvent("Done", 100));
return "Processed";
}
}Consuming Custom Events
Same pattern as built-in events:
csharp
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
switch (evt)
{
// Built-in events
case TextDeltaEvent delta:
Console.Write(delta.Text);
break;
// Custom events
case AnalysisProgressEvent progress:
UpdateProgressBar(progress.PercentComplete);
break;
case WorkflowStepCompletedEvent step:
if (step.Success)
Console.WriteLine($"✓ {step.StepName}");
else
Console.WriteLine($"✗ {step.StepName}: {step.ErrorMessage}");
break;
}
}Source Generator Diagnostics
The source generator provides helpful diagnostics:
| Code | Description | Fix |
|---|---|---|
| HPD010 | Duplicate event type discriminator | Use [EventType("UNIQUE_NAME")] to resolve |
| HPD011 | Generic events not supported | Remove generic type parameters |
| HPD012 | Abstract events skipped (info only) | Concrete events will be generated |
Example: HPD010
csharp
// ERROR: Both generate "MY_EVENT" discriminator
public record MyEvent() : AgentEvent;
public record MyEvent2() : AgentEvent; // Naming collision!
// FIX: Use explicit discriminators
[EventType("MY_EVENT_V1")]
public record MyEvent() : AgentEvent;
[EventType("MY_EVENT_V2")]
public record MyEvent2() : AgentEvent;Native AOT Support
The source generator automatically adds [JsonSerializable] attributes for Native AOT:
csharp
// Generated: CustomEventJsonContext.g.cs
[JsonSerializable(typeof(AnalysisProgressEvent))]
[JsonSerializable(typeof(WorkflowStepCompletedEvent))]
// ... etc
partial class CustomEventJsonContext : JsonSerializerContext
{
}No manual registration needed!
Best Practices
Use Records
csharp
// GOOD: Records are immutable and work well with pattern matching
public record MyEvent(string Data) : AgentEvent;Name Events Descriptively
csharp
// GOOD: Clear what this event represents
public record DocumentProcessingCompletedEvent(...)
public record ValidationFailedEvent(...)
public record AnalysisProgressEvent(...)
// BAD: Vague names
public record Event1(...)
public record CustomEvent(...)Include Relevant Data
csharp
// GOOD: All context needed to handle the event
public record ProcessingErrorEvent(
string OperationName,
string ErrorMessage,
Exception Exception,
int RetryCount
) : AgentEvent;
// BAD: Missing context
public record ErrorEvent(string Message) : AgentEvent;Use Marker Interfaces
csharp
// GOOD: Categorize related events
public interface IWorkflowEvent
{
string WorkflowId { get; }
}
public record WorkflowStartedEvent(
string WorkflowId,
string WorkflowName
) : AgentEvent, IWorkflowEvent;
public record WorkflowCompletedEvent(
string WorkflowId,
bool Success
) : AgentEvent, IWorkflowEvent;
// Filter by interface
if (evt is IWorkflowEvent workflowEvt)
{
TrackWorkflow(workflowEvt.WorkflowId);
}Don't Use Generic Events
csharp
// NOT SUPPORTED: Generic events don't work
public record GenericEvent<T>(T Data) : AgentEvent;
// USE: Concrete types instead
public record StringDataEvent(string Data) : AgentEvent;
public record IntDataEvent(int Data) : AgentEvent;Complete Example
csharp
// Define custom events
public record AnalysisStartedEvent(
string AnalysisId,
string DataSource
) : AgentEvent;
public record AnalysisProgressEvent(
string AnalysisId,
string Stage,
int PercentComplete
) : AgentEvent;
public record AnalysisCompletedEvent(
string AnalysisId,
string Result,
TimeSpan Duration
) : AgentEvent;
// Use in middleware
public class AnalysisMiddleware : IAgentMiddleware
{
public async Task<ModelResponse> WrapModelCallAsync(
ModelRequest request,
IAgentContext context,
Func<ModelRequest, Task<ModelResponse>> next)
{
var analysisId = Guid.NewGuid().ToString();
var coordinator = context.EventCoordinator;
coordinator.Emit(new AnalysisStartedEvent(analysisId, "UserInput"));
coordinator.Emit(new AnalysisProgressEvent(analysisId, "Validation", 25));
// ... validate
coordinator.Emit(new AnalysisProgressEvent(analysisId, "Processing", 50));
var response = await next(request);
coordinator.Emit(new AnalysisProgressEvent(analysisId, "Finalizing", 75));
// ... finalize
coordinator.Emit(new AnalysisCompletedEvent(
analysisId,
"Success",
TimeSpan.FromSeconds(3)));
return response;
}
}
// Consume in UI
await foreach (var evt in agent.RunAsync(messages))
{
switch (evt)
{
case AnalysisStartedEvent started:
Console.WriteLine($"Analysis {started.AnalysisId} started");
break;
case AnalysisProgressEvent progress:
UpdateProgressBar(progress.PercentComplete);
Console.WriteLine($"{progress.Stage}: {progress.PercentComplete}%");
break;
case AnalysisCompletedEvent completed:
Console.WriteLine($"Completed in {completed.Duration.TotalSeconds}s");
break;
}
}See Also
- Events Overview - Event lifecycle basics
- Event Types Reference - Built-in event types
- Bidirectional Events - Request/response patterns
- Middleware Events - Emitting from middleware