using Microsoft.Extensions.AI; using System.Text.Json; namespace AnchorCli; internal sealed class ChatSession { private readonly IChatClient _agent; public ContextCompactor Compactor { get; } public List History { get; } public ChatSession(IChatClient innerClient) { Compactor = new ContextCompactor(innerClient); var tools = ToolRegistry.GetTools(); _agent = new ChatClientBuilder(innerClient) .UseFunctionInvocation() .Build(); History = new List { new(ChatRole.System, $$""" You are anchor, a coding assistant that edits files using the Hashline technique. ## Reading files When you read a file, lines are returned in the format: lineNumber:hash|content The "lineNumber:hash|" prefix is METADATA for anchoring — it is NOT part of the file. ## Editing files To edit, reference anchors as "lineNumber:hash" in startAnchor/endAnchor parameters. The newLines/initialLines parameter must contain RAW SOURCE CODE ONLY. ❌ WRONG: ["5:a3| public void Foo()"] ✅ RIGHT: [" public void Foo()"] Never include the "lineNumber:hash|" prefix in content you write — it will corrupt the file. ## Workflow 1. ALWAYS call GrepFile before ReadFile on any source file. Use patterns like "public|func|function|class|interface|enum|def|fn " to get a structural outline of the file and identify the exact line numbers of the section you need. Only then call ReadFile with a targeted startLine/endLine range. ❌ WRONG: ReadFile("Foo.cs") — reads blindly without knowing the structure. ✅ RIGHT: GrepFile("Foo.cs", "public|class|interface") → ReadFile("Foo.cs", 42, 90) 2. After reading, edit the file before verifying the returned fingerprint. 3. Edit from bottom to top so line numbers don't shift. 4. If an anchor fails validation, re-read the relevant range to get fresh anchors. Keep responses concise. You have access to the current working directory. You are running on: {{System.Runtime.InteropServices.RuntimeInformation.OSDescription}} """) }; } public void Reset() { // Keep only the system message var systemMessage = History[0]; History.Clear(); History.Add(systemMessage); } public async IAsyncEnumerable GetStreamingResponseAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) { var options = new ChatOptions { Tools = ToolRegistry.GetTools() }; var stream = _agent.GetStreamingResponseAsync(History, options, cancellationToken); await foreach (var update in stream.WithCancellation(cancellationToken)) { yield return update; } } public async Task SaveAsync(string filePath, CancellationToken cancellationToken = default) { // Skip the system message when saving (it will be recreated on load) var messagesToSave = History.Skip(1).ToList(); var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; var json = JsonSerializer.Serialize(messagesToSave, AppJsonContext.Default.ListChatMessage); await File.WriteAllTextAsync(filePath, json, cancellationToken); } public async Task LoadAsync(string filePath, CancellationToken cancellationToken = default) { var json = await File.ReadAllTextAsync(filePath, cancellationToken); var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var messages = JsonSerializer.Deserialize>(json, AppJsonContext.Default.ListChatMessage) ?? new List(); // Keep the system message and append loaded messages var systemMessage = History[0]; History.Clear(); History.Add(systemMessage); History.AddRange(messages); } }