docs(prompt-mgmt): 方案A 协同回写 + 补声明 structlog 依赖
- memory/contracts.md:立 C6-ext(load_prompt/SPECS/SCHEMA_CATALOG/
REVIEW_RESERVED_NAMES/SpecResolver + 打包契约)+ 变更日志一笔
- memory/decisions.md:方案A 决策(运行时值金标准/尾换行方案B/守卫前移/
schema 留 Python/同一实例)
- memory/gotchas.md:spec.name 连字符命名 / 改稿须重生成金标准 /
.gitattributes 完整嵌套路径 / wheel artifacts 带 .md / structlog 补声明 /
SpecResolver 零 DB + 精确匹配
- PROGRESS.md:已封板「Prompt 管理重构 + 全量重写」波次
- ARCHITECTURE.md §5.1:补 prompt 外置 + SPECS/SCHEMA_CATALOG/SpecResolver,
内置 agent 计数 8→21
- packages/{llm_gateway,core}/pyproject.toml:补声明 structlog>=24.1
(原直接 import 未声明,靠 apps/api 传递;裸装 import ww_agents 会缺)
This commit is contained in:
@@ -326,9 +326,22 @@
|
||||
- **token 纪律**:job result/status/日志只记章号/计数/标志,绝不含 prompt/正文/token(§5,已断言)。
|
||||
- → 影响 @frontend(链发起页/进度/裁决续跑面板,**非目标本期不做**,契约稳定后 follow-up `pnpm gen:api`);@db/@devops C3(检查点建表迁移 + 可选 jobs 注释)。
|
||||
|
||||
## C6-ext · Prompt 外置 + SpecResolver owner @llm(SPECS/loader/catalog) + @backend(resolver/守卫) 状态: 稳定(方案A,2026-06-24)
|
||||
> 设计真源:`docs/design/prompt-management.md`。纯重构、零功能/schema 变更、零迁移(前端无需 `pnpm gen:api`)。
|
||||
- **`load_prompt(name) -> str`**(`packages/agents/ww_agents/prompt_loader.py`,@llm):按 `prompts/<spec.name>.md` import 期读盘 → 去 BOM(`utf-8-sig`)/CRLF·CR→LF/NFC/`rstrip('\n')` → 内存缓存 → 返回完整 UTF-8 文本;缺文件 `PromptNotFoundError`(fail-fast,不 fallback、不插值)。
|
||||
- **`SPECS: Final[dict[name, AgentSpec]]`**(`specs.py`,@llm):21 内置 spec 集中名册,`assert len(SPECS)==21`;`system_prompt=load_prompt(name)`、`output_schema=SCHEMA_CATALOG[name]`。**不变量**:`SPECS[name] is *_spec`(兼容期同一实例,旧 `from ww_agents import *_spec` 不破);`prompts/<name>.md`/`SPECS[name]`/`SCHEMA_CATALOG[name]` 三集合按 name 恒等。
|
||||
- **`SCHEMA_CATALOG: Final[dict[name, type[BaseModel]|None]]`**(`schema_catalog.py`,@llm):name→output type **唯一真相源**(`refiner=None`);`output_schema_for(name)`。input 全 None,本波不建 input 槽(YAGNI)。
|
||||
- **`REVIEW_RESERVED_NAMES: Final[frozenset]={continuity,foreshadow,style,pace}`**(@llm):四审受信名显式白名单(非派生集合),安全边界锚此。
|
||||
- **`SpecResolver`**(`packages/skills/ww_skills/spec_resolver.py`,@backend):`build(skills)` 纯合并 `dict(SPECS)`+`SkillRegistry`(读路径无冲突校验);`get(name)` 先查内置 `SPECS`(**纯内存、零 DB**)未命中才查 registry,都无→`NOT_FOUND`;`output_schema_for`/`names`/`list_scope`。name **精确字符串相等**(无大小写/连字符归一)。
|
||||
- **守卫前移**:用户 skill 同名内置(`REVIEW_RESERVED_NAMES ∪ set(SPECS)`)在 **`SkillRegistry` 入库/加载校验期**即拒(`AppError(VALIDATION)`,与 `validate_declaration` 同处),**不在 resolver 读路径**——内置 `get` 确定性、零运行时分叉,守不变量 #3。
|
||||
- **打包契约**(@devops):`.gitattributes` 锁 `packages/agents/ww_agents/prompts/*.md text eol=lf`;`packages/agents/pyproject.toml` hatchling `artifacts` 纳入 `prompts/*.md` 随 wheel/sdist 分发;CI `agents-wheel-smoke`(build→裸装→`import ww_agents; assert ww_agents.SPECS`)防 `.md` 漏带(源码树 pytest 测不出)。`packages/{llm_gateway,core}` 补声明 `structlog`(原直接 import 未声明,靠 apps/api 传递)。
|
||||
- 消费方:`toolbox_registry.GeneratorTool.spec` 走 `SPECS["<name>"]`(不再直接 import 生成器 `*_spec`)。**本波不改**编排器(`REVIEW_SPECS`/节点/§6.5 precheck/chain)与 apps/api 3 路由(`toolbox/outline/style`)——兼容期续用 `*_spec`(因同一实例不变量无回归),切 resolver 列后续波次。
|
||||
|
||||
## 契约变更日志(append-only)
|
||||
> 格式:`- [date] @skill 改 Cx:<改了什么> → 影响 <依赖方/任务>`
|
||||
|
||||
- [2026-06-24] @llm/@backend 立 C6-ext(Prompt 外置方案A):21 prompt 散文外置 `prompts/<name>.md` + `load_prompt`/`SPECS`/`SCHEMA_CATALOG`/`REVIEW_RESERVED_NAMES`(@llm)+ `SpecResolver` + SkillRegistry 入库守卫前移 + toolbox 桥接 `SPECS`(@backend)+ `.gitattributes`/wheel package-data/CI 冒烟(@devops)。随后全量重写 21 prompt 内容(任务对齐,schema 契约/不变量保持,金标准 fixture 重生成)。门禁绿:ruff/format/mypy(209)/pytest(744)。→ 影响:后续波次可将编排器 + apps/api 路由切 `SpecResolver` 并删 `*_spec` 导出;前端零影响(GeneratorTool 描述符字段不变)。
|
||||
|
||||
- [2026-06-23] @backend 立 C-Chain(C2):3 端点 + `schemas/chain.py` + `services/{chain_runner,chain_deps}.py` + `jobs` 零迁移复用(加 `status="awaiting_input"` + `JobRepo.set_awaiting`)+ 新 `ErrorCode.CONFLICT`(409) + `project_deps` 加 `build_chain_gateway`/`get_chain_gateway`/`get_digest_gateway_builder`/`get_checkpointer_factory`。OpenAPI 含 2 新 POST(GET /jobs 复用)。门禁绿:ruff/format/mypy(193) 干净 + pytest 600 passed + alembic 无漂移。→ 影响 @frontend(follow-up `pnpm gen:api`)、@db/@devops C3(检查点迁移)。
|
||||
|
||||
- [2026-06-20] @backend 扩 C3:新增 **`GET /projects/{project_id}/chapters/{chapter_no}/injection`**(本章注入透明,B0 读端点)→ `InjectionResponse{project_id, chapter_no, selected:[InjectionEntity{kind,name,reasons[]}], recent_n}`(`schemas/injection.py`,snake_case)。实现仅调既有 `assemble()` 回放确定性 `SelectionTrace`——**无 LLM、无 commit、无 DDL**;项目不存在→404,无大纲→`selected:[]`。reasons 取值同 `SelectionReason`(explicit_beat/main_character/recent_digest/foreshadow_window)。→ 影响 @frontend(已 `pnpm gen:api` + `ChapterAssistant` 消费)。**B0 可控版(PUT override + `select_relevant_entities` 加 pinned/excluded/recent_n + draft 端点同读 override + 持久化)尚未实现,届时再扩本契约。**
|
||||
|
||||
@@ -155,3 +155,9 @@
|
||||
- 决策 2(**多档分派网关 `build_chain_gateway`**):一条链 run 跨 writer(write)/analyst(review)/light(digest) 三档,但 `build_gateway_for_tier(tier)` 装出的网关 `chain_resolver` 恒返该单档链、忽略 `req.tier`——会把 review/digest 错路由到 writer。故链需一个**按请求 tier 分派**的网关:union 三档所有 provider 适配器 + `chain_resolver` 据 `req.tier` 返回对应档链。digest 在 accept 节点自建短事务内按 session 现建 light 档网关(`get_digest_gateway_builder`)。
|
||||
- 决策 3(**accept_op 在 apps/api 装配**):链编排(图/节点)在 `ww_core`,但具体验收事务(`run_accept_transaction`/冲突 gate/digest 提炼/伏笔到期扫描)属 apps/api——经 `build_accept_op` 闭包注入图节点,守目录所有权 + 不变量 #3/#4。自动链无作者改稿,`final_text` = `chapters` 该章草稿正文(write 节点所落)。
|
||||
- 决策 4(**新错误码 `CONFLICT`**):resume 非 awaiting 态须 409,但既有 409 码 `CONFLICT_UNRESOLVED` 语义专指「未决冲突禁验收」,不宜复用。新增通用 `ErrorCode.CONFLICT`→409(资源状态冲突)。
|
||||
|
||||
## [2026-06-24] Prompt 外置方案A — @llm/@backend/@devops
|
||||
- 背景:21 个内置 agent 的 `system_prompt` 散文内联在 `specs.py` 的 Python 三引号常量里,改稿要碰 Python、diff 脏、加 agent 要改 4 处。设计真源 `docs/design/prompt-management.md`。
|
||||
- 选择:① 散文外置 `packages/agents/ww_agents/prompts/<spec.name>.md`,`system_prompt=load_prompt(name)` import 期读盘+内存缓存+确定性+fail-fast(去 BOM `utf-8-sig`/LF 归一/NFC/`rstrip('\n')`,**无运行期插值**——需插值的 prompt 不走此路径,属本波范围外);② **金标准 fixture 取 AST/运行时 `system_prompt` 值算 sha256**(`tests/fixtures/prompt_hashes.json`),**不取源码文本**——因旧常量含反斜杠折行,源码物理换行≠运行时换行;导出时 `.md` 物理换行≡运行时换行;③ 尾换行方案 B:`.md` 允许 ≤1 尾 LF,loader `rstrip` 不补回,文件层断言守「尾 LF≤1」(与运行时金标准双层契约);④ Pydantic 类型**永留 Python**,`SCHEMA_CATALOG[name]` 是 name→output 唯一真相源(收敛为 `dict[name,type|None]`,去恒 None 的 input 槽,YAGNI);⑤ 内置 name(含 `REVIEW_RESERVED_NAMES`=continuity/foreshadow/style/pace)为保留命名空间,守卫**前移至 SkillRegistry 入库校验**(非 resolver 读路径),用户 skill 同名→`VALIDATION`(安全边界,呼应不变量 #3);⑥ `*_spec` 兼容期保留且 `SPECS[name] is *_spec`(同一实例),编排器 + apps/api 3 路由本波不切 resolver。
|
||||
- 理由 / 取舍:非工程改稿不碰 Python、缓存断点前块字节稳定(不变量 #9);守卫前移让内置 `get` 纯内存零 DB、确定性、不把安全校验耦合进写章热路径。代价:`git log -p specs.py` 追不到旧 prompt 演进(迁移 commit body 注明),后续改 prompt 须同步重生成金标准 fixture。
|
||||
- 影响:加 agent = 写 `.md` + 注册 `SPECS`/`SCHEMA_CATALOG`;wheel 必须 `force-include/artifacts` 带 `prompts/*.md`(@devops,CI 冒烟守);`packages/{llm_gateway,core}` 补声明 `structlog`(原直接 import 未声明)。后续波次可将编排器/路由切 `SpecResolver` 后删 `*_spec`。
|
||||
|
||||
@@ -87,3 +87,6 @@
|
||||
- [2026-06-23] @backend 多章链网关(C2):**单档 `build_gateway_for_tier(tier)` 网关不能驱动跨档链**。其 `chain_resolver`/`_resolver` 闭包恒返该 tier 的链、**忽略入参 `req.tier`**,故拿它跑 review(analyst)/digest(light) 会全部错路由到 writer 档的 provider/model。链/任何跨档编排须用 `build_chain_gateway`(union 三档适配器 + `chain_resolver` 据 `req.tier` 分派)。测试用 mock 网关按 `req.output_schema` 路由(无 schema=write,有=review),不受此影响。
|
||||
- [2026-06-23] @backend langgraph `CompiledStateGraph.ainvoke` 的 mypy 重载:`config` 必须是 `RunnableConfig`(`from langchain_core.runnables import RunnableConfig`),裸 `dict[str,dict[str,str]]` 不匹配任何重载 → call-overload 错。返回值是 `dict[str,Any] | Any`,传给取 `__interrupt__`/`written` 的 helper 前先 `dict(raw_final)` 收敛为 `dict[str,Any]`。interrupt 命中时返回值含 `__interrupt__`(Interrupt 对象列表),载荷读 `getattr(first,"value",first)`(兼容 dict)。
|
||||
- [2026-06-23] @backend 改 `JobRepo` Protocol 加方法(如 `set_awaiting`)= 契约变更:**所有 fake 实现都要同步补**,否则 mypy 报 Protocol 缺成员(本次漏了 `packages/core/tests/test_job_repo.py::FakeJobRepo` + `apps/api/tests/fakes_projects.py::FakeJobRepo`)。grep `class Fake.*JobRepo` 全仓再补。
|
||||
- [2026-06-24] @llm/@devops Prompt 外置:`prompts/*.md` 文件名按 **spec.name(连字符)** 非 Python 变量名——`style` agent 的 name 是 `"style"`→`style.md`(非 `style_drift`),另有 `character-gen.md`/`golden-finger.md`/`book-title.md`/`fine-outline.md`/`de-ai.md`。**改 prompt 内容后必须重生成金标准** `uv run python packages/agents/tests/_gen_golden.py`(覆盖 `tests/fixtures/prompt_hashes.json`),否则 `test_prompt_loader.py` 字节回归红;旧常量含反斜杠折行——外迁/比对一律用**运行时值**别用源码文本。`.gitattributes` 锁 `prompts/*.md text eol=lf`(路径含斜杠是**根锚定**,必须写完整嵌套路径 `packages/agents/ww_agents/prompts/*.md` 才匹配,裸 `prompts/*.md` 不生效)。
|
||||
- [2026-06-24] @devops wheel 默认不打非-`.py` 数据文件——`prompts/*.md` 必须在 `packages/agents/pyproject.toml` 显式纳入(hatchling 用 `artifacts`,**别用 `force-include`**:hatchling 已含包目录下全部文件,force-include 会 duplicate-path 构建失败);源码树 pytest **测不出**漏带(fail-fast 仅裸装时触发),靠 CI `agents-wheel-smoke`(build→裸装→`import ww_agents; assert ww_agents.SPECS`)守。`ww_agents` import 链拉起 `ww_llm_gateway`(`types.Tier`)→需 `structlog`,故 `packages/{llm_gateway,core}` 必须自声明 `structlog`(原靠 apps/api 传递,裸装 import 会 ModuleNotFoundError)。
|
||||
- [2026-06-24] @backend `SpecResolver`:内置 name 走 `SPECS` 纯内存、**零 DB**;保留命名空间守卫在 **SkillRegistry 入库校验期**(非 resolver 读路径),用户 skill 与内置同名(`REVIEW_RESERVED_NAMES ∪ set(SPECS)`)→ `AppError(VALIDATION)`。name **精确字符串相等**:拼错近似名(`character_gen` vs `character-gen`)`output_schema_for` 返 None/不命中,是预期行为非 bug。
|
||||
|
||||
Reference in New Issue
Block a user