From 8a0d710d6ee7ee6c97cdcef9ba0e6c6da93898a5 Mon Sep 17 00:00:00 2001 From: "ProjectKoi-Kalo\\Kalo" Date: Thu, 1 Jan 2026 18:48:19 +0800 Subject: [PATCH] zc --- .../EventSystem/AI/AIIntelligenceCore.cs | 546 +++++++++--------- 1 file changed, 282 insertions(+), 264 deletions(-) diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 6991dff7..fa346194 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -46,6 +46,8 @@ namespace WulaFallenEmpire.EventSystem.AI private string _actionToolLedgerNote = "Tool Ledger (Action): None (no successful tool calls)."; private bool _querySuccessfulToolCall; private bool _actionSuccessfulToolCall; + private bool _queryRetryUsed; + private bool _actionRetryUsed; private readonly List _actionSuccessLedger = new List(); private readonly HashSet _actionSuccessLedgerSet = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly HashSet _lastActionStepSignatures = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -101,20 +103,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori private const string ToolRulesInstruction = @" # TOOL USE RULES -1. **FORMATTING**: Your output MUST be valid JSON with fields: - { ""thought"": ""..."", ""tool_calls"": [ { ""type"": ""function"", ""function"": { ""name"": ""tool_name"", ""arguments"": { ... } } } ] } +1. **FORMATTING**: Tool calls MUST be valid JSON using the following schema: + { ""tool_calls"": [ { ""type"": ""function"", ""function"": { ""name"": ""tool_name"", ""arguments"": { ... } } } ] } 2. **STRICT OUTPUT**: - - If tools are needed, output non-empty ""tool_calls"". - - If no tools are needed, output exactly: { ""tool_calls"": [] } (you may still include ""thought""). - - Do NOT include any natural language, explanation, markdown, or extra text outside JSON. -3. **THOUGHT**: ""thought"" is internal and will NOT be shown to the user. -4. **MULTI-REQUEST RULE**: - - If the user requests multiple items or information, you MUST output ALL required tool calls in the SAME response. + - Your output MUST be either: + - A JSON object with ""tool_calls"" (may be empty), OR + - Exactly: { ""tool_calls"": [] } + Do NOT include any natural language, explanation, markdown, or additional commentary. +3. **MULTI-REQUEST RULE**: + - If the user requests multiple items or information, you MUST output ALL required tool calls in the SAME tool-phase response. - Do NOT split multi-item requests across turns. -5. **TOOLS**: You MAY call any tools listed in ""# TOOLS (AVAILABLE)"". -6. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, output { ""tool_calls"": [] }. -7. **WORKFLOW PREFERENCE**: Prefer the flow Query tools -> Action tools -> Reply. If action results reveal missing info, you MAY return to Query and then Action again. -8. **NO TAGS**: Do NOT use tags, code fences, or any extra text outside JSON."; +4. **TOOLS**: You MAY call any tools listed in ""# TOOLS (AVAILABLE)"". +5. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, output { ""tool_calls"": [] } and proceed to the next phase."; public AIIntelligenceCore(World world) : base(world) { @@ -2036,6 +2036,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori { if (_isThinking) return; SetThinkingState(true); + _thinkingPhaseTotal = 3; + SetThinkingPhase(1, false); ResetTurnState(); try @@ -2062,165 +2064,214 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori var client = new SimpleAIClient(apiKey, baseUrl, model, settings.useGeminiProtocol); _currentClient = client; - // ReAct Tool Loop: Start with null image. The model must request it using analyze_screen or capture_screen if needed. + // Model-Driven Vision: Start with null image. The model must request it using analyze_screen or capture_screen if needed. string base64Image = null; - int maxSteps = int.MaxValue; - float maxSeconds = DefaultReactMaxSeconds; + - if (settings != null) + var queryPhase = RequestPhase.QueryTools; + if (Prefs.DevMode) { - maxSeconds = Math.Max(2f, settings.reactMaxSeconds <= 0f ? DefaultReactMaxSeconds : settings.reactMaxSeconds); + WulaLog.Debug($"[WulaAI] ===== Turn 1/3 ({queryPhase}) ====="); } - _thinkingPhaseTotal = 0; - string toolPhaseReplyCandidate = null; - for (int step = 1; step <= maxSteps; step++) + + string queryInstruction = GetToolSystemInstruction(queryPhase, !string.IsNullOrEmpty(base64Image)); + string queryResponse = await client.GetChatCompletionAsync(queryInstruction, BuildToolContext(queryPhase), maxTokens: 2048, temperature: 0.1f, base64Image: base64Image); + if (string.IsNullOrEmpty(queryResponse)) { - if (HasTimedOut(maxSeconds)) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] ReAct loop timed out."); - } - break; - } - - SetThinkingPhase(step, false); - - string reactInstruction = GetReactSystemInstruction(!string.IsNullOrEmpty(base64Image)); - reactInstruction += "\n\n" + BuildNarratorInstruction(step); - var reactContext = BuildReactContext(); - string response = await client.GetChatCompletionAsync(reactInstruction, reactContext, maxTokens: 2048, temperature: 0.1f, base64Image: base64Image); - if (string.IsNullOrEmpty(response)) - { - AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate()); - return; - } - - string normalizedResponse = NormalizeReactResponse(response); - string parsedSource = normalizedResponse; - if (!JsonToolCallParser.TryParseToolCallsFromText(normalizedResponse, out var toolCalls, out string jsonFragment)) - { - if (LooksLikeNaturalReply(normalizedResponse)) - { - toolPhaseReplyCandidate = normalizedResponse; - AddTraceNote(normalizedResponse); - break; - } - - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] ReAct step missing JSON envelope; attempting format fix."); - } - string fixInstruction = BuildReactFormatFixInstruction(normalizedResponse); - string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, reactContext, maxTokens: 1024, temperature: 0.1f, base64Image: base64Image); - string normalizedFixed = NormalizeReactResponse(fixedResponse); - if (string.IsNullOrEmpty(normalizedFixed) || - !JsonToolCallParser.TryParseToolCallsFromText(normalizedFixed, out toolCalls, out jsonFragment)) - { - if (LooksLikeNaturalReply(normalizedFixed)) - { - toolPhaseReplyCandidate = normalizedFixed; - AddTraceNote(normalizedFixed); - } - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] ReAct format fix failed."); - } - break; - } - parsedSource = normalizedFixed; - } - - if (!string.IsNullOrWhiteSpace(parsedSource)) - { - string traceText = parsedSource; - if (!string.IsNullOrWhiteSpace(jsonFragment)) - { - int idx = traceText.IndexOf(jsonFragment, StringComparison.Ordinal); - if (idx >= 0) - { - traceText = traceText.Remove(idx, jsonFragment.Length); - } - } - traceText = traceText.Trim(); - if (LooksLikeNaturalReply(traceText)) - { - AddTraceNote(traceText); - } - } - string thoughtNote = ExtractThoughtFromToolJson(jsonFragment) ?? ExtractThoughtFromToolJson(parsedSource); - if (!string.IsNullOrWhiteSpace(thoughtNote)) - { - AddTraceNote($"Thought: {thoughtNote}"); - } - - var invalidTools = toolCalls - .Where(c => !IsToolAvailable(c.Name)) - .Select(c => c.Name) - .Where(n => !string.IsNullOrWhiteSpace(n)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - if (invalidTools.Count > 0) - { - if (Prefs.DevMode) - { - WulaLog.Debug($"[WulaAI] ReAct step used invalid tools: {string.Join(", ", invalidTools)}"); - } - string correctionInstruction = BuildReactToolCorrectionInstruction(invalidTools); - string correctedResponse = await client.GetChatCompletionAsync(correctionInstruction, reactContext, maxTokens: 1024, temperature: 0.1f, base64Image: base64Image); - string normalizedCorrected = NormalizeReactResponse(correctedResponse); - if (!string.IsNullOrEmpty(normalizedCorrected) && - JsonToolCallParser.TryParseToolCallsFromText(normalizedCorrected, out toolCalls, out jsonFragment)) - { - parsedSource = normalizedCorrected; - - invalidTools = toolCalls - .Where(c => !IsToolAvailable(c.Name)) - .Select(c => c.Name) - .Where(n => !string.IsNullOrWhiteSpace(n)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - } - } - - if (toolCalls != null && toolCalls.Count > 0) - { - if (invalidTools.Count > 0) - { - if (Prefs.DevMode) - { - WulaLog.Debug("[WulaAI] Invalid tools remain after correction; skipping tool execution."); - } - _history.Add(("toolcall", "{\"tool_calls\": []}")); - _history.Add(("tool", $"[Tool Results]\nToolRunner Error: Invalid tool(s): {string.Join(", ", invalidTools)}.\nToolRunner Guidance: Re-issue valid tool calls only.")); - PersistHistory(); - break; - } - PhaseExecutionResult stepResult = await ExecuteJsonToolsForStep(jsonFragment); - if (!string.IsNullOrEmpty(stepResult.CapturedImage)) - { - base64Image = stepResult.CapturedImage; - } - _lastSuccessfulToolCall = _querySuccessfulToolCall || _actionSuccessfulToolCall; - if (stepResult.ForceStop) - { - AddTraceNote("Tool loop stop: duplicate action detected; switching to reply."); - break; - } - continue; - } - // No tool calls requested: exit tool loop and generate natural-language reply separately. - break; - } - _lastSuccessfulToolCall = _querySuccessfulToolCall || _actionSuccessfulToolCall; - - if (HasTimedOut(maxSeconds)) - { - AddAssistantMessage("Error: AI request timed out."); + AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate()); return; } - string replyInstruction = GetSystemInstruction(false, ""); + if (!IsToolCallJson(queryResponse)) + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Turn 1/3 missing JSON tool calls; treating as no_action."); + } + queryResponse = "{\"tool_calls\": []}"; + } + + PhaseExecutionResult queryResult = await ExecuteJsonToolsForPhase(queryResponse, queryPhase); + + // DATA FLOW: If Query Phase captured an image, propagate it to subsequent phases. + if (!string.IsNullOrEmpty(queryResult.CapturedImage)) + { + base64Image = queryResult.CapturedImage; + } + + if (!queryResult.AnyToolSuccess && !_queryRetryUsed) + { + _queryRetryUsed = true; + string lastUserMessage = _history.LastOrDefault(entry => entry.role == "user").message ?? ""; + string persona = GetActivePersona(); + string retryInstruction = persona + + "\n\n# RETRY DECISION\n" + + "No successful tool calls occurred in PHASE 1 (Query).\n" + + "If you need to use tools in PHASE 1, output exactly: {\"retry_tools\": true}.\n" + + "If you will proceed without actions, output exactly: {\"retry_tools\": false}.\n" + + "Output JSON only and NOTHING else.\n" + + "\nLast user request:\n" + lastUserMessage; + + string retryDecision = await client.GetChatCompletionAsync(retryInstruction, new List<(string role, string message)>(), maxTokens: 256, temperature: 0.1f); + if (!string.IsNullOrEmpty(retryDecision) && ShouldRetryTools(retryDecision)) + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Retry requested; re-opening query phase once."); + } + + SetThinkingPhase(1, true); + string retryQueryInstruction = GetToolSystemInstruction(queryPhase, !string.IsNullOrEmpty(base64Image)) + + "\n\n# RETRY\nYou chose to retry. Output JSON tool calls only (or {\"tool_calls\": []})."; + string retryQueryResponse = await client.GetChatCompletionAsync(retryQueryInstruction, BuildToolContext(queryPhase), maxTokens: 2048, temperature: 0.1f, base64Image: base64Image); + if (string.IsNullOrEmpty(retryQueryResponse)) + { + AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate()); + return; + } + + if (!IsToolCallJson(retryQueryResponse)) + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Retry query phase missing JSON tool calls; treating as no_action."); + } + retryQueryResponse = "{\"tool_calls\": []}"; + } + queryResult = await ExecuteJsonToolsForPhase(retryQueryResponse, queryPhase); + } + } + + var actionPhase = RequestPhase.ActionTools; + if (Prefs.DevMode) + { + WulaLog.Debug($"[WulaAI] ===== Turn 2/3 ({actionPhase}) ====="); + } + + SetThinkingPhase(2, false); + string actionInstruction = GetToolSystemInstruction(actionPhase, !string.IsNullOrEmpty(base64Image)); + var actionContext = BuildToolContext(actionPhase, includeUser: true); + // Important: Pass base64Image to Action Phase as well if available, so visual_click works. + string actionResponse = await client.GetChatCompletionAsync(actionInstruction, actionContext, maxTokens: 2048, temperature: 0.1f, base64Image: base64Image); + if (string.IsNullOrEmpty(actionResponse)) + { + AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate()); + return; + } + + bool actionHasJson = IsToolCallJson(actionResponse); + bool actionIsNoActionOnly = actionHasJson && IsNoActionOnly(actionResponse); + bool actionHasActionTool = actionHasJson && HasActionToolCall(actionResponse); + if (!actionHasJson || (!actionHasActionTool && !actionIsNoActionOnly)) + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Turn 2/3 missing JSON or no action tool; attempting JSON-only conversion."); + } + string fixInstruction = "# FORMAT FIX (ACTION JSON ONLY)\n" + + "Preserve the intent of the previous output.\n" + + "If the previous output indicates no action is needed or refuses action, output exactly: {\"tool_calls\": []}.\n" + + "Do NOT invent new actions.\n" + + "Output VALID JSON tool calls only. No natural language, no commentary.\nIgnore any non-JSON text.\n" + + "Allowed tools: spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop, set_overwatch_mode, remember_fact.\n" + + "Schema: {\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}\n" + + "\nPrevious output:\n" + TrimForPrompt(actionResponse, 600); + string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 2048, temperature: 0.1f); + bool fixedHasJson = !string.IsNullOrEmpty(fixedResponse) && IsToolCallJson(fixedResponse); + bool fixedIsNoActionOnly = fixedHasJson && IsNoActionOnly(fixedResponse); + bool fixedHasActionTool = fixedHasJson && HasActionToolCall(fixedResponse); + if (fixedHasJson && (fixedHasActionTool || fixedIsNoActionOnly)) + { + actionResponse = fixedResponse; + } + else + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Turn 2/3 conversion failed; treating as no_action."); + } + actionResponse = "{\"tool_calls\": []}"; + } + } + PhaseExecutionResult actionResult = await ExecuteJsonToolsForPhase(actionResponse, actionPhase); + if (!actionResult.AnyActionSuccess && !_actionRetryUsed) + { + _actionRetryUsed = true; + string lastUserMessage = _history.LastOrDefault(entry => entry.role == "user").message ?? ""; + string persona = GetActivePersona(); + string retryInstruction = persona + + "\n\n# RETRY DECISION\n" + + "No successful action tools occurred in PHASE 2 (Action).\n" + + "If you need to execute an in-game action, output exactly: {\"retry_tools\": true}.\n" + + "If you will proceed without actions, output exactly: {\"retry_tools\": false}.\n" + + "Output JSON only and NOTHING else.\n" + + "\nLast user request:\n" + lastUserMessage; + + string retryDecision = await client.GetChatCompletionAsync(retryInstruction, new List<(string role, string message)>(), maxTokens: 256, temperature: 0.1f); + if (!string.IsNullOrEmpty(retryDecision) && ShouldRetryTools(retryDecision)) + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Retry requested; re-opening action phase once."); + } + + SetThinkingPhase(2, true); + string retryActionInstruction = GetToolSystemInstruction(actionPhase, !string.IsNullOrEmpty(base64Image)) + + "\n\n# RETRY\nYou chose to retry. Output JSON tool calls only (or {\"tool_calls\": []})."; + var retryActionContext = BuildToolContext(actionPhase, includeUser: true); + string retryActionResponse = await client.GetChatCompletionAsync(retryActionInstruction, retryActionContext, maxTokens: 2048, temperature: 0.1f, base64Image: base64Image); + if (string.IsNullOrEmpty(retryActionResponse)) + { + AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate()); + return; + } + + if (!IsToolCallJson(retryActionResponse)) + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Retry action phase missing JSON; attempting JSON-only conversion."); + } + string retryFixInstruction = "# FORMAT FIX (ACTION JSON ONLY)\n" + + "Preserve the intent of the previous output.\n" + + "If the previous output indicates no action is needed or refuses action, output exactly: {\"tool_calls\": []}.\n" + + "Do NOT invent new actions.\n" + + "Output VALID JSON tool calls only. No natural language, no commentary.\nIgnore any non-JSON text.\n" + + "Allowed tools: spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop, set_overwatch_mode, remember_fact.\n" + + "Schema: {\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}\n" + + "\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600); + string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 2048, temperature: 0.1f); + bool retryFixedHasJson = !string.IsNullOrEmpty(retryFixedResponse) && IsToolCallJson(retryFixedResponse); + bool retryFixedIsNoActionOnly = retryFixedHasJson && IsNoActionOnly(retryFixedResponse); + bool retryFixedHasActionTool = retryFixedHasJson && HasActionToolCall(retryFixedResponse); + if (retryFixedHasJson && (retryFixedHasActionTool || retryFixedIsNoActionOnly)) + { + retryActionResponse = retryFixedResponse; + } + else + { + if (Prefs.DevMode) + { + WulaLog.Debug("[WulaAI] Retry action conversion failed; treating as no_action."); + } + retryActionResponse = "{\"tool_calls\": []}"; + } + } + + actionResult = await ExecuteJsonToolsForPhase(retryActionResponse, actionPhase); + } + } + + _lastSuccessfulToolCall = _querySuccessfulToolCall || _actionSuccessfulToolCall; + + var replyPhase = RequestPhase.Reply; + if (Prefs.DevMode) + { + WulaLog.Debug($"[WulaAI] ===== Turn 3/3 ({replyPhase}) ====="); + } + + SetThinkingPhase(3, false); + string replyInstruction = GetSystemInstruction(false, "") + "\n\n" + GetPhaseInstruction(replyPhase); if (!string.IsNullOrWhiteSpace(_queryToolLedgerNote)) { replyInstruction += "\n" + _queryToolLedgerNote; @@ -2240,7 +2291,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } if (!_lastSuccessfulToolCall) { - replyInstruction += "\nIMPORTANT: No successful tool calls occurred in the tool phase. You MUST NOT claim any tools or actions succeeded."; + replyInstruction += "\nIMPORTANT: No successful tool calls occurred in the tool phases. You MUST NOT claim any tools or actions succeeded."; } if (_lastActionHadError) { @@ -2251,6 +2302,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } } + // VISUAL CONTEXT FOR REPLY: Pass the image so the AI can describe what it sees. string reply = await client.GetChatCompletionAsync(replyInstruction, BuildReplyHistory(), base64Image: base64Image); if (string.IsNullOrEmpty(reply)) { @@ -2258,21 +2310,10 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return; } - string fallbackReply = string.IsNullOrWhiteSpace(toolPhaseReplyCandidate) - ? "" - : StripToolCallJson(toolPhaseReplyCandidate)?.Trim() ?? ""; bool replyHadToolCalls = IsToolCallJson(reply); string strippedReply = StripToolCallJson(reply)?.Trim() ?? ""; if (replyHadToolCalls || string.IsNullOrWhiteSpace(strippedReply)) { - if (!string.IsNullOrWhiteSpace(fallbackReply)) - { - reply = fallbackReply; - replyHadToolCalls = false; - strippedReply = fallbackReply; - } - else - { string retryReplyInstruction = replyInstruction + "\n\n# RETRY (REPLY OUTPUT)\n" + "Your last reply included tool call JSON or was empty. Tool calls are DISABLED.\n" + @@ -2284,24 +2325,16 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori replyHadToolCalls = IsToolCallJson(reply); strippedReply = StripToolCallJson(reply)?.Trim() ?? ""; } - } } if (replyHadToolCalls) { - if (!string.IsNullOrWhiteSpace(fallbackReply)) - { - reply = fallbackReply; - } - else - { string cleaned = StripToolCallJson(reply)?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(cleaned)) { cleaned = "(system) AI reply returned tool call JSON only and was discarded. Please retry or send /clear to reset context."; } reply = cleaned; - } } AddAssistantMessage(reply); @@ -2617,15 +2650,19 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori AddAssistantMessage(finalReply); TriggerMemoryUpdate(); } - private async Task ExecuteJsonToolsForStep(string json) + private async Task ExecuteJsonToolsForPhase(string json, RequestPhase phase) { - string guidance = "ToolRunner Guidance: Tool steps MUST be JSON only. " + - "If tools are needed, output tool_calls; if none, output exactly: {\"tool_calls\": []}. " + - "Do NOT output natural language. Prefer Query -> Action -> Reply; if action results reveal missing info, you may return to Query."; + if (phase == RequestPhase.Reply) + { + await Task.CompletedTask; + return default; + } + + string guidance = "ToolRunner Guidance: Reply to the player in natural language only. Do NOT output any tool call JSON. You may include [EXPR:n] to set expression (n=1-6)."; if (!JsonToolCallParser.TryParseToolCallsFromText(json ?? "", out var toolCalls, out string jsonFragment)) { - UpdateReactToolLedger(new List(), new List()); + UpdatePhaseToolLedger(phase, false, new List()); _history.Add(("toolcall", "{\"tool_calls\": []}")); _history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}")); PersistHistory(); @@ -2636,7 +2673,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori if (toolCalls.Count == 0) { - UpdateReactToolLedger(new List(), new List()); + UpdatePhaseToolLedger(phase, false, new List()); _history.Add(("toolcall", "{\"tool_calls\": []}")); _history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}")); PersistHistory(); @@ -2645,25 +2682,25 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return default; } - int maxTools = ReactMaxToolsPerStep; + int maxTools = MaxToolsPerPhase(phase); int executed = 0; bool executedActionTool = false; bool successfulToolCall = false; - bool repeatedActionAcrossSteps = false; - var successfulQueryTools = new List(); - var successfulActionTools = new List(); - var failedActionTools = new List(); - var seenActionSignaturesThisStep = new HashSet(StringComparer.OrdinalIgnoreCase); - var successfulActionSignaturesThisStep = new HashSet(StringComparer.OrdinalIgnoreCase); + var successfulTools = new List(); + var successfulActions = new List(); + var failedActions = new List(); + var nonActionToolsInActionPhase = new List(); var historyCalls = new List>(); StringBuilder combinedResults = new StringBuilder(); - string capturedImageForStep = null; + string capturedImageForPhase = null; + + bool countActionSuccessOnly = phase == RequestPhase.ActionTools; foreach (var call in toolCalls) { if (executed >= maxTools) { - combinedResults.AppendLine($"ToolRunner Note: Skipped remaining tools because this step allows at most {maxTools} tool call(s)."); + combinedResults.AppendLine($"ToolRunner Note: Skipped remaining tools because this phase allows at most {maxTools} tool call(s)."); break; } @@ -2681,28 +2718,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori continue; } - bool isActionTool = IsActionToolName(toolName); - string actionSignature = null; - if (isActionTool) - { - actionSignature = BuildActionSignature(toolName, call.Arguments); - if (!string.IsNullOrWhiteSpace(actionSignature)) - { - if (_lastActionStepSignatures.Contains(actionSignature)) - { - repeatedActionAcrossSteps = true; - combinedResults.AppendLine($"ToolRunner Guard: Action tool '{toolName}' already executed in the previous action step with the same parameters. Skipping duplicate action call."); - executed++; - continue; - } - if (!seenActionSignaturesThisStep.Add(actionSignature)) - { - combinedResults.AppendLine($"ToolRunner Guard: Duplicate '{toolName}' with the same parameters in the same step was skipped."); - executed++; - continue; - } - } - } var historyCall = new Dictionary { ["type"] = "function", @@ -2720,10 +2735,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori if (toolName.Equals("analyze_screen", StringComparison.OrdinalIgnoreCase) || toolName.Equals("capture_screen", StringComparison.OrdinalIgnoreCase)) { - capturedImageForStep = ScreenCaptureUtility.CaptureScreenAsBase64(); - combinedResults.AppendLine($"Tool '{toolName}' Result: Screen captured successfully. Context updated for the next step."); + capturedImageForPhase = ScreenCaptureUtility.CaptureScreenAsBase64(); + combinedResults.AppendLine($"Tool '{toolName}' Result: Screen captured successfully. Context updated for next phase."); successfulToolCall = true; - successfulQueryTools.Add(toolName); + successfulTools.Add(toolName); + executed++; + continue; + } + + if (phase == RequestPhase.ActionTools && IsQueryToolName(toolName)) + { + combinedResults.AppendLine($"ToolRunner Note: Ignored query tool in action phase: {toolName}."); + nonActionToolsInActionPhase.Add(toolName); executed++; continue; } @@ -2732,7 +2755,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori if (tool == null) { combinedResults.AppendLine($"Error: Tool '{toolName}' not found."); - combinedResults.AppendLine("ToolRunner Guard: Tool execution failed (tool missing). In your final reply, acknowledge the failure and do NOT claim success."); + combinedResults.AppendLine("ToolRunner Guard: The tool call failed. In your reply you MUST acknowledge the failure and MUST NOT claim success."); executed++; continue; } @@ -2740,7 +2763,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori string argsJson = call.ArgumentsJson ?? "{}"; if (Prefs.DevMode) { - WulaLog.Debug($"[WulaAI] Executing tool (ReAct step): {toolName} with args: {argsJson}"); + WulaLog.Debug($"[WulaAI] Executing tool (phase {phase}): {toolName} with args: {argsJson}"); } string result = (await tool.ExecuteAsync(argsJson)).Trim(); @@ -2755,35 +2778,32 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } if (isError) { - combinedResults.AppendLine("ToolRunner Guard: Tool execution returned an error. In your final reply, acknowledge the failure and do NOT claim success."); + combinedResults.AppendLine("ToolRunner Guard: The tool returned an error. In your reply you MUST acknowledge the failure and MUST NOT claim success."); } if (!isError) { - successfulToolCall = true; - if (IsActionToolName(toolName)) + bool countsAsSuccess = !countActionSuccessOnly || IsActionToolName(toolName); + if (countsAsSuccess) { - successfulActionTools.Add(toolName); + successfulToolCall = true; + successfulTools.Add(toolName); } else { - successfulQueryTools.Add(toolName); + nonActionToolsInActionPhase.Add(toolName); } } - - if (isActionTool) + if (IsActionToolName(toolName)) { - executedActionTool = true; if (!isError) { + executedActionTool = true; + successfulActions.Add(toolName); AddActionSuccess(toolName); - if (!string.IsNullOrWhiteSpace(actionSignature)) - { - successfulActionSignaturesThisStep.Add(actionSignature); - } } else { - failedActionTools.Add(toolName); + failedActions.Add(toolName); AddActionFailure(toolName); } } @@ -2791,35 +2811,25 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori executed++; } - _lastActionStepSignatures.Clear(); - if (successfulActionSignaturesThisStep.Count > 0) - { - _lastActionStepSignatures.UnionWith(successfulActionSignaturesThisStep); - } if (!string.IsNullOrWhiteSpace(jsonFragment) && !string.Equals((json ?? "").Trim(), jsonFragment, StringComparison.Ordinal)) { - combinedResults.AppendLine("ToolRunner Note: Non-JSON text in the tool step was ignored."); + combinedResults.AppendLine("ToolRunner Note: Non-JSON text in the tool phase was ignored."); + } + if (phase == RequestPhase.ActionTools && nonActionToolsInActionPhase.Count > 0) + { + combinedResults.AppendLine($"ToolRunner Note: Action phase ignores non-action tools for success: {string.Join(", ", nonActionToolsInActionPhase)}."); } - if (executedActionTool) { - if (failedActionTools.Count == 0) - { - combinedResults.AppendLine("ToolRunner Guard: Action tools executed. You MAY confirm only these actions; do NOT invent additional actions."); - } - else - { - combinedResults.AppendLine("ToolRunner Guard: Some action tools failed. You MUST acknowledge failures and do NOT claim success."); - } + combinedResults.AppendLine("ToolRunner Guard: An in-game action tool WAS executed this turn. You MAY reference it, but do NOT invent additional actions."); } else { - combinedResults.AppendLine("ToolRunner Guard: No action tools executed. You MUST NOT claim any deliveries, reinforcements, bombardments, or other actions."); - } - - if (repeatedActionAcrossSteps) - { - combinedResults.AppendLine("ToolRunner Guard: Duplicate action request detected after the previous action step with the same parameters. Tool phase will end; reply next."); + combinedResults.AppendLine("ToolRunner Guard: NO in-game actions were executed. You MUST NOT claim any deliveries, reinforcements, bombardments, or other actions occurred."); + if (phase == RequestPhase.ActionTools) + { + combinedResults.AppendLine("ToolRunner Guard: Action phase failed (no action tools executed)."); + } } combinedResults.AppendLine(guidance); @@ -2830,17 +2840,16 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori _history.Add(("tool", $"[Tool Results]\n{combinedResults.ToString().Trim()}")); PersistHistory(); - UpdateReactToolLedger(successfulQueryTools, successfulActionTools); + UpdatePhaseToolLedger(phase, successfulToolCall, successfulTools); UpdateActionLedgerNote(); await Task.CompletedTask; return new PhaseExecutionResult { AnyToolSuccess = successfulToolCall, - AnyActionSuccess = successfulActionTools.Count > 0, - AnyActionError = failedActionTools.Count > 0, - CapturedImage = capturedImageForStep, - ForceStop = repeatedActionAcrossSteps + AnyActionSuccess = successfulActions.Count > 0, + AnyActionError = failedActions.Count > 0, + CapturedImage = capturedImageForPhase }; } @@ -2921,6 +2930,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori _actionToolLedgerNote = "Tool Ledger (Action): None (no successful tool calls)."; _querySuccessfulToolCall = false; _actionSuccessfulToolCall = false; + _queryRetryUsed = false; + _actionRetryUsed = false; _actionSuccessLedger.Clear(); _actionSuccessLedgerSet.Clear(); _lastActionStepSignatures.Clear(); @@ -2942,6 +2953,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori + + + + + + +