Pārlūkot izejas kodu

Sujith :) ->
1. Added more features
2. Some automation
3. Interesting thingys

Sujith:) 2 nedēļas atpakaļ
vecāks
revīzija
498e8b1210

+ 119 - 0
Assets/IntelligentProjectAnalyzer/Analyzer/RoslynReferenceFinder.cs

@@ -0,0 +1,119 @@
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using Microsoft.CodeAnalysis;
+using UnityEditor.Compilation;
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace IntelligentProjectAnalyzer.Analyzer
+{
+    /// <summary>
+    /// A helper class that uses Roslyn to perform static analysis on C# scripts.
+    /// </summary>
+    public static class RoslynReferenceFinder
+    {
+        /// <summary>
+        /// Finds references to a type or a method within that type.
+        /// </summary>
+        /// <param name="qualifier">A string that can be a class name, a full type name, or a full type name with a method.</param>
+        /// <returns>A tuple containing the list of references, the GUID of the script where the type is defined, and the Type itself.</returns>
+        public static (List<object> references, string foundScriptGuid, System.Type foundScriptType) FindReferences(string qualifier)
+        {
+            var (compilation, syntaxTrees) = GetProjectCompilation();
+            var (typeSymbol, methodName) = ResolveQualifier(qualifier, compilation);
+
+            if (typeSymbol == null)
+            {
+                return (new List<object>(), null, null);
+            }
+            
+            var results = new List<object>();
+            var classFullName = typeSymbol.ToDisplayString();
+
+            foreach (var tree in syntaxTrees)
+            {
+                var semanticModel = compilation.GetSemanticModel(tree);
+                var root = tree.GetRoot();
+                
+                var invocations = root.DescendantNodes().OfType<InvocationExpressionSyntax>();
+
+                foreach (var invocation in invocations)
+                {
+                    if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) continue;
+
+                    var isMatch = (methodName != null)
+                        ? methodSymbol.Name == methodName && methodSymbol.ContainingType.ToDisplayString() == classFullName
+                        : methodSymbol.ContainingType.ToDisplayString() == classFullName;
+
+                    if (!isMatch) continue;
+                    var location = tree.GetLineSpan(invocation.Span);
+                    results.Add(new
+                    {
+                        path = tree.FilePath,
+                        guid = AssetDatabase.AssetPathToGUID(tree.FilePath),
+                        line = location.StartLinePosition.Line + 1,
+                        invokedMethod = methodSymbol.Name,
+                        text = invocation.ToString()
+                    });
+                }
+            }
+            
+            var scriptPath = typeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree.FilePath;
+            var scriptGuid = !string.IsNullOrEmpty(scriptPath) ? AssetDatabase.AssetPathToGUID(scriptPath) : null;
+            var scriptType = !string.IsNullOrEmpty(scriptGuid) ? AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath)?.GetClass() : null;
+
+            return (results.Distinct().ToList(), scriptGuid, scriptType);
+        }
+
+        /// <summary>
+        /// Intelligently parses the qualifier string to find the type symbol and an optional method name.
+        /// </summary>
+        private static (INamedTypeSymbol typeSymbol, string methodName) ResolveQualifier(string qualifier, CSharpCompilation compilation)
+        {
+            // First, try to resolve the whole string as a type.
+            var typeSymbol = compilation.GetTypeByMetadataName(qualifier);
+            if (typeSymbol != null)
+            {
+                return (typeSymbol, null); // Found a class/type, no method.
+            }
+
+            // If that fails, assume the last part is a method name.
+            var lastDotIndex = qualifier.LastIndexOf('.');
+            if (lastDotIndex == -1)
+            {
+                return (null, null); // Not a namespaced type or method.
+            }
+
+            var potentialTypeName = qualifier[..lastDotIndex];
+            var potentialMethodName = qualifier[(lastDotIndex + 1)..];
+
+            typeSymbol = compilation.GetTypeByMetadataName(potentialTypeName);
+            if (typeSymbol == null) return (null, null); // Could not resolve.
+            // Check if a method with that name actually exists on the type
+            return typeSymbol.GetMembers(potentialMethodName).Any(m => m.Kind == SymbolKind.Method) ? 
+                (typeSymbol, potentialMethodName) : (null, null); // Could not resolve.
+        }
+
+        private static (CSharpCompilation, List<SyntaxTree>) GetProjectCompilation()
+        {
+            var sourceFiles = AssetDatabase.FindAssets("t:MonoScript")
+                .Select(AssetDatabase.GUIDToAssetPath)
+                .Where(p => p.StartsWith("Assets/"))
+                .ToList();
+                
+            var syntaxTrees = sourceFiles.Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), path: path)).ToList();
+
+            var references = new List<MetadataReference>();
+            foreach (var assembly in CompilationPipeline.GetAssemblies())
+            {
+                references.AddRange(assembly.compiledAssemblyReferences
+                    .Select(p => MetadataReference.CreateFromFile(p)));
+            }
+            
+            var compilation = CSharpCompilation.Create("ReferenceAnalysisAssembly", syntaxTrees, references);
+            return (compilation, syntaxTrees);
+        }
+    }
+}

+ 3 - 0
Assets/IntelligentProjectAnalyzer/Analyzer/RoslynReferenceFinder.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 76a8bc7a15ce4217b4a0a91a44a84f63
+timeCreated: 1752578985

+ 130 - 7
Assets/LLM/Assets/MCP_SystemPrompt.txt

@@ -2,13 +2,29 @@ You are MCP, an expert Unity developer integrated as an AI assistant inside the
 Your response MUST be a valid JSON object and nothing else. The response must start with `{` and end with `}`. Do not include any other text, explanations, or Markdown formatting like `json ` before or after the JSON object.
 The root JSON object must contain a single key, "commands", which is an array of command objects.
 
-## CORE PRINCIPLES
+## CORE PRINCIPLES & TWO-PHASE PROTOCOL
 
-1. Be an Expert: Act as a senior Unity developer. Understand concepts like prefabs, components, physics, and scripting.
-2. Be Curious (The Golden Rule): Your primary directive is to ask for information before taking action. Do not assume. If a user asks you to create something, first check if a suitable asset already exists. If they ask to modify something, first get the current state of that object. Use your tools to investigate.
-3. Be Precise: Use the tools provided to gather specific, detailed context. The more you know, the better your actions will be.
-4. Be Adaptive: Use the tools provided to adapt to the user's needs. Your responses should be flexible and context-sensitive.
-5. Be Concise: Your responses should be short and to the point. Avoid unnecessary details or explanations.
+Your workflow is structured into two phases to ensure efficiency and accuracy, mirroring an expert developer's process.
+
+### Phase 1: Disambiguation & Scoping
+Goal: Quickly resolve vague user prompts and identify the exact target asset or GameObject.
+
+1. Be Curious (The Golden Rule): Your primary directive is to ask for information before taking action. Do not assume.
+2. Handle Vague Prompts: If a user's prompt is vague (e.g., "Fix my grenade launcher"), your first action MUST be to use the `GetContextualMap` tool to find all potential targets.
+3. Handle Multiple Subjects: If the prompt mentions multiple potential subjects (e.g., "The grenade launcher should hit the target"), you MUST use GetContextualMap for EACH subject in a single GatherContextCommand to find all potential candidates. Do not use RequestAnalysisContextCommand.
+4. Clarify Choices:
+   - If `GetContextualMap` returns multiple items, use `DisplayMessageCommand` to present the choices to the user and ask for clarification.
+   - If it returns only one item, proceed directly to Phase 2 for that item.
+   - If it returns no items, inform the user with `DisplayMessageCommand`.
+5. Be Concise: Your responses should be short and to the point.
+
+### Phase 2: Deep Dive & Execution
+Goal: Once a target is identified, gather all necessary information about it and its dependencies in a single, efficient query.
+
+1. Gather Holistic Data: Use `GetDataFromPath` to perform a deep dive. When you request a component, you will automatically receive its data, its source code (if it's a custom script), and a list of all other components on the same GameObject.
+2. Verify Component Pre-requisites: This is critical. After getting the component data, you must analyze the allComponentsOnGameObject list. For example, if a script uses OnCollisionEnter, you MUST verify that a Collider and a Rigidbody are present in the list. If they are missing, your primary goal is to add them.
+3. Trace Critical Code Paths: If you identify a key method (e.g., FireGrenade), you MUST use the new FindReferences tool to see if it's being called anywhere in the project. If it's not, your primary goal is to find where it should be called from or inform the user.
+4. Execute Actions: After your holistic analysis, formulate a plan and use the action commands to fix the root cause of the problem.
 
 ## YOUR TOOL CHEST (INFORMATION GATHERING)
 
@@ -36,6 +52,18 @@ JSON Schema for GatherContextCommand:
 
 Merge as many GatherContextCommands as you can, assuming all the possible combinations of data you need at once for one request. 
 
+Memory Management Tools:
+
+* `UpdateWorkingContextCommand`: To write, update, or clear your short-term memory. This is your most-used utility command.
+   * `jsonData` Schema:
+   ```
+    {
+      "updates": { "newKey": "newValue", "existingKey": "updatedValue" },
+      "keysToRemove": ["oldKey"],
+      "clearAll": false
+    }
+    ```
+
 Available `dataType` Tools:
 
 * `ComponentData`: Gets the serialized properties of a single component on a GameObject.
@@ -68,6 +96,26 @@ Available `dataType` Tools:
    * `subjectIdentifier`: The **GUID** of the asset file in the project.
    * `qualifier`: "recursive" or "direct".
    * Pass qualifier if you want to get a dependency graph for a specific asset recursively, else you can pass null or "direct".
+   
+* `GetContextualMap`: To find potential targets when the user's prompt is vague.
+   * `subjectIdentifier`: (Not used).
+   * `qualifier`: The search term from the user's prompt (e.g., "grenade launcher").
+   * Returns: A JSON object with categorized lists of matches: {"prefabs": [...], "sceneObjects": [...], "scripts": [...]}.
+   
+* `GetDataFromPath`: To perform a "deep dive" on a specific target to get all necessary data in one request.
+   * `subjectIdentifier`: The ID of the root GameObject or asset to start from.
+   * `qualifier`: A JSON object defining a sequence of steps: {"steps": [{"type": "...", "name": "..."}, ...]}.
+   * Path Step Types: 
+     - {"type": "child", "name": "ChildObjectName"}: Navigates to a child GameObject.
+     - {"type": "component", "name": "UnityEngine.Rigidbody"}: Gets a component.
+     - {"type": "property", "name": "mass"}: Gets the value of a public property.
+     - {"type": "field", "name": "_myField"}: Gets the value of a public or private field.
+  * Eager Loading: When you request a custom user script via the component step, its full source code will be automatically included in the response. You do not need a separate SourceCode request unless absolutely necessary.
+  
+* `FindReferences`: Finds all references to a method or class.
+   * `subjectIdentifier`: (Not used).
+   * `qualifier`: The full name of the method or class (e.g., MyNamespace.MyClass.MyMethod).
+   * Shortcut: If you have recently analyzed a script and saved its class name to the lastScriptAnalyzed key in your working context, you can simply use the method name as the qualifier (e.g., "MyMethod").
 
 ## ACTION & UTILITY COMMANDS
 
@@ -225,4 +273,79 @@ Follow this logic when responding to a user prompt:
    - Is the request ambiguous?
      - YES: Use `RequestAnalysisContextCommand` to ask the user to provide the relevant objects.
 
-4. Construct and Return Your Command JSON.
+4. Construct and Return Your Command JSON.
+
+## HOLISTIC ANALYSIS
+   - Scenario: User says, "The grenade launcher should hit the target."
+   - Phase 1: You use GetContextualMap to find candidates for "grenade launcher" and "target" and clarify with the user. The user confirms the GrenadeLauncher prefab and the Target prefab.
+   - Phase 2 - Holistic Deep Dive (Step 1): You decide to investigate the GrenadeLauncher's script.
+   
+   {
+     "commands": [
+       {
+         "commandName": "GatherContextCommand",
+         "jsonData": {
+           "requests": [
+             {
+               "contextKey": "launcherAnalysis",
+               "subjectIdentifier": "ID_of_Launcher_Prefab",
+               "dataType": "GetDataFromPath",
+               "qualifier": {
+                 "steps": [
+                   { "type": "component", "name": "GrenadeLauncher" }
+                 ]
+               }
+             }
+           ]
+         }
+       }
+     ]
+   }
+   
+   - System returns the GrenadeLauncher component data, its source code, and the list of all other components on the prefab. You analyze the source and see the FireGrenade() method.
+   - Phase 2 - Holistic Deep Dive (Step 2): You must now validate the code path.
+   
+   {
+     "commands": [
+       {
+         "commandName": "GatherContextCommand",
+         "jsonData": {
+           "requests": [
+             {
+               "contextKey": "fireGrenadeReferences",
+               "dataType": "FindReferences",
+               "qualifier": "FireGrenade"
+             }
+           ]
+         }
+       }
+     ]
+   }
+   
+   - System returns that FireGrenade has no references. You have found a root cause. You also notice from the previous step that the Grenade prefab itself is assigned, but you haven't checked its components.
+   - Phase 2 - Holistic Deep Dive (Step 3): You now investigate the Grenade prefab to check its pre-requisites.
+   
+   {
+     "commands": [
+       {
+         "commandName": "GatherContextCommand",
+         "jsonData": {
+           "requests": [
+             {
+               "contextKey": "grenadeAnalysis",
+               "subjectIdentifier": "ID_of_Grenade_Prefab",
+               "dataType": "GetDataFromPath",
+               "qualifier": {
+                 "steps": [
+                   { "type": "component", "name": "Grenade" }
+                 ]
+               }
+             }
+           ]
+         }
+       }
+     ]
+   }
+   
+   - System returns the Grenade component data and the list of all components on the prefab. You analyze the allComponentsOnGameObject list and see there is no Rigidbody. You have found the second root cause.
+   - Final Action: You now have a complete diagnosis. Your response would be a sequence of commands to first add the Rigidbody to the Grenade prefab, and then use DisplayMessageCommand to inform the user that FireGrenade() is never called and ask them how they intend to trigger it.

+ 3 - 1
Assets/LLM/Editor/Analysis/ContextProviderRegistry.cs

@@ -1,4 +1,3 @@
-using UnityEngine;
 using System.Collections.Generic;
 
 namespace LLM.Editor.Analysis
@@ -21,6 +20,9 @@ namespace LLM.Editor.Analysis
             RegisterProvider("FindInScene", new FindInSceneProvider());
             RegisterProvider("ConsoleLog", new ConsoleLogProvider());
             RegisterProvider("DependencyGraph", new DependencyGraphProvider());
+            RegisterProvider("GetContextualMap", new GetContextualMapProvider());
+            RegisterProvider("GetDataFromPath", new GetDataFromPathProvider());
+            RegisterProvider("FindReferences", new FindReferencesProvider());
         }
 
         private static void RegisterProvider(string dataType, IContextProvider provider)

+ 88 - 0
Assets/LLM/Editor/Analysis/FindReferencesProvider.cs

@@ -0,0 +1,88 @@
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using LLM.Editor.Core;
+using System.Collections.Generic;
+using IntelligentProjectAnalyzer.Analyzer;
+using Object = UnityEngine.Object;
+
+namespace LLM.Editor.Analysis
+{
+    /// <summary>
+    /// Finds all references to a specific method or class in the project using Roslyn for scripts,
+    /// AssetDatabase for prefabs, and a scene search for GameObjects.
+    /// </summary>
+    public class FindReferencesProvider : IContextProvider
+    {
+        public object GetContext(Object target, string qualifier)
+        {
+            if (string.IsNullOrEmpty(qualifier))
+            {
+                return "Error: A method or class name (qualifier) is required for FindReferences.";
+            }
+
+            if (!qualifier.Contains(".")) // If it's just a method name...
+            {
+                // ...try to find the class from the working context.
+                var workingContext = SessionManager.LoadWorkingContext();
+                var lastScript = workingContext["lastScriptAnalyzed"]?["className"]?.ToString();
+                if (!string.IsNullOrEmpty(lastScript))
+                {
+                    var fullQualifier = $"{lastScript}.{qualifier}";
+                    Debug.Log($"[FindReferencesProvider] Inferred full qualifier as '{fullQualifier}' from working context.");
+                }
+            }
+
+            // Let Roslyn handle the complex task of parsing the qualifier.
+            var (scriptReferences, foundScriptGuid, scriptType) = RoslynReferenceFinder.FindReferences(qualifier);
+
+            if (string.IsNullOrEmpty(foundScriptGuid) || scriptType == null)
+            {
+                return $"Error: Could not resolve '{qualifier}' to a known type or method in the project.";
+            }
+
+            var prefabReferences = FindPrefabReferences(foundScriptGuid);
+            var sceneReferences = FindSceneReferences(scriptType);
+
+            return new
+            {
+                scriptReferences,
+                prefabReferences,
+                sceneReferences
+            };
+        }
+
+        /// <summary>
+        /// Finds all prefabs that have the target script as a dependency.
+        /// </summary>
+        private static List<object> FindPrefabReferences(string scriptGuid)
+        {
+            var results = new List<object>();
+            var allPrefabGuids = AssetDatabase.FindAssets("t:Prefab");
+
+            foreach (var prefabGuid in allPrefabGuids)
+            {
+                var path = AssetDatabase.GUIDToAssetPath(prefabGuid);
+                var dependencies = AssetDatabase.GetDependencies(path, false);
+                if (dependencies.Any(dep => AssetDatabase.AssetPathToGUID(dep) == scriptGuid))
+                {
+                    results.Add(new { path, guid = prefabGuid });
+                }
+            }
+            return results;
+        }
+
+        /// <summary>
+        /// Finds all GameObjects in the active scene that have the target script component.
+        /// </summary>
+        private static List<object> FindSceneReferences(System.Type scriptType)
+        {
+            var instances = Object.FindObjectsOfType(scriptType, true);
+            return instances.Select(inst => new
+            {
+                inst.name,
+                instanceId = inst.GetInstanceID()
+            }).Cast<object>().ToList();
+        }
+    }
+}

+ 3 - 0
Assets/LLM/Editor/Analysis/FindReferencesProvider.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 05898c1e208f40b0bf15d3fa7fb55ce6
+timeCreated: 1752578460

+ 46 - 0
Assets/LLM/Editor/Analysis/GetContextualMapProvider.cs

@@ -0,0 +1,46 @@
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+namespace LLM.Editor.Analysis
+{
+    public class GetContextualMapProvider : IContextProvider
+    {
+        public object GetContext(Object target, string qualifier)
+        {
+            if (string.IsNullOrEmpty(qualifier))
+            {
+                return "Error: A search term (qualifier) is required for GetContextualMap.";
+            }
+
+            var prefabs = AssetDatabase.FindAssets($"t:Prefab {qualifier}")
+                .Select(AssetDatabase.GUIDToAssetPath)
+                .Select(path => new { name = System.IO.Path.GetFileName(path), path, guid = AssetDatabase.AssetPathToGUID(path) })
+                .ToList();
+
+            var scripts = AssetDatabase.FindAssets($"t:MonoScript {qualifier}")
+                .Select(AssetDatabase.GUIDToAssetPath)
+                .Where(path => path.StartsWith("Assets/"))
+                .Select(path => new { name = System.IO.Path.GetFileName(path), path, guid = AssetDatabase.AssetPathToGUID(path) })
+                .ToList();
+
+            var sceneObjects = Object.FindObjectsOfType<GameObject>()
+                .Where(go => go.name.ToLower().Contains(qualifier.ToLower()))
+                .Select(go => new { go.name, id = go.GetInstanceID().ToString() })
+                .ToList();
+            
+            var sceneObjectsByComponent = Object.FindObjectsOfType<Component>()
+                .Where(c => c.GetType().Name.ToLower().Contains(qualifier.ToLower()))
+                .Select(c => new { name = c.gameObject.name + $" Component: ({c.GetType().Name})", id = c.gameObject.GetInstanceID().ToString() })
+                .ToList();
+
+            return new
+            {
+                prefabs,
+                scripts,
+                sceneObjects,
+                sceneObjectsByComponent
+            };
+        }
+    }
+}

+ 3 - 0
Assets/LLM/Editor/Analysis/GetContextualMapProvider.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f1448a43d771490481dd3ad99f58d790
+timeCreated: 1752575739

+ 99 - 0
Assets/LLM/Editor/Analysis/GetDataFromPathProvider.cs

@@ -0,0 +1,99 @@
+using System;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using System.Reflection;
+using LLM.Editor.Helper;
+using System.Collections.Generic;
+using Object = UnityEngine.Object;
+
+namespace LLM.Editor.Analysis
+{
+    public class GetDataFromPathProvider : IContextProvider
+    {
+        public object GetContext(Object target, string qualifier)
+        {
+            if (string.IsNullOrEmpty(qualifier))
+            {
+                return "Error: A JSON path (qualifier) is required for GetDataFromPath.";
+            }
+
+            var path = qualifier.FromJson<PathRequest>();
+            if (path?.steps == null || !path.steps.Any())
+            {
+                return "Error: Invalid path provided.";
+            }
+
+            object currentObject = target;
+            var currentGameObject = target switch
+            {
+                GameObject gameObject => gameObject,
+                Component component2 => component2.gameObject,
+                _ => null
+            };
+
+            foreach (var step in path.steps)
+            {
+                if (currentObject == null)
+                {
+                    return "Error: Path evaluation failed at a null object.";
+                }
+                
+                Debug.Log($"Object is: {currentObject}. Type: {currentObject.GetType()}");
+
+                currentObject = step.type.ToLower() switch
+                {
+                    "component" => currentGameObject ? currentGameObject.GetComponent(step.name) : target is Component ? target : null,
+                    "field" => currentObject.GetType().GetField(step.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(currentObject),
+                    "property" => currentObject.GetType().GetProperty(step.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(currentObject),
+                    "child" => ((GameObject)currentObject).transform.Find(step.name)?.gameObject,
+                    _ => "Error: Unknown path step type."
+                };
+            }
+
+            if (currentObject is not Component component) return currentObject;
+            
+            var allComponents = component.GetComponents<Component>()
+                .Select(c => c.GetType().FullName)
+                .ToList();
+
+            var result = new Dictionary<string, object>
+            {
+                ["requestedComponentData"] = component,
+                ["allComponentsOnGameObject"] = allComponents
+            };
+
+            if (component is not MonoBehaviour monoBehaviour || !IsCustomScript(monoBehaviour)) return result;
+            
+            // Eager loading for custom scripts
+            var script = MonoScript.FromMonoBehaviour(monoBehaviour);
+            var scriptPath = AssetDatabase.GetAssetPath(script);
+            result["sourceCode"] = File.ReadAllText(scriptPath);
+
+            return result;
+        }
+
+        private static bool IsCustomScript(Component component)
+        {
+            if (!component) return false;
+            var script = MonoScript.FromMonoBehaviour(component as MonoBehaviour);
+            if (!script) return false;
+            var path = AssetDatabase.GetAssetPath(script);
+            return !string.IsNullOrEmpty(path) && path.StartsWith("Assets/");
+        }
+    }
+
+    [Serializable]
+    public class PathRequest
+    {
+        public List<PathStep> steps;
+    }
+
+    [Serializable]
+    public class PathStep
+    {
+        public string type;
+        public string name;
+    }
+}

+ 3 - 0
Assets/LLM/Editor/Analysis/GetDataFromPathProvider.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: fcb2e5c61188439ab20a1b13a01dfde4
+timeCreated: 1752575887

+ 17 - 3
Assets/LLM/Editor/Client/GeminiApiClient.cs

@@ -22,7 +22,7 @@ namespace LLM.Editor.Client
     /// </summary>
     public class GeminiApiClient : ILlmApiClient
     {
-        [System.Serializable]
+        [Serializable]
         private class CommandResponse { public List<CommandData> commands; }
 
         private Settings.MCPSettings _settings;
@@ -103,11 +103,13 @@ namespace LLM.Editor.Client
         {
             var url = $"https://{_settings.gcpRegion}-aiplatform.googleapis.com/v1/projects/{_settings.gcpProjectId}/locations/{_settings.gcpRegion}/publishers/google/models/{_settings.modelName}:generateContent";
             
+            var systemPromptWithContext = GetWorkingContext();
+            
             var apiRequest = new ApiRequest
             {
                 system_instruction = new SystemInstruction
                 {
-                    parts = new List<Part> { new() { text = _systemPrompt } }
+                    parts = new List<Part> { new() { text = systemPromptWithContext } }
                 },
                 contents = chatHistory.Select(entry => new Content
                 {
@@ -232,11 +234,23 @@ namespace LLM.Editor.Client
                 Debug.LogError($"[GeminiApiClient] gcloud auth error: {error}");
                 return null;
             }
-            catch (System.Exception e)
+            catch (Exception e)
             {
                 Debug.LogError($"[GeminiApiClient] Exception while getting auth token: {e.Message}");
                 return null;
             }
         }
+
+        private string GetWorkingContext()
+        {
+            var workingContext = SessionManager.LoadWorkingContext();
+            var systemPromptWithContext = new StringBuilder(_systemPrompt);
+            systemPromptWithContext.AppendLine("\n\n## CURRENT WORKING CONTEXT");
+            systemPromptWithContext.AppendLine("This is your short-term memory. Use the data here before asking for it again.");
+            systemPromptWithContext.AppendLine("```json");
+            systemPromptWithContext.AppendLine(workingContext.ToString(Newtonsoft.Json.Formatting.Indented));
+            systemPromptWithContext.AppendLine("```");
+            return systemPromptWithContext.ToString();
+        }
     }
 }

+ 8 - 1
Assets/LLM/Editor/Commands/GatherContextCommand.cs

@@ -43,8 +43,15 @@ namespace LLM.Editor.Commands
                     results[request.contextKey] = $"Error: No provider for dataType '{request.dataType}'";
                     continue;
                 }
+                
+                // The qualifier is now passed as a JToken, which can be either a string or a complex object.
+                // The provider itself will handle the specific type it expects. For most providers, this
+                // will just be a string. For GetDataFromPath, it will be a PathRequest object.
+                var qualifierString = request.qualifier?.Type == Newtonsoft.Json.Linq.JTokenType.String 
+                    ? request.qualifier.ToString() 
+                    : request.qualifier?.ToString(Newtonsoft.Json.Formatting.None);
 
-                var contextData = provider.GetContext(targetObject, request.qualifier);
+                var contextData = provider.GetContext(targetObject, qualifierString);
                 results[request.contextKey] = contextData;
             }
 

+ 3 - 2
Assets/LLM/Editor/Commands/GatherContextParams.cs

@@ -1,3 +1,4 @@
+using Newtonsoft.Json.Linq;
 using System.Collections.Generic;
 
 namespace LLM.Editor.Commands
@@ -9,7 +10,7 @@ namespace LLM.Editor.Commands
     [System.Serializable]
     public class GatherContextParams
     {
-        public List<ContextRequest> requests = new List<ContextRequest>();
+        public List<ContextRequest> requests = new();
     }
 
     /// <summary>
@@ -40,6 +41,6 @@ namespace LLM.Editor.Commands
         /// An optional qualifier to provide more specific instructions to the data provider.
         /// e.g., "UnityEngine.Rigidbody", "Physics", "t:Prefab player"
         /// </summary>
-        public string qualifier;
+        public JToken qualifier;
     }
 }

+ 71 - 0
Assets/LLM/Editor/Commands/UpdateWorkingContextCommand.cs

@@ -0,0 +1,71 @@
+using UnityEngine;
+using LLM.Editor.Data;
+using LLM.Editor.Core;
+using LLM.Editor.Helper;
+using Newtonsoft.Json.Linq;
+using JetBrains.Annotations;
+
+namespace LLM.Editor.Commands
+{
+    /// <summary>
+    /// A command that allows the LLM to write to, update, or clear its own
+    /// short-term memory for the current session.
+    /// </summary>
+    [UsedImplicitly]
+    public class UpdateWorkingContextCommand : ICommand
+    {
+        [System.Serializable]
+        private class UpdateWorkingContextParams
+        {
+            public JObject updates;
+            public string[] keysToRemove;
+            public bool clearAll;
+        }
+
+        private readonly UpdateWorkingContextParams _params;
+
+        public UpdateWorkingContextCommand(string jsonParams)
+        {
+            _params = jsonParams?.FromJson<UpdateWorkingContextParams>();
+        }
+
+        public CommandOutcome Execute(CommandContext context)
+        {
+            if (_params == null)
+            {
+                Debug.LogError("[UpdateWorkingContextCommand] Invalid parameters.");
+                return CommandOutcome.Error;
+            }
+
+            var workingContext = SessionManager.LoadWorkingContext();
+
+            if (_params.clearAll)
+            {
+                workingContext = new JObject();
+            }
+            else
+            {
+                if (_params.keysToRemove != null)
+                {
+                    foreach (var key in _params.keysToRemove)
+                    {
+                        workingContext.Remove(key);
+                    }
+                }
+
+                if (_params.updates != null)
+                {
+                    workingContext.Merge(_params.updates, new JsonMergeSettings
+                    {
+                        MergeArrayHandling = MergeArrayHandling.Replace
+                    });
+                }
+            }
+
+            SessionManager.SaveWorkingContext(workingContext);
+            Debug.Log($"[UpdateWorkingContextCommand] Working context updated:\n{workingContext.ToString(Newtonsoft.Json.Formatting.Indented)}");
+
+            return CommandOutcome.Success;
+        }
+    }
+}

+ 3 - 0
Assets/LLM/Editor/Commands/UpdateWorkingContextCommand.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 771f68987ceb4a96ac9f24efc4dae16c
+timeCreated: 1752579773

+ 33 - 4
Assets/LLM/Editor/Core/CommandExecutor.cs

@@ -1,11 +1,11 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
-using System.Reflection;
-using LLM.Editor.Commands;
-using LLM.Editor.Data;
 using UnityEditor;
 using UnityEngine;
+using LLM.Editor.Data;
+using System.Reflection;
+using LLM.Editor.Commands;
+using System.Collections.Generic;
 
 namespace LLM.Editor.Core
 {
@@ -20,6 +20,12 @@ namespace LLM.Editor.Core
 
         public static Action OnQueueUpdated;
         public static event Action<string> OnContextReadyForNextTurn;
+        
+        private static readonly HashSet<string> AutoExecutableCommands = new()
+        {
+            nameof(GatherContextCommand),
+            nameof(UpdateWorkingContextCommand)
+        };
 
         static CommandExecutor()
         {
@@ -43,6 +49,7 @@ namespace LLM.Editor.Core
                 Debug.Log($"[CommandExecutor] Resuming session with {_commandQueue.Count} command(s) in queue.");
                 _currentContext = new CommandContext();
                 OnQueueUpdated?.Invoke();
+                TriggerAutoExecution();
             }
             else
             {
@@ -62,6 +69,7 @@ namespace LLM.Editor.Core
                 Debug.Log($"<color=cyan>[CommandExecutor]: {command.commandName}</color>");
             }
             Debug.Log("[CommandExecutor] Queue set.");
+            TriggerAutoExecution();
         }
 
         private static void ClearQueue()
@@ -74,6 +82,17 @@ namespace LLM.Editor.Core
         public static bool HasPendingCommands() => _commandQueue != null &&  _commandQueue.Any();
         
         public static CommandData GetNextCommand() => HasPendingCommands() ? _commandQueue.First() : null;
+        
+        private static void TriggerAutoExecution()
+        {
+            EditorApplication.delayCall += () =>
+            {
+                if (IsNextCommandAutoExecutable())
+                {
+                    ExecuteNextCommand();
+                }
+            };
+        }
 
         public static void ExecuteNextCommand()
         {
@@ -99,6 +118,7 @@ namespace LLM.Editor.Core
                         case CommandOutcome.Success:
                             // The command succeeded, so we can remove it and continue.
                             _commandQueue.RemoveAt(0);
+                            TriggerAutoExecution();
                             break;
                         case CommandOutcome.Error:
                             Debug.LogError($"[CommandExecutor] Command '{commandData.commandName}' failed. Clearing remaining command queue.");
@@ -137,6 +157,15 @@ namespace LLM.Editor.Core
             SessionManager.SaveCommandQueue(_commandQueue);
             OnQueueUpdated?.Invoke();
         }
+        
+        private static bool IsNextCommandAutoExecutable()
+        {
+            if (!HasPendingCommands()) return false;
+            var nextCommandName = GetNextCommand().commandName;
+            // Ensure the command name doesn't have "Command" suffix before checking
+            var cleanName = nextCommandName.EndsWith("Command") ? nextCommandName : nextCommandName + "Command";
+            return AutoExecutableCommands.Contains(cleanName);
+        }
 
         private static ICommand CreateCommandInstance(CommandData data)
         {   

+ 22 - 3
Assets/LLM/Editor/Core/SessionManager.cs

@@ -1,9 +1,10 @@
-using System.Collections.Generic;
 using System.IO;
-using LLM.Editor.Data;
-using LLM.Editor.Helper;
 using UnityEditor;
 using UnityEngine;
+using LLM.Editor.Data;
+using LLM.Editor.Helper;
+using Newtonsoft.Json.Linq;
+using System.Collections.Generic;
 
 namespace LLM.Editor.Core
 {
@@ -19,6 +20,7 @@ namespace LLM.Editor.Core
         private const string SESSION_ID_KEY = "MCP_CurrentSessionID";
         private const string QUEUE_FILE = "queue.json";
         private const string CHAT_LOG_FILE = "chat_log.json";
+        private const string WORKING_CONTEXT_FILE = "working_context.json";
 
         // Helper class for serializing a list to JSON
         [System.Serializable] private class CommandListWrapper { public List<CommandData> commands; }
@@ -41,6 +43,7 @@ namespace LLM.Editor.Core
             _currentSessionId = $"session_{System.DateTime.Now:yyyyMMddHHmmss}";
             SessionState.SetString(SESSION_ID_KEY, _currentSessionId);
             Directory.CreateDirectory(Path.Combine(_sessionCacheRoot, _currentSessionId));
+            SaveWorkingContext(new JObject());
             Debug.Log($"[SessionManager] Started new session: {_currentSessionId}");
         }
 
@@ -97,5 +100,21 @@ namespace LLM.Editor.Core
             var wrapper = json.FromJson<ChatLogWrapper>();
             return wrapper?.history ?? new List<ChatEntry>();
         }
+        
+        public static void SaveWorkingContext(JObject context)
+        {
+            if (!HasActiveSession()) return;
+            var path = Path.Combine(_sessionCacheRoot, _currentSessionId, WORKING_CONTEXT_FILE);
+            File.WriteAllText(path, context.ToString(Newtonsoft.Json.Formatting.Indented));
+        }
+
+        public static JObject LoadWorkingContext()
+        {
+            if (!HasActiveSession()) return new JObject();
+            var path = Path.Combine(_sessionCacheRoot, _currentSessionId, WORKING_CONTEXT_FILE);
+            if (!File.Exists(path)) return new JObject();
+            var json = File.ReadAllText(path);
+            return JObject.Parse(json);
+        }
     }
 }

+ 0 - 1
Assets/LLM/Editor/Helper/JsonWriter.cs

@@ -28,7 +28,6 @@ namespace LLM.Editor.Helper
             Formatting = Formatting.None,
             NullValueHandling = NullValueHandling.Ignore,
             DefaultValueHandling = DefaultValueHandling.Include,
-            TypeNameHandling = TypeNameHandling.Auto,
             ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
             Converters = UnityTypeConverters.Converters
         };

+ 40 - 10
Assets/LLM/Editor/Helper/UnityTypeConverters.cs

@@ -1,7 +1,9 @@
 using System;
-using System.Collections.Generic;
-using Newtonsoft.Json;
+using UnityEditor;
 using UnityEngine;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using Object = UnityEngine.Object;
 
 namespace LLM.Editor.Helper
 {
@@ -9,6 +11,7 @@ namespace LLM.Editor.Helper
     {
         public static readonly List<JsonConverter> Converters = new()
         {
+            new ObjectConverter(),
             new Vector3Converter(),
             new QuaternionConverter(),
             new ColorConverter(),
@@ -23,9 +26,41 @@ namespace LLM.Editor.Helper
             new BoundsIntConverter()
         };
         
-        /// <summary>
-        /// Custom JsonConverter to safely serialize a Vector3 without self-referencing loops.
-        /// </summary>
+        private class ObjectConverter : JsonConverter<Object>
+        {
+            public override void WriteJson(Newtonsoft.Json.JsonWriter writer, Object value, JsonSerializer serializer)
+            {
+                if (value == null)
+                {
+                    writer.WriteNull();
+                    return;
+                }
+
+                writer.WriteStartObject();
+                writer.WritePropertyName("name");
+                writer.WriteValue(value.name);
+                writer.WritePropertyName("instanceId");
+                writer.WriteValue(value.GetInstanceID());
+                writer.WritePropertyName("type");
+                writer.WriteValue(value.GetType().FullName);
+
+                if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(value, out var guid, out long _))
+                {
+                    if (!string.IsNullOrEmpty(guid) && guid != "00000000000000000000000000000000")
+                    {
+                        writer.WritePropertyName("guid");
+                        writer.WriteValue(guid);
+                    }
+                }
+                writer.WriteEndObject();
+            }
+
+            public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, bool hasExistingValue, JsonSerializer serializer)
+            {
+                throw new NotImplementedException();
+            }
+        }
+        
         private class Vector3Converter : JsonConverter<Vector3>
         {
             public override void WriteJson(Newtonsoft.Json.JsonWriter writer, Vector3 value, JsonSerializer serializer)
@@ -42,14 +77,10 @@ namespace LLM.Editor.Helper
 
             public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
             {
-                // Not needed for serialization-only purposes
                 throw new NotImplementedException();
             }
         }
     
-        /// <summary>
-        /// Custom JsonConverter to safely serialize a Quaternion.
-        /// </summary>
         private class QuaternionConverter : JsonConverter<Quaternion>
         {
             public override void WriteJson(Newtonsoft.Json.JsonWriter writer, Quaternion value, JsonSerializer serializer)
@@ -68,7 +99,6 @@ namespace LLM.Editor.Helper
 
             public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
             {
-                // Not needed for serialization-only purposes
                 throw new NotImplementedException();
             }
         }

+ 2 - 1
Assets/LLM/Editor/LLM.Editor.asmdef

@@ -2,7 +2,8 @@
     "name": "LLM.Editor",
     "rootNamespace": "LLM.Editor",
     "references": [
-        "GUID:64b128cb094ec484c9b122f90ae690f3"
+        "GUID:64b128cb094ec484c9b122f90ae690f3",
+        "GUID:e7c9410007423435ea89724962e62f27"
     ],
     "includePlatforms": [
         "Editor"