using System;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using LLM.Editor.Data;
using System.Reflection;
using LLM.Editor.Client;
using LLM.Editor.Commands;
using LLM.Editor.Helper;
using System.Collections.Generic;
namespace LLM.Editor.Core
{
///
/// Responsible for finding and executing commands from the session queue.
///
[InitializeOnLoad]
public static class CommandExecutor
{
private static List _commandQueue;
private static CommandContext _currentContext;
private static string _currentUserPrompt;
private static int _initialCommandCount;
public static Action OnQueueUpdated;
public static event Action OnContextReadyForNextTurn;
public static event Action> OnPlanUpdated;
private static readonly HashSet AutoExecutableCommands = new()
{
nameof(GatherContextCommand),
nameof(UpdateWorkingContextCommand)
};
static CommandExecutor()
{
// This will run when the editor loads, including after a recompile.
EditorApplication.delayCall += Initialize;
}
private static void Initialize()
{
Debug.Log("[CommandExecutor] Initializing...");
if (SessionManager.HasActiveSession())
{
_commandQueue = SessionManager.LoadCommandQueue();
if (!_commandQueue.Any())
{
return;
}
Debug.Log($"[CommandExecutor] Resuming session with {_commandQueue.Count} command(s) in queue.");
_currentContext = new CommandContext
{
IdentifierMap = SessionManager.LoadIdentifierMap()
};
_currentUserPrompt = SessionManager.LoadCurrentUserPrompt();
_initialCommandCount = _commandQueue.Count;
OnQueueUpdated?.Invoke();
TriggerAutoExecution();
}
else
{
_commandQueue = new List();
}
}
public static void SetQueue(List commands, string userPrompt)
{
_commandQueue = commands;
_currentContext = new CommandContext
{
IdentifierMap = SessionManager.LoadIdentifierMap()
};
_currentUserPrompt = userPrompt;
_initialCommandCount = commands.Count;
SessionManager.SaveCommandQueue(_commandQueue);
SessionManager.SaveCurrentUserPrompt(_currentUserPrompt);
OnQueueUpdated?.Invoke();
Debug.Log($"[CommandExecutor] Queue set with {_commandQueue.Count} commands.");
foreach (var commandData in _commandQueue)
{
Debug.Log($"[CommandExecutor] {commandData.commandName}");
}
TriggerAutoExecution();
}
private static void ClearQueue()
{
_commandQueue.Clear();
SessionManager.SaveCommandQueue(_commandQueue);
OnQueueUpdated?.Invoke();
}
public static bool HasPendingCommands() => _commandQueue != null && _commandQueue.Any();
public static CommandData GetNextCommand() => HasPendingCommands() ? _commandQueue.First() : null;
private static void TriggerAutoExecution()
{
EditorApplication.delayCall += async () =>
{
if (IsNextCommandAutoExecutable())
{
await ExecuteNextCommand();
}
};
}
public static async Task ExecuteNextCommand()
{
if (!HasPendingCommands())
{
Debug.LogWarning("[CommandExecutor] No commands to execute.");
return;
}
var commandData = _commandQueue.First();
var outcome = CommandOutcome.Error;
float[] promptEmbedding = null;
try
{
EditorApplication.LockReloadAssemblies();
// Generate the embedding *before* executing the command, but only for non-dummy clients.
var apiClient = ApiClientFactory.GetClient();
if (apiClient is not DummyApiClient)
{
if (!string.IsNullOrEmpty(_currentUserPrompt))
{
if (apiClient != null)
{
promptEmbedding = await EmbeddingHelper.GetEmbedding(_currentUserPrompt, apiClient.GetAuthToken);
}
else
{
Debug.LogError("[CommandExecutor] Could not get API client to generate embedding.");
}
}
}
var commandInstance = CreateCommandInstance(commandData);
if (commandInstance != null)
{
Debug.Log($"[CommandExecutor] Executing: {commandData.commandName}");
var planBefore = _currentContext.Plan;
outcome = commandInstance.Execute(_currentContext);
var planAfter = _currentContext.Plan;
if (planAfter != null && !planAfter.SequenceEqual(planBefore ?? Enumerable.Empty()))
{
OnPlanUpdated?.Invoke(planAfter);
}
switch (outcome)
{
case CommandOutcome.Success:
_commandQueue.RemoveAt(0);
TriggerAutoExecution();
break;
case CommandOutcome.Error:
var errorMessage = $"Command '{commandData.commandName}' failed.";
Debug.LogError($"[CommandExecutor] {errorMessage}. Message:\n{_currentContext.ErrorMessage ?? "An unspecified error occurred."}");
var errorContext = new
{
lastActionStatus = "Error",
failedCommand = commandData.commandName,
errorMessage = _currentContext.ErrorMessage ?? "An unspecified error occurred."
};
OnContextReadyForNextTurn?.Invoke(errorContext.ToJson(true));
ClearQueue();
break;
case CommandOutcome.AwaitingNextTurn:
Debug.Log("[CommandExecutor] Pausing queue. Awaiting next turn with LLM.");
if (_currentContext.CurrentSubject is string detailedContextJson)
{
OnContextReadyForNextTurn?.Invoke(detailedContextJson);
}
break;
}
}
else
{
Debug.LogError($"[CommandExecutor] Could not create instance for command: {commandData.commandName}");
ClearQueue();
}
}
catch(Exception e)
{
Debug.LogError($"[CommandExecutor] Failed to execute command '{commandData.commandName}'");
Debug.LogException(e);
ClearQueue();
}
finally
{
var record = new InteractionRecord
{
UserPrompt = _currentUserPrompt,
LLMResponse = commandData,
Outcome = outcome,
Feedback = outcome == CommandOutcome.Error ? _currentContext.ErrorMessage : "Success",
isMultiStep = _initialCommandCount > 1,
PromptEmbedding = promptEmbedding
};
MemoryLogger.AddRecord(record);
SessionManager.SaveCommandQueue(_commandQueue);
OnQueueUpdated?.Invoke();
EditorApplication.UnlockReloadAssemblies();
}
}
private static bool IsNextCommandAutoExecutable()
{
if (!HasPendingCommands()) return false;
var nextCommandName = GetNextCommand().commandName;
var cleanName = nextCommandName.EndsWith("Command") ? nextCommandName : nextCommandName + "Command";
return AutoExecutableCommands.Contains(cleanName);
}
private static ICommand CreateCommandInstance(CommandData data)
{
var commandClassName = data.commandName.EndsWith("Command") ? data.commandName : $"{data.commandName}Command";
var type = Assembly.GetExecutingAssembly().GetTypes()
.FirstOrDefault(t => t.Namespace == "LLM.Editor.Commands" && t.Name == commandClassName);
if (type != null)
{
var jsonString = data.jsonData?.ToString() ?? "{}";
return (ICommand)Activator.CreateInstance(type, jsonString);
}
Debug.LogError($"[CommandExecutor] Command type '{commandClassName}' not found.");
return null;
}
}
}