Files
writer-work-flow/memory/contracts.md
Yaojia Wang 68f194a043 feat: M2 — 写→审(一致性)→裁决→验收(事务);未决冲突禁验收
- 续审 Agent 声明(AgentSpec) + 结构化输出契约(ContinuityReview/Conflict 五类)
- LangGraph 并行审子图(可扩四审) + collect 落 chapter_reviews 留痕 + review SSE(section/conflict)
- 验收-side Repository:章节 accepted 版本晋升 + digest append-only + 审稿留痕/裁决
- API:review(SSE) + reviews 历史 + accept(单原子事务:晋升 version + 终稿 digest + 裁决留痕)
- 冲突 gate:未决裁决拦截(CONFLICT_UNRESOLVED);digest 从终稿提炼(不变量#4)
- 前端:审稿报告页 + 冲突就地标注 + 裁决(采纳/忽略/手改) + 未决禁验收 + 「本次将更新」清单
- M2 E2E:真实 DB + 多档位 mock 网关零 token 走通 写→审→裁决→验收→摘要入库
- 多 agent 协同台账(PROGRESS.md) + 共享记忆(memory/contracts·decisions·gotchas)
2026-06-18 11:38:28 +02:00

16 KiB
Raw Blame History

契约登记(解耦缝)

跨 agent 的契约——多 agent 靠它解耦并行。契约先行:定义方先在此登记并标 稳定,依赖方才动工;改契约必须在此记一笔 + 在 PROGRESS.md 通知依赖任务(前端要重生成 TS 客户端、依赖模块要重同步)。 状态:待定义草拟(@skill)稳定已变更(见日志)


C1 · LLM 网关接口 owner @llm 状态: 稳定T1.1, 2026-06-18

  • 来源:ARCHITECTURE.md §4.1LlmRequest / LlmResponse / Block / Usagesnake_case)。
  • 消费方:编排器、所有 AgentGateway.run / Gateway.stream)。
  • 关键不变量agent 只传 tierwriter/analyst/light不传具体 model。
  • 已实现(packages/llm_gateway/ww_llm_gateway
    • 类型 types.pyBlock(text,cache=False)Scope(user_id,project_id?)LlmRequest(tier,input:str|list[Block],system:list[Block],stream,output_schema?,thinking,max_tokens?,scope)Usage(provider,model,input_tokens,output_tokens,cache_read_tokens,cost_minor,currency)ServedBy(provider,model,fell_back)LlmResponse(text,parsed?,usage,served_by)Delta(text)Tier
    • Gateway(adapters:dict[str,ProviderAdapter], ledger:LedgerSink, resolver=resolve_route)async run(req)->LlmResponsestream(req)->AsyncIterator[Delta]。每次调用落 1 条 usage_ledger(经 LedgerSink,可注入内存替身)。
    • 适配器 ProviderAdapter(Protocol)providercapabilities()->Capabilitiesasync complete(req,model)->ProviderResultstream(req,model)->AsyncIterator[StreamChunk]。M1 实现 OpenAICompatAdapter(provider, client:AsyncOpenAI)DeepSeek注入 client 便于测试)。
    • 档位路由 resolve_route(tier)->Route(provider,model)config.tier_defaultsM1 仅全局默认;回退/熔断属 M5/T5.4未实现)。
    • 记账 SqlAlchemyLedgerSink(session)UsageLedgerowner_id=scope.user_id 单用户 stub列名 cache_read)。成本表 pricing.py(未知 provider/model→0

C2 · DB schemaSQLAlchemy 模型 owner @db 状态: 待定义

  • 来源:ARCHITECTURE.md §3.1 DDL11 创作表 + chapter_reviews + 运营表无向量列users stub
  • 消费方:@backend(Repository/记忆服务/验收)、@llm(Agent reads/writes)。

C3 · API / OpenAPI 端点 owner @backend 状态: 稳定M1 端点全落T1.7 settings + T1.4 projects/chapters, 2026-06-18

  • 来源:ARCHITECTURE.md §7.2 端点清单(章节端点统一 /projects/:id/chapters/:no/...;含 PUT .../draft(自动保存)、/refine/jobs/:id/reviews)。
  • 消费方:@frontend(经 OpenAPI→TS 客户端)。改端点/字段 → 前端必须 pnpm gen:api 重生成客户端。
  • 已落T1.7, 2026-06-18snake_case响应仅脱敏 key
    • GET /settings/providersProvidersResponse{ providers:[ProviderView{provider, masked_key}], tier_routing:[TierRoutingView{tier, provider, model, fallback:list[str]}] }
    • PUT /settings/providersProvidersUpsertRequest{ credentials:[ProviderCredentialInput{provider, api_key}], tier_routing:[TierRoutingInput{tier, provider, model, fallback}] }ProvidersResponse(脱敏)
    • POST /settings/providers/testTestConnectionRequest{provider}TestConnectionResponse{provider, ok, capabilities:CapabilitiesView{structured_output, prefix_cache, thinking}}
    • 明文 key 永不过边界probe 经可注入 ProviderProbe(测试用 fake不联网
  • 已落T1.4, 2026-06-18snake_case
    • POST /projectsProjectCreateRequest{title, genre?, logline?, premise?, theme?, selling_points:list, structure?} → 201 ProjectResponse{id, title, genre?, logline?, premise?, theme?, selling_points, structure?}
    • GET /projectsProjectListResponse{projects:[ProjectResponse]}GET /projects/{id}ProjectResponse404 NOT_FOUND 信封)
    • POST /projects/{id}/chapters/{no}/draftSSE text/event-stream,帧 event:<token|done|error>\ndata:<json>\n\ntoken{text}/done{length}/error{code,message,request_id});无凭据 → 流前 LLM_UNAVAILABLE(503) JSON 信封(非帧)。
    • PUT /projects/{id}/chapters/{no}/draftDraftSaveRequest{text} → 200 DraftResponse{project_id, chapter_no, volume, status, version, length}幂等version 固定 1、status='draft')。
    • 网关注入缝 get_writer_gateway(从凭据构 OpenAICompatAdapter+SqlAlchemyLedgerSink);测试经 app.dependency_overrides 注 mock 网关(只需 .stream(req))。
  • @frontend 行动M1 端点已全在 OpenAPI → Wave D 前先 cd apps/web && pnpm gen:api 重生成 lib/api/schema.d.ts
  • 已落T2.4+T2.5, 2026-06-18snake_case全部挂 projects.router、已在 OpenAPI— M2 审/裁/验收三端点
    • POST /projects/{id}/chapters/{no}/reviewReviewRequest{draft?}(空/缺→回退已存草稿无草稿→404 NOT_FOUND)→ SSE text/event-stream,帧:section{name,status:"done"|"incomplete"}(每审一条M2 仅 continuity) / conflict{type,where,refs:list,suggestion}(每冲突一条,形=C6 Conflict 五类) / done{length=审项数} / error{code,message,request_id}。无凭据→流前 LLM_UNAVAILABLE(503 JSON 信封)。续审网关 tier=analystget_review_gateway)。端点流耗尽后 session.commit()(网关 ledger + collect 均只 flush
    • GET /projects/{id}/chapters/{no}/reviewsReviewHistoryResponse{reviews:[ReviewHistoryItem{id,project_id,chapter_no,chapter_version?,conflicts:list,foreshadow_sug:list,style?,pace?,health_score?,decisions?}]}(新→旧)。
    • POST /projects/{id}/chapters/{no}/acceptAcceptRequest{final_text(min1), decisions:[ConflictDecision{conflict_index:int>=0, verdict:"accept"|"ignore"|"manual", note?}]}AcceptResponse{project_id,chapter_no,accepted_version:int,digest_added:bool,decisions_recorded:int,review_id?:uuid}冲突 gate:裁决的 conflict_index 集合须覆盖 range(len(最近一条 review.conflicts))缺判→409 CONFLICT_UNRESOLVED + details.missing_conflict_indices/conflict_count无留痕或零冲突→直通。digest 提炼 tier=lightget_digest_gateway在事务外R2单事务 promote(R4)+digest.append(#4)+set_decisions 末尾一次 commitR3
    • 注入缝:build_gateway_for_tier(session, store, tier)(原 build_writer_gateway 退化为 writer 特例) + get_review_gateway/get_digest_gateway/get_review_repo/get_digest_append_repo;测试经 app.dependency_overrides 注 mock。
    • @frontend 行动cd apps/web && pnpm gen:api 重生成客户端含此三端点T2.6)。

C4 · 编排器接口LangGraph 写章图 owner @llm 状态: 稳定M1/T1.3, 2026-06-18M2 扩四审/验收)

  • 来源:ARCHITECTURE.md §5.2(图)/ §7.3SSEM1 仅单 write 节点;并行四审/collect/interrupt(accept) 属 M2。
  • 位置:packages/core/ww_core/orchestrator/
  • 图状态 ChapterState(TypedDict, snake_case){project_id:UUID, chapter_no:int, user_id:UUID, stable_core:str, volatile:str, draft:str}——仅控制流+组装上下文+累积草稿(不变量#5resume 从领域表重读。
  • 节点缝:
    • build_write_request(*, stable_core, volatile, user_id, project_id) -> LlmRequest(纯函数;tier="writer"system=[Block(stable_core,cache=True)]input=volatilestream=Truescope=Scope(user_id,project_id))。
    • async stream_chapter_draft(gateway, *, stable_core, volatile, user_id, project_id) -> AsyncIterator[Delta]——T1.4 拿来喂 normalize_deltas 的底层流缝
    • async write_node(state, *, gateway) -> {"draft":str}——可直接单测(注入 mock 网关)。GatewayStream Protocol = 节点对网关最小依赖(只需 .stream(req))。
  • SSE 归一缝T1.4 消费)async normalize_deltas(deltas, *, request_id=None) -> AsyncIterator[SseEvent]SseEvent{event:str, data:dict};事件 token{text}/done{length}/error{code,message,request_id}ARCH §7.3 子集M2 加 section/conflict。底层异常→发 error 事件后收尾(不上抛);AppError.code 透传,未知→INTERNAL。HTTP event-stream 编码归 T1.4。
  • 图工厂:build_write_graph(gateway, *, checkpointer=None) -> CompiledStateGraphSTART→write→END单测传 MemorySaver
  • checkpointer setup 入口:async setup_checkpointer(conn_string) -> None——只在 migrations/CI 调(内部 AsyncPostgresSaver.from_conn_string 跑一次 setup() DDL懒 import绝不在 app-runtime 跑)。
  • 不变量agent 只传 tierDB 唯一 agent 间通道;不可变更新;瞬时重试在网关不在节点。

C4 扩展T2.2, 2026-06-18· 审稿子图 + review SSE

  • 图工厂 build_review_graph(gateway, review_repo, *, review_specs=(continuity_spec,), checkpointer=None) -> CompiledStateGraphSTART →各 review spec 并行节点→ collect → END。M2 默认仅 continuityreview_specs 可扩M3/M4 加 foreshadow/style/pacebuild_write_graph 保留不动draft 端点仍用它)。
  • ChapterState(state.py) 新增 review_context:strreviews:Annotated[dict, merge_reviews](并行分支浅合并 reducer返回新 dictTypedDict 改 total=False(按节点逐步填充);仍仅控制流+组装上下文+产物句柄(不变量#5
  • 节点缝:run_review(spec, state, *, gateway)->{"reviews":{spec.name:{status,result}}}(裸函数可单测);make_review_node(spec, gateway)(绑 gateway 的公共缝,供 T2.5 跑单审);审稿用 gateway.run()(非 stream只读不写库(不变量#3任一审网关失败被隔离为 {status:"incomplete",result:None}§5.2),不上抛、不毁图。GatewayRun Protocol=节点对网关最小依赖(.run(req)->LlmResponse)。
  • collect 缝:collect_reviews(state, *, review_repo)->{}:抽 continuity 冲突 → review_repo.record(project_id, chapter_no, chapter_version=None, conflicts=[...])chapter_reviews 留痕;只 flush 不 commit(提交归端点/T2.4 事务)。ReviewRecorder Protocol 形对齐 domain.review_repo.ReviewRepo.record
  • SSE 新增事件:section{name,status}status ∈ started/done/incompleteconflict{type,where,refs,suggestion}(对齐 C6 Conflict);新归一缝 normalize_review(reviews, *, request_id=None)->AsyncIterator[SseEvent](每审一条 section + 每冲突一条 conflict + done{length=审项数};异常→error 不上抛,同 normalize_deltas 纪律。HTTP event-stream 编码归 T2.5。
  • accept 不在本图interrupt_before=["accept"]/accept 节点属 T2.4确定性事务代码从领域表重读R3/不变量#5
  • 消费方行动T2.5 跑 review 子图 + normalize_review + 端点流耗尽后 await session.commit()(网关 ledger + collect 均只 flush不提交则记账/留痕静默丢失,同 M1 draft 坑);GET .../reviewsreview_repo.list_for_chapter(新→旧)。T2.4 从 review_repo.list_for_chapter 读 collect 落的行(decisions=None)裁决。

C5 · 记忆服务 assemble / select_relevant_entities owner @backend 状态: 稳定T1.2, 2026-06-18

  • 来源:ARCHITECTURE.md §3.4 / §5.3(确定性选择:显式+主角+近况;渲染卡片;缓存断点)。
  • 位置:packages/core/ww_core/memory/+ domain/repositories.py)。
  • 输出(决策:中性文本,非 LlmRequest,见 decisions 2026-06-18
    • AssembledContext{ stable_core:str, volatile:str, selection:SelectionTrace }
      • stable_core:断点前(世界硬规则+定型主角(无 latest_state)+文风指纹+合并规则),已排序/无时间戳/无 UUID。
      • volatile:断点后(注入卡片(含 latest_state)+伏笔窗口+近况摘要+本章 beats
    • SelectionTrace{ selected:list[SelectedEntity] }SelectedEntity{ kind:"character"|"world_entity", name:str, reasons:list[SelectionReason] }SelectionReason="explicit_beat"|"main_character"|"recent_digest"|"foreshadow_window"
  • 入口:async assemble(repos:MemoryRepos, project_id, chapter_no, recent_k=5)->AssembledContext;纯函数 select_relevant_entities(*, outline, characters, world_entities, recent_digests)->SelectionTracerender_cards(selection, characters, world_entities)->strmerge_rules(rules)->list[RuleView]global→genre→style→project
  • 依赖注入:MemoryRepos 捆绑 7 个 ProtocolOutline/Character/WorldEntity/Digest/Foreshadow/Style/Rules单测注入内存 fake运行时 sql_memory_repos(AsyncSession)T1.4 注入点)。
  • 不变量:确定性选择(无向量 #6);统一 project_id 过滤(§3.5)latest_state 严格归 volatile(#9)。
  • 消费方:T1.3 write 节点 → 构造 LlmRequest(system=[Block(stable_core, cache=True)], input=volatile)

C6 · Agent 声明AgentSpec+ 续审 I/O schema owner @llm 状态: 稳定T2.1, 2026-06-18

  • 来源:ARCHITECTURE.md §5.1 / §5.4 / §6.1。位置:packages/agents/ww_agents/specs.py / schemas.py,经 ww_agents 导出)。
  • AgentSpecfrozen Pydantic不可变name:strtier:Tier(writer/analyst/light复用 ww_llm_gateway.types.Tier——只声明档位不写 model不变量#2)、system_prompt:strinput_schema:type[BaseModel]|Noneoutput_schema:type[BaseModel]|None(writer 为 None=纯文本)、reads:list[str]writes:list[str]genre:str|None=Nonescope:str="builtin"
  • continuity_spectier="analyst"reads=["chapter_digests","characters","world_entities"]writes=[](只读,不变量#3)、input_schema=None(注入材料为序列化文本)、output_schema=ContinuityReview
  • ContinuityReview{ conflicts: list[Conflict] }仅冲突digest 不在审稿期产,不变量#4
  • Conflict{ type: ConflictType, where:str, refs:list[str]=[], suggestion:str }ConflictType = Literal["性格漂移","能力不符","设定违例","地理矛盾","时间线倒错"]ARCH §6.1 五类)。
  • 续审节点调法:req = LlmRequest(tier="analyst", system=[Block(system_prompt, cache=True)], input=审稿上下文文本, output_schema=ContinuityReview, scope=...)resp = await gateway.run(req)resp.parsedContinuityReview 实例(带 schema 时必非 None。续审用 run()stream();只读+产冲突,不写库(写在 T2.4 验收事务)。
  • 消费方:编排器(T2.2 续审/collect 节点)、技能运行时(M5)、前端审稿页(经 C3)。foreshadow/style/pace 三审 spec 待 M3/M4 补入本契约。

C7 · 前端 ↔ 后端类型契约 owner @backend(产出 OpenAPI) / @frontend(生成) 状态: 待定义

  • 机制FastAPI OpenAPI → apps/web/lib/api TS 类型openapi-typescript/orval
  • 规则:后端 schema 任何变更 → 跑 gen:api 重生成;不手写共享类型。

契约变更日志append-only

格式:- [date] @skill 改 Cx<改了什么> → 影响 <依赖方/任务>

  • [2026-06-18] @llm 改 C1Gateway.run() 现消费 LlmRequest.output_schema——OpenAICompatAdapter.complete 在 schema 非空时经 instructor(create_with_completion(response_model=...)) 取已校验 Pydantic 实例并填 LlmResponse.parsed;无 schema 时 parsed is None、纯文本路径不变;记账仍 1 条 usage_ledger/调用usage 从 raw completion 提取)。ProviderResult 新增 parsed 字段。结构化路径经可注入 StructuredClient Protocol 注 fake测试不联网。→ 影响 T2.2(续审节点可直接 gateway.run(req).parsed)、未来所有结构化输出 Agent。