feat(iteration): start_next_iteration 二次确认——当前迭代有未完成任务时须 confirm=true 才作废

当前迭代存在活跃任务时,start_next_iteration 不带 confirm 返回 CONFIRM 提示
("还有 N 个未完成任务,确认作废并启动下一迭代?"),用户确认后 agent 传 confirm=true 才执行。
无活跃任务时无需确认直接推进。抽 _active_task_ids 供计数与作废复用。
This commit is contained in:
ymq 2026-08-19 17:17:32 +08:00
parent d77910ea2e
commit ccc7baafbd
2 changed files with 30 additions and 12 deletions

View File

@ -180,21 +180,25 @@ async def get_current_iteration(sor, project_id):
return None
async def _cancel_active_tasks(sor, project_id, iteration_name):
"""作废某迭代内所有活跃任务(非终态 → cancelled + 清 claimed_by)。
async def _active_task_ids(sor, project_id, iteration_name):
"""某迭代内所有活跃任务的 ID 列表非终态submitted/running/review/qc_review/waiting)。
任务经 params.iteration_id存迭代名归属迭代只作废该迭代的活跃任务
已终态(completed/approved/cancelled/failed)的不动返回作废数量
任务经 params.iteration_id存迭代名归属迭代已终态(completed/approved/cancelled/failed)不计
"""
if not iteration_name:
return 0
return []
recs = await sor.sqlExe(
"SELECT id FROM pipeline_tasks WHERE tenant_id=${pid}$ "
"AND state NOT IN ('completed','approved','cancelled','failed') "
"AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.iteration_id'))=${nm}$",
{"pid": project_id, "nm": iteration_name})
await sor.sqlExe("COMMIT", {})
ids = [getattr(r, 'id', '') for r in (recs or []) if getattr(r, 'id', '')]
return [getattr(r, 'id', '') for r in (recs or []) if getattr(r, 'id', '')]
async def _cancel_active_tasks(sor, project_id, iteration_name):
"""作废某迭代内所有活跃任务(非终态 → cancelled + 清 claimed_by。返回作废数量。"""
ids = await _active_task_ids(sor, project_id, iteration_name)
if ids:
in_clause = ",".join(["'" + x.replace("'", "''") + "'" for x in ids])
await sor.sqlExe(
@ -203,10 +207,11 @@ async def _cancel_active_tasks(sor, project_id, iteration_name):
return len(ids)
async def start_next_iteration(project_id, who=None, agent_id=None):
async def start_next_iteration(project_id, who=None, agent_id=None, confirm=False):
"""启动下一个迭代(用户指令推进):
1. 若存在当前迭代(in_progress)先强制完成它 + 作废其活跃任务
1. 若存在当前迭代(in_progress)且有活跃任务 confirm=True 才强制完成它 + 作废活跃任务
否则返回 CONFIRM 提示等用户确认
2. 找下一个 planning 迭代seq_no 最小且 > 已结束迭代的最大 seq_nostart
返回 (ok, message)
"""
@ -214,8 +219,15 @@ async def start_next_iteration(project_id, who=None, agent_id=None):
return False, "缺少 project_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
# 1. 强制完成当前迭代 + 作废其活跃任务
cur = await get_current_iteration(sor, project_id)
# 二次确认:当前迭代有活跃任务时,须 confirm=True 才作废(否则返回确认提示)
if cur and not confirm:
cur_name = cur.get('iteration_name', '') or ''
active_ids = await _active_task_ids(sor, project_id, cur_name)
if active_ids:
return False, (f"CONFIRM: 当前迭代「{cur_name}」还有 {len(active_ids)} 个未完成任务,"
f"确认作废这些任务并启动下一迭代请回复「确认」agent 将传 confirm=true 执行")
# 1. 强制完成当前迭代 + 作废其活跃任务
if cur:
cur_id = cur.get('id', '')
cur_name = cur.get('iteration_name', '') or ''

View File

@ -174,7 +174,7 @@ SDL_TOOLS = [
ToolDefinition(name="start_iteration", description="开始迭代(planning→in_progress)", parameters={"iteration_id": "迭代ID"}, category="iteration"),
ToolDefinition(name="complete_iteration", description="完成迭代(in_progress→completed)", parameters={"iteration_id": "迭代ID"}, category="iteration"),
ToolDefinition(name="cancel_iteration", description="取消迭代(planning/in_progress→cancelled)", parameters={"iteration_id": "迭代ID", "comment": "取消原因(可选)"}, category="iteration"),
ToolDefinition(name="start_next_iteration", description="启动下一个迭代(强制完成当前迭代+作废其活跃任务,再启动编号最小的下一个planning迭代)", parameters={}, category="iteration"),
ToolDefinition(name="start_next_iteration", description="启动下一个迭代(若当前迭代有未完成任务需二次确认confirm=true否则强制完成当前+作废其活跃任务+启动下一planning迭代)", parameters={"confirm": "二次确认(当前迭代有未完成任务时必须为true)"}, category="iteration"),
# ── 交付件评审 ──
ToolDefinition(name="submit_deliverable", description="提交交付件(pending)", parameters={"title": "交付件标题", "deliverable_type": "类型(code/doc/config等)", "content": "交付件内容", "task_id": "关联任务ID(可选)"}, category="deliverable"),
ToolDefinition(name="approve_deliverable", description="评审通过交付件(pending→approved)", parameters={"deliverable_id": "交付件ID"}, category="deliverable"),
@ -1081,10 +1081,16 @@ async def _h_start_next_iteration(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
confirm = (str(p.get("confirm", "") or "")).strip().lower() == "true"
from .iteration_capability import start_next_iteration
ok, msg = await start_next_iteration(pid, who="agent.main_agent",
agent_id=ctx.get("user_id", "") or "")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
agent_id=ctx.get("user_id", "") or "",
confirm=confirm)
if ok:
return f"OK: {msg}"
if msg.startswith("CONFIRM:"):
return msg # 确认提示原样返回,让 agent 引导用户确认
return f"ERROR: {msg}"
async def _h_create_iteration(sor, p, ctx):