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:
@@ -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 外置(方案A,2026-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 图)
|
||||
|
||||
|
||||
15
PROGRESS.md
15
PROGRESS.md
@@ -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 步骤1–3:`spec_model`/`prompt_loader`/`prompts/*.md`×21/`schema_catalog`/`SPECS`/`REVIEW_RESERVED_NAMES` | ✅ | @llm | 金标准 sha256 取运行时值;`SPECS[name] is *_spec` 同一实例;三集合 name 恒等 |
|
||||
| 方案A 步骤4–5:`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 订阅 plan(device-flow OAuth)。**已知情接受 ToS/封号风险**——研究确认:用订阅 plan 调用需伪造官方客户端 `User-Agent` + `X-Msh-*` 头,Kimi ToS 视为违规、可能封停会员(见变更日志研究结论)。用户明确选择此路径(非合规的 API Key 路径)。与既有 API-key провайдер `kimi` **分离**,新增独立 provider `kimi-code` 以便切换。
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
4
uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user