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 return None
async def _cancel_active_tasks(sor, project_id, iteration_name): async def _active_task_ids(sor, project_id, iteration_name):
"""作废某迭代内所有活跃任务(非终态 → cancelled + 清 claimed_by)。 """某迭代内所有活跃任务的 ID 列表(非终态:submitted/running/review/qc_review/waiting)。
任务经 params.iteration_id(存迭代名)归属迭代;只作废该迭代的活跃任务, 任务经 params.iteration_id(存迭代名)归属迭代;已终态(completed/approved/cancelled/failed)不计。
已终态(completed/approved/cancelled/failed)的不动。返回作废数量。
""" """
if not iteration_name: if not iteration_name:
return 0 return []
recs = await sor.sqlExe( recs = await sor.sqlExe(
"SELECT id FROM pipeline_tasks WHERE tenant_id=${pid}$ " "SELECT id FROM pipeline_tasks WHERE tenant_id=${pid}$ "
"AND state NOT IN ('completed','approved','cancelled','failed') " "AND state NOT IN ('completed','approved','cancelled','failed') "
"AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.iteration_id'))=${nm}$", "AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.iteration_id'))=${nm}$",
{"pid": project_id, "nm": iteration_name}) {"pid": project_id, "nm": iteration_name})
await sor.sqlExe("COMMIT", {}) 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: if ids:
in_clause = ",".join(["'" + x.replace("'", "''") + "'" for x in ids]) in_clause = ",".join(["'" + x.replace("'", "''") + "'" for x in ids])
await sor.sqlExe( await sor.sqlExe(
@ -203,10 +207,11 @@ async def _cancel_active_tasks(sor, project_id, iteration_name):
return len(ids) 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_no),start 它。 2. 找下一个 planning 迭代(seq_no 最小且 > 已结束迭代的最大 seq_no),start 它。
返回 (ok, message)。 返回 (ok, message)。
""" """
@ -214,8 +219,15 @@ async def start_next_iteration(project_id, who=None, agent_id=None):
return False, "缺少 project_id" return False, "缺少 project_id"
db, dbname = _get_db() db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
# 1. 强制完成当前迭代 + 作废其活跃任务
cur = await get_current_iteration(sor, project_id) 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: if cur:
cur_id = cur.get('id', '') cur_id = cur.get('id', '')
cur_name = cur.get('iteration_name', '') or '' 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="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="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="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="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"), 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", "") pid = ctx.get("project_id", "")
if not pid: if not pid:
return "请先切换到项目" return "请先切换到项目"
confirm = (str(p.get("confirm", "") or "")).strip().lower() == "true"
from .iteration_capability import start_next_iteration from .iteration_capability import start_next_iteration
ok, msg = await start_next_iteration(pid, who="agent.main_agent", ok, msg = await start_next_iteration(pid, who="agent.main_agent",
agent_id=ctx.get("user_id", "") or "") agent_id=ctx.get("user_id", "") or "",
return f"OK: {msg}" if ok else f"ERROR: {msg}" 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): async def _h_create_iteration(sor, p, ctx):