feat: Step 5 引擎接入 + H1-H3/S3 修复 + 审计 D1/D2/D5 修复
引擎接入(dispatcher/spawner/ticker → handler 统一路由): - dispatcher: guardrail/on_checks_passed/on_complete → handler 查询 - spawner: _build_prompt/_build_api_section → handler.build_prompt - ticker: 虚拟项目扫描/assignee/claimed/review/幻觉门控 → handler 判断 Handler 缺陷修复: - H1: _mark_task_status 加 3 次重试(防 DB 锁) - H2: review @mention 加 comment_type='review' - H3: review 非 approved 保持 review 状态(不标 working) - S3: 通知链接改 Gitea(PR/Issue/Commit) 审计修复: - D1: pre_spawn 返回值未检查 → 加 if not 抛 RuntimeError - D2: PromptContext 缺 from_agent/mail_type → 从 must_haves 解析 - D5: _check_reply 查错表 → 恢复查 tasks 表找 in_reply_to 旧方法保留未删(deprecated),确认稳定后再清理。
This commit is contained in:
+73
-103
@@ -22,6 +22,7 @@ from src.blackboard.models import Task
|
||||
from src.blackboard.db import get_connection
|
||||
from src.daemon.spawner import AgentBusyError
|
||||
from src.daemon.router import AgentRouter
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
|
||||
logger = logging.getLogger("moziplus-v2.dispatcher")
|
||||
|
||||
@@ -123,10 +124,11 @@ class Dispatcher:
|
||||
"status": "dispatched"|"skipped"|"error"|"blocked", "reason": str}
|
||||
"""
|
||||
# 安全红线检查(调度前拦截)
|
||||
# Mail 是 Agent 间通信,不做 guardrail 检查
|
||||
is_mail = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if self.guardrails and not is_mail:
|
||||
# handler 项目(_mail/_toolchain)不做 guardrail 检查
|
||||
handler = TaskTypeRegistry.get_by_project(
|
||||
project_config.get("project_id", "") if project_config else "")
|
||||
is_handler_task = handler is not None
|
||||
if self.guardrails and not is_handler_task:
|
||||
violations = self.guardrails.check_task(task)
|
||||
critical = [
|
||||
v for v in violations if v.action in (
|
||||
@@ -190,27 +192,26 @@ class Dispatcher:
|
||||
}
|
||||
|
||||
try:
|
||||
# [v2.7.1] Mail: 标 working 移到 spawn_full_agent 内部(check 通过后、subprocess 前)
|
||||
is_mail = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if is_mail:
|
||||
db_path = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
# [Step 5] Handler: pre_spawn + on_checks_passed 统一
|
||||
project_id = project_config.get("project_id", "") if project_config else ""
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
db_path = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
|
||||
# on_checks_passed: 所有检查通过后才标 working,检查失败不标
|
||||
# on_checks_passed: handler 项目在 check 通过后调用 handler.pre_spawn
|
||||
on_checks_passed = None
|
||||
_mail_marked_working = False
|
||||
if is_mail and db_path:
|
||||
handler_marked_working = False
|
||||
if handler and db_path:
|
||||
_task_id = task.id
|
||||
_mail_db = db_path
|
||||
_disp = self
|
||||
_handler_db = db_path
|
||||
_handler = handler
|
||||
|
||||
def _mail_on_checks_passed():
|
||||
nonlocal _mail_marked_working
|
||||
if not _disp._mail_auto_working(_task_id, _mail_db):
|
||||
raise RuntimeError("mail_auto_working_failed")
|
||||
_mail_marked_working = True
|
||||
on_checks_passed = _mail_on_checks_passed
|
||||
def _handler_on_checks_passed():
|
||||
nonlocal handler_marked_working
|
||||
if not _handler.pre_spawn(_task_id, _handler_db):
|
||||
raise RuntimeError("handler_pre_spawn_failed")
|
||||
handler_marked_working = True
|
||||
on_checks_passed = _handler_on_checks_passed
|
||||
|
||||
# 构建 spawn message
|
||||
message = self._build_spawn_message(task, agent_id, project_config,
|
||||
@@ -218,94 +219,46 @@ class Dispatcher:
|
||||
"mode", ""),
|
||||
spawn_type=action_type or "executor")
|
||||
|
||||
# v2.7.2: on_complete 只含业务逻辑,不含 counter.release
|
||||
# counter.release 由 spawn_full_agent 内部的 wrapped_on_complete 保证
|
||||
# [Step 5] Handler: on_complete 统一走 handler.post_complete
|
||||
# 保留旧路径作为 fallback(无 handler 的项目)
|
||||
on_complete = None
|
||||
if is_mail:
|
||||
if handler:
|
||||
_task_id = task.id
|
||||
_mail_db = db_path
|
||||
_must_haves = task.must_haves or ""
|
||||
_dispatcher = self
|
||||
_handler_db = db_path
|
||||
_handler = handler
|
||||
|
||||
def _mail_on_complete(aid, outcome):
|
||||
# 幻觉门控:检查是否有回复,自动标 done/failed
|
||||
def _handler_on_complete(aid, outcome):
|
||||
try:
|
||||
_dispatcher._mail_auto_complete(
|
||||
_task_id, aid, _mail_db, _must_haves, outcome=outcome)
|
||||
_handler.post_complete(
|
||||
_task_id, aid, outcome, _handler_db)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Mail %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _mail_on_complete
|
||||
"Handler %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _handler_on_complete
|
||||
else:
|
||||
# #02: Task 路径也加 on_complete(幻觉门控)
|
||||
# 旧路径:无 handler 的项目(_general 等)
|
||||
_task_id = task.id
|
||||
_task_db = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
_task_db = db_path
|
||||
_dispatcher = self
|
||||
_is_review = action_type == "review"
|
||||
|
||||
# #07.2: executor/review 统一 crash 回退
|
||||
ROLLBACK_CURRENT_AGENT_OUTCOMES = frozenset({
|
||||
"crashed", "compact_failed", "process_crash",
|
||||
"session_stuck", "compact_hanging",
|
||||
})
|
||||
|
||||
def _task_on_complete(aid, outcome):
|
||||
def _legacy_on_complete(aid, outcome):
|
||||
try:
|
||||
# #07.2: 统一 crash 回退——executor 和 review 都回退 current_agent
|
||||
if outcome in ROLLBACK_CURRENT_AGENT_OUTCOMES and _task_db:
|
||||
_dispatcher._rollback_current_agent(
|
||||
_task_db, _task_id, aid)
|
||||
|
||||
if _is_review:
|
||||
if _task_db and outcome in (
|
||||
"completed", "session_revived"):
|
||||
# #09: 读 verdict 决定后续动作
|
||||
conn = get_connection(_task_db)
|
||||
try:
|
||||
review = conn.execute(
|
||||
"SELECT verdict FROM reviews WHERE task_id=? ORDER BY created_at DESC LIMIT 1",
|
||||
(_task_id,)
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if review and review["verdict"] == "approved":
|
||||
_dispatcher._mark_task_status(
|
||||
_task_db, _task_id, "done")
|
||||
logger.info(
|
||||
"Task %s: review approved, marking done", _task_id)
|
||||
else:
|
||||
# 非 approved → @mention 被审
|
||||
# agent(assignee,非 current_agent)
|
||||
verdict_str = review["verdict"] if review else "未知"
|
||||
conn2 = get_connection(_task_db)
|
||||
try:
|
||||
task_row = conn2.execute(
|
||||
"SELECT assignee FROM tasks WHERE id=?", (_task_id,)).fetchone()
|
||||
finally:
|
||||
conn2.close()
|
||||
|
||||
if task_row and task_row["assignee"]:
|
||||
from src.blackboard.blackboard import Blackboard
|
||||
bb = Blackboard(_task_db)
|
||||
bb.add_comment(_task_id, "daemon",
|
||||
f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
||||
comment_type="review")
|
||||
logger.info("Task %s: review verdict=%s, notified assignee=%s",
|
||||
_task_id, verdict_str, task_row["assignee"] if task_row else "?")
|
||||
# 不标 done,保持 review 状态
|
||||
else:
|
||||
logger.warning(
|
||||
"Task %s: review agent %s (%s), NOT marking done", _task_id, aid, outcome)
|
||||
else:
|
||||
# executor: 三信号验证 → 标 review
|
||||
if not _is_review:
|
||||
_dispatcher._task_auto_complete(
|
||||
_task_id, _task_db)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Task %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _task_on_complete
|
||||
"Legacy %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _legacy_on_complete
|
||||
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
agent_id=agent_id,
|
||||
@@ -354,8 +307,26 @@ class Dispatcher:
|
||||
}
|
||||
except Exception as e:
|
||||
# on_checks_passed 已执行但 subprocess 失败 → 回退 working → pending
|
||||
if _mail_marked_working:
|
||||
self._mail_revert_to_pending(task.id, db_path)
|
||||
if handler_marked_working and handler and db_path:
|
||||
# handler 项目:回退到 pending
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task.id,)).fetchone()
|
||||
if row and row["status"] == "working":
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status='pending', updated_at=datetime('now') WHERE id=?",
|
||||
(task.id,))
|
||||
conn.commit()
|
||||
logger.info(
|
||||
"Task %s: reverted working → pending (spawn failed)", task.id)
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as revert_err:
|
||||
logger.error(
|
||||
"Task %s: failed to revert to pending: %s", task.id, revert_err)
|
||||
self._record_routing(
|
||||
task, decision, "error", str(e), _routing_db)
|
||||
return {
|
||||
@@ -580,17 +551,18 @@ class Dispatcher:
|
||||
try:
|
||||
# NOTE: _legacy_dispatch 仅在 router=None 时触发,当前配置不会进入。
|
||||
# Mail 永远走 dispatch() 主路径(on_checks_passed 方案),不走此路径。
|
||||
# 如果未来 legacy 路径被启用,需同步 on_checks_passed 逻辑。
|
||||
is_mail_legacy = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if is_mail_legacy:
|
||||
# [Step 5] handler 统一:用注册表查 handler
|
||||
project_id_legacy = project_config.get("project_id", "") if project_config else ""
|
||||
handler_legacy = TaskTypeRegistry.get_by_project(project_id_legacy)
|
||||
if handler_legacy:
|
||||
db_path_legacy = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
if not db_path_legacy or not self._mail_auto_working(
|
||||
task.id, db_path_legacy):
|
||||
if db_path_legacy:
|
||||
handler_legacy.pre_spawn(task.id, db_path_legacy)
|
||||
else:
|
||||
return {"level": level.value, "agent_id": agent_id,
|
||||
"session_id": None, "status": "error",
|
||||
"reason": "mail_auto_working_failed"}
|
||||
"reason": "no db_path for handler"}
|
||||
|
||||
if hasattr(self.spawner,
|
||||
'build_spawn_message') and project_config:
|
||||
@@ -612,20 +584,18 @@ class Dispatcher:
|
||||
|
||||
# v2.7.2: on_complete 只含业务逻辑
|
||||
on_complete_legacy = None
|
||||
if is_mail_legacy:
|
||||
if handler_legacy:
|
||||
_t_id = task.id
|
||||
_m_db = db_path_legacy
|
||||
_m_mh = task.must_haves or ""
|
||||
_disp = self
|
||||
_h_db = db_path_legacy
|
||||
_h = handler_legacy
|
||||
|
||||
def _mail_oc_legacy(aid, outcome):
|
||||
def _handler_oc_legacy(aid, outcome):
|
||||
try:
|
||||
_disp._mail_auto_complete(
|
||||
_t_id, aid, _m_db, _m_mh, outcome=outcome)
|
||||
_h.post_complete(_t_id, aid, outcome, _h_db)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Mail %s: legacy on_complete error: %s", _t_id, e)
|
||||
on_complete_legacy = _mail_oc_legacy
|
||||
"Handler %s: legacy on_complete error: %s", _t_id, e)
|
||||
on_complete_legacy = _handler_oc_legacy
|
||||
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
agent_id=agent_id, message=message,
|
||||
|
||||
Reference in New Issue
Block a user