Changelog¶
Unreleased (v0.5-dev)¶
下一发版的累积变更, 完成后 cut.
platform/common/server — rate limit 默认 60/min → 6000/min (P2 UI 第一次 deploy 后撞限速教训, 2026-05-04)¶
PM 第一次打开 hub.flytoex.net (v0.5.0-alpha.23 deploy 后) 撞 429 "rate limit exceeded, retry after 54 seconds". 根因: server.go rateLimitMiddleware 默认 60 req/min/IP, 但 frontend SPA 一开页面就并发拉一堆 stub 端点 (workspace 拉 /users/me, flow 页拉 /nodes/registry + /flows, runtime 页拉 /dispatches + SSE 心跳, settings 页双层 preset 切换), 加用户正常导航和刷新 60/min 一打就爆.
改文件:
platform/common/internal/server/server.go—Config.RateLimitPerMingodoc 重写 (双语完整解释新默认 6000 = 100/s 的设计理由 + 公网防爆破仍有效的论证 + phase 2 切 per-user 的演进路径); fallback60→6000.
核心决策:
- 6000 = 100/s 而非 600 或 60000: 100/s 吸纳所有 SPA 多页面 mount 突发 + SSE 心跳稳态 + 决策者 demo 快速刷新 + 多用户 NAT 共享 IP 场景, 同时公网爆破 100 req/s/IP 真 botnet 上游 Caddy/Cloudflare 拦截就够, 应用层不必再卷紧.
- 不引 cmd/common flag: stage 1 alpha 阶段保持简单, fallback 一行改即可. 真要 per-deployment 调用 phase 2 设置面板 "API 限速" 模块 (P2 §九 模块 6 范围) 一并做.
- per-IP 不切 per-user: 当前还没真鉴权, OIDC sub claim 进 ctx 是 stage 2 的事. phase 2 切 per-user 让内部租户脱离 IP-shaped 限速 (NAT 共享 IP 多人撞同 budget 是隐藏问题).
- 不豁免业务端点: 健康检查 / OPTIONS / Swagger 已豁免, 业务端点继续受限是对的 — 公网拿到这些路径有意义.
测试: go test -race -count=1 ./internal/server/ PASS (test 用 1000/min 显式 cfg 不依赖 fallback 默认值, 改默认无回归).
对照设计文档: P2 §九 模块 6 "审计 / 日志 / 计费" 应该新增 "API 限速 / quota" 子项 phase 2 实装 — TODO.md L699 已涵盖 stage 2 设置面板.
frontend — P2 stage 1 frontend 骨架 + 部署接入 (Vite + React + Tailwind + shadcn + R3F + React Flow, 2026-05-04)¶
ADR-0009 §7 implementation 第三+四+五步. PM 拍 "go" + "把 stage1 乱序完成 自动 commit" batch 授权下一气交付 frontend 整套骨架 + HK-133 部署接入.
新文件 (28) — 全部在 frontend/ 子目录下:
工程脚手架 (8): package.json (366 deps + 23 dev deps) / package-lock.json (npm ci 重现) / vite.config.ts (dev :5173, proxy /api/v1/ + /admin/ + /swagger/* 到本地 platform/common, build target es2022) / vitest.config.ts (拆开避免 vite/vitest plugins 类型 universe 冲突 TS2769) / tsconfig.json + tsconfig.app.json + tsconfig.node.json (strict + noUnusedLocals + path alias @/*) / tailwind.config.ts (shadcn theme tokens 全 CSS variable + flyto 双层 preset tokens) / postcss.config.js / index.html
核心源码 (16): src/main.tsx + src/App.tsx (BrowserRouter + 5 路由 + QueryClient) + src/index.css (shadcn dark 默认 + .theme-worker 浅色 override) + src/test-setup.ts + 5 pages (login / workspace / flow / runtime / settings) + 4 lib (api / sse / auth / utils) + 2 store (preset / session) + src/types/api.ts (9 schema TS 类型镜像 swagger) + src/components/ui/button.tsx (shadcn vendored cva + radix slot)
部署 (4): Dockerfile (multi-stage node:20-alpine → nginx:alpine, npm ci layer cache 优化, vite build → dist 落 nginx) / nginx.conf (SPA try_files $uri /index.html, /assets/ 长缓存 immutable, index.html no-cache 让发布立即生效, /healthz 健康探测) / .dockerignore / Dockerfile
改文件 (4):
.gitignore—frontend/tsconfig.tsbuildinfo→frontend/tsconfig*.tsbuildinfo(tsc -b 给 app/node 各产一份 buildinfo).deploy/docker-compose.yml— 加frontendservice (image flyto-agent-frontend:${VERSION:-latest}, build context=../frontend, expose :80, depends_on common). 拓扑注释更新 (everything else 从 logistics:8080 → frontend:80).deploy/Caddyfile—handle {}块 reverse_proxy 从 logistics:8080 → frontend:80. 注释解释 SPA fallback 由容器内 nginx 处理, logistics 退路径理由..gitea/workflows/release.yml— changes detection 加frontend=true(path =^frontend/); release.yml self-change 兜底全 build 加 frontend; 加Build and push flyto-agent-frontendstep (context=frontend, registry cache mode=max).
5 个页面骨架 (stage 1 stub 含义: 调真实 stub 端点 + 渲染响应 shape, 不真画 React Flow canvas / 决策树 / 头像剧院, 真组件 stage 2-4 落):
/login— OIDC 起跳 (调 /auth/login + window.assign authorize_url)/— 业务员工作台 (拉 /users/me + 3 nav 卡片到 /flow /runtime /settings)/flow— 编排页占位 (拉 /nodes/registry + /flows, React Flow canvas + 节点拖拽 + flow.json 序列化 stage 2 真实装)/runtime— 实时视图占位 (拉 /dispatches + 接 SSE event 流, 决策树 + 头像剧院 + token 流式 + await 冷场 由 Claude Design 协作流 sprint 1 prototype 真画)/settings— 设置面板 P2 §九 八模块入口 + 双层 preset 切换演示
双层 preset 机制 (P2 §V + ADR-0009 §2.3):
- Tailwind theme tokens 全 CSS variable (
--background/--foreground/--primary/ ...) - Flyto preset tokens (
--particle-density/--glass-blur/--animation-duration/--metric-scale) - Zustand
usePreset切 mode 时:root.style.setProperty改 CSS variable, Tailwind class 立即响应不重渲染整树 - 三档 mode (
worker/decision/sales-walkthrough), 默认decision(玻璃感深色, 决策者视觉基线),worker切.theme-workerclass 浅色 override
核心决策:
- Tailwind 锁 v3.4 不 v4: ADR-0009 文本说"Tailwind 4+", 实战 Tailwind v4 alpha 期 ecosystem (shadcn / autoprefixer / 各 plugins) 未齐, 落 v3.4 stable. ADR-0009 §2.1 同步更新.
- vitest config 拆开 vite.config.ts: vitest 内嵌 vite 类型跟项目 vite 6 的 PluginOption 类型 universe 不一致, 同文件
defineConfig({ test: ... })报 TS2769. 拆vitest.config.ts让两套 type 不交叉 (alias 重复声明). - shadcn vendor 不 npm: ADR-0009 §3.4 拍, 主题 override 必须自由, npm 库会锁住. 当前只 vendor 1 个 Button 占位, stage 2-3 业务实装时 vendor 更多 (dialog / form / select / toast).
- frontend Docker context = frontend/ 不是仓根: 跟 common/godoc 等 image 不一样 — frontend 是独立 npm 项目, 不依赖 Go monorepo 树, build layer 薄很多.
- SPA fallback 在容器内 nginx 不在 Caddy: nginx try_files $uri /index.html. Caddy 上游只做路径分流, 不管 SPA 路由. Caddy 层简单, 容器内自治.
build 验证: npm install 366 packages 42s, npm run build PASS — dist/ 244KB JS / 12KB CSS / 0.5KB HTML, gzip 后 ~83KB. 当前 R3F/Three/React Flow tree-shake 掉未真用进 bundle, stage 2 真用进时再涨.
stage 1 不做 (留 stage 2-4):
- 鉴权 guard (登录页跳转)
- 路由权限 (角色 gate)
- 404 页 + 错误 boundary
- 真组件 test (脚手架阶段无业务组件)
- React Flow canvas + 节点拖拽 + flow.json 序列化
- 决策树 + 头像剧院 + token 流式 (Claude Design 协作流)
- await 冷场 + L697 SSE 桥接
对照设计文档: P2 综合设计 §六 Claude Design 协作流接入路径已在 /runtime 页面占位文案体现; §九 八大模块在 /settings 页面 enumerate 出来 (phase 1 alpha / phase 2 / phase 3 标签).
下次 cut tag (v0.5.0+ 含 frontend 改) release.yml 自动 build flyto-agent-frontend image push registry, deploy job 拉新 image + Caddy reload, hub.flytoex.net 根路径活到 React SPA 工作台.
platform/common/server — P2 stage 1 stub endpoints (frontend alpha 后端骨架, 2026-05-03)¶
ADR-0009 (P2 UI frontend 架构选型 — Vite/React/R3F/React Flow) §7 implementation. PM 拍 "go" 后启动 stage 1 第二步, 落 6 endpoint 类别 11 路由覆盖 frontend phase 1 alpha 必需路径. stub 含义: 路由注册 + handler 函数 + 输入/输出 JSON schema 占位 + Swagger 注解全套. 不接 DB / 不真鉴权 / 不真 dispatch event 流, 真实装 stage 2-4 落地, 每个 handler TODO 注释标 stage 2 路径.
新文件:
platform/common/internal/server/server_p2_stubs.go(530 行) — 6 endpoint 类别 11 handler 集中 1 文件, stage 2 真实装时 split per domain 并替换. 设计选择写在文件 header 双语注释 (集中放方便一次性 split + delete 不留拆迁包袱).platform/common/internal/server/server_p2_stubs_test.go(310 行) — 14 test 覆盖路由 + status + shape 三层契约. 不锁 stub 行为细节 (stage 2 swap 时 test 不需大改).
改文件:
platform/common/internal/server/server.go—registerRoutes加 11 路由 (POST /auth/login + GET /users/me + /flows GET POST + /flows/{id} GET PUT + GET /nodes/registry + GET /dispatches + GET /dispatches/{id}/events + GET POST /billcost/dispatch/{id}/answer).platform/common/docs/{swagger.json,swagger.yaml,docs.go}—make -C core docs-swag重生, 加 11 endpoint + 9 schema (LoginRequest/Response, UserMeResponse, FlowSummary, Flow, CreateFlowRequest, UpdateFlowRequest, FlowMutationResponse, NodeSchema, DispatchSummary, AnswerRequest, AnswerResponse).
6 endpoint 类别:
POST /api/v1/auth/login— OIDC 起跳, stub authorize_url. Stage 2 真实装走auth.Verifier.GET /api/v1/users/me— 当前用户身份. Stage 2 从 auth middleware ctx claims 读./api/v1/flowsGET POST +/{id}GET PUT — flow 草稿 CRUD. Stage 2 接 flows table + tenant_id 过滤 + 乐观并发版本号 CAS.GET /api/v1/nodes/registry— 4 核心节点 (main-agent/sub-agent/await/tool.exec). Stage 2 接真 registry + vertical/tenant filter. 节点 metadata 形态 (name/namespace/kind/in_schema/out_schema/idempotent) 预留 ABI 兼容性给 ADR-0010 (待起草) 跨语言节点软约束.GET /api/v1/dispatches+GET /{id}/events(SSE) — 历史 list + event 重放. Stage 2 接 dispatches table + sessionstore 持久化 events.GET POST /api/v1/billcost/dispatch/{id}/answer— await 桥接 (L697 P2). Stage 2 接 HumanInputProvider channel.
核心决策:
- 集中 1 文件 stub 而非按 domain 拆 5 文件: stage 1 stub 临时实装, stage 2 真实装时本文件每个 handler 被相应 domain 文件替换, 集中放方便一次性 split + delete 不留拆迁包袱.
- 节点 declarative metadata 形态现在落, 跨语言 ABI 推后: PM 2026-05-03 拍 "先把物流对账和客户索赔做了之后再和 C# 团队开会决定". metadata 形态 (in_schema/out_schema/idempotent) 跟节点 namespace (flyto.core. / industry. / tenant.*) 现在落不阻塞 ABI 决策, 真有跨语言节点需求时拆 declarative 层接 HTTP handler 即可.
- handler 命名
handleP2Xxx前缀: 跟既有handleHealth/handleAgentRun/handleQuoteDispatch区分, stage 2 真实装 split 后再去前缀. - test 不锁行为细节: stage 1 stub 测试只断言 (1) 路由注册, (2) status code, (3) 响应 shape 跟声明 schema 对齐. stage 2 真实装 swap 时 test 不需大改.
测试: go test -race -count=1 ./internal/server/ — 14 P2 test 全 PASS (TestP2_AuthLogin × 2 / UsersMe / Flows × 5 / NodeRegistry / Dispatches × 2 / DispatchAnswer × 3), 既有 server / agentprompt / billrecon / quotedispatch 等测试不破坏. go vet ./... clean.
红线 / 不在本范围:
- 不改 core/pkg/engine/: ADR-0001/ADR-0005 红线.
- 不接 DB / 不真鉴权 / 不真节点 registry: 留 stage 2.
- 不立新 ADR: 这是 ADR-0009 §7 implementation, 既有方向延伸. ADR-0010 节点 declarative metadata 软约束待 stage 2 起草.
- stage 1 第三步 frontend 脚手架 + 第四步登录页 + 第五步 docker-compose 接入 后续 commit 落.
对照设计文档: P2 综合设计 §四 "现有后端 endpoint 速查" 6 个待补 endpoint 全部覆盖 + ADR-0009 §2 项目结构 (frontend/lib/api.ts 接的是这套 endpoint).
platform/common/quotedispatch — main↔sub agent 转发 loop 抽包 + 接生产 REST 端点 (P1, 2026-05-03)¶
ADR-0008 v2.2 follow-up: 把 cmd/quote-engine-probe r31 v7 实证收敛的多轮主↔子 agent 转发 loop 从单 binary 抽到 platform/common/quotedispatch/ 通用包, probe 与新接的生产 REST 端点 POST /api/v1/billcost/dispatch 跑同一份代码 (跟 ADR-0008 v2.2 schema drift 单一定义点同思路 — 字符串模板集中之后, loop 编排也集中).
新文件:
platform/common/quotedispatch/doc.go— 包文档 (双语). 明确"在本包内 / 不在本包内"边界: 拿主↔子转发的 multi-round loop + HumanInputProvider 接口 + 引擎装配工厂 + xlsx dump + 日期 post-process; 协议字符串走 agentprompt 不重复.platform/common/quotedispatch/dispatch.go—Run(ctx, Request) Result主入口. round 1 sub Send → main verdict 解析 → 走 ok/retry/await 三态. retry+new_prompt 重建 sub engine + session, retry+hint 复用 session, await 调 HumanInputProvider, max-rounds 用尽给部分结果不算错.platform/common/quotedispatch/provider.go—HumanInputProvider.Ask(ctx, q) (answer, err)接口 +StdinProvider(probe 用) +UnavailableProvider{Note}(server 默认 stub, 返 ErrAwaitUnavailable, P2 接 SSE 推 + POST 答之前的占位).ErrEOFAbort(Ctrl-D 干净 abort) +ErrAwaitUnavailable两个 sentinel error 让消费者细分语义.platform/common/quotedispatch/engine_factory.go—BuildSubEngine(SubEngineSpec) / BuildMainEngine(EngineSpec) / DefaultReflectTool(). 把 ADR-0005 中性化 flag 全套 (Toolset.None / LiteralSystemPrompt / DisableX × 7 / RequireExplicitSubAgentPrompt) 收口到这里.platform/common/quotedispatch/postprocess.go—PostProcessFinalJSON(含日期 clamp + 围栏剥) +clampOverflowDate+lastDayOfMonth+extractJSON. 与原 probe 内同款逻辑, 单一定义点版本.platform/common/quotedispatch/sheetdump.go—DumpSheetWithMerges(*excelize.File, sheet) (string, error). 镜像原 probe 私有版本, server 与 probe 共一份.platform/common/quotedispatch/*_test.go— 30+ 测试: provider 7 + dispatch 12 (含 stub flyto.ModelProvider + fakeEngineRunner test seam 让 verdict 路径 ok/retry+hint/retry+new_prompt/await/exhaust/parse-fail 全覆盖) + postprocess 11 + sheetdump 4. -race 全绿.platform/common/internal/server/quotedispatch_handler.go—POST /api/v1/billcost/dispatchmultipart/form-data 端点 (xlsx + sub_prompt + main_prompt + sheet). 同步路径: 200 + final JSON + cost 统计. await 路径: 503 + 指引文案 (HumanInputProvider 是 UnavailableProvider stub 时). bad input: 4xx. 引擎 / dispatch 错: 5xx.QuoteDispatchConfig控制 DeepSeek API key + 默认 prompt + 接LoadDefaultPromptsFromDir. swag 注解走 server.go 同款体例.platform/common/internal/server/quotedispatch_handler_test.go— 6 handler 单测: 无 cfg 503 + 缺 xlsx 400 + 缺 prompt 400 + 坏 xlsx 400 + LoadDefaultPromptsFromDir + cfg wired 路径未短路.
改文件:
platform/common/internal/server/server.go—Server加quoteDispatchCfg *QuoteDispatchConfig字段;registerRoutes加POST /api/v1/billcost/dispatch路由 (无条件注册让无 cfg 时 503 而非 404, 部署诊断友好).platform/common/cmd/common/main.go— 加--deepseek-api-keyflag (回退 DEEPSEEK_API_KEY env) +--quote-prompt-dirflag, 在--rest-addr启用时调s.AttachQuoteDispatch(quoteCfg).platform/common/cmd/quote-engine-probe/main.go— 整体改成 quotedispatch.Run 的薄壳: 保留 CLI flag 解析 + 多 provider 选择 (minimax / openrouter / deepseek) + xlsx + prompt 文件 IO, 把编排让给 quotedispatch.Run.probeBanner透 OnEvent hook 实时打 stderr 镜像旧 drainSession 输出. 删原 inline 主循环 / 引擎装配 / dumpSheetWithMerges / postProcessFinalJSON / clampOverflowDate / extractJSON / mainAgentVerdict.platform/common/docs/{swagger.json,swagger.yaml,docs.go}—make -C core docs-swag自动重生, 加/billcost/dispatchendpoint +QuoteDispatchResponse/QuoteDispatchAwaitResponseschema.
核心决策:
- 抽包到
platform/common/quotedispatch/而非扩 agentprompt: agentprompt 是协议字符串单一定义点 (ADR-0008 v2.2 § scope), 包内明文 "dispatch loop 不在本包". quotedispatch 是 agentprompt 的消费者, 单向依赖. - per-request 引擎装配: engine.Config.SystemPrompt 在 engine.New 时定死 + verdict=retry+new_prompt 必须 rebuild, 共享单 engine 跨请求会让 rebuild 路径互相影响. sub engine 跑 4-7 分钟时引擎构造开销可忽略, per-request 是简单安全选择. 多副本扩展走 ADR-0003 sticky routing.
- HumanInputProvider 接口 + UnavailableProvider stub: 现状 server UI 未接通 (P2 工作), stub 返 ErrAwaitUnavailable + Note → 503, 客户端看到 "human-input wiring pointer" 文案. 接口 + Stub 让 P2 接 SSE 推 + POST 答时改 server 一行接线即可, dispatch loop 完全不动.
- probe 改用 quotedispatch.Run: ADR-0008 v2.2 schema drift 教训 — 协议层散在多处易漂. 现在 probe 与 server 字面共享同一段 loop 代码, 只在 I/O / provider 选择 / event banner 上分叉.
红线 / 不在本范围:
- 不改 core/pkg/engine/: 引擎中性化 ADR-0001 / ADR-0005 红线. 仅 import + 装配 cfg.
- 不立新 ADR: 这是 ADR-0008 v2.2 follow-up 实施 + L692 (业务 REST 通道) 既有方向的延伸, 不引入新协议红线.
- 不实装 SSE 推 + POST 答: P2 工作, 等 UI 选型. quotedispatch.HumanInputProvider 接口为该实装预留.
- 不接 SDK 自动生成: 沿 ADR-0002 § 6 立场 (5+ 客户端语言才启用).
测试: go test -count=1 -race -timeout 300s ./... 全 platform/common 模块绿. quotedispatch 34 单测 + server 6 handler 单测 + 既有 server / agentprompt / billrecon 等测试不破坏. make -C core docs-swag 重生 swagger 三件套 (CI docs drift gate 对齐).
遗留 follow-up:
- HumanInput SSE 通道实装 (P2): UI 选型后 server 端实装真 HumanInputProvider (SSE 推 question + POST 答接 channel reply 桥接). 单点改
server.AttachQuoteDispatch注入新 provider, dispatch loop 不动. - Result.HumanQuestion 字段 (P2 minor): quotedispatch.Result 暂未把 await 路径的 question 显式带出 (handler 现从 LastVerdictReason 取近似), 接 SSE 时一并补.
- 可观测性 metric / observer: round 数 / verdict 分布 / await 触发率 / cost / latency 这些指标接 OpenTelemetry, 当前仅 stderr log, P2 实装.
ADR-0008 v2.2 — agentprompt 通用包集中 main↔sub 协议字符串 (P0, 3 commit, 2026-05-02)¶
r31 v5 full multi-round 实证暴露 schema drift bug 死循环铁证: round 2 / 3 / 4 sub text_len 字面 22955 完全一致, 烧 ~$0.057 RMB. 真因是 ADR-0008 v2 删 sub 输出 _uncertain 三件套时漏改 probe 内联字符串模板 (probe main.go:1056 还在跟 sub 说 "请按答复修正对应 _uncertain 行", sub 找不到这个被删的字段干脆原样重发). PM 评 "改协议忘了改另一端的话" — 这是分布式 / multi-agent 系统经典 schema drift bug, 协议端散在 prompt 层 / 反射器层 / 引擎层 / 消费者字符串模板 4 个 layer, 字符串模板那一档没类型签名编译期 / 测试期都不挂.
Commit 顺序:
- C1 (
598c367) 起platform/common/agentprompt/通用包: 一个 .go 文件四样:Verdict三态 (ok / retry / await_human_input) +ParseVerdict(从带围栏 / prose 前缀的 main 输出抽 verdict JSON + 校验三态 + retry 互斥 + await question 必填) +BuildAwaitMessage(question, answer)verdict=await 后给 sub 的 user message 不点名任何具体 schema 字段 让 sub 用 session history 自行定位答复对应行 +BuildRetryMessage(hint)verdict=retry+hint 后给 sub 的 user message wrap 让 sub 知道是修正提示不是新抽取请求. 测试 11 case 含回归TestBuildAwaitMessage_NoUncertainPhrasing锁措辞不再出现 "_uncertain". - C2 (
93161e6) probe 改用 agentprompt: 删mainAgentVerdict+parseMainVerdict+snippet搬包内. await 路径 line 1056 改用agentprompt.BuildAwaitMessage(verdict.HumanQuestion, answer)(旧措辞 "请按答复修正对应 _uncertain 行" 删除). retry 路径改用agentprompt.BuildRetryMessage(hint)(wrap 让 sub 知道是修正不是新任务).extractJSON保留 (postProcessFinalJSON 还在用; agentprompt 内有自己私有版本供 verdict 解析两者独立). probe build + -race 全绿. 删 3 个迁移到包内的 verdict parsing 测试. - C3 (本 commit) ADR-0008 § 8 v2.2 + CHANGELOG + CLAUDE.md prepend: schema drift 真因 + 范式级修法 + PM 4 红线 + 反向论证 4 道 + 业界对照 + 升级路径登记.
核心决策:
- 包名 agentprompt 通用 (不绑 quote / billcost): PM 拍板 "都是通用的", 物流 platform 真接同款 main↔sub dispatch 时直接复用. main verdict 三态 + retry hint + await human input 是 multi-agent 通用模式不是 quote 业务专属.
- 不抽 Dispatch loop / HumanInputHandler 接口: PM 反问 "正常一个 session 不都支持用户输入么? 引擎天然支持的情况下, 就这个屁事那么复杂么?" 戳穿过度设计. 引擎
Session.Send已原生支持多轮用户输入, bug 不出在多轮支持, 只出在"Send 给 sub 的字符串内容对不上协议"这一档. 真正最小修法是把字符串模板搬到一处, 接待器接口 / Dispatch loop 等待第二个消费者真出现时再抽 (rule of two). - 不立 ADR-0008 v3: 这是协议改了一端漏改另一端的 follow-up 修法, 不引入新红线、不改 sub↔main 契约. v2 sealed StructuralValidator 才算范式级. v2.2 修订记录足够, 后续有新协议红线触发再立 v3.
反向论证:
- 包名 agentprompt 是否过早抽象? — 否. 触发是 r31 v5 实证 schema drift bug, 物流 platform 接时一定复现 (multi-agent 通用模式). 包名通用让物流接时直接 import, 不必另起炉灶. PM 拍板"都是通用的"不是 rule of two 难题.
- sealed marker interface 防消费者绕过自拼字符串? — Go 语言层不能 100% 防, 跟 ADR-0008 v2 StructuralValidator 同 limitation. 靠 godoc + code review + 测试 lock. 跟 typed error / sealed validator 体例同档.
- 测试覆盖怎么保证? —
TestBuildAwaitMessage_NoUncertainPhrasing锁 r31 v5 真因措辞不回归 +TestBuildAwaitMessage_GuidesSubFromHistory锁措辞引导 sub 用 history 不点名 schema 字段. 协议改时这两个测试挂.
业界对照: 业界 schema drift 经典解法是单一定义点 (single source of truth) + 编译期挂掉 (例 protobuf / OpenAPI 生成 client+server 同源 schema). LLM prompt 文本协议没法 100% 类型化, 走"通用包 + 函数签名"是工程上等价的次佳方案. Pydantic AI / Instructor / LangChain BaseChatPromptTemplate / LangGraph state schema 同方向 — prompt 模板集中包内业务调函数填参. agentprompt 跟此族同档.
测试 -race 全绿: agentprompt 11 case + probe 全套 (删 3 个迁移到包内的 verdict parsing 测试) + 既有不破坏.
r31 v6 重跑期望 (PM 拍板触发, 不擅自跑): 同款 ytosample.xlsx + 同款主子组合, round 2 / 3 sub text_len 应出现真实变化 (新答复消化体现在字段值差异), 不再三轮 22955 字面一致死循环.
TD-24: engine.New 自动注 Provider.Models() — cost=$0 显示 bug 修 (P1, 3 commit, 2026-05-02)¶
config/models.go:63 注释明文设计意图 "各 provider 通过 RegisterModels(registry) 注册自己的模型" 跟实装从来没串起 — 0 provider 子包暴露顶级 RegisterModels, 0 消费者 (probe / cmd/common) 调过 register, EstimateCost 看到 cfg==nil 直接返 0. 这是 cost=$0.0000 显示真因, r31 v3 PM 看到 deepseek dashboard 累计 1.48 CNY (~$0.21 USD) 真实计费暴露此 gap. 同时 deepseek 子包静态表 4 个价格字段长期为 0 (注释明说 "TBD when consumers need it"), 即使 register 通了 cost 仍 0 — 双重原因都修.
Commit 顺序:
- C1 (
b13a3f4) deepseek 价格静态表 4 字段填空: 取自官方文档https://api-docs.deepseek.com/quick_start/pricing. Flash $0.14 input(miss) / $0.28 output / $0.0028 cache hit. Pro $0.435 / $0.87 / $0.003625. CacheWrite 留 0 (DeepSeek auto-cache 不需 explicit cache_control 不单独计费). Pro 当前 75% off 至 2026-05-31, 静态表填原价不入折扣 — 静态表跨时间稳定 + 5月31号自动恢复 + dashboard 已知有差异. 测试TestModelInfo_Pricing表驱动断言 + sanity check (cache hit 应远低于 miss). - C2 (
1e5776a) engine.New 自动注 Provider.Models() (PM 选 B 路径): 加 helperautoRegisterProviderModels(cfg, reg)在reg := cfg.ModelRegistry()之后 +agentctx.SetContextWindowProvider之前调一次. 内部context.WithTimeout(ctx, 200ms)调cfg.Provider.Models(ctx)→ 每个 modelInfo 注 ModelRegistry. 静态 provider (anthropic/deepseek/minimax/gemini/openai) 纳秒返回 OK; 动态 provider (openrouter HTTP fetch) 200ms 触底跳过 (消费者按需自缓存 fetch 结果再 register). 已 register model 不覆盖 (消费者手动优先, 自定义价格场景). 失败 / 超时 silent log stderr 不 panic 不 return err — 现状是 silent 0 cost, 失败模式同档非回归. 7 单测 (StaticProvider / SkipsExisting / TimeoutSkips / FailureSkips / NilProvider / EmptyIDSkipped / TestNew_AutoRegistersProviderModels 集成测试). - C3 (本 commit) CHANGELOG + CLAUDE.md prepend: TD-24 已登记债务不立新 ADR.
核心决策:
- timeout=200ms: 静态 provider 纳秒级返回 (4x 富裕), 动态 provider 真去 HTTP 必触底跳过. 跟 ADR-0007 capability tracking 同档"对静态 OK 对动态降级"模式. 不开 cfg 字段让消费者调 — 200ms 是工程合理通用值.
- silent 失败: 加自动 register 后失败仍是 silent log stderr → cost=0 fallback, 状态等价于 register 之前的 silent 0 cost, 不回归. fail-loud 会让 OpenRouter 网络抖时 engine.New panic 不能接受.
- 已 register 不覆盖: 消费者手动 register 应优先 (e.g. 自定义价格调整场景). 这是常见 "convention over configuration" 模式.
- engine.New 不加 ctx 参数: 现签名
New(cfg) (*Engine, error), 加 ctx 破坏调用方. 内部context.Background() + WithTimeout兜底, 缓存 fetch 优化留后续.
反向论证:
- Pro 折扣不入静态表 — 静态值跨时间稳定 dashboard 已知有差异, 修 cost=$0 → cost=理论价已够覆盖观测需求, 5月31号自动恢复正确状态.
- timeout 触底是否会让 OpenRouter 永远不 register? — 是, 设计如此 (动态 provider live metadata 该消费者按需 fetch 缓存; ADR-0007 § 2.2 bifurcate direct/aggregator 跟此对齐). 消费者 (probe / cmd/common) 用 OpenRouter 时自己跑一次
cfg.Provider.Models(longerCtx)把结果 register 到 cfg.Models 即可. - 是否该在 cmd/common / probe 里手动调 register 而不靠 engine.New? — 否. PM 反向论证: 设计图纸已立 (config/models.go:63 注释), "靠消费者每次记得调" 模式跟 ADR-0008 v2 反射器"靠消费者每次自觉" 同源问题, 引擎层契约自动接通才根治.
业界对照: LangChain BaseChatModel._get_pricing() 走 model registry 查表; LiteLLM litellm.model_cost PR-maintained model db 用消费者手动 register 顶级函数; Anthropic SDK / OpenAI SDK 不做 cost estimation 把责任推给消费者. Flyto 此前 ~7 维度 + 0 自动 register, 此次跟 LangChain 同档 (provider 子包静态表 + engine 层自动 wire).
测试 -race 全绿: core engine 7 auto-register 单测 (除 3 个 pre-existing fail/panic 与本无关) + deepseek 子包 1 pricing 测试; platform/common 全套 (billrecon/llm 17 + responseguard) 不破坏.
r31 v5 实证连带验证: 跟 P2 反射器观测性一起重跑时, turn_end / done event 的 cost 字段应显示真值 (不再 $0.0000). deepseek-v4-flash 8K input + 16K output 一轮 ≈ $0.0056 ($0.14 × 8 / 1M + $0.28 × 16 / 1M). 跟 dashboard 累计真实计费应在 5% 内对齐 (考虑 Pro 75% 折扣后实际计费比静态表低).
ADR-0008 v2.1 — 反射器 happy path 观测性 gap 修 (P2, 3 commit, 2026-05-02)¶
ADR-0008 v2 引擎层契约落地 + r31 v4 实证 round 1 通过 (sub tool_use=0 / final text 21381 字符 / main v4-pro 业务判 verdict=retry "面单首重(0kg)" base_weight 应=0) 后, PM 反问 "反射器跑了没" — log 0 行可答, 必须从代码路径 (probe sub engine cfg + sub final text 经反射器逻辑放行) 推断, 直接证据缺失. 真因: 引擎 fail 时 emit WarningEvent code=response_reflector_block, PASS 完全静默. 跟上次会话 silent compact 5 路径同源观测性 gap (上次修了 compact, 反射器没修).
PM 反思反射器机制本身 — "如果 sub 不能对反射结果做任何反应, 反射的意义在哪里? 总要反射回去给会反应的人" — 戳穿 v2 描述含糊, 拉清两条 retry 路径分工: (A) 反射器→sub turn 内修 (引擎 hook fail 时注 user message, sub 同 turn 看到反馈修, 最多 ResponseReflectorMaxBlocks=3 次, ADR-0008 v2 已立) vs (B) main 业务判→重派 sub (round 路径 retry, main 业务 cross-check verdict=retry 重写 sub prompt rebuild engine + 新 session 进 round 2). 两条路径独立并行, 反射器和 main 之间完全脱钩.
Commit 顺序:
- C1 (
ed54e1f)flyto.ResponseValidatedEvent类型加:core/pkg/flyto/events.go加事件类型 (Approved bool / ValidatorName string / Reason string / BlockCount int / MaxBlocks int / Turn int). godoc 详细说明 4 verdict 形态 (PASS first try / PASS after self-correction / Fail mid-turn / Fail at cap) + Pre-X 段说明历史 gap.events_test.go加 schema 测试:TestEventType_AllEvents加新 row +TestResponseValidatedEvent_Schema4 case round-trip + EventType 不变 + BlockCount<=MaxBlocks 不变量反 false-pass. - C2 (
ddf0c2c) engine.go runLoop 6.5 段 wire emit: 单点 emit 在 PASS / Fail 分支处理之前, BlockCount 是 post-increment 后的值 — PASS 看到本 turn 之前累计 fail 次数 (0 = 一次过), Fail 看到含本次的累计. 双通道并存 (Fail 路径双 emit ValidatedEvent + WarningEvent, PASS 路径仅 emit ValidatedEvent), 跟 ToolResultEvent + tool_executed observe 同档. 3 emit 实证测试response_reflector_emit_test.go(~270 行):OnPass(单 emit Approved=true BlockCount=0 验 happy path 不再静默) /OnFailThenPass(双 emit Fail/Pass + BlockCount 1→1 PASS 不增) /AtCap(MaxBlocks 个 emit BlockCount 1→2→3 + WarningEvent response_reflector_max_blocks 同时 emit). - C3 (本 commit) probe banner + doc 同步:
quote-engine-probe drainSession加 case*flyto.ResponseValidatedEvent打 banner[reflector] approved validator=billcost_reflect block=0/3 turn=1(Fail 时附reason=...); ADR-0008 § 8 v2.1 修订记录 (含 r31 v4 实证表 + MAOS 反射器机制对比 + 4 反向论证 + 业界对照 + 单一 emit 点设计选择 + r31 v5 重跑期望); CHANGELOG; 本 CLAUDE.md prepend.
核心决策: 单一 emit 点 (Validate 完之后无条件 emit + 然后走 fail 分支), 替代双 emit 点 (PASS 在 if-else 块内 / Fail 在 if 块内) — 单点 emit 字段语义统一 + post-increment 一处算 + 后续维护新字段一处改 + godoc 描述跟代码 1:1 对齐.
反向论证: (1) PASS emit 噪音? — sub 一次 turn 反射器最多跑 maxBlocks+1=4 次, 量小, 跟 turn_end / tool_use 同档; (2) 复用 WarningEvent code=response_reflector_pass? — 否, WarningEvent 语义"出问题能继续", PASS 不是 warning 不能滥用, 跟 CompactEvent.Kind 加 micro/full 区分同精神拒绝事件类型语义漂移; (3) fail 路径双 emit 重复? — 不重复, ValidatedEvent 是机器可读 verdict 通道 (Web UI / 监控 / audit sink 字段消费), WarningEvent 是人类可读告警通道 (字符串模板); (4) emit 时机 (Validate 前 / 后)? — 后, verdict 出才有意义.
MAOS 反射器机制对比 (cowork claims-attribution + test-sdk-retry.mjs):
- MAOS: 反射器 = LLM 视野内 MCP tool (claims-verify-result 6 参数), tool 返 isError=true + 字符串 issues, Anthropic Agent SDK 自动塞回下一轮, LLM 主动决定调几次 verify, retry budget = SDK query({maxTurns: 50}) 整个 agent 总 turn 数. LLM-decided retry, 反射器对 LLM 可见.
- Flyto v2: 反射器 = 引擎 hook 不可见 (Toolset.None()), engine 在 stop_reason=end_turn 强制跑 ResponseReflector.Validate, fail 时引擎自己注 user message 给 sub 同 turn 修, retry budget = ResponseReflectorMaxBlocks (turn 内 cap, 不是整个 agent 总数). engine-decided retry, 反射器对 LLM 不可见.
跟 MAOS LLM-as-tool-caller 形态本质不同 — ADR-0008 v2 拍板纯 hook 就是否决 MAOS 那种, 防止 sub 学会"主动调反射器规避" + 防止反射器 vs ordinary tool 语义模糊.
测试 -race 全绿: core engine 3 emit 测试 (除 3 个 pre-existing fail / panic 与本无关: TestEmitCheckpointSuggested_Bash{Dangerous,Safe} / TestRunWorkers_TaskNotification / TestSubAgent_Forward_ConcurrentSubAgents); flyto 22 schema row + 4 case Schema; platform/common 全套 (billrecon/llm 17 + responseguard) 不破坏.
r31 v5 重跑期望 (PM 拍板触发, 不擅自跑): probe 拉 ResponseValidatedEvent banner 直接打 [reflector] approved=... block=N/3 turn=K, PM 实证从 log 直接看到反射器跑了几次 / 每次 verdict 是什么, 不必再凭代码路径推断.
ADR-0008 v2 follow-up: 引擎层反射器契约 (StructuralValidator sealed interface + Toolset 双挂 fail-fast, 3 commit, 2026-05-02)¶
ADR-0008 v2 范式 (反射器纯安检门 sub 看不到反射器存在) 上次会话 commit 82fa66e 在消费者层 + prompt 层落地. PM 跑 r31 v1+v2+v3 三次跨 model 实证 (主子组合 minimax / deepseek-flash / deepseek-pro 全排列, 同款 problem 3 sub round 2 跨行扩散到上海) 后戳穿: "反射器强制本应是引擎层契约保证, 不是消费者每次自觉" — 上次改的是消费者层 (probe cfg.Toolset.None()) + prompt 层, engine.go 0 行改, 下次别的消费者 (logistics platform / billrecon 业务) 用 ResponseValidator 也会重蹈覆辙. 本 follow-up 3 commit 落地引擎层契约根治. ADR core/docs/adr/0008-quote-probe-protocol-revisions.md § 8 v2 修订记录.
Commit 顺序:
- C1 (
11112eb) 接口分裂奠基 —validator.StructuralValidatorsealed interface +ErrReflectorDoubleWired: 走 ADR-0006 § 3 红线 + ADR-0007 § 2.3 红线同模式 (设计层契约 + 类型签名 + godoc + 替代路径显眼). 不靠 runtime 强制 (做不到 100%), 靠类型签名层让消费者自然分流到对的位置. 新加validator.StructuralValidatorsealed sub-interface (嵌入Validator+ 包内私有structuralMarker()method) +validator.StructuralMarker嵌入 helper (~100 行新文件). 包外类型必须 import validator 包并嵌入 StructuralMarker 才能满足接口 — 嵌入即声明契约 "无 LLM 调用 / 无业务判断 / 仅做确定性 schema/parse/单位 校验". 错误码flyto.ErrReflectorDoubleWired+engine.ErrReflectorDoubleWired双声明 mirror (沿用 ADR-0006 mirror 模式让消费者用 errors.As 一致识别). - C2 (
651b504) engine.Config 字段重命名 + 类型收紧到StructuralValidator+ Toolset 双挂检测 + 全消费者迁移:cfg.ResponseValidator validator.Validator→cfg.ResponseReflector validator.StructuralValidator(含ResponseValidatorMaxBlocks→ResponseReflectorMaxBlocks同步,responseValidatorBlocks变量 +"response_validator_*"WarningEvent code 同步 batch rename). godoc 第一行红线引 ADR-0008 v2 + CRITIC 框架 + 业务校验三条决策层路径 (main agent verdict cross-check / staging ML / await_human_input). engine.New buildToolRegistry 之后加 Toolset 双挂检测 —cfg.ResponseReflector.Name()跟cfg.Toolset.Resolve()撞名 → 拒构造返ErrReflectorDoubleWired(装配期 fail-fast, 不 silent override 不 warning, ADR-0006 § 3 红线延续). 双挂检测依据:validator_adapter.go注释明文设计意图 "Validator 与 Tool 共享 Name 让审计日志可 join" — 引擎层依此契约做检测合法. 现有消费者全迁移单 commit 保 build/test 干净:QuoteResponseValidator→QuoteResponseReflector+ 嵌入StructuralMarker+ 编译期断言改validator.StructuralValidator;JSONVerdictValidator同款嵌入 + 编译期断言同步;quote-engine-probe+reflect_tool.go+responseguard/doc.go字段引用全 batch rename. - C3 (本 commit) doc + ADR-0008 § 8 v2 + CHANGELOG + CLAUDE.md prepend + 引擎双挂检测 5 单测:
core/pkg/engine/reflector_double_wire_test.go新文件 (TestNew_ReflectorDoubleWired_FailsFast 双挂拒返 ErrReflectorDoubleWired / TestNew_ReflectorAndDifferentNamedTool_OK 不同名通过 / TestNew_ToolsetNoneWithReflector_OK ADR-0008 v2 quote-engine-probe 典型配置 / TestNew_NoReflector_OK 向后兼容 nil 路径无侵入 / TestStructuralValidator_SealedInterface_QuoteResponseReflector 编译期断言 marker 机制) + ADR-0008 § 8 v2 修订记录 (含 r31 v1+v2+v3 实证表 + 4 反向论证 + 业界对照 + CRITIC 框架对照 + 业务校验三条决策层路径 + hook 列表方案否决理由 + tracked debt 调整) + 本 CLAUDE.md prepend + HANDOFF.md 删 (跨用户接手完成).
ADR-0008 v2 引擎层契约红线: 反射器槽位 (cfg.ResponseReflector) 仅接受 validator.StructuralValidator (sealed sub-interface, 强制嵌入 StructuralMarker 声明契约). LLM-backed 业务校验禁止挂此槽位 — 业务校验赶决策层三条路径: (1) main agent verdict cross-check (LLM 协议层决策当下) / (2) staging ML 验证 (决策提交前 ML 二审, L434 待加) / (3) await_human_input (人工最终判断). 跟 ADR-0006 § 3 + ADR-0007 § 2.3 红线同精神 — 设计层契约 + 类型签名 + godoc + 替代路径显眼.
hook 列表方案否决: 中途讨论提"升华版" cfg.Hooks.PostResponse []validator.Validator 跟 PreToolUse 同形态 hook 列表. PM 戳穿 "实践证明业务反射器不可靠, 反射器只做基础逻辑校验" — hook 列表反而鼓励错误用法 (消费者看到能挂多个反射器自然想加 schema + 业务两个反射器, 业务反射器违反 CRITIC 框架). 撤回 hook 列表方向, 走类型签名层收紧 (StructuralValidator sealed) + 业务校验赶决策层 — 不是放宽, 而是收紧.
业界对照: Pydantic AI 的 result_validators 跟 tools= 列表是装饰器 vs 参数两个名字空间, 语言层面就不能交叉 (隐式 sealed 模式). Claude Agent SDK / OpenAI Assistants / LangGraph 没有 ResponseValidator + Tool 双 wire 形态. Flyto 独有双 wire 形态, 检测算法跟自己设计契约 (Validator + Tool 同名 = 同身份) 对齐. CRITIC 框架 (MAOS commit e9e09e463c) 实验: 简单 6 参数自检 30/30 vs 复杂 9 参数 4/9 — LLM 自评工具复杂度临界点 ~7-8 参数, 超此点准确率断崖跌, ADR-0008 v1.1 sub agent ambiguity 决策违反此点.
测试 -race 全绿: core engine 5 双挂检测单测 + validator 全套含编译期断言 + platform/common 全套 (billrecon/llm 17 测试 + responseguard 测试 + quote-engine-probe smoke). 既有不破坏 (TestEmitCheckpointSuggested_BashDangerous 是 pre-existing nil pointer panic 与本无关).
r31 v4 实证待跑 (PM 拍板触发, 不擅自跑): ADR-0008 v2 修法 + 引擎层契约同时生效. 期望 sub LLM tool_use=0 (Toolset.None 物理看不到反射器); engine ResponseReflector 兜底强制跑反射器 hook 在每个 final-text turn; sub round 1 直接按推断填全部字段; main agent v4-pro cross-check sub vs 原表 dump 应识别 4 行单带费 base_weight 强猜 → verdict=await_human_input + human_question 单行问深圳; PM 答深圳后 sub round 2 复用 session 重抽只更新深圳不跨行扩散; round 2 main cross-check 上海 / 苏州 / 崇明 还是强猜 → verdict=await 下一行 → 多轮收敛. 装配期双挂检测验证: 任何未来消费者把同名 Tool 既挂 Toolset 又挂 ResponseReflector → engine.New 立即拒构造返 ErrReflectorDoubleWired, 不到 runtime.
Engine: maybeCompact + forceCompact 全 5 路径 emit CompactEvent — observability bug 修复 (2 commit, 2026-05-02)¶
engine.go maybeCompact + forceCompact 5 条压缩路径历史上仅 1 条 emit *flyto.CompactEvent, 其余 4 条 silent — 消费者 (probe / orchestrator) 看到 token 预算回弹但拿不到事件, 无法在 banner 区分 micro/full compact 还是其他原因. 5 处补 emit + 加 CompactEvent.Kind 字段 ("" legacy / "micro" / "full") 让消费者根据 kind 区分压缩档位.
Commit 顺序:
- C1 (
7172f04) engine,probe: 5 silent compact 路径补 emit +CompactEvent.Kind字段; probe banner 加[compact:%s]kind 显示 - C2 (
d587cc6) test(engine):TestMaybeCompact_EmitsFullCompactEvent完整压缩 emit 实证 (236 行 mock provider 测试)
测试 -race 全绿: events_test.go schema 测试 + compact_event_test.go 完整压缩 emit 测试. 既有不破坏.
ADR-0008: quote-engine-probe LLM/Agent 协议层 4 设计修正 (3 commit, 2026-05-02)¶
ADR-0007 follow-up r26+ 物流业务 round 2 收敛后, PM review 价格输出反思 4 个我 (Claude) 之前没经讨论擅自做的设计: (1) sub_agent.md schema 跟 WMS 不对齐 / (2) 接受"无法识别"部分 (无 ambiguity marker) / (3) main agent 与人对话能力 (verdict 只 ok|retry) / (4) 日期合法性校验从 LLM prompt 移到 Go 后处理. PM 拍板"一气呵成" 4 项一起改, 走 3 commit 节奏.
Commit 顺序:
- C1 (
4616ae2) sub_agent 直出 WMS schema + _uncertain marker + reflect_tool WMS 输入: sub_agent.md 输出 schema 改{master + details[] + return_fee?}直接对齐 WMSShipCostCfgMaster + ShipCostCfg(parser/adjustment.go ADR-0004 alpha.11+ 实装). 0 in-process mapping 落库. 加单位铁律段 (g 锚: 1kg=1000) 让 LLM 不照搬 kg. 加 _uncertain ambiguity marker 三件套 (_uncertain bool + _raw_text + _reason). 删日期合法性校验段. 4 例 (5 段全功能 / 3 段无单带费 / 偏远独立 PM 强调今 sample 漏过 / _uncertain marker 示范). reflect_tool.go 输入改 WMS 形态,_uncertain=true行 skip 不构造 band. 反射器单测 +3 (UncertainSkipped / PassWithUncertainAndConcrete / StripFeeRouting). - C2 (
21fdadb) main verdict 加 await_human_input + Go 日期 clamp: mainAgentVerdict 加 HumanQuestion 字段 + parseMainVerdict 接受 "await_human_input" 第三态 + 校验 human_question 非空. main loop 加 await_human_input 分支:bufio.Reader.ReadString('\n')不用 Scanner (advisor 提示 64KB silent truncate) + EOF (Ctrl-D) 当 abort + 答复包到 prevFeedback. 加lastDayOfMonth + clampOverflowDate + postProcessFinalJSON:time.Parse失败且日越界 (4/6/9/11→30 / 2→28-29 含 Gregorian 闰年 / 其余→31) clamp + warning. main_agent.md 加决策树 5 条 (选 ok/retry/await 哪条) + 字段互斥规则 + "日期合法性不归你管"段. 单测 +14 (clampOverflowDate 4 块 / postProcessFinalJSON 5 块 / parseMainVerdict 3 块). - C3 (本 commit) doc + ADR-0008 + TODO + CLAUDE.md 同步: ADR-0008 (8 节体例对齐 ADR-0007: Status/Date/Deciders/Related/Context/Decision/Alternatives/Reverse thinking/升华/Triggers/Status note + 修订记录) + 业界 HITL prior art 对照 (LangChain ask_human / OpenAI Assistants requires_action / Claude Agent SDK ElicitationHandler / CrewAI HumanInputMode) + WMS-aligned schema 对照 (Stripe Products+Prices / ShipBob ServiceLevels / Pydantic AI schema validator+retry).
ADR-0008 § 2.3.3 红线: await 路径走 stdin 单进程, 不抽 PermissionBus / SSE / IM bot 接口 (rule of two: probe 是当下唯一 consumer, IM bot 是第二个时启动抽象). 业务 logistics 接 IM 时升级到 SDK 接口, stdin 路径作为 reference 实现保留.
ADR-0008 § 2.1.2 红线: parser/quote_to_bundle.go (441 行 mapper) 不动 — bill-recon 平行 vlm 路径在用, 跟 quote-engine-probe 不耦合. 等 vlm 路径未来也直出 WMS schema 时一起退役 (§ 6 触发条件登记).
测试 -race 全绿: 14 quote-engine-probe 单元测试 (clampOverflowDate / postProcessFinalJSON / parseMainVerdict) + 7 billrecon/llm reflect 测试 (含 3 新 _uncertain / strip routing 等) + 既有不破坏.
实证待跑: PM 用 ytosample.xlsx + minimax main + deepseek-v4-flash sub 跑 r27+, 期望 round 1 sub 标 _uncertain (深圳/崇明) → main verdict=await_human_input + human_question → probe stdin 阻塞 → Claude 充当桥梁 PM 答 → round 2 收敛.
ADR-0008 v1.1 follow-up: r27-r30 实证暴露 3 层独立问题 + 引擎 NewSession API 设计 (5 commit, 2026-05-02)¶
ADR-0008 v1.0 (3 commit) 落地后 PM 跑 r27-r30 实证, 暴露 3 层独立漏洞 + 1 个引擎 API 设计问题, 5 commit follow-up 落地修补. ADR core/docs/adr/0008-quote-probe-protocol-revisions.md § 8 v1.1 修订记录.
Commit 顺序:
- C4 (
e7d2c09) reflect_tool 容忍 markdown 围栏 + prose — r27 实测 deepseek-v4-flash sub agent 给最终 JSON 加json ...围栏, 引擎 ResponseValidator 路径 (走 QuoteResponseValidator → ReflectQuoteTool.Execute) 严格 unmarshal 撞 'å' 拒收 (block 1/3 → block 2/3), sub 重试 loop 跑超 perSessionTimeout 10m. PM 评 "比较傻 — 人家只是加了个 json 围栏". 实际数据 sub 抽对了 (31 省按一/二/三区分清续重价 0.6/1.5/2 + 偏远 6 省独立段 + 5 个 _uncertain 单带费), 只是输出形态被 strict unmarshal 拦下. 修法:ReflectQuoteTool.Execute第一次严格 unmarshal 失败时跑extractJSONPayload(剥单层json ...围栏 + 截首 '{' 末 '}' 剔 prose) 再 unmarshal. happy path 0 开销 (干净 JSON 第一次就 pass), 真 malformed (纯 prose 无 JSON) 仍 IsError 不软化契约. Tool 层鲁棒让两路径 (sub agent tool_use + engine ResponseValidator) 都受益. +4 测试 (TolerateMarkdownFences / TolerateProsePrefix / TolerateCombinedFenceAndProse / StillRejectsTrueGarbage). - C5 (
16cf5a9) postProcessFinalJSON 也加 fence 容忍 — r28 实测 sub 4m33s 出 final + main verdict=ok 5m 收敛, 但postProcessFinalJSON撞同款 fence: sub final text "反射器通过 0 违规。现在输出完整 JSON:\njson\n{...}\n", strict json.Unmarshal 撞 'å' 报错 → 跳过日期后处理 → outPath 写的是 prose+围栏混合 (消费者拿到无法 1:1 unmarshal, 违反 ADR-0008 § 2.1 "0 mapping 落库" 前提). 修法: postProcessFinalJSON 入口先调extractJSON(它本身已经先 stripFences 再截首 '{' 末 '}'), 然后 unmarshal cleaned payload. 即使没 clamp 改动, 返回 cleaned payload 让 outPath 写干净 JSON. 重命名末段 cleaned → indented 避免与新顶级 cleaned 重名. +1 测试 (TolerateFencesAndProse 验 r28 真实形态: prose 前缀 +json 围栏 + 后缀 prose + 内含 9月31日 → clamp 9月30日, outPath 不残留或 prose 字面). - C6 (
a14a3c6) main_agent 见 _uncertain 必 await 强制不绕 — r29 实测 main 见 4 个 _uncertain=true 行 (上海/深圳/苏州/崇明 单带费) 自评 "标记合理 ... 均属原表模糊范畴" 直接 verdict=ok 没 escalate. PM 拍板 "如果有模糊部分得问人, 停下来, 等指示" — main 不应自己判断 "是不是真模糊", 见 _uncertain 必走 await. main_agent.md 决策树改: 第 3 条 "歧义源于原表本身模糊→await" 改 "任何 _uncertain=true 行→await 停下问人不绕过" / 第 4 条 "_uncertain 可 prompt 教 sub agent 解→retry 教规则不问人" 删除 (即使能教也不绕过, 让人确认才上路) / 重要段补 "判不准时倾向 ok 但 _uncertain 不属于此条". 效果: 见任何 _uncertain → 必 escalate stdin → PM 答 → 下轮 sub 重抽该行落 _uncertain=false 或更新值. 即使下游可以 query _uncertain=true 看 marker, main 也不能跳过 escalate. - C7 (
c57bd03) probe sub session 跨 round 复用 — r30 实证 PM 答深圳 round 4+6+8 三次 sub 都没消化, 真因是 probe 每轮subEng.Session(fmt.Sprintf("sub-r%d", round))用 round 编号当 session id 故意每轮新建. PM 答经 prevFeedback 单一字符串覆盖, round 7 PM 答上海/苏州/崇明 3 条覆盖了 round 4+6 答深圳的内容, sub round 8 session 是 fresh 看不到深圳曾被答过. PM 反问 "session id 字面就是会话标识不是 turn 标识" 直接戳穿 — 我把"会话"当成"轮次"用了, 概念错位.engine.Session.Send(session.go:117) 本来就支持: 同 session 多次 Send 自动累积 message history (快照 history 喂 Run + trackEvents 完成时 append user/assistant 回 s.messages). 修法: subSession 在 loop 外建一次 "sub" 单 id 跨 round 复用; round 1 user prompt 同原来 (开场指令), round 2+ 直接发 PM 答 / main feedback 短消息 — sub 自己 history 记得自己上轮出过什么 + main 的 question, 不再 wrap "上一轮未通过, 反馈如下..." 长 prompt 重发 (旧实现重发让 history 同信息 2 次记录浪费 token + LLM 可能误以为是新独立任务); await 路径 prevFeedback 简化"人工答复:, 请按答复修正对应 _uncertain 行" 不再重发 question + reason; verdict=retry+new_sub_agent_prompt 路径必 close+rebuild engine+新 session (system prompt 漂 history 失效); loop 退出后一次性 subSession.Close() (sync.Once 保护 retry 路径已 close 的不会重 close). prompt cache 自然受益 (Anthropic 5min TTL + DeepSeek KV cache, multi-turn 重发同 system+history 高 hit). - C8 (本 commit) engine 加 NewSession() + Session(id) godoc 补完整 + 4 单元测试 + ADR-0008 v1.1 — advisor 关键洞察:
Session(id)强制要 id 把消费者推到 "loop 内造 id" 错误思考线上, 让 probe 作者顺手抓 round 编号当 session id. 加Engine.NewSession() *Session自动生成 id (session-<unix-nano>-<8 hex>格式镜像 generatePlanID, plan_queue.go:670), 让"开新会话"无 id 钩子, 消费者自然写 loop 外建一次.Session(id)godoc 补完整 (双语 ~110 行, 含 multi-turn idiomatic 范式 + 反模式 r30 警告 + 何时用 vs NewSession + Close 后语义 + context window 自动压缩). 4 单元测试 -race 全绿 (TestNewAutoSessionID_FormatAndUnique 1000 iter / ConcurrentUnique 50×50 goroutine / TestEngine_NewSession_AndSessionGetOrCreate 验 NewSession 多次返不同 + s1 ID 后续 Session(id) 拿回同 ref / TestEngine_Session_SameIDReturnsSameInstance 验 get-or-create). ADR-0008 § 8 修订记录 v1.1 含 5 commit + 业界对照 (Claude Agent SDK create_session / OpenAI Assistants threads.create / LangGraph Checkpointer + thread_id / CrewAI memory=True flag).
核心发现 — agentctx 三件套已就绪不必新加: PM 反问 "明明记得有会话压缩和会话记忆存档" 戳穿 sub agent 调研漏扫. 实查 core/pkg/context/ (1300+ 行) 含 Compactor / CompactionPolicy / CompactCircuitBreaker 防自压自杀循环 / MicroCompact / EstimateTokens / WithTokenGap 精确跳步; core/pkg/engine/session_snapshot.go 含 SessionSnapshot + SnapshotStore + FileSnapshotStore + ResumeConversation 跨进程恢复; core/pkg/engine/session_persist.go 含 Transcript + SaveTranscript / UpdateTranscript / LoadTranscript; engine.go runLoop 已 wire compact (engine.go:3741 tokenBudget.AutoCompactThreshold + context_calibrator.go 实测校准 + ErrContextOverflow 已分类). 本次 ADR 仅在 NewSession + Session(id) godoc 链接这些已就绪机制让消费者发现, 不新加.
关键概念错位 — session id ≠ turn id: 这是本次 follow-up 最深层教训. probe 把 round 编号当 session id 是把 "会话" 当 "轮次" 概念错位. session 是承载多轮对话的容器, 一个 session 可以 Send 多次累积 history; turn 是 session 内的单次 user/assistant 交换. PM 直接戳穿 "session id 字面就是会话标识不是 turn 标识" — 引擎层 godoc 一行 "创建一个有状态的多轮会话" 没明写 get-or-create + 多轮范式 + Close 后语义, 是 godoc 单薄但不算误导 (字面读对了就懂). 真正错的是 API 设计强制要 id 把消费者推到 "loop 内造 id" 思考线 — 加 NewSession 移除此钩子.
业界对照新增 (sub-agent context 管理 prior art): Claude Agent SDK client.create_session() (无 id 自动生成 uuid) / OpenAI Assistants client.beta.threads.create() (server-side state 累积) / LangGraph Checkpointer + thread_id (跨进程 resume) / CrewAI 显式 memory=True flag. flyto NewSession + Session(id) + SnapshotStore 三层覆盖等同 LangChain/LangGraph 同档抽象.
测试 -race 全绿: core 4 NewSession + platform/common 14 quote-engine-probe + 11 billrecon/llm reflect 测试 + 既有不破坏.
实证待跑 (PM 拍板触发): r31 用新 probe (sub session loop 外复用) + 新 engine API. r30 因 PM 答深圳 round 4+6+8 三次 sub 都没消化触底 max rounds 已 kill; r31 期望同款 PM 答只需一次, sub session history 自然累积消化所有答复 round 1-3 内收敛.
ADR-0007 follow-up: DeepSeek 官方直连 + TD-20 实证 + Mix engine 物流胜利 (5 commit, 2026-05-01)¶
ADR-0007 Capability tracking 决策的实证 + 第 5 个 direct provider 接入. PM 反向论证否决 "openai provider + BaseURL 借壳" 工程捷径, 拍板 deepseek 走独立子包 (与 ADR-0007 § 2.1 direct provider 列表一致). TD-13 + TD-20 落地实施 + 实证, 物流业务 r 系列首次见 deepseek-v4-flash 收敛 (r22-r25 全失败 → r26+ round 2 收敛).
Commit 顺序:
- C1 (
79a5be2) wire/openai.go DeepSeek 顶级 prompt_cache_hit_tokens 字段 fallback: openaiChunk.Usage 加 PromptCacheHitTokens / PromptCacheMissTokens 顶级字段, buildUsageEvent 在嵌套 OpenAI 字段 (prompt_tokens_details.cached_tokens) 未填时用 deepseek 顶级字段 fallback. 嵌套优先保证 OpenAI 原生路径零回归; 双填假设性 provider 走嵌套防双计数. ADR-0007 § cache mapping 跟踪. 2 测试新增 (DeepSeekTopLevelCacheHit / NestedFieldWinsOverTopLevel). - C2 (
01b2022) deepseek provider 子包 (ADR-0007 § 2.1 第 5 个 direct provider):core/pkg/providers/deepseek/双模式 (ModeOpenAI 默认主路径 / ModeAnthropic 备选, 跟 minimax 同款). 2 V4 model 静态表 (deepseek-v4-flash + deepseek-v4-pro), ModelInfo ADR-0007 三字段静态填: ProviderKind="direct" + ToolNameRegex=^[a-zA-Z0-9_-]+$+ ReasoningPassbackMode="string" (r24 真因 + 官方 /guides/thinking_mode 文档双确认). resolveCapabilities provider-level fallback 让消费者忘记 RegisterModels 也能接通 reasoning passback (硬要求, 否则多轮 tool calling 必被服务端 400 拒). shared.CheckNoImageBlocks 在 Stream 入口拒 image (V4 文档无 vision 支持). 13 unit test 全绿 -race. - C3 (
a6f0c1d) capability-probe 接入 deepseek + caching ladder 路径: 加 DEEPSEEK_API_KEY env 读取/校验 + deepseek 2 targets (走 ModeOpenAI 主路径). caching 探测路径修复: 默认 generic 路径系统提示只 ~10 tokens 触发不了 deepseek auto-cache, 给 deepseek target 设 cachingProvider 走 probeCachingProvider 的 100/300/600/1200 重复 phrase ladder. 实证: 7/7 capability ✓, cache rd=1152 接通. - C4 (
02cf0b3) TD-20 capability tracking 三 prober 实测: CapabilityResult / target / ModelCapabilities 三 schema 扩 ADR-0007 三字段. probeToolNameRegex (单点探测 dotted name) + probeReasoningPassback (2 round-trip 测 reasoning_content passback 协议) + ProviderKind 静态标 (注册期人类知识). 5 target 注册点全填 providerKind. 实证完美命中: deepseek-v4-flash 服务端字面回 regex^[a-zA-Z0-9_-]+$(跟 ModelInfo 静态值一致) + passback "string" 报 r24 真因 "must be passed back" (字面). OpenRouter 经 Azure 路由 claude 4.6 系列实证 regex^[a-zA-Z0-9_-]{1,128}$(实际比 anthropic provider 静态填的 1-64 更宽, TD-26 follow-up). ADR-0007 § 2.2 bifurcate direct vs aggregator 决策得到强力实证: 同一 deepseek-v4-flash model id, direct strict + passback string vs aggregator permissive + passback none. - C5 (
ff08515) quote-engine-probe 接入 deepseek + 物流 r26+ 实证胜利: 加 deepseek 选项 (主+子可独立选 minimax/openrouter/deepseek). 跑参 main=minimax-M2.7-highspeed sub=deepseek-v4-flash 官方直连 → round 2 收敛 (7m37s). 跨厂商主+子架构验证通过, ADR-0007 C4 wire 层 reasoning_content passback 在生产场景真生效 (子 agent ~6m thinking + 多轮 tool calling 没被 deepseek 服务端 400 拒).
ADR-0007 § 2.2 bifurcate 实证矩阵 (TD-20 prober 真实数据):
| Path | model id | regex | passback | 来源证据 |
|---|---|---|---|---|
| direct (deepseek 官方) | deepseek-v4-flash | strict | string | 服务端 400 字面 ^[a-zA-Z0-9_-]+$ + "must be passed back" |
| aggregator (OpenRouter) | deepseek/v4-flash | permissive | none | 后端路由 (SiliconFlow/Together) 不预校验 + 不强制 passback |
| aggregator (OpenRouter→Anthropic) | claude-haiku-4.5 | strict | - | 服务端 400 字面 ^[a-zA-Z0-9_-]{1,128}$ |
| aggregator (OpenRouter→Azure 新发现) | claude-opus-4.6 | strict | - | Azure 400 字面 ^[a-zA-Z0-9_-]{1,128}$ (anthropic provider 静态 1-64 实际更严) |
物流 r 系列对照 (业务命门追踪): - r21 minimax-only sub agent: 0 violations 收敛 (历史 baseline ✅) - r22-r25 deepseek-v4-flash via OpenRouter: 全失败 (engine 中性化 + ADR-0007 之前) - r26+ deepseek-v4-flash 官方直连 (本次): round 2 收敛 ✅ — ADR-0005 + ADR-0006 + ADR-0007 累积修复全栈胜利
Tracked debt (ADR-0007 § 7 调整):
- TD-13 (capability-probe tool name regex 启动期校验) → drained (probe 实证完成, 数据落 capabilities.json)
- TD-20 (capability-probe 实测扩 3 项) → drained (probeToolNameRegex / probeReasoningPassback / probeProviderKind 全部实施 + 实证)
- TD-22 (OpenRouter per-model ReasoningPassbackMode 实测填) → partial (probe 已跑出 8 model 数据, 真消费者用 RegisterModels 接通留 follow-up)
- 新 TD-26: anthropic provider 静态 ToolNameRegex ^[a-zA-Z0-9_-]{1,64}$ 实际太严, OpenRouter 经 Azure/Anthropic 实证 1-128, 调研真实上限 + 调整
- 新 TD-27: OpenRouter→Azure 路径 (claude 4.6 系列实测发现) — capability 跟 Anthropic 直连可能不一致, 监控+对照
- 新 TD-28: anthropic / minimax provider 也跑 TD-20 prober (本次未跑节省配额)
测试 -race 全绿: wire/openai_test.go +2 (DeepSeek cache fallback) + deepseek 13 unit test + capability-probe 既有不破坏.
ADR-0007 Capability tracking 接入纪律 + Bug W tool_call_id dedup hotfix (6 commit, 2026-05-01)¶
物流业务命门 r22-r25 序列暴露 "修一层揭露下一层" wire 协议 gap (tool 名 regex / reasoning_content passback / tool_call_id dedup), PM 反思真因不是 5 个独立 bug 是同一底层问题 — 引擎对每个 (provider × model) 能力没真测过全走兜底假设. 3-agent 并行 review reconcile 后走方案 B' (3 字段 schema 扩 + opt-in flag 无 strict + bifurcate direct/aggregator + OpenRouter live metadata 消费扩展).
Commit 顺序:
- C1 (
09b6bc1) Bug W dedup hotfix: wire/openai.go flytoMessagesToOpenAI RoleAssistant 加 seenToolCallIDs map 去重. 单 message 内同 tool_use_id 重复 silent skip; 空 ID 不 collision 误吞. 跟 final_text_duplicate_blocks (engine.go:5039) 同源模型纪律漂移. 切出 ADR-0007 范围 (engine/wire 实现 bug 不是 capability 维度). 真因调研留 TD-19. 3 测试新增. - C2 (
dac31d6) ModelInfo 加 3 字段:ToolNameRegex string(provider 强制 regex) /ReasoningPassbackMode string(enum: ""/"none"/"string"/"details_array") /ProviderKind string(enum: ""/"direct"/"aggregator"). 零值=未知=旧行为零回归. 双语 godoc + 替代方案保留入注释. - C3 (
f77b2c3) 4 direct provider modelInfo 静态填值: anthropic (regex^[a-zA-Z0-9_-]{1,64}$/ passback "none" / kind "direct") + openai (^[a-zA-Z0-9_-]+$/ "none" / "direct") + minimax (^[a-zA-Z0-9_-]+$/ "" 暂留 / "direct") + gemini (^[a-zA-Z0-9_-]+$/ "none" / "direct"). 4 文件 batch replace_all 修改. - C4 (
17f7ba4) wire 层 capability-aware: openaiMsg 加 ReasoningContent omitempty 字段 + StreamRequest 加 ReasoningPassbackMode + ToolNameRegex 字段 + flytoMessagesToOpenAI 签名加第 4 参数 + RoleAssistant BlockThinking 累积 + mode=="string" 时 inject reasoning_content / 其他 mode 跳过 (零回归). wire/tools.go 加 ValidateToolNames(tools, regex) pre-flight 校验返 ErrModelToolUnsupported typed error. openrouter Provider Stream 接通 (req.Capabilities → wire StreamRequest 透传). 7 测试新增. - C5 (
16423ac) OpenRouter live metadata 消费扩展: FetchOpenRouterModels 扩展消费 architecture.input_modalities (auto SupportsVision) + top_provider.max_completion_tokens (实际后端上界优先 root) + pricing.input_cache_read (auto SupportsCaching) + ProviderKind="aggregator" 默认 + ToolNameRegex^[a-zA-Z0-9_-]+$默认. 1 测试 fixture 镜像 r24 实证 deepseek-v4-flash + claude-sonnet-4.6. - C6 (本 commit) ADR-0007 + 文档同步:
core/docs/adr/0007-capability-tracking-intake-discipline.md8 节体例对齐 ADR-0001/2/3/5/6 + § 2.2 bifurcate direct/aggregator + § 2.3 红线 (capability 信号驱动 wire 行为 ≠ auto-fallback) + § 物流锁 minimax sub agent 隐性约束 + 7 tracked debt (TD-19..25). TODO/CHANGELOG/CLAUDE.md 同步.
业界对照: SDK 走 string + server-validates (Anthropic/OpenAI Python); 聚合层 (LiteLLM/Aider) 走 PR-maintained model db 30+ 维度. Flyto 此前 ~7 维度 + 无 capability-aware wire 行为, ADR-0007 拉到 LiteLLM 同档.
ADR-0007 § 2.3 红线: capability code 仅供 wire 层信号化决定行为, 引擎层禁用 capability code 驱动 auto-fallback. 与 ADR-0005 引擎中性化 + ADR-0006 § 3 红线 性质相同.
与 ADR-0006 互补: ADR-0006 事后归类 (provider 4xx 暴露真因), ADR-0007 事前预防 (wire pre-flight reject + capability-aware inject). 不互斥.
§ 物流业务隐性约束: v0.5 周期物流 sub agent 锁定 minimax (r21 0 violations 收敛), ADR-0007 期间不切换其他 model 探索. escape hatch: 消费者可 RegisterModels 自填 capability 字段绕过.
Tracked debt: TD-19 engine 重复 emit 真因调研 / TD-20 capability-probe 实测扩 3 项 / TD-21 wire details_array mode 实装 / TD-22 OpenRouter per-model ReasoningPassbackMode 实测填 / TD-23 input_modalities 全 modality 字段 / TD-24 消费者 RegisterModels 接通 / TD-25 lmstudio/ollama capability 推断.
测试 -race 全绿: wire 全套 + 4 provider 全套.
实证待跑: r26 (改名 + Bug W dedup + capability-aware) 物流业务命门继续走 minimax sub 锁定不变, deepseek-v4-flash 探索由消费者 RegisterModels 接通后跑.
ADR-0006 错误分类 typed ErrorCode + wire→engine fail-loud cause 链 (6 commit, 2026-05-01)¶
物流报价表抽取业务命门 r22 (OpenRouter + deepseek-v4-flash) 3s 内 internal_error 中文兜底吞掉真因. 3-agent 并行 review reconcile 实证 task spec 假说 ("wire reasoning_details JSON tag 错") 全错 — 真因是 (1) 业务调用方 bug: 工具名 billcost.reflect 含 . 违反 OpenAI function-calling regex ^[a-zA-Z0-9_-]+$, OpenRouter 转底层 SiliconFlow HTTP 400 reject + (2) 独立的引擎 fail-loud bug: HTTP 非 200 路径不读 body 直接返 "openai_compat: http %d", parseNonSSEError 不解 OpenRouter 嵌套 metadata.raw, EngineError.Error() 不暴露 Detail, ClassifyAPIError 走字符串模式 fallback 默认 ErrInternal 折叠掉具体形态. 两件事性质独立都要修.
Commit 顺序:
- C1 (
f140eb1) 业务 fix:core/pkg/billcost/tool.goReflectTool.Name"billcost.reflect"→"billcost_reflect"+platform/common/internal/billrecon/llm/reflect_tool.goReflectQuoteTool.Name + validator_adapter.go QuoteResponseValidator.Name + PolicyVersion"billcost_reflect.v1"+ 测试断言 + sub_agent.md prompt + probe 注释全对齐. CLEVER 注释入档 ReflectTool.Name + reflect_tool.go 包注释说明 OpenAI regex 限制让以后加新 tool 不再撞. - C2 (
e009350) wire OpenRouterparseNonSSEError解嵌套 metadata.raw: 顶层 message="Provider returned error" + metadata.raw 内嵌真错误 (SiliconFlow{code,message}形态 / OpenAI passthrough{error:{message,type}}形态). 解一层 raw 抽真错误 + 标 provider_name; 形态未识别回退顶层 message; 故意不递归保 surface 小可预测. 3 测试新增 (SiliconFlow / OpenAI / unrecognized fallback). - C3 (
960445c)EngineError.Error()拼 Detail:Message: DetailGo 经典 wrapping 形态; 字符串路径让 fmt.Errorf / log.Fatalf /%v调用方零侵入看到真因, 不必 type-assert. 替代方案 (B 方案 ErrorEvent.Detail 字段不改 Error()) 入注释 — 80% caller 走字符串场景 B 不解决人类可读问题. 2 测试新增 (Message+Detail / Detail-only). - C4 (
152903d) 加 5 typed ErrorCode 词汇表: ErrProviderHTTPStatus / ErrProviderNonSSE / ErrProviderMidStreamErr / ErrWireUnmarshal / ErrModelToolUnsupported + Suggestion (actionable hint 不是 auto-action) + Retryable 默认全 false (消费方决定). ADR-0006 § 3 红线 godoc 入档: 这些 code 仅供分类, 引擎层禁用驱动 auto-fallback (如静默关 tools / 自动改 model) — 与 ADR-0005 引擎中性化精神连续. - C5 (
b3a083c) wire→engine 透传 cause 链 + ErrorEvent.Detail:core/pkg/flyto/errors.go同步加 5 typed code (wire 层只 import flyto 不 import engine, 必须镜像) +EngineError.Error()镜像拼 Detail;core/pkg/flyto/events.goErrorEvent 加 Detail 字段;core/internal/wire/openai.goHTTP 非 200 路径修真出血点 (旧实现不读 body 现统一读经 parseNonSSEError 抽消息包成 flyto.EngineError 持 ErrProviderHTTPStatus + Detail), 200+非 SSE 包成 ErrProviderNonSSE, mid-stream chunk.error 包成 ErrProviderMidStreamErr;core/internal/wire/gemini.go同款 (rule of two 第二条 wire 路径触发); anthropic 走 transport.classifier api.APIError 已 typed 不动 (engine.go:4278 已 errors.As 接), minimax 用 OpenAICompatClient 自动继承 openai.go fix;core/pkg/engine/errors.go加 ClassifyAPIErrorTyped(error) errors.As 优先 typed code 字符串 fallback, WrapError 加 flyto.EngineError 分支让 Detail 直接复用避免三层字符串叠加;core/pkg/engine/engine.go三处 retry caller (4214/4234/4406) 改用 ClassifyAPIErrorTyped + newErrorEvent errors.As 链同时识别两 sibling type 透传 Detail. 5 测试新增. - C6 (本 commit) ADR-0006 + 文档同步:
core/docs/adr/0006-error-classification-typed-errors.md8 节体例对齐 ADR-0001/2/3/5 + TODO.md / CHANGELOG.md / CLAUDE.md "最后变更" 段同步.
业界对照: Anthropic Python SDK / OpenAI Python SDK / Vercel AI SDK / LangChain 全 typed error + cause 链 + 字段访问. Flyto 早期走字符串 pattern match 是反模式, 本 ADR 对齐业界.
ADR-0006 § 3 红线: typed code 仅供分类, 引擎层禁用驱动自动行为. 例外是 ADR-0006 之前已立的 ErrContextTooLong 自动压缩重试 + ErrAPIRateLimit/Overloaded 自动 retry, 不动. 新加 5 code 都不挂 auto behavior.
Tracked debt 登记:
- TD-11: flyto.EngineError + engine.EngineError sibling 合并 (历史重复, ADR 接受 patch 不修, v1.0 release 评估)
- TD-12: anthropic 路径 transport.classifier *api.APIError 与 wire 层 typed error 合一评估
- TD-13: capability-probe 加 tool name regex ^[a-zA-Z0-9_-]+$ 启动期校验避免运行时 4xx (跟 ErrModelToolUnsupported 配套)
- TD-14: lmstudio / ollama / openrouter 等剩余 provider 是否同款 typed error 迁移 (rule of two 触发条件)
测试 (-race 全绿): core/pkg/engine/errors_test.go +5 + core/internal/wire/openai_test.go +3, 既有测试全过 (TestEmitCheckpointSuggested_BashDangerous panic 是 pre-existing 无关).
实证: r23 改名后 thinking + tool_use 路径 (不再 3s 立挂); r24 background 验证 ADR-0006 fail-loud 通路 — 跑挂时 ErrorEvent 持具体 typed code + Detail 而非泛化 ErrInternal.
Session.trackEvents 取消路径 fail-loud (1 commit, 2026-05-01)¶
core/pkg/engine/session.gotrackEvents在ctx.Done()/s.done分支 drain rawCh 之前先 best-effort 推送 (a) 当前从 rawCh 读到但未 转发的 evt + (b) 明确的*ErrorEvent(ctx 路径用WrapError(ctx.Err(), ErrInternal, "操作已取消"), session.Close 路径用WrapError(context.Canceled, ErrSessionClosed, "session closed")) 到 outCh; 两路 send 都select/default防 outCh 满时 阻塞 (饱和丢弃是设计行为, 消费方未读取是另一个问题). 抽 helperfailLoudCancel+drainRawEvents让ctx.Done与s.done两条 分支共用一个实现, 不发散.- 实证背景 — quote-engine-probe r17 round 1 真实数据: 共享
per-round
ctx, cancel := context.WithTimeout(ctx, 600s)在 sub agent 段触发 3 次final_text_duplicate_blocksretry 后 600s deadline 触底 (sub_dur=10m0s ctx_err=context deadline exceeded), trackEvents 进ctx.Done分支 drain 掉 sub 端 buffered DoneEvent — outCh silent close, 消费层 (probedrainSession) 看到range outCh退出但既无[done]也无[error], 无法区分 "引擎正常完成" vs "ctx 中途触底". main agent 用同一已 done ctx Send → engine.go:3611case <-ctx.Done()立即 emit操作已取消→ main 段也 silent close (Go select 多 case ready 伪随机, r15/r16 main 收到 ErrorEvent 而 r17 main 也 silent close — 同一 race 不同 outcome). - 修复后 — sub 端 ctx-deadline 触发会推 ErrorEvent code=internal_error
到 outCh, drainSession 收到 → return err → caller 立即
log.Fatalf暴露真因, 不会再 silent 退出当 OK; main agent 任何依赖 sub 端事件流 完整性的消费者 (orchestrator / SDK / TUI) 都得到明确的 cancel 信号. - 单元测试 5 个 (
session_failloud_test.go):failLoudCancel双 emit / outCh 饱和 nonblock /ctx.Done分支 fire ErrorEvent /s.done分支 fire ErrorEvent (code=session_closed) / drainRawEvents drain to close. Go select 多 case ready 伪随机用 12 iter 兜底 (单次 false-pass < 1/4096). 全绿 -race 3x. - probe-side 配套改动 (work-tree only, 不 commit) —
platform/common /cmd/quote-engine-probe/main.go把 per-round 共享ctx, cancel拆为subCtx/subCancel+mainCtx/mainCancel, 让 main agent 不再继承 sub 端 ctx-done 状态 + 加 timing printf 帮调试. trackEvents engine 修复 之后, 即使 probe 不拆 ctx 也只是 main 段会因同 ctx 触底立即得到清晰 ErrorEvent 而非 silent close, 数据形态可观测.
反射器 engine 强制响应闸 (3 commit, 2026-04-30)¶
- C1
core/pkg/engine:Config.ResponseValidator validator.Validator字 段 + runLoop 末端 wire — 当 LLM 出 final text 没调工具时 (stop_reason=end_turn), 引擎在 generic stop hook 之前先跑Validate(ctx, DiffInput{SourceTool:"LLMResponse", Raw: finalText}). fail (Verdict.Approved=false) 或非 nil error → 把 Reason 注入下一轮 user message, turn loop 继续; 被拒文本不作为最终答复推送. nil = 无侵入 (默认行为零开销). 复用validator.Validator接口不引入新 Response* 接口 —DiffInput.SourceTool + Raw已是 schema-agnostic opaque bytes (godoc 自述 "future HTTP-patch shapes"), LLM 文本响应正是同档情况. helperextractFinalAssistantText跳 thinking + tool_use 块, validator 看到的是消费方收到的最终答复. 5 单元测试全绿 -race (commitcc9ab20). - C2
platform/common/internal/billrecon/llm:QuoteResponseValidator把ReflectQuoteTool包成validator.Validator— 同包内复用 wrapper 的 segments+rows fan-out + summarize 逻辑, ExtraTools 工具路径 + ResponseValidator 闸路径共用一处实现.IsError→ Approved=false + Reason 透传 unmarshal 错让引擎注入 user message 时 LLM 知道哪段 JSON 错; 违规 → Approved=false + Details (violations + violation_count) 让 CompositeValidator / 审计槽按规则索引. 默认PolicyVersion="billcost.reflect.v1"供回放审计 pin. 6 单元测试全绿. 同 commit 把上轮已写未 commit 的reflect_tool.go也加进版本 (依赖) (commitfeb9c34). - C3 quote-engine-probe wire + sub_agent.md prompt 改写 — sub engine
装
ResponseValidator: &llm.QuoteResponseValidator{Tool: reflectTool}, 同一 reflectTool 实例两路径共享 Cfg + state. sub_agent.md 把 "你必须调用 billcost.reflect" 改写为"工具可主动调自查 (省一轮 retry), 引擎会强制跑同一反射器 — 不存在'绕过反射器直接出答案'路 径". r8/r9/r10 实测背景: r8 (qwen3-14B) 5 round 4 次跳反射器 / r10 (qwen3-32B) 反射器调稳 0 violations 但业务规则 (日期 / null 字段 / 重复 JSON) 反射器规则薄度漏判 — engine 强制兜住 r8 类, r10 类需 follow-upvalidator.CompositeValidator链 LLM-based 业务校验. - ADR-0001 修订: 反射器 engine wire 与反向思维 gate 不同时机 (反向思维
对应 PreToolUse, 反射器对应 stream 末端 final-text 验) 但同 hook-not-
engine 范式 — 复用
validator.Validator不开新 gate 接口 / 业界对照 Pydantic AI / Instructor schema validator+retry 模式吻合.
Hard Contract 系列 (5 commit, 2026-05-01)¶
引擎层 + tool 层对位 Claude Code 的 fail-closed 契约, 抽象层补齐让消费者 不需要手写 wire 也得到默认安全. 跟 ResponseValidator (cc9ab20) / Read-before-Edit (cc FileEditTool.ts:275-310) / SSRF / 路径黑名单同 范式 — 引擎默认开 + opt-in 替换, 不强制 / 不破坏既有 API.
e6e715acore/pkg/engine:Config.Temperature *float64 + TopP *float64wire gap 补齐. flyto.Request 字段 v0.3 L683 commitacae658已接通 7 provider passthrough, 但 engine.Config 当时只给 evolve LLMCallOpts 用没暴露 engine 消费层. r12 (Gemma 4 26B A4B MoE) probe 实测"输出纪律漂移" (JSON 多次重复 / thought 标签) 时无法降温 治理 — wire gap 跟 ResponseValidator 之前同源. buildFlytoRequest 签名 加 temperature / topP 参数, runLoop 传e.cfg.{Temperature,TopP}. 2 编译期 field type 测试.361dacecore/pkg/engine: final text 重复块 hard contract (引擎层 schema-agnostic, 不开新 Config 字段). probe r12/r13/r14 实测模型输出 纪律漂移 — 一次 final response 内输出多个内容完全相同的 ContentText 块 (r14 主 agent gpt-5.1 把同一个 verdict JSON 输出两遍 → probe 下游 JSON parse FAIL → 写空 verdict.json 还报 "converged" misleading).detectDuplicateTextBlocks块级而非拼接字符串扫描 (拼接后 schema- aware ResponseValidator 只看 corrupted 单串). runLoop 6.4 段排在 ResponseValidator (6.5) 之前, 命中 → WarningEventfinal_text_duplicate_blocks+ 注 user message 纠偏 + continue. fail-closed 类比 Read-before-Edit, 不让 corrupted 文本流给消费层. 不加独立 cap (MaxTurns 已限总 retry, 避免新 wire gap 候选). 5 单元 测试覆盖 NoDuplicate / ExactDuplicate / MixedThinkingAndDuplicate / EmptySkipped / DifferentTexts.e7c81beplatform/common/responseguard(新包):JSONVerdictValidator实现validator.Validator, 给 main agent (orchestrator) 装到engine.Config.ResponseValidator上. fail-closed 触发: 空 / parse 错 / 多 top-level JSON 拼接 (json.Decoder.More()抓 r14 主漂移: gpt-5.1 把 verdict JSON 输出两遍中间夹空白 → 解一个后 More() 仍 true) / top- level 非 object / RequiredKeys 缺字段. 与 sub agent 端业务反射器 (如QuoteResponseValidator) 形成 sub+main 双端守卫模式. doc.go 给配对 样例. 10 单元测试. 放 platform/common (非 core): schema-aware 守卫不 是引擎层 generic 结构检测, 多个 industry platform (logistics/ERP/sales) 共享"主 agent 出 JSON verdict"模式, helper 放 platform 层让业务 orchestrator 消费.041cebecore/pkg/tools/builtin: Read-before-Edit hard contract 强制, 对位 Claude Code FileEditTool.ts:275-310. 调研发现 Flyto 之前已实装 一半 —FileStateCacheRecorder(write 侧) +FileStateCacheEntry存 ContentHash/Size/LineCount/ModTime/IsPartialView, FileReadTool Execute 末尾已 RecordState. 但 FileEditTool 完全没查 (跟 Temperature/ TopP wire gap 同模式). 加FileStateCacheReader接口 (GetState 对偶 RecordState) +DefaultFileStateCache实现 both Recorder + Reader, RWMutex 线程安全, Reset() 对位 Claude Code commands/clear/conversation .ts:130. FileEditTool struct 加stateCache字段; 构造期 nil panic with friendly message (no opt-out, fail-closed by design). 删除 6 个 旧构造器 (NewFileEditTool/WithCwd/WithCache/Full/Complete/WithGuard) 替换为单一NewFileEditTool(stateCache, opts...)+ functional options (WithFileEditCache/History/Cwd/Guard). validate 在 ReadFile 后 / CRLF 转换前加三重 check: (1) 路径必须有 Read 记录 → 否则 "has not been read yet"; (2) 记录非 partial view → 否则 "partial view, read fully first"; (3) mtime 漂时走 content equality fallback (cloud sync / antivirus 改 mtime 不改字节假阳性) → hash 不等才 reject. FileReadTool collectFull 触发条件由fileCache != nil拓宽为(fileCache != nil || stateCache != nil), 让 stateCache 单独启用时 ContentHash 仍基于 raw bytes 与 Edit 端 hash 比对一致. engine.registerBuiltinTools 创建一次 共享 fileStateCache 给 FileRead + FileEdit (Run 内共享, 不跨 Run / 不跨 sub-agent 持久化). 5 cache 测试 + 6 contract 测试 + 18 旧测试 升级到 editToolForTest helper. 破坏性 API 变更 (v0.4 周期内消费者 数量小, 可破): 唯一 production caller engine.go 已升级.e522a3ecore/pkg/tools/builtin: Bash 危险路径黑名单 + WebFetch SSRF TOCTOU 加固. Bash 现状: bash_classify.go 仅做命令分类用于截断 策略 / 权限 UI 提示, 零 enforcement — rm -rf /etc 直通 execenv. 新增bash_dangerous_path.go对位 Claude Code utils/permissions/ pathValidation.ts:isDangerousRemovalPath. 黑名单: / / / 直接子目录 (/etc /usr /tmp /var /bin 等) / Windows 盘根 /$HOME本身 /*与/*通配; 子目录 (/usr/local / /tmp/build / ~/code) 不算危险开发 rm -rf dist/ 仍允许. extractRemovalTargets 简单 regex + Fields 锚定 语句边界 (^ ; && || | \n) 防 echo 'rm -rf /' 误抓; v1 不做 POSIX shell AST 留 v2. bash.go Execute 早期插入拦截 fail-loud reject. 设计差异 (vs cc): cc 走 user-in-the-loop ask (UI 弹审批可一次授权), Flyto v1 hard reject — LLM 看错误自纠. user-audit 通道 (ToolResult 加 RequireApproval 字段) 待 PM 决策驱动. 16 单元测试. WebFetch SSRF TOCTOU 微补: 已有 10 条 CIDR 黑名单 + safeDialContext- DNS 解析后 IP 逐条检查 + 字符串绕过防护非常完整. 仅
d.DialContext用原 hostname 调, Dialer 内部二次 resolve 留 DNS rebinding race 漏洞. 修: dial 用第一次 verified 的addrs[0].IP直接构造 host:port, Dialer 不再二次 resolve. HTTPS SNI 由 net/http.Transport 用原 hostname 设 ServerName 与 dial address 解耦, 此 hijack 不影响 TLS 证书校验.
调研副产品: B-9 SQL readonly 强制原计划项, 但调研发现 sql_validator.go 已完整实装 SQLValidatorTool (强制 SELECT/WITH/EXPLAIN 单条语句), 零 工作量已覆盖, 跳过编码工作仅文档同步.
新增消费者文档 core/docs/hard-contracts.md 给装 platform/industry 的
团队列引擎层全部 hard contract 总览 (类型 / 触发 / 配置 / 升级路径) 让
消费者看一份知道边界, 不用挖源码.
bill-recon C1-C10 + alpha.x 部署优化 (大头)¶
- alpha.5 → alpha.10 chat UI + workflow 链路: C5 LLM 列分类 (commit
2163d34) → C6 vision 调价图 vlm 抽取 + chat 确认 + price_adjustments 落库 (6f40c9c) → bills 列表+详情+删除 (e6f1258) → registry cache - Dockerfile mod download 单独 layer (
233baad/8db2089) → -alpha 跳过 godoc/docs build (2b97672, alpha cut 10.5min → 1.4min) → alpha.10 chat UI 显示原图 + vlm 失败标红 + 双提交 409 (44b277d). - ADR-0004 立项: PM C6 review 时拉飞驼内部 WMS 数据字典 (
CloudWM/02_Design/ CWM云仓数据库设计说明书.xlsxSheetCWMACCTShipCostCfgMaster + ShipCostCfgCostType=0/1), 指出当前 12 字段parser.Adjustment跟 WMS 现有承运商 价格表 schema 5 关键 gap (free-form 文本 vs 结构化首重续重定价 / Regions 数组 vs 一行一省 1:N / 无 StandardCostSysNo / Carrier 名字 vs ShipTypeId / 无重量段+体积重比+优先级). 拍板: 大修对齐 WMS, 不分阶段, 不落 WMS db 保留 bill-recon 自身两表镜像. 8 commit chain (S1 schema migration / S2 parser 拆 master+detail / S3 store / S4 LLM prompt + parse / S5 workflow - web / S6 review UI / S7 测试 / S8 bill detail 接新 schema).
v0.4.0 (2026-04-26)¶
v0.3.3 后的小步发版, 核心交付 platform 走 Postgres 平台化奠基 —
SessionStore 抽接口 + Postgres 后端 + db.Pool 共享 + docker-compose pg
service. ADR-0003 立 SessionStore 多副本边界 (三层进程内 pin 物理事实
+ sticky routing 必选). v0.4.0 是 v1.0 多副本部署能力的奠基, 但单副本
dev / docker-compose 行为零变化 —— cmd/common 不传 --postgres-dsn
时维持 InMemoryStore 默认, 与 v0.3.3 完全等价.
platform/common 层¶
- SessionStore 接口 + InMemoryStore + Postgres + db.Pool (L693) — 4 commit 节奏完整落地, 3-agent review reconcile 后 PM 三轮决策 (Postgres 肯定用 → 整个 platform psql → 拒绝迁移工具+sqlite 走 plain SQL + testcontainers → C3 staging Postgres 跳过待真消费者). 关闭 ADR-0002 § 4 标注的 "in-memory sessions 单实例假设" 限制
platform/common/internal/server/sessionstore/— SessionStore interface (Create/Get/Delete 三方法, 不要 Touch/List/owner_id 投机 表面) + InMemoryStore (sync.RWMutex + map) + PostgresStore (INSERT ON CONFLICT DO NOTHING + RowsAffected==0 单语句原子, 不需显式事务). SessionMetadata 仅 ID + CreatedAt 两字段 (engine.Session pointer 物 理只能在 server-local sessionCache, channel/mutex 不可序列化)platform/common/internal/db/— 共享 *pgxpool.Pool wrapper + 中央 schema 权威 (platformMigrations append-onlyCREATE TABLE IF NOT EXISTSlist, idempotent Migrate). 当前注册 sessions 表; 后续 staging / verdict-audit 真消费者落地时追加. 不引正式 migration 工具 (1 张表 schema 还在变, plain SQL 自检足够; ≥ 5 张 + 稳定再引)cmd/common --postgres-dsnflag + db.Pool 顶层 init + Migrate (10s/30s timeout) + defer Close. dsn 空时所有 Postgres 后端子系统 回落内存参考实现 (今天 sessionstore.InMemoryStore, 后续 staging 同 模式)server.go改造: sessions map + mu → sessionStore (持久 metadata, interface 注入) + sessionCache (in-process *engine.Session pointer map, sessionMu RWMutex 独立锁). 4 handler 改造 (handleCreateSession / handleGetSession / handleDeleteSession / handleSendMessage) 用 新 store + cache; handlePermissionReply 不动 (走 permCh 不直接 access sessions). TOCTOU race 折叠: RLock-then-Lock 两段 → SessionStore. Create 单语句原子- 测试: 13 个 (6 InMemoryStore + 5 testcontainers Postgres 真 pg + 2 db pool), 全模块 -race 全绿. 测试参数化契约 harness 提取留 follow-up (第三后端或 SDK 消费者落地后)
-
commit 顺序:
eec48feC1 接口 + InMemoryStore drop-in /beaff60C2 Postgres + db.Pool + docker-compose pg + release.yml POSTGRES_PASSWORD secret /96e893aC3 ADR-0003 + Caddyfile 注释 /69500dbC4 TODO + CHANGELOG + CLAUDE.md 同步 -
deploy/docker-compose.yml: 新加
postgres:16-alpineservice (健 康检查 pg_isready + 持久 volumepostgres_data+ bridge-internal 不 映射 :5432 到 host); common.depends_on.postgres.condition: service_healthy; command 加--postgres-dsn=postgres://flyto:${POSTGRES_PASSWORD}@postgres: 5432/flyto?sslmode=disable. POSTGRES_PASSWORD 走 .env :?missing fail-loud (跟 ANTHROPIC_API_KEY / FLYTO_PROXY_TOKEN 同模式) -
.gitea/workflows/release.ymldeploy step 加export POSTGRES_PASSWORD="${{ secrets.POSTGRES_PASSWORD }}"(跟 ANTHROPIC_API_KEY 同位). PM 已配 Gitea secrets -
deploy/Caddyfile/api/v1/*handle 块加单副本 vs 多副本部署区 别注释: 多副本必须lb_policy ip_hash(phase 2 改header X-Session-ID) 并列所有 replica, 或 k8sService.sessionAffinity = ClientIP. 当前 reverse_proxy 单 upstreamcommon:8080不动 (HK-133 phase 1+ 单副本 生产, sticky 是 no-op)
关键设计原则¶
- 三层进程内 pin 物理事实, 平台层只能解一层 (ADR-0003 § 2.4) —
server.permCh+Session.pendingPermissions+engine.sessionState .sessions三层都是进程内 (Go channel + mutex + goroutine 不可序列化). 后两层在 core 引擎层, 平台层即使做完整 SessionStore + PermissionBus pub/sub 也只能解 A 一层. 业界 LangGraph/Vercel/Temporal SessionStore - PermissionBus 双件套需要改 core engine API, 是 RFC 级别留给 v1.0+
- 多副本必须 LB sticky routing (ADR-0003 § 2.5) — phase 1 ip_hash / k8s ClientIP (NAT 聚集 + 跨设备脱黏可接受); phase 2 触发条件由真 投诉驱动, 改 X-Session-ID header lb_policy
- 接口三方法不要投机表面 — 5 handler 真实操作只有 Create/Get/Delete,
Touch/List/owner_id 等会被 dead-field-scanner ratchet 标为死表面.
真消费者出现 (admin UI / 多租户 gating / SLA watchdog) 时方法跟消费
者同 commit 落地 (memory
feedback_dead_fields_are_implementation_debt) - ephemeral metadata ≠ staging audit (ADR-0003 § 5.5) — staging.Store meta-pattern 复用 (InMemoryStore + SQL-later + tracked-debt), 接口 verbs 不复用 (sessions 是 ephemeral Get/Set/Delete 形态, 不是 staging 7 状态机 + 优化锁)
- engine.Session 不动, 借 SnapshotStore —
core/pkg/engine/ session_snapshot.go已有 SnapshotStore 接口 + WithMessages 注入路径, cache miss 自动恢复历史留 follow-up commit, 不在 SessionStore 重新发明 序列化 API (channel/mutex/goroutine 不可序列化是物理事实) - plain CREATE IF NOT EXISTS + testcontainers — PM 拍板拒绝过度工程: 不引 migration 工具 (1 张表 schema 还在变, 等 ≥ 5 张稳定再引), 不用 sqlite mock (与 Postgres SQL 方言差别大会掩盖真 bug). 用 testcontainers-go 启真 pg 容器跑测试 (~30s 测试启动 cost 接受)
- drop Redis + drop staging Postgres — Redis 元数据 payload 太薄 (id + created_at ~50 字节) 不值, 与 staging / verdict-audit 共享 pg 池更经济; staging Postgres 后端没真消费者 (cmd/common 没装 staging), 跳过等真消费者驱动 (ADR-0003 § 5.5)
已知限制¶
继承 v0.3.0 已知限制 (ML Validator / 熔断器 / cross-transport request-id / SSE 带宽监控 / CAP-4 自动化 / Provider 模型表 / AuditSink / WMS / 场景 化教程 / 微信 / .proto 注释), 不重复列出. v0.4.0 新增:
- 三层进程内 pin 后两层在 core 引擎层 — 真跨副本 live session 失败 转移要改 core engine API (engine.Config.PermissionBus + Session. MarshalState/RestoreState), 是 RFC 级别留给 v1.0+ (ADR-0003 § 5.2)
- NAT IP 聚集 + 跨设备脱黏 — phase 1 ip_hash / ClientIP 在企业 NAT 后多客户端共享外网 IP 会聚到同副本; 客户端切换网络 IP 会脱黏到另 一副本 → 503 (cache miss). phase 2 改 X-Session-ID header 解决, 触发 条件由真投诉驱动
- Postgres 是新单点 — 池挂 → 所有 SessionStore 操作 503. healthcheck
- restart unless-stopped + 持久 volume 是基础保护; HA Postgres (pgpool / patroni / Aurora) 是 v1.0 SLA 课题
- engine.SnapshotStore 接线 cache miss 恢复历史 — 留 follow-up commit, 触发条件: 真出现 sticky 失败导致的 503 投诉 (ADR-0003 § 2.6)
- staging Postgres 后端待真消费者 — staging 子包当前 cmd/common 没 wire, 行业 platform 也没用. 等 staging 真接入 (验证器 + ML + 审批工 作流被 logistics 或其他行业 platform 实际跑) 时同模式追加 (ADR-0003 § 5.5)
发布事实¶
- TODO: 52 → 53 done (+1: L693 SessionStore + Postgres 平台化奠基,
4 commit chain
eec48fe → beaff60 → 96e893a → 69500db), 13 → 12 open - 测试: 13 个 (6 InMemoryStore + 5 testcontainers Postgres + 2 db pool), 全模块 -race 全绿; testcontainers 启 6 个 docker pg 容器实测 通过 (~75s 总, docker 不可用 t.Skip 兜底)
- 依赖: 新增
github.com/jackc/pgx/v5(runtime, Postgres driver) +github.com/testcontainers/testcontainers-go(test-only) +github.com/testcontainers/testcontainers-go/modules/postgres(test-only). test-only dep 不进 production binary - ADR: ADR-0003 SessionStore 多副本 sticky routing + 三层 pin 物理 事实分析 (387 行 8 节体例对齐 ADR-0001/0002)
- 设计决策工作流: Agent Teams 3 角色并行 review 持续作 default, v0.4 cycle 第 1 次实战 (L693, v0.3 cycle 5 次延续)
- CI: release.yml deploy step 加 export POSTGRES_PASSWORD; Gitea secrets 已配
- dead-field-scanner baseline: 220 不变 (scanner 只扫 core/, 不扫 platform; 本版改动全在 platform/common)
- Tag: v0.4.0 一次性 cut 不走 alpha (与 v0.2.0/v0.3.0 同模式)
文档同步¶
core/docs/adr/0003-session-store-multi-replica-bounds.md新建 387 行 8 节deploy/Caddyfile加 16 行单副本 vs 多副本部署区别注释指向 ADR-0003core/TODO.mdL693 [x] + 4 commit chain + 统计 53/12/65 + 剩 12 项分组- 关键观察段加 ADR-0003 + 近期主要动作首条
CLAUDE.md关键记忆 platform 消费层 9 → 8 项 + 最后变更 L693 段 (v0.3.3 移到上一变更)CHANGELOG.mdv0.4.0 段 (本段) + 起新 Unreleased (v0.5-dev) 空段
v0.3.3 (2026-04-26)¶
L407 follow-up: 内部开发者一站式文档地图 + 双子域文档站 (godoc + docs).
PM 反馈 v0.3.0 cut 的 Swagger UI 只能展示业务 REST API, 不展示 internal Go API + 内部 markdown, 需要"内部开发者方便看文档" 的统一入口. 实现 A + B + C 三件套 (PM 拍板"都要"):
A — README 文档地图 (commit fea0d27)¶
- 升级 root
README.md加 "## 文档地图" section ~70 行 - 3 个分组: 在线入口 (always-on) / 按目标读者分组 / 5 步入门顺序
- 7 类读者: Agent / 消费层 / core 贡献者 / Provider / 部署 SRE / TUI / PM
- 进 Gitea web 看 README 一眼看全 27+ markdown 在哪
B — godoc.flytoex.net 子域 (pkgsite Go API HTML)¶
- 子域而非
hub.flytoex.net/godoc/*子路径: pkgsite 内部 href 全是绝对路径 (实测href="/",href="/git.flytoex.net/yuanwei/flyto-agent"), Caddy strip prefix 后浏览器 link 全断 (跳到 logistics:8080).pkgsite --help无--base-path/--mountflag, sub-path 部署不可行 deploy/Dockerfile.godoc多阶段: golang:1.26-alpine 装 pkgsite + 拷整 monorepo 源 +go mod download all预解析依赖 → final image 仅含 pkgsite 二进制 + 源 + module cache. ENTRYPOINTpkgsite -http=:8080 -list=false .(首页只列本地 module). cold start ~30-60s (load 472 packages), 后续 in-memory cache- 覆盖 core/ 22 子包 + platform/common 所有 exported type, 不鉴权对齐 pkg.go.dev
C — docs.flytoex.net 子域 (mkdocs-material 整合站)¶
deploy/Dockerfile.docs多阶段: python:3.12-alpine 装 mkdocs-material@9.5.31 (与 staticcheck@v0.7.0 / swag@v1.16.6 / protoc-gen-doc@v1.5.1 同 pin 版模 式) + COPY 显式列每个顶层目录 markdown (避免拉 .go/.proto/vendor 让 image 膨胀) →mkdocs build→ nginx:alpine serve/usr/share/nginx/htmlmkdocs.ymlrepo 根: docs_dir=/work(绝对路径), config 文件/config/mkdocs.yml-- mkdocs 不允许 docs_dir = config 父目录, sibling 目录绕开. nav 12 个分组 (入门 / ADR / 核心引擎 / API / Provider / 部署 / TUI / 战略 / 写作规范 / 历史) 串 README + CONSUMERS + CONTRIBUTING + FLYTO- ADR-0001/2 + CHANGELOG + TODO + 7 provider 文档 + architecture + writing-guide
- theme material 中文 + indigo + 暗色切换 + tabs/sections/search/code copy
deploy + CI 接线¶
deploy/docker-compose.yml加 godoc + docs 两 service (compose 内部 expose, Caddy 唯一入口); caddy.depends_on 加 godoc/docsdeploy/Caddyfile加godoc.flytoex.net+docs.flytoex.net两 site block (LE 自动证书 + gzip/zstd, 不鉴权; 跟 hub.flytoex.net 平级三 site block, Caddy SNI 分流).gitea/workflows/release.ymlbuild-push job 加 flyto-agent-godoc + flyto-agent-docs 两 build-push step (跟 common + logistics 同 buildx 模式)
DNS (2 条 A 记录, Cloudflare API 加)¶
godoc.flytoex.net→ 45.145.229.197 (DNS-only 灰云, 跟 hub 同模式)docs.flytoex.net→ 45.145.229.197 同上
调用 PUT /client/v4/zones/<zoneID>/dns_records 用 PM 给的 API token
(Zone:DNS:Edit scope on flytoex.net), 一次 201 Created. 不走 PM web UI.
实测¶
docker build -f deploy/Dockerfile.godoc .→ 61.8s 通过, image 跑起来 / 200, /git.flytoex.net/yuanwei/flyto-agent 200docker build -f deploy/Dockerfile.docs .→ 5.83s mkdocs build 通过, nginx serve / 200,<title>Flyto Agent 文档</title>渲染- mkdocs build INFO 警告 (不阻 build, follow-up 修):
docs/evolve-strategy.md几个章节 anchor link 不存在 (中文 heading anchor 不一致);platform/common/docs/grpc-api.md#google-protobuf-Timestampanchor 不存在
已知 follow-up¶
- pkgsite cold start 30-60s 是首次请求 load 全 module 不是启动. 生产可加 Docker HEALTHCHECK 触发预热, 当前接受首访慢
- mkdocs build 几个 anchor warning, 后续清理文档时顺手修
- Swagger UI (hub.flytoex.net/swagger/) 与 godoc 现独立 host 不交叉, docs.flytoex.net nav 可加链接到 swagger 入口 (后续)
v0.3.2 (2026-04-26)¶
Hotfix #2, v0.3.1 紧随其后.
v0.3.1 push 后 build-push job 绿 (image 成功 push 到 Gitea registry), 但
deploy job SSH 到 HK-133 跑 docker compose pull 在 parse 阶段 fail:
error while interpolating services.common.environment.ANTHROPIC_API_KEY:
required variable ANTHROPIC_API_KEY is missing a value
根因: v0.3.0 commit c35e761 给 deploy/docker-compose.yml common.environment
加了 ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:?missing ...} 强制语法, 但
.gitea/workflows/release.yml deploy script 没同步加 export ANTHROPIC_API_KEY=...
(跟 FLYTO_PROXY_TOKEN / ADMIN_BASIC_AUTH_* 同模式漏写一行). 服务器仍跑老
image (v0.2.0 时期), /api/v1/* 与 /swagger/* 没生效.
修法两处 (本 hotfix):
1. release.yml deploy script 加 export ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}"
2. Gitea repo Settings → Actions → Secrets 配 ANTHROPIC_API_KEY (通过 Gitea
API PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{name} 自动化完成,
201 Created)
教训登记 memory (待写): 给 docker-compose 加强制 env 必须同步三处 — compose
yaml 自身 / release.yml deploy export / Gitea secret. 漏一处部署链断.
feedback_validate_network_path_before_deploy.md v0.3 cycle 内被验证两次
(L407 commit C0 path bug + 本 hotfix env bug).
v0.3.1 (2026-04-26)¶
Hotfix, v0.3.0 紧随其后.
platform/common/go.sum 在 v0.3.0 commit 时少 golang.org/x/net@v0.50.0 的
transitive checksums (net/trace / net/http2 / net/http2/hpack), 平时
开发用 GOWORK=on 拉父 workspace 全套 sum 没踩坑, 但 Dockerfile build 走
GOWORK=off, Go 1.26 严格模式直接 missing go.sum entry 拒绝 build. 跑
GOWORK=off go mod tidy 补齐即修复. 触发原因: v0.3.0 cycle go get
github.com/swaggo/http-swagger/v2 把 golang.org/x/net v0.49.0 → v0.50.0,
后者新切了 net/trace 等 sub-package, 老 go.sum 不含.
教训登记 memory: workspace 模式下 go get 不会主动跑 go.sum tidy, 升级
间接依赖时必须 GOWORK=off go mod tidy 模拟 CI 视角再 verify; release
checklist 加 "docker build -f platform/common/Dockerfile ." 实测一次, 不要
靠 CI runner 头次发现.
platform/common/go.sum+14 / -4 (golang.org/x/net v0.50.0 transitive hash 补齐);platform/common/go.mod+4 / -2 (indirect dep 重排序)
v0.3.0 (2026-04-26)¶
v0.2.0 之后的三轮发版, 核心交付 platform/common 业务通道激活 + 消费层文档自动化全产物. v0.3 标志 Flyto Agent 从 "core 引擎就绪 + platform 静默" 过渡到 "platform 业务 REST/SSE 通路打通 + Swagger UI / gRPC markdown / CONSUMERS.md 三件套消费层一站式入口". core 引擎层新增 counterfactual + skills/reverse_think + shadowdb 三个子包, 7 provider 全接通 Temperature/TopP sampling 旋钮; ADR-0002 立 "REST 业务 / gRPC 观测" bifurcation, ADR-0001 归档反事实工作流引擎级 enforcement 否决方案.
核心新增 (core 引擎库)¶
- counterfactual 子包 + reverse_think Go skill + ReverseThinkingHook reference (L569 缩水) — 反向思维 first-class 数据结构 + Go 化 MiniMax client + platform 层一行启用入口. 6 commit 节奏落地, 原方案 A 完整版 (引擎级 8 模块 1500-2500 行) 经 3-agent review 否决, 走 Option 2-Plus 缩水方案 (4 件套 + ADR-0001 归档否决理由)
core/pkg/counterfactual/—Deliverablestruct 对齐~/.claude/skills/reverse-think/SKILL.mdJSON schema (hidden_assumptions / failure_scenarios / verdict / verdict_reason 4 核心 + ToolName / Step / DecisionID / OccurredAt 4 元数据). Verdict 常量 (A_holds/B_wins/depends_on_X) 取值开放,Validate不强制枚举允许 LLM 引入新标签 forward compat.MetadataKey = "flyto.counterfactual.deliverable"跨包 staging.Record.Metadata 落点常量core/pkg/skills/reverse_think/—Client持 APIKey + Endpoint + Model + MaxTokens (默认 8000 thinking+text 共享池) + HTTPClient + Now 注入.Run(ctx, Prompt, Annotations) (*Deliverable, error)渲染中文 prompt → POST Anthropic 兼容端点 → unmarshal → stamp metadata → Validate. 不读MINIMAX_TOKEN_PLAN_KEYenv (core 与环境解耦, 调用方注入), 刻意不 strip code-fence 包装 (LLM 遵守度 bug 应暴露非兜底). 4 sentinel error: ErrAPIKeyRequired / ErrEndpointFailed / ErrNoTextContent / ErrParseDeliverabletools.Metadata.RequiresReverseThinking— 与RequiresCheckpoint同源 opt-in flag, 默认 false. 适用非平凡设计空间 (多策略选择 / 失败模式不直观 / schema 漂移), 不适用纯机械 CRUD. 与 RequiresCheckpoint 边界: 后者问 "需要人确认?", 前者问 "agent 应不应该挑战自己的建议?", 两者可叠加 core 不规定顺序由消费 hook 链决定Deliverable.AsReplayEvent进化沉淀接线 — counterfactual import evolve 单向, evolve schema-agnostic 不感知 counterfactual. Payload 是 Clone 防 Reflector 摄入时回溯污染 staging.Record. Feedback 返 nil 对齐 evolve.ReplayEvent godoc "决策刚发生 KPI 反馈延迟中". MetaKey 4 常量 (Step/Verdict/Source) 跨包消费方按字面量匹配. 关上 "各行业自写 skill 进化结果停在哪里" 循环: 各行业触发策略可不同, 但 Deliverable schema + AsReplayEvent adapter + evolve.Reflector 学习池在 core 共享统一platform/common/safetychain/ReverseThinkingHook— Go 行业一行启用入口, 与Assemble平行 (Assemble 装 Validator+CircuitBreaker decorator, 本 hook 走 hooks.PreToolUse 不 wrap Tool). Client + Lookup + PromptBuilder + Sink 字段; fail-open advisory (LLM 错不阻断业务, ExitCode=0 + Error 挂供观测层暴露降级状态). DefaultPromptBuilder 仅合格下限, 行业按域覆盖. JSONOutput[counterfactual.MetadataKey] 携带 *Deliverable 让下游 hook / 引擎读不必二次 LLM 调用- 业界共识对齐: 调研 agent 拉的 8 框架 (Claude Agent SDK / OpenAI Assistants / Vertex ADK / LangGraph / AutoGen / CrewAI / Semantic Kernel / Cline) 全部走 "engine 提供 hook primitive, 策略由开发者写", 零框架在引擎里 hardcode "决策前必须填五步 deliverable 才能调 tool". 本缩水方案与之同形 (hook 路径 + tool metadata + 开发者自填 PromptBuilder + Sink), 没在 Flyto 引擎核心引入业界都没做的 enforcement primitive
- 35 测试 -race 全绿: counterfactual 13 + reverse_think 12 + tools 1 新增 + safetychain 9. core + platform/common 双 module build 干净. 4 件套 + reference hook 总量 ~2,131 行 (含双语 godoc + 测试)
-
3-agent 并行 review (调研业界 8 框架矩阵 + 决策前 gate 形态调查 / 质疑 agent 5 道硬质疑含
tools.Metadata.RequiresCheckpoint已实装 anchor 修正 / 设计 agent 4 备选 + 边界图 + 拍板假设矩阵). PM 拍板 Option 2-Plus (在原 Option 1 不做 + Option 2 缩水之间加进化沉淀件),core/docs/adr/0001-reverse-thinking-gate.md归档完整方案对比与否决理由 -
shadowdb 子包 (L437) — Agent 多轮推理 session 级 shadow 表. 方案 C 列标记隔离 (
session_id VARCHAR(64) NOT NULL+ filter), 规避 PG TEMP TABLE 在 pooled*sql.DB下蒸发问题 + driver 分裂. 零 DDL 纯INSERT/UPDATE/DELETE/SELECT + ?, PG/MySQL/SQLite 通吃 Openerinterface +InMemoryOpener参考实现 (开 / 关 / Reap 孤儿)Session结构 +ShadowDBnewtype (与tools/builtin.StagingDB同族意图标记)EnforceSessionFilter(sql)三层防御中层 — quote-aware 剥离字符串/注释 后正则校验session_id=?filter 存在, 不防对抗性 SQL 拼接但捕捉 LLM 随手遗漏- pull-only
Reap(ctx, olderThan)— core 不起 goroutine, 平台层从自己的 cron / watchdog 触发 - 与 staging 边界清晰: staging 决策级 pre-commit (1 decision = 1 Record), shadowdb session 级推理中 scratchpad; 单向 shadow → staging → production, shadow 永不 merge
-
24 tests 全绿含
-race+ 并发 Close/Reap 赛跑测试 (in-memory sqliteSetMaxOpenConns(1)串行化让测试验证 bookkeeping 竞态不测 driver 内部) -
Temperature / TopP cross-provider passthrough (L683) —
flyto.Request加Temperature *float64+TopP *float64, 7 provider (anthropic / openai / minimax / gemini / ollama / lmstudio / openrouter) 全部接通 sampling 旋钮. 此前 7 provider 的 Config 与 Stream 一律不传 temperature/top_p, 是 day-1 产品功能缺失而非简单 wire 问题 - 纯 passthrough + 极小特例: 越界一律不在 flyto 层 clamp, 上游 4xx 自然冒泡为 ErrorEvent (业界共识 — Vercel AI SDK / LangChain / instructor / LlamaIndex 全 passthrough; 仅 litellm 尝试 drop_params 且 bug 频发, issues #8192 / #16090 / #5884)
- 仅 1 个 wire 时已知冲突预拦: Anthropic + NeedsThinking + Temperature != 1.0 → silent override 1.0 +
parameter_overriddenWarningEvent; TopP < 0.95 同理 (服务端硬约束 [0.95, 1.0]) - 不加 OpenAI o-prefix model 检测: server 4xx 已是清晰 ErrorEvent, model-prefix 列表长期维护 burden 不值 (litellm 先例实证)
- 0 是合法 deterministic 值,
*float64+omitempty干净分离 nil 与 0 语义; helperflyto.Float(v)简化指针字段构造 - ADR rule of two: 本 commit 是 sampling knob 最小 subset (Temperature + TopP); TopK / Seed / FrequencyPenalty / PresencePenalty 走 follow-up TODO 等真消费者驱动. 业界 Vercel AI SDK / LangChain 同等对待 sampling knobs, cherry-pick 单字段是合理边界
- 7 provider wire 路径: anthropic 经
internal/transport.MessageRequest+applyThinkingSamplingConstraintshelper; openai/ollama/lmstudio/openrouter/minimax(streamOpenAI) 经internal/wire.StreamRequest+openaiReqjson:temperature/top_ptop-level; gemini 嵌generationConfig.temperature/topP; minimax(streamAnthropic) + anthropic 兼容路径同 anthropic - evolve 进化算法层串通:
LLMCallOpts.TopP+WithTopPGenOpt +genConfig.TopP+buildMeta写meta["top_p"](ParameterEvolver 与 evaluator 现可追溯候选完整 sampling 配置).FlytoLLMClientadapter 把 LLMCallOpts (float64, zero=未设) 翻译到flyto.Request(*float64, nil=未设):if v != 0 { req.X = flyto.Float(v) } - 23 tests 全绿 (17 wire/provider 层 + 6 evolve 层); baseline 220 → 219 (
LLMCallOpts.Temperature实际 drain — adapter 现在真读 selector)
platform/common 层¶
- 业务 REST/SSE 通道激活 (L692, ADR-0002) —
platform/common/internal/server/server.go1263 行已写好但未启动的业务 REST/SSE 实现接进 cmd/common, 通过新增--rest-addr=:8080启用. 6 commit 节奏完整落地, 3-agent review reconcile + PM 二次拍板. 核心决策: REST/SSE 唯一业务消费通道 + gRPC 仅观测面 (SafetyChain / Health) + 不为 C# 单独加业务 RPC + 不加 grpc-gateway (admin/server.go 已有观测面 REST handler) + Tool 级 SafetyChain 装饰留给行业 platform server.go三 API 拆分:buildHandler() http.Handler(mux + middleware chain 抽出) +Serve(ctx, ln) error(主 API, 接收外部 listener + ctx 不接管 signal) +ListenAndServe()(向后兼容 wrapper). 让 cmd/common 用一个 signal handler 统一协调 gRPC + admin HTTP + 业务 REST- OIDC 鉴权统一 (Q2.1 致命修复): 删
Config.BearerToken+authMiddlewareraw shared-secret + ConstantTimeCompare 时序防御实现; 加Config.Verifier *auth.Verifier走 auth.HTTPMiddleware. dev 模式 (Verifier=nil) 跳过鉴权与 admin/server.go 一致, 让 cmd/common 跨 gRPC + admin HTTP + 业务 REST 共用同一套 OIDC 验签 - server 不再内嵌 anthropic provider 写死:
Config删 9 字段 (APIKey / BaseURL / BearerAuth / Model / SystemPrompt / AppendPrompt / Tools / MaxTurns / PermissionMode), 不再读 ANTHROPIC_API_KEY 不再 hard-code "claude-sonnet-4-6". 新增Attach(eng *engine.Engine)+HandlePermission(ctx, req)让外部装配 engine 时把 SSE 桥接的 permission.Handler 传给 engine.Config.PermissionHandler 再 Attach cmd/common/main.go三 listener wire: 新增--rest-addrflag (默认空 = 不启动); 启用后装 anthropic provider + engine + s.Attach + net.Listen → s.Serve(restCtx, ln). signal handler 三路协调 SIGINT/SIGTERM 触发 grpcSrv.GracefulStop + httpSrv.Shutdown + restCancel (server.Serve 内部用 10s 上限做 Shutdown). errCh capacity 升 3, wantListeners 按启用 listener 数动态加- Tool 级 SafetyChain 装饰刻意不在 cmd/common 装 — 一刀切 AlwaysApprove + DefaultExtractor 会把所有 Tool 锁同一组合或 fan out 到不匹配 Tool 语义的 wrap. common 保持纯 transport, verdictStore 接线就绪等行业驱动代码 (logistics 等) 在 Tool 注册时装饰并写数据
- deploy 配置就绪:
deploy/docker-compose.ymlcommon.expose 加 8080 + command 加--rest-addr=:8080+ environment.ANTHROPIC_API_KEY${...:?...}必填;deploy/Caddyfile加handle /api/v1/* { reverse_proxy common:8080 { flush_interval -1; transport http { response_header_timeout 0 } } }让 SSE chunk 透传不被代理缓冲, 模型长思考时不被 idle timeout 切断 (客户端断线检测靠 server.go 15s 心跳); README topology + 业务 REST/SSE curl 例子同步 - ADR-0002 立项:
core/docs/adr/0002-rest-business-grpc-observation.md体例对齐 ADR-0001 八节结构 (背景 / 决策 / 评估流程 / 后果 / 替代方案保留 / 触发重评估条件 / 参考 / 修订记录). 核心定性: 与 9 天前 memoryfeedback_architecture_principle_over_rule_of_two.md的 "gRPC 优 HTTP / C# 走 gRPC" 是 bifurcation 不是 U-turn (观测仍 gRPC 沿用, 业务用 REST 是补另一面). 调研引用 11 LLM API 矩阵 (Anthropic / OpenAI / Bedrock / Cohere / Mistral / Replicate / Together / Fireworks / Ollama / LM Studio / LangServe) 全 REST/SSE 单通道, Vertex AI 是 gRPC-first 例外但 Google 内部全栈延伸不仿照 - 测试: server_test.go 既有 546 行用 httptest 直接打 handler 不动; 删 4 raw bearer TestAuth_* (auth.HTTPMiddleware 自身覆盖在 internal/auth 包测试); 新加
TestServer_Serve_RespectsCtxctx 取消干净返回 + 集成测试 listener bind/shutdown 时序. -race 全绿. cmd/common --help 验证--rest-addrflag 出现 - 4 tracked debt 登记:
L693业务 REST 多副本 SessionStore interface (P2, in-memory sessions 单实例假设);L694gRPC + REST cross-transport request-id / trace 串通 (P3, OpenTelemetry);L695SSE 1000 单/s 带宽监控 (P3, framing overhead 5-10x);L407Swagger/OpenAPI (P2, ADR-0002 直接子项) -
commit 顺序:
01f08e7C1 拆 Serve+wrapper /8189d05C2 OIDC /694bd07C3 Attach + HandlePermission /e1db327C4 cmd/common --rest-addr /c35e761C5 deploy /e330774C6 ADR-0002 + 文档 -
消费层文档自动化三件套 (L407) — 业务 REST + 观测 gRPC + 顶层 wrapper 一站式入口. ADR-0002 直接子项, 7 commit 节奏 (含 1 path bug 顺手修). queued task 锁定 swag (注解式 OpenAPI 2.0) > huma (code-first 要重写 1263 行 server.go) 路线; SDK 自动生成 (Stainless 模式) 不做 rule of two 等 5+ 语言客户端
platform/common/internal/server/server.go8 业务 handler (handleHealth/handleAgentRun/handleCreateSession/handleGetSession/handleDeleteSession/handleSendMessage/handlePermissionReply/handleListTools) 加@Summary @Description @Tags @Accept @Produce @Param @Success @Failure @Router @Security注解块, 不动业务逻辑. 新增 3 named response type (HealthResponse/StatusResponse/ListToolsResponse) 替代 ad-hoc map[string]any 让 swag schema 准 (handler 仍写 map, JSON 字段名与 struct 一致, 外部契约对齐)platform/common/internal/server/swag.goseed file 持 service-level @-block (@titleFlyto Agent Engine Business REST API /@versionv1 /@hosthub.flytoex.net /@BasePath/api/v1 /@securityDefinitions.apikey BearerAuth). 放路由旁不放 cmd/common/main.go, single-source-of-truth: 拥有路由的文件也拥有 swagger specplatform/common/docs/4 自动生成产物:swagger.json22.5K (12 schema, paths) +swagger.yaml12.3K +docs.go23.1K (Go embed, side-effect import 让 cmd/common --swagger UI 不读文件系统就嵌入) +grpc-api.md278 行 (protoc-gen-doc v1.5.1 产, HealthService + SafetyChainService 字段表)cmd/common --swaggerflag opt-in/swagger/*Swagger UI (httpSwagger v2 包内嵌). server.go authMiddleware + rateLimitMiddleware allow-list 加strings.HasPrefix("/swagger/")让 UI 在 OIDC 启用时仍可加载, 不烧 rate-limit 预算 (业界共识: Stripe / Anthropic / OpenAI 都公开 OpenAPI spec). 路径在 /api/v1 版本命名空间外, swagger 是 meta artifact 不是版本化业务端点docs/CONSUMERS.md顶层 wrapper 133 行: 端口拓扑表 (3 listener × Caddy 路径分流) / 必填环境变量表 / cmd/common 10 flag 表 / OIDC auth 流程图 (含 allow-list 例外 /api/v1/health, OPTIONS, /swagger/*) / 业务 REST 一次性 + 多轮会话 curl 例子 / 观测 gRPC SafetyChain 指引 / 进一步阅读链 (ADR-0002 + deploy/README + TODO + Dockerfile). 链 swagger.json + grpc-api.md 不重复 spec, 只放代码抽不出来的 (流程图 / 端口拓扑 / 跨 service 调用 narrative)core/Makefile加 4 个 docs target (docs-swag / docs-grpc / docs-consumers / docs-all) + 升级 docs-install 装 swag@v1.16.6 + protoc-gen-doc@v1.5.1 (与 staticcheck@v0.7.0 同 pin 版模式). 顶部export PATH := $(shell go env GOPATH)/bin:$(PATH)让 make 子 shell 找到 install 的二进制;ROOT := git rev-parse --show-toplevel让 docs-* 从任意 cwd 都能 cd 到 platform/common.gitea/workflows/release.yml加 docs drift gate (apt-get protoc + make docs-install + make docs-swag + make docs-grpc + git diff --exit-code 4 产物 swagger.json/yaml/docs.go/grpc-api.md). 与 dead-field-ratchet 平级位于 build-push job 开头. 业界对照: Stripe / Anthropic / OpenAI 都对 OpenAPI spec 跑同等 drift 闸 (PR 必须 commit 生成产物, 否则 CI 红)deploy/Caddyfile加handle /swagger/* { reverse_proxy common:8080 }(无 flush_interval, 静态资源 + JSON 不流式) +deploy/docker-compose.ymlcommon.command 加--swaggerflag (HK-133 lab 默认启用; 生产 deploy 用另份 compose 关闭)- 意外+顺手修 (C0 commit
89c38d3): 上一会话 commit 5 (c35e761) Caddyfilehandle /api/v1/*不剥前缀, server.go mux 注册/v1/*不匹配, 经 hub.flytoex.net 全 404 (内部直连 localhost:8080/v1/* 才通). L407 之前先打通真实部署链, server.go 8 mux 路由 + server_test.go ~25 处 path + main.go 注释 + ADR-0002 § 2.1 端点形态 + Caddyfile 注释 SSE 例子全部对齐/api/v1/*一致 (内外一致, BasePath=/api/v1 干净). 触发 memoryfeedback_validate_network_path_before_deploy.md警告再次成立 - smoke:
ANTHROPIC_API_KEY=fake go run ./cmd/common --rest-addr=:18080 --swagger→ /api/v1/health 200 + /swagger/index.html 200 + /swagger/doc.json 200 返回 commit 1 嵌入 spec; caddy:2-alpine validate Caddyfile = "Valid configuration"; docker compose config common.command 4 flag 全到位; build + -race 全绿 - commit 顺序:
89c38d3C0 path bug fix /26bc732C1 swag 注解 + spec 首版 /bce7670C2 --swagger flag + UI /22b6388C3 Makefile + CI drift gate + grpc-api.md /a9b04abC4 CONSUMERS.md /bf5af1dC5 deploy / 本 commit C6 同步
关键设计原则¶
- bifurcation, not U-turn (ADR-0002) — 业务 REST / 观测 gRPC 分层. 与 9 天前 memory
feedback_architecture_principle_over_rule_of_two.md的 "gRPC 优 HTTP / C# 走 gRPC" 不冲突: 观测仍 gRPC 沿用, 业务用 REST 是补另一面. 业界对照 11 LLM API (Anthropic / OpenAI / Bedrock / Cohere / Mistral / Replicate / Together / Fireworks / Ollama / LM Studio / LangServe) 全 REST/SSE 单通道, Vertex AI 是 gRPC-first 例外但 Google 内部全栈延伸不仿照 - single-source-of-truth (代码即文档) — 业务 REST swag 注解长在 server.go 旁 (拥有路由的文件拥有 swagger spec), 不放 cmd/common/main.go; 观测 gRPC 字段表由 protoc-gen-doc 直读 .proto 注释; CONSUMERS.md 链 auto 产物不重复 spec. CI drift gate 卡
git diff --exit-code4 产物, 业界对照 Stripe / Anthropic / OpenAI 都对 OpenAPI spec 跑同等闸 (PR 必须 commit 生成产物) - 顺手修部署 path bug — L407 commit 0 整体迁移 server.go (8 mux + 6 godoc + 2 middleware allow-list) + Caddyfile (注释举例) + ADR-0002 § 2.1 + main.go (3 处注释) + server_test.go (~25 处) 路径到
/api/v1/*一致 (上一会话 commit 5c35e761Caddyfilehandle /api/v1/*不剥前缀致 server.go 注册/v1/*经 hub.flytoex.net 全 404, 没真实经 Caddy 实测). 触发 memoryfeedback_validate_network_path_before_deploy.md再次成立 - fail-open advisory vs fail-closed gating —
ReverseThinkingHook对 LLM 错不阻断业务 (ExitCode=0 + Error 挂 HookResult.Error 供观测层暴露降级状态), 与 Validator / Staging / CircuitBreaker 的 fail-closed 形成边界互补 (advisory hook vs gating decorator). DefaultPromptBuilder 仅合格下限, 行业按域覆盖 - schema-agnostic 旁支 (shadowdb) — session 级 shadow 表用列标记
session_id VARCHAR(64) NOT NULL+ filter, PG / MySQL / SQLite 通吃 (规避 PG TEMP TABLE 在 pooled *sql.DB 下蒸发 + driver 分裂). 与 staging 决策级隔离边界清晰: 单向 shadow → staging → production, shadow 永不 merge - sampling knob passthrough — Temperature / TopP 越界一律不在 flyto 层 clamp, 上游 4xx 自然冒泡 (业界共识 — Vercel AI SDK / LangChain / instructor / LlamaIndex 全 passthrough; 仅 litellm 尝试 drop_params 且 bug 频发, issues #8192 / #16090 / #5884). 仅 1 个 in-Request 已知冲突预拦: anthropic + NeedsThinking + Temperature != 1.0 → silent override 1.0 +
parameter_overriddenWarningEvent - Tool 级安全链装饰是行业 platform 责任 — cmd/common 不调
safetychain.Assemble. 一刀切 AlwaysApprove + DefaultExtractor 会把所有 Tool 锁同一组合或 fan out 到不匹配 Tool 语义的 wrap; common 保持纯 transport,verdictStore接线就绪等行业驱动代码 (logistics 等) 在 Tool 注册时装饰并写数据 (ADR-0002 § Decision 第 3 条) - swag 注解 > huma code-first — L407 选型: huma (code-first) 要重写 1263 行 server.go, swag (注解式 OpenAPI 2.0) 不动业务逻辑只加注释. SDK 自动生成 (Stainless 模式) 不做 rule of two 等 5+ 语言客户端
已知限制¶
以下功能留待 v0.4+ 完成 (不影响 v0.3 使用):
- ML Validator backend 未接入 — core 接口就绪 (
validator.Validator+LLMValidator+CompositeValidator+AlwaysApprove+NewValidatedToolnil fail-fast), platform 层接外部 ML HTTP backend 实现 + 装配即启用 (TODO L434, P1) - 熔断器未在消费层启用 — core 三态 breaker +
VerdictSink桥接就绪, platform 选作用域 (NoOpScope/DestructiveOnlyScope/PerToolScope) + wireValidatedToolsink 即启用 (TODO L435, P1) - 业务 REST 多副本 SessionStore interface —
server.go当前 process-local in-memorysessions map, k8s 多副本 LB round-robin 立刻挂; v1.0 发版前必做 (TODO L693, P2; ADR-0002 § Consequences 显式标注) - gRPC + REST cross-transport request-id / trace 串通 — 单租户 dogfood 不需要, 上量后 OpenTelemetry 接 Tempo / Jaeger (TODO L694, P3)
- SSE 1000 单/s 带宽监控 — prometheus exporter 量化 framing overhead 5-10x, 真到瓶颈再优化 (压缩 / batch / fallback gRPC streaming, v1.0+ 课题; TODO L695, P3)
- CAP-4 自动化 × 3 — Flyto CLI 无头自消费 / CI/CD 集成 / WebSearch 前置 (TODO L488-490, P2; 低优工具效率)
- Provider 模型表自动更新 — Anthropic / MiniMax / OpenAI / Gemini 新模型上线需手工更
capabilities.json(TODO L454, P2) - AuditSink DB 实现 — 接口就绪, PostgreSQL 写入 + session 查询 + 批量回滚在 platform 层 (TODO L438, P3)
- WMS 波次参考实现 — "任务已建待确认" 状态机扩展在 platform 层 (TODO L439, P3)
- 场景化编排 Go 教程 —
examples/orchestration/{ssh_deploy,db_migration,system_config}三组示例等真消费者驱动 (TODO L408, P3) - 微信 ClawBot 接入 — 触发渠道扩展, 当前无业务驱动 (TODO L571, P3)
- .proto 字段注释完善 —
health.proto部分字段 description 列空,protoc-gen-doc自动反映, 后续工作
发布事实¶
- TODO: 47 → 52 done (+5: L437 shadowdb / L569 反事实缩水 4 件套 / L683 Temp+TopP / L692 业务 REST 通道 / L407 文档自动化三件套), 14 → 13 open (L693 / L694 / L695 / L407 → 仅 L407 完成出列, L693-695 自 L692 ADR-0002 tracked debt 入列)
- 测试: counterfactual 13 + reverse_think 12 + tools 1 + safetychain 9 (L569) + shadowdb 24 (L437) + Temp/TopP wire 17 + evolve 6 (L683) + L692 server_test 既有 546 行不动 + 1 ctx 测试 + L407 server smoke (curl 3 个端点) -race 全绿. core + platform/common 双 module build 干净
- 依赖: 新增
github.com/swaggo/http-swagger/v2 v2.0.2+github.com/swaggo/files/v2 v2.0.0; 升github.com/swaggo/swag v1.8.1 → v1.16.6(CLI 1.16 产 docs.go 用了 LeftDelim/RightDelim 新字段, dep schema 必须对齐) - ADR:
ADR-0001反事实工作流引擎级 enforcement 否决归档 (8 框架业界共识 + 5 套现有 gate 概念重叠 + 范畴错位 + 吞吐数学不成立);ADR-0002REST 业务 / gRPC 观测 bifurcation 立项 (11 LLM API 矩阵 + grpc-gateway/gRPC-Web 现状 + ConnectRPC 替代). 体例八节结构对齐 - 设计决策工作流: Agent Teams 3 角色并行 review 持续作 default, v0.3 cycle 实战 5 次 (L437 / L569 / L683 / L692 / L407)
- CI: 新增 docs drift gate 与 dead-field-ratchet 平级在
release.ymlbuild-push job 开头 (apt-get protoc + make docs-install + make docs-swag + make docs-grpc + git diff --exit-code 4 产物 swagger.json/yaml/docs.go/grpc-api.md) - Tag: 历史以
v0.1.0-alpha.{1..8}走 8 次 alpha 后 cut v0.1.0; v0.2.0 一次性 cut 不走 alpha; v0.3.0 同 v0.2.0 一次性 cut
文档同步¶
core/docs/data-safety.md:635原CREATE TEMP TABLE shadow_inventory_<sid>伪 SQL 回销为方案 C 列标记实现, 注明为什么不走 TEMP TABLE 路径core/TODO.mdL437 + L683 + L692 + L407 打勾, 加 L693 / L694 / L695 (P2/P3 tracked debt 自 L692 ADR-0002), v0.3 platform 消费层条目重新计数为 9 (含 L693-695)core/docs/adr/0002-rest-business-grpc-observation.md新建, 业务=REST 观测=gRPC bifurcation 决策记录core/docs/adr/0001-reverse-thinking-gate.md新建, 反事实工作流引擎级 enforcement 否决归档docs/CONSUMERS.md新建顶层 wrapper 133 行, L407 三件套唯一手写部分
v0.2.0 (2026-04-24)¶
v0.1.0 之后的二轮发版, 核心交付 "Agent → staging → Validator → ValidatedTool → CircuitBreaker → WMS API" 完整安全链 core 侧就绪. 引擎层新增 4 个子包 (validator / circuitbreaker / reflector / staging), evolve 9/9 接口矩阵参考实现全部落地, SQL 工具链 3 件套补齐 staging 一跳, platform/common 装配 + 观测 + gRPC 三面暴露就绪供 Go / C# 行业 platform 消费.
核心新增 (core 引擎库)¶
- validator 子包 — 可插拔审批契约: RuleValidator (DiffSize / TableWhitelist / Pattern 3 内置规则) / LLMValidator (Flyto provider 桥接) / CompositeValidator (AllMustApprove + Waterfall 2 模式), 显式
AlwaysApproveopt-out, nil fail-fast 构造期 panic - circuitbreaker 子包 — 三态状态机 (Closed / Open / HalfOpen) + Clock DI + VerdictSink 桥接, 订阅 ValidatedTool 的 Verdict 流水自动推进状态
- reflector umbrella 包 — 表达 "反射器" 产品抽象, 4 个跨家族 adapter (
ValidatorAsEvaluator/EvaluatorAsValidator/ValidatorAsReflector/EvaluatorAsReflector), 不引新顶层接口; 规则 / LLM / ML 后端可互换接入validator.Validator/evolve.Evaluator/evolve.Reflector任一同族接口 - staging 子包 — 决策包级 staging 表, 7 状态机 (
pending_tech → rejected_tech | pending_ml → rejected_ml | approved → executed | failed), 混合控制 Engine (前两段 arc staging 主动调 Validator, 末段approved → executed/failed外部推 MarkExecuted/MarkFailed), 可插拔DependencyGuard+InMemoryStore参考实现 + 内置TenantDenyGuard, pull-only query API - evolve v0.2+ 接口矩阵 9/9 参考实现 — Generator / Evaluator / Reflector / ApprovalFunc / ParameterStore / ParameterEvolver / LogReplayer / LogSource / FeedbackChannel / ShadowRunner 全部落地, 闭环 example 单进程跑通 (baseline 1.0 → version 2 的 1.5, divergence 0.1736, shadow delta +0.105)
- tools SQL 工具链 3 件套 — 只读校验器 (零 DB 依赖字符串解析) / CAS 乐观锁 (
maxRetries=0fail-fast 默认) / Dry-run 三路 (before + after SELECT +preview_predicate+ 100 行 truncate), 支撑 staging 一跳 - evolve FlytoLLMClient adapter —
flyto.ModelProvider → LLMClient桥接, LLMGenerator 可驱动任意 provider (anthropic / openai / minimax / gemini / ollama / lmstudio / openrouter); TextEvent 权威 + TextDeltaEvent 兜底避免 anthropic 双倍计数
platform/common 层 (SaaS 基础设施)¶
- safetychain 装配包 — 一行
Assemble(inner, v, extractor, scope, sink) tools.Tool组合 Validator + CircuitBreaker + ValidatedTool decorator, 3 breaker scope 工厂 (NoOpScope/DestructiveOnlyScope/PerToolScope) - admin HTTP 观测端点 —
GET /admin/safetychain/verdicts+GET /admin/safetychain/breakers, opt-in 挂载, auth middleware 兜底;VerdictStore接口 +RingStorebounded 内存实现 O(1) 热路径无分配 - gRPC SafetyChainService — 给 C# industry platform 原生通路, 2 RPC (
ListVerdicts/ListBreakerStates), bufconn e2e 互操作验证 proto 线格式稳定
关键设计原则¶
- fail-closed: Validator 返回 error 视 Block; CompositeValidator Waterfall 任一子 Validator Block 整体 Block; CircuitBreaker Open 直接拒绝调用; nil 依赖构造期 panic 拒启动
- schema-agnostic:
DiffInput.SourceTool作分发键,Raw []byte+Metadata map[string]any携带 opaque 载荷, 引擎层不假设任何行业 schema - 显式 opt-out 必须写出来:
AlwaysApprove{}/AllowAlwaysGuard{}/NoOpScope()/ nil Validator panic — 没有静默默认, industry 刻意关审批必须 code-level 显式动作, 消灭 "以为开了审批实际没开" 的安全假象 - 产品可替换性: 核心接口 (
Validator/Evaluator/Reflector/DependencyGuard/Store/VerdictStore/BreakerScopePolicy) 全部多态可插拔, 规则 / LLM / ML backend 互换接入任一 slot, reflector umbrella + 4 adapter 保证跨家族兼容
已知限制¶
以下功能留待 v0.3+ 完成 (不影响 v0.2 使用):
- ML Validator backend 未接入 — core 接口就绪 (
validator.Validator+LLMValidator+CompositeValidator+AlwaysApprove+NewValidatedToolnil fail-fast), platform 层接外部 ML HTTP backend 实现 + 装配即启用 (TODO L434) - 熔断器未在消费层启用 — core 三态 breaker +
VerdictSink桥接就绪, platform 选作用域 (全熔 / 只熔写 / 每工具一熔) 并 wireValidatedToolsink 即启用 (TODO L435) - 影子表生命周期管理 — Dry-run 工具已做 (模块 23 SQL Dry-run), 多轮推理临时表创建 + 会话结束清理在 platform 层 (TODO L437)
- AuditSink DB 实现 — 接口就绪, PostgreSQL 写入 + session 查询 + 批量回滚在 platform 层 (TODO L438)
- Temperature cross-provider 契约缺口 —
LLMCallOpts.Temperature被FlytoLLMClientadapter 刻意忽略 (flyto.Request最大公约数无 Temperature 字段), 温度控制仍需在 provider 工厂 Config 设置 (TODO L683, P3 设计决策, 侵入式扩flyto.Request跨所有 provider) - CAP-4 自动化 × 3 — Flyto CLI 无头自消费 / CI/CD 集成 / WebSearch 前置 (TODO L488-490, P2)
- Provider 模型表自动更新 — MiniMax / Gemini 新模型上线需手工更
capabilities.json(TODO L454, P2) - WMS 波次参考实现 — 状态机新增 "任务已建待确认" 在 platform 层 (TODO L439)
- 反事实工作流引擎级 enforcement — 1500-2500 行改造远期 2026 Q3-Q4 候选 (TODO L569, P3)
发布事实¶
- Baseline: 212 → 216 dead fields (+4 合法 tracked debt:
Record.TechVerdict / BizVerdict / ExecutionError / ExecutionProof外部 audit dashboard 消费, core 无内部 reader, 按feedback_exported_field_delete_needs_review.md) - TODO: 46 → 47 done, 15 → 14 open (L436 staging ✅ check off, 模块 23 SQL × 3 ✅, 安全链 C1-C4 ✅)
- 测试: staging 47 + reflector 25 + validator / circuitbreaker / safetychain assemble 16 + admin 11 + gRPC server 6 (bufconn e2e) 全绿, race detector 通过
- 依赖:
modernc.org/sqlitetest-only dep 落地 (core 第一条非图像处理第三方依赖, SQL CAS 测试用, 生产路径仍仅依赖database/sql标准库) - 设计决策工作流: Agent Teams 3 角色并行 review 成默认, session 实战 5 次 (SQL CAS / 候选选型 / Validator 接口 / reflector umbrella Option d / staging 5+2 决策)
详细变更¶
以下 sub-sections 保留 session-by-session 技术决策 reasoning 作为 release notes 详录, 供下游 consumer / 技术反思参考.
evolve: v0.2+ 系统级演化接口矩阵 9/9 delivered (2026-04-18)¶
core/pkg/evolve/interfaces.go 定义的 10 个核心接口 (9 interface + 1 func type) 全部落地参考实现, 战略路线见 docs/evolve-strategy.md §7.4.
| # | 接口 | 参考实现 |
|---|---|---|
| 1 | Generator |
generator_llm.go |
| 2 | Evaluator |
evaluator_impls.go |
| 3 | Reflector |
reflector_impls.go + self_reflector.go |
| 4 | ApprovalFunc |
evolve.go |
| 5 | ParameterStore |
parameter_store_file.go |
| 6 | ParameterEvolver |
parameter_evolver_default.go |
| 7 | LogReplayer |
default_log_replayer.go |
| 8 | LogSource |
log_source_file.go |
| 9 | FeedbackChannel |
feedback_channel_file.go |
| 10 | ShadowRunner |
shadow_runner_default.go |
闭环 example: core/examples/evolve_closed_loop/main.go -- 9 接口在单进程串联跑通 (baseline 1.0 → version 2 的 1.5, divergence 0.1736, shadow delta +0.105).
领域无关原则¶
所有接口用 any / string / float64 传递业务数据, 不假设任何行业 schema. 具体数据源接入由 consumer 层实现, 引擎只操作抽象接口 (见 memory project_architecture_decisions.md "数据接入边界").
evolve: FlytoLLMClient -- flyto.ModelProvider 桥接 (2026-04-18)¶
core/pkg/evolve/llm_adapter_flyto.go 提供 flyto.ModelProvider -> LLMClient adapter, 让 LLMGenerator 可直接驱动任意 provider (anthropic / openai / minimax / gemini / ollama / lmstudio / openrouter). 事件聚合以 TextEvent 为权威, TextDeltaEvent 作兜底, 避免 anthropic 同时推两者时的双倍计数. ErrorEvent 经 %w ErrLLMFailed 包装, errors.Is(err, ErrLLMFailed) 可用. 非文本事件 (ToolUse / Thinking / Usage / Done / ...) 统一忽略.
限制: flyto.Request 为跨 provider 最大公约数, 不含 Temperature; LLMCallOpts.Temperature 被 adapter 忽略 -- 温度控制仍需在 provider 工厂 Config 设置.
tools: SQL 工具链 3 件套 (2026-04-23)¶
面向 staging / 影子表的三件套, 支撑 AI Agent 写业务 DB 的 staging 一跳 (Agent -> staging -> ML 审批 -> WMS API 写生产). 2026-04-23 commit 981a2ea 分类翻盘, 从原 "消费层不属于引擎层" 挪到引擎层模块 23 -- schema-agnostic / driver-agnostic 通过 StagingDB newtype + *sql.DB DI 由客户端注入 driver, 引擎层仅 database/sql 标准库, 所有客户复用.
| 组件 | 位置 | commit |
|---|---|---|
| SQL 只读校验器 | core/pkg/tools/builtin/sql_validator.go |
79670c7 |
| SQL CAS 乐观锁 | core/pkg/tools/builtin/sql_cas.go |
bf31278 |
| SQL Dry-run 三路 | core/pkg/tools/builtin/sql_dryrun.go |
a935604 |
要点: CAS maxRetries=0 fail-fast 默认 (Agent 看 version 冲突应重 plan, 非 silent retry), version 非 int 运行时 reject, modernc.org/sqlite test-only dep 落地 (core 第一条非图像处理第三方依赖). Dry-run 方案 E (before + after 都 SELECT) + LLM 传 preview_predicate (工具不 parse SQL) + 100 行 truncate 显式提示. 只读校验器纯字符串 quote-aware 解析零 DB 依赖.
staging: 决策包级 staging 表 + 7 状态机 + 混合控制 Engine (2026-04-24)¶
core/pkg/staging/ 新子包, 管 "Agent 决策 → staging → 审批 → 生产" 链路中 staging 一环. 决策包级 Record (一次 Agent 决策 = 一条 Record, 做法 I 整体打包原子), 7 状态机:
pending_tech -> rejected_tech | pending_ml
pending_ml -> rejected_ml | approved
approved -> executed | failed
混合控制 (产品决策 Y): Engine 主动调 validator.Validator 推进前两段 arc (ValidateTech / ValidateBiz, fail-closed 语义), 终端 approved → executed/failed 由平台层外部推 (MarkExecuted(id, proof any) / MarkFailed(id, reason)). core 对生产写路径无权限, 无法自驱最后 arc.
接口契约 (产品决策 X): 技术层和业务层两个 slot 都复用 validator.Validator, 不造 TechValidator/BizValidator 新接口. Verdict.Score + Severity 能表达 "SQL 过了但 plan 差" (Warn) 与 "ML 打分 0.3" (Block). 客户用 reflector.EvaluatorAsValidator 把 Evaluator 适配为 Validator 接入任一层.
依赖可插拔 (产品决策 #3): DependencyGuard 接口, nil fail-fast, AllowAlwaysGuard{} 显式 opt-out (对齐 validator.AlwaysApprove 模式); 内置 TenantDenyGuard 示例 (metadata-driven deny-list 典型形态).
客户接入 pull-only (产品决策 #5): Store.List / ListBySession / ListStuck 查询 API, core 不向客户库推送. 客户按需查 (cron / dashboard / 事件 sink 自选).
幂等 + 审计链: MarkExecuted / MarkFailed first-write-wins 保原 proof / reason, 审计不可变性. ListStuck(state, olderThan) 给平台层 watchdog 发现 approved 卡住不推进 (外部 arc 无内置超时, AWS Step Functions task token pattern 借鉴).
| 文件 | 内容 | 行数 |
|---|---|---|
doc.go / state.go / record.go / guard.go |
7 状态 + Record + Query + DependencyGuard + AllowAlwaysGuard + TenantDenyGuard | ~350 |
store.go / inmemory.go |
Store 接口 9 方法 + InMemoryStore 参考实现 | ~350 |
engine.go |
Engine 混合控制 (NewEngine 4 参 nil fail-fast, 5 方法) | ~175 |
*_test.go |
47 test 全绿 (race detector 通过) | ~700 |
Baseline 212 → 216 (净 +4 合法 tracked debt: Record.TechVerdict / BizVerdict / ExecutionError / ExecutionProof — 外部 audit dashboard 消费, core 无内部 reader, 按 memory feedback_exported_field_delete_needs_review.md). 设计决策走 Agent Teams 3 角色 review; 产品经理 5 轮产品决策 + 7 条质疑 reconcile (4 硬挡采纳, 2 软挡采纳, 2 保留原决定含前提不成立的). TODO.md L436 check off.
reflector: 产品抽象伞形包 + 4 adapter (2026-04-24)¶
core/pkg/reflector/ 新 umbrella 包, 用于表达 "反射器" 产品抽象 — validator.Validator (sync commit 闸) / evolve.Evaluator (sync 打分) / evolve.Reflector (async 回放) 三个同族接口. 本包不引入新的顶层接口, 只提供跨家族类型安全 adapter, 兑现 "规则 / LLM / ML 后端可互换接入任一同族接口" 的可替换性, 子包代码零改动.
| Adapter | 作用 | 关键参数 |
|---|---|---|
ValidatorAsEvaluator |
Validator 包为 Evaluator | CandidateToDiff extractor; 可选 WithRejectFitness (默认 Approved=false → fitness=0 保守) |
EvaluatorAsValidator |
Evaluator 包为 Validator | DiffToCandidate extractor + threshold 必传; 可选 WithName / WithBelowThresholdSeverity / WithPolicyVersion |
ValidatorAsReflector |
Validator 包为 Reflector (旁路观察) | EventToDiff + VerdictSink 必传; OnEvent 永远返回 nil, 错误走 sink |
EvaluatorAsReflector |
Evaluator 包为 Reflector (旁路观察) | EventToCandidate + FitnessSink 必传 |
反向 Reflector → Validator/Evaluator 刻意不做 — OnEvent 无返回载荷, 无法反推同步 Verdict 或 fitness. ErrExtractFailed 统一包装 extractor 错误 (errors.Is 可识别). nil src/extract/sink 构造期 panic fail-fast.
设计决策走 Agent Teams 3 角色 review (Option d umbrella 胜出 Option a "真合并三接口") -- 合并超集接口违 Go 惯例 (io.Reader/AWS SDK step/gRPC Unary vs Stream 先例), 且 sync/async 语义不可同一接口承载 (2026-04-23 那次 review 判定的技术事实). umbrella 包以 godoc + 类型安全 adapter 达成 "反射器体系" 产品抽象, 不推翻已 ship 的 validator / evolve 代码.
safety chain: core 层装配就绪 (2026-04-23 / 24)¶
"Agent → staging → Validator → ValidatedTool → CircuitBreaker → WMS API" 安全链 core 侧 7 commit 一次到位, 加上 2026-04-24 的严格化补丁, core 层全部就绪等 platform 装配.
| 组件 | 位置 | commit |
|---|---|---|
| Validator 接口 + DiffInput / Verdict / Severity | core/pkg/validator/interfaces.go |
30639fb |
| RuleValidator + 3 内置规则 (DiffSize/TableWhitelist/Pattern) | core/pkg/validator/rule_validator.go |
e6239c2 |
| LLMValidator + FlytoLLMClient 桥接 (pkg/flyto 独立副本) | core/pkg/validator/llm_adapter_flyto.go |
ca6309e |
| CompositeValidator (AllMustApprove / Waterfall 2 模式) | core/pkg/validator/composite.go |
8b1d556 |
| ValidatedTool Decorator + VerdictSink + 3 extractor | core/pkg/tools/builtin/validated_tool.go |
6ac07b3 |
| CircuitBreaker 三态 (Closed/Open/HalfOpen) + Clock DI | core/pkg/circuitbreaker/breaker.go |
3342425 |
| AlwaysApprove 显式 opt-out + NewValidatedTool nil fail-fast | core/pkg/validator/always_approve.go + 同上 decorator |
1b0a860 |
要点:
- schema-agnostic: DiffInput.SourceTool 作分发键, Raw []byte + Metadata map[string]any 携带 opaque 载荷, 引擎不假设任何行业 schema.
- fail-closed: Validator 返回 error 视作 Block; CompositeValidator Waterfall 模式任何子 Validator Block 整体 Block.
- 熔断桥接: ValidatedTool 每次 Verdict 触发 VerdictSink(toolName, verdict), CircuitBreaker.VerdictSink() helper 订阅即接入 (reject 累积 → Open → Cooldown → HalfOpen 半开试探).
- 显式 opt-out 必须写出来: NewValidatedTool(v=nil, ...) 构造期 panic, industry 刻意不要审批必须显式传 validator.AlwaysApprove{}, code review 和审计 dashboard 过滤 ValidatorName="always-approve" 可抓到. 消灭 "以为开了审批实际没开" 的静默安全假象.
消费位点: core 侧全部就绪. 消费层 P1 L434 (ML 验证器) / L435 (熔断器) 等 platform 层装配 — 选 backend (LLM provider / 外部 ML HTTP / 自部署 ML) + 选 breaker 作用域 (全熔 / 只熔写工具 / 每工具一熔) + wire 到 Tool Registry.
safety chain: platform/common 装配层 (2026-04-24, C2)¶
platform/common/safetychain/ 新公共包 (非 internal, Go 行业 platform 可直接 import; C# logistics 走 gRPC, 见 C4). 把 core 的 Validator + CircuitBreaker + ValidatedTool decorator 组合成一行 Assemble() 调用, 产出可注册的 tools.Tool.
| 组件 | 形状 | 作用 |
|---|---|---|
Assemble(inner, v, extractor, scope, sink) tools.Tool |
装配函数 | Validator / extractor 为 nil 时由 core 构造期 panic (C1 保证); scope / sink 可 nil |
BreakerScopePolicy |
func(tools.Tool) *CircuitBreaker |
决定每 Tool 要哪个 breaker; 函数类型非 interface, 行业自定义直接写 |
NoOpScope() |
(policy, registry) | 不挂 breaker; registry 空 |
DestructiveOnlyScope(cfg) |
(policy, registry) | destructive tools 共享 1 breaker, 对齐 "写路径整体保护" 语义 |
PerToolScope(cfg) |
(policy, registry) | 每 tool 独立 lazy 分配 |
BreakerRegistry |
.Snapshot() map[string]State + .Names() []string sorted |
给 C3 admin 端点消费; 每工厂返回句柄 |
设计决策:
- 无默认: industry 不调 Assemble 就没有审批也没有熔断; 没有 safety 是显式 code-level 选择, 不是隐式回退.
- 显式 opt-out: Validator 为 nil 由 C1 的 NewValidatedTool panic 守住; industry 刻意 unchecked 必须显式传 validator.AlwaysApprove{}.
- 不再抽装配 interface: Assemble 返回的就是 core 的 *builtin.ValidatedTool. 这是接线层不是新契约. Agent Teams 3 角色 review 里"质疑"角色观察"抽 ValidatorAssembler interface 是 YAGNI", 采纳.
- fan-out 顺序: industry sink 先 (记录到 observation store 不丢) / breaker sink 后 (推进状态). 供 C3 observation store 消费时保证顺序稳定.
16 test 全绿 (TestAssemble_* × 6 + TestFanOut_AllCombinations + scope 系列 × 9). core baseline 212 不变.
消费位点: 装配层就绪. industry (logistics / erp / crm 等) 选 backend (LLM provider / 外部 ML HTTP) + 选 scope + 调 Assemble 即可启用安全链. C3 加观测端点, C4 加 gRPC 暴露给 C# 消费.
safety chain: admin 观测端点 (2026-04-24, C3)¶
安全链运维可见性: platform/common/safetychain/ 加 VerdictStore 接口 + RingStore bounded 内存实现, 把 Verdict 流水暴露给 admin HTTP 端点让运维和 dashboard 能查实时状态.
| 组件 | 形状 | 说明 |
|---|---|---|
VerdictStore 接口 |
Record(tool, v) + Snapshot() []VerdictRecord |
Record 签名对齐 builtin.VerdictSink, industry 传 store.Record 到 Assemble sink 无需 wrapper |
RingStore |
bounded 内存环形, O(1) Record 热路径无分配 | 容量显式参数, 无默认 (Warn 流量各行业差异大); clock 注入可测, nil 用 wall time |
VerdictRecord |
{Timestamp, ToolName, Verdict} 带 json tag |
导出结构, 外部观测栈 (日志收集器 / 审计管道) 可直接 decode |
admin.New(..., WithSafetyChain(store, reg)) |
variadic option 向后兼容 | 任一参数 nil 视为不启用 (配对契约); 端点仅 opt-in 时挂载, 默认 404 |
GET /admin/safetychain/verdicts |
[]VerdictRecord JSON |
空 snapshot 返回 [] 不是 null, 下游无需判 null |
GET /admin/safetychain/breakers |
[{name, state}] JSON 按 name 排序 |
轮询间 JSON diff 稳定 |
安全考虑: 两端点都走 authed() helper, verifier 非 nil 时套 auth.HTTPMiddleware (和 /admin/tenant 同级). Verdict.Reason 可能携带运营敏感信息 (为什么某 SQL 被 block), 不能未鉴权暴露. /admin/health 仍免鉴权供 Caddy / k8s 探针.
测试: 5 个 RingStore 测试 (cap panic / 未 wrap 顺序 / wrap 后保留最后 N / Snapshot 返回拷贝 / 并发 -race 无 data race / VerdictSink 签名兼容) + 6 个 admin 端点测试 (未 opt-in 返回 404 / partial opt-in 拒绝 / happy path JSON shape / 空 snapshot 返回 [] / breakers 排序 + state 字符串). core baseline 212 不变.
消费位点: core + common 装配 + common 观测都就绪. 只差 C4 gRPC 暴露给 C# logistics. 本 commit 后行业 platform (Go) 的完整消费模式是:
mp := anthropic.NewProvider(...)
v := validator.NewCompositeValidator(...) // 或 AlwaysApprove{} 显式 opt-out
scope, reg := safetychain.DestructiveOnlyScope(cfg)
store := safetychain.NewRingStore(512, nil)
admin := admin.New("v1", verifier, admin.WithSafetyChain(store, reg))
safe := safetychain.Assemble(innerTool, v, extractor, scope, store.Record)
registry.Register(safe)
safety chain: C# 消费通路 gRPC (2026-04-24, C4)¶
给 platform/industry/logistics/ (C#) 一条原生 gRPC 通路消费安全链状态, 和 C3 的 admin HTTP 形成同源双面暴露. 沿用 HealthService 已经走的 proto + genpb + grpcapi 模式, 不创造新架构.
| 组件 | 位置 | 说明 |
|---|---|---|
| proto 合约 | platform/common/internal/api/grpc/proto/safetychain.proto |
flyto.platform.common.safetychain.v1 命名, csharp_namespace Flyto.Platform.Common.SafetyChain.V1 对齐 health |
| 生成代码 | gen/safetychain.pb.go + safetychain_grpc.pb.go |
共享 package genpb, 和 health 同包 |
| server 实现 | grpcapi.SafetyChainServer{Store, Registry} |
依赖注入, 与 admin.WithSafetyChain 消费同一对 VerdictStore + BreakerRegistry (单进程单源) |
| cmd/common wire | cmd/common/main.go 启动默认 |
构造 RingStore(1024) + NoOpScope registry, 同时挂给 admin HTTP 和 gRPC, 默认端到端可访问 |
2 RPC:
- ListVerdicts(ListVerdictsRequest) -> ListVerdictsResponse — 当前 Snapshot 窗口老的在前; 空窗口返空列表不是错
- ListBreakerStates(ListBreakerStatesRequest) -> ListBreakerStatesResponse — 按 Name 排序
设计决策:
- proto Verdict 刻意省略 Details 字段 (map[string]any): proto3 无直接等价, 且没有现消费者读 per-rule breakdown. 未来需要时加 repeated DetailEntry { key, json_value }.
- Severity / State 用 string 不用 enum: 第三方 Validator / 未来新增 breaker 状态不应强迫 proto schema 升级, 前向兼容优先.
- nil Store / Registry 返空列表不报错: 观测端点不能把 "没东西看" 和 "服务挂了" 混淆. cmd/common 已 wire 空 store/registry, 但即使漏 wire 也 graceful degrade.
- 默认启用 gRPC safetychain service (和 HealthService 同级别): common 启动即可供 C# stub 对接, 哪怕数据为空. 与 "C3 admin 默认 opt-in 挂" 口径一致: cmd/common 选择了 opt-in.
生成工具链: 需要 protoc-gen-go 和 protoc-gen-go-grpc 插件在 $GOPATH/bin. 本 commit 装了一次:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
regen 命令 (在 platform/common/internal/api/grpc/proto/ 下跑):
PATH=$GOPATH/bin:$PATH protoc \
--go_out=../gen --go_opt=paths=source_relative \
--go-grpc_out=../gen --go-grpc_opt=paths=source_relative \
safetychain.proto
测试: 6 个 server 测试 (nil-store / nil-registry / 字段 round-trip / 排序 / 空 Snapshot yields empty list / bufconn 端到端 gRPC round-trip). bufconn 测试是 C# stub 互操作的代理证据 — Go client + Go server 经 bufconn 走 proto 序列化往返成功, 说明线格式稳定, C# 用同一 proto 生成 stub 可互操作. core baseline 212 不变.
消费位点: 整条 "Agent → staging → Validator → ValidatedTool → CircuitBreaker → WMS API" 安全链在 core / common 层全部就绪. C# logistics 可用 proto + dotnet add package Grpc.Net.Client 直接消费. 剩的工作落到 industry platform 侧: 选 LLM / ML backend, 装配 Tool 到 engine 消费层, 把真实 Verdict 流水填进 common 的 VerdictStore.
v0.1.0 (2026-04-18)¶
首次公开发布。core/ 引擎库作为独立 Go module 可用。
核心能力¶
- 引擎运行时 — 多会话管理、子 Agent (fork)、Dream 记忆巩固、Plan 工作流、Token 预算
- Provider 层 — Anthropic / Gemini / MiniMax / OpenAI / OpenRouter / Ollama / LM Studio,统一接口
- Pricing — 运行时获取模型单价,ModelRegistry 集成,支持多供应商按 token 计费
- Hook 系统 — 12 种 HookType,支持 Shell / Callback / Webhook 三种执行后端,session 级隔离
- Plugin 系统 — DFS 依赖解析、manifest 验证、能力 probe
- Context 压缩 — 三层降级(部分 / 完整 / 反应式),断路器保护
- 记忆系统 — AI 相关性选择、新鲜度警告、Git/HTTP 团队同步
- 权限引擎 — 白名单 → 规则 → AI 三层级联,递归 AST 危险命令检测
- 安全 — 45 条内置 Secret 扫描规则,AuditSink 接口
- Agent Teams — Leader/Worker 多 Agent 协调,TaskList 共享任务板
- Bridge — SSE / WebSocket 传输,断线重连,串行批量上传
- Daemon — 后台守护进程,会话池 + 容量控制,崩溃恢复(指数退避),空闲超时自动关闭
- MCP 协议客户端 — JSON-RPC 2.0,多服务器并发管理,stdio / SSE / HTTP 三种传输,Elicitation 支持
- 自进化(Evolve) — 动态工具构建、Skill 学习、自我反思;接口完备,v1.0 前完善完整能力环路
已知限制¶
以下功能留待 v1.0 前完成(不影响 v0.1 使用):
- AuditSink DB 实现(platform 层)
- CAP-4 自动化测试 × 3(core 工具层)
- Provider 模型表更新(MiniMax / Gemini)