diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java index 97716f09..bbd205ce 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java @@ -256,9 +256,23 @@ public class NodeStreamingChatHelper { } } + /** + * Broadcast a lightweight progress event so the frontend shows activity + * during silent LLM calls (e.g. triage). Sent as a "progress" SSE event. + */ + public void broadcastProgress(String conversationId, String message) { + if (streamTracker == null || conversationId == null || conversationId.isEmpty()) { + return; + } + streamTracker.broadcastObject(conversationId, "progress", + Map.of("message", message != null ? message : "")); + } + // ==================== 重试配置 ==================== private static final int MAX_RETRIES = 5; + // For rate-limit and server-error: fail fast to failover chain. + private static final int MAX_RETRIES_RATE_LIMIT = 2; private static final long BACKOFF_BASE_MS = 3000; private static final long BACKOFF_CAP_MS = 60_000; @@ -540,11 +554,22 @@ public class NodeStreamingChatHelper { broadcastDelta(conversationId, "warning", buildDeltaJson("⏱️ 请求频率受限,等待 " + (delay / 1000) + " 秒后重试(第 " + attempt + "/" + MAX_RETRIES + " 次)...")); } - try { - Thread.sleep(delay); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - return buildErrorResult("LLM 调用被中断", conversationId, phase); + // Poll stop flag every 100ms so user Stop is honored mid-backoff. + long remaining = delay; + while (remaining > 0) { + if (streamTracker != null && streamTracker.isStopRequested(conversationId)) { + log.info("[{}] Stop requested during backoff — aborting retry: conversationId={}", + phase, conversationId); + throw new CancellationException("Stream stopped by user"); + } + long slice = Math.min(100, remaining); + try { + Thread.sleep(slice); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return buildErrorResult("LLM 调用被中断", conversationId, phase); + } + remaining -= slice; } } @@ -731,10 +756,12 @@ public class NodeStreamingChatHelper { conversationId, phase, errorType); } - // Rate limit / Server error: 重试 - if (attempt < MAX_RETRIES && (errorType == ErrorType.RATE_LIMIT || errorType == ErrorType.SERVER_ERROR)) { + // Rate limit / Server error: retry with reduced limit for rate-limit/server-error + boolean isRateLimitOrServerError = errorType == ErrorType.RATE_LIMIT || errorType == ErrorType.SERVER_ERROR; + int effectiveMaxRetries = isRateLimitOrServerError ? MAX_RETRIES_RATE_LIMIT : MAX_RETRIES; + if (attempt < effectiveMaxRetries && isRateLimitOrServerError) { log.warn("[{}] Retryable error (attempt {}/{}, type={}): {}", - phase, attempt, MAX_RETRIES, errorType, error.getMessage()); + phase, attempt, effectiveMaxRetries, errorType, error.getMessage()); return null; // 返回 null 触发重试 } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java index 64520d3a..c00c08b7 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolExecutionExecutor.java @@ -38,13 +38,10 @@ import java.util.concurrent.*; public class ToolExecutionExecutor { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final ExecutorService TOOL_EXECUTOR = Executors.newFixedThreadPool( - Math.max(4, Runtime.getRuntime().availableProcessors()), - r -> { - Thread t = new Thread(r, "tool-executor"); - t.setDaemon(true); - return t; - }); + // JDK 21 virtual threads: no blocking stall for I/O-bound tools. + // Each tool invocation gets its own lightweight carrier thread. + private static final ExecutorService TOOL_EXECUTOR = + Executors.newVirtualThreadPerTaskExecutor(); /** * Legacy hardcoded unsafe set, kept as a fallback when no diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolResultProperties.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolResultProperties.java index 8d7304e5..638e05a7 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolResultProperties.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/executor/ToolResultProperties.java @@ -26,8 +26,8 @@ import java.util.Set; * agent: * tool-result: * enabled: true - * per-result-threshold-chars: 4000 - * per-turn-budget-chars: 16000 + * per-result-threshold-chars: 16000 + * per-turn-budget-chars: 32000 * preview-head-chars: 800 * storage-base-dir: * @@ -39,10 +39,10 @@ public class ToolResultProperties { private boolean enabled = true; /** A single tool result larger than this is spilled to disk. */ - private int perResultThresholdChars = 4000; + private int perResultThresholdChars = 16000; // was 4000 — prevents WebSearch spill-to-disk /** Aggregate cap on combined response size in one tool turn. */ - private int perTurnBudgetChars = 16000; + private int perTurnBudgetChars = 32000; // was 16000 — headroom for multi-tool turns /** Number of leading characters kept inline as a preview after spilling. */ private int previewHeadChars = 800; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java index 57b71bdd..ab7dbe8b 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java @@ -165,6 +165,12 @@ public class PlanGenerationNode implements NodeAction { Prompt prompt = new Prompt(promptMessages); + // Broadcast a lightweight progress token so the frontend shows activity + // during the silent triage call (typically 1-3 s). + if (streamingHelper != null) { + streamingHelper.broadcastProgress(conversationId, "分析中..."); + } + // Silent streaming call — structured JSON is parsed below; tokens are not forwarded to the client. NodeStreamingChatHelper.StreamResult result = streamingHelper.streamCallSilent( chatModel, prompt, conversationId, "plan_generation"); diff --git a/mateclaw-server/src/main/resources/application.yml b/mateclaw-server/src/main/resources/application.yml index 6e95f9e1..bb5e2048 100644 --- a/mateclaw-server/src/main/resources/application.yml +++ b/mateclaw-server/src/main/resources/application.yml @@ -167,8 +167,8 @@ mate: # the in-context preview points the agent at the spill file (read_file tool). tool-result: enabled: true - per-result-threshold-chars: 4000 - per-turn-budget-chars: 16000 + per-result-threshold-chars: 16000 # was 4000 — prevents WebSearch spill-to-disk + per-turn-budget-chars: 32000 # was 16000 — headroom for multi-tool turns preview-head-chars: 800 storage-base-dir: "" # Retrieval-style tools that must NEVER be spilled. Spilling read_file's