feat: 添加 Escape/Ctrl+C 中断 Agent 运行功能
- AgentLoop 添加 cancel()/resetCancel() 中断机制 - executeLoop 在每次迭代前和API调用后检查取消标志 - Escape 键和 Ctrl+C 在 Agent 运行时触发中断 - 快捷键栏更新提示 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -66,6 +66,9 @@ public class AgentLoop {
|
|||||||
/** 拒绝追踪器 */
|
/** 拒绝追踪器 */
|
||||||
private final DenialTracker denialTracker = new DenialTracker();
|
private final DenialTracker denialTracker = new DenialTracker();
|
||||||
|
|
||||||
|
/** 中断标志 —— 用于取消当前运行中的 Agent 循环 */
|
||||||
|
private volatile boolean cancelled = false;
|
||||||
|
|
||||||
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
|
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
|
||||||
private final List<Message> messageHistory = new ArrayList<>();
|
private final List<Message> messageHistory = new ArrayList<>();
|
||||||
|
|
||||||
@@ -132,6 +135,16 @@ public class AgentLoop {
|
|||||||
this.onThinkingContent = onThinkingContent;
|
this.onThinkingContent = onThinkingContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 取消当前运行中的 Agent 循环 */
|
||||||
|
public void cancel() {
|
||||||
|
cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置取消标志(每次新的循环开始时调用) */
|
||||||
|
private void resetCancel() {
|
||||||
|
cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 阻塞模式 ====================
|
// ==================== 阻塞模式 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,6 +174,7 @@ public class AgentLoop {
|
|||||||
// ==================== 核心循环(统一阻塞/流式) ====================
|
// ==================== 核心循环(统一阻塞/流式) ====================
|
||||||
|
|
||||||
private String executeLoop(boolean streaming, Consumer<String> onToken) {
|
private String executeLoop(boolean streaming, Consumer<String> onToken) {
|
||||||
|
resetCancel();
|
||||||
List<ToolCallback> callbacks = toolRegistry.toCallbacks(toolContext);
|
List<ToolCallback> callbacks = toolRegistry.toCallbacks(toolContext);
|
||||||
ChatOptions options = ToolCallingChatOptions.builder()
|
ChatOptions options = ToolCallingChatOptions.builder()
|
||||||
.toolCallbacks(callbacks)
|
.toolCallbacks(callbacks)
|
||||||
@@ -171,6 +185,13 @@ public class AgentLoop {
|
|||||||
String lastAssistantText = "";
|
String lastAssistantText = "";
|
||||||
|
|
||||||
while (iteration < MAX_ITERATIONS) {
|
while (iteration < MAX_ITERATIONS) {
|
||||||
|
// 检查取消标志
|
||||||
|
if (cancelled) {
|
||||||
|
log.info("Agent loop cancelled by user at iteration {}", iteration);
|
||||||
|
lastAssistantText += "\n\n[Interrupted by user]";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
iteration++;
|
iteration++;
|
||||||
log.debug("Agent loop iteration {} ({})", iteration, streaming ? "streaming" : "blocking");
|
log.debug("Agent loop iteration {} ({})", iteration, streaming ? "streaming" : "blocking");
|
||||||
|
|
||||||
@@ -184,6 +205,12 @@ public class AgentLoop {
|
|||||||
result = blockingIteration(prompt);
|
result = blockingIteration(prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查取消标志(API调用后)
|
||||||
|
if (cancelled) {
|
||||||
|
log.info("Agent loop cancelled by user after API call at iteration {}", iteration);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 记录 Token 使用量
|
// 记录 Token 使用量
|
||||||
if (result.promptTokens > 0 || result.completionTokens > 0) {
|
if (result.promptTokens > 0 || result.completionTokens > 0) {
|
||||||
tokenTracker.recordUsage(result.promptTokens, result.completionTokens);
|
tokenTracker.recordUsage(result.promptTokens, result.completionTokens);
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Box.of(
|
return Box.of(
|
||||||
Text.of("↑↓ history wheel scroll Ctrl+D exit").dimmed(),
|
Text.of("↑↓ history Esc interrupt Ctrl+D exit").dimmed(),
|
||||||
Spacer.create(),
|
Spacer.create(),
|
||||||
Text.of(tokenInfo).color(Color.BRIGHT_GREEN)
|
Text.of(tokenInfo).color(Color.BRIGHT_GREEN)
|
||||||
).paddingX(1).height(1);
|
).paddingX(1).height(1);
|
||||||
@@ -536,7 +536,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
// Ctrl+C: 取消当前输入或中断 Agent
|
// Ctrl+C: 取消当前输入或中断 Agent
|
||||||
if (key.ctrl() && "c".equals(input)) {
|
if (key.ctrl() && "c".equals(input)) {
|
||||||
if (agentRunning.get()) {
|
if (agentRunning.get()) {
|
||||||
// TODO: 中断 Agent 运行
|
agentLoop.cancel();
|
||||||
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
|
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
|
||||||
} else {
|
} else {
|
||||||
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
|
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
|
||||||
@@ -554,9 +554,15 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI 运行中时忽略大部分输入(但允许滚动)
|
// AI 运行中时允许滚动和 Escape 中断
|
||||||
if (agentRunning.get()) {
|
if (agentRunning.get()) {
|
||||||
handleScrollInput(key, s);
|
if (key.escape()) {
|
||||||
|
// Esc: 中断 Agent 运行
|
||||||
|
agentLoop.cancel();
|
||||||
|
addMessageInternal(new SystemMsg("⚡ Interrupted", Color.BRIGHT_YELLOW), s);
|
||||||
|
} else {
|
||||||
|
handleScrollInput(key, s);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user