feat(workflow): 中断系统(x 杀单 agent / K 杀整个 workflow,Dialog 二次确认)

- claudeCodeBackend 桥接 ctx.signal → runAgent.override.abortController(修 'x' 无效根因:abort 到不了内部 fetch)
- AbortError 识别为 throw WorkflowAbortedError(不再吞成 dead,workflow 能感知被 kill)
- ports.taskRegistrar 加 registerAgentAbort/unregisterAgentAbort/killAgent;service.killAgent(runId, agentId) 精确中断
- 面板键位:'x' 杀当前 agent(agents 列聚焦时) / 'K' 杀整个 workflow;Dialog 二次确认 + confirm 模式吞导航键防误触
- 新增测试 8 项(backend signal bridge / hooks inject / ports killAgent / service killAgent)

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-14 10:38:15 +08:00
parent 5aa39c36b7
commit bcd6771c48
14 changed files with 592 additions and 25 deletions

View File

@@ -5,6 +5,7 @@ import {
type AgentAdapterContext,
type AgentRunParams,
type AgentRunResult,
WorkflowAbortedError,
} from '@claude-code-best/workflow-engine'
import { assembleToolPool } from '../../tools.js'
import { finalizeAgentTool } from '@claude-code-best/builtin-tools/tools/AgentTool/agentToolUtils.js'
@@ -146,14 +147,16 @@ export const claudeCodeBackend: AgentAdapter = {
const appState = toolUseContext.getAppState()
const agentDef = resolveAgentDefinition(params.agentType, toolUseContext)
const model = mapWorkflowModel(params.model)
const agentId = createAgentId()
// coreAgentIdcore 层子 agent 跟踪 ID字符串runAgent 内部用)。
// 与 ctx.agentId引擎 number seq用于面板/killAgent 路由)是两个不同概念,不可混用。
const coreAgentId = createAgentId()
// isolation:'worktree' — 在独立 git worktree 里跑 agent并发写互不冲突。
let worktreeInfo: WorkflowWorktreeInfo | null = null
if (params.isolation === 'worktree') {
try {
worktreeInfo = await createAgentWorktree(
makeWorkflowWorktreeSlug(ctx.runId, agentId),
makeWorkflowWorktreeSlug(ctx.runId, coreAgentId),
)
} catch (e) {
// fail-closed隔离未达成不静默退化为共享 cwd否则并发写数据竞争
@@ -170,6 +173,21 @@ export const claudeCodeBackend: AgentAdapter = {
runWithCwdOverride(worktreeInfo!.worktreePath, fn)
: <T>(fn: () => T): T => fn()
// 桥接 ctx.signal → runAgent.override.abortController。否则 workflow 被 kill
// 时 runAgent 不知道('x' 无效根因abort 信号到不了内部 fetchagent 跑到完成。
// 单 agent kill 走 service.kill(runId, agentId) → ports.taskRegistrar.killAgent →
// agentAbortControllers.get(agentId).abort();同一 controller 接管两条路径。
const agentAbort = new AbortController()
const onParentAbort = (): void => agentAbort.abort()
if (ctx.signal.aborted) {
agentAbort.abort()
} else {
ctx.signal.addEventListener('abort', onParentAbort, { once: true })
}
if (typeof ctx.registerAgentAbort === 'function') {
ctx.registerAgentAbort(ctx.agentId, agentAbort)
}
const workerPermissionContext = {
...appState.toolPermissionContext,
mode: agentDef.permissionMode ?? 'acceptEdits',
@@ -201,9 +219,10 @@ export const claudeCodeBackend: AgentAdapter = {
isAsync: true,
querySource: toolUseContext.options.querySource ?? 'workflow',
availableTools: workerTools,
override: { agentId },
// override 同一对象coreAgentIdcore 子 agent 跟踪)+ abortControllerkill 桥接)。
// runAgent 的 model 是顶层 ModelAliasworkflow 的 model 是任意别名串,
// 类型上不兼容,运行时由 provider 层解析。双重断言透传(优于 as any/never
override: { agentId: coreAgentId, abortController: agentAbort },
...(model ? { model: model as unknown as ModelAlias } : {}),
...(worktreeInfo ? { worktreePath: worktreeInfo.worktreePath } : {}),
})) {
@@ -224,12 +243,23 @@ export const claudeCodeBackend: AgentAdapter = {
}
})
} catch (e) {
// abortkill workflow / kill agent识别后必须重抛 WorkflowAbortedError
// 否则 hooks.agent 会把 abort 当作普通失败吞成 deadworkflow 不知道被 kill
// kill 路径 'x' 无效的另一面:信号虽然到了,但结果被伪装成正常完成)。
if (agentAbort.signal.aborted || (e as Error)?.name === 'AbortError') {
throw new WorkflowAbortedError()
}
logForDebugging(
`workflow sub-agent error (${agentDef.agentType}): ${(e as Error).message}`,
)
logEvent('tengu_workflow_agent', { ok: 0 })
return { kind: 'dead' }
} finally {
// 清理幂等listener removeEventListener / Map.delete 重复调用安全。
if (typeof ctx.unregisterAgentAbort === 'function') {
ctx.unregisterAgentAbort(ctx.agentId)
}
ctx.signal.removeEventListener('abort', onParentAbort)
if (worktreeInfo) {
const info = worktreeInfo
worktreeInfo = null
@@ -237,7 +267,7 @@ export const claudeCodeBackend: AgentAdapter = {
}
}
const finalized = finalizeAgentTool(messages, agentId, {
const finalized = finalizeAgentTool(messages, coreAgentId, {
prompt: params.prompt,
resolvedAgentModel: toolUseContext.options.mainLoopModel,
isBuiltInAgent: isBuiltInAgent(agentDef),