Handle settings logs agent failures
PR and Push Build/Test / build-and-test (push) Successful in 9m8s

This commit is contained in:
2026-06-09 17:10:06 +02:00
parent 8af89a622d
commit ecdd23bde7
8 changed files with 147 additions and 15 deletions
@@ -126,12 +126,14 @@ public sealed class LiteLlmResponsesChatClientTests
}
""")
});
using var client = CreateClient(handler, reconnectionAttempts: 1);
var retryCount = 0;
using var client = CreateClient(handler, reconnectionAttempts: 1, retrying: () => retryCount++);
var response = await client.GetResponseAsync([new ChatMessage(ChatRole.User, "write summary")]);
Assert.Equal("Done.", response.Text);
Assert.Equal(2, handler.RequestCount);
Assert.Equal(1, retryCount);
}
[Fact]
@@ -303,7 +305,8 @@ public sealed class LiteLlmResponsesChatClientTests
private static LiteLlmResponsesChatClient CreateClient(
HttpMessageHandler handler,
int reconnectionAttempts,
LiteLlmResponsesCompactionOptions? compactionOptions = null)
LiteLlmResponsesCompactionOptions? compactionOptions = null,
Action? retrying = null)
{
return new LiteLlmResponsesChatClient(
new HttpClient(handler)
@@ -316,7 +319,8 @@ public sealed class LiteLlmResponsesChatClientTests
reasoningEffort: "none",
reconnectionAttempts,
TimeSpan.Zero,
compactionOptions);
compactionOptions,
retrying: retrying);
}
private sealed class SequencedHttpMessageHandler : HttpMessageHandler
@@ -853,6 +853,52 @@ public sealed class WorkflowRulesEditorTests
Assert.Equal("Updated rules.", viewModel.Messages[1].Content);
}
[Fact]
public async Task ViewModelShowsAgentErrorWhenRequestTimesOut()
{
var viewModel = new WorkflowRulesEditorChatViewModel(
new ThrowingRulesEditorPipeline(new TaskCanceledException("The request timed out.")))
{
Draft = "check logs"
};
await viewModel.SendAsync();
Assert.False(viewModel.IsThinking);
Assert.Equal("", viewModel.Draft);
Assert.Equal(
[
new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, "check logs"),
new WorkflowRulesEditorChatMessage(
WorkflowRulesEditorChatRole.Agent,
"Settings and logs failed: The request timed out.")
],
viewModel.Messages.ToArray());
}
[Fact]
public async Task ViewModelShowsReconnectStatusReportedByPipeline()
{
var pipeline = new StatusReportingRulesEditorPipeline("Updated rules.");
var viewModel = new WorkflowRulesEditorChatViewModel(pipeline)
{
Draft = "check logs"
};
var sendTask = viewModel.SendAsync();
await pipeline.Reported.Task.WaitAsync(TimeSpan.FromSeconds(2));
Assert.True(viewModel.IsThinking);
Assert.Equal("Reconnecting...", viewModel.ActivityMessage);
pipeline.Release.SetResult();
await sendTask.WaitAsync(TimeSpan.FromSeconds(2));
Assert.False(viewModel.IsThinking);
Assert.Equal("Thinking...", viewModel.ActivityMessage);
Assert.Equal("Updated rules.", viewModel.Messages[1].Content);
}
[Theory]
[InlineData(0, 500, 0, true)]
[InlineData(0, 500, 400, true)]
@@ -1149,7 +1195,8 @@ public sealed class WorkflowRulesEditorTests
public async Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
Action<string>? statusChanged = null)
{
Started.SetResult();
await Release.Task.WaitAsync(cancellationToken);
@@ -1162,6 +1209,56 @@ public sealed class WorkflowRulesEditorTests
}
}
private sealed class ThrowingRulesEditorPipeline : IWorkflowRulesEditorChatPipeline
{
private readonly Exception exception;
public ThrowingRulesEditorPipeline(Exception exception)
{
this.exception = exception;
}
public Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<string>? statusChanged = null)
{
throw exception;
}
}
private sealed class StatusReportingRulesEditorPipeline : IWorkflowRulesEditorChatPipeline
{
private readonly string response;
public StatusReportingRulesEditorPipeline(string response)
{
this.response = response;
}
public TaskCompletionSource Reported { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource Release { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public async Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<string>? statusChanged = null)
{
statusChanged?.Invoke("Reconnecting...");
Reported.SetResult();
await Release.Task.WaitAsync(cancellationToken);
return new WorkflowRulesEditorChatResult(
response,
conversation
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, userMessage))
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.Agent, response))
.ToList());
}
}
private sealed class CapturingLogger : ILogger
{
public List<string> Messages { get; } = [];
@@ -21,6 +21,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
private readonly TimeSpan reconnectionDelay;
private readonly LiteLlmResponsesCompactionOptions? compactionOptions;
private readonly ILogger? logger;
private readonly Action? retrying;
private int responsesRequestCount;
public LiteLlmResponsesChatClient(
@@ -33,7 +34,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
TimeSpan reconnectionDelay,
LiteLlmResponsesCompactionOptions? compactionOptions = null,
ILogger? logger = null,
bool firstRequestIsUser = true)
bool firstRequestIsUser = true,
Action? retrying = null)
: this(
new HttpClient { BaseAddress = NormalizeEndpoint(endpoint) },
apiKey,
@@ -44,7 +46,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
reconnectionDelay,
compactionOptions,
logger,
firstRequestIsUser)
firstRequestIsUser,
retrying)
{
}
@@ -58,7 +61,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
TimeSpan reconnectionDelay,
LiteLlmResponsesCompactionOptions? compactionOptions = null,
ILogger? logger = null,
bool firstRequestIsUser = true)
bool firstRequestIsUser = true,
Action? retrying = null)
{
this.httpClient = httpClient;
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
@@ -69,6 +73,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
this.reconnectionDelay = reconnectionDelay > TimeSpan.Zero ? reconnectionDelay : TimeSpan.Zero;
this.compactionOptions = compactionOptions;
this.logger = logger;
this.retrying = retrying;
responsesRequestCount = firstRequestIsUser ? 0 : 1;
}
@@ -377,6 +382,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
lastException = exception;
}
retrying?.Invoke();
await DelayBeforeRetryAsync(cancellationToken).ConfigureAwait(false);
}
@@ -19,5 +19,6 @@ public interface IWorkflowRulesEditorChatPipeline
Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken);
CancellationToken cancellationToken,
Action<string>? statusChanged = null);
}
@@ -58,7 +58,8 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
public async Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
Action<string>? statusChanged = null)
{
if (string.IsNullOrWhiteSpace(userMessage))
{
@@ -106,7 +107,8 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
agentOptions.ReconnectionDelay,
compactionOptions,
logger,
firstRequestIsUser: true);
firstRequestIsUser: true,
retrying: () => statusChanged?.Invoke("Reconnecting..."));
var functionClient = chatClient
.AsBuilder()
.UseFunctionInvocation(loggerFactory)
@@ -17,6 +17,8 @@ public sealed class WorkflowRulesEditorChatViewModel
public bool IsThinking { get; private set; }
public string ActivityMessage { get; private set; } = "Thinking...";
public event EventHandler? Changed;
public async Task SendAsync(CancellationToken cancellationToken = default)
@@ -31,6 +33,7 @@ public sealed class WorkflowRulesEditorChatViewModel
var priorConversation = Messages.ToList();
Messages.Add(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, prompt));
IsThinking = true;
ActivityMessage = "Thinking...";
OnChanged();
try
@@ -38,26 +41,39 @@ public sealed class WorkflowRulesEditorChatViewModel
var result = await pipeline.SendAsync(
priorConversation,
prompt,
cancellationToken);
cancellationToken,
SetActivityMessage);
Messages.Clear();
foreach (var message in result.Conversation)
{
Messages.Add(message);
}
}
catch (Exception exception) when (exception is not OperationCanceledException)
catch (Exception exception) when (exception is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
{
Messages.Add(new WorkflowRulesEditorChatMessage(
WorkflowRulesEditorChatRole.Agent,
$"Rules editor failed: {exception.Message}"));
$"Settings and logs failed: {exception.Message}"));
}
finally
{
IsThinking = false;
ActivityMessage = "Thinking...";
OnChanged();
}
}
private void SetActivityMessage(string message)
{
if (!IsThinking || string.IsNullOrWhiteSpace(message))
{
return;
}
ActivityMessage = message;
OnChanged();
}
private void OnChanged()
{
Changed?.Invoke(this, EventArgs.Empty);
@@ -297,7 +297,7 @@ internal sealed class WorkflowRulesEditorWpfWindow : Window
{
conversationPanel.Children.Add(new TextBlock
{
Text = "Thinking...",
Text = viewModel.ActivityMessage,
Foreground = MutedText,
Margin = new Thickness(4, 2, 4, 2)
});
+7 -1
View File
@@ -270,7 +270,7 @@ The tray icon menu SHALL show every configured launch profile when recording can
When the user selects `Edit rules and identities`, Meeting Assistant SHALL open a basic MewUI chat window for editing the configured workflow rules file and speaker identities.
The chat window SHALL be titled `Edit rules and identities`, SHALL display user and assistant messages as visually distinct cards, SHALL display basic markdown emphasis, inline code, fenced code blocks, pipe tables rendered as grid views, and line breaks in agent responses, SHALL display a plain `Thinking...` line while the agent is working, SHALL provide a multiline text input at the bottom with placeholder text for asking to make a rule or list identities, SHALL send on Enter, SHALL insert a newline on Shift+Enter, and SHALL provide an explicit Send button.
The chat window SHALL be titled `Edit rules and identities`, SHALL display user and assistant messages as visually distinct cards, SHALL display basic markdown emphasis, inline code, fenced code blocks, pipe tables rendered as grid views, and line breaks in agent responses, SHALL display a plain `Thinking...` line while the agent is working, MAY replace that line with `Reconnecting...` while retrying a transient agent request, SHALL provide a multiline text input at the bottom with placeholder text for asking to make a rule or list identities, SHALL send on Enter, SHALL insert a newline on Shift+Enter, and SHALL provide an explicit Send button.
When a new chat message or thinking state is appended, the chat window SHALL scroll to the bottom of the newly rendered content if the conversation was already near the bottom or did not need scrolling before the append.
@@ -350,6 +350,12 @@ Meeting Assistant SHALL provide a diagnostic endpoint that opens the workflow ru
- **AND** the first model request for that turn is marked as user-initiated
- **AND** the final assistant response replaces the thinking line
#### Scenario: Rules editor displays agent request failures
- **GIVEN** the rules editor chat window is open
- **WHEN** the agent request fails or times out without caller cancellation
- **THEN** the user message remains in the conversation
- **AND** the thinking line is replaced by a visible assistant error message
#### Scenario: Chat auto-scrolls after appended content
- **GIVEN** the rules editor chat window content fits without scrolling or is already near the bottom
- **WHEN** a new user or assistant message is appended