From 63b01db83d9aad8a15276b4236a9a22e4aeef065 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 05 五月 2026 12:30:59 +0800
Subject: [PATCH] # Agent数据分析V3.0.1.7

---
 src/main/java/com/zy/ai/service/impl/AutoTuneAgentServiceImpl.java |  533 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 503 insertions(+), 30 deletions(-)

diff --git a/src/main/java/com/zy/ai/service/impl/AutoTuneAgentServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AutoTuneAgentServiceImpl.java
index b7a20f4..cdfe44f 100644
--- a/src/main/java/com/zy/ai/service/impl/AutoTuneAgentServiceImpl.java
+++ b/src/main/java/com/zy/ai/service/impl/AutoTuneAgentServiceImpl.java
@@ -2,6 +2,9 @@
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.zy.ai.domain.autotune.AutoTuneApplyResult;
+import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot;
+import com.zy.ai.domain.autotune.AutoTuneTriggerType;
 import com.zy.ai.entity.AiPromptTemplate;
 import com.zy.ai.entity.ChatCompletionRequest;
 import com.zy.ai.entity.ChatCompletionResponse;
@@ -9,14 +12,17 @@
 import com.zy.ai.mcp.service.SpringAiMcpToolManager;
 import com.zy.ai.service.AiPromptTemplateService;
 import com.zy.ai.service.AutoTuneAgentService;
+import com.zy.ai.service.AutoTuneControlModeService;
 import com.zy.ai.service.LlmChatService;
+import com.zy.ai.utils.AiPromptUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Slf4j
 @Service
@@ -26,30 +32,45 @@
     private static final int MAX_TOOL_ROUNDS = 10;
     private static final double TEMPERATURE = 0.2D;
     private static final int MAX_TOKENS = 2048;
+    private static final String TOOL_GET_SNAPSHOT = "wcs_local_dispatch_get_auto_tune_snapshot";
+    private static final String TOOL_GET_RECENT_JOBS = "wcs_local_dispatch_get_recent_auto_tune_jobs";
+    private static final String TOOL_APPLY_CHANGES = "wcs_local_dispatch_apply_auto_tune_changes";
+    private static final String TOOL_REVERT_LAST_JOB = "wcs_local_dispatch_revert_last_auto_tune_job";
+    private static final String MCP_STATUS_SUCCESS = "success";
+    private static final String MCP_STATUS_REJECTED = "rejected";
+    private static final String MCP_STATUS_FAILED = "failed";
+    private static final Set<String> ALLOWED_TOOL_NAMES = Set.of(
+            TOOL_GET_SNAPSHOT,
+            TOOL_GET_RECENT_JOBS,
+            TOOL_APPLY_CHANGES,
+            TOOL_REVERT_LAST_JOB
+    );
 
     private final LlmChatService llmChatService;
     private final SpringAiMcpToolManager mcpToolManager;
     private final AiPromptTemplateService aiPromptTemplateService;
+    private final AutoTuneControlModeService autoTuneControlModeService;
 
     @Override
     public AutoTuneAgentResult runAutoTune(String triggerType) {
         String normalizedTriggerType = normalizeTriggerType(triggerType);
+        AutoTuneControlModeSnapshot controlMode = buildControlModeSnapshot();
         UsageCounter usageCounter = new UsageCounter();
-        int toolCallCount = 0;
+        RunState runState = new RunState();
         boolean maxRoundsReached = false;
         StringBuilder summaryBuffer = new StringBuilder();
 
         try {
-            List<Object> tools = mcpToolManager.buildOpenAiTools();
+            List<Object> tools = filterAllowedTools(mcpToolManager.buildOpenAiTools());
             if (tools == null || tools.isEmpty()) {
-                throw new IllegalStateException("No MCP tools registered");
+                throw new IllegalStateException("No auto-tune MCP tools registered");
             }
 
             AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.AUTO_TUNE_DISPATCH.getCode());
-            List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedTriggerType);
+            List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedTriggerType, controlMode);
 
             for (int round = 0; round < MAX_TOOL_ROUNDS; round++) {
-                ChatCompletionResponse response = llmChatService.chatCompletion(messages, TEMPERATURE, MAX_TOKENS, tools);
+                ChatCompletionResponse response = llmChatService.chatCompletionOrThrow(messages, TEMPERATURE, MAX_TOKENS, tools);
                 ChatCompletionRequest.Message assistantMessage = extractAssistantMessage(response);
                 usageCounter.add(response.getUsage());
                 messages.add(assistantMessage);
@@ -57,25 +78,30 @@
 
                 List<ChatCompletionRequest.ToolCall> toolCalls = assistantMessage.getTool_calls();
                 if (toolCalls == null || toolCalls.isEmpty()) {
-                    return buildResult(true, normalizedTriggerType, summaryBuffer, toolCallCount, usageCounter, false);
+                    return buildResult(runState.isSuccessful(), normalizedTriggerType, summaryBuffer, runState,
+                            usageCounter, false, controlMode);
                 }
 
                 for (ChatCompletionRequest.ToolCall toolCall : toolCalls) {
-                    Object toolOutput = callMountedTool(toolCall);
-                    toolCallCount++;
+                    Object toolOutput = callMountedTool(toolCall, runState, normalizedTriggerType);
                     messages.add(buildToolMessage(toolCall, toolOutput));
                 }
             }
             maxRoundsReached = true;
-            return buildResult(true, normalizedTriggerType, summaryBuffer, toolCallCount, usageCounter, maxRoundsReached);
+            return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached,
+                    controlMode);
         } catch (Exception exception) {
             log.error("Auto tune agent stopped with error", exception);
             appendSummary(summaryBuffer, "鑷姩璋冨弬 Agent 鎵ц寮傚父: " + exception.getMessage());
-            return buildResult(false, normalizedTriggerType, summaryBuffer, toolCallCount, usageCounter, maxRoundsReached);
+            runState.markToolError();
+            return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached,
+                    controlMode);
         }
     }
 
-    private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate, String triggerType) {
+    private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate,
+                                                              String triggerType,
+                                                              AutoTuneControlModeSnapshot controlMode) {
         List<ChatCompletionRequest.Message> messages = new ArrayList<>();
 
         ChatCompletionRequest.Message systemMessage = new ChatCompletionRequest.Message();
@@ -85,9 +111,7 @@
 
         ChatCompletionRequest.Message userMessage = new ChatCompletionRequest.Message();
         userMessage.setRole("user");
-        userMessage.setContent("璇锋墽琛屼竴娆″悗鍙� WCS 鑷姩璋冨弬銆倀riggerType=" + triggerType
-                + "銆傚繀椤诲厛璋冪敤 wcs_local_dispatch_get_auto_tune_snapshot 鑾峰彇浜嬪疄锛涘闇�鎻愪氦鍙樻洿锛�"
-                + "蹇呴』鍏� dry-run锛屽啀鏍规嵁 dry-run 缁撴灉鍐冲畾鏄惁瀹為檯搴旂敤銆備笉瑕佽緭鍑鸿嚜鐢辨牸寮� JSON 渚涘灞傝В鏋愩��");
+        userMessage.setContent(AiPromptUtils.buildAutoTuneRuntimeGuard(triggerType, controlMode));
         messages.add(userMessage);
         return messages;
     }
@@ -103,17 +127,291 @@
         return message;
     }
 
-    private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall) {
+    private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall,
+                                   RunState runState,
+                                   String triggerType) {
         String toolName = resolveToolName(toolCall);
-        JSONObject arguments = parseArguments(toolCall);
-        try {
-            return mcpToolManager.callTool(toolName, arguments);
-        } catch (Exception exception) {
-            LinkedHashMap<String, Object> error = new LinkedHashMap<>();
-            error.put("tool", toolName);
-            error.put("error", exception.getMessage());
-            return error;
+        if (!ALLOWED_TOOL_NAMES.contains(toolName)) {
+            throw new IllegalArgumentException("Disallowed auto-tune MCP tool: " + toolName);
         }
+        JSONObject arguments = parseArguments(toolCall);
+        applySchedulerTriggerType(toolName, triggerType, arguments);
+        long startTimeMillis = System.currentTimeMillis();
+        try {
+            Object output = mcpToolManager.callTool(toolName, arguments);
+            runState.markToolSuccess(toolName);
+            recordMutationResult(toolName, arguments, output, runState);
+            if (isRejectedApplyResult(output)) {
+                runState.markApplyRejected(resolveApplyError(output));
+                if (TOOL_APPLY_CHANGES.equals(toolName)) {
+                    Object wrappedOutput = withRejectedApplyInstruction(output);
+                    runState.addMcpCall(buildMcpCall(toolName, arguments, wrappedOutput, startTimeMillis, null));
+                    return wrappedOutput;
+                }
+            }
+            runState.addMcpCall(buildMcpCall(toolName, arguments, output, startTimeMillis, null));
+            return output;
+        } catch (Exception exception) {
+            runState.addMcpCall(buildMcpCall(toolName, arguments, null, startTimeMillis, exception));
+            throw new IllegalStateException("Auto-tune MCP tool failed: " + toolName + ", " + exception.getMessage(),
+                    exception);
+        }
+    }
+
+    private AutoTuneAgentService.McpCallResult buildMcpCall(String toolName,
+                                                            JSONObject arguments,
+                                                            Object output,
+                                                            long startTimeMillis,
+                                                            Exception exception) {
+        AutoTuneAgentService.McpCallResult call = new AutoTuneAgentService.McpCallResult();
+        call.setToolName(toolName);
+        call.setDryRun(resolveDryRun(arguments));
+        call.setDurationMs(Math.max(0L, System.currentTimeMillis() - startTimeMillis));
+        call.setRequestJson(JSON.toJSONString(arguments == null ? new JSONObject() : arguments));
+        if (exception != null) {
+            call.setStatus(MCP_STATUS_FAILED);
+            call.setErrorMessage(exception.getMessage());
+            return call;
+        }
+        call.setStatus(resolveMcpStatus(output));
+        call.setResponseJson(JSON.toJSONString(output));
+        call.setApplyJobId(resolveApplyJobId(output));
+        call.setSuccessCount(resolveSuccessCount(output));
+        call.setRejectCount(resolveRejectCount(output));
+        if (MCP_STATUS_REJECTED.equals(call.getStatus())) {
+            call.setErrorMessage(resolveApplyError(output));
+        }
+        return call;
+    }
+
+    private Boolean resolveDryRun(JSONObject arguments) {
+        if (arguments == null || !arguments.containsKey("dryRun")) {
+            return null;
+        }
+        return Boolean.TRUE.equals(arguments.getBoolean("dryRun"));
+    }
+
+    private String resolveMcpStatus(Object output) {
+        if (isRejectedApplyResult(output)) {
+            return MCP_STATUS_REJECTED;
+        }
+        return MCP_STATUS_SUCCESS;
+    }
+
+    private Long resolveApplyJobId(Object output) {
+        if (output instanceof AutoTuneApplyResult) {
+            return ((AutoTuneApplyResult) output).getJobId();
+        }
+        if (output instanceof Map<?, ?>) {
+            Object jobId = ((Map<?, ?>) output).get("jobId");
+            if (jobId instanceof Number) {
+                return ((Number) jobId).longValue();
+            }
+            if (jobId != null) {
+                try {
+                    return Long.parseLong(String.valueOf(jobId));
+                } catch (NumberFormatException ignore) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    private Integer resolveSuccessCount(Object output) {
+        if (output instanceof AutoTuneApplyResult) {
+            return safeCount(((AutoTuneApplyResult) output).getSuccessCount());
+        }
+        if (output instanceof Map<?, ?>) {
+            return safeCount(((Map<?, ?>) output).get("successCount"));
+        }
+        return null;
+    }
+
+    private Integer resolveRejectCount(Object output) {
+        if (output instanceof AutoTuneApplyResult) {
+            return safeCount(((AutoTuneApplyResult) output).getRejectCount());
+        }
+        if (output instanceof Map<?, ?>) {
+            return safeCount(((Map<?, ?>) output).get("rejectCount"));
+        }
+        return null;
+    }
+
+    private String resolveApplyError(Object output) {
+        if (output instanceof AutoTuneApplyResult) {
+            AutoTuneApplyResult result = (AutoTuneApplyResult) output;
+            return firstRejectReason(result.getChanges(), result.getSummary());
+        }
+        if (output instanceof Map<?, ?>) {
+            Map<?, ?> result = (Map<?, ?>) output;
+            Object changes = result.get("changes");
+            if (changes instanceof List<?>) {
+                for (Object change : (List<?>) changes) {
+                    if (change instanceof Map<?, ?>) {
+                        Object rejectReason = ((Map<?, ?>) change).get("rejectReason");
+                        if (!isBlank(rejectReason == null ? null : String.valueOf(rejectReason))) {
+                            return String.valueOf(rejectReason);
+                        }
+                    }
+                }
+            }
+            Object summary = result.get("summary");
+            return summary == null ? null : String.valueOf(summary);
+        }
+        return null;
+    }
+
+    private String firstRejectReason(List<?> changes, String fallback) {
+        if (changes != null) {
+            for (Object change : changes) {
+                String rejectReason = null;
+                if (change instanceof com.zy.ai.entity.AiAutoTuneChange) {
+                    rejectReason = ((com.zy.ai.entity.AiAutoTuneChange) change).getRejectReason();
+                } else if (change instanceof Map<?, ?>) {
+                    Object value = ((Map<?, ?>) change).get("rejectReason");
+                    rejectReason = value == null ? null : String.valueOf(value);
+                }
+                if (!isBlank(rejectReason)) {
+                    return rejectReason;
+                }
+            }
+        }
+        return fallback;
+    }
+
+    private void recordMutationResult(String toolName, JSONObject arguments, Object output, RunState runState) {
+        if (TOOL_APPLY_CHANGES.equals(toolName)) {
+            boolean dryRun = Boolean.TRUE.equals(arguments.getBoolean("dryRun"));
+            if (!dryRun) {
+                runState.addCounts(output);
+                if (outputHasSuccessfulChange(output)) {
+                    runState.markActualApply();
+                }
+            } else if (isRejectedApplyResult(output)) {
+                runState.addCounts(output);
+            }
+            return;
+        }
+        if (TOOL_REVERT_LAST_JOB.equals(toolName)) {
+            runState.markRollback();
+            runState.addCounts(output);
+        }
+    }
+
+    private boolean outputHasSuccessfulChange(Object output) {
+        if (output instanceof AutoTuneApplyResult) {
+            AutoTuneApplyResult result = (AutoTuneApplyResult) output;
+            if (result.getChanges() != null) {
+                return hasSuccessfulChange(result.getChanges());
+            }
+            return safeCount(result.getSuccessCount()) > 0;
+        }
+        if (output instanceof Map<?, ?>) {
+            Map<?, ?> result = (Map<?, ?>) output;
+            if (!isApplyResultShape(result)) {
+                return false;
+            }
+            if (result.containsKey("changes")) {
+                return hasSuccessfulChange(result.get("changes"));
+            }
+            return safeCount(result.get("successCount")) > 0;
+        }
+        return false;
+    }
+
+    private boolean hasSuccessfulChange(Object changes) {
+        if (!(changes instanceof List<?>)) {
+            return false;
+        }
+        for (Object change : (List<?>) changes) {
+            String resultStatus = null;
+            if (change instanceof com.zy.ai.entity.AiAutoTuneChange) {
+                resultStatus = ((com.zy.ai.entity.AiAutoTuneChange) change).getResultStatus();
+            } else if (change instanceof Map<?, ?>) {
+                Object status = ((Map<?, ?>) change).get("resultStatus");
+                resultStatus = status == null ? null : String.valueOf(status);
+            }
+            if (resultStatus != null && "success".equalsIgnoreCase(resultStatus.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isRejectedApplyResult(Object output) {
+        if (output instanceof AutoTuneApplyResult) {
+            AutoTuneApplyResult result = (AutoTuneApplyResult) output;
+            return !Boolean.TRUE.equals(result.getSuccess()) || safeCount(result.getRejectCount()) > 0;
+        }
+        if (output instanceof Map<?, ?>) {
+            Map<?, ?> result = (Map<?, ?>) output;
+            if (!isApplyResultShape(result)) {
+                return false;
+            }
+            if (!Boolean.TRUE.equals(result.get("success"))) {
+                return true;
+            }
+            return safeCount(result.get("rejectCount")) > 0 || hasRejectedChange(result.get("changes"));
+        }
+        return false;
+    }
+
+    private boolean isApplyResultShape(Map<?, ?> result) {
+        return result.containsKey("success")
+                || result.containsKey("rejectCount")
+                || result.containsKey("changes")
+                || result.containsKey("dryRun")
+                || result.containsKey("dryRunToken");
+    }
+
+    private Object withRejectedApplyInstruction(Object output) {
+        JSONObject wrappedOutput = JSON.parseObject(JSON.toJSONString(output));
+        wrappedOutput.put("agentInstruction",
+                "鏈 dry-run/apply 鏈畬鍏ㄩ�氳繃锛岀姝㈢户缁疄闄呭簲鐢ㄣ�傚繀椤昏鍙� changes[].rejectReason锛�"
+                        + "骞跺洖鍒� snapshot.ruleSnapshot 鎸夋瘡涓洰鏍囧弬鏁扮殑 minValue銆乵axValue/dynamicMaxValue銆乵axStep銆乧ooldownMinutes 鍜� note 閲嶆柊鏍¢獙銆�"
+                        + "鍙湁鎵�鏈� changes 鍧囨弧瓒宠鍒欏苟閫氳繃 dry-run 鍚庯紝鎵嶅厑璁稿疄闄呭簲鐢ㄣ��");
+        return wrappedOutput;
+    }
+
+    private boolean hasRejectedChange(Object changes) {
+        if (!(changes instanceof List<?>)) {
+            return false;
+        }
+        for (Object change : (List<?>) changes) {
+            if (!(change instanceof Map<?, ?>)) {
+                continue;
+            }
+            Object status = ((Map<?, ?>) change).get("resultStatus");
+            if ("rejected".equals(status) || "failed".equals(status)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static int safeCount(Object value) {
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        if (value == null) {
+            return 0;
+        }
+        try {
+            return Integer.parseInt(String.valueOf(value));
+        } catch (NumberFormatException ignore) {
+            return 0;
+        }
+    }
+
+    private void applySchedulerTriggerType(String toolName, String triggerType, JSONObject arguments) {
+        if (!TOOL_APPLY_CHANGES.equals(toolName)) {
+            return;
+        }
+        if (!AutoTuneTriggerType.AUTO.getCode().equals(triggerType)) {
+            return;
+        }
+        arguments.put("triggerType", AutoTuneTriggerType.AUTO.getCode());
     }
 
     private ChatCompletionRequest.Message buildToolMessage(ChatCompletionRequest.ToolCall toolCall, Object toolOutput) {
@@ -150,28 +448,90 @@
     private AutoTuneAgentResult buildResult(boolean success,
                                             String triggerType,
                                             StringBuilder summaryBuffer,
-                                            int toolCallCount,
+                                            RunState runState,
                                             UsageCounter usageCounter,
-                                            boolean maxRoundsReached) {
+                                            boolean maxRoundsReached,
+                                            AutoTuneControlModeSnapshot controlMode) {
         AutoTuneAgentResult result = new AutoTuneAgentResult();
         result.setSuccess(success);
         result.setTriggerType(triggerType);
-        result.setToolCallCount(toolCallCount);
+        result.setAnalysisOnly(controlMode.getAnalysisOnly());
+        result.setAllowApply(controlMode.getAllowApply());
+        result.setExecutionMode(controlMode.getModeCode());
+        result.setToolCallCount(runState.getToolCallCount());
         result.setLlmCallCount(usageCounter.getLlmCallCount());
         result.setPromptTokens(usageCounter.getPromptTokens());
         result.setCompletionTokens(usageCounter.getCompletionTokens());
         result.setTotalTokens(usageCounter.getTotalTokens());
         result.setMaxRoundsReached(maxRoundsReached);
+        result.setActualApplyCalled(runState.isActualApplyCalled());
+        result.setRollbackCalled(runState.isRollbackCalled());
+        result.setSuccessCount(runState.getSuccessCount());
+        result.setRejectCount(runState.getRejectCount());
+        result.setMcpCalls(runState.getMcpCalls());
 
         String summary = summaryBuffer == null ? "" : summaryBuffer.toString().trim();
-        if (toolCallCount <= 0 && success) {
-            summary = "鑷姩璋冨弬 Agent 鏈皟鐢ㄤ换浣� MCP 宸ュ叿锛屾湭鎵ц璋冨弬銆�" + (summary.isEmpty() ? "" : "\n" + summary);
+        if (runState.getToolCallCount() <= 0 && runState.getMcpCallCount() <= 0) {
+            summary = "鑷姩璋冨弬 Agent 鏈皟鐢ㄤ换浣曞厑璁哥殑 MCP 宸ュ叿锛屾湭鎵ц璋冨弬銆�" + (summary.isEmpty() ? "" : "\n" + summary);
+        } else if (!runState.isSnapshotCalled()) {
+            summary = summary + "\n鑷姩璋冨弬 Agent 鏈皟鐢ㄥ揩鐓у伐鍏凤紝缁撴灉涓嶅畬鏁淬��";
+        }
+        if (runState.hasToolError()) {
+            summary = summary + "\n鑷姩璋冨弬 Agent 瀛樺湪宸ュ叿璋冪敤閿欒锛屽凡鏍囪涓哄け璐ャ��";
+        }
+        if (runState.hasApplyRejected()) {
+            summary = summary + "\n鑷姩璋冨弬 Agent 瀛樺湪琚嫆缁濈殑 dry-run/apply 缁撴灉锛屾湭瑙嗕负鎴愬姛璋冨弬銆�";
+            if (!isBlank(runState.getFirstRejectReason())) {
+                summary = summary + "鎷掔粷鍘熷洜: " + runState.getFirstRejectReason();
+            }
+        }
+        if (success && !runState.hasActualMutation()) {
+            summary = "鑷姩璋冨弬 Agent 鏈皟鐢ㄥ疄闄呭簲鐢ㄦ垨鍥炴粴宸ュ叿锛屾湭淇敼杩愯鍙傛暟銆�"
+                    + (summary.isEmpty() ? "" : "\n" + summary);
         }
         if (maxRoundsReached) {
             summary = summary + "\n鑷姩璋冨弬 Agent 杈惧埌鏈�澶у伐鍏疯皟鐢ㄨ疆娆★紝宸插仠姝€��";
         }
+        summary = buildModeSummary(controlMode) + (summary.isEmpty() ? "" : "\n" + summary);
         result.setSummary(summary);
         return result;
+    }
+
+    private String buildModeSummary(AutoTuneControlModeSnapshot controlMode) {
+        return "鎵ц妯″紡: " + controlMode.getModeCode()
+                + "锛宎nalysisOnly=" + controlMode.getAnalysisOnly()
+                + "锛宎llowApply=" + controlMode.getAllowApply()
+                + "锛宮odeLabel=" + controlMode.getModeLabel();
+    }
+
+    private AutoTuneControlModeSnapshot buildControlModeSnapshot() {
+        return autoTuneControlModeService.currentMode();
+    }
+
+    private List<Object> filterAllowedTools(List<Object> tools) {
+        List<Object> allowedTools = new ArrayList<>();
+        if (tools == null || tools.isEmpty()) {
+            return allowedTools;
+        }
+        for (Object tool : tools) {
+            String toolName = resolveOpenAiToolName(tool);
+            if (ALLOWED_TOOL_NAMES.contains(toolName)) {
+                allowedTools.add(tool);
+            }
+        }
+        return allowedTools;
+    }
+
+    private String resolveOpenAiToolName(Object tool) {
+        if (!(tool instanceof Map<?, ?> toolMap)) {
+            return null;
+        }
+        Object function = toolMap.get("function");
+        if (!(function instanceof Map<?, ?> functionMap)) {
+            return null;
+        }
+        Object name = functionMap.get("name");
+        return name == null ? null : String.valueOf(name);
     }
 
     private void appendSummary(StringBuilder summaryBuffer, String content) {
@@ -188,7 +548,7 @@
         return isBlank(triggerType) ? "agent" : triggerType.trim();
     }
 
-    private boolean isBlank(String value) {
+    private static boolean isBlank(String value) {
         return value == null || value.trim().isEmpty();
     }
 
@@ -224,4 +584,117 @@
             return llmCallCount;
         }
     }
+
+    private static class RunState {
+        private int toolCallCount;
+        private boolean snapshotCalled;
+        private boolean toolError;
+        private boolean applyRejected;
+        private String firstRejectReason;
+        private boolean actualApplyCalled;
+        private boolean rollbackCalled;
+        private int successCount;
+        private int rejectCount;
+        private final List<AutoTuneAgentService.McpCallResult> mcpCalls = new ArrayList<>();
+
+        void markToolSuccess(String toolName) {
+            toolCallCount++;
+            if (TOOL_GET_SNAPSHOT.equals(toolName)) {
+                snapshotCalled = true;
+            }
+        }
+
+        void markToolError() {
+            toolError = true;
+        }
+
+        void markApplyRejected(String rejectReason) {
+            applyRejected = true;
+            if (isBlank(firstRejectReason) && !isBlank(rejectReason)) {
+                firstRejectReason = rejectReason;
+            }
+        }
+
+        void markActualApply() {
+            actualApplyCalled = true;
+        }
+
+        void markRollback() {
+            rollbackCalled = true;
+        }
+
+        void addCounts(Object output) {
+            if (output instanceof AutoTuneApplyResult) {
+                AutoTuneApplyResult result = (AutoTuneApplyResult) output;
+                successCount += safeCount(result.getSuccessCount());
+                rejectCount += safeCount(result.getRejectCount());
+                return;
+            }
+            if (output instanceof Map<?, ?>) {
+                Map<?, ?> result = (Map<?, ?>) output;
+                successCount += safeCount(result.get("successCount"));
+                rejectCount += safeCount(result.get("rejectCount"));
+            }
+        }
+
+        void addMcpCall(AutoTuneAgentService.McpCallResult mcpCall) {
+            if (mcpCall == null) {
+                return;
+            }
+            mcpCall.setCallSeq(mcpCalls.size() + 1);
+            mcpCalls.add(mcpCall);
+        }
+
+        boolean isSuccessful() {
+            return toolCallCount > 0 && snapshotCalled && !toolError && !applyRejected;
+        }
+
+        int getToolCallCount() {
+            return toolCallCount;
+        }
+
+        int getMcpCallCount() {
+            return mcpCalls.size();
+        }
+
+        boolean isSnapshotCalled() {
+            return snapshotCalled;
+        }
+
+        boolean hasToolError() {
+            return toolError;
+        }
+
+        boolean hasApplyRejected() {
+            return applyRejected;
+        }
+
+        String getFirstRejectReason() {
+            return firstRejectReason;
+        }
+
+        boolean isActualApplyCalled() {
+            return actualApplyCalled;
+        }
+
+        boolean isRollbackCalled() {
+            return rollbackCalled;
+        }
+
+        boolean hasActualMutation() {
+            return actualApplyCalled || rollbackCalled;
+        }
+
+        int getSuccessCount() {
+            return successCount;
+        }
+
+        int getRejectCount() {
+            return rejectCount;
+        }
+
+        List<AutoTuneAgentService.McpCallResult> getMcpCalls() {
+            return new ArrayList<>(mcpCalls);
+        }
+    }
 }

--
Gitblit v1.9.1