Commit: ea4d873
Parent: 80cc152

Backup

Mårten Åsberg committed on 2026-08-13 at 14:36
src/Reviewer.Cli/FoundryLocal/FoundryLocalChatClient.cs +58 -12
diff --git a/src/Reviewer.Cli/FoundryLocal/FoundryLocalChatClient.cs b/src/Reviewer.Cli/FoundryLocal/FoundryLocalChatClient.cs
index 6193f16..2e77353 100644
@@ -15,6 +15,7 @@ namespace Reviewer.Cli.FoundryLocal;
internal sealed class FoundryLocalChatClient : IChatClient, IAsyncDisposable
{
private readonly ILoggerFactory loggerFactory;
private readonly ILogger<FoundryLocalChatClient> logger;
private readonly IServiceProvider serviceProvider;
@@ -23,11 +24,13 @@ internal sealed class FoundryLocalChatClient : IChatClient, IAsyncDisposable
private readonly Task<OpenAIChatClient> chatClientTask;
public FoundryLocalChatClient(
ILoggerFactory loggerFactory,
ILogger<FoundryLocalChatClient> logger,
IOptions<FoundryLocalOptions> options,
IServiceProvider serviceProvider
)
{
this.loggerFactory = loggerFactory;
this.logger = logger;
this.serviceProvider = serviceProvider;
@@ -38,7 +41,10 @@ internal sealed class FoundryLocalChatClient : IChatClient, IAsyncDisposable
private async Task<IModel> InitializeModel(FoundryLocalOptions options, CancellationToken cancellationToken)
{
await FoundryLocalManager.CreateAsync(new() { AppName = "ReviewCli" }, logger);
await FoundryLocalManager.CreateAsync(
new() { AppName = "ReviewCli" },
loggerFactory.CreateLogger("FoundryLocal")
);
var result = await FoundryLocalManager.Instance.DownloadAndRegisterEpsAsync(cancellationToken);
if (!result.Success)
@@ -69,23 +75,62 @@ internal sealed class FoundryLocalChatClient : IChatClient, IAsyncDisposable
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default
) =>
MapChatResponse(
await (await chatClientTask).CompleteChatAsync(messages.Select(MapChatMessage), cancellationToken)
)
{
var chatClient = await chatClientTask.WaitAsync(cancellationToken);
return MapChatResponse(
await chatClient.CompleteChatAsync(messages.Select(MapChatMessage), CancellationToken.None)
);
var mappedMessages = messages.Select(MapChatMessage).ToList();
var promptChars = mappedMessages.Sum(m => m.Content?.Length ?? 0);
logger.LogInformation(
"Starting chat completion. Messages: {MessageCount}, prompt length: ~{PromptChars} chars. Caller token cancelled: {CallerCancelled}",
mappedMessages.Count,
promptChars,
cancellationToken.IsCancellationRequested
);
try
{
// Pass CancellationToken.None on purpose: this completion should run to completion even if
// the workflow's token is cancelled. That way, if this throws "cancelled", we know the
// cancellation came from inside Foundry Local Core rather than from our token.
return MapChatResponse(await chatClient.CompleteChatAsync(mappedMessages, CancellationToken.None));
}
catch (Exception ex)
{
logger.LogError(
ex,
"CompleteChatAsync failed ({ExceptionType}). Caller token cancelled: {CallerCancelled}, init token cancelled: {InitCancelled}",
ex.GetType().FullName,
cancellationToken.IsCancellationRequested,
initializationCancellationTokenSource.IsCancellationRequested
);
throw;
}
}
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default
CancellationToken cancellationToken = default
)
{
var chatClient = await chatClientTask;
var stream = chatClient.CompleteChatStreamingAsync(messages.Select(MapChatMessage), cancellationToken);
await foreach (var message in stream)
{
yield return MapChatResponseUpdate(message);
}
// cancellationToken.Register(() =>
// {
// logger.LogWarning("Outer token cancelled: {StackTrace}", new StackTrace(true));
// });
// initializationCancellationTokenSource.Token.Register(() =>
// {
// logger.LogWarning("Inner token cancelled: {StackTrace}", new StackTrace(true));
// });
// var chatClient = await chatClientTask;
// var stream = chatClient.CompleteChatStreamingAsync(messages.Select(MapChatMessage), cancellationToken);
// await foreach (var message in stream)
// {
// yield return MapChatResponseUpdate(message);
// }
throw new NotImplementedException();
}
private static Betalgo.Ranul.OpenAI.ObjectModels.RequestModels.ChatMessage MapChatMessage(
@@ -133,6 +178,7 @@ internal sealed class FoundryLocalChatClient : IChatClient, IAsyncDisposable
}
else
{
logger.LogWarning("Disposing foundry local client");
await initializationCancellationTokenSource.CancelAsync();
}
}
src/Reviewer/ReviewService.cs +1 -1
diff --git a/src/Reviewer/ReviewService.cs b/src/Reviewer/ReviewService.cs
index b757290..c9d0ad6 100644
@@ -26,7 +26,7 @@ public sealed class ReviewService(
{
events.Add(e);
}
logger.LogInformation("Workflow completed with {EventCount} events", events);
// logger.LogInformation("Workflow completed with {EventCount} events", events);
var response = events.OfType<AgentResponseEvent>().LastOrDefault()?.Response;
if (response is not null)
src/Reviewer/Workflow/OrchestratorPromptBuilder.cs +3 -2
diff --git a/src/Reviewer/Workflow/OrchestratorPromptBuilder.cs b/src/Reviewer/Workflow/OrchestratorPromptBuilder.cs
index 50b3277..d1d602f 100644
@@ -16,10 +16,11 @@ internal sealed class OrchestratorPromptBuilder() : Executor<ReviewableDiff>(nam
CancellationToken cancellationToken = default
)
{
var chatMessage = new ChatMessage(ChatRole.User, reviewableDiff.ToString());
var diff = reviewableDiff.ToString();
var chatMessage = new ChatMessage(ChatRole.User, diff);
await context.SendMessageAsync(chatMessage, cancellationToken);
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken);
await context.SendMessageAsync(new TurnToken(), cancellationToken);
}
}
src/Reviewer/Workflow/WorkflowServiceCollectionExtensions.cs +1 -5
diff --git a/src/Reviewer/Workflow/WorkflowServiceCollectionExtensions.cs b/src/Reviewer/Workflow/WorkflowServiceCollectionExtensions.cs
index a4e2016..064f43c 100644
@@ -22,11 +22,7 @@ internal static class WorkflowServiceCollectionExtensions
var inputProcessor = sp.GetRequiredService<InputProcessor>();
var diffProcessor = sp.GetRequiredService<DiffProcessor>();
var orchestratorPromptBuilder = sp.GetRequiredService<OrchestratorPromptBuilder>();
var hostOptions = new AIAgentHostOptions
{
EmitAgentResponseEvents = true,
ForwardIncomingMessages = false,
};
var hostOptions = new AIAgentHostOptions { };
var orchestratorAgent = sp.GetRequiredKeyedService<AIAgent>(nameof(OrchestratorAgent))
.BindAsExecutor(hostOptions);
return new WorkflowBuilder(inputProcessor)