CommandExecutor.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. using System;
  2. using System.Linq;
  3. using UnityEditor;
  4. using UnityEngine;
  5. using LLM.Editor.Data;
  6. using System.Reflection;
  7. using LLM.Editor.Commands;
  8. using System.Collections.Generic;
  9. namespace LLM.Editor.Core
  10. {
  11. /// <summary>
  12. /// Responsible for finding and executing commands from the session queue.
  13. /// </summary>
  14. [InitializeOnLoad]
  15. public static class CommandExecutor
  16. {
  17. private static List<CommandData> _commandQueue;
  18. private static CommandContext _currentContext;
  19. public static Action OnQueueUpdated;
  20. public static event Action<string> OnContextReadyForNextTurn;
  21. private static readonly HashSet<string> AutoExecutableCommands = new()
  22. {
  23. nameof(GatherContextCommand),
  24. nameof(UpdateWorkingContextCommand)
  25. };
  26. static CommandExecutor()
  27. {
  28. // This will run when the editor loads, including after a recompile.
  29. EditorApplication.delayCall += Initialize;
  30. }
  31. private static void Initialize()
  32. {
  33. Debug.Log("[CommandExecutor] Initializing...");
  34. if (SessionManager.HasActiveSession())
  35. {
  36. _commandQueue = SessionManager.LoadCommandQueue();
  37. if (!_commandQueue.Any())
  38. {
  39. // Currently only manual "end-session" is supported, so that we can support
  40. // continuous chats even after re-compilation or domain reload.
  41. // SessionManager.EndSession();
  42. return;
  43. }
  44. Debug.Log($"[CommandExecutor] Resuming session with {_commandQueue.Count} command(s) in queue.");
  45. _currentContext = new CommandContext();
  46. OnQueueUpdated?.Invoke();
  47. TriggerAutoExecution();
  48. }
  49. else
  50. {
  51. _commandQueue = new List<CommandData>();
  52. }
  53. }
  54. public static void SetQueue(List<CommandData> commands)
  55. {
  56. _commandQueue = commands;
  57. _currentContext = new CommandContext();
  58. SessionManager.SaveCommandQueue(_commandQueue);
  59. OnQueueUpdated?.Invoke();
  60. Debug.Log($"[CommandExecutor] Queue {_commandQueue.Count} contents:");
  61. foreach (var command in commands)
  62. {
  63. Debug.Log($"<color=cyan>[CommandExecutor]: {command.commandName}</color>");
  64. }
  65. Debug.Log("[CommandExecutor] Queue set.");
  66. TriggerAutoExecution();
  67. }
  68. private static void ClearQueue()
  69. {
  70. _commandQueue.Clear();
  71. SessionManager.SaveCommandQueue(_commandQueue);
  72. OnQueueUpdated?.Invoke();
  73. }
  74. public static bool HasPendingCommands() => _commandQueue != null && _commandQueue.Any();
  75. public static CommandData GetNextCommand() => HasPendingCommands() ? _commandQueue.First() : null;
  76. private static void TriggerAutoExecution()
  77. {
  78. EditorApplication.delayCall += () =>
  79. {
  80. if (IsNextCommandAutoExecutable())
  81. {
  82. ExecuteNextCommand();
  83. }
  84. };
  85. }
  86. public static void ExecuteNextCommand()
  87. {
  88. if (!HasPendingCommands())
  89. {
  90. Debug.LogWarning("[CommandExecutor] No commands to execute.");
  91. return;
  92. }
  93. var commandData = _commandQueue.First();
  94. try
  95. {
  96. var commandInstance = CreateCommandInstance(commandData);
  97. if (commandInstance != null)
  98. {
  99. Debug.Log($"[CommandExecutor] Executing: {commandData.commandName}");
  100. var outcome = commandInstance.Execute(_currentContext);
  101. switch (outcome)
  102. {
  103. // Decide what to do based on the outcome
  104. case CommandOutcome.Success:
  105. // The command succeeded, so we can remove it and continue.
  106. _commandQueue.RemoveAt(0);
  107. TriggerAutoExecution();
  108. break;
  109. case CommandOutcome.Error:
  110. Debug.LogError($"[CommandExecutor] Command '{commandData.commandName}' failed. Clearing remaining command queue.");
  111. ClearQueue();
  112. break;
  113. case CommandOutcome.AwaitingNextTurn:
  114. {
  115. // The command has gathered context and is waiting for the next API call.
  116. // The queue is paused. The command remains at the top of the queue.
  117. Debug.Log("[CommandExecutor] Pausing queue. Awaiting next turn with LLM.");
  118. // Check if the command produced detailed context to send back.
  119. if (_currentContext.CurrentSubject is string detailedContextJson)
  120. {
  121. OnContextReadyForNextTurn?.Invoke(detailedContextJson);
  122. }
  123. break;
  124. }
  125. }
  126. }
  127. else
  128. {
  129. Debug.LogError($"[CommandExecutor] Could not create instance for command: {commandData.commandName}");
  130. ClearQueue(); // Clear queue on critical error
  131. }
  132. }
  133. catch(Exception e)
  134. {
  135. Debug.LogError($"[CommandExecutor] Failed to execute command '{commandData.commandName}'");
  136. Debug.LogException(e);
  137. ClearQueue();
  138. }
  139. // Save the modified queue
  140. SessionManager.SaveCommandQueue(_commandQueue);
  141. OnQueueUpdated?.Invoke();
  142. }
  143. private static bool IsNextCommandAutoExecutable()
  144. {
  145. if (!HasPendingCommands()) return false;
  146. var nextCommandName = GetNextCommand().commandName;
  147. // Ensure the command name doesn't have "Command" suffix before checking
  148. var cleanName = nextCommandName.EndsWith("Command") ? nextCommandName : nextCommandName + "Command";
  149. return AutoExecutableCommands.Contains(cleanName);
  150. }
  151. private static ICommand CreateCommandInstance(CommandData data)
  152. {
  153. // Use reflection to find the command class in the Commands namespace
  154. // This makes the system extensible without needing a giant switch statement.
  155. var commandClassName = data.commandName.EndsWith("Command") ? data.commandName : $"{data.commandName}Command";
  156. var type = Assembly.GetExecutingAssembly().GetTypes()
  157. .FirstOrDefault(t => t.Namespace == "LLM.Editor.Commands" && t.Name == commandClassName);
  158. if (type != null)
  159. {
  160. // Assumes commands have a constructor that takes a single string (the JSON parameters)
  161. var jsonString = data.jsonData?.ToString() ?? "{}";
  162. return (ICommand)Activator.CreateInstance(type, jsonString);
  163. }
  164. Debug.LogError($"[CommandExecutor] Command type '{commandClassName}' not found.");
  165. return null;
  166. }
  167. }
  168. }