feat: 多层上下文压缩 + 多级权限管理系统
权限管理: - PermissionTypes: 权限行为/模式/规则/决策/选择类型系统 - PermissionSettings: 用户级/项目级/会话级权限持久化(settings.json) - PermissionRuleEngine: 多层规则引擎(deny→allow→readOnly→mode→dangerous→ASK) - DangerousPatterns: 危险shell命令检测(rm -rf, eval, exec等) - ReplSession: Y/A/N/D四选项权限确认UI - ConfigCommand: permission-mode/list/reset子命令 - AgentLoop: 集成规则引擎,PermissionChoice回调 上下文压缩: - TokenTracker: 上下文窗口监控(93%自动压缩/82%警告/98%阻塞) - MicroCompact: 微压缩,裁剪旧tool_result(无API调用) - SessionMemoryCompact: Session Memory压缩,AI摘要旧消息保留近期段 - FullCompact: 全量压缩+PTL重试(按API Round逐步丢弃) - AutoCompactManager: 压缩编排器(micro→session→full),含熔断机制 - CompactCommand: 改造为委托FullCompact - StatusLine: token使用百分比+颜色告警(绿→黄→红→闪烁) - AppConfig: 注册PermissionSettings/RuleEngine/AutoCompactManager Bean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
package com.claudecode.core;
|
||||
|
||||
import com.claudecode.core.compact.AutoCompactManager;
|
||||
import com.claudecode.permission.PermissionRuleEngine;
|
||||
import com.claudecode.permission.PermissionTypes.PermissionChoice;
|
||||
import com.claudecode.permission.PermissionTypes.PermissionDecision;
|
||||
import com.claudecode.tool.ToolCallbackAdapter;
|
||||
import com.claudecode.tool.ToolContext;
|
||||
import com.claudecode.tool.ToolRegistry;
|
||||
@@ -52,6 +56,12 @@ public class AgentLoop {
|
||||
private final TokenTracker tokenTracker;
|
||||
private final HookManager hookManager;
|
||||
|
||||
/** 权限规则引擎(可选,为 null 时使用传统回调方式) */
|
||||
private PermissionRuleEngine permissionEngine;
|
||||
|
||||
/** 自动压缩管理器(可选) */
|
||||
private AutoCompactManager autoCompactManager;
|
||||
|
||||
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
|
||||
private final List<Message> messageHistory = new ArrayList<>();
|
||||
|
||||
@@ -64,8 +74,8 @@ public class AgentLoop {
|
||||
/** 流式输出开始回调:通知 UI 停止 spinner */
|
||||
private Runnable onStreamStart;
|
||||
|
||||
/** 权限确认回调:危险操作前请求用户确认(返回 true 表示允许) */
|
||||
private Function<PermissionRequest, Boolean> onPermissionRequest;
|
||||
/** 权限确认回调:危险操作前请求用户确认(返回 PermissionChoice) */
|
||||
private Function<PermissionRequest, PermissionChoice> onPermissionRequest;
|
||||
|
||||
/** Thinking 内容回调:显示 AI 的思考过程 */
|
||||
private Consumer<String> onThinkingContent;
|
||||
@@ -98,10 +108,22 @@ public class AgentLoop {
|
||||
this.onStreamStart = onStreamStart;
|
||||
}
|
||||
|
||||
public void setOnPermissionRequest(Function<PermissionRequest, Boolean> onPermissionRequest) {
|
||||
public void setOnPermissionRequest(Function<PermissionRequest, PermissionChoice> onPermissionRequest) {
|
||||
this.onPermissionRequest = onPermissionRequest;
|
||||
}
|
||||
|
||||
public void setPermissionEngine(PermissionRuleEngine engine) {
|
||||
this.permissionEngine = engine;
|
||||
}
|
||||
|
||||
public void setAutoCompactManager(AutoCompactManager manager) {
|
||||
this.autoCompactManager = manager;
|
||||
}
|
||||
|
||||
public AutoCompactManager getAutoCompactManager() {
|
||||
return autoCompactManager;
|
||||
}
|
||||
|
||||
public void setOnThinkingContent(Consumer<String> onThinkingContent) {
|
||||
this.onThinkingContent = onThinkingContent;
|
||||
}
|
||||
@@ -183,6 +205,14 @@ public class AgentLoop {
|
||||
|
||||
// 执行工具调用
|
||||
executeToolCalls(result.assistant.getToolCalls(), callbacks);
|
||||
|
||||
// 自动压缩检查(在工具调用后,下次 API 调用前)
|
||||
if (autoCompactManager != null) {
|
||||
autoCompactManager.autoCompactIfNeeded(
|
||||
() -> messageHistory,
|
||||
this::replaceHistory
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (iteration >= MAX_ITERATIONS) {
|
||||
@@ -302,12 +332,34 @@ public class AgentLoop {
|
||||
String result;
|
||||
ToolCallbackAdapter adapter = findCallbackByName(callbacks, toolName);
|
||||
if (adapter != null) {
|
||||
// 权限确认:非只读工具需要用户确认
|
||||
// 权限检查:优先使用规则引擎,回退到传统回调
|
||||
boolean permitted = true;
|
||||
if (!adapter.getTool().isReadOnly() && onPermissionRequest != null) {
|
||||
if (permissionEngine != null) {
|
||||
PermissionDecision decision = permissionEngine.evaluate(
|
||||
toolName, parsedArgs, adapter.getTool().isReadOnly());
|
||||
if (decision.isAllowed()) {
|
||||
permitted = true;
|
||||
} else if (decision.isDenied()) {
|
||||
permitted = false;
|
||||
log.info("[{}] Denied by rule: {}", toolName, decision.reason());
|
||||
} else if (decision.needsAsk() && onPermissionRequest != null) {
|
||||
String activity = adapter.getTool().activityDescription(parsedArgs);
|
||||
PermissionRequest req = new PermissionRequest(toolName, toolArgs, activity);
|
||||
req.setDecision(decision);
|
||||
PermissionChoice choice = onPermissionRequest.apply(req);
|
||||
permitted = (choice == PermissionChoice.ALLOW_ONCE || choice == PermissionChoice.ALWAYS_ALLOW);
|
||||
// 持久化用户选择
|
||||
String command = parsedArgs != null ? (String) parsedArgs.get("command") : null;
|
||||
permissionEngine.applyChoice(choice, toolName, command);
|
||||
} else {
|
||||
permitted = false;
|
||||
}
|
||||
} else if (!adapter.getTool().isReadOnly() && onPermissionRequest != null) {
|
||||
// 传统回调模式(向后兼容)
|
||||
String activity = adapter.getTool().activityDescription(parsedArgs);
|
||||
PermissionRequest req = new PermissionRequest(toolName, toolArgs, activity);
|
||||
permitted = onPermissionRequest.apply(req);
|
||||
PermissionChoice choice = onPermissionRequest.apply(req);
|
||||
permitted = (choice == PermissionChoice.ALLOW_ONCE || choice == PermissionChoice.ALWAYS_ALLOW);
|
||||
}
|
||||
|
||||
if (permitted) {
|
||||
@@ -438,7 +490,24 @@ public class AgentLoop {
|
||||
}
|
||||
|
||||
/** 权限确认请求 */
|
||||
public record PermissionRequest(String toolName, String arguments, String activityDescription) {}
|
||||
public static class PermissionRequest {
|
||||
private final String toolName;
|
||||
private final String arguments;
|
||||
private final String activityDescription;
|
||||
private PermissionDecision decision;
|
||||
|
||||
public PermissionRequest(String toolName, String arguments, String activityDescription) {
|
||||
this.toolName = toolName;
|
||||
this.arguments = arguments;
|
||||
this.activityDescription = activityDescription;
|
||||
}
|
||||
|
||||
public String toolName() { return toolName; }
|
||||
public String arguments() { return arguments; }
|
||||
public String activityDescription() { return activityDescription; }
|
||||
public PermissionDecision decision() { return decision; }
|
||||
public void setDecision(PermissionDecision decision) { this.decision = decision; }
|
||||
}
|
||||
|
||||
/** 工具事件,用于 UI 展示 */
|
||||
public record ToolEvent(String toolName, Phase phase, String arguments, String result) {
|
||||
|
||||
@@ -3,29 +3,72 @@ package com.claudecode.core;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Token 使用量追踪器 —— 记录 API 调用的 token 消耗。
|
||||
* Token 使用量追踪器 —— 记录 API 调用的 token 消耗并监控上下文窗口。
|
||||
* <p>
|
||||
* 从 ChatResponse 的 usage 元数据中提取 token 统计信息,
|
||||
* 支持按会话累计和费用估算。
|
||||
* 支持按会话累计、费用估算和上下文窗口阈值监控。
|
||||
*/
|
||||
public class TokenTracker {
|
||||
|
||||
// ── 上下文窗口阈值常量 ──
|
||||
/** 自动压缩触发百分比(有效窗口的 93%) */
|
||||
public static final double AUTO_COMPACT_THRESHOLD_PCT = 0.93;
|
||||
/** 警告阈值百分比(82%) */
|
||||
public static final double WARNING_THRESHOLD_PCT = 0.82;
|
||||
/** 阻塞阈值百分比(98%,必须压缩才能继续) */
|
||||
public static final double BLOCKING_THRESHOLD_PCT = 0.98;
|
||||
/** 自动压缩缓冲 token 数 */
|
||||
public static final long AUTO_COMPACT_BUFFER_TOKENS = 13_000;
|
||||
/** 手动压缩缓冲 token 数 */
|
||||
public static final long MANUAL_COMPACT_BUFFER_TOKENS = 3_000;
|
||||
|
||||
/** 上下文窗口警告状态 */
|
||||
public enum TokenWarningState {
|
||||
NORMAL, // 正常(绿色)
|
||||
WARNING, // 接近阈值(黄色)
|
||||
ERROR, // 达到压缩阈值(红色)
|
||||
BLOCKING // 必须压缩才能继续(闪烁红)
|
||||
}
|
||||
|
||||
private final AtomicLong totalInputTokens = new AtomicLong(0);
|
||||
private final AtomicLong totalOutputTokens = new AtomicLong(0);
|
||||
private final AtomicLong totalCacheReadTokens = new AtomicLong(0);
|
||||
private final AtomicLong totalCacheCreationTokens = new AtomicLong(0);
|
||||
private final AtomicLong apiCallCount = new AtomicLong(0);
|
||||
|
||||
/** 最近一次 API 调用报告的 prompt token 数(近似当前上下文大小) */
|
||||
private final AtomicLong lastPromptTokens = new AtomicLong(0);
|
||||
|
||||
/** 模型定价(每百万 token 的美元价格) */
|
||||
private double inputPricePerMillion = 3.0; // Claude Sonnet 4 input
|
||||
private double outputPricePerMillion = 15.0; // Claude Sonnet 4 output
|
||||
private double cacheReadPricePerMillion = 0.3; // 缓存读取
|
||||
private String modelName = "claude-sonnet-4-20250514";
|
||||
|
||||
/** 上下文窗口总大小(token) */
|
||||
private long contextWindowSize;
|
||||
/** 预留给输出的 token 数 */
|
||||
private long reservedTokens = 20_000;
|
||||
|
||||
public TokenTracker() {
|
||||
// 支持环境变量覆盖上下文窗口大小
|
||||
String envWindow = System.getenv("CLAUDE_CODE_CONTEXT_WINDOW");
|
||||
if (envWindow != null && !envWindow.isBlank()) {
|
||||
try {
|
||||
this.contextWindowSize = Long.parseLong(envWindow.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
this.contextWindowSize = 200_000; // 默认 200K
|
||||
}
|
||||
} else {
|
||||
this.contextWindowSize = 200_000;
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录一次 API 调用的 token 使用 */
|
||||
public void recordUsage(long inputTokens, long outputTokens) {
|
||||
totalInputTokens.addAndGet(inputTokens);
|
||||
totalOutputTokens.addAndGet(outputTokens);
|
||||
lastPromptTokens.set(inputTokens);
|
||||
apiCallCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@@ -35,6 +78,7 @@ public class TokenTracker {
|
||||
totalOutputTokens.addAndGet(outputTokens);
|
||||
totalCacheReadTokens.addAndGet(cacheRead);
|
||||
totalCacheCreationTokens.addAndGet(cacheCreation);
|
||||
lastPromptTokens.set(inputTokens);
|
||||
apiCallCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@@ -73,12 +117,64 @@ public class TokenTracker {
|
||||
return inputCost + outputCost + cacheCost;
|
||||
}
|
||||
|
||||
// ── 上下文窗口监控 ──
|
||||
|
||||
/** 有效上下文窗口大小(总窗口 - 预留输出) */
|
||||
public long getEffectiveWindow() {
|
||||
return contextWindowSize - reservedTokens;
|
||||
}
|
||||
|
||||
/** 最近一次 prompt 的 token 数(近似当前上下文大小) */
|
||||
public long getLastPromptTokens() {
|
||||
return lastPromptTokens.get();
|
||||
}
|
||||
|
||||
/** 当前上下文使用百分比 */
|
||||
public double getUsagePercentage() {
|
||||
long effective = getEffectiveWindow();
|
||||
if (effective <= 0) return 0;
|
||||
return (double) lastPromptTokens.get() / effective;
|
||||
}
|
||||
|
||||
/** 是否应触发自动压缩 */
|
||||
public boolean shouldAutoCompact() {
|
||||
return getUsagePercentage() >= AUTO_COMPACT_THRESHOLD_PCT;
|
||||
}
|
||||
|
||||
/** 是否已达到阻塞阈值(必须压缩才能继续) */
|
||||
public boolean isBlocking() {
|
||||
return getUsagePercentage() >= BLOCKING_THRESHOLD_PCT;
|
||||
}
|
||||
|
||||
/** 获取自动压缩触发的 token 阈值 */
|
||||
public long getAutoCompactThreshold() {
|
||||
return (long) (getEffectiveWindow() * AUTO_COMPACT_THRESHOLD_PCT);
|
||||
}
|
||||
|
||||
/** 获取当前 token 警告状态 */
|
||||
public TokenWarningState getTokenWarningState() {
|
||||
double pct = getUsagePercentage();
|
||||
if (pct >= BLOCKING_THRESHOLD_PCT) return TokenWarningState.BLOCKING;
|
||||
if (pct >= AUTO_COMPACT_THRESHOLD_PCT) return TokenWarningState.ERROR;
|
||||
if (pct >= WARNING_THRESHOLD_PCT) return TokenWarningState.WARNING;
|
||||
return TokenWarningState.NORMAL;
|
||||
}
|
||||
|
||||
public long getContextWindowSize() { return contextWindowSize; }
|
||||
|
||||
public void setContextWindowSize(long size) { this.contextWindowSize = size; }
|
||||
|
||||
public long getReservedTokens() { return reservedTokens; }
|
||||
|
||||
public void setReservedTokens(long reserved) { this.reservedTokens = reserved; }
|
||||
|
||||
/** 重置统计 */
|
||||
public void reset() {
|
||||
totalInputTokens.set(0);
|
||||
totalOutputTokens.set(0);
|
||||
totalCacheReadTokens.set(0);
|
||||
totalCacheCreationTokens.set(0);
|
||||
lastPromptTokens.set(0);
|
||||
apiCallCount.set(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.claudecode.core.compact;
|
||||
|
||||
import com.claudecode.core.TokenTracker;
|
||||
import com.claudecode.core.compact.CompactionResult.CompactLayer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 自动压缩编排器 —— 根据 token 使用量自动选择并执行压缩策略。
|
||||
* <p>
|
||||
* 对应 claude-code 的自动压缩编排逻辑。在 AgentLoop 中每次 API 响应后调用。
|
||||
* 流程:检查阈值 → 微压缩 → Session Memory 压缩 → 全量压缩(兜底)
|
||||
* 熔断器:连续失败 {@value MAX_CONSECUTIVE_FAILURES} 次后暂停自动压缩。
|
||||
*/
|
||||
public class AutoCompactManager {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AutoCompactManager.class);
|
||||
|
||||
/** 连续失败阈值,超过后暂停自动压缩 */
|
||||
private static final int MAX_CONSECUTIVE_FAILURES = 3;
|
||||
|
||||
private final MicroCompact microCompact;
|
||||
private final SessionMemoryCompact sessionMemoryCompact;
|
||||
private final FullCompact fullCompact;
|
||||
private final TokenTracker tokenTracker;
|
||||
|
||||
/** 连续压缩失败次数 */
|
||||
private int consecutiveFailures = 0;
|
||||
|
||||
/** 是否已触发过熔断 */
|
||||
private boolean circuitBroken = false;
|
||||
|
||||
/** 压缩事件回调(用于通知 UI) */
|
||||
private Consumer<CompactionResult> onCompactionEvent;
|
||||
|
||||
public AutoCompactManager(ChatModel chatModel, TokenTracker tokenTracker) {
|
||||
this.tokenTracker = tokenTracker;
|
||||
this.microCompact = new MicroCompact();
|
||||
this.sessionMemoryCompact = new SessionMemoryCompact(chatModel);
|
||||
this.fullCompact = new FullCompact(chatModel);
|
||||
}
|
||||
|
||||
public void setOnCompactionEvent(Consumer<CompactionResult> onCompactionEvent) {
|
||||
this.onCompactionEvent = onCompactionEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在每次 API 响应后调用,根据 token 使用状态自动执行压缩。
|
||||
*
|
||||
* @param historySupplier 获取当前消息历史的函数
|
||||
* @param historyReplacer 替换消息历史的函数
|
||||
* @return 如果执行了压缩返回结果,否则返回 null
|
||||
*/
|
||||
public CompactionResult autoCompactIfNeeded(
|
||||
Supplier<List<Message>> historySupplier,
|
||||
Consumer<List<Message>> historyReplacer) {
|
||||
|
||||
// 熔断器检查
|
||||
if (circuitBroken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否需要压缩
|
||||
if (!tokenTracker.shouldAutoCompact()) {
|
||||
// 即使不需要自动压缩,也执行微压缩(成本极低)
|
||||
List<Message> history = historySupplier.get();
|
||||
if (history instanceof java.util.ArrayList<Message> mutableHistory) {
|
||||
microCompact.compact(mutableHistory);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("Auto-compact triggered at {}% token usage",
|
||||
String.format("%.1f", tokenTracker.getUsagePercentage() * 100));
|
||||
|
||||
List<Message> history = historySupplier.get();
|
||||
|
||||
// 阶段 1:微压缩
|
||||
if (history instanceof java.util.ArrayList<Message> mutableHistory) {
|
||||
CompactionResult microResult = microCompact.compact(mutableHistory);
|
||||
if (microResult.success()) {
|
||||
notifyEvent(microResult);
|
||||
// 微压缩后重新检查是否仍需深度压缩
|
||||
if (!tokenTracker.shouldAutoCompact()) {
|
||||
consecutiveFailures = 0;
|
||||
return microResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 阶段 2:Session Memory 压缩
|
||||
try {
|
||||
List<Message> compacted = sessionMemoryCompact.getCompactedHistory(history);
|
||||
if (compacted != null) {
|
||||
historyReplacer.accept(compacted);
|
||||
CompactionResult result = CompactionResult.success(
|
||||
CompactLayer.SESSION_MEMORY,
|
||||
history.size(), compacted.size(),
|
||||
"Auto session memory compact");
|
||||
consecutiveFailures = 0;
|
||||
notifyEvent(result);
|
||||
log.info("Session memory compact: {} → {} messages", history.size(), compacted.size());
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Session memory compact failed: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 阶段 3:全量压缩(兜底)
|
||||
try {
|
||||
List<Message> compacted = fullCompact.compact(history);
|
||||
if (compacted != null) {
|
||||
historyReplacer.accept(compacted);
|
||||
CompactionResult result = CompactionResult.success(
|
||||
CompactLayer.FULL,
|
||||
history.size(), compacted.size(),
|
||||
"Auto full compact (fallback)");
|
||||
consecutiveFailures = 0;
|
||||
notifyEvent(result);
|
||||
log.info("Full compact fallback: {} → {} messages", history.size(), compacted.size());
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Full compact failed: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 所有压缩方式均失败
|
||||
consecutiveFailures++;
|
||||
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
||||
circuitBroken = true;
|
||||
log.error("Auto-compact circuit breaker triggered after {} consecutive failures",
|
||||
consecutiveFailures);
|
||||
CompactionResult result = CompactionResult.failure(CompactLayer.FULL,
|
||||
"Circuit breaker: auto-compact disabled after " + consecutiveFailures + " failures");
|
||||
notifyEvent(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return CompactionResult.failure(CompactLayer.SESSION_MEMORY,
|
||||
"All compression strategies failed");
|
||||
}
|
||||
|
||||
/** 手动重置熔断器 */
|
||||
public void resetCircuitBreaker() {
|
||||
circuitBroken = false;
|
||||
consecutiveFailures = 0;
|
||||
log.info("Auto-compact circuit breaker reset");
|
||||
}
|
||||
|
||||
public boolean isCircuitBroken() {
|
||||
return circuitBroken;
|
||||
}
|
||||
|
||||
public int getConsecutiveFailures() {
|
||||
return consecutiveFailures;
|
||||
}
|
||||
|
||||
/** 获取 FullCompact 实例(供 CompactCommand 委托使用) */
|
||||
public FullCompact getFullCompact() {
|
||||
return fullCompact;
|
||||
}
|
||||
|
||||
private void notifyEvent(CompactionResult result) {
|
||||
if (onCompactionEvent != null) {
|
||||
try {
|
||||
onCompactionEvent.accept(result);
|
||||
} catch (Exception e) {
|
||||
log.debug("Compaction event notification failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.claudecode.core.compact;
|
||||
|
||||
/**
|
||||
* 压缩操作的结果数据。
|
||||
*
|
||||
* @param success 是否成功
|
||||
* @param layer 执行的压缩层级
|
||||
* @param messagesBefore 压缩前消息数
|
||||
* @param messagesAfter 压缩后消息数
|
||||
* @param summary AI 生成的摘要(可能为 null)
|
||||
* @param reason 结果原因/描述
|
||||
*/
|
||||
public record CompactionResult(
|
||||
boolean success,
|
||||
CompactLayer layer,
|
||||
int messagesBefore,
|
||||
int messagesAfter,
|
||||
String summary,
|
||||
String reason
|
||||
) {
|
||||
|
||||
/** 压缩层级 */
|
||||
public enum CompactLayer {
|
||||
/** 微压缩:裁剪旧 tool_result 内容 */
|
||||
MICRO,
|
||||
/** Session Memory:AI 摘要旧消息,保留近期段 */
|
||||
SESSION_MEMORY,
|
||||
/** 全量压缩:AI 摘要全部,PTL 重试 */
|
||||
FULL,
|
||||
/** 用户手动触发的全量压缩 */
|
||||
MANUAL
|
||||
}
|
||||
|
||||
public static CompactionResult success(CompactLayer layer, int before, int after, String summary) {
|
||||
return new CompactionResult(true, layer, before, after, summary,
|
||||
"Compacted from " + before + " to " + after + " messages");
|
||||
}
|
||||
|
||||
public static CompactionResult noAction(CompactLayer layer, String reason) {
|
||||
return new CompactionResult(false, layer, 0, 0, null, reason);
|
||||
}
|
||||
|
||||
public static CompactionResult failure(CompactLayer layer, String reason) {
|
||||
return new CompactionResult(false, layer, 0, 0, null, reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.claudecode.core.compact;
|
||||
|
||||
import com.claudecode.core.compact.CompactionResult.CompactLayer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 全量压缩 —— AI 摘要全部对话历史,带 PTL(Prompt Too Long)重试。
|
||||
* <p>
|
||||
* 对应 claude-code 的 fullCompact。当 SessionMemoryCompact 无法有效压缩时作为兜底。
|
||||
* PTL 重试策略:按 API Round(user→assistant→tool_result 为一组)逐步丢弃最旧的组。
|
||||
*/
|
||||
public class FullCompact {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FullCompact.class);
|
||||
|
||||
/** PTL 重试最大次数 */
|
||||
private static final int MAX_PTL_RETRIES = 5;
|
||||
|
||||
/** 保留最近 N 条消息(不压缩) */
|
||||
private static final int KEEP_RECENT_MESSAGES = 2;
|
||||
|
||||
private static final String FULL_COMPACT_PROMPT = """
|
||||
Please compress the following conversation history into a thorough summary. Requirements:
|
||||
1. Preserve ALL key decisions, code changes, and technical details
|
||||
2. Keep file paths, function names, class names, and specific identifiers
|
||||
3. Preserve user preferences, requirements, and constraints
|
||||
4. Record the current state of work: what was completed, what remains, what's blocked
|
||||
5. Note any errors encountered and their resolutions
|
||||
6. Keep important context about the project structure and architecture
|
||||
7. Output within 1000 words, using structured bullet points
|
||||
|
||||
Conversation history:
|
||||
""";
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
public FullCompact(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行全量压缩。
|
||||
*
|
||||
* @param history 当前消息历史
|
||||
* @return 压缩后的新历史;如果失败返回 null
|
||||
*/
|
||||
public List<Message> compact(List<Message> history) {
|
||||
if (history.size() <= KEEP_RECENT_MESSAGES + 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int before = history.size();
|
||||
Message systemMsg = history.getFirst();
|
||||
|
||||
// 按 API Round 分组
|
||||
List<ApiRound> rounds = groupByRounds(history);
|
||||
|
||||
// PTL 重试循环:逐步丢弃最旧的 round
|
||||
int dropCount = 0;
|
||||
while (dropCount < rounds.size() - 1 && dropCount < MAX_PTL_RETRIES) {
|
||||
List<ApiRound> remaining = rounds.subList(dropCount, rounds.size());
|
||||
|
||||
try {
|
||||
String summary = generateFullSummary(remaining);
|
||||
if (summary != null && !summary.isBlank()) {
|
||||
// 构建新历史
|
||||
List<Message> newHistory = new ArrayList<>();
|
||||
newHistory.add(systemMsg);
|
||||
newHistory.add(new SystemMessage("[Conversation Summary]\n" + summary));
|
||||
|
||||
// 保留最后几条消息
|
||||
for (int i = Math.max(1, before - KEEP_RECENT_MESSAGES); i < before; i++) {
|
||||
newHistory.add(history.get(i));
|
||||
}
|
||||
|
||||
log.info("Full compact succeeded: {} → {} messages (dropped {} rounds)",
|
||||
before, newHistory.size(), dropCount);
|
||||
return newHistory;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Full compact attempt failed (drop={}): {}", dropCount, e.getMessage());
|
||||
// PTL error — drop oldest round and retry
|
||||
}
|
||||
|
||||
dropCount++;
|
||||
}
|
||||
|
||||
log.error("Full compact failed after {} PTL retries", dropCount);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行全量压缩并返回 CompactionResult。
|
||||
*/
|
||||
public CompactionResult compactWithResult(List<Message> history) {
|
||||
int before = history.size();
|
||||
List<Message> result = compact(history);
|
||||
if (result == null) {
|
||||
return CompactionResult.failure(CompactLayer.FULL, "Full compact failed");
|
||||
}
|
||||
return CompactionResult.success(CompactLayer.FULL, before, result.size(), null);
|
||||
}
|
||||
|
||||
// ── 内部方法 ──
|
||||
|
||||
/** 按 API Round 分组:一个 round = [UserMessage] + [AssistantMessage + ToolResponseMessages...] */
|
||||
private List<ApiRound> groupByRounds(List<Message> history) {
|
||||
List<ApiRound> rounds = new ArrayList<>();
|
||||
List<Message> currentRound = new ArrayList<>();
|
||||
|
||||
for (int i = 1; i < history.size(); i++) { // 跳过系统消息
|
||||
Message msg = history.get(i);
|
||||
if (msg instanceof UserMessage && !currentRound.isEmpty()) {
|
||||
rounds.add(new ApiRound(List.copyOf(currentRound)));
|
||||
currentRound.clear();
|
||||
}
|
||||
currentRound.add(msg);
|
||||
}
|
||||
|
||||
if (!currentRound.isEmpty()) {
|
||||
rounds.add(new ApiRound(List.copyOf(currentRound)));
|
||||
}
|
||||
|
||||
return rounds;
|
||||
}
|
||||
|
||||
/** 生成全量摘要 */
|
||||
private String generateFullSummary(List<ApiRound> rounds) {
|
||||
StringBuilder dialogText = new StringBuilder();
|
||||
|
||||
for (ApiRound round : rounds) {
|
||||
for (Message msg : round.messages()) {
|
||||
switch (msg) {
|
||||
case UserMessage um -> dialogText.append("[User] ").append(um.getText()).append("\n");
|
||||
case AssistantMessage am -> {
|
||||
if (am.getText() != null && !am.getText().isBlank()) {
|
||||
String text = am.getText();
|
||||
if (text.length() > 600) text = text.substring(0, 600) + "...";
|
||||
dialogText.append("[Assistant] ").append(text).append("\n");
|
||||
}
|
||||
if (am.hasToolCalls()) {
|
||||
for (var tc : am.getToolCalls()) {
|
||||
dialogText.append("[Tool Call] ").append(tc.name()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
case ToolResponseMessage trm -> {
|
||||
for (var resp : trm.getResponses()) {
|
||||
dialogText.append("[Tool Result: ").append(resp.name()).append("]\n");
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
dialogText.append("---\n");
|
||||
}
|
||||
|
||||
if (dialogText.isEmpty()) return null;
|
||||
|
||||
Prompt prompt = new Prompt(List.of(new UserMessage(FULL_COMPACT_PROMPT + dialogText)));
|
||||
ChatResponse response = chatModel.call(prompt);
|
||||
return response.getResult().getOutput().getText();
|
||||
}
|
||||
|
||||
/** API Round:一个用户请求 + AI 响应 + 工具调用的完整回合 */
|
||||
private record ApiRound(List<Message> messages) {}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.claudecode.core.compact;
|
||||
|
||||
import com.claudecode.core.compact.CompactionResult.CompactLayer;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 微压缩 —— 在每次 API 调用后执行,裁剪旧的 tool_result 内容。
|
||||
* <p>
|
||||
* 对应 claude-code 的 microCompact。不需要额外 API 调用,纯本地操作。
|
||||
* 策略:保留最近 N 轮的 tool 结果,更早的只保留摘要行 "[Tool result truncated]"。
|
||||
*/
|
||||
public class MicroCompact {
|
||||
|
||||
/** 保留最近 N 条 ToolResponseMessage 的完整内容 */
|
||||
private static final int KEEP_RECENT_TOOL_RESULTS = 6;
|
||||
|
||||
/** 截断阈值:超过此长度的旧 tool result 才会被截断 */
|
||||
private static final int TRUNCATE_THRESHOLD = 200;
|
||||
|
||||
/** 截断后的占位文本 */
|
||||
private static final String TRUNCATED_MARKER = "[Tool result truncated — %d chars omitted]";
|
||||
|
||||
/**
|
||||
* 对消息历史执行微压缩。
|
||||
* 直接在原始列表上原地修改以提升性能。
|
||||
*
|
||||
* @param history 消息列表(直接修改)
|
||||
* @return 压缩结果
|
||||
*/
|
||||
public CompactionResult compact(List<Message> history) {
|
||||
int totalToolResponses = 0;
|
||||
int truncated = 0;
|
||||
|
||||
// 倒序扫描,找到所有 ToolResponseMessage 的位置
|
||||
int recentCount = 0;
|
||||
for (int i = history.size() - 1; i >= 0; i--) {
|
||||
if (history.get(i) instanceof ToolResponseMessage) {
|
||||
totalToolResponses++;
|
||||
recentCount++;
|
||||
if (recentCount > KEEP_RECENT_TOOL_RESULTS) {
|
||||
// 需要截断
|
||||
ToolResponseMessage trm = (ToolResponseMessage) history.get(i);
|
||||
if (shouldTruncate(trm)) {
|
||||
history.set(i, truncateToolResponse(trm));
|
||||
truncated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (truncated == 0) {
|
||||
return CompactionResult.noAction(CompactLayer.MICRO, "No tool results to truncate");
|
||||
}
|
||||
|
||||
return CompactionResult.success(CompactLayer.MICRO, totalToolResponses,
|
||||
totalToolResponses - truncated, null);
|
||||
}
|
||||
|
||||
/** 判断 ToolResponseMessage 是否需要截断 */
|
||||
private boolean shouldTruncate(ToolResponseMessage trm) {
|
||||
var responses = trm.getResponses();
|
||||
if (responses == null || responses.isEmpty()) return false;
|
||||
for (var resp : responses) {
|
||||
if (resp.responseData() != null && resp.responseData().toString().length() > TRUNCATE_THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 创建截断后的 ToolResponseMessage */
|
||||
private ToolResponseMessage truncateToolResponse(ToolResponseMessage original) {
|
||||
var responses = original.getResponses();
|
||||
if (responses == null || responses.isEmpty()) return original;
|
||||
|
||||
var truncatedResponses = responses.stream().map(resp -> {
|
||||
String data = resp.responseData() != null ? resp.responseData().toString() : "";
|
||||
if (data.length() > TRUNCATE_THRESHOLD) {
|
||||
String marker = String.format(TRUNCATED_MARKER, data.length());
|
||||
return new ToolResponseMessage.ToolResponse(resp.id(), resp.name(), marker);
|
||||
}
|
||||
return resp;
|
||||
}).toList();
|
||||
|
||||
return ToolResponseMessage.builder()
|
||||
.responses(truncatedResponses)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.claudecode.core.compact;
|
||||
|
||||
import com.claudecode.core.compact.CompactionResult.CompactLayer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Session Memory 压缩 —— 保留近期消息段,用 AI 摘要旧消息。
|
||||
* <p>
|
||||
* 对应 claude-code 的 sessionMemoryCompact。这是主要的自动压缩方式。
|
||||
* 算法:
|
||||
* <ol>
|
||||
* <li>找到上次压缩的边界(通过检测 [Conversation Summary] 标记)</li>
|
||||
* <li>计算需要保留的近期消息段(至少保留 MIN_KEEP_TOKENS token 估算量 + MIN_KEEP_TEXT_MSGS 条文本消息)</li>
|
||||
* <li>将边界之后、保留段之前的消息通过 AI 生成摘要</li>
|
||||
* <li>用 [系统提示] + [历史摘要] + [新摘要] + [保留段] 替换历史</li>
|
||||
* </ol>
|
||||
*/
|
||||
public class SessionMemoryCompact {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SessionMemoryCompact.class);
|
||||
|
||||
/** 最少保留的文本消息数(用户 + 助手) */
|
||||
private static final int MIN_KEEP_TEXT_MSGS = 5;
|
||||
|
||||
/** 估算最少保留的 token 数 */
|
||||
private static final int MIN_KEEP_TOKENS = 10_000;
|
||||
|
||||
/** 估算最多保留的 token 数 */
|
||||
private static final int MAX_KEEP_TOKENS = 40_000;
|
||||
|
||||
/** 每字符估算的 token 数(粗略近似) */
|
||||
private static final double CHARS_PER_TOKEN = 4.0;
|
||||
|
||||
private static final String SUMMARY_PROMPT = """
|
||||
Summarize the following conversation segment concisely but thoroughly.
|
||||
Preserve:
|
||||
- All key technical decisions and their rationale
|
||||
- File paths, function names, class names, and specific code identifiers
|
||||
- User requirements and preferences
|
||||
- Current state of work (what was done, what remains)
|
||||
- Any errors encountered and their resolutions
|
||||
|
||||
Keep the summary under 800 words. Use bullet points for clarity.
|
||||
|
||||
Conversation segment to summarize:
|
||||
""";
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
public SessionMemoryCompact(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Session Memory 压缩。
|
||||
*
|
||||
* @param history 当前消息历史(不直接修改,返回新列表)
|
||||
* @return 压缩结果;如果无法压缩返回 noAction
|
||||
*/
|
||||
public CompactionResult compact(List<Message> history) {
|
||||
if (history.size() <= MIN_KEEP_TEXT_MSGS + 2) {
|
||||
return CompactionResult.noAction(CompactLayer.SESSION_MEMORY,
|
||||
"Too few messages to compact");
|
||||
}
|
||||
|
||||
int before = history.size();
|
||||
|
||||
// 找到系统提示词(第一条)
|
||||
Message systemMsg = history.getFirst();
|
||||
|
||||
// 找到上一次摘要的位置(如果有的话)
|
||||
int lastSummaryIndex = findLastSummaryIndex(history);
|
||||
|
||||
// 从摘要之后开始计算可压缩区域
|
||||
int compressibleStart = lastSummaryIndex + 1;
|
||||
|
||||
// 从末尾向前找保留段的起始位置
|
||||
int keepStart = findKeepStart(history, compressibleStart);
|
||||
|
||||
// 如果可压缩区域太小,不值得压缩
|
||||
if (keepStart - compressibleStart < 4) {
|
||||
return CompactionResult.noAction(CompactLayer.SESSION_MEMORY,
|
||||
"Not enough messages to compress (only " + (keepStart - compressibleStart) + " in range)");
|
||||
}
|
||||
|
||||
// 提取需要压缩的消息段
|
||||
List<Message> toCompress = history.subList(compressibleStart, keepStart);
|
||||
|
||||
// 生成摘要
|
||||
String summary;
|
||||
try {
|
||||
summary = generateSummary(toCompress);
|
||||
} catch (Exception e) {
|
||||
log.warn("Session memory compression failed: {}", e.getMessage());
|
||||
return CompactionResult.failure(CompactLayer.SESSION_MEMORY,
|
||||
"Summary generation failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (summary == null || summary.isBlank()) {
|
||||
return CompactionResult.failure(CompactLayer.SESSION_MEMORY,
|
||||
"Empty summary generated");
|
||||
}
|
||||
|
||||
// 构建新历史
|
||||
List<Message> newHistory = new ArrayList<>();
|
||||
newHistory.add(systemMsg);
|
||||
|
||||
// 保留旧的摘要(如果有的话,合并到新摘要中)
|
||||
String previousSummary = extractPreviousSummary(history, lastSummaryIndex);
|
||||
if (previousSummary != null) {
|
||||
summary = "=== Earlier Context ===\n" + previousSummary + "\n\n=== Recent Activity ===\n" + summary;
|
||||
}
|
||||
|
||||
// 添加新的摘要消息
|
||||
newHistory.add(new SystemMessage("[Conversation Summary]\n" + summary));
|
||||
|
||||
// 添加保留段
|
||||
for (int i = keepStart; i < history.size(); i++) {
|
||||
newHistory.add(history.get(i));
|
||||
}
|
||||
|
||||
int after = newHistory.size();
|
||||
return new CompactionResult(true, CompactLayer.SESSION_MEMORY, before, after, summary,
|
||||
"Session memory compacted: " + before + " → " + after + " messages");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取压缩后的新历史。调用方需要先调用 compact() 确认成功,然后调用此方法获取结果。
|
||||
* 为避免重复逻辑,此方法重新执行压缩并返回新历史。
|
||||
*/
|
||||
public List<Message> getCompactedHistory(List<Message> history) {
|
||||
if (history.size() <= MIN_KEEP_TEXT_MSGS + 2) return null;
|
||||
|
||||
Message systemMsg = history.getFirst();
|
||||
int lastSummaryIndex = findLastSummaryIndex(history);
|
||||
int compressibleStart = lastSummaryIndex + 1;
|
||||
int keepStart = findKeepStart(history, compressibleStart);
|
||||
|
||||
if (keepStart - compressibleStart < 4) return null;
|
||||
|
||||
List<Message> toCompress = history.subList(compressibleStart, keepStart);
|
||||
String summary;
|
||||
try {
|
||||
summary = generateSummary(toCompress);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
if (summary == null || summary.isBlank()) return null;
|
||||
|
||||
List<Message> newHistory = new ArrayList<>();
|
||||
newHistory.add(systemMsg);
|
||||
|
||||
String previousSummary = extractPreviousSummary(history, lastSummaryIndex);
|
||||
if (previousSummary != null) {
|
||||
summary = "=== Earlier Context ===\n" + previousSummary + "\n\n=== Recent Activity ===\n" + summary;
|
||||
}
|
||||
|
||||
newHistory.add(new SystemMessage("[Conversation Summary]\n" + summary));
|
||||
for (int i = keepStart; i < history.size(); i++) {
|
||||
newHistory.add(history.get(i));
|
||||
}
|
||||
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
// ── 内部方法 ──
|
||||
|
||||
/** 找到历史中最后一个 [Conversation Summary] 系统消息的索引 */
|
||||
private int findLastSummaryIndex(List<Message> history) {
|
||||
for (int i = history.size() - 1; i >= 1; i--) {
|
||||
if (history.get(i) instanceof SystemMessage sm
|
||||
&& sm.getText() != null
|
||||
&& sm.getText().startsWith("[Conversation Summary]")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // 没有摘要,从系统提示之后开始
|
||||
}
|
||||
|
||||
/** 从末尾向前找保留段的起始位置 */
|
||||
private int findKeepStart(List<Message> history, int minStart) {
|
||||
int textMsgCount = 0;
|
||||
long estimatedTokens = 0;
|
||||
|
||||
for (int i = history.size() - 1; i >= minStart; i--) {
|
||||
Message msg = history.get(i);
|
||||
|
||||
// 估算 token 量
|
||||
long msgTokens = estimateTokens(msg);
|
||||
estimatedTokens += msgTokens;
|
||||
|
||||
if (msg instanceof UserMessage || msg instanceof AssistantMessage) {
|
||||
textMsgCount++;
|
||||
}
|
||||
|
||||
// 确保不会拆分 tool_use / tool_result 对
|
||||
// 如果当前是 ToolResponseMessage,它的 AssistantMessage(含 tool_calls)应在前面
|
||||
if (msg instanceof ToolResponseMessage && i > minStart) {
|
||||
continue; // 继续往前包含对应的 AssistantMessage
|
||||
}
|
||||
|
||||
// 满足最小保留条件,且已达到上限则停止
|
||||
if (textMsgCount >= MIN_KEEP_TEXT_MSGS && estimatedTokens >= MIN_KEEP_TOKENS) {
|
||||
// 检查是否达到 token 上限
|
||||
if (estimatedTokens >= MAX_KEEP_TOKENS) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果从 minStart 开始全部都在保留范围内,返回 minStart
|
||||
// 说明消息不够多,不需要压缩
|
||||
return minStart;
|
||||
}
|
||||
|
||||
/** 估算消息的 token 数 */
|
||||
private long estimateTokens(Message msg) {
|
||||
String text = switch (msg) {
|
||||
case UserMessage um -> um.getText();
|
||||
case AssistantMessage am -> am.getText();
|
||||
case SystemMessage sm -> sm.getText();
|
||||
case ToolResponseMessage trm -> {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (var resp : trm.getResponses()) {
|
||||
if (resp.responseData() != null) {
|
||||
sb.append(resp.responseData().toString());
|
||||
}
|
||||
}
|
||||
yield sb.toString();
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
if (text == null || text.isEmpty()) return 10; // 最小估算
|
||||
return (long) (text.length() / CHARS_PER_TOKEN);
|
||||
}
|
||||
|
||||
/** 提取上一次的摘要文本 */
|
||||
private String extractPreviousSummary(List<Message> history, int summaryIndex) {
|
||||
if (summaryIndex <= 0) return null;
|
||||
Message msg = history.get(summaryIndex);
|
||||
if (msg instanceof SystemMessage sm && sm.getText() != null) {
|
||||
String text = sm.getText();
|
||||
if (text.startsWith("[Conversation Summary]\n")) {
|
||||
return text.substring("[Conversation Summary]\n".length());
|
||||
}
|
||||
if (text.startsWith("[Conversation Summary] ")) {
|
||||
return text.substring("[Conversation Summary] ".length());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 调用 AI 生成对话段摘要 */
|
||||
private String generateSummary(List<Message> segment) {
|
||||
StringBuilder dialogText = new StringBuilder();
|
||||
for (Message msg : segment) {
|
||||
switch (msg) {
|
||||
case UserMessage um -> dialogText.append("[User] ").append(um.getText()).append("\n");
|
||||
case AssistantMessage am -> {
|
||||
if (am.getText() != null && !am.getText().isBlank()) {
|
||||
String text = am.getText();
|
||||
if (text.length() > 800) text = text.substring(0, 800) + "...";
|
||||
dialogText.append("[Assistant] ").append(text).append("\n");
|
||||
}
|
||||
if (am.hasToolCalls()) {
|
||||
for (var tc : am.getToolCalls()) {
|
||||
dialogText.append("[Tool Call] ").append(tc.name()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
case ToolResponseMessage trm -> {
|
||||
for (var resp : trm.getResponses()) {
|
||||
String data = resp.responseData() != null ? resp.responseData().toString() : "";
|
||||
if (data.length() > 200) data = data.substring(0, 200) + "...";
|
||||
dialogText.append("[Tool Result: ").append(resp.name()).append("] ")
|
||||
.append(data).append("\n");
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogText.isEmpty()) return null;
|
||||
|
||||
Prompt prompt = new Prompt(List.of(new UserMessage(SUMMARY_PROMPT + dialogText)));
|
||||
ChatResponse response = chatModel.call(prompt);
|
||||
return response.getResult().getOutput().getText();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user