Public Access
Preserve recovery transcripts and add user-approved agent requests
PR and Push Build/Test / build-and-test (push) Successful in 12m0s
PR and Push Build/Test / build-and-test (push) Successful in 12m0s
Append successful offline recovery with a duplicate-warning marker and retain failed replay sources. Offer newly available microphones through confirmation notifications. Add project discovery metadata and blocking association approval, plus nonblocking workflow requests that start a separate settings-agent conversation. Include regression tests, OpenSpec changes, and verified local Release startup.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using MeetingAssistant.Notifications;
|
||||
using MeetingAssistant.Workflow;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace MeetingAssistant.Summary;
|
||||
|
||||
public sealed class WorkflowChangeRequestService(
|
||||
AgentApprovalService approvals,
|
||||
IWorkflowRulesEditorWindowService editor,
|
||||
ILogger<WorkflowChangeRequestService> logger) : BackgroundService
|
||||
{
|
||||
private readonly Channel<RequestDetails> requests = Channel.CreateUnbounded<RequestDetails>(
|
||||
new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
public string Request(string intention, string detailedPrompt, string meetingNotePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(intention) || intention.Length > 100 || intention.Contains('\r') || intention.Contains('\n'))
|
||||
return "Refused: intention must be a nonempty single line of at most 100 characters.";
|
||||
if (string.IsNullOrWhiteSpace(detailedPrompt))
|
||||
return "Refused: supply a detailed change prompt, preferably a scenario or specification.";
|
||||
var request = new RequestDetails(Guid.NewGuid().ToString("N"), intention, detailedPrompt, meetingNotePath);
|
||||
return requests.Writer.TryWrite(request) ? $"Requested: {request.Id}. User approval is pending."
|
||||
: "Refused: application is stopping.";
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var pending = new List<Task>();
|
||||
try
|
||||
{
|
||||
await foreach (var request in requests.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
pending.RemoveAll(task => task.IsCompleted);
|
||||
pending.Add(HandleAsync(request, stoppingToken));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { }
|
||||
finally
|
||||
{
|
||||
await Task.WhenAll(pending);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAsync(RequestDetails request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await approvals.ConfirmAsync($"Workflow change requested: {request.Intention}",
|
||||
$"The summarizer for {Path.GetFileName(request.MeetingNotePath)} requests a workflow change. Open the settings agent to apply it?",
|
||||
cancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
editor.ShowWithPrompt(request.DetailedPrompt);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { }
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Workflow request {RequestId} could not be handled", request.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
requests.Writer.TryComplete();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private sealed record RequestDetails(string Id, string Intention, string DetailedPrompt, string MeetingNotePath);
|
||||
}
|
||||
Reference in New Issue
Block a user