feat: unify coordinator semantics (Muse role alignment phase 1)
Platform coordinator.py is the single source of truth; the skill coordinator template (templates/roles/00-coordinator.md + SKILL.md appendix 00 + body) now derives from it instead of describing a second, model-played coordinator (U1-U13). - server: _create_task accepts parent_task_id (404/400 checks, coordinator-under-coordinator rejected); follow-ups to a running coordinator return 409 at entry (_create_followup / _deliver_to_thread) with documented alternatives (U14/U15). - tests: 8 new coordinator tests (depth limit, lineage, replay, follow-up 409); appendix sync/provenance/transplant suites green. - check_appendix.py: 12 chapters match templates/roles/.
This commit is contained in:
1 parent
834eced686
commit
f6df76ee8f
5 files changed
+308
-28
No files matched your search
+41
-1
@@ -3812,6 +3812,31 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
"error": "协调员任务不跑模型回合,不能与"
|
||||
" goal/plan/验收命令同用"})
|
||||
return
|
||||
# 父任务血缘(Muse 角色对齐 P1):创建请求可携带
|
||||
# parent_task_id;父任务是协调员且本次新建也是协调员
|
||||
# 时拒绝(深度恒 ≤1)。父不存在 404、跨项目 400。
|
||||
# 不带 coordinator_parts 时 parent_task_id 仅作血缘记录
|
||||
# (第二阶段旁聊的父子关系用同一入参)。
|
||||
parent_task_id = body.get("parent_task_id")
|
||||
if parent_task_id is not None:
|
||||
if not isinstance(parent_task_id, str):
|
||||
self._send_json(400, {
|
||||
"error": "parent_task_id must be a string"})
|
||||
return
|
||||
parent_row = self._fetch_task(parent_task_id)
|
||||
if parent_row is None:
|
||||
self._send_json(404, {"error": "parent task not found"})
|
||||
return
|
||||
parent_task = _task_to_dict(parent_row)
|
||||
if parent_task["project_id"] != project_id:
|
||||
self._send_json(400, {
|
||||
"error": "parent task belongs to another project"})
|
||||
return
|
||||
if coord_parts and parent_task.get("coordinator_parts"):
|
||||
self._send_json(400, {
|
||||
"error": "coordinator task cannot be created"
|
||||
" under a coordinator task"})
|
||||
return
|
||||
idempotency_key = body.get("idempotency_key")
|
||||
if idempotency_key is not None and not isinstance(idempotency_key, str):
|
||||
self._send_json(400, {"error": "idempotency_key must be a string"})
|
||||
@@ -4031,7 +4056,7 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
model, effort, approval, plan, goal_mode,
|
||||
environment_id,
|
||||
json.dumps(coord_parts) if coord_parts else None,
|
||||
None, None, child_model, child_effort,
|
||||
parent_task_id, None, child_model, child_effort,
|
||||
agent_name_val, agent_version_val,
|
||||
agent_instructions_val, agent_tools_val,
|
||||
agent_tool_approvals_val, audit_loop_val),
|
||||
@@ -4440,6 +4465,15 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
return
|
||||
if self._reject_if_over_quota_for_task(user_id, task):
|
||||
return
|
||||
# 协调员任务不接受后续消息(Muse 角色对齐 P1):写入侧
|
||||
# 会 201 成功、消费侧没有任何读点,宁可入口 409 明拒。
|
||||
# 改分工请取消后重建协调员;给分工追加指令请直接发给
|
||||
# 该子任务。
|
||||
if task.get("coordinator_parts"):
|
||||
self._send_json(409, {
|
||||
"error": "coordinator tasks do not accept"
|
||||
" follow-up messages"})
|
||||
return
|
||||
text, mode, body_err = _validate_followup_body(body)
|
||||
if body_err is not None:
|
||||
self._send_json(body_err[0], body_err[1])
|
||||
@@ -5922,6 +5956,12 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
|
||||
(http_code, payload),不直接 _send_json;配额检查
|
||||
由调用方在调本方法前做。"""
|
||||
task_id = task["id"]
|
||||
# 协调员任务不接受后续消息(Muse 角色对齐 P1):与
|
||||
# _create_followup 同款入口 409,见该处注释。
|
||||
if task.get("coordinator_parts"):
|
||||
return 409, {
|
||||
"error": "coordinator tasks do not accept"
|
||||
" follow-up messages"}
|
||||
goal_target = _parse_goal_command(text)
|
||||
# /goal 前置判定见 _classify_goal_followup(纯计算,与
|
||||
# _create_followup 共用,但只用于 goal_target 非空时:
|
||||
|
||||
Reference in new issue
Block a user