You need to sign in before continuing.
role-package-abstraction-design.md 10.4 KB

统一 Role Package 抽象层——详细设计(SDD)

日期: 2026-08-06 性质: 详细设计,对应需求文档 docs/done/role-package-abstraction-requirements.md(条目 H,需求评审已通过,见 docs/todo/graph-engineering-open-items.md H 节的评审记录)。回答该需求文档第 8 节的开放问题。 范围确认(用户 2026-08-06):FR-3 的 4 张 CHECK 约束表一次性全改,不分阶段。 状态: 设计草案,实施中。

1. Role Package 载体格式:JSON,不是 YAML

需求文档 FR-2 的开放问题是"单文件 vs 目录约定",但更根本的约束是 05 §5.7 的纯 stdlib 限制——graphlemon 自己的代码不能依赖 PyYAML。skills/<name>/SKILL.md 现有的 YAML frontmatter 之所以能用,是因为graphlemon 自己的代码从来没有解析过它(skill_query.py/skill_update.py 只把 SKILL.md 当不透明文本读写,真正解析 skills:/tools: frontmatter 的是 Claude Code 自己,不是这个仓库的 Python 代码)。Role Package 不一样:agent_profile.py 需要真正读懂它的结构化字段(哪些 skills、哪些 tools、memory 配置……),手写一个 YAML 子集解析器风险高、容易在多行字符串/嵌套列表这类边界情况上出 bug——纯 stdlib 的 json 模块能零风险做到同样的事。

决定:roles/<role_id>/role.json,纯 JSON,json.load 直接解析。

2. role.json 是"引用清单",不是内容复制

需求文档 FR-1 自己的措辞已经暗示了这一点——"context(memory 配置 + 运行时 context 组装规则的引用)"、"skills(对应现有 skills: frontmatter)"、"acceptance criteria(引用)"——七个维度里大多数本来就该是指针,不是把内容整个复制一份。

具体到 role/identity 维度:agents/<role>/SOUL.md(+ validator 的 AGENTS.md)原样保留,不删除、不合并、不改名——role.json 只存这两个文件的相对路径引用(identity.soul_md_path/identity.agents_md_path)。这个决定直接让验收标准 2("迁移后部署产物逐字节一致")几乎自动成立:render_agent_definition() 的 body 拼接逻辑(SOUL.md 内容 + AGENTS.md 内容)完全不变,唯一变化的是 frontmatter 字段的来源从硬编码 FRONTMATTER 字典变成解析 role.json。

{
  "role_id": "validator",
  "description": "graphlemon Validator: independently verifies a task, PASS/FAIL/INCONCLUSIVE, read-only on source.",
  "migrated_from_profile": "qa",
  "identity": {
    "soul_md_path": "agents/validator/SOUL.md",
    "agents_md_path": "agents/validator/AGENTS.md"
  },
  "context": { "memory": "project" },
  "model": "claude-sonnet-5",
  "skills": ["qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph", "unknown-scope-defect-sweep"],
  "applications": { "mcp_servers": [] },
  "workflows": { "dynamic_planning": "generic" },
  "permissions": {
    "tools": ["Read", "Bash", "Grep", "Glob"],
    "disallowed_tools": ["Edit", "Write"],
    "notes": [
      { "constraint": "不提交、推送、reset、清理或覆盖用户工作树", "enforced": "prose_only" }
    ]
  },
  "acceptance_criteria": { "ref": "docs/sdd/06-验收与测试锚点.md" }
}
  • applications(FR-6,"应该"):mcp_servers 列表,允许为空——满足"这个维度存在、字段长什么样、允许为空",不要求真实集成(非目标 #4)。
  • workflows:接 E 的产出(非目标 #2 明确不重复设计 E 已经做的事,这里只是一个指向枚举)。planner 是 "canonical_doc_chain"(work_graph_plan.py 里 org_node == "planner" 走的固定链),coder/validator 是 "generic"(E 的 _GENERIC_PROMPT_TEMPLATE/generic_node_prompt_builder 路径)。
  • permissions.notes(验收标准 4):Validator 的 Bash 子命令限定失效那条已知漏洞,诚实标注 "enforced": "prose_only",不假装是工具层强制——非目标 #3 明确不在本文档修复这个漏洞本身,只要求如实反映现状。

3. agent_profile.py 改造:ORG_NODES 从硬编码元组变成目录发现

_ROLES_SOURCE_DIR = os.path.join(_REPO_ROOT, "roles")

def _discover_role_ids(roles_dir=None):
    """扫描 roles/<role_id>/role.json 是否存在,role_id 就是目录名。
    roles_dir 可覆盖(测试用真实文件系统 fixture 验证'新增角色不用改代码'
    这条验收标准,不需要真的往这个仓库的 roles/ 目录里塞测试数据)。"""
    roles_dir = roles_dir or _ROLES_SOURCE_DIR
    if not os.path.isdir(roles_dir):
        return ()
    return tuple(sorted(
        name for name in os.listdir(roles_dir)
        if os.path.isfile(os.path.join(roles_dir, name, "role.json"))
    ))

ORG_NODES = _discover_role_ids()

FRONTMATTER/_DESCRIPTIONS/_MIGRATED_FROM_PROFILE 三个硬编码字典全部删除,替换成 _load_role(role_id, roles_dir=None)(json.load 解析对应 role.json)。render_agent_definition()/deploy_agent_definitions()/record_agent_profiles() 都接受一个可选 roles_dir 参数(默认 None 即用真实目录),这是验收标准 1 能被自动化测试验证的关键——测试往一个临时目录写一个新角色的 role.json,不改任何 Python 代码或 roles_dir 之外的任何东西,直接调用这三个未经修改的函数,确认新角色能被发现、渲染、部署。

ORG_NODES 这个名字保留不改(org_node_trigger.py 等外部模块已经在用 from src.agent_profile import ORG_NODES,改名会不必要地扩大改动面),但注释说明它现在是数据驱动的,不再是硬编码元组。

4. Schema:4 张表的 CHECK 约束怎么改

决定的形状:CHECK (x IN ('planner', 'coder', 'validator')) → CHECK (x != '')——仍然是数据库层面的最小完整性保证(非空字符串),但不再枚举具体值。不是完全去掉 CHECK(那样连"总不能是空字符串"这点基本保证都没有了),也不是新建一张 role 表做外键(需求文档 §8 留的另一个选项)——本设计选择更简单的那条路径:4 张表全改,但改的目标是"放开枚举"而不是"建立引用完整性",避免这次改动同时背上"外键完整性设计"这个新的复杂度。

真实的改动面(这是这次评审最重要的发现,比需求文档原始风险描述更细):task/work_graph_run/skill 三张表被 work_node_execution/artifact/bug_record/execution_event 等多张表通过外键引用;agent_profile 没有任何表引用它。SQLite 不支持 ALTER TABLE ... DROP CONSTRAINT,改 CHECK 约束必须用"建新表复制数据删旧表改名"的标准操作——但因为改的是父表、外键定义本身(REFERENCES task(task_id) 这类文本)不需要跟着变,所以只需要重建这 4 张父表本身,不需要动任何一张子表。SQLite 官方文档给的正确流程("Making Other Kinds Of Table Schema Changes")是:

  1. PRAGMA foreign_keys = OFF(关闭本连接的外键校验,只在这次迁移期间)
  2. 开事务
  3. 对每张表:建同结构、新 CHECK 的 <table>_new → INSERT INTO <table>_new SELECT * FROM <table> → DROP TABLE <table> → ALTER TABLE <table>_new RENAME TO <table> → 重建这张表原有的索引
  4. PRAGMA foreign_key_check 确认没有产生悬空外键
  5. 提交事务
  6. PRAGMA foreign_keys = ON

migrate_relax_org_node_constraints(conn) 一个函数按这个流程处理全部 4 张表(一次性开一个事务,不是 4 个独立迁移各自开关 foreign_keys),每张表重建前用 sqlite_master 里的 CREATE TABLE 文本判断是否已经是新约束,已经是就跳过(幂等,migrate_expand_artifact_types 的同款前置检查模式)。SCHEMA_SQL 里的 4 处 CHECK (... IN ('planner', 'coder', 'validator')) 直接改成 CHECK (... != ''),新数据库不需要迁移。

索引重建清单(重建表之后必须重新建的,DROP TABLE 会连带丢失表自己的索引):

  • skill:无额外索引。
  • task:idx_task_status、idx_task_assigned_org_node、idx_task_project_id。
  • work_graph_run:ux_work_graph_run_active(分区唯一索引,D 已经依赖这条索引的语义,重建后必须原样保留)。
  • agent_profile:PK 本身即索引,无需额外重建。

5. work_graph_run_query.py::_VALID_ORG_NODES(评审时发现的遗漏依赖)

改成直接引用 agent_profile.ORG_NODES,不再自己维护一份硬编码三元组——这条不在需求文档原始 FR 列表里,是评审阶段核实代码时发现的真实遗漏(docs/todo/graph-engineering-open-items.md H 节评审记录已经点名),顺手一起改掉。

6. 明确不做的部分(如实记录,呼应评审阶段的结论)

  • frontend/app.js 的 4 处硬编码三角色枚举(路由正则、ORG_NODE_ORDER 排序数组、任务创建表单下拉、byOrgNode 查找对象)——评审阶段已经确认这些不在验收标准 1 的范围内(验收标准 1 明确"这个新角色不需要真的接入 Org Graph 执行"),本轮不改。新增一个角色之后,PH3 控制台依然认不出它,这是本轮已知、刻意保留的缺口,不是遗漏。
  • role 表 + 外键完整性(§4 提到的另一个选项)——本轮用更简单的"放开枚举"实现,不建立真正的引用完整性约束。
  • org_node_trigger.py 校验逻辑本身——它已经用 from src.agent_profile import ORG_NODES 做校验,ORG_NODES 变成数据驱动之后这条校验自动跟着数据驱动,不需要改这个文件。
  • permissions 维度的 Bash 子命令限定漏洞本身——诚实标注(§2),不修复(需求文档非目标 #3)。

7. 回答需求文档第 8 节开放问题

  • 载体格式:单文件 role.json(不是目录多文件),JSON 不是 YAML(§1)。
  • CHECK 约束改造方式:放开成 != '',不建 role 表外键(§4)。
  • applications 最小定义:{"mcp_servers": []}(§2)。
  • 迁移步骤:三个角色各自的 role.json 直接引用现有 agents/<role>/SOUL.md/AGENTS.md,不复制内容(§2),验证方式是 test_agent_profile.py 现有测试断言部署产物不变。
  • Role Package 跟 E 的接口:workflows.dynamic_planning 字段,"canonical_doc_chain" / "generic" 二选一(§2)。