CommandExecutor.cs 6.3 KB

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