Preserve recovery transcripts and add user-approved agent requests
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:
2026-09-25 11:58:34 +02:00
parent e011e36ef7
commit 7b2bcd3631
50 changed files with 2188 additions and 113 deletions
@@ -1,4 +1,5 @@
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Notifications;
using MeetingAssistant.Transcription;
using MeetingAssistant.Workflow;
using Microsoft.Agents.AI;
@@ -19,6 +20,8 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
private readonly IMeetingSummaryInstructionBuilder instructionBuilder;
private readonly IDictationWordStore dictationWordStore;
private readonly IMeetingWorkflowEngine meetingWorkflowEngine;
private readonly AgentApprovalService approvals;
private readonly WorkflowChangeRequestService workflowRequests;
public OpenAiMeetingSummaryAgentPipeline(
IOptions<MeetingAssistantOptions> options,
@@ -28,7 +31,9 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
IMeetingSummaryFailureWriter failureWriter,
IMeetingSummaryInstructionBuilder instructionBuilder,
IDictationWordStore dictationWordStore,
IMeetingWorkflowEngine meetingWorkflowEngine)
IMeetingWorkflowEngine meetingWorkflowEngine,
AgentApprovalService approvals,
WorkflowChangeRequestService workflowRequests)
{
this.options = options.Value;
this.loggerFactory = loggerFactory;
@@ -38,6 +43,8 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
this.instructionBuilder = instructionBuilder;
this.dictationWordStore = dictationWordStore;
this.meetingWorkflowEngine = meetingWorkflowEngine;
this.approvals = approvals;
this.workflowRequests = workflowRequests;
}
public async Task<MeetingSummaryRunResult> RunAsync(
@@ -60,7 +67,9 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
options,
dictationWordStore,
writeAudit,
meetingWorkflowEngine);
meetingWorkflowEngine,
approvals,
workflowRequests);
var tools = CreateTools(meetingTools);
var instructions = await instructionBuilder.BuildAsync(artifacts, options, cancellationToken);
using var compactionSummaryClient = LiteLlmResponsesChatClient.Create(
@@ -80,7 +89,11 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
.AsBuilder()
.UseFunctionInvocation(
loggerFactory,
client => client.FunctionInvoker = FunctionInvocationGuard.InvokeAsync)
client =>
{
client.FunctionInvoker = FunctionInvocationGuard.InvokeAsync;
client.AllowConcurrentInvocation = false;
})
.Build()
.AsAIAgent(
instructions,
@@ -176,7 +189,15 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
AIFunctionFactory.Create(
tools.ListProjects,
"list_projects",
"List the configured project folders bound to the current meeting note frontmatter."),
"List all configured projects with id, displayname and description. Catalog visibility does not grant access; project files require current meeting association."),
AIFunctionFactory.Create(
tools.RequestProjectAssociation,
"request_project_association",
"Request user approval to associate a project_id with this meeting. reason must be one line and at most 100 characters. Waits up to ten minutes; approval adds the project and returns its AGENTS.md if present."),
AIFunctionFactory.Create(
tools.RequestWorkflowChange,
"request_workflow_change",
"Request a workflow change with a one-line intention (maximum 100 characters) and a detailed_prompt describing the desired scenario/specification. Returns requested immediately; user approval separately opens and starts the settings agent."),
AIFunctionFactory.Create(
tools.ListProjectFiles,
"list_projectfiles",
@@ -188,7 +209,7 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
AIFunctionFactory.Create(
tools.WriteProjectFile,
"write_projectfile",
"Write a file inside an existing project folder. With no line arguments, append or create the file. Set replace_file=true only when intentionally replacing the whole file. With from and to, replace that inclusive 1-based line range. With insert, insert content at that 1-based line position."),
"Write a file inside a project associated with the current meeting. Root PROJECT.md requires YAML name and description (maximum 256 characters). With no line arguments, append or create. Set replace_file=true only for intentional whole-file replacement. With from and to, replace that inclusive 1-based line range; with insert, insert at that line."),
AIFunctionFactory.Create(
tools.ListPastProjectMeetings,
"list_past_project_meetings",