diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 12cab727..bbd14082 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -113,7 +113,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori - 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. **NO TAGS**: Do NOT use tags, code fences, or any extra text outside JSON."; +7. **STEP BUDGET (OPTIONAL)**: You MAY include { ""meta"": { ""step_budget"": } } to request more tool steps for this turn. +8. **NO TAGS**: Do NOT use tags, code fences, or any extra text outside JSON."; public AIIntelligenceCore(World world) : base(world) { @@ -923,14 +924,27 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return false; } + private static int? ExtractStepBudget(string json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + var match = Regex.Match(json, "\"step_budget\"\\s*:\\s*\"?(\\d+)\"?", RegexOptions.IgnoreCase); + if (!match.Success) return null; + if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)) + { + return value; + } + return null; + } + private static string BuildReactFormatFixInstruction(string previousOutput) { return "# FORMAT FIX (REACT JSON ONLY)\n" + "Output valid JSON with fields thought/tool_calls.\n" + "If tools are needed, tool_calls must be non-empty.\n" + "If no tools are needed, output exactly: {\"tool_calls\": []} (you may include thought).\n" + + "You MAY include an optional meta block: {\"meta\":{\"step_budget\":}}.\n" + "Do NOT output any text outside JSON.\n" + - "Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}\n" + + "Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}],\"meta\":{\"step_budget\":7}}\n" + "\nPrevious output:\n" + TrimForPrompt(previousOutput, 600); } @@ -964,8 +978,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori "You used tool names that are NOT available: " + invalidList + "\n" + "Re-emit JSON with only available tools from # TOOLS (AVAILABLE).\n" + "If no tools are needed, output exactly: {\"tool_calls\": []}.\n" + + "You MAY include an optional meta block: {\"meta\":{\"step_budget\":}}.\n" + "Do NOT output any text outside JSON.\n" + - "Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}"; + "Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}],\"meta\":{\"step_budget\":7}}"; } private static bool ShouldRetryTools(string response) @@ -1832,13 +1847,16 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori int maxSteps = DefaultReactMaxSteps; float maxSeconds = DefaultReactMaxSeconds; + int stepBudgetMax = int.MaxValue; if (settings != null) { - maxSteps = Math.Max(1, Math.Min(10, settings.reactMaxSteps)); - maxSeconds = Mathf.Clamp(settings.reactMaxSeconds, 2f, 60f); + stepBudgetMax = Math.Max(1, settings.reactMaxStepsMax); + maxSteps = Math.Max(1, settings.reactMaxSteps); + maxSeconds = Math.Max(2f, settings.reactMaxSeconds); } _thinkingPhaseTotal = maxSteps; string toolPhaseReplyCandidate = null; + bool budgetApplied = false; for (int step = 1; step <= maxSteps; step++) { if (Time.realtimeSinceStartup - startTime > maxSeconds) @@ -1862,6 +1880,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } string normalizedResponse = NormalizeReactResponse(response); + string parsedSource = normalizedResponse; if (!JsonToolCallParser.TryParseToolCallsFromText(normalizedResponse, out var toolCalls, out string jsonFragment)) { if (LooksLikeNaturalReply(normalizedResponse)) @@ -1890,6 +1909,27 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori } break; } + parsedSource = normalizedFixed; + } + + if (!budgetApplied && !string.IsNullOrWhiteSpace(jsonFragment)) + { + string budgetSource = jsonFragment; + int? requested = ExtractStepBudget(budgetSource); + if (!requested.HasValue) + { + requested = ExtractStepBudget(parsedSource); + } + if (requested.HasValue) + { + budgetApplied = true; + int clamped = Math.Max(1, Math.Min(stepBudgetMax, requested.Value)); + if (clamped > maxSteps) + { + maxSteps = clamped; + _thinkingPhaseTotal = maxSteps; + } + } } var invalidTools = toolCalls @@ -1907,9 +1947,30 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori 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) && + if (!string.IsNullOrEmpty(normalizedCorrected) && JsonToolCallParser.TryParseToolCallsFromText(normalizedCorrected, out toolCalls, out jsonFragment)) { + if (!budgetApplied && !string.IsNullOrWhiteSpace(jsonFragment)) + { + string budgetSource = jsonFragment; + int? requested = ExtractStepBudget(budgetSource); + if (!requested.HasValue) + { + requested = ExtractStepBudget(normalizedCorrected); + } + if (requested.HasValue) + { + budgetApplied = true; + int clamped = Math.Max(1, Math.Min(stepBudgetMax, requested.Value)); + if (clamped > maxSteps) + { + maxSteps = clamped; + _thinkingPhaseTotal = maxSteps; + } + } + } + parsedSource = normalizedCorrected; + invalidTools = toolCalls .Where(c => !IsToolAvailable(c.Name)) .Select(c => c.Name) @@ -2046,7 +2107,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori private async Task ExecuteJsonToolsForStep(string json) { string guidance = "ToolRunner Guidance: Continue with JSON only using {\"thought\":\"...\",\"tool_calls\":[...]}. " + - "If no more tools are needed, output exactly: {\"tool_calls\": []}. Do NOT output any text outside JSON."; + "If no more tools are needed, output exactly: {\"tool_calls\": []}. " + + "You MAY include {\"meta\":{\"step_budget\":}} to request more steps. " + + "Do NOT output any text outside JSON."; if (!JsonToolCallParser.TryParseToolCallsFromText(json ?? "", out var toolCalls, out string jsonFragment)) { diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs index bd7f6157..eb5b9898 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs +++ b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs @@ -15,6 +15,7 @@ namespace WulaFallenEmpire public static bool _showVlmApiKey = false; private string _maxContextTokensBuffer; private string _reactMaxStepsBuffer; + private string _reactMaxStepsMaxBuffer; private string _reactMaxSecondsBuffer; public WulaFallenEmpireMod(ModContentPack content) : base(content) @@ -98,9 +99,13 @@ namespace WulaFallenEmpire listingStandard.GapLine(); listingStandard.Label("ReAct Loop Settings"); - listingStandard.Label("Max Steps (1-10):"); + listingStandard.Label("Default Steps:"); Rect stepsRect = listingStandard.GetRect(Text.LineHeight); - Widgets.TextFieldNumeric(stepsRect, ref settings.reactMaxSteps, ref _reactMaxStepsBuffer, 1, 20); + Widgets.TextFieldNumeric(stepsRect, ref settings.reactMaxSteps, ref _reactMaxStepsBuffer, 1, int.MaxValue); + + listingStandard.Label("Max Steps Limit (step_budget upper bound):"); + Rect stepsMaxRect = listingStandard.GetRect(Text.LineHeight); + Widgets.TextFieldNumeric(stepsMaxRect, ref settings.reactMaxStepsMax, ref _reactMaxStepsMaxBuffer, 1, int.MaxValue); listingStandard.Label("Max Seconds (2-60):"); Rect secondsRect = listingStandard.GetRect(Text.LineHeight); diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs index 5cf29de0..49476f4e 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs +++ b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs @@ -24,6 +24,7 @@ namespace WulaFallenEmpire public bool commentOnNegativeOnly = false; public string extraPersonalityPrompt = ""; public int reactMaxSteps = 4; + public int reactMaxStepsMax = 7; public float reactMaxSeconds = 60f; public bool showReactTraceInUI = false; @@ -48,6 +49,7 @@ namespace WulaFallenEmpire Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false); Scribe_Values.Look(ref extraPersonalityPrompt, "extraPersonalityPrompt", ""); Scribe_Values.Look(ref reactMaxSteps, "reactMaxSteps", 4); + Scribe_Values.Look(ref reactMaxStepsMax, "reactMaxStepsMax", 7); Scribe_Values.Look(ref reactMaxSeconds, "reactMaxSeconds", 60f); Scribe_Values.Look(ref showReactTraceInUI, "showReactTraceInUI", false);