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:
Yaojia Wang
2026-06-24 08:25:26 +02:00
parent eb356c42a9
commit 0021426f09
8 changed files with 48 additions and 2 deletions

View File

@@ -597,7 +597,7 @@ tier ──▶ resolve(scope) ──▶ { provider, model, fallback[] }
class AgentSpec(BaseModel):
name: str # worldbuilder / writer / continuity ...
tier: Tier # 能力档位(网关解析 provider+model)
system_prompt: str # 角色与约束
system_prompt: str # 角色与约束(值由 load_prompt(name) import 期注入, 见下)
input_schema: type[BaseModel] # 入参契约
output_schema: type[BaseModel] | None # 结构化产出契约(网关保证; writer 为 None=纯文本)
reads: list[TableName] # 声明式表读权限
@@ -605,8 +605,11 @@ class AgentSpec(BaseModel):
genre: str | None = None # 题材适用(Skill 用)
```
- 内置 8 Agent 即 `scope=builtin` 的 AgentSpec用户 Skill 为 `custom/community`§5.6)。
- 内置 21 Agent 即 `scope=builtin` 的 AgentSpec用户 Skill 为 `custom/community`§5.6)。
- `reads/writes` 是**契约**:运行时强制,越权拒绝(安全见 §5.6 / §9.2)。
- **Prompt 外置方案A2026-06**`system_prompt` 散文不再内联于 Python 常量,而外置为 `packages/agents/ww_agents/prompts/<spec.name>.md`,由 `load_prompt(name)``prompt_loader.py`)在 **import 期确定性加载**(去 BOM/LF 归一/NFC/`rstrip` 尾 LF + 内存缓存 + fail-fast `PromptNotFoundError`,无任何运行期插值)。`system_prompt` 仍是字节稳定的整块 `str`,进 `system``cache=True`——缓存断点前块字节不变(不变量 #9);改稿只动 `.md`,金标准 fixture `tests/fixtures/prompt_hashes.json` 守字节回归。Pydantic 类型**永留 Python**`SCHEMA_CATALOG[name]→output type` 是唯一真相源,`output_schema` 由它派生。
- **注册表 `SPECS: dict[name, AgentSpec]`** 是内置 agent 的集中名册name 为唯一主键,`SPECS[name]`/`prompts/<name>.md`/`SCHEMA_CATALOG[name]` 三者由 name 对齐,缺一即 fail-fast。四审受信名锚在显式白名单 `REVIEW_RESERVED_NAMES={continuity,foreshadow,style,pace}`
- **统一解析入口 `SpecResolver.get(name)`**`packages/skills/ww_skills/spec_resolver.py`):内置走纯内存 `SPECS`(零 DB用户 skill 走 `SkillRegistry`DB。同读接口、不同信任级别内置 name 为保留命名空间,用户 skill 同名在 **SkillRegistry 入库校验期**即被拒(不在读路径),守不变量 #3
### 5.2 编排器LangGraph 图)

View File

@@ -29,6 +29,21 @@
---
## 已封板Prompt 管理重构方案A+ 全量 prompt 重写 ✅
> **背景**:把 21 个内置 agent 的 `system_prompt` 散文从 `specs.py` Python 常量外置为 `.md` 文件,并统一内置/用户 skill 的只读解析入口。设计真源 `docs/design/prompt-management.md`;契约 `memory/contracts.md` C6-ext决策/坑见 `memory/{decisions,gotchas}.md`2026-06-24。**多 agent 工作流交付**TDD + 并行开发 + 交叉验证评审。
| 任务 | 状态 | 负责 | 备注 |
|---|---|---|---|
| 方案A 步骤13`spec_model`/`prompt_loader`/`prompts/*.md`×21/`schema_catalog`/`SPECS`/`REVIEW_RESERVED_NAMES` | ✅ | @llm | 金标准 sha256 取运行时值;`SPECS[name] is *_spec` 同一实例;三集合 name 恒等 |
| 方案A 步骤45`SpecResolver`(内置纯内存零 DB+ SkillRegistry 入库守卫前移 + `toolbox_registry` 桥接 `SPECS` | ✅ | @backend | 守卫锚 `REVIEW_RESERVED_NAMES set(SPECS)`;删 12 生成器 `*_spec` 直接 import |
| 打包/CI`.gitattributes eol=lf` + wheel `artifacts``prompts/*.md` + `agents-wheel-smoke` 冒烟;`structlog` 补声明 | ✅ | @devops | 裸装 `import ww_agents; assert SPECS` 通过;`.gitattributes` 须完整嵌套路径 |
| 集成回归:#12 编排器无回归 + #15 apps/api import-smoke`*_spec is SPECS[name]`+ #16 CI 冒烟 | ✅ | @qa | 内容无关,长青 |
| 全量重写 21 prompt 内容(任务对齐 + prompt 工程最佳实践schema 契约/不变量保持) | ✅ | @llm/@qa | 激进重写;金标准 fixture 重生成;护栏全验(无占位/无 model 名/中文/≤1 尾 LF/四审只读) |
**出口DoD✅**:后端门禁绿 ruff/format clean · **mypy 209** · **pytest 744 passed**;缓存断点前块字节稳定(不变量 #9,金标准守);零 schema 变更、前端零影响(无 `pnpm gen:api`)。已合并 `develop`(外置 wave `4f561da`、重写 wave `eb356c4`)。**后续可选**:编排器 + apps/api 3 路由切 `SpecResolver` 后删 `*_spec` 导出。
---
## 已封板K1 · Kimi Code OAuth订阅 plan 接入)✅
> **背景**:用户要求接入 Kimi Code 订阅 plandevice-flow OAuth。**已知情接受 ToS/封号风险**——研究确认:用订阅 plan 调用需伪造官方客户端 `User-Agent` + `X-Msh-*` 头Kimi ToS 视为违规、可能封停会员(见变更日志研究结论)。用户明确选择此路径(非合规的 API Key 路径)。与既有 API-key провайдер `kimi` **分离**,新增独立 provider `kimi-code` 以便切换。

View File

@@ -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/守卫) 状态: 稳定方案A2026-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·CRLF/NFC/`rstrip('\n')` 内存缓存 返回完整 UTF-8 文本缺文件 `PromptNotFoundError`fail-fast fallback不插值)。
- **`SPECS: Final[dict[name, AgentSpec]]`**`specs.py`@llm21 内置 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`@llmnameoutput 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-extPrompt 外置方案A21 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-ChainC23 端点 + `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 POSTGET /jobs 复用)。门禁绿ruff/format/mypy(193) 干净 + pytest 600 passed + alembic 无漂移。→ 影响 @frontendfollow-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 + 持久化尚未实现届时再扩本契约。**

View File

@@ -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 尾 LFloader `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`@devopsCI 冒烟守);`packages/{llm_gateway,core}` 补声明 `structlog`(原直接 import 未声明)。后续波次可将编排器/路由切 `SpecResolver` 后删 `*_spec`

View File

@@ -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

View File

@@ -6,6 +6,7 @@ dependencies = [
"langgraph>=0.2.40",
"langgraph-checkpoint-postgres>=2.0",
"pydantic>=2.7",
"structlog>=24.1",
"ww-shared",
"ww-config",
"ww-db",

View File

@@ -9,6 +9,7 @@ dependencies = [
"instructor>=1.5",
"pydantic>=2.7",
"tenacity>=8.2",
"structlog>=24.1",
"ww-shared",
"ww-config",
"ww-db",

4
uv.lock generated
View File

@@ -2471,6 +2471,7 @@ dependencies = [
{ name = "langgraph" },
{ name = "langgraph-checkpoint-postgres" },
{ name = "pydantic" },
{ name = "structlog" },
{ name = "ww-config" },
{ name = "ww-db" },
{ name = "ww-llm-gateway" },
@@ -2482,6 +2483,7 @@ requires-dist = [
{ name = "langgraph", specifier = ">=0.2.40" },
{ name = "langgraph-checkpoint-postgres", specifier = ">=2.0" },
{ name = "pydantic", specifier = ">=2.7" },
{ name = "structlog", specifier = ">=24.1" },
{ name = "ww-config", editable = "packages/config" },
{ name = "ww-db", editable = "packages/db" },
{ name = "ww-llm-gateway", editable = "packages/llm_gateway" },
@@ -2521,6 +2523,7 @@ dependencies = [
{ name = "instructor" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "structlog" },
{ name = "tenacity" },
{ name = "ww-config" },
{ name = "ww-db" },
@@ -2534,6 +2537,7 @@ requires-dist = [
{ name = "instructor", specifier = ">=1.5" },
{ name = "openai", specifier = ">=1.40" },
{ name = "pydantic", specifier = ">=2.7" },
{ name = "structlog", specifier = ">=24.1" },
{ name = "tenacity", specifier = ">=8.2" },
{ name = "ww-config", editable = "packages/config" },
{ name = "ww-db", editable = "packages/db" },