- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点 - 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件 - 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板 - 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE - bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试 - M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug - 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
23 KiB
23 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)。
- 已落(T3.2, 2026-06-18,snake_case)— 伏笔登记/状态端点 + 验收后到期扫描(挂
foreshadow.router,prefix/projects,tagforeshadow,已注册):POST /projects/{id}/foreshadow←ForeshadowRegisterRequest{code(min1),title(min1),planted_at?,content?,expected_close_from?,expected_close_to?,importance?}→ 201ForeshadowView{code,title,status,planted_at?,content?,expected_close_from?,expected_close_to?,importance?,links:list,progress:list}(status=OPEN)。重复 code(DB 唯一(project_id,code))→ 422VALIDATION+details{field:"code",code,reason:"duplicate"}。PATCH /projects/{id}/foreshadow/{code}←ForeshadowTransitionRequest{to_status?,progress_entry?}→ 200ForeshadowView。非法转移→422VALIDATION+details{reason:"invalid_transition"};二者都缺→422empty_update;code 不存在→404NOT_FOUND。先转移后追加进展,端点写后commit()(repo 只 flush)。- 错误码取舍:重复/非法转移映射
VALIDATION(422) 非 409——现有唯一 409 码CONFLICT_UNRESOLVED语义专指未决冲突禁验收,不复用;未动packages/shared/errors.py契约。若 T3.5/前端需真 409 须先加专用码。 - 验收后到期扫描:
POST .../accept单事务 commit 成功后经BackgroundTasks登记run_overdue_scan(session_factory, project_id, chapter_no=刚验收章号)——自建独立 session(请求 session 已关闭)、应用current_ch>expected_close_to AND status≠CLOSED→OVERDUE、有变更才 commit;日志foreshadow_overdue_scan(带 request_id/overdue_count/codes),失败吞不冒泡。§7.4 持久性局限(进程内/重启丢任务)原型接受、不引 jobs 表。 - T3.5 续接:
GET /projects/{id}/foreshadow?status=看板加到本foreshadow.router(注入get_foreshadow_repo→list_by_status,建议响应包{foreshadow:[...]});POST .../outline落独立routers/outline.py(别混进 foreshadow.py),调run_outline(C6 扩)+build_gateway_for_tier(...,"analyst"),逐章 upsertoutline表、端点末尾 commit。 - @frontend 行动:T3.6 前
pnpm gen:api纳入伏笔登记/状态端点(+ T3.5 看板/outline)。
- 已落(T3.5, 2026-06-18,snake_case)— 伏笔看板 + 大纲生成端点:
GET /projects/{id}/foreshadow?status=(tagforeshadow):querystatus?∈OPEN|PARTIAL|CLOSED|OVERDUE(缺省=全部,非法→422VALIDATION+details{field:"status",reason:"invalid_status"}) → 200ForeshadowBoardResponse{foreshadow:list[ForeshadowView]}(按 code 升序)。POST /projects/{id}/outline(tagoutline,独立routers/outline.py)←OutlineGenerateRequest{volume:int=1,ge=1}(M3 简单分卷:整批同卷)→ 200OutlineResponse{chapters:list[OutlineChapterView{no,volume,beats:list[str],foreshadow_windows:list[ForeshadowWindowView{code,plant_chapter?,expected_close_from?,expected_close_to?}]}]};项目不存在→404NOT_FOUND;无凭据/网关失败→503LLM_UNAVAILABLE(流前 JSON 信封)。调run_outline(analyst 网关get_outline_gateway)→逐章 upsertoutline表(写侧SqlOutlineWriteRepo,只 flush)→端点末尾commit()(含 1 条 usage_ledger)。- beats 存储形:DB
outline.beats是 JSONB dict→写侧包成{"beats":[...]}落库,API 出参OutlineChapterView.beats解包成裸list[str]。 - @frontend 行动:T3.6
pnpm gen:api纳入看板/大纲端点。
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)裁决。
C4 扩展(T3.3, 2026-06-18)· 三审齐(foreshadow + pace 并入)
REVIEW_SPECS = (continuity_spec, foreshadow_spec, pace_spec);build_review_graph(..., review_specs=REVIEW_SPECS)默认三审齐(文风第四审留 M4)。run_review/make_review_node对 spec 泛型(按req.output_schema产 parsed)。- collect 列映射(一次
review_repo.record(...)落齐):continuity→conflicts(list);foreshadow→foreshadow_sug(list,把planted/resolved扁平成单 list、每条加kind:"planted"|"resolved");pace→pace(dict,整{water,hook,beat_map}入列);style 留 M4。任一审 incomplete→该列空/None,不阻塞其余(§5.2)。 normalize_reviewSSE(向后兼容,section/conflict不变)新增:foreshadow{kind,code?,title,where?,note?}(每条建议一条)、pace{water:[{where,reason}],hook:bool,beat_map:[int]}(一条)。键按字典序确定性遍历(continuity<foreshadow<pace)。incomplete 审仅发section不发结果。- 消费方:前端 T3.6(冲突就地标注 / 伏笔看板联动 / 节拍图 ▁▃▅)、E2E T3.7。
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-T3.3/M4 补入本契约。
C6 扩展(T3.4, 2026-06-18)· outliner Agent + 大纲/伏笔窗口 schema
- 位置:
packages/agents/ww_agents/{schemas.py,specs.py}(经ww_agents导出);节点在packages/core/ww_core/orchestrator/outline_node.py(经 orchestrator 导出)。 outliner_spec(frozenAgentSpec):name="outliner"、tier="analyst"、reads=["projects","foreshadow","characters","world_entities"]、writes=["outline"](声明式,真写库经 T3.5,不变量#3)、input_schema=None、output_schema=OutlineResult。OutlineResult{chapters:list[OutlineChapter]};OutlineChapter{no:int, beats:list[str], foreshadow_windows:list[ForeshadowWindow]};ForeshadowWindow{code:str, plant_chapter?:int, expected_close_from?:int, expected_close_to?:int}(§6.2 关联章与伏笔)。- 节点缝(沿用 review_node 的
GatewayRunProtocol.run(req)->LlmResponse):build_outline_request(spec, *, context, user_id, project_id)->LlmRequest(output_schema=OutlineResult,stream=False) +async run_outline(spec, *, context, gateway, user_id, project_id)->OutlineResult(gateway.run().parsed;只读不写库;大纲独立生成、非并行审,网关失败直接上抛不隔离,parsed 违约抛ValueError)。 - 消费方:T3.5 端点调
run_outline拿OutlineResult→ 逐章 upsertoutline表(beats/foreshadow_windows入 JSONB,volume由端点分卷逻辑提供,schema 不含 volume);analyst 网关复用build_gateway_for_tier(session,store,"analyst")/get_review_gateway;记账闭环同 M2(端点末尾commit())。T3.6 前端大纲编辑器消费窗口。
C6 扩展(T3.3, 2026-06-18)· foreshadow-analyst + pace-checker spec
foreshadow_spec(name="foreshadow", tier="analyst", reads=["foreshadow"], writes=[]只读, output=ForeshadowReview{planted:[ForeshadowSuggestion],resolved:[ForeshadowSuggestion]};ForeshadowSuggestion{code?,title,where?,note?})。pace_spec(name="pace", tier="light", reads=["rules"], writes=[]只读, output=PaceReview{water:[PaceIssue{where,reason}],hook:bool,beat_map:[int]})。genre 模板 DSL = pace system_prompt 内描述黄金三章/章末钩子/爽点密度,genre 级模板经 review_context 规则合并(rules)注入,未新建表/repo。- 三审只读,留痕经 collect→review_repo(写库 commit 归端点)。详细图/SSE 见上「C4 扩展(T3.3)」。
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。