Why: The three libraries cannot be reused or recovered independently, and the tested dependency combination is not the one the product builds. Evidence: Read-only audit at native c6d867c, squad 590fd86, intent 1dbbe81, recorded with file references in docs/records/2026-10-02-agent-stack-audit.md. Impact: Documents only. No library code or gitlinks change in this commit. Later stages stay separate tasks. Refs: Dev feature/agent-stack-blueprint Co-authored-by: Cursor <cursoragent@cursor.com>
18 KiB
18 KiB
记录:Agent-Native / Agent-Squad / Agent-Intent 只读审计
日期:2026-10-02。任务主记录:Dev 仓 feature/agent-stack-blueprint。
设计结论见 2026-10-02-agent-stack-completion.md。本文只记证据,不实施修复。
基线(Dev 顶层 deps/,git rev-parse):
- agent-native
c6d867cde7d87696feaf3073f097ca15b0145273 - agent-squad
590fd86f4c8bfada0ec76efbfb57133c61607bd6 - agent-intent
1dbbe8149c561b36282bf781e61a06d1284be3a1
方法:只读读码。P0 条目已对照源码复核。标注「待确认」的条目没有在本次复核中跑测试。路径相对各库根目录。
1. 跨库与集成
| 编号 | 级 | 现象 | 证据 | 影响 | 方向 |
|---|---|---|---|---|---|
| X1 | P0 | 测过的组合不等于发布的组合 | Backend go.mod replace 指向自己的 deps/:intent 7d9d505、native c6d867c、squad 5cbac00。Squad 自测钉 native a34689c、intent 3674dc4。Dev 顶层是 intent 1dbbe81、squad 590fd86。Go 只认主模块的 replace |
各库测试全绿也不能证明产品编译的组合正确 | Dev 根 go.work;make test 断言 Backend 用到的 SHA 等于 Dev 指针 |
| X2 | P1 | gitlink 递归嵌套 | deps/agent-squad/deps/agent-native/deps/agent-intent 等。agent-intent 在 Dev 检出里有多份副本,SHA 不完全一致 |
读码容易看错副本;磁盘上的嵌套仓不参与 Backend 编译 | 库仓不再嵌套兄弟库;适配器子模块用 go.mod 版本号钉依赖 |
| X3 | P1 | 边界检查不管库与库 | scripts/lint-boundaries.sh 只禁止扩展 SDK 与示例 import agent-system-backend/internal |
实现级耦合没有门禁 | 用 go list -deps 断言核心模块不得依赖兄弟库 |
| X4 | P1 | 内核为了一个没有调用方的包依赖 Intent | deps/agent-native/go.mod require agent-intent;唯一引用是 adapters/intentgate。Backend 零引用该包 |
内核不能单独使用 | 删除该适配器,或迁到独立子模块 |
| X5 | P1 | Squad 公开 API 暴露内核类型 | squad/backend.go:BackendFactory 返回 exec.Backend;AgentNativeConfig 含 *llm.Client、tools.SearchProvider、tools.ExecConfig、mcp.Manager、subagent.Factory。planner_llm.go 的 LLMPlanner.Client 是 *llm.Client。intent.go 的 IntentRunner 返回 *exec.Result |
换执行器必须改 Squad 核心;编译期去不掉 agent-native | 核心只定义 Executor、ModelClient 端口;跨库代码进 adapters/native |
| X6 | P1 | platform 绕过执行端口 | platform/runtime.go 的 RunInProcess 直接 runtime.NewAgentSession |
内核内部类型进入 platform 公开签名 | 经 Executor 端口 |
| X7 | P1 | CLI 直接 import Intent | deps/agent-squad/cli/cli.go import agent-intent/intent |
与「只经 squadintent 接入」的约定不一致 | CLI 迁到独立 cmd/,经适配器装配 |
| X8 | P1 | 同一概念多处定义 | 意图闸门:squad.IntentGate、squadintent.Gate、adapters/intentgate、backend/internal/intentgate。目录三套。HITL:Squad pending、Backend 卡片、命令确认、Intent 的 Question。预算:exec.Budget、Backend RunBudget、设置项。事件三套。工作流:squad.WorkflowPlan 与 core.WorkflowDef。结果状态:command 四态、intent 五分支、native 的 Success 布尔 |
扩展作者要对齐四套键:handler、metadata.blueprint、CommandSpec.id、WorkflowDef.IntentID |
每个概念一处定义,见提案第 4 节 |
| X9 | P1 | 产品没有接上 Squad 的意图能力 | modules/backend/internal/squadadapter 未装配 IntentGate、Catalog、Runner |
Squad 侧已做的目录注入与计划校验在产品路径不生效 | 组合根装配适配器 |
| X10 | P2 | 模块路径是裸名 | go.mod 的 module agent-native / agent-squad / agent-intent |
第三方不能 go get,只能 replace |
改为可获取路径并打 semver tag。路线图已把改模块路径列为单独发版任务,不在本次 |
2. Agent-Native(c6d867c)
P0
| 编号 | 现象 | 证据 | 影响 | 方向 |
|---|---|---|---|---|
| N1 | 工具预算超限留下没有结果的 tool_calls | runtime/loop.go 先 persist 带 tool_calls 的 assistant,再在 toolCalls+len > Max 时 budgetStop。budgetStop(同文件约 542 行)不补工具结果 |
续跑时 OpenAI / Anthropic 会拒绝这段历史 | 超预算则不落盘该 assistant,或先写入合成的 error 结果再停 |
| N2 | 并行工具的事件回调有数据竞争 | executeParallel(loop.go 约 366 行)每个工具一个 goroutine 调 runOne→emit。adapters/native/backend.go 约 332 行的 OnEvent 无锁写同一个 strings.Builder |
-race 会报;日志交错 |
串行投递事件,或每工具私有缓冲再合并 |
| N3 | MCP stdio 取消后读协程不退出 | runtime/mcp/stdio.go 的 readResponseLine:ctx.Done 返回后,后台 goroutine 仍在 ReadBytes。下次调用再起一个读者 |
stdout 帧错乱、卡死 | 取消时关闭进程,或单读循环按 id 分发 |
| N4 | 预设与真实客户端协议不一致 | runtime/llm/client.go 固定 POST …/chat/completions。BuildBody 的 Anthropic / Google 分支不被该 Client 使用。Anthropic body(providers.go 约 303 行)把 tool 结果收成普通 user 文本,不带 tool_use / thinking 签名,也不挂 tools |
选 anthropic / google 预设不能多轮工具 | 按 Protocol 分流真实客户端;未实现的协议不要出现在可用预设里 |
P1
| 编号 | 现象 | 证据 | 方向 |
|---|---|---|---|
| N5 | token 预算只在回合开头检查,超限后本轮工具仍执行 | loop.go 用量在 consume 之后累加,执行工具前只查 MaxToolCalls |
累加后、执行工具前再查 token 预算 |
| N6 | Stop.Replacement 只改内存 |
loop.go 改最后一条消息内容,不 persist |
追加更正记录或提供改写 API |
| N7 | compaction 按条数裁尾,可切断工具配对 | runtime/session/compact.go 的 keepLastN 不按 tool 组对齐。现有测试没有工具消息 |
裁切对齐 tool_calls 与 tool 结果 |
| N8 | compact 与 append 有并发窗口 | compact 读完条目后解锁再摘要,再加锁追加 | 摘要期间禁止追加,或按版本号 CAS |
| N9 | 会话追加无 fsync、无跨进程锁 | runtime/session/tree.go 用 O_APPEND 写,锁只在单进程内 |
可选 Sync;文档写明单写者,或加文件锁 |
| N10 | 流式错误不落盘、默认不重试 | loop.go 遇到错误帧直接返回。WithRetry 只在 CLI 包装 |
Backend 适配默认重试;写明错误帧是否落盘 |
| N11 | SSE 不拼接多行 data: |
runtime/llm/sse.go 逐行处理 |
按空行分事件再拼接 |
| N12 | 取消一批工具时要等全部结束 | executeParallel 用 Wait;顺序路径工具之间不看 ctx.Err() |
批内协作取消;超时后仍写 error 结果以保持配对 |
| N13 | fetch_url 有 DNS 重绑定窗口 | runtime/tools/fetch.go 先 LookupIP 校验,client.Do 再解析 |
Dial 时连已校验的 IP |
| N14 | 宿主自带 CheckRedirect 时跳过重定向校验 |
fetch.go 仅在 CheckRedirect==nil 时包装 |
始终链式包装 |
| N15 | write / edit 不是崩溃原子 | runtime/tools/builtin.go 直接 os.WriteFile |
临时文件再 rename |
| N16 | 按字节截断,破坏 UTF-8 | builtin.go、exec.go、mcp/manager.go |
按 rune 回退 |
| N17 | 工具 schema 校验过松 | runtime/tools/registry.go 不强制 additionalProperties:false,array / object 一律放行 |
至少拒绝未知键 |
| N18 | MCP「失败重连一次」只有注释 | runtime/mcp/manager.go 注释与实现不符,失败只返回错误 |
实现或删注释 |
| N19 | MCP HTTP Close 是空操作 |
runtime/mcp/http.go |
发送 session terminate |
| N20 | 用量把每轮完整历史的 prompt token 累加 | loop.go |
分开「上一请求」与「累计计费」 |
| N21 | 毒化轮转状态只在内存 | adapters/native/backend.go 的 lastFailure |
旁路文件落盘 |
| N22 | 提醒消息是追加的 user 角色 | loop.go 的 withEphemeral。OpenAI 在 tool 结果之后通常可接受。Anthropic Messages 是否拒绝,待确认 |
用提供方的 ephemeral / system 通道;补一条 Anthropic 往返测试 |
P2
| 编号 | 现象 | 证据 | 方向 |
|---|---|---|---|
| N23 | run_python 的 ScriptDir 不经路径解析 |
runtime/tools/exec.go 约 112 行:filepath.Join(t.root, ScriptDir)。该字段只能由宿主配置,模型参数改不了,因此从 P0 降为加固 |
Resolve 后再用;单测覆盖 .. |
| N24 | grep / WalkDir 无文件数与总字节上限 | builtin.go 只有命中条数和单文件上限 |
加 maxFiles / maxBytes |
| N25 | HTTP 客户端默认超时很长、无空闲读超时 | runtime/llm/client.go |
ResponseHeaderTimeout 与读空闲超时 |
| N26 | Registry 无锁 | registry.go 的 Register / Restrict 与 Execute 并发不安全 |
运行前冻结快照 |
| N27 | SSE 工具名用 += 拼接 |
sse.go 若 delta 重复全名会拼重 |
仅空时写入 |
| N28 | 绝对路径 jail 用未 Eval 的根做 Rel | runtime/tools/jail.go。symlink 根加绝对真实路径是否可绕,待确认 |
校验统一对已解析的根 |
| N29 | contract/ 与 exec/ 两套契约 |
contract 只有 conformance 的 fake 在用;native 实现的是 exec.Backend |
保留 exec,删除或并入 contract |
| N30 | native.Options 汇集全部配置 |
MCP、技能、压缩、宿主工具、执行、抓取、搜索在同一结构 | 按端口拆分 |
| N31 | adapters/native 不透传 Stop / Reminders / RepeatGuard / BeforeTool |
adapters/native/backend.go 的 run 不设置这些字段 |
小队路径拿不到直连的门禁。透传或由宿主显式配置 |
缺测(本次未找到对应用例):N1 配对、N2 race、N3 取消双读、N4 Anthropic 线上格式、N7 切断工具组、N13 DNS 重绑定、N6 持久化。
3. Agent-Squad(590fd86)
P0
| 编号 | 现象 | 证据 | 影响 | 方向 |
|---|---|---|---|---|
| Q1 | RequiresApproval 批准后再次挂起 |
squad/workflow.go 约 60 行检查 Approved["step:"+id]。全仓只写 Approved["role:…"](engine.go 约 578 行)。Respond 的确认只恢复挂起,不写 step 键 |
该步骤永不执行 | 批准时写入 step: 键并落盘;补回归 |
| Q2 | 授权、修复预算、澄清、steering 不落盘 | runState 有这些字段(squad/runstate.go)。save() 只写 workspace、workflow、squad、blackboard、usage |
重启后授权丢失、修复预算归零 | 原子写入 runstate.json,Resume 必读 |
| Q3 | 状态文件不是原子写 | squad/workspace.go 的 writeJSON 直接 os.WriteFile。只有黑板用临时文件再 rename(blackboard.go) |
崩溃留下半截 JSON,Resume 失败 | 统一 tmp + Sync + rename |
| Q4 | Resume 会重跑 status=running 的步骤 | advance 只跳过 completed / skipped / failed(workflow.go 约 33 行)。崩溃可留下 running。Resume 无挂起则直接 advance |
写文件、工具、MCP 副作用重复 | 步骤 attempt id;恢复前把 running 标成待重试并校验产物 |
| Q5 | 没有跨进程 run 锁 | engine.go 约 144 行只有进程内 map |
两进程可交错写同一 run 目录 | run 目录文件锁,或单写者 |
P1
| 编号 | 现象 | 证据 | 方向 |
|---|---|---|---|
| Q6 | HITL 超时字段未实现 | hitl.go 有 TimeoutAfter / ExpireAction,无扫描 |
sweeper 按到期动作合成决策 |
| Q7 | Respond 先删 pending 再应用决策 |
engine.go 约 477 行先 saveGate(nil)。二次回应走「无挂起」 |
先记 answered 再删 |
| Q8 | wakeup 自动批准 confirmation / permission / quality | engine.go 约 901 行 |
高风险 kind 禁止自动批准 |
| Q9 | SweepWakeups 在锁外改 WakeupFires |
先自增写盘再 Respond(锁在 Respond 内) |
整段放进 run 锁 |
| Q10 | 并行组无 fail-fast,且丢掉 gate | runParallel 等全部结束;忽略 runOneStep 的 gate |
失败即取消兄弟 |
| Q11 | 步骤间上下文是过期快照 | BuildSubWorkspaces 用当时黑板写 TASK.md,顺序执行不刷新 |
每步前用最新黑板和上游产物索引重写 |
| Q12 | LLM 规划失败无回退、无规模上限、目标原文注入 | planner_llm.go fail-closed;prompt 直接拼 Goal.Text |
可配置回退;目标分段;硬上限 |
| Q13 | intent 步要求注册表里存在名为 intent 的角色 | materialize 对每步 Registry.Get(role) |
IntentID 非空时跳过普通角色检查 |
| Q14 | Server 与 platform HTTP 无鉴权 | server/server.go、platform/http.go |
至少 API token |
| Q15 | run id 未校验 | filepath.Join(DataDir, "runs", runID),id 来自路径 |
拒绝非 run_ 前缀的安全字符集 |
| Q16 | 请求体无大小上限(webhook 除外) | handleCreateRun 无 MaxBytesReader;webhook 才有 256KiB |
全局 body 上限 |
| Q17 | SSE 扇出关闭竞态、慢消费者丢帧 | progress.go default 丢帧;回放 goroutine 往已关闭 channel 写 |
done 信号停回放 |
| Q18 | 事件序号重启后从 1 重计 | store/eventsink.go 的 seq 只在内存 |
启动时读最后一行 |
| Q19 | Claim 并发上限先查后改;未开 WAL | platform/queue.go 先 COUNT 再 UPDATE;db.go 只有 busy_timeout |
单条条件更新;journal_mode=WAL |
| Q20 | 文件 runs 与 SQLite 两个真相源 | platform/db.go 注释;server 可同时挂两者 |
platform 降为 Store 实现,状态以图执行为准 |
| Q21 | StepSpec.Needs 未参与调度 |
只定义在 models.go。调度看相邻 Parallel(workflow.go) |
实现 DAG 或删除字段 |
P2
| 编号 | 现象 | 证据 | 方向 |
|---|---|---|---|
| Q22 | leader 先 Turns++ 再执行 |
leader.go 约 242 行 |
成功后再递增 |
| Q23 | MaxLeaderTurns 用尽直接失败,无降级收口 |
leader.go |
可配置收口 |
| Q24 | 步骤重试共享同一个 deadline | workflow.go 约 209 行。短超时下重试几乎无预算 |
文档写明,或每次重试单独预算 |
| Q25 | 角色与蓝图写盘忽略错误 | store.go 的 _ = os.WriteFile |
原子写并返回错误 |
| Q26 | 人工 retry 直接把修复预算加一 | engine.go 约 608 行。是否有意,待确认 |
设上限 |
| Q27 | 拓扑写死 | standard / leader / router 在引擎内分支,无 Topology 接口 | 插件接口 |
缺测:Q1 全路径、Q4 从 running 恢复、Q5 多进程、Q6 超时、Q8 自动批准、Q15 路径穿越。
4. Agent-Intent(1dbbe81)
P0
| 编号 | 现象 | 证据 | 影响 | 方向 |
|---|---|---|---|---|
| I1 | Classifier.Refine 返回值不校验 |
intent/recognizer.go 约 97–123 行:不检查 ID 是否属于原候选,不把置信度限制在 0 到 1,不禁止改 Metadata / Handler。失败才保持 Ambiguous |
LLM 分类器可以把任意 ID 抬成 Top | ID 集合必须相等;置信度钳制;禁止新增特权字段 |
| I2 | 槽位校验器是进程级全局表 | intent/slots.go 的 slotValidators sync.Map,无注销、无命名空间 |
多 Registry 同名互相覆盖 | 挂到 Registry / Options |
P1
| 编号 | 现象 | 证据 | 方向 |
|---|---|---|---|
| I3 | 中文子串匹配、无词界 | intent/registry.go 约 120 行 strings.Contains。语料只有正向命中 |
词界模式或最短词长告警;补假阳性负例 |
| I4 | 只做 ToLower,无 NFKC / 全半角 / 繁简 |
recognizer.go 折叠;slot normalize 仅 trim / lower / upper |
可选 NFKC;文档写明无繁简 |
| I5 | 同分恒为 Ambiguous,priority 不参与裁决 | 分差小于 margin 即 Ambiguous(recognizer.go)。priority 只用于排序 |
允许 margin 为 0 时按 priority 决胜。当前 <=0 会被改回 0.1(registry.go) |
| I6 | 分类器解歧后的 Top 可能没有槽位 | candidatesOf(..., false) 先不抽槽 |
裁决成功后再对 Top 抽槽 |
| I7 | Snapshot 的 Metadata 浅拷贝 | SpecByID / Catalog 拷贝 map 但共享底层 |
深拷贝 |
| I8 | Catalog 整表输出 Metadata 与 Handler | intent/catalog.go |
白名单键;默认不进 Markdown |
| I9 | 闸门钉死旧 *Snapshot |
squadintent/gate.go、adapters/intentgate 不持有 Registry |
Reload 后须重建,或持有 Registry |
| I10 | 识别是全量线性扫描 | evalRules,上限 2048 |
规模上来再做倒排;先写明上限 |
| I11 | 多槽位各自第一次匹配,无重叠消解 | intent/slots.go |
文档写明;冲突时标歧义 |
| I12 | 否定只靠 excludes_any 子串 |
语料 negated_write_readonly 依赖硬编码「配置」 |
不在库内做语义否定;交给命令层的 Cancel |
P2
| 编号 | 现象 | 证据 | 方向 |
|---|---|---|---|
| I13 | pattern 无长度上限 | 条数上限 MaxPatternsPerSpec=32,单条无长度限制 |
构建期限制长度 |
| I14 | 评测语料约 10 条,无 precision / recall | testdata/corpus.json |
多轮、逐轮期望命令,输出指标 |
已核实相对健全:Snapshot 用原子指针交换且失败保留旧快照;正则在构建期编译(RE2);同分排序稳定(score、priority、注册序)。
5. 与产品侧的关系(不在三库内,供路线引用)
这些能力今天在 Backend,提案主张下沉到库,本次不改:
- 终轮验证与计划提醒:
modules/backend/internal/runtime/verify.go - 评审流水线:
internal/runtime/review.go - 工作流执行与 critic:
internal/runtime/workflow.go、internal/workflow/ - 子代理循环与 profile:
internal/runtime/sub_loop.go、internal/prompts/library/agent-sub-*.md - 后台子代理通知:
internal/runmgr/runmgr.go - 直连回合:
internal/runtime/llm_direct.go(约 692 行)
core.WorkflowDef 仍是插件作者面对的契约。执行器与 squad.WorkflowPlan 是两套编排。