A nice .net agent:
// ==========================================================================
// Sovereign Dev Agent — .NET 10 single-file port (Native AOT compatible)
// ==========================================================================
// Run with: dotnet run Program.cs
// Publish AOT: dotnet publish -p:PublishAot=true
//
// Requires: ALBERT_API_KEY environment variable.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
// --------------------------------------------------------------------
// Configuration
// --------------------------------------------------------------------
string albertApiKey = Environment.GetEnvironmentVariable("ALBERT_API_KEY") ?? "VOTRE_CLE_ALBERT";
const string BaseUrl = "https://albert.api.etalab.gouv.fr/v1/chat/completions";
const string ModelName = "openai/gpt-oss-120b";
if (string.IsNullOrWhiteSpace(albertApiKey) || albertApiKey == "VOTRE_CLE_ALBERT")
{
Console.Error.WriteLine("❌ Error: Please define your ALBERT_API_KEY environment variable.");
Environment.Exit(1);
}
// --------------------------------------------------------------------
// Persistent state
// --------------------------------------------------------------------
string currentWorkingDir = Directory.GetCurrentDirectory();
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", albertApiKey);
// --------------------------------------------------------------------
// DRY helper: path resolution & validation
// --------------------------------------------------------------------
string ResolveAndValidatePath(string relativeOrAbsolutePath, bool checkExists = true)
{
string resolvedPath = Path.GetFullPath(Path.Combine(currentWorkingDir, relativeOrAbsolutePath));
if (checkExists && !File.Exists(resolvedPath) && !Directory.Exists(resolvedPath))
{
throw new FileNotFoundException($"Target path does not exist: {resolvedPath}");
}
return resolvedPath;
}
// --------------------------------------------------------------------
// JsonElement helpers (AOT-safe: no reflection, no serializer)
// --------------------------------------------------------------------
static string? GetStringProp(JsonElement obj, string name)
{
if (obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
// --------------------------------------------------------------------
// Local tools — updated to support async tool invocation naturally
// --------------------------------------------------------------------
Task<ToolResult> ListDirectoryAsync(JsonElement args)
{
try
{
string dirPath = GetStringProp(args, "dirPath") ?? ".";
string targetPath = ResolveAndValidatePath(dirPath);
// EnumerateFileSystemInfos surfaces directory-vs-file via Attributes from the
// same underlying syscall as enumeration, avoiding a second per-entry stat
// call that Directory.Exists(entry) would otherwise require.
var files = new List<(string, string)>();
foreach (var entry in new DirectoryInfo(targetPath).EnumerateFileSystemInfos())
{
bool isFolder = (entry.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
files.Add((entry.Name, isFolder ? "folder" : "file"));
}
return Task.FromResult(ToolResult.Ok(currentWorkingDir: currentWorkingDir, files: files));
}
catch (Exception err)
{
return Task.FromResult(ToolResult.Fail(err.Message));
}
}
async Task<ToolResult> ReadProjectFileAsync(JsonElement args)
{
try
{
string? filePath = GetStringProp(args, "filePath");
if (string.IsNullOrEmpty(filePath))
return ToolResult.Fail("filePath is required");
string targetPath = ResolveAndValidatePath(filePath);
// Optimize: Check file size before reading to avoid blowing out memory limits
var fileInfo = new FileInfo(targetPath);
const int MaxAllowedReadBytes = 120_000; // ~120KB maximum chunk threshold
if (fileInfo.Length > MaxAllowedReadBytes)
{
// Stream only the head block of the huge target file seamlessly
using var fs = new FileStream(targetPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
byte[] buffer = new byte[MaxAllowedReadBytes];
int readBytes = await fs.ReadAsync(buffer, 0, MaxAllowedReadBytes);
string partialContent = Encoding.UTF8.GetString(buffer, 0, readBytes);
return ToolResult.Ok(content: partialContent + $"\n\n[... TRUNCATED SYSTEM OPTIMIZATION: File size is {fileInfo.Length} bytes. Read limit capped at {MaxAllowedReadBytes} bytes to prevent network latency loops ...] ");
}
string content = await File.ReadAllTextAsync(targetPath, Encoding.UTF8);
return ToolResult.Ok(content: content);
}
catch (Exception err)
{
return ToolResult.Fail(err.Message);
}
}
async Task<ToolResult> WriteProjectFileAsync(JsonElement args)
{
try
{
string? filePath = GetStringProp(args, "filePath");
string content = GetStringProp(args, "content") ?? "";
if (string.IsNullOrEmpty(filePath))
return ToolResult.Fail("filePath is required");
string targetPath = ResolveAndValidatePath(filePath, checkExists: false);
string? dir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
await File.WriteAllTextAsync(targetPath, content, Encoding.UTF8);
return ToolResult.Ok(message: $"Successfully wrote file to {targetPath}");
}
catch (Exception err)
{
return ToolResult.Fail(err.Message);
}
}
Task<ToolResult> ChangeDirectoryAsync(JsonElement args)
{
try
{
string? targetPathArg = GetStringProp(args, "targetPath");
if (string.IsNullOrEmpty(targetPathArg))
return Task.FromResult(ToolResult.Fail("targetPath is required"));
string newPath = ResolveAndValidatePath(targetPathArg);
if (!Directory.Exists(newPath))
return Task.FromResult(ToolResult.Fail($"Not a directory: {newPath}"));
currentWorkingDir = newPath;
return Task.FromResult(ToolResult.Ok(currentWorkingDir: currentWorkingDir));
}
catch (Exception err)
{
return Task.FromResult(ToolResult.Fail(err.Message));
}
}
async Task<ToolResult> ExecuteTerminalCommandAsync(JsonElement args)
{
try
{
string? command = GetStringProp(args, "command");
if (string.IsNullOrEmpty(command))
return ToolResult.Fail("command is required");
Console.WriteLine($"\n [Terminal Executing]: {command}");
var psi = new ProcessStartInfo
{
FileName = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/bash",
ArgumentList = { OperatingSystem.IsWindows() ? "/c" : "-c", command },
WorkingDirectory = currentWorkingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = psi };
var stdoutBuilder = new StringBuilder();
var stderrBuilder = new StringBuilder();
process.OutputDataReceived += (s, e) => { if (e.Data != null) stdoutBuilder.AppendLine(e.Data); };
process.ErrorDataReceived += (s, e) => { if (e.Data != null) stderrBuilder.AppendLine(e.Data); };
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
string stdout = stdoutBuilder.ToString();
string stderr = stderrBuilder.ToString();
if (process.ExitCode != 0)
{
return ToolResult.Fail($"Command exited with code {process.ExitCode}", stderr: stderr);
}
return ToolResult.Ok(stdout: stdout);
}
catch (Exception err)
{
return ToolResult.Fail(err.Message);
}
}
// Map of tool name -> asynchronous function execution targets
var localToolsMap = new Dictionary<string, Func<JsonElement, Task<ToolResult>>>
{
["listDirectory"] = ListDirectoryAsync,
["readProjectFile"] = ReadProjectFileAsync,
["writeProjectFile"] = WriteProjectFileAsync,
["changeDirectory"] = ChangeDirectoryAsync,
["executeTerminalCommand"] = ExecuteTerminalCommandAsync
};
// --------------------------------------------------------------------
// Tool schema sent to the model — written directly with Utf8JsonWriter.
// --------------------------------------------------------------------
void WriteToolsSchema(Utf8JsonWriter w)
{
w.WriteStartArray();
void WriteTool(string name, string description, Action<Utf8JsonWriter> writeProperties, string[] required)
{
w.WriteStartObject();
w.WriteString("type", "function");
w.WriteStartObject("function");
w.WriteString("name", name);
w.WriteString("description", description);
w.WriteStartObject("parameters");
w.WriteString("type", "object");
w.WriteStartObject("properties");
writeProperties(w);
w.WriteEndObject();
if (required.Length > 0)
{
w.WriteStartArray("required");
foreach (var r in required) w.WriteStringValue(r);
w.WriteEndArray();
}
w.WriteEndObject();
w.WriteEndObject();
w.WriteEndObject();
}
void WriteStringParam(Utf8JsonWriter writer, string name, string? description = null)
{
writer.WriteStartObject(name);
writer.WriteString("type", "string");
if (description is not null) writer.WriteString("description", description);
writer.WriteEndObject();
}
WriteTool(
"listDirectory",
"List files and folders in a target directory",
wr => WriteStringParam(wr, "dirPath", "Relative or absolute directory path"),
required: Array.Empty<string>());
WriteTool(
"readProjectFile",
"Read contents of a file",
wr => WriteStringParam(wr, "filePath"),
required: new[] { "filePath" });
WriteTool(
"writeProjectFile",
"Write content to a file, creates directory branches recursively if missing",
wr =>
{
WriteStringParam(wr, "filePath");
WriteStringParam(wr, "content");
},
required: new[] { "filePath", "content" });
WriteTool(
"changeDirectory",
"Change current working directory path context",
wr => WriteStringParam(wr, "targetPath"),
required: new[] { "targetPath" });
WriteTool(
"executeTerminalCommand",
"Execute arbitrary bash shell commands natively with a 60s timeout.",
wr => WriteStringParam(wr, "command"),
required: new[] { "command" });
w.WriteEndArray();
}
// --------------------------------------------------------------------
// Conversation history storage helpers
// --------------------------------------------------------------------
string BuildUserOrSystemMessage(string role, string content)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
w.WriteString("role", role);
w.WriteString("content", content);
w.WriteEndObject();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
string BuildToolMessage(string toolCallId, string toolName, string contentJson)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
w.WriteString("role", "tool");
w.WriteString("tool_call_id", toolCallId);
w.WriteString("name", toolName);
w.WriteString("content", contentJson);
w.WriteEndObject();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
// --------------------------------------------------------------------
// Shared payload builder (avoids re-parsing each history entry's JSON
// on every loop turn — history strings are already well-formed JSON
// produced by our own builders, so we splice their raw bytes directly
// instead of round-tripping through JsonDocument.Parse + WriteTo).
// --------------------------------------------------------------------
byte[] BuildChatPayload(List<string> history, bool stream)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
w.WriteString("model", ModelName);
w.WriteStartArray("messages");
foreach (var msgJson in history)
{
w.WriteRawValue(msgJson, skipInputValidation: true);
}
w.WriteEndArray();
w.WritePropertyName("tools");
WriteToolsSchema(w);
w.WriteString("tool_choice", "auto");
if (stream) w.WriteBoolean("stream", true);
w.WriteEndObject();
}
return ms.ToArray();
}
// --------------------------------------------------------------------
// Albert Non-Streaming API call (Used exclusively for Tool Dispatch evaluations)
// --------------------------------------------------------------------
async Task<JsonElement> QueryAlbertAsync(List<string> history)
{
byte[] payloadBytes = BuildChatPayload(history, stream: false);
using var content = new ByteArrayContent(payloadBytes);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" };
using var response = await httpClient.PostAsync(BaseUrl, content);
string body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Albert API Error ({(int)response.StatusCode}): {body}");
}
using var responseDoc = JsonDocument.Parse(body);
var message = responseDoc.RootElement.GetProperty("choices")[0].GetProperty("message");
return message.Clone();
}
// --------------------------------------------------------------------
// Albert Streaming API call (AOT-Safe real-time streaming evaluator)
// --------------------------------------------------------------------
async Task<string> QueryAlbertStreamAsync(List<string> history, AnsiStreamParser streamParser)
{
byte[] payloadBytes = BuildChatPayload(history, stream: true);
using var request = new HttpRequestMessage(HttpMethod.Post, BaseUrl)
{
Content = new ByteArrayContent(payloadBytes)
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" };
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
string errorBody = await response.Content.ReadAsStringAsync();
throw new Exception($"Albert API Error ({(int)response.StatusCode}): {errorBody}");
}
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream, Encoding.UTF8);
var fullContentBuilder = new StringBuilder();
while (await reader.ReadLineAsync() is string line)
{
if (string.IsNullOrWhiteSpace(line)) continue;
if (line.StartsWith("data: [DONE]")) break;
if (line.StartsWith("data: "))
{
string jsonChunk = line[6..].Trim();
if (string.IsNullOrEmpty(jsonChunk)) continue;
byte[] chunkBytes = Encoding.UTF8.GetBytes(jsonChunk);
var jsonReader = new Utf8JsonReader(chunkBytes);
while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonTokenType.PropertyName && jsonReader.ValueTextEquals("content"))
{
jsonReader.Read();
string? token = jsonReader.GetString();
if (!string.IsNullOrEmpty(token))
{
fullContentBuilder.Append(token);
string formattedToken = streamParser.AppendChunk(token);
Console.Write(formattedToken);
}
}
}
}
}
Console.WriteLine();
return fullContentBuilder.ToString();
}
// --------------------------------------------------------------------
// Runtime engine
// --------------------------------------------------------------------
async Task HandleChatAsync(string userInput)
{
string cleanInput = userInput.Trim();
if (string.IsNullOrEmpty(cleanInput))
{
PromptUser();
return;
}
// 2. Fast-Track File Viewers (Bypasses LLM network loops entirely with Instant Pipeline)
if (cleanInput.StartsWith("cat ", StringComparison.OrdinalIgnoreCase) ||
cleanInput.StartsWith("type ", StringComparison.OrdinalIgnoreCase) ||
cleanInput.StartsWith("gc ", StringComparison.OrdinalIgnoreCase))
{
try
{
string filename = cleanInput.Split(' ', 2)[1].Trim();
string targetPath = ResolveAndValidatePath(filename);
Console.WriteLine($"\n\x1b[1;35m📄 Local File Stream ({filename}):\x1b[0m");
var streamParser = new AnsiStreamParser();
Console.Write(streamParser.ForceEnterCodeBlock());
using var fileStream = new FileStream(targetPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
using var reader = new StreamReader(fileStream, Encoding.UTF8);
string? lineStr;
while ((lineStr = await reader.ReadLineAsync()) != null)
{
Console.Write(streamParser.AppendChunk(lineStr + "\n"));
}
Console.Write(streamParser.ForceExitCodeBlock());
PromptUser();
return;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Local file read failed: {ex.Message}");
PromptUser();
return;
}
}
// 3. Application Exit Commands
if (string.Equals(cleanInput, "exit", StringComparison.OrdinalIgnoreCase) ||
string.Equals(cleanInput, "quit", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("\nSovereign Dev Agent closed.");
return;
}
const string SystemPrompt =
@"You are an expert, senior full-stack software engineer and local development agent, and also a capable, friendly general-purpose conversational assistant. Decide for yourself, per message, which mode the user actually needs — most messages will be one or the other, not both.
MODE SELECTION:
- Use tools (listDirectory, readProjectFile, writeProjectFile, changeDirectory, executeTerminalCommand) only when the user's request requires touching the filesystem, running commands, or inspecting/modifying the actual project — e.g. ""fix this bug"", ""add a function to X"", ""run the tests"", ""what's in this folder"".
- Answer directly in conversation, with no tool calls, for everything else — explanations, opinions, brainstorming, general knowledge, casual chat, conceptual or ""should I use X or Y"" questions, or anything where you already have what you need to answer. Do not call a tool just to look busy or thorough if the user's question doesn't actually require touching the workspace.
- If you're unsure which mode fits, default to answering conversationally first; only reach for tools once it's clear the user wants you to act on the project.
WHEN ACTING AS A DEV AGENT (tool calling), follow these OPERATIONAL PRINCIPLES:
1. ANALYSIS FIRST: Before writing files or executing scripts, inspect the existing directory structure and relevant files to understand the project architecture.
2. DEFENSIVE CODING: Implement comprehensive error handling, type checks, and edge-case validations in all code you generate. Do not write placeholder code or ""TODOs"".
3. STEP-BY-STEP EXECUTION: Break down complex engineering objectives into deliberate, sequential tool calls. Verify the outcome of a step (e.g., compile or test results) before moving to the next.
4. ATOMICITY: Keep file modifications precise. Avoid rewriting massive files if only a single function needs optimization.
5. SELF-CORRECTION: If a terminal command returns a standard error (stderr) or a compilation failure, analyze the output, fix the root cause, and re-run the process immediately.
6. PROFESSIONAL PRESENTATION OF THE WORK — consistent naming and formatting conventions for the language/stack in use, clear and minimal comments only where they add real value, sensible project structure, no leftover debug prints or commented-out dead code, and meaningful commit-quality messages if you generate any.
FINAL RESPONSE FORMATTING (applies once you're done with tool calls, if any, and are giving your concluding answer):
- If the turn involved tool calls: structure your concluding answer like a polished engineering handoff, not a raw log dump.
- Open with a one- or two-sentence summary of what was accomplished.
- Use clear Markdown structure: short headings or bold labels for sections, bullet points or numbered lists for steps/changes, and fenced code blocks with the correct language tag for any code, commands, or file paths.
- If you changed or created files, list them explicitly (e.g. as a short table or bullet list of path → purpose).
- If you ran commands, summarize the outcome in plain language rather than pasting raw terminal output verbatim; quote only the specific error or result line if it's relevant.
- End with next steps, caveats, or follow-up suggestions when relevant (e.g. ""run the test suite"", ""set the FOO env var before deploying"").
- Never present a final answer as an unformatted wall of text when the content has any structure to it (multiple files, multiple steps, comparisons, etc.).
- If the turn was purely conversational (no tool calls): just answer naturally, like a normal chat response. Use Markdown structure (headings, bullets, code blocks) only when the content actually has that kind of structure — a short answer or a casual remark should stay a short, plain response, not be forced into a report format.
- In both cases, keep tone confident, concise, and free of filler — no rambling, no unnecessary apologies, no exposing internal reasoning or tool mechanics.
You have total technical authority to manipulate files and execute shell strings within this workspace to achieve the user's goal, and full latitude to talk about anything else the user brings up as a normal conversational partner.";
var localHistory = new List<string>
{
BuildUserOrSystemMessage("system", SystemPrompt),
BuildUserOrSystemMessage("user", $"Current Working Directory Context: {currentWorkingDir}\nUser Query: {cleanInput}")
};
bool keepLooping = true;
int loopsCounter = 0;
const int MaxLoops = 12;
try
{
while (keepLooping && loopsCounter < MaxLoops)
{
loopsCounter++;
JsonElement message = await QueryAlbertAsync(localHistory);
bool hasToolCalls = message.TryGetProperty("tool_calls", out var toolCallsEl) &&
toolCallsEl.ValueKind == JsonValueKind.Array &&
toolCallsEl.GetArrayLength() > 0;
if (hasToolCalls)
{
using (var ms = new MemoryStream())
{
using (var w = new Utf8JsonWriter(ms))
{
message.WriteTo(w);
}
localHistory.Add(Encoding.UTF8.GetString(ms.ToArray()));
}
var toolCall = toolCallsEl[0];
var function = toolCall.GetProperty("function");
string toolName = function.GetProperty("name").GetString() ?? "";
string toolCallId = toolCall.TryGetProperty("id", out var idEl) ? (idEl.GetString() ?? "") : "";
string argumentsJson = function.TryGetProperty("arguments", out var argsEl)
? (argsEl.GetString() ?? "{}")
: "{}";
Console.WriteLine($"\n🤖 [Agent Step {loopsCounter}]: Executing {toolName}");
if (localToolsMap.TryGetValue(toolName, out var executionFunction))
{
JsonDocument argsDoc = JsonDocument.Parse(string.IsNullOrEmpty(argumentsJson) ? "{}" : argumentsJson);
ToolResult toolResult;
using (argsDoc)
{
toolResult = await executionFunction(argsDoc.RootElement);
}
string resultStr = toolResult.ToJson();
string preview = resultStr.Length > 150 ? resultStr.Substring(0, 150) + "..." : resultStr;
Console.WriteLine($"⚙️ [Result]: {preview}");
localHistory.Add(BuildToolMessage(toolCallId, toolName, resultStr));
}
else
{
Console.WriteLine($"❌ Tool {toolName} missing implementation.");
keepLooping = false;
}
}
else
{
Console.Write("\n\x1b[1;35m🤖 Agent Albert:\x1b[0m\n");
var streamParser = new AnsiStreamParser();
string streamedContent = await QueryAlbertStreamAsync(localHistory, streamParser);
localHistory.Add(BuildUserOrSystemMessage("assistant", streamedContent));
keepLooping = false;
}
}
if (loopsCounter >= MaxLoops)
{
Console.WriteLine($"\n⚠️ Safety Loop Cap reached ({MaxLoops} actions).");
}
}
catch (Exception error)
{
Console.Error.WriteLine($"\n❌ Runtime Engine Error: {error.Message}");
}
PromptUser();
}
void PromptUser()
{
Console.WriteLine($"\n[{currentWorkingDir}]");
Console.Write("You: ");
}
// --------------------------------------------------------------------
// Entry point loop
// --------------------------------------------------------------------
Console.WriteLine("=========================================================");
Console.WriteLine("🔓 Unrestricted Multi-Turn State-Streaming Agent — .NET 10");
Console.WriteLine("=========================================================");
PromptUser();
string? inputLine;
while ((inputLine = Console.ReadLine()) != null)
{
await HandleChatAsync(inputLine);
if (string.Equals(inputLine.Trim(), "exit", StringComparison.OrdinalIgnoreCase) ||
string.Equals(inputLine.Trim(), "quit", StringComparison.OrdinalIgnoreCase))
{
break;
}
}
// ==========================================================================
// Type declarations (Placed last to satisfy Top-Level Statements constraint)
// ==========================================================================
sealed class ToolResult
{
public bool Success { get; init; }
public string? Error { get; init; }
public string? CurrentWorkingDir { get; init; }
public string? Message { get; init; }
public string? Content { get; init; }
public string? Stdout { get; init; }
public string? Stderr { get; init; }
public List<(string Name, string Type)>? Files { get; init; }
public static ToolResult Ok(string? currentWorkingDir = null, string? message = null,
string? content = null, string? stdout = null, List<(string, string)>? files = null) =>
new()
{
Success = true,
CurrentWorkingDir = currentWorkingDir,
Message = message,
Content = content,
Stdout = stdout,
Files = files
};
public static ToolResult Fail(string error, string? stderr = null) =>
new() { Success = false, Error = error, Stderr = stderr };
public string ToJson()
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
if (Error is not null)
{
w.WriteBoolean("success", false);
w.WriteString("error", Error);
if (Stderr is not null) w.WriteString("stderr", Stderr);
}
else
{
w.WriteBoolean("success", true);
if (CurrentWorkingDir is not null) w.WriteString("currentWorkingDir", CurrentWorkingDir);
if (Message is not null) w.WriteString("message", Message);
if (Content is not null) w.WriteString("content", Content);
if (Stdout is not null) w.WriteString("stdout", Stdout);
if (Files is not null)
{
w.WriteStartArray("files");
foreach (var (name, type) in Files)
{
w.WriteStartObject();
w.WriteString("name", name);
w.WriteString("type", type);
w.WriteEndObject();
}
w.WriteEndArray();
}
}
w.WriteEndObject();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
}
sealed class AnsiStreamParser
{
private const string Reset = "\x1b[0m";
private const string Green = "\x1b[32m";
private const string DarkGray = "\x1b[90m";
private const string CodeBg = "\x1b[48;5;235m";
private const string CodeFg = "\x1b[38;5;250m";
private enum ParserState { Text, CodeBlock, InlineCode }
private enum CodeState { Normal, StringLiteral, Comment }
private ParserState _currentState = ParserState.Text;
private CodeState _currentCodeState = CodeState.Normal;
private readonly StringBuilder _lineBuffer = new();
private readonly StringBuilder _wordBuffer = new();
private static readonly HashSet<string> Keywords = new()
{
"public", "private", "protected", "internal", "class", "struct",
"interface", "enum", "sealed", "static", "void", "string", "int",
"var", "bool", "object", "new", "return", "if", "else", "foreach",
"in", "try", "catch", "using", "async", "await", "const"
};
public string ForceEnterCodeBlock()
{
_currentState = ParserState.CodeBlock;
_currentCodeState = CodeState.Normal;
return $"\n{DarkGray}----------------------------------{Reset}\n {CodeBg}{CodeFg}";
}
public string ForceExitCodeBlock()
{
_currentState = ParserState.Text;
return $"{Reset}\n{DarkGray}----------------------------------{Reset}\n";
}
public string AppendChunk(string chunk)
{
if (string.IsNullOrEmpty(chunk)) return string.Empty;
var output = new StringBuilder();
foreach (char c in chunk)
{
_lineBuffer.Append(c);
switch (_currentState)
{
case ParserState.Text:
// Only worth checking once the line is short and ends with a backtick;
// avoids allocating a string on every character of normal prose.
if (c == '`' && LineTrimEquals(_lineBuffer, "```
{% endraw %}
"))
{
output.Append(ForceEnterCodeBlock());
_lineBuffer.Clear();
continue;
}
if (c == '{% raw %}`')
{
_currentState = ParserState.InlineCode;
output.Append(Green);
continue;
}
output.Append(c);
break;
case ParserState.CodeBlock:
if (c == '\n' && LineTrimEquals(_lineBuffer, "```"))
{
output.Append(ForceExitCodeBlock());
_lineBuffer.Clear();
continue;
}
if (_currentCodeState == CodeState.Comment)
{
output.Append(c);
if (c == '\n')
{
_currentCodeState = CodeState.Normal;
output.Append($"{Reset}{CodeBg}{CodeFg} ");
}
}
else if (_currentCodeState == CodeState.StringLiteral)
{
output.Append(c);
if (c == '"')
{
_currentCodeState = CodeState.Normal;
output.Append($"{Reset}{CodeBg}{CodeFg}");
}
}
else
{
if (char.IsLetterOrDigit(c) || c == '_')
{
_wordBuffer.Append(c);
}
else
{
FlushWord(output);
if (c == '"')
{
_currentCodeState = CodeState.StringLiteral;
output.Append("\x1b[33m\"");
}
else if (c == '/' && LineEndsWithDoubleSlash(_lineBuffer))
{
_currentCodeState = CodeState.Comment;
if (output.Length > 0) output.Length--;
output.Append($"{DarkGray}//");
}
else
{
output.Append(c);
}
}
}
break;
case ParserState.InlineCode:
if (c == '`{% endraw %}')
{
output.Append(Reset);
_currentState = ParserState.Text;
}
else
{
output.Append(c);
}
break;
}
if (c == '\n') _lineBuffer.Clear();
}
return output.ToString();
}
// Allocation-free equivalent of {% raw %}`sb.ToString().TrimStart() == value` / `.Trim() == value`
// for short fence markers, avoiding a full string copy on every streamed character.
private static bool LineTrimEquals(StringBuilder sb, string value)
{
int start = 0, end = sb.Length;
while (start < end && char.IsWhiteSpace(sb[start])) start++;
while (end > start && char.IsWhiteSpace(sb[end - 1])) end--;
int len = end - start;
if (len != value.Length) return false;
for (int i = 0; i < len; i++)
{
if (sb[start + i] != value[i]) return false;
}
return true;
}
// Allocation-free equivalent of `sb.ToString().EndsWith("//")`.
private static bool LineEndsWithDoubleSlash(StringBuilder sb)
{
int len = sb.Length;
return len >= 2 && sb[len - 1] == '/' && sb[len - 2] == '/';
}
private void FlushWord(StringBuilder output)
{
if (_wordBuffer.Length == 0) return;
string word = _wordBuffer.ToString();
_wordBuffer.Clear();
if (Keywords.Contains(word))
output.Append($"\x1b[36m{word}{Reset}{CodeBg}{CodeFg}");
else
output.Append(word);
}
}
Top comments (0)