aps-agent/server/agent_core/mesh.py

810 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 多智能体编排 Mesh(moduleId: core-agent-mesh, 可重生 ✅)
# 演示与编排层:按需创建智能体、挂 Goal、按能力边界分发任务、
# 智能体消息互通、RUNNING 超期自动重建智能体、Goal 看门狗。
# 诚实边界:任务通过统一工具运行时走真实业务管线(分解/试排/校验/报告),
# P2/P3 确认卡必须由人工审批;审批完成后恢复 Goal 分发。
# 落盘:<aps_home>/data/agent_mesh.json(可用 APS_MESH_PATH 覆盖)
# ============================================================
from __future__ import annotations
import asyncio
import json
import logging
import os
import tempfile
import threading
import time
import uuid
from datetime import datetime
from typing import Any
from server.timeutil import fmt_dt
logger = logging.getLogger("aps.mesh")
# ---------------- 角色模板(能力边界) ----------------
ROLE_TEMPLATES: dict[str, dict[str, Any]] = {
"data-analyst": {
"label": "数据解析智能体",
"capabilities": ["folder.analyze", "data.analyze", "master.query", "plan.trace"],
"desc": "解析工程目录/表格,校验排产齐备度",
},
"mrp-planner": {
"label": "MRP分解智能体",
"capabilities": ["order.decompose", "mrp.release"],
"desc": "按 BOM/供需净算产出采购/委外建议并下达",
},
"scheduler": {
"label": "柔性排产智能体",
"capabilities": ["flex.schedule", "flex.capacity", "flex.reschedule"],
"desc": "能力池组虚拟产线,闭环求解排产",
},
"verifier": {
"label": "结果校验智能体",
"capabilities": ["readiness.query", "conflict.list", "plan.trace"],
"desc": "复核排产版本、冲突、齐备度与计划追溯证据",
},
"reporter": {
"label": "报告智能体",
"capabilities": ["report.generate"],
"desc": "生成排产分析报告",
},
}
# 看门狗系统智能体(内置,不可删)
WATCHDOG_AGENT_ID = "agent-watchdog"
WATCHDOG_NAME = "Goal看门狗"
# Goal 模板:APS 排产全流程(任务带能力意图与依赖)
GOAL_TEMPLATES: dict[str, dict[str, Any]] = {
"aps-full-flow": {
"label": "APS 排产全流程",
"tasks": [
{"key": "analyze", "title": "解析工程目录数据", "intent": "data.analyze",
"role": "data-analyst", "params": {}, "dependsOn": []},
{"key": "readiness", "title": "复查排产齐备度", "intent": "readiness.query",
"role": "verifier", "params": {}, "dependsOn": ["analyze"]},
{"key": "decompose", "title": "执行 MRP 分解", "intent": "order.decompose",
"role": "mrp-planner", "params": {}, "dependsOn": ["readiness"]},
{"key": "schedule", "title": "生成一版试排", "intent": "flex.schedule",
"role": "scheduler", "params": {}, "dependsOn": ["decompose"]},
{"key": "verify", "title": "核对冲突与计划追溯", "intent": "conflict.list",
"role": "verifier", "params": {}, "dependsOn": ["schedule"]},
{"key": "report", "title": "生成排产分析报告", "intent": "report.generate",
"role": "reporter", "params": {"reportType": "plan"}, "dependsOn": ["verify"]},
],
},
}
_DEFAULT_STALL_TIMEOUT_SEC = 45.0
_DEFAULT_MAX_RETRIES = 1
def _now() -> str:
return fmt_dt(datetime.now())
def _uid(prefix: str) -> str:
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def _mesh_path() -> str:
override = os.environ.get("APS_MESH_PATH")
if override:
return override
try:
from server.aps_home import aps_home
return str(aps_home() / "data" / "agent_mesh.json")
except Exception:
return os.path.join("server", "data", "agent_mesh.json")
class MeshStore:
"""多智能体编排仓:线程安全 + 原子落盘。"""
def __init__(self, path: str | None = None) -> None:
self.path = path or _mesh_path()
self._lock = threading.RLock()
self.data: dict[str, Any] = self._load()
self._ensure_watchdog()
# ---------------- 持久化 ----------------
def _load(self) -> dict[str, Any]:
try:
with open(self.path, encoding="utf-8") as fh:
data = json.load(fh)
if isinstance(data, dict):
data.setdefault("agents", [])
data.setdefault("goals", [])
data.setdefault("messages", [])
data.setdefault("watchdog", {})
return data
except (OSError, json.JSONDecodeError):
pass
return {"agents": [], "goals": [], "messages": [], "watchdog": {}}
def save(self) -> None:
with self._lock:
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(self.path) or ".", suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
json.dump(self.data, fh, ensure_ascii=False, indent=1)
os.replace(tmp, self.path)
except OSError:
try:
os.unlink(tmp)
except OSError:
pass
def _ensure_watchdog(self) -> None:
with self._lock:
if not any(a.get("id") == WATCHDOG_AGENT_ID for a in self.data["agents"]):
self.data["agents"].insert(0, {
"id": WATCHDOG_AGENT_ID, "name": WATCHDOG_NAME, "role": "watchdog",
"label": "Goal看门狗(系统)", "capabilities": ["mesh.watchdog"],
"status": "IDLE", "system": True, "createdBy": "SYSTEM",
"createdAt": _now(), "heartbeatAt": _now(),
})
wd = self.data["watchdog"]
wd.setdefault("enabled", True)
wd.setdefault("stallTimeoutSec", _DEFAULT_STALL_TIMEOUT_SEC)
wd.setdefault("maxRetries", _DEFAULT_MAX_RETRIES)
wd.setdefault("lastTickAt", None)
wd.setdefault("alerts", [])
# ---------------- 智能体 ----------------
def create_agent(self, name: str, role: str, capabilities: list[str] | None = None,
*, created_by: str = "USER", parent_id: str | None = None,
goal_id: str | None = None) -> dict[str, Any]:
tpl = ROLE_TEMPLATES.get(role, {})
caps = capabilities if capabilities is not None else list(tpl.get("capabilities") or [])
agent = {
"id": _uid("agent"), "name": name.strip() or tpl.get("label") or role,
"role": role, "label": tpl.get("label") or role,
"capabilities": caps, "status": "IDLE", "system": False,
"createdBy": created_by, "parentId": parent_id,
"parentGoalId": goal_id,
"goalIds": [goal_id] if goal_id else [],
"createdAt": _now(), "heartbeatAt": _now(),
"currentTaskId": None,
}
with self._lock:
self.data["agents"].append(agent)
self.save()
return agent
def _capable_idle(self, intent: str, role: str | None = None) -> dict[str, Any] | None:
for agent in self.data["agents"]:
if agent.get("system"):
continue
if role and agent.get("role") != role:
continue
if agent.get("status") == "IDLE" and intent in (agent.get("capabilities") or []):
return agent
return None
def _agent_for_role(self, role: str | None) -> str | None:
if not role:
return None
agent = next(
(item for item in self.data["agents"]
if not item.get("system") and item.get("role") == role),
None,
)
return str(agent.get("id")) if agent else None
def _spawn_for_intent(self, intent: str, *, created_by: str, goal_id: str,
role: str | None = None) -> dict[str, Any] | None:
"""没有能执行该意图的空闲智能体时,按角色模板自动新建(允许新建智能体)。"""
for candidate_role, tpl in ROLE_TEMPLATES.items():
if role and candidate_role != role:
continue
if intent in (tpl.get("capabilities") or []):
seq = sum(1 for a in self.data["agents"] if a.get("role") == candidate_role) + 1
agent = self.create_agent(
f"{tpl['label']}-{seq}", candidate_role,
created_by=created_by, parent_id=None, goal_id=goal_id,
)
self.post_message(
from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal_id,
kind="INFO",
text=f"没有空闲的「{tpl['label']}」,已自动新建 {agent['name']} 接管 {intent}。",
)
return agent
return None
# ---------------- Goal / 任务 ----------------
def create_goal(self, title: str, *, template: str | None = "aps-full-flow",
tasks: list[dict[str, Any]] | None = None,
created_by: str = "USER", world_key: str | None = None,
session_id: str | None = None, project_id: str | None = None,
source_sha256: str | None = None,
run_id: str | None = None) -> dict[str, Any]:
tpl = GOAL_TEMPLATES.get(template or "", {})
raw_tasks = tasks if tasks is not None else tpl.get("tasks") or []
goal_id = _uid("goal")
run_id = str(run_id or _uid("run"))
task_rows = []
for spec in raw_tasks:
task_rows.append({
"id": _uid("task"), "goalId": goal_id,
"key": spec.get("key") or spec.get("intent"),
"title": spec.get("title") or spec.get("intent"),
"intent": spec["intent"], "role": spec.get("role"),
"params": dict(spec.get("params") or {}),
"dependsOn": list(spec.get("dependsOn") or []),
"status": "PENDING", "assigneeAgentId": None, "assigneeName": None,
"attempts": 0, "result": None, "error": None,
"startedAt": None, "finishedAt": None,
})
goal = {
"id": goal_id, "title": title.strip() or tpl.get("label") or "未命名 Goal",
"template": template, "status": "ACTIVE",
"createdBy": created_by, "worldKey": world_key,
"sessionId": session_id, "projectId": project_id,
"sourceSha256": source_sha256, "runId": run_id,
"createdAt": _now(), "tasks": task_rows,
"watchdog": {"stallTimeoutSec": float(self.data["watchdog"].get("stallTimeoutSec") or _DEFAULT_STALL_TIMEOUT_SEC),
"maxRetries": int(self.data["watchdog"].get("maxRetries") or _DEFAULT_MAX_RETRIES)},
}
with self._lock:
self.data["goals"].append(goal)
self.save()
sub_agents = self._ensure_goal_agents(goal)
goal["subAgentIds"] = [agent["id"] for agent in sub_agents]
self.save()
self.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal_id,
run_id=run_id, kind="INFO",
text=(f"Goal「{goal['title']}」已挂载(run {run_id}),"
f"已拉起 {len(sub_agents)} 个子智能体,"
f"{len(task_rows)} 个任务进入看门狗监控。"))
return goal
def _ensure_goal_agents(self, goal: dict[str, Any]) -> list[dict[str, Any]]:
"""Create one bounded sub-agent for every distinct task role in the Goal."""
roles = list(dict.fromkeys(
str(task.get("role") or "data-analyst") for task in goal.get("tasks") or []))
agents: list[dict[str, Any]] = []
for role in roles:
tpl = ROLE_TEMPLATES.get(role, {})
agent = next(
(item for item in self.data["agents"]
if not item.get("system") and item.get("role") == role
and goal["id"] in (item.get("goalIds") or [])),
None,
)
if agent is None:
seq = sum(1 for item in self.data["agents"] if item.get("role") == role) + 1
agent = self.create_agent(
f"{tpl.get('label') or role}-{seq}", role,
created_by=f"GOAL:{goal['id']}", goal_id=goal["id"],
)
elif goal["id"] not in (agent.get("goalIds") or []):
agent.setdefault("goalIds", []).append(goal["id"])
self.save()
agents.append(agent)
return agents
def get_goal(self, goal_id: str) -> dict[str, Any] | None:
return next((g for g in self.data["goals"] if g.get("id") == goal_id), None)
# ---------------- 消息总线 ----------------
def post_message(self, *, from_agent_id: str, to_agent_id: str | None,
goal_id: str | None, kind: str, text: str,
run_id: str | None = None,
evidence_refs: list[str] | None = None) -> dict[str, Any]:
msg = {
"id": _uid("msg"), "fromAgentId": from_agent_id,
"fromName": self._agent_name(from_agent_id),
"toAgentId": to_agent_id, "toName": self._agent_name(to_agent_id) if to_agent_id else "全体",
"goalId": goal_id, "runId": run_id, "kind": kind, "text": text,
"evidenceRefs": list(evidence_refs or []), "createdAt": _now(),
}
with self._lock:
self.data["messages"].append(msg)
self.data["messages"] = self.data["messages"][-300:]
self.save()
return msg
def _agent_name(self, agent_id: str | None) -> str:
if not agent_id:
return "?"
agent = next((a for a in self.data["agents"] if a.get("id") == agent_id), None)
return (agent or {}).get("name") or agent_id
# ---------------- 看门狗 ----------------
def watchdog_tick(self) -> dict[str, Any]:
"""扫描 RUNNING 超期任务:标记 STALLED → 自动重试/失败 → Goal 升级。幂等可频繁调用。"""
wd = self.data["watchdog"]
wd["lastTickAt"] = _now()
if not wd.get("enabled", True):
self.save()
return {"enabled": False, "stalled": 0}
stalled = 0
now = time.time()
for goal in self.data["goals"]:
if goal.get("status") not in ("ACTIVE", "ATTENTION"):
continue
gwd = goal.get("watchdog") or {}
timeout = float(gwd.get("stallTimeoutSec") or _DEFAULT_STALL_TIMEOUT_SEC)
max_retries = int(gwd.get("maxRetries") or _DEFAULT_MAX_RETRIES)
for task in goal.get("tasks") or []:
if task.get("status") != "RUNNING" or not task.get("startedAt"):
continue
try:
started = datetime.strptime(task["startedAt"], "%Y-%m-%d %H:%M:%S").timestamp()
except (ValueError, TypeError):
continue
if now - started <= timeout:
continue
stalled += 1
attempts = int(task.get("attempts") or 0)
assignee = task.get("assigneeName") or "未分配"
if attempts <= max_retries:
task["status"] = "PENDING"
task["startedAt"] = None
task["assigneeAgentId"] = None
task["assigneeName"] = None
self.post_message(
from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal["id"],
kind="ALERT",
text=f"任务「{task['title']}」在 {assignee} 上超过 {timeout:.0f}s 未完成,已收回并重新排队(第 {attempts} 次重试)。")
else:
task["status"] = "FAILED"
task["error"] = f"看门狗:超过 {timeout:.0f}s 未完成且重试耗尽"
task["finishedAt"] = _now()
goal["status"] = "ATTENTION"
self.post_message(
from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal["id"],
kind="ALERT",
text=f"任务「{task['title']}」重试 {max_retries} 次仍超时,标记 FAILED,Goal「{goal['title']}」升级为 ATTENTION。")
if stalled:
wd.setdefault("alerts", []).append({"at": _now(), "stalled": stalled})
wd["alerts"] = wd["alerts"][-50:]
self.save()
return {"enabled": True, "stalled": stalled, "lastTickAt": wd["lastTickAt"]}
def configure_watchdog(self, *, stall_timeout_sec: float | None = None,
max_retries: int | None = None, enabled: bool | None = None) -> dict[str, Any]:
wd = self.data["watchdog"]
if stall_timeout_sec is not None:
wd["stallTimeoutSec"] = max(5.0, float(stall_timeout_sec))
if max_retries is not None:
wd["maxRetries"] = max(0, int(max_retries))
if enabled is not None:
wd["enabled"] = bool(enabled)
self.save()
return wd
# ---------------- 快照 ----------------
def reset(self) -> dict[str, Any]:
"""恢复起始状态:清空 Goal/消息/告警,只保留系统看门狗智能体。"""
with self._lock:
self.data["agents"] = [a for a in self.data["agents"] if a.get("system")]
self.data["goals"] = []
self.data["messages"] = []
self.data["watchdog"]["alerts"] = []
self.save()
return self.snapshot()
def snapshot(self) -> dict[str, Any]:
with self._lock:
return json.loads(json.dumps({
"agents": self.data["agents"],
"goals": self.data["goals"],
"messages": self.data["messages"][-100:],
"watchdog": self.data["watchdog"],
"roleTemplates": [
{"role": role, **{k: v for k, v in tpl.items()}}
for role, tpl in ROLE_TEMPLATES.items()
],
"goalTemplates": [
{"key": key, "label": tpl["label"], "taskCount": len(tpl["tasks"])}
for key, tpl in GOAL_TEMPLATES.items()
],
}))
_MESH: MeshStore | None = None
_MESH_LOCK = threading.Lock()
def get_mesh() -> MeshStore:
global _MESH
with _MESH_LOCK:
if _MESH is None:
_MESH = MeshStore()
return _MESH
# ---------------- 分发执行(真实业务管线) ----------------
_DISPATCH_THREADS: dict[str, threading.Thread] = {}
def dispatch_goal(goal_id: str, *, session_id: str | None = None) -> dict[str, Any]:
"""异步分发 Goal:按依赖序把任务分给有能力的空闲智能体,必要时自动新建。"""
mesh = get_mesh()
goal = mesh.get_goal(goal_id)
if goal is None:
raise ValueError(f"Goal 不存在:{goal_id}")
# Goal 绑定的项目世界与当前活动世界不一致时先切换,保证任务打在正确的数据边界上
goal_world = (goal.get("worldKey") or "").strip()
if goal_world:
try:
from server.state.store import get_store, switch_store
if getattr(get_store(), "world_key", "default") != goal_world:
switch_store(goal_world)
except Exception as exc: # noqa: BLE001 - 项目边界不可用必须失败关闭
logger.exception("mesh: switch store to %s failed", goal_world)
raise RuntimeError(f"Goal 绑定的项目世界不可用:{goal_world}") from exc
eff_session = session_id or (goal.get("sessionId") or "") or f"mesh-{goal_id}"
try:
from server.auth.context import get_identity
identity = get_identity()
except Exception: # noqa: BLE001 - 无请求身份时用系统兜底
identity = None
running = DispatchThread(goal_id=goal_id, session_id=eff_session,
world_key=goal_world or None, identity=identity)
if goal_id in _DISPATCH_THREADS and _DISPATCH_THREADS[goal_id].is_alive():
return {"message": "该 Goal 已在分发执行中", "goalId": goal_id, "alreadyRunning": True}
_DISPATCH_THREADS[goal_id] = running
running.start()
return {"message": f"Goal「{goal['title']}」已开始分发执行", "goalId": goal_id, "alreadyRunning": False}
def resume_goal_after_confirmation(
confirm_id: str, *, approve: bool, actor: str,
) -> dict[str, Any]:
"""把人工审批结果回写到 Mesh,并只在批准后恢复 Goal 分发。
该函数只消费 Mesh 自己登记过的 confirmationId;普通业务确认卡不会命中,
因此不会改变既有审批路径。重复调用通过任务内确认历史实现幂等。
"""
requested = str(confirm_id or "").strip()
if not requested:
return {"matched": False, "resolved": False, "resumed": False}
mesh = get_mesh()
actor_name = str(actor or "unknown")
matched_goal: dict[str, Any] | None = None
matched_task: dict[str, Any] | None = None
with mesh._lock:
for goal in mesh.data.get("goals") or []:
for task in goal.get("tasks") or []:
pending_ids = [str(item) for item in (task.get("confirmationIds") or [])]
resolutions = task.get("confirmationResolutions") or []
if requested in pending_ids:
if task.get("status") != "AWAITING_APPROVAL":
return {
"matched": True,
"resolved": False,
"resumed": False,
"reason": "TASK_NOT_AWAITING_APPROVAL",
"goalId": goal.get("id"),
"taskId": task.get("id"),
"taskStatus": task.get("status"),
}
matched_goal, matched_task = goal, task
break
if any(str(item.get("confirmId") or "") == requested for item in resolutions):
return {
"matched": True,
"resolved": True,
"resumed": False,
"reason": "ALREADY_RESOLVED",
"goalId": goal.get("id"),
"taskId": task.get("id"),
"taskStatus": task.get("status"),
"alreadyRunning": bool(
goal.get("id") in _DISPATCH_THREADS
and _DISPATCH_THREADS[goal["id"]].is_alive()
),
}
if matched_task is not None:
break
if matched_goal is None or matched_task is None:
return {"matched": False, "resolved": False, "resumed": False}
now = _now()
matched_task["confirmationIds"] = []
matched_task["finishedAt"] = now
resolutions = matched_task.setdefault("confirmationResolutions", [])
resolutions.append({
"confirmId": requested,
"approve": bool(approve),
"actor": actor_name,
"at": now,
})
if approve:
matched_task["status"] = "DONE"
matched_task["result"] = f"人工已批准 {requested} by {actor_name}"
matched_task["error"] = None
matched_goal["status"] = "ACTIVE"
mesh.post_message(
from_agent_id=WATCHDOG_AGENT_ID,
to_agent_id=mesh._agent_for_role(matched_task.get("role")),
goal_id=matched_goal.get("id"),
run_id=matched_goal.get("runId"),
kind="RESPONSE",
evidence_refs=[f"confirm:{requested}"],
text=(f"人工批准 {requested}({actor_name}),任务"
f"「{matched_task.get('title')}」已完成,Goal 继续分发。"),
)
else:
matched_task["status"] = "FAILED"
matched_task["error"] = f"人工驳回 {requested} by {actor_name}"
matched_goal["status"] = "ATTENTION"
mesh.post_message(
from_agent_id=WATCHDOG_AGENT_ID,
to_agent_id=None,
goal_id=matched_goal.get("id"),
run_id=matched_goal.get("runId"),
kind="ALERT",
evidence_refs=[f"confirm:{requested}"],
text=(f"人工驳回 {requested}({actor_name}),任务"
f"「{matched_task.get('title')}」已失败,Goal 停止分发。"),
)
mesh.save()
if not approve:
return {
"matched": True,
"resolved": True,
"resumed": False,
"goalId": matched_goal.get("id"),
"taskId": matched_task.get("id"),
"taskStatus": "FAILED",
}
dispatch_result = dispatch_goal(
str(matched_goal["id"]), session_id=matched_goal.get("sessionId"),
)
already_running = bool(dispatch_result.get("alreadyRunning"))
return {
"matched": True,
"resolved": True,
"resumed": True,
"started": not already_running,
"alreadyRunning": already_running,
"goalId": matched_goal.get("id"),
"taskId": matched_task.get("id"),
"taskStatus": "DONE",
"dispatch": dispatch_result,
}
class DispatchThread(threading.Thread):
def __init__(self, *, goal_id: str, session_id: str, world_key: str | None = None,
identity: Any = None) -> None:
super().__init__(daemon=True, name=f"mesh-dispatch-{goal_id}")
self.goal_id = goal_id
self.session_id = session_id
self.world_key = world_key
self.identity = identity
def run(self) -> None: # noqa: D102
mesh = get_mesh()
goal = mesh.get_goal(self.goal_id)
if goal is None:
return
token = None
if self.identity is not None:
try:
from server.auth.context import bind_identity
token = bind_identity(self.identity) # 项目仓按用户隔离:绑回创建时身份
except Exception: # noqa: BLE001
token = None
try:
while True:
task = self._next_ready(goal)
if task is None:
break
self._run_task(mesh, goal, task)
self._finish_goal(mesh, goal)
except Exception as exc: # noqa: BLE001 - 编排兜底:任何异常落 Goal,不炸线程
logger.exception("mesh dispatch failed")
goal["status"] = "ATTENTION"
mesh.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None,
goal_id=self.goal_id, kind="ALERT",
text=f"分发执行异常:{exc}")
mesh.save()
finally:
if token is not None:
try:
from server.auth.context import reset_identity
reset_identity(token)
except Exception: # noqa: BLE001
pass
def _next_ready(self, goal: dict[str, Any]) -> dict[str, Any] | None:
done_keys = {t["key"] for t in goal["tasks"] if t.get("status") == "DONE"}
for task in goal["tasks"]:
if task.get("status") == "PENDING" and all(dep in done_keys for dep in task.get("dependsOn") or []):
return task
# 还有 RUNNING(看门狗收回中会重新 PENDING)则等待
if any(t.get("status") == "RUNNING" for t in goal["tasks"]):
time.sleep(1.0)
return self._next_ready(goal)
return None
def _run_task(self, mesh: MeshStore, goal: dict[str, Any], task: dict[str, Any]) -> None:
intent = task["intent"]
role = task.get("role")
agent = mesh._capable_idle(intent, role) or mesh._spawn_for_intent(
intent, created_by=f"GOAL:{goal['id']}", goal_id=goal["id"], role=role)
if agent is None:
task["status"] = "FAILED"
task["error"] = f"没有任何角色模板能执行 {intent}"
task["finishedAt"] = _now()
goal["status"] = "ATTENTION"
mesh.save()
return
task["status"] = "RUNNING"
task["assigneeAgentId"] = agent["id"]
task["assigneeName"] = agent["name"]
task["attempts"] = int(task.get("attempts") or 0) + 1
task["startedAt"] = _now()
agent["status"] = "RUNNING"
agent["currentTaskId"] = task["id"]
agent["heartbeatAt"] = _now()
mesh.save()
mesh.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=agent["id"],
goal_id=goal["id"], run_id=goal.get("runId"), kind="REQUEST",
text=f"分发任务「{task['title']}」({intent}),第 {task['attempts']} 次尝试。")
try:
text, blocks = _execute_intent(self.session_id, intent, task.get("params") or {}, agent,
world_key=self.world_key, goal=goal, task=task)
pending_cards = [
str((block.get("props") or {}).get("confirmId"))
for block in blocks or []
if (block.get("props") or {}).get("confirmId")
]
if pending_cards:
task["status"] = "AWAITING_APPROVAL"
task["confirmationIds"] = pending_cards
task["result"] = (text or "")[:600]
task["finishedAt"] = None
agent["status"] = "IDLE"
agent["currentTaskId"] = None
agent["heartbeatAt"] = _now()
goal["status"] = "ATTENTION"
mesh.save()
mesh.post_message(
from_agent_id=agent["id"], to_agent_id=WATCHDOG_AGENT_ID,
goal_id=goal["id"], run_id=goal.get("runId"), kind="ALERT",
evidence_refs=[f"confirm:{cid}" for cid in pending_cards],
text=(f"「{task['title']}」需要人工审批,执行智能体不会自动批准。"
f"待确认:{', '.join(pending_cards)}"),
)
return
# 排产/分解被阻断(fail-closed 文案)不算任务成功:落 FAILED 交看门狗升级
if "没有可下达的草稿建议单" in text and _released_supply_exists(self.world_key):
text = "供应建议单此前已下达(RELEASED 在账),本次幂等跳过。"
elif "被阻断" in text or "无法生成" in text or "没有可下达" in text:
raise RuntimeError(text.strip().splitlines()[0][:200])
task["status"] = "DONE"
task["result"] = (text or "")[:600]
task["finishedAt"] = _now()
agent["status"] = "IDLE"
agent["currentTaskId"] = None
agent["heartbeatAt"] = _now()
mesh.save()
next_task = self._peek_next_task(goal, task)
nxt = (next_task or {}).get("title")
summary_line = next(
(ln for ln in reversed((text or "").splitlines()) if ln.strip()), "")
next_agent = mesh._agent_for_role((next_task or {}).get("role"))
mesh.post_message(from_agent_id=agent["id"], to_agent_id=next_agent,
goal_id=goal["id"], run_id=goal.get("runId"), kind="RESPONSE",
evidence_refs=[f"mesh-task:{task['id']}"],
text=f"「{task['title']}」完成 ✅ {summary_line[:80]}"
+ (f",@{nxt} 请接棒。" if nxt else ",Goal 任务全部就绪。"))
except Exception as exc: # noqa: BLE001 - 任务失败落状态,交给看门狗/重试
logger.exception("mesh task failed: %s", intent)
task["status"] = "FAILED"
task["error"] = str(exc)[:300]
task["finishedAt"] = _now()
agent["status"] = "IDLE"
agent["currentTaskId"] = None
goal["status"] = "ATTENTION"
mesh.save()
mesh.post_message(from_agent_id=agent["id"], to_agent_id=WATCHDOG_AGENT_ID,
goal_id=goal["id"], run_id=goal.get("runId"), kind="ALERT",
text=f"「{task['title']}」执行失败:{exc}")
@staticmethod
def _peek_next_task(goal: dict[str, Any], just_done: dict[str, Any]) -> dict[str, Any] | None:
done_keys = {t["key"] for t in goal["tasks"] if t.get("status") == "DONE"}
for task in goal["tasks"]:
if task.get("status") == "PENDING" and all(dep in done_keys for dep in task.get("dependsOn") or []):
return task
return None
def _finish_goal(self, mesh: MeshStore, goal: dict[str, Any]) -> None:
statuses = {t.get("status") for t in goal["tasks"]}
if statuses and statuses <= {"DONE"}:
goal["status"] = "ACHIEVED"
mesh.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None,
goal_id=goal["id"], kind="INFO",
text=f"Goal「{goal['title']}」全部任务完成 ✅ 看门狗结案。")
elif goal.get("status") != "ATTENTION":
goal["status"] = "ATTENTION"
mesh.save()
def _execute_intent(session_id: str, intent: str, params: dict[str, Any],
agent: dict[str, Any], *, world_key: str | None = None,
goal: dict[str, Any] | None = None,
task: dict[str, Any] | None = None) -> tuple[str, list[dict[str, Any]]]:
"""通过真实业务管线执行意图(与对话同一入口 handle_intent)。
world_key 指定时先切到 Goal 绑定的项目世界:全局活动世界可能被
前端工作区同步抢占,必须在每个任务执行当下重新钉住数据边界。
"""
from server.agent_core.tool_runtime import run_tool_async
from server.contracts import IntentResult
from server.state.store import get_store, switch_store
if world_key and getattr(get_store(), "world_key", "default") != world_key:
switch_store(world_key)
store = get_store()
enriched = dict(params or {})
if goal:
enriched["_mesh"] = {
"goalId": goal.get("id"),
"runId": goal.get("runId"),
"worldKey": goal.get("worldKey"),
"sessionId": goal.get("sessionId"),
"sourceSha256": goal.get("sourceSha256"),
"projectId": goal.get("projectId"),
"fromAgentId": agent.get("id"),
"taskId": (task or {}).get("id"),
}
reply = asyncio.run(run_tool_async(
store, session_id,
IntentResult(intent=intent, params=enriched, confidence=1.0, source="LLM"),
actor=f"mesh:{agent['name']}",
))
blocks = [b.model_dump() for b in (reply.blocks or [])]
return reply.text or "", blocks
# ---------------- 看门狗后台循环 ----------------
_WATCHDOG_STARTED = False
def start_watchdog_loop(interval_sec: float = 3.0) -> None:
"""启动看门狗后台节拍(幂等)。桌面/服务启动时调用一次。"""
global _WATCHDOG_STARTED
if _WATCHDOG_STARTED:
return
_WATCHDOG_STARTED = True
def _loop() -> None:
while True:
try:
get_mesh().watchdog_tick()
except Exception: # noqa: BLE001 - 看门狗自身绝不能崩
logger.exception("mesh watchdog tick failed")
time.sleep(interval_sec)
threading.Thread(target=_loop, daemon=True, name="mesh-watchdog").start()
def _released_supply_exists(world_key: str | None) -> bool:
"""目标世界是否已有非草稿(已下达)采购/委外单:幂等判断依据。"""
try:
from server.state.store import get_store, switch_store
if world_key and getattr(get_store(), "world_key", "default") != world_key:
switch_store(world_key)
data = get_store().data
for row in (data.get("purchaseOrders") or []) + (data.get("outsourceOrders") or []):
if isinstance(row, dict) and row.get("status") and row.get("status") != "DRAFT":
return True
except Exception: # noqa: BLE001 - 判断失败保守返回 False(维持失败语义)
pass
return False