feat(agent): support user-initiated cancel for in-flight agent runs

This commit is contained in:
zhayujie
2026-05-26 23:36:09 +08:00
parent ad2db1a776
commit 8d67177a1b
19 changed files with 691 additions and 22 deletions

View File

@@ -26,16 +26,19 @@ from common.log import logger
from cli import __version__
# Known top-level subcommands that cow supports
# Known top-level subcommands that cow supports.
# "start" / "stop" / "restart" refer to daemon lifecycle on the host shell;
# in chat, "/cancel" aborts the in-flight agent run instead.
KNOWN_COMMANDS = {
"help", "version", "status", "logs",
"start", "stop", "restart",
"cancel",
"skill", "context", "config",
"knowledge", "memory",
"install-browser",
}
# Commands that can only run from the CLI (terminal), not in chat
# Commands that can only run from the CLI (terminal), not in chat.
CLI_ONLY_COMMANDS = {"start", "stop", "restart"}
# Commands that can only run from chat (need access to in-process memory)
@@ -225,6 +228,7 @@ class CowCliPlugin(Plugin):
" /help 显示此帮助",
" /version 查看版本",
" /status 查看运行状态",
" /cancel 中止当前正在运行的 Agent 任务",
" /logs [N] 查看最近N条日志 (默认20)",
" /context 查看当前对话上下文信息",
" /context clear 清除当前对话上下文",
@@ -250,6 +254,41 @@ class CowCliPlugin(Plugin):
def _cmd_version(self, args: str, e_context, **_) -> str:
return f"CowAgent v{__version__}"
# ------------------------------------------------------------------
# cancel — abort the in-flight agent run for the current session.
# Fallback handler; in practice chat_channel/web_channel intercept
# /cancel earlier so it bypasses the per-session serial queue.
# ------------------------------------------------------------------
def _cmd_cancel(self, args: str, e_context: EventContext, session_id: str = "", **_) -> str:
"""Signal the running agent to halt at its next checkpoint."""
from agent.protocol import get_cancel_registry
target_session = self._get_session_id(e_context, fallback=session_id)
registry = get_cancel_registry()
# Prefer per-turn request_id (matches the key agent_bridge registered)
cancelled = 0
request_id = ""
if e_context is not None:
try:
ctx = e_context["context"]
request_id = ctx.kwargs.get("request_id") or ctx.get("request_id", "")
except Exception:
request_id = ""
if request_id and registry.cancel_request(request_id):
cancelled = 1
# Fall back to session-wide cancel
if cancelled == 0 and target_session:
cancelled = registry.cancel_session(target_session)
if cancelled <= 0:
return "当前没有可中止的任务。"
return "🛑 已中止"
# ------------------------------------------------------------------
# status
# ------------------------------------------------------------------