- 续审 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)
16 KiB
16 KiB
契约登记(解耦缝)
跨 agent 的契约——多 agent 靠它解耦并行。契约先行:定义方先在此登记并标
稳定,依赖方才动工;改契约必须在此记一笔 + 在PROGRESS.md通知依赖任务(前端要重生成 TS 客户端、依赖模块要重同步)。 状态:待定义→草拟(@skill)→稳定→已变更(见日志)。
C1 · LLM 网关接口 owner @llm 状态: 稳定(T1.1, 2026-06-18)
- 来源:
ARCHITECTURE.md §4.1(LlmRequest/LlmResponse/Block/Usage,snake_case)。 - 消费方:编排器、所有 Agent(经
Gateway.run/Gateway.stream)。 - 关键不变量:agent 只传
tier(writer/analyst/light),不传具体 model。 - 已实现(
packages/llm_gateway/ww_llm_gateway):- 类型
types.py:Block(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)->LlmResponse、stream(req)->AsyncIterator[Delta]。每次调用落 1 条usage_ledger(经LedgerSink,可注入内存替身)。- 适配器
ProviderAdapter(Protocol):provider、capabilities()->Capabilities、async complete(req,model)->ProviderResult、stream(req,model)->AsyncIterator[StreamChunk]。M1 实现OpenAICompatAdapter(provider, client:AsyncOpenAI)(DeepSeek,注入 client 便于测试)。 - 档位路由
resolve_route(tier)->Route(provider,model)读config.tier_defaults(M1 仅全局默认;回退/熔断属 M5/T5.4,未实现)。 - 记账
SqlAlchemyLedgerSink(session)写UsageLedger(owner_id=scope.user_id 单用户 stub;列名cache_read)。成本表pricing.py(未知 provider/model→0)。
- 类型
C2 · DB schema(SQLAlchemy 模型) owner @db 状态: 待定义
- 来源:
ARCHITECTURE.md §3.1DDL(11 创作表 + 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-18,snake_case,响应仅脱敏 key):
GET /settings/providers→ProvidersResponse{ providers:[ProviderView{provider, masked_key}], tier_routing:[TierRoutingView{tier, provider, model, fallback:list[str]}] }PUT /settings/providers←ProvidersUpsertRequest{ credentials:[ProviderCredentialInput{provider, api_key}], tier_routing:[TierRoutingInput{tier, provider, model, fallback}] }→ProvidersResponse(脱敏)POST /settings/providers/test←TestConnectionRequest{provider}→TestConnectionResponse{provider, ok, capabilities:CapabilitiesView{structured_output, prefix_cache, thinking}}- 明文 key 永不过边界;probe 经可注入
ProviderProbe(测试用 fake,不联网)。
- 已落(T1.4, 2026-06-18,snake_case):
POST /projects←ProjectCreateRequest{title, genre?, logline?, premise?, theme?, selling_points:list, structure?}→ 201ProjectResponse{id, title, genre?, logline?, premise?, theme?, selling_points, structure?}GET /projects→ProjectListResponse{projects:[ProjectResponse]};GET /projects/{id}→ProjectResponse(404NOT_FOUND信封)POST /projects/{id}/chapters/{no}/draft→ SSEtext/event-stream,帧event:<token|done|error>\ndata:<json>\n\n(token{text}/done{length}/error{code,message,request_id});无凭据 → 流前LLM_UNAVAILABLE(503) JSON 信封(非帧)。PUT /projects/{id}/chapters/{no}/draft←DraftSaveRequest{text}→ 200DraftResponse{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-18,snake_case,全部挂
projects.router、已在 OpenAPI)— M2 审/裁/验收三端点:POST /projects/{id}/chapters/{no}/review←ReviewRequest{draft?}(空/缺→回退已存草稿;无草稿→404NOT_FOUND)→ SSEtext/event-stream,帧:section{name,status:"done"|"incomplete"}(每审一条,M2 仅continuity) /conflict{type,where,refs:list,suggestion}(每冲突一条,形=C6Conflict五类) /done{length=审项数}/error{code,message,request_id}。无凭据→流前LLM_UNAVAILABLE(503 JSON 信封)。续审网关 tier=analyst(get_review_gateway)。端点流耗尽后session.commit()(网关 ledger + collect 均只 flush)。GET /projects/{id}/chapters/{no}/reviews→ReviewHistoryResponse{reviews:[ReviewHistoryItem{id,project_id,chapter_no,chapter_version?,conflicts:list,foreshadow_sug:list,style?,pace?,health_score?,decisions?}]}(新→旧)。POST /projects/{id}/chapters/{no}/accept←AcceptRequest{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)),缺判→409CONFLICT_UNRESOLVED+details.missing_conflict_indices/conflict_count;无留痕或零冲突→直通。digest 提炼 tier=light(get_digest_gateway)在事务外(R2),单事务 promote(R4)+digest.append(#4)+set_decisions 末尾一次 commit(R3)。- 注入缝:
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-18;M2 扩四审/验收)
- 来源:
ARCHITECTURE.md §5.2(图)/ §7.3(SSE)。M1 仅单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}——仅控制流+组装上下文+累积草稿(不变量#5);resume 从领域表重读。 - 节点缝:
build_write_request(*, stable_core, volatile, user_id, project_id) -> LlmRequest(纯函数;tier="writer",system=[Block(stable_core,cache=True)],input=volatile,stream=True,scope=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 网关)。GatewayStreamProtocol = 节点对网关最小依赖(只需.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) -> CompiledStateGraph(START→write→END);单测传MemorySaver。 - checkpointer setup 入口:
async setup_checkpointer(conn_string) -> None——只在 migrations/CI 调(内部AsyncPostgresSaver.from_conn_string跑一次setup()DDL;懒 import,绝不在 app-runtime 跑)。 - 不变量:agent 只传 tier;DB 唯一 agent 间通道;不可变更新;瞬时重试在网关不在节点。
C4 扩展(T2.2, 2026-06-18)· 审稿子图 + review SSE
- 图工厂
build_review_graph(gateway, review_repo, *, review_specs=(continuity_spec,), checkpointer=None) -> CompiledStateGraph:START →各 review spec 并行节点→ collect → END。M2 默认仅 continuity,review_specs可扩(M3/M4 加 foreshadow/style/pace)。build_write_graph保留不动(draft 端点仍用它)。 ChapterState(state.py) 新增review_context:str、reviews:Annotated[dict, merge_reviews](并行分支浅合并 reducer,返回新 dict);TypedDict 改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),不上抛、不毁图。GatewayRunProtocol=节点对网关最小依赖(.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 事务)。ReviewRecorderProtocol 形对齐domain.review_repo.ReviewRepo.record。 - SSE 新增事件:
section{name,status}(status ∈ started/done/incomplete)、conflict{type,where,refs,suggestion}(对齐 C6Conflict);新归一缝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 .../reviews用review_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)->SelectionTrace;render_cards(selection, characters, world_entities)->str;merge_rules(rules)->list[RuleView](global→genre→style→project)。 - 依赖注入:
MemoryRepos捆绑 7 个 Protocol(Outline/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导出)。 AgentSpec(frozen Pydantic,不可变):name:str、tier:Tier(writer/analyst/light,复用ww_llm_gateway.types.Tier——只声明档位不写 model,不变量#2)、system_prompt:str、input_schema:type[BaseModel]|None、output_schema:type[BaseModel]|None(writer 为 None=纯文本)、reads:list[str]、writes:list[str]、genre:str|None=None、scope:str="builtin"。continuity_spec:tier="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.parsed为ContinuityReview实例(带 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/apiTS 类型(openapi-typescript/orval)。 - 规则:后端 schema 任何变更 → 跑
gen:api重生成;不手写共享类型。
契约变更日志(append-only)
格式:
- [date] @skill 改 Cx:<改了什么> → 影响 <依赖方/任务>
- [2026-06-18] @llm 改 C1:
Gateway.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字段。结构化路径经可注入StructuredClientProtocol 注 fake(测试不联网)。→ 影响 T2.2(续审节点可直接gateway.run(req).parsed)、未来所有结构化输出 Agent。