From 1bcca679b27b9de9f1823e41b58430e5f3cd6868 Mon Sep 17 00:00:00 2001 From: DevMando Date: Sat, 22 Aug 2026 19:07:40 -0700 Subject: [PATCH 1/2] Move Desktop off Semantic Kernel onto Microsoft.Extensions.AI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine dropped Semantic Kernel in the Agent Framework migration, and Desktop had been compiling against SK types it never declared — it inherited them transitively through the harness project reference. When the harness stopped bringing SK in, Desktop stopped building (CS0234). Two separate breaks, both fixed here: The history boundary. AIService.GetHistoryAsync now returns MEAI ChatMessage, so IAiService, AiServiceAdapter, the fake in ResponseStreamerTests, and HistorySummarizer all move to that type. MEAI's FunctionResultContent carries only a call id where SK's carried the function name, so HistorySummarizer now builds a call-id-to-name map per walk to keep tool lines reading "read_file -> ..." instead of a bare id — the same approach the engine took in SynthesizeHistorySummary. Desktop's own Ollama callers. NoteAssistant, SnapshotEnhancer, and SkillAuthor each built a throwaway SK Kernel to reach Ollama directly. They now use OllamaApiClient as an IChatClient, with ChatOptions in place of OllamaPromptExecutionSettings. Prompts, temperatures, and streaming semantics are unchanged; the clients are disposed now, which the kernels never were. Both projects declare Microsoft.Extensions.AI and OllamaSharp explicitly rather than leaning on the harness's transitive copies. Depending on packages we never declared is what turned an engine-side removal into a Desktop build failure, and the comments in both csproj files say so. --- CHANGELOG.md | 7 ++- .../HistorySummarizerTests.cs | 25 +++++----- .../MandoCode.Desktop.Tests.csproj | 10 ++-- .../ResponseStreamerTests.cs | 4 +- .../Controls/ChatTabView.Transcript.cs | 2 +- .../MandoCode.Desktop.csproj | 11 ++++ .../Services/AiServiceAdapter.cs | 4 +- .../Services/HistorySummarizer.cs | 50 ++++++++++++++----- src/MandoCode.Desktop/Services/IAiService.cs | 4 +- .../Services/NoteAssistant.cs | 35 +++++-------- src/MandoCode.Desktop/Services/SkillAuthor.cs | 24 ++++----- .../Services/SnapshotEnhancer.cs | 40 +++++++-------- 12 files changed, 121 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4421a..0bf25a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,12 @@ it moves 0.14.1 → 0.15.0. Desktop from source, where the engine project now resolves its .NET 10 build. - **Engine dependencies moved to current releases**, including Model Context Protocol 2.2.0 and YamlDotNet 18.1.0. -- **Pinned engine commit: `6eb3fc3`** (engine 0.15.0). The exact engine each Desktop release +- **Desktop moved off Semantic Kernel too.** The notes assistant, the snapshot summarizer, and + the skill author each opened their own connection to Ollama through Semantic Kernel; they now + use the same Microsoft.Extensions.AI client the engine standardized on. Same prompts, same + temperatures, same behavior — but Desktop no longer depends on a framework the engine has + removed. Snapshot recaps and note replies are the surfaces to sanity-check. +- **Pinned engine commit: `3b5f667`** (engine 0.15.0). The exact engine each Desktop release ships is recorded by the `MandoCode` submodule. ## [0.14.1] — 2026-07-28 diff --git a/src/MandoCode.Desktop.Tests/HistorySummarizerTests.cs b/src/MandoCode.Desktop.Tests/HistorySummarizerTests.cs index 392c953..b7c5563 100644 --- a/src/MandoCode.Desktop.Tests/HistorySummarizerTests.cs +++ b/src/MandoCode.Desktop.Tests/HistorySummarizerTests.cs @@ -1,6 +1,5 @@ using MandoCode.Desktop.Services; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.Extensions.AI; using Xunit; namespace MandoCode.Desktop.Tests; @@ -12,22 +11,22 @@ namespace MandoCode.Desktop.Tests; /// public sealed class HistorySummarizerTests { - private static ChatMessageContent Sys(string t) => new(AuthorRole.System, t); - private static ChatMessageContent Usr(string t) => new(AuthorRole.User, t); - private static ChatMessageContent Asst(string t) => new(AuthorRole.Assistant, t); + private static ChatMessage Sys(string t) => new(ChatRole.System, t); + private static ChatMessage Usr(string t) => new(ChatRole.User, t); + private static ChatMessage Asst(string t) => new(ChatRole.Assistant, t); [Fact] public void HasContent_False_WhenOnlySystemPrompt() - => Assert.False(HistorySummarizer.HasContent(new List { Sys("you are helpful") })); + => Assert.False(HistorySummarizer.HasContent(new List { Sys("you are helpful") })); [Fact] public void HasContent_True_WhenUserSpoke() - => Assert.True(HistorySummarizer.HasContent(new List { Sys("sys"), Usr("hello") })); + => Assert.True(HistorySummarizer.HasContent(new List { Sys("sys"), Usr("hello") })); [Fact] public void Full_SkipsSystemPrompt_AndKeepsBothTurns() { - var history = new List { Sys("SECRET SYSTEM"), Usr("hi there"), Asst("hey back") }; + var history = new List { Sys("SECRET SYSTEM"), Usr("hi there"), Asst("hey back") }; var text = HistorySummarizer.Full(history); @@ -39,16 +38,16 @@ public void Full_SkipsSystemPrompt_AndKeepsBothTurns() [Fact] public void Full_ReturnsPlaceholder_WhenNothingToSummarize() => Assert.Equal("(no prior activity captured)", - HistorySummarizer.Full(new List { Sys("sys") })); + HistorySummarizer.Full(new List { Sys("sys") })); [Fact] public void Full_DescribesFunctionCall_WhenTextIsEmpty() { - var toolTurn = new ChatMessageContent(AuthorRole.Assistant, content: null) + var toolTurn = new ChatMessage(ChatRole.Assistant, new List { - Items = { new FunctionCallContent("read_file", arguments: new KernelArguments { ["path"] = "Program.cs" }) } - }; - var history = new List { Sys("sys"), toolTurn }; + new FunctionCallContent("call-1", "read_file", new Dictionary { ["path"] = "Program.cs" }) + }); + var history = new List { Sys("sys"), toolTurn }; var text = HistorySummarizer.Full(history); diff --git a/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj b/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj index 41f4259..69498a6 100644 --- a/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj +++ b/src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj @@ -11,13 +11,17 @@ + + + config-clone tests) and DiffModels transitively — which is why the direct DiffModels + compile-include below was removed. (Chat history types now come from the + Microsoft.Extensions.AI.Abstractions reference above, not from the harness.) --> diff --git a/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs b/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs index 01d3fc6..d968d0d 100644 --- a/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs +++ b/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs @@ -3,7 +3,7 @@ using MandoCode.Desktop.ViewModels; using MandoCode.Models; using MandoCode.Services; -using Microsoft.SemanticKernel; +using Microsoft.Extensions.AI; using Xunit; namespace MandoCode.Desktop.Tests; @@ -66,7 +66,7 @@ public event Action? OnFunctionCompleted { add { } remo public int TryRestoreHistoryJson(string json) => throw new NotSupportedException(); public Task EnterLearnModeAsync() => throw new NotSupportedException(); public Task ClearHistoryAsync() => throw new NotSupportedException(); - public Task> GetHistoryAsync() => throw new NotSupportedException(); + public Task> GetHistoryAsync() => throw new NotSupportedException(); } private static (ResponseStreamer streamer, List blocks) Make(FakeAiService ai) diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs b/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs index 4718e6d..53c2902 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs @@ -72,7 +72,7 @@ public async Task RestoreConversationMemoryAsync() { try { - // 1) Full fidelity: rehydrate the harness's ChatHistory verbatim. + // 1) Full fidelity: rehydrate the harness's chat history verbatim. var historyJson = SessionHistoryStore.Load(Session.PersistKey); if (historyJson != null) { diff --git a/src/MandoCode.Desktop/MandoCode.Desktop.csproj b/src/MandoCode.Desktop/MandoCode.Desktop.csproj index 4235a08..2265141 100644 --- a/src/MandoCode.Desktop/MandoCode.Desktop.csproj +++ b/src/MandoCode.Desktop/MandoCode.Desktop.csproj @@ -56,6 +56,17 @@ + + + + diff --git a/src/MandoCode.Desktop/Services/AiServiceAdapter.cs b/src/MandoCode.Desktop/Services/AiServiceAdapter.cs index a8474e8..ea20bd5 100644 --- a/src/MandoCode.Desktop/Services/AiServiceAdapter.cs +++ b/src/MandoCode.Desktop/Services/AiServiceAdapter.cs @@ -1,6 +1,6 @@ using MandoCode.Models; using MandoCode.Services; -using Microsoft.SemanticKernel; +using Microsoft.Extensions.AI; namespace MandoCode.Desktop.Services; @@ -58,5 +58,5 @@ public Func>? OnCommandApprovalRequested public int TryRestoreHistoryJson(string json) => _ai.TryRestoreHistoryJson(json); public Task EnterLearnModeAsync() => _ai.EnterLearnModeAsync(); public Task ClearHistoryAsync() => _ai.ClearHistoryAsync(); - public Task> GetHistoryAsync() => _ai.GetHistoryAsync(); + public Task> GetHistoryAsync() => _ai.GetHistoryAsync(); } diff --git a/src/MandoCode.Desktop/Services/HistorySummarizer.cs b/src/MandoCode.Desktop/Services/HistorySummarizer.cs index 38d82b9..f50c3e2 100644 --- a/src/MandoCode.Desktop/Services/HistorySummarizer.cs +++ b/src/MandoCode.Desktop/Services/HistorySummarizer.cs @@ -1,5 +1,5 @@ using System.Text; -using Microsoft.SemanticKernel; +using Microsoft.Extensions.AI; namespace MandoCode.Desktop.Services; @@ -11,32 +11,38 @@ namespace MandoCode.Desktop.Services; /// (The deterministic per-line/overall truncation this once did — a port of the harness's compaction /// summary — was dropped when snapshots moved to LLM summaries: it kept the oldest turns and cut the /// most recent, which is backwards for a resumption recap.) +/// +/// Messages are (Microsoft.Extensions.AI) since the engine moved off +/// Semantic Kernel. Tool activity lives in alongside any text, so +/// walking Contents — not the Text convenience view — is what surfaces a tool turn. /// public static class HistorySummarizer { private const string Empty = "(no prior activity captured)"; /// True if there is anything worth snapshotting beyond the system prompt at index 0. - public static bool HasContent(IReadOnlyList history, int startIndex = 1) + public static bool HasContent(IReadOnlyList history, int startIndex = 1) { + var names = MapCallIdsToNames(history, startIndex); for (int i = Math.Max(0, startIndex); i < history.Count; i++) - if (!string.IsNullOrEmpty(FormatMessage(history[i], int.MaxValue))) return true; + if (!string.IsNullOrEmpty(FormatMessage(history[i], names, int.MaxValue))) return true; return false; } /// Full untruncated dump — the text handed to the summarizer. - public static string Full(IReadOnlyList history, int startIndex = 1) + public static string Full(IReadOnlyList history, int startIndex = 1) => Build(history, startIndex, lineMax: int.MaxValue, maxChars: int.MaxValue); - private static string Build(IReadOnlyList history, int startIndex, int lineMax, int maxChars) + private static string Build(IReadOnlyList history, int startIndex, int lineMax, int maxChars) { + var names = MapCallIdsToNames(history, startIndex); var sb = new StringBuilder(); for (int i = Math.Max(0, startIndex); i < history.Count; i++) { - var line = FormatMessage(history[i], lineMax); + var line = FormatMessage(history[i], names, lineMax); if (string.IsNullOrEmpty(line)) continue; - sb.Append('[').Append(history[i].Role.Label).Append("] ").AppendLine(line); + sb.Append('[').Append(history[i].Role.Value).Append("] ").AppendLine(line); if (sb.Length > maxChars) { sb.AppendLine("... (older entries truncated)"); @@ -46,17 +52,32 @@ private static string Build(IReadOnlyList history, int start return sb.Length == 0 ? Empty : sb.ToString().TrimEnd(); } + /// + /// Call id → function name, built once per walk. Unlike Semantic Kernel's, MEAI's + /// carries no function name — only the call id it shares + /// with its matching — so a result line would otherwise read + /// as a bare id instead of "read_file → ...". + /// + private static Dictionary MapCallIdsToNames(IReadOnlyList history, int startIndex) + { + var map = new Dictionary(); + for (int i = Math.Max(0, startIndex); i < history.Count; i++) + foreach (var item in history[i].Contents) + if (item is FunctionCallContent fc) map[fc.CallId] = fc.Name; + return map; + } + /// One-line recap of a single message; falls back to function calls/results when the /// text content is empty (a tool turn). - private static string FormatMessage(ChatMessageContent msg, int lineMax) + private static string FormatMessage(ChatMessage msg, Dictionary callIdToName, int lineMax) { - var content = msg.Content?.Trim(); + var content = msg.Text?.Trim(); if (!string.IsNullOrEmpty(content)) return Cap(content, lineMax); - if (msg.Items == null || msg.Items.Count == 0) return ""; + if (msg.Contents.Count == 0) return ""; var parts = new List(); - foreach (var item in msg.Items) + foreach (var item in msg.Contents) { switch (item) { @@ -65,12 +86,15 @@ private static string FormatMessage(ChatMessageContent msg, int lineMax) var args = fc.Arguments is { Count: > 0 } ? string.Join(", ", fc.Arguments.Select(kv => $"{kv.Key}={Truncate(kv.Value?.ToString(), 40)}")) : ""; - parts.Add($"called {fc.FunctionName}({args})"); + parts.Add($"called {fc.Name}({args})"); break; } case FunctionResultContent fr: - parts.Add($"{fr.FunctionName} → {Truncate(fr.Result?.ToString(), 80)}"); + { + var name = callIdToName.TryGetValue(fr.CallId, out var n) ? n : fr.CallId; + parts.Add($"{name} → {Truncate(fr.Result?.ToString(), 80)}"); break; + } case TextContent tc when !string.IsNullOrWhiteSpace(tc.Text): parts.Add(tc.Text.Trim()); break; diff --git a/src/MandoCode.Desktop/Services/IAiService.cs b/src/MandoCode.Desktop/Services/IAiService.cs index 339d12a..6181b59 100644 --- a/src/MandoCode.Desktop/Services/IAiService.cs +++ b/src/MandoCode.Desktop/Services/IAiService.cs @@ -1,6 +1,6 @@ using MandoCode.Models; using MandoCode.Services; -using Microsoft.SemanticKernel; +using Microsoft.Extensions.AI; namespace MandoCode.Desktop.Services; @@ -40,5 +40,5 @@ public interface IAiService int TryRestoreHistoryJson(string json); Task EnterLearnModeAsync(); Task ClearHistoryAsync(); - Task> GetHistoryAsync(); + Task> GetHistoryAsync(); } diff --git a/src/MandoCode.Desktop/Services/NoteAssistant.cs b/src/MandoCode.Desktop/Services/NoteAssistant.cs index 6e375c9..b44ce51 100644 --- a/src/MandoCode.Desktop/Services/NoteAssistant.cs +++ b/src/MandoCode.Desktop/Services/NoteAssistant.cs @@ -1,7 +1,6 @@ using System.Text; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.Ollama; +using Microsoft.Extensions.AI; +using OllamaSharp; namespace MandoCode.Desktop.Services; @@ -10,7 +9,7 @@ namespace MandoCode.Desktop.Services; /// that's open, and asking about the pad as a whole. /// /// It has no tools, and that is the design. Like , this builds a -/// bare Ollama kernel with no plugins, filters, or shared history — so it cannot read or write a +/// bare Ollama chat client with no tools, middleware, or shared history — so it cannot read or write a /// single file. "No agent ever touches your note" is therefore true by construction rather than by /// policy: the only route from a reply into a note is the user pressing Insert or Replace. That's also /// why it isn't an AIService agent — those exist to change your files, which is the opposite of @@ -163,9 +162,9 @@ public static int CountBodiesSent(IReadOnlyList full) } /// - /// One streamed round on a throwaway kernel. Stateless by construction: a fresh - /// per call, built from the system prompt, the recent thread, and this - /// message. Deltas arrive on a background thread — the caller marshals. + /// One streamed round on a throwaway chat client. Stateless by construction: a fresh message + /// list per call, built from the system prompt, the recent thread, and this message. Deltas + /// arrive on a background thread — the caller marshals. /// private static async Task StreamAsync( string endpoint, @@ -176,33 +175,25 @@ private static async Task StreamAsync( Action onDelta, CancellationToken ct) { - var kernel = Kernel.CreateBuilder() - .AddOllamaChatCompletion(modelId: model, endpoint: new Uri(endpoint)) - .Build(); + using IChatClient chat = new OllamaApiClient(new Uri(endpoint), model); - var chat = kernel.GetRequiredService(); - - var history = new ChatHistory(); - history.AddSystemMessage(systemPrompt); + var history = new List { new(ChatRole.System, systemPrompt) }; foreach (var turn in thread.TakeLast(ThreadTurns)) - { - if (turn.FromUser) history.AddUserMessage(turn.Text); - else history.AddAssistantMessage(turn.Text); - } + history.Add(new ChatMessage(turn.FromUser ? ChatRole.User : ChatRole.Assistant, turn.Text)); - history.AddUserMessage(message); + history.Add(new ChatMessage(ChatRole.User, message)); // Low temperature: this rewrites the user's own words, so faithful beats inventive. - var settings = new OllamaPromptExecutionSettings { Temperature = 0.3f }; + var options = new ChatOptions { Temperature = 0.3f }; // ConfigureAwait(false): per-chunk continuations must not hop through the caller's UI // dispatcher — a small model can stream hundreds of chunks a second. - await foreach (var chunk in chat.GetStreamingChatMessageContentsAsync(history, settings, kernel, ct) + await foreach (var update in chat.GetStreamingResponseAsync(history, options, ct) .ConfigureAwait(false)) { if (ct.IsCancellationRequested) return; - if (!string.IsNullOrEmpty(chunk.Content)) onDelta(chunk.Content); + if (!string.IsNullOrEmpty(update.Text)) onDelta(update.Text); } } } diff --git a/src/MandoCode.Desktop/Services/SkillAuthor.cs b/src/MandoCode.Desktop/Services/SkillAuthor.cs index 2c5991d..a0935d8 100644 --- a/src/MandoCode.Desktop/Services/SkillAuthor.cs +++ b/src/MandoCode.Desktop/Services/SkillAuthor.cs @@ -1,6 +1,5 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.Ollama; +using Microsoft.Extensions.AI; +using OllamaSharp; namespace MandoCode.Desktop.Services; @@ -58,19 +57,18 @@ public static async Task RefineAsync( private static async Task CompleteAsync( string endpoint, string model, string system, string user, CancellationToken ct) { - var kernel = Kernel.CreateBuilder() - .AddOllamaChatCompletion(modelId: model, endpoint: new Uri(endpoint)) - .Build(); - var chat = kernel.GetRequiredService(); + using IChatClient chat = new OllamaApiClient(new Uri(endpoint), model); - var history = new ChatHistory(); - history.AddSystemMessage(system); - history.AddUserMessage(user); + var history = new List + { + new(ChatRole.System, system), + new(ChatRole.User, user) + }; // A touch of latitude helps phrasing without drifting off-spec. - var settings = new OllamaPromptExecutionSettings { Temperature = 0.4f }; - var result = await chat.GetChatMessageContentAsync(history, settings, kernel, ct); - return result.Content?.Trim() ?? ""; + var options = new ChatOptions { Temperature = 0.4f }; + var result = await chat.GetResponseAsync(history, options, ct); + return result.Text?.Trim() ?? ""; } /// Strips a wrapping ```fenced block if the model added one despite instructions. diff --git a/src/MandoCode.Desktop/Services/SnapshotEnhancer.cs b/src/MandoCode.Desktop/Services/SnapshotEnhancer.cs index bfa32b6..3050e8f 100644 --- a/src/MandoCode.Desktop/Services/SnapshotEnhancer.cs +++ b/src/MandoCode.Desktop/Services/SnapshotEnhancer.cs @@ -1,7 +1,6 @@ using System.Text; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.Ollama; +using Microsoft.Extensions.AI; +using OllamaSharp; namespace MandoCode.Desktop.Services; @@ -71,23 +70,19 @@ public static class SnapshotEnhancer public static async Task SummarizeAsync( string endpoint, string model, string rawHistory, CancellationToken ct = default) { - var kernel = Kernel.CreateBuilder() - .AddOllamaChatCompletion(modelId: model, endpoint: new Uri(endpoint)) - .Build(); - - var chat = kernel.GetRequiredService(); + using IChatClient chat = new OllamaApiClient(new Uri(endpoint), model); var chunks = Chunk(rawHistory); // Short conversation — one pass, straight to a final-shaped recap. if (chunks.Count <= 1) - return await SummarizeOneAsync(chat, kernel, SinglePrompt, rawHistory, ct); + return await SummarizeOneAsync(chat, SinglePrompt, rawHistory, ct); // Map: summarize each segment independently. var partials = new List(chunks.Count); for (int i = 0; i < chunks.Count; i++) { - var part = await SummarizeOneAsync(chat, kernel, MapPrompt, chunks[i], ct); + var part = await SummarizeOneAsync(chat, MapPrompt, chunks[i], ct); if (!string.IsNullOrWhiteSpace(part)) partials.Add($"Segment {i + 1}/{chunks.Count}:\n{part}"); } @@ -95,7 +90,7 @@ public static async Task SummarizeAsync( if (partials.Count == 0) return ""; // Reduce: fold the segment summaries into one recap. - return await SummarizeOneAsync(chat, kernel, ReducePrompt, string.Join("\n\n", partials), ct); + return await SummarizeOneAsync(chat, ReducePrompt, string.Join("\n\n", partials), ct); } private const string NamePrompt = @@ -115,10 +110,7 @@ public static async Task SummarizeAsync( { if (string.IsNullOrWhiteSpace(recap)) return null; - var kernel = Kernel.CreateBuilder() - .AddOllamaChatCompletion(modelId: model, endpoint: new Uri(endpoint)) - .Build(); - var chat = kernel.GetRequiredService(); + using IChatClient chat = new OllamaApiClient(new Uri(endpoint), model); var instruction = NamePrompt; if (avoid.Count > 0) @@ -126,7 +118,7 @@ public static async Task SummarizeAsync( + string.Join("; ", avoid.Take(40)) + "."; // A touch of warmth so titles aren't all phrased alike, but still grounded in the recap. - var raw = await SummarizeOneAsync(chat, kernel, instruction, recap, ct, temperature: 0.4f); + var raw = await SummarizeOneAsync(chat, instruction, recap, ct, temperature: 0.4f); return SnapshotNaming.Clean(raw); } catch @@ -138,18 +130,20 @@ public static async Task SummarizeAsync( /// One chat round: system instruction + the text to summarize. Stateless — a fresh /// history each call, so nothing leaks between chunks. private static async Task SummarizeOneAsync( - IChatCompletionService chat, Kernel kernel, string instruction, string text, CancellationToken ct, + IChatClient chat, string instruction, string text, CancellationToken ct, float temperature = 0.2f) { - var history = new ChatHistory(); - history.AddSystemMessage(instruction); - history.AddUserMessage(text); + var history = new List + { + new(ChatRole.System, instruction), + new(ChatRole.User, text) + }; // Low temperature by default — a recap should be faithful, not creative. Naming nudges higher. - var settings = new OllamaPromptExecutionSettings { Temperature = temperature }; + var options = new ChatOptions { Temperature = temperature }; - var result = await chat.GetChatMessageContentAsync(history, settings, kernel, ct); - return result.Content?.Trim() ?? ""; + var result = await chat.GetResponseAsync(history, options, ct); + return result.Text?.Trim() ?? ""; } /// Splits the history into chunks on line boundaries. Chunk size grows if needed so the From c2d24b88dac000fcfb550ae890de5f02fb5ebd0f Mon Sep 17 00:00:00 2001 From: DevMando Date: Sat, 22 Aug 2026 19:15:32 -0700 Subject: [PATCH 2/2] Update docs that named Semantic Kernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README described the app as built on Semantic Kernel's Ollama connector, and session-persistence.md pointed at SK's content types for the cross-provider carry note. Both now name what actually runs: Agent Framework in the engine, Microsoft.Extensions.AI content types across the boundary. Older changelog entries keep their SK references — they were accurate for the versions they describe. --- README.md | 2 +- docs/session-persistence.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3cc72c1..0fc7de7 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Two problems, one app: Ollama: swap models per agent, run one entirely on your own hardware for free, or reach for a hosted model when you want more ceiling. You're never locked to a single closed API. - **A coding assistant is something you live in all day, not a popup.** So it gets a real native - app built on C#, WinUI 3, and Semantic Kernel's Ollama connector — multiple agents open at once, + app built on C#, WinUI 3, and Microsoft's Agent Framework over Ollama — multiple agents open at once, a real shell, a file tree that actually knows about git, a place to jot down a thought without opening a text editor, and a look you can make your own instead of one fixed dark theme. diff --git a/docs/session-persistence.md b/docs/session-persistence.md index 44b8143..ccd0e34 100644 --- a/docs/session-persistence.md +++ b/docs/session-persistence.md @@ -108,6 +108,6 @@ In rough order of value: - **CLI `--continue`** — `ExportHistoryJson`/`TryRestoreHistoryJson` live in the harness precisely so the CLI can grow its own resume without new plumbing. - **Cross-provider carry verification** — verbatim history with function-call content moving - between Ollama and cloud connectors should map cleanly through Semantic Kernel's generic - content types; it deserves a deliberate test before "Keep memory" is treated as guaranteed - across providers (the graceful fallback already handles failure). + between Ollama and cloud connectors should map cleanly through Microsoft.Extensions.AI's + generic content types; it deserves a deliberate test before "Keep memory" is treated as + guaranteed across providers (the graceful fallback already handles failure).