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();
}
}
}