Skip to content

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 continues

If 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 seconds

Permission 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 respond

Handle 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

Released under the MIT License.