Bidirectional Events
Request/response patterns for permissions, clarifications, and user interaction
Bidirectional events enable middleware to ask questions and wait for answers. This powers permissions, clarifications, and other interactive workflows.
CRITICAL CONCEPT: Two Steps Required
The #1 beginner mistake: Handling the request but forgetting to send the response!
csharp
// WRONG: Agent hangs forever
case PermissionRequestEvent permission:
var approved = PromptUser("Allow this?");
// FORGOT TO SEND RESPONSE - Agent blocks until timeout!
break;
// CORRECT: Send response
case PermissionRequestEvent permission:
var approved = PromptUser("Allow this?");
await agent.SendResponseAsync(permission.PermissionId,
new PermissionResponseEvent
{
PermissionId = permission.PermissionId,
Approved = approved
});
break;How Bidirectional Events Work
Internal Flow
1. Middleware emits PermissionRequestEvent
└── Middleware calls WaitForResponseAsync() ← BLOCKS HERE
2. Your await foreach receives PermissionRequestEvent
└── You show a dialog to the user
3. User approves/denies in dialog
4. You MUST call agent.SendResponseAsync() ← OFTEN FORGOTTEN!
└── If skipped: Middleware hangs until timeout (30-60 seconds)
5. Middleware receives response
└── WaitForResponseAsync() returns, execution continuesIf you forget step 4: Your agent appears frozen, then eventually times out with an exception like:
TimeoutException: No response received for PermissionRequest 'abc123' within 30 secondsPermission Events
Request Permission
csharp
public record PermissionRequestEvent(
string PermissionId, // Unique ID for this request
string SourceName, // Middleware that requested it
string FunctionName, // Tool being called
string? Description, // Human-readable description
string CallId, // Tool call ID
IDictionary<string, object?>? Arguments // Tool arguments
) : AgentEvent, IPermissionEvent;Send Response
csharp
public record PermissionResponseEvent(
string PermissionId, // Matches request ID
string SourceName, // Matches request
bool Approved, // User's decision
string? Reason = null, // Optional reason
PermissionChoice Choice = PermissionChoice.Ask
) : AgentEvent, IPermissionEvent;Complete Example
csharp
await foreach (var evt in agent.RunAsync(messages))
{
if (evt is IObservabilityEvent) continue;
switch (evt)
{
case PermissionRequestEvent permission:
// Step 1: Show dialog to user
Console.WriteLine($"\nPermission Request:");
Console.WriteLine($" Function: {permission.FunctionName}");
Console.WriteLine($" Description: {permission.Description}");
if (permission.Arguments != null)
{
Console.WriteLine(" Arguments:");
foreach (var (key, value) in permission.Arguments)
{
Console.WriteLine($" {key}: {value}");
}
}
Console.Write("Allow? (y/n): ");
var input = Console.ReadLine();
var approved = input?.ToLower() == "y";
// Step 2: MUST send response!
await agent.SendResponseAsync(permission.PermissionId,
new PermissionResponseEvent
{
PermissionId = permission.PermissionId,
SourceName = permission.SourceName,
Approved = approved,
Reason = approved ? null : "User denied"
});
break;
case PermissionApprovedEvent approved:
Console.WriteLine($"✓ Permission granted: {approved.PermissionId}");
break;
case PermissionDeniedEvent denied:
Console.WriteLine($"✗ Permission denied: {denied.Reason}");
break;
}
}Clarification Events
Ask the user for additional information:
Request Clarification
csharp
public record ClarificationRequestEvent(
string RequestId,
string SourceName,
string Question, // Question to ask user
string? Context = null // Additional context
) : AgentEvent, IClarificationEvent;Send Response
csharp
public record ClarificationResponseEvent(
string RequestId, // Matches request
string SourceName,
string Answer // User's answer
) : AgentEvent, IClarificationEvent;Example
csharp
case ClarificationRequestEvent clarification:
Console.WriteLine($"\nClarification Needed:");
Console.WriteLine($" Question: {clarification.Question}");
if (clarification.Context != null)
{
Console.WriteLine($" Context: {clarification.Context}");
}
Console.Write("Your answer: ");
var answer = Console.ReadLine() ?? "";
// MUST send response!
await agent.SendResponseAsync(clarification.RequestId,
new ClarificationResponseEvent
{
RequestId = clarification.RequestId,
SourceName = clarification.SourceName,
Answer = answer
});
break;Continuation Events
Ask to continue beyond max iterations:
Request Continuation
csharp
public record ContinuationRequestEvent(
string ContinuationId,
string SourceName,
int CurrentIteration,
int MaxIterations
) : AgentEvent, IPermissionEvent;Example
csharp
case ContinuationRequestEvent continuation:
Console.WriteLine($"\nAgent reached max iterations ({continuation.MaxIterations})");
Console.WriteLine($"Current iteration: {continuation.CurrentIteration}");
Console.Write("Continue? (y/n): ");
var shouldContinue = Console.ReadLine()?.ToLower() == "y";
await agent.SendResponseAsync(continuation.ContinuationId,
new ContinuationResponseEvent
{
ContinuationId = continuation.ContinuationId,
SourceName = continuation.SourceName,
Approved = shouldContinue,
ExtensionAmount = shouldContinue ? 10 : 0 // Add 10 more iterations
});
break;Building Middleware with Bidirectional Events
This is an advanced topic. See Middleware/04.3 Middleware Events for complete guide.
Basic Pattern
csharp
public class PermissionMiddleware : IAgentMiddleware
{
public async Task<FunctionResult> WrapFunctionCallAsync(
FunctionRequest request,
IAgentContext context,
Func<FunctionRequest, Task<FunctionResult>> next)
{
var coordinator = context.EventCoordinator;
var permissionId = Guid.NewGuid().ToString();
// 1. Emit permission request
coordinator.Emit(new PermissionRequestEvent
{
PermissionId = permissionId,
SourceName = "PermissionMiddleware",
FunctionName = request.FunctionName,
CallId = request.CallId,
Arguments = request.Arguments
});
// 2. Wait for response (BLOCKS HERE)
var response = await coordinator.WaitForResponseAsync<PermissionResponseEvent>(
permissionId,
timeout: TimeSpan.FromSeconds(30),
cancellationToken: context.CancellationToken);
// 3. Check response
if (!response.Approved)
{
throw new PermissionDeniedException($"User denied: {response.Reason}");
}
// 4. Continue to next middleware
return await next(request);
}
}Web UI Patterns
React Example
typescript
import { useAgent } from 'hpd-agent-client/react';
function ChatComponent() {
const [permissionDialog, setPermissionDialog] = useState(null);
const { messages, sendMessage, sendResponse } = useAgent({
conversationId: 'my-conversation',
onEvent: (event) => {
switch (event.type) {
case 'PERMISSION_REQUEST':
// Step 1: Show dialog
setPermissionDialog({
id: event.permissionId,
functionName: event.functionName,
description: event.description,
arguments: event.arguments
});
break;
}
}
});
const handlePermissionResponse = async (approved: boolean) => {
// Step 2: Send response
await sendResponse(permissionDialog.id, {
type: 'PERMISSION_RESPONSE',
permissionId: permissionDialog.id,
approved,
reason: approved ? null : 'User denied'
});
setPermissionDialog(null);
};
return (
<div>
{/* Permission Dialog */}
{permissionDialog && (
<Dialog>
<h3>Permission Required</h3>
<p>Function: {permissionDialog.functionName}</p>
<p>{permissionDialog.description}</p>
<button onClick={() => handlePermissionResponse(true)}>Allow</button>
<button onClick={() => handlePermissionResponse(false)}>Deny</button>
</Dialog>
)}
{/* Chat UI */}
{messages.map((msg, i) => <div key={i}>{msg.content}</div>)}
</div>
);
}Common Mistakes
Forgetting to Send Response
csharp
// WRONG: Agent hangs
case PermissionRequestEvent permission:
var approved = PromptUser("Allow?");
// Missing SendResponseAsync!
break;Wrong Permission ID
csharp
// WRONG: Response doesn't match request
await agent.SendResponseAsync("wrong-id", // Wrong ID!
new PermissionResponseEvent
{
PermissionId = permission.PermissionId, // ✓ Correct ID
Approved = true
});Not Awaiting SendResponseAsync
csharp
// WRONG: Fire and forget - response might not be sent
agent.SendResponseAsync(...); // Not awaited!
break;
// CORRECT: Await the response
await agent.SendResponseAsync(...); // ✓ Awaited
break;Timeout Handling
Middleware typically waits 30-60 seconds for a response. If no response is received:
csharp
try
{
var response = await coordinator.WaitForResponseAsync<PermissionResponseEvent>(
permissionId,
timeout: TimeSpan.FromSeconds(30),
cancellationToken: ct);
}
catch (TimeoutException)
{
// No response received - deny by default
_logger.LogWarning("Permission request timed out");
throw new PermissionDeniedException("User did not respond in time");
}Best Practices
Always Send Response
csharp
case PermissionRequestEvent permission:
try
{
var approved = await PromptUserAsync(permission);
await agent.SendResponseAsync(permission.PermissionId,
new PermissionResponseEvent
{
PermissionId = permission.PermissionId,
SourceName = permission.SourceName,
Approved = approved
});
}
catch (Exception ex)
{
// Even on error, send response (deny)
await agent.SendResponseAsync(permission.PermissionId,
new PermissionResponseEvent
{
PermissionId = permission.PermissionId,
SourceName = permission.SourceName,
Approved = false,
Reason = $"Error: {ex.Message}"
});
}
break;Show Context to User
csharp
case PermissionRequestEvent permission:
Console.WriteLine($"\n{'='*60}");
Console.WriteLine($"PERMISSION REQUEST");
Console.WriteLine($"{'='*60}");
Console.WriteLine($"Function: {permission.FunctionName}");
Console.WriteLine($"Description: {permission.Description}");
if (permission.Arguments?.Count > 0)
{
Console.WriteLine($"\nArguments:");
foreach (var (key, value) in permission.Arguments)
{
Console.WriteLine($" {key}: {value}");
}
}
Console.WriteLine($"{'='*60}");
// ... prompt and respondHandle Async Prompts Properly
csharp
case PermissionRequestEvent permission:
// Good: Await async dialog
var approved = await ShowPermissionDialogAsync(permission);
await agent.SendResponseAsync(permission.PermissionId,
new PermissionResponseEvent
{
PermissionId = permission.PermissionId,
SourceName = permission.SourceName,
Approved = approved
});
break;See Also
- Events Overview - Event lifecycle basics
- Event Types Reference - All event types
- Middleware Events - Building middleware
- Building Console Apps - Console patterns
- Building Web Apps - Web UI patterns