Public Access
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.
71 lines
2.9 KiB
C#
71 lines
2.9 KiB
C#
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);
|
|
}
|