统一配置资产模型(Unified Config Asset Model)— P0.3 设计文档
Date: 2026-07-07 Author: zongzheng (with Claude Code) Status: Draft for review(D2/D3 两个决策待评审确认) Ancestry: master plan v2 §三 · PRD prd-specialist-value-output R1–R3/R6 · ADR-020 · ADR-030 · ADR-033 L0/L1 · ADR-034 · ADR-035(Proposed)· ADR-037(Proposed) Gates: Phase 1(P1.1 底座实体)· Phase 2(资产接入)· Phase 3(统一装配)
1. 目标与非目标
目标:为五种 Specialist 配置资产(Soul / Skill / KB golden answer / KB domain rule / tool binding )定义一套存储模型、版本模型、发布状态机与校验管道——版本、审计、回滚、eval 门禁、archetype 继承各实现一次。
非目标:
- 不做通用配置平台:底座只为这五种已知 asset_type 设计;第六种类型出现前不加扩展点。
- KB 文档本体(K1/K2 上传文档、Haystack 索引、ADR-008 管道)不迁入——只有策展型资产进底座。
- 角色模板(
agent/prompts/<key>.txt,ADR-034 4-key closed union)本期不作为资产类型——装配器把它当第 0 层输入(见 §6);是否最终被 skills 吸收是 PRD Q3,Phase 2 后重估。 - eval 门禁的 harness/runner 实现(Phase 4,P4.1)——本文档只预留挂点。
2. 底座数据模型
2.1 specialist_config_assets(资产主表)
ADR-020 隔离矩阵行(实体 docstring 必须原样声明):
org_instance行属 row-1 类(Org × Specialist)——任何查询必须同时过滤org_idANDspecialist_id;catalog行是平台资产(SuperAdmin 面),org_id IS NULL,只经 service 层显式 catalog 分支读取,绝不混入租户查询结果。RLS 全面下沉前(issue #3),隔离在 service-query 层强制。
| 列 | 类型 | 约束/默认 | 注释 |
|---|---|---|---|
id | uuid | PK, gen_random_uuid() | |
asset_type | varchar(32) | NOT NULL, CHECK IN (soul,skill,kb_golden_answer,kb_domain_rule,tool_binding) | 闭合枚举,加类型走 migration |
scope | varchar(16) | NOT NULL, CHECK IN (catalog,org_instance) | |
org_id | uuid | NULL, FK→organizations | catalog 行必须 NULL;org_instance 必须 NOT NULL(CHECK 见下) |
specialist_id | uuid | NOT NULL, FK→specialists | catalog 行指向 archetype Specialist(specialists 表已有 catalog 字段) |
slug | varchar(64) | NOT NULL | kebab-case,作用域内唯一(见索引) |
display_name | varchar(128) | NOT NULL | |
archetype_asset_id | uuid | NULL, FK→self | 实例的原型血缘(§7);catalog 行恒 NULL |
published_version_id | uuid | NULL, FK→versions(延迟约束) | 当前生效版本指针;NULL=从未发布 |
draft_version_id | uuid | NULL, FK→versions | 当前唯一在编草稿指针;同一资产同时至多一个 draft |
source | varchar(24) | NOT NULL DEFAULT manual, CHECK IN (manual,imported_repo,loop_proposal) | loop_proposal = R4.4 提案(ADR-037) |
metadata | jsonb | NULL | 资产级注记:物化来源 hash(materializedFromHash,§7)、导入来源路径等;不进装配、不进 content hash |
created_by | uuid | NOT NULL | user id |
created_at / updated_at | timestamptz | NOT NULL DEFAULT now() | |
archived_at | timestamptz | NULL | 软删;检索/装配一律过滤 archived_at IS NULL |
CHECK 约束(scope-shape 耦合,防止"半 catalog"行):
CHECK ( (scope = 'catalog' AND org_id IS NULL AND archetype_asset_id IS NULL)
OR (scope = 'org_instance' AND org_id IS NOT NULL) )
索引:
-- 作用域内 slug 唯一(两条部分唯一索引,因 org_id 可 NULL)
CREATE UNIQUE INDEX uq_config_assets_instance_slug
ON specialist_config_assets (asset_type, org_id, specialist_id, slug)
WHERE scope = 'org_instance' AND archived_at IS NULL;
CREATE UNIQUE INDEX uq_config_assets_catalog_slug
ON specialist_config_assets (asset_type, specialist_id, slug)
WHERE scope = 'catalog' AND archived_at IS NULL;
-- 装配热查询:某 (org, specialist) 的全部生效资产
CREATE INDEX idx_config_assets_lookup
ON specialist_config_assets (org_id, specialist_id, asset_type)
WHERE archived_at IS NULL;
-- 血缘反查("upstream changed?" 扫描)
CREATE INDEX idx_config_assets_archetype
ON specialist_config_assets (archetype_asset_id)
WHERE archetype_asset_id IS NOT NULL;
org_id 为什么用 NULL 而不是哨兵 org(与 ADR-008 K2 的差异,显式记录):K2 用 HP_GLOBAL_ORG_UUID 哨兵是因为检索过滤器需要 org_id IN (org, global) 的统一查询形状(检索是 fan-in 读)。配 置资产没有这种 fan-in 读——catalog 与 instance 走 service 层两个显式分支(catalog 列表页 vs 租户配置页),NULL 语义诚实、CHECK 可强制 shape 耦合,且避免一个"魔法 org"进入 FK 图。装配器读取时只看 org_instance 行(物化后 catalog 内容已复制进实例,§7),不存在跨作用域 join。
2.2 specialist_config_versions(版本表,全类型共用)
ADR-020:随属资产行(读版本必经资产行的 scope 检查);docstring 声明同 §2.1。
| 列 | 类型 | 约束/默认 | 注释 |
|---|---|---|---|
id | uuid | PK | |
asset_id | uuid | NOT NULL, FK→assets ON DELETE CASCADE | |
version_no | int | NOT NULL | 资产内单调递增;UNIQUE(asset_id, version_no) |
content | jsonb | NOT NULL | 按 asset_type 的 Zod schema 校验后写入(§3);大文本走 R2 时存指针对象(§4) |
content_r2_key | varchar(512) | NULL | R2 对象键;非 NULL 时 content 内含 {"$r2": true, ...} 指针 |
content_hash | varchar(64) | NOT NULL | sha256(canonical-JSON(content) ‖ R2 bytes);导入幂等与 pull 检测(§7)都用它 |
publish_status | varchar(16) | NOT NULL DEFAULT draft, CHECK IN (draft,publishing,published,blocked,rolled_back,discarded) | 状态机见 §5 |
eval_run_id | uuid | NULL | Phase 4 接 EvalRun;门禁未接线期恒 NULL(§5.3 过渡语义) |
publish_note | varchar(1000) | NULL | override 时必填(written reason) |
author_id | uuid | NOT NULL | |
created_at | timestamptz | NOT NULL DEFAULT now() | |
published_at / published_by | timestamptz / uuid | NULL |
索引:UNIQUE(asset_id, version_no);idx_config_versions_asset (asset_id, created_at DESC)。
不变式的 DB 级强制(文字规则必须落成约束,防止并发绕过):
-- 同一资产至多一个在编草稿(§2.1 draft_version_id 语义的硬化)
CREATE UNIQUE INDEX uq_config_versions_one_draft
ON specialist_config_versions (asset_id) WHERE publish_status = 'draft';
-- 同一资产至多一个 publishing 中版本(§5.1 T1 前置条件的硬化)
-- 兼作异步 eval worker 的扫描面
CREATE UNIQUE INDEX uq_config_versions_one_publishing
ON specialist_config_versions (asset_id) WHERE publish_status = 'publishing';
状态机并发:所有状态转换用原子 claim(根 CLAUDE.md 幂等契约 3):
UPDATE specialist_config_versions SET publish_status = $next, ...
WHERE id = $id AND publish_status = $prev RETURNING *;
2.3 Expand-only migration 顺序
CREATE TABLE specialist_config_assets(不含指向 versions 的两个指针列);CREATE TABLE specialist_config_versions+ FK→assets;ALTER TABLE specialist_config_assets ADD COLUMN published_version_id / draft_version_id+ FK→versions(NOT VALID后VALIDATE,避免建表期循环 FK);- 索引
CONCURRENTLY(沿用1806600000000-AddReleaseSignalIndex.ts的 transaction=false + indisvalid 后检模式); - 导入回填(Phase 1 P1.3 / Phase 2,独立 PR);
specialists.systemPrompt/specialists.tools[]的 contract(弃列)在全量切换 + 4 周未用后另行 migration(PRD R1.2 弃用准则、master plan P2.5)。
3. 五个 asset_type 的 content Zod schema(初稿)
统一约定:所有操作者可见字符串字段经 noInfraDisclosure() refinement(NFR5——复用 scrubFilesystemPaths 的检测正则 + #3054 规则做校验而非清洗:save 时拒绝并给 field-level error,PRD R1 AC3 同款体验)。所有 max 值是 token 预算的字符近似(≈4 chars/token),预算常量与装配器共享单一来源(config-asset-budgets.ts,防止校验器与装配器口径漂移——见 concerns)。
// api/src/config-assets/schemas/soul.schema.ts (R1.1)
export const SoulContent = z.object({
identity: z.object({
displayName: z.string().min(1).max(64),
roleTitle: z.string().min(1).max(128),
bio: z.string().max(2_000),
}),
voice: z.object({
tone: z.array(z.string().max(64)).max(8), // e.g. "warm", "concise"
styleRules: z.array(z.string().max(300)).max(12), // "总是用主动语态…"
}),
boundaries: z.object({
never: z.array(z.string().max(300)).max(16), // 永不做/说
escalate: z.array(z.string().max(300)).max(16), // 遇到即升级
}),
languages: z.array(z.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/)).min(1).max(8),
signOff: z.object({ enabled: z.boolean(), template: z.string().max(200) }).optional(),
}); // 整体序列化预算 ≤ SOUL_BUDGET_CHARS(§6 预算表)——超出 save 时拒绝,运行时永不截断
// skill.schema.ts (R2.1;#3447 archetype/instance 同 schema)
export const SkillContent = z.object({
trigger: z.object({
description: z.string().max(500), // 人读的场景描述
topics: z.array(z.string().max(64)).min(1).max(12),
keywords: z.array(z.string().max(64)).max(24), // Phase 3 trigger 引擎的匹配输入
}),
instructions: z.string().min(1).max(12_000),
responseTemplates: z.array(z.object({
name: z.string().max(64),
body: z.string().max(4_000),
})).max(8),
escalationRules: z.array(z.string().max(300)).max(12),
redLines: z.array(z.object({ // 结构化红线(R2.5 的数据基底)
id: z.string().regex(/^[a-z0-9-]{1,48}$/),
rule: z.string().min(1).max(500),
severity: z.enum(["block", "flag"]), // block=装配硬约束+post-draft 拦; flag=仅标给 Expert
})).max(24),
tools: z.array(z.string().regex(/^[a-z0-9_]{1,64}$/)).default([]), // 预留:tool slug(R6/P4.7 消费, skills∩bindings)
});
// ── AS-BUILT (Phase 2A, 已实现于 api/src/config-assets/schemas/skill.schema.ts) ──
// 相对上面初稿的实现偏差(皆为对齐 soul.schema.ts 既有惯例,非语义变更):
// 1. 所有操作者可见字符串字段用共享 helper opStr(max)=z.string().max(max).superRefine(noInfraDisclosure)
// 以强制 NFR5(初稿只在 §3 引言文字说明"字段过 noInfraDisclosure",实现落到每字段)。
// regex-only 标识符字段(redLines[].id、tools[])保留 bare z.string().regex(...)。
// 2. keywords / responseTemplates / escalationRules / redLines 加 .default([])(导入器与表单省略时的稳态)。
// 3. instructions / redLines[].rule 用 opStr(N).min(1)(zod v3 惯例,同 soul 的 identity.displayName),
// 等价于初稿的"非空+限长+NFR5",未采用 .pipe()。
// 4. 顶层 .superRefine 对 JSON.stringify(val).length > SKILL_BUDGET_CHARS(16k) 报错(layer⑤ 预算,save 拒绝,运行时不截断)。
// kb-golden-answer.schema.ts (R3.1/R3.2)
const Applicability = z.object({
effectiveFrom: z.string().date().optional(),
effectiveTo: z.string().date().optional(),
audience: z.array(z.string().max(64)).optional(), // e.g. ["retail","vip"]
jurisdiction: z.array(z.string().max(8)).optional(), // ISO 3166
});
export const KbGoldenAnswerContent = z.object({
question: z.string().min(1).max(1_000),
answer: z.string().min(1).max(6_000), // verbatim-with-adaptation 的母本 (ADR-033 L1)
sources: z.array(z.string().url()).max(8),
applicability: Applicability.default({}),
adaptationNotes: z.string().max(1_000).optional(), // 允许怎样改写
});
// kb-domain-rule.schema.ts (R3.2)
export const KbDomainRuleContent = z.object({
statement: z.string().min(1).max(2_000),
category: z.enum(["policy", "eligibility", "process", "fact"]),
applicability: Applicability.default({}),
references: z.array(z.string().max(300)).max(8),
});
// tool-binding.schema.ts (R6.2 存储半场)
// ⚠️ 以下 3 处已按真实代码纠正 @ 490517fb(P2.5 实现,原设计占位名与代码不符):
// ① 字段名 kind(非 sourceKind)—— 真实 AgentToolDescriptor.kind(agent-api/tool-registry.ts:1)。
// ② 枚举无 mcp —— 真实 AgentToolKind = 'bespoke' | 'nango';mcp 是 P4.6 才加的值,代码里还没有,
// 预置会让 P4.7 service 层二次校验对不上注册表。
// ③ credentialRef 引用 integration_credentials 行,粒度 (org_id, integration_type)
// (entities.ts @Unique(["orgId","integrationType"])),不是 ADR-030 的 OSA/Channel 维度。
// 真实运行时下发链是 tool_permissions × TOOL_CATALOG → AgentRun.toolsManifest(不是 AGENT_TOOL_REGISTRY,
// 那是旧域模式静态表);本 schema 只做存储半场,工具暴露/门禁在 P4.7。
export const ToolBindingContent = z.object({
toolSlug: z.string().regex(/^[a-z0-9_]{1,64}$/), // 与 skill.schema.ts tools[] 同 regex;slug 存在性校验在 P4.7
kind: z.enum(["bespoke", "nango"]), // ① + ②:真实字段名 + 真实枚举(无 mcp)
accessClass: z.enum(["read", "write"]), // write ⇒ P4.7 每次调用走 Expert 审批(R6.4)
constraints: z.object({
allowedOps: z.array(z.string().max(64)).max(64).optional(), // 受限 op 标识符,非操作者散文 → bare string 正确
rateLimitPerHour: z.number().int().positive().max(10_000).optional(),
}).default({}),
// ③ 引用 integration_credentials 行(粒度 org_id × integration_type);凭证本体永不入 content/prompt/trace(R6 AC3)。
// 存在性/解析校验在 P4.7。
credentialRef: z.string().uuid().optional(),
});
4. D2 — 存储 split:PG jsonb vs R2(建议,待评审确认)
| 项 | 建议 | 理由 |
|---|---|---|
| 默认 | 全部 PG jsonb | 五类 schema 的 max 合计单版本 < 32KB,远低于 jsonb 舒适区;版本 diff、eval 快照、审计都是 DB 内操作 |
| R2 溢出阈值 | canonical-JSON 序列化 > 64KB 时整体外置:content 存 {"$r2": true, "key": ..., "bytes": n, "sha256": ...},content_r2_key 冗余列化 | 按当前 schema 上限实际不可触发——阈值是防御未来类型(如富文档模板);不做字段级拆分(复杂度不值) |
| eval sandbox 只读访问 | 内部 API GET /v1/internal/eval/config-snapshot?versionIds=…(AgentTokenGuard 同款鉴权),server 侧解引用 R2 后返回完整 content | sandbox 不持有 R2 凭证;快照按 version id 取,天然可复现(ADR-035 "pin what you back-test") |
| 版本 GC | 每资产保留全部 published/rolled_back 历史 + 最近 50 个其他版本;discarded 草稿 90 天后物理清理;R2 对象随版本行删除 | 审计口径:发布过的永不删 |
5. 发布状态机(edit ≠ publish 的机器化)
5.1 转换表
| # | 从 → 到 | 触发者 | 前置条件 | 审计事件 |
|---|---|---|---|---|
| T1 | draft → publishing | PRD 权限表角色(catalog: SA;instance: AM+SA)点 Publish | content 已过 Zod + NFR5(save 时已验,此处重验 hash 一致);资产无其他 publishing 中版本 | config.publish.requested |
| T2 | publishing → published | eval worker 回调(Phase 4 前见 §5.3) | eval 通过(或门禁未接线);原子 claim 成功 | config.publish(含 eval_run_id) |
| T3 | publishing → blocked | eval worker 回调 | golden-set 回归超噪声带(ADR-035 语义) | config.publish.blocked |
| T4 | blocked → published | 同 T1 角色显式 override | publish_note 必填(written reason,PRD R4 AC2) | config.publish.override |
| T5 | published(旧) → 被替换 | T2 副作用 | 资产指针 published_version_id 原子切换到新版本;旧版本状态保持 published(历史),仅指针离开 | (并入 T2 事件 payload:previousVersionId) |
| T6 | 资产级 rollback | 同 T1 角色 | 指针切回历史某 published 版本;被切离的版本标 rolled_back | config.rollback |
| T7 | draft → discarded | 作者/同权限角色 | — | config.draft.discarded |
审计走既有 AuditService(ADR-019:业务 tx 外、fire-and-forget)。
5.2 NFR2 异步语义
publishing 期间 旧 published_version_id 继续服务所有流量——装配器只读资产指针,指针只在 T2 原子切换。eval 运行入 BullMQ(stable jobId = version id,跨 pod 幂等),永不阻塞 live chat。
5.3 过渡语义(Phase 1–3,eval 门禁未接线)⚠️
P4.1 之前没有 harness 接线。显式规定(否则 "edit≠publish" 是表演):T1 后 worker 立即走 T2,eval_run_id = NULL,audit payload 带 evalGate: "not_wired"——发布流、审计、指针切换、回滚从 Phase 1 第一天就是真实的,唯独 eval 判定是 pass-through,且在审计里可辨认。Phase 4 接线后 evalGate: "not_wired" 的存量记录即"未过门禁的历史发布"清单。
5.4 eval 钩子挂点
publishing 状态即挂点:Phase 4 的 worker 消费 WHERE publish_status='publishing' 部分索引,调 agent/evals/domain harness(可包装为 Langfuse run_experiment 的 task,score 落 Langfuse 看板;gate 判定与 EvalRun 记录留 API 侧——master plan P4.1 调研注记)。本期只建状态与字段,不建 worker。
6. 装配优先级全链与 token 预算(Phase 3 的输入契约)
层序(高优先在前;⑤⑥⑦为既有行为,装配器必须保序纳入,不得回归):
| 层 | 来源 | 初始预算(chars≈4/token) | 截断策略 |
|---|---|---|---|
| ① SECURITY_PREFIX + 激活 skills 的 redLines(block) | 代码常量 + skill 版本 | 红线 ≤ 4k | 永不截断(NFR3);超预算在 save 时拒绝 |
| ② crisis directive | org.crisisInstructions(既有;full_stop 时 pipeline 根本不派发,装配器不会遇到) | ≤ 2k | 永不截断(save 侧已限长) |
| ③ active directives | #3692(既有 L0.5) | ≤ 4k | 永不截断(条数上限在写侧) |
| ④ Soul(identity+voice+boundaries+signOff) | soul 版本 | ≤ 8k | 永不运行时截断;save 时整体预算拒绝(R1 AC3) |
| ⑤ 激活 skill 的 instructions + templates | skill 版本 | ≤ 16k | 截断次序第 4(最后被砍的可砍层) |
| ⑥ 角色模板 body | agent/prompts/<key>.txt(第 0 层输入,非资产) | ~8k 固定 | 不砍(base 层) |
| ⑦ KB 引用(golden answers 优先 → hybrid) | 检索 | ≤ 16k(现 MAX_KB_CONTEXT_CHARS=4000 chars 需同步上调评估) | 截断次序第 3 |
| ⑧ history | 会话 | ≤ 24k | 第 2 |
| ⑨ correction few-shot | #3406 | ≤ 6k | 第 1(最先砍) |
NFR3 截断次序 = ⑨→⑧→⑦→⑤;①–④⑥ 不参与截断。预算常量单一来源:api/src/config-assets/config-asset-budgets.ts,agent 侧经 runtime-config/装配输入下发同值(不得两边各写一份字面量)。
7. Archetype → instance 继承(R2.6:copy-on-materialize + 显式 pull)
catalog asset (scope=catalog, specialist=archetype)
│ materialize(AM 为 org 启用该 archetype)
▼
org_instance asset:深拷贝 catalog 的 published content → 实例 version 1 (draft)
archetype_asset_id = catalog.id (血缘)
→ 走正常 publish 流(T1–T2)
- 检测 upstream 变更:比较
catalog.published_version.content_hashvs 实例物化时记录在资产metadata.materializedFromHash(§2.1 metadata 列)的 hash。Ops UI 列表页对 hash 不等的实例显示 "upstream changed, pull update?"。 - pull 动作:以 catalog 新 published content 生成实例新 draft version(保留实例本地改动的三方 diff 展示),操作者确认后走 publish/eval 门。
- 绝不静默传播:catalog 发布不触碰任何实例行——传播只经显式 pull(R2.6:silent propagation would bypass R4.6)。
8. 导入框架(三源 → source=imported_repo)
| 源 | 现状位 置 | 目标 asset_type | 解析 |
|---|---|---|---|
| SOUL.md | repo orgs/<slug>/SOUL.md(bind-mount /opt/data/SOUL.md;M2 经 runtime-config 下发) | soul | markdown 分节启发式映射到 SoulContent 字段;无法归类的段落进 voice.styleRules 并在 diff 报告标注 unmapped |
| SKILL.md | repo orgs/<slug>/skills/**/SKILL.md | skill | frontmatter/标题映射 trigger、正文映射 instructions;红线需人工补(导入报告列 TODO) |
| systemPrompt | specialists.system_prompt 列 | soul(并入同 Specialist 的 soul 资产) | >300 字符或含 4 信号词(persona-override 启发式命中者)单独标注——它们实际是"整模板替换",导入为 soul 会改变行为,报告强制人工裁决 |
dry-run 输出(默认模式;--apply 才落库):
[soul] amy@kaito CREATE (from orgs/kaito/SOUL.md, 1_842 chars, hash 3fa2…)
identity.displayName: "Amy" ← "# Amy — IR Specialist"
unmapped: 2 sections → voice.styleRules ⚠ review
[skill] launchpad-faq@kaito UPDATE (content_hash changed 9c01… → 77ab…)
--- instructions (unified diff) ---
...
[soul] bob@acme SKIP (hash unchanged)
⚠ MANUAL: specialists.system_prompt for amy@kaito matches override heuristic (1_204 chars) — decide split
幂等:按 content_hash 跳过未变更源;重跑安全。导入产物首个 publish 依 §5.3(过渡期审计标注;eval 门接线后必须过门,PRD §8 规则 4)。
9. ADR-033 L0/L1 对照自查 + 边界重申
- L0(Identity & Persona):Soul 结构化 + 永不置换角色模板(层可分离规则)——本模型以"Soul 是独立资产、模板是装配第 0 层输入"从数据形状上杜绝 re-collapse;persona-override 启发式的移除(P1.6)以 P0.6 trace 的命中率数据 护航。✅
- L1(Knowledge:golden answers + structured rules):kb_golden_answer / kb_domain_rule 进底座获得版本+发布+eval;检索时 golden 优先(P2.4)。✅
- 层分离红线:redLines 是 skill 的结构化 section 而非 prompt 文本;装配层①与⑤分离注入。✅
- 边界:KB 文档本体不迁入(ADR-008 管道原样);
kb_golden_answer(生产检索资产)≠ eval 的 golden scenario(ADR-035 目录式 fixture bundle,格式 OD-13 未决)——实体、命名、存储彻底隔离,前者在底座,后者在 Phase 4 的EvalGoldenScenario。
10. 评审 checklist 与开放问题
Checklist(P0.3 验收,评审时逐项勾):
- §2 两表全字段 + 索引 + ADR-020 docstring 措辞
- §3 五个 Zod schema(红线 section、tools 预留字段、applicability 元数据、凭证 id 引用)—— 全部实现:soul/skill(P1/P2A)、kb_golden_answer/kb_domain_rule(P2B)、tool_binding 存储半场(P2.5,schema 里 3 处占位名已按真实代码纠正)
- §5 状态机 + §5.3 过渡语义("not_wired" 审计标注)
- §4 D2 存储 split 建议
- §6 优先级链含 crisis/active_directives + 预算单一来源
- §7 copy-on-materialize + 显式 pull
- §8 三源导入 dry-run + diff
- §9 ADR-033 对照 + golden answer/golden scenario 命名隔离
开放问题:
| # | 问题 | 状态 |
|---|---|---|
| D2 | PG/R2 阈值 64KB + snapshot API + GC 策略 | 本文档给出建议,待评审确认 |
| D3 | 装配器 placement:短期 agent 侧、契约 runtime 中立 | 本文档给出建议(§11),待评审确认 |
| OD-13 | eval golden scenario 贡献格式 | ADR-035 未决,只影响 Phase 4,不 gate 本模型 |
| Q3(PRD) | 角色模板是否终被 skills 吸收 | Phase 2 后重估;本模型按"第 0 层输入"处理 |
已定:资产 metadata jsonb 列(§2.1 已含该列,§7 已更新) |
11. D3 — 装配器 placement(建议,待评审确认)
结论:短期(本 PRD 周期)装配器落 agent 侧——prompts_loader.py 演进为确定性 assembler;输入契约与 trace 输出契约定义为 runtime 中立,未来可整体搬移 API 侧。
论证:
- root CLAUDE.md 明文 "ConversationsService still calls the Python Hermes agent … Hermes is deliberately preserved"——主 draft 路径短期宿主是既定事实;
- ADR-034 E2 phase-4.5(已批准)只迁了 Expert 咨询(Site B),
runAgentPipeline的结构化输出迁移是未排期 follow-up——在未排期的假设上把装配器建到 API 侧,会让 Phase 3 依赖一个不存在的宿主; - 配置下发通道已存在且是 agent 侧消费(runtime-config:
soul_md + config_yaml + skills[]),P1.5 把它改为从底座读后,agent 侧装配的输入天然齐备; - 中立契约是对冲:装配器输入 =
{assembly_input_version: 1, layers: [{kind, version_id, content_hash, content, budget}...]}(NestJS 组包,agent 只做确定性拼装与截断);trace 输出 = version_id 集合 + 逐层预算实耗(P3.4 全量 trace 的行 shape)。宿主搬移 = 搬拼装函数,契约不变。
若评审推翻(选 API 侧):Phase 3 的 P3.1/P3.2 改挂 LlmService 路径并前置依赖 runAgentPipeline 迁移排期——影响范围已在 master plan 依赖主线标出。
评审人:Franky(Product)+ Eng owner。D2/D3 结论回填 master plan §五决策表后,P1.1 方可动工。