using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace AI.Client.SCL { /// /// Represents the outcome of an SCL command execution. /// public class SclResult { /// /// 0 for Success, any non-zero value for Error. /// public int StatusCode { get; set; } /// /// The output data or error message to return to the AI. /// public string Data { get; set; } /// /// Flag indicating if the response should be omitted entirely to save tokens. /// public bool OmitResponse { get; set; } /// /// Creates a successful result. /// public static SclResult Success(string data = "Success") { return new SclResult { StatusCode = 0, Data = data, OmitResponse = false }; } /// /// Creates an error result. /// public static SclResult Error(string errorMessage, int statusCode = 1) { return new SclResult { StatusCode = statusCode, Data = errorMessage, OmitResponse = false }; } /// /// Creates a result that instructs the processor to NOT send a response back to the AI. /// public static SclResult NoResponse() { return new SclResult { OmitResponse = true }; } } /// /// The core engine for parsing and executing Synapse Command Language (SCL) instructions. /// public class SclProcessor { // Thread-safe dictionary to hold registered commands and their execution logic. private readonly ConcurrentDictionary>> _commandHandlers; public SclProcessor() { _commandHandlers = new ConcurrentDictionary>>(StringComparer.OrdinalIgnoreCase); } /// /// Registers a new command that the AI can invoke. /// /// The alphanumeric identifier for the command (e.g., "ls", "mouse_move"). /// An asynchronous function that takes an array of string parameters and returns an SclResult. public void RegisterCommand(string commandId, Func> handler) { if (string.IsNullOrWhiteSpace(commandId)) throw new ArgumentException("Command ID cannot be null or whitespace.", nameof(commandId)); if (commandId.Contains(" ") || commandId.Contains("[") || commandId.Contains("]") || commandId.Contains("|") || commandId.Contains("~") || commandId.Contains("^")) throw new ArgumentException("Command ID contains invalid characters.", nameof(commandId)); _commandHandlers[commandId] = handler ?? throw new ArgumentNullException(nameof(handler)); } /// /// Removes a previously registered command. /// public bool UnregisterCommand(string commandId) { return _commandHandlers.TryRemove(commandId, out _); } /// /// Parses the AI's text output, executes any found SCL commands, and returns the formatted SCL result string. /// /// The raw text output from the AI. /// A string containing the concatenated ^ result blocks to be sent back to the AI. public async Task ProcessTextAsync(string aiText) { if (string.IsNullOrWhiteSpace(aiText)) return string.Empty; var parsedCommands = ParseCommands(aiText); if (parsedCommands.Count == 0) return string.Empty; StringBuilder resultBuilder = new StringBuilder(); foreach (var cmd in parsedCommands) { SclResult executionResult; if (_commandHandlers.TryGetValue(cmd.CommandId, out var handler)) { try { // Execute the registered handler executionResult = await handler(cmd.Parameters); } catch (Exception ex) { // Catch unhandled exceptions in the client code to prevent crashing the processor executionResult = SclResult.Error($"Internal Execution Exception: {ex.Message}"); } } else { executionResult = SclResult.Error($"Unknown command: {cmd.CommandId}"); } // If the command requested no response (e.g. background GUI app), skip appending it if (executionResult.OmitResponse) { continue; } // Format the result back into SCL syntax: ^commandId[statusCode|escapedData] string escapedData = EscapeForScl(executionResult.Data ?? string.Empty); resultBuilder.Append($"^{cmd.CommandId}[{executionResult.StatusCode}|{escapedData}] "); } return resultBuilder.ToString().TrimEnd(); } /// /// Internal struct to hold parsed command data. /// private struct ParsedCommand { public string CommandId { get; } public string[] Parameters { get; } public ParsedCommand(string commandId, string[] parameters) { CommandId = commandId; Parameters = parameters; } } /// /// A robust character-stepping state machine that extracts SCL commands while respecting escape characters, quotes, and nested brackets. /// private List ParseCommands(string input) { var commands = new List(); for (int i = 0; i < input.Length; i++) { // Look for the Action Trigger if (input[i] == '~') { int bracketIdx = input.IndexOf('[', i); if (bracketIdx == -1) continue; // Malformed, no opening bracket found string commandId = input.Substring(i + 1, bracketIdx - i - 1).Trim(); if (string.IsNullOrEmpty(commandId)) continue; // Malformed, no command ID List parameters = new List(); StringBuilder currentParam = new StringBuilder(); bool isEscaped = false; bool isClosed = false; int bracketDepth = 1; // We just passed the first '[' bool inDoubleQuotes = false; bool inSingleQuotes = false; int j = bracketIdx + 1; for (; j < input.Length; j++) { char c = input[j]; if (isEscaped) { // Only consume the escape character if it's escaping an SCL control char. // Otherwise, preserve the backslash (e.g., for Windows file paths like C:\Temp). if (c == '[' || c == ']' || c == '|' || c == '\\') { currentParam.Append(c); } else { currentParam.Append('\\'); currentParam.Append(c); } isEscaped = false; continue; } if (c == '\\') { isEscaped = true; continue; } // Toggle quote states if (c == '"' && !inSingleQuotes) inDoubleQuotes = !inDoubleQuotes; if (c == '\'' && !inDoubleQuotes) inSingleQuotes = !inSingleQuotes; bool inQuotes = inDoubleQuotes || inSingleQuotes; if (!inQuotes && c == '[') { bracketDepth++; currentParam.Append(c); } else if (!inQuotes && c == ']') { bracketDepth--; if (bracketDepth == 0) { parameters.Add(currentParam.ToString()); isClosed = true; break; } else { currentParam.Append(c); } } else if (!inQuotes && bracketDepth == 1 && c == '|') { parameters.Add(currentParam.ToString()); currentParam.Clear(); } else { currentParam.Append(c); } } // Only register the command if the parameter block was properly closed if (isClosed) { commands.Add(new ParsedCommand(commandId, parameters.ToArray())); i = j; // Advance the outer loop past this parsed command } } } return commands; } /// /// Escapes reserved SCL characters in the output data to ensure the AI parses the result correctly. /// private string EscapeForScl(string input) { if (string.IsNullOrEmpty(input)) return input; StringBuilder sb = new StringBuilder(input.Length + 10); foreach (char c in input) { if (c == '\\' || c == '|' || c == ']' || c == '[') { sb.Append('\\'); } sb.Append(c); } return sb.ToString(); } } }