PicoBot/src/command/handlers/stop_execution.rs
oudecheng fc3a95b152 feat: 异步子代理 v8 实现与 /stop 取消流程修复
后端核心实现:
- 新增 wait_coordinator:释放/重获取 serial_lock 的 in-tool waiting 模式,替代旧的 break-exit 方案
- 新增 wait_for_subagents 工具:select! 等待子代理完成/用户消息/超时,支持 try_drain 批量消费
- task 工具异步 spawn 路径:CancellationToken 注册表 + RAII guard + Semaphore 并发限流
- process exit 安全检查:pending 子代理存在时抑制 ExecutionCompleted
- 崩溃恢复:启动时标记 running->interrupted,history 加载时对账占位

/stop 取消流程修复:
- wait_coordinator select! 添加 cancel 分支,完整清理状态
- ToolContext 注入 cancel_rx(watch::Receiver clone)
- agent_loop 工具执行 select! 对 wait 工具跳过竞速,防止 drop coordinator 清理逻辑
- 子代理完成状态通过 execution_completed metadata 传播

存储层:
- pending_subagents 表 + 条件 UPDATE
- mark_all_running_as_interrupted 崩溃恢复
2026-08-13 22:12:20 +08:00

128 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use async_trait::async_trait;
use std::sync::Arc;
use crate::command::Command;
use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::response::{CommandError, CommandResponse, MessageKind};
use crate::gateway::cancel_manager::CancelManager;
use crate::gateway::session::SessionManager;
use crate::tools::SubAgentRuntime;
/// 处理 StopExecution 命令:按话题取消当前正在执行的 Agent。
///
/// 取消传播:同时取消该 topic 下所有正在运行的异步子代理(通过 CancellationToken
pub struct StopExecutionCommandHandler {
cancel_manager: CancelManager,
session_manager: SessionManager,
subagent_executor: Option<Arc<dyn SubAgentRuntime>>,
}
impl StopExecutionCommandHandler {
pub fn new(
cancel_manager: CancelManager,
session_manager: SessionManager,
subagent_executor: Option<Arc<dyn SubAgentRuntime>>,
) -> Self {
Self {
cancel_manager,
session_manager,
subagent_executor,
}
}
}
#[async_trait]
impl CommandHandler for StopExecutionCommandHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::StopExecution)
}
fn metadata(&self) -> Option<CommandMetadata> {
Some(CommandMetadata {
name: "stop",
description: "停止当前话题正在执行的 Agent",
usage: "/stop",
})
}
async fn handle(
&self,
_cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
// 优先使用 ctx.topic_id如果没有则从 session_manager 获取真实的 topic_id
let topic_id = match ctx.topic_id.as_deref() {
Some(id) => {
tracing::info!(
channel = %ctx.channel_name,
chat_id = ?ctx.chat_id,
topic_id = %id,
source = "ctx",
"Stop execution command received"
);
id.to_string()
}
None => {
// 从 SessionManager 获取真实的 current topic
let chat_id = ctx.chat_id.as_deref().unwrap_or("");
match self
.session_manager
.get_current_topic(&ctx.channel_name, chat_id)
.await
{
Ok(Some(id)) => {
tracing::info!(
channel = %ctx.channel_name,
chat_id = %chat_id,
topic_id = %id,
source = "session_manager",
"Stop execution command received (resolved from session)"
);
id
}
Ok(None) => {
return Ok(CommandResponse::success(ctx.request_id).with_message(
MessageKind::Notification,
"当前没有活跃的话题,无法停止",
));
}
Err(e) => {
return Ok(CommandResponse::error(
ctx.request_id,
CommandError::new("QUERY_TOPIC_ERROR", e.to_string()),
));
}
}
}
};
let cancelled = self.cancel_manager.cancel_by_topic(&topic_id).await;
// 取消传播:同时取消该 topic 下所有正在运行的异步子代理
let cancelled_subagents = if let Some(ref executor) = self.subagent_executor {
executor.cancel_pending_for_topic(&topic_id).await
} else {
0
};
if cancelled || cancelled_subagents > 0 {
let msg = if cancelled && cancelled_subagents > 0 {
format!(
"正在停止当前任务及 {} 个后台子代理...",
cancelled_subagents
)
} else if cancelled {
"正在停止当前任务...".to_string()
} else {
format!("正在停止 {} 个后台子代理...", cancelled_subagents)
};
Ok(CommandResponse::success(ctx.request_id)
.with_message(MessageKind::Notification, msg))
} else {
Ok(CommandResponse::success(ctx.request_id)
.with_message(MessageKind::Notification, "当前没有正在执行的任务"))
}
}
}