一份完整的定义
维度一:工具
白名单扩容过一轮——以前只有bash / read / write / edit / glob / grep 六个,
业务工具一个都够不到。现在可授予的是一张**「允许表的允许表」**:
刻意缺席的工具,缺席理由和「白名单扩容」同等重要:
声明大小写不敏感,落库一律是规范注册名。 写
webfetch 能解析成 WebFetch。
这条是被真实 bug 逼出来的:会话工具注册名大小写不统一(WebFetch 是驼峰,
use_skill 是蛇形),而旧解析器对每个工具名无条件小写化,
于是驼峰注册名永远匹配不上——工具静默解析为空,且没有任何报错。
运行时还有第二个条件:声明的工具必须确实存在于当前会话的工具表。
两个条件都满足才授予,否则记一条诊断告诉你缺了什么,而不是悄悄不给。
维度二:技能
skills 是按名白名单,解析复用主代理同一套分层发现
(工作区 > 生态·工作区 > 系统 > 生态·用户 > 插件)。
- 没声明
skills→ 不注入任何技能目录,use_skill也不可用; - 声明了但某个技能在当前作用域不存在 → 记诊断,那一条从目录里消失, 不静默:用户需要知道自己声明的东西没生效;
- 目录里只有 name / description / location 三行,正文经
use_skill按需加载。
维度三:MCP 服务器
子代理拿到的不是共享网关,而是作用域化网关:search/describe:工具索引按声明过滤,只列已声明服务器的工具;call一个未声明的服务器 → 立即拒绝,并告诉它「你被授权了这些」;call一个已声明的服务器 → 命中该服务器的审批白名单就直接执行; 没命中则把审批卡转发到父线程,你在正在看的那个会话里看到并裁决。
write / edit / bash 走的是同一条父会话审批链。
这些内置工具当初是从父会话的工具表里按定义取的,却没有接审批钩子——
“把活交给带写入能力的子代理”于是等于把权限放大一档。现在判定收口在同一个函数里:
档位(变更前确认 / 工作区内自动 / 自动编辑 / 完全访问)、工作区边界与可写根清单、
配置类工具的确认、无人值守的即时裁决,对子代理与主代理一视同仁,卡显示在父会话。
Task 本身不作为审批项——它不写盘、无副作用。
代价已明确接受:一个等待裁决的后台子代理会挂起,TaskStop 可以中止它(中止时它
名下挂起的卡按拒绝结算,子代理不会卡在那次调用上)。已知残留:父会话没有活跃
请求时(主代理这一轮已跑完、子代理还在后台),直播通道送不出卡,卡片要等下一次
快照/挂载拉取才现身。
声明了但服务器当前没启用/没配置时,网关照样挂载,只记一条诊断。 因为「能力不存在」和「配置还没到位」的下一步动作完全不同—— 后者该去设置页配服务器,而不是让模型以为自己没这项本事。
维度四:知识源
一份知识源就是一份文档:名称 + 工作区相对 glob,正文留在磁盘。 永不预加载。 系统提示词只拿到每源一行的目录,检索走kb_search:
纯内存逐行扫描打分,返回排好序的 path:line 命中,子代理再自己 read 打开感兴趣的文件。
命中是一行,不是答案——工具描述里就这么写着。
8 MiB
单次检索最多读取的字节数,覆盖常规产品手册规模。
50 条
单次返回命中上限,比
grep 的 200 更紧。400 字符
单条命中行长度上限,与
grep 一致。./docs/**/*.md 不该是「给 agent 一个知识库」的前置知识。
选到工作区之外的目录会被直接拒绝(检索侧按 path.join(cwd, rel) 解析,
绝对路径会拼成无意义的串,静默搜不到任何东西)。
声明知识源就必须同时授予 read,否则它拿到 path:line 却打不开文件。
解析层只警告(不赔掉整份定义),保存路径直接拒绝——填完才被拒太晚了,
所以编辑时就 inline 提示。
外部系统(飞书表格、Notion、数据库)不走知识源,由
mcp.servers 授予,
agent 直接调那些服务器的工具。把 MCP 也做进知识源等于同一件事说两遍,
还多一套要维护的类型分支。维度五:记忆
三档互斥,缺省即「无」:
私有是推荐档。 子代理写不进用户主记忆,结构上不可能污染;
它生成的内容可能是幻觉,本就不该进主代理的下一次提示词。
共享档保留是因为「业务 agent 与主代理共享一条经验」确有场景——
UI 上会附一行知情说明,不是阻止,是知情。
三件套工具
memory_write / memory_read / memory_search,仅在非「无」时挂载。
关键一条:scope 参数不进工具 schema,目录在闭包里钉死,
子代理在参数里伪造 scope: "global" 无门可过——隔离是结构性的,不是运行时校验参数值。
投递方式与主记忆同构的两层:根级 *.md 视为常驻记忆,经提示词段注入
(逐文件 4K、整段 12K 预算,超预算的文件整体略去并留一行说明);
daily/*.md 只参与关键词检索。
并发上,同一子代理的多次委派写同一目录——按目录串行 append,
不用文件锁(sidecar 是单进程,进程内串行就够)。
主记忆的全局开关不控制子代理记忆:那是主代理提示词的缓存纪律,
用一个默认关闭的开关去否决你显式声明的 memory: private 是错的耦合。
knowledge与memory的分工:knowledge是外部权威资料(产品手册、政策库), 只读;memory是agent 自己攒下的经验,可读可写。 一个是「世界告诉它的」,一个是「它自己记住的」。
提示词怎么拼
顺序固定:框架 → 能力目录 → 定义正文。 能力目录的段序也固定:技能 → 知识源 → MCP → 记忆。 定义正文放最后,让它对「怎么干活」有最后发言权。不变量:每个维度未声明时,该段整体省略——
所以既有定义的输出提示词字节级不变,
provider 侧的 prompt cache 才命中。这条不变式进了测试。
设置页怎么改
编辑器是二级页面而不是弹窗。加了能力维度后,内容远超一个sm:max-w-2xl 弹窗能从容承载的高度——表单被挤出视口,保存按钮要滚到底才够得着。
改成整页后:顶部返回、内容区独立滚动、操作条常驻底部,两列布局让高度回到一屏内。
表单分五组:基本信息 / 系统提示词 / 可用工具 / 能力授予 / 知识源。
几条实现上的取舍值得单独说:
- 三个选择器都用真实候选,不给自由文本——工具取自 sidecar 的可授予清单, 技能取自技能清单,MCP 服务器取自 MCP 配置。 能力声明是引用既有实体,不是新定义;给自由文本框只会让人写出不存在的名字。 后端是唯一事实源,旧 sidecar 缺这个字段时回落旧 6 项,不白屏。
- 引用了不存在的名字,以警示色 chip 显式出现,hover 说明、点击可移除, 不静默丢弃。
- YAML 原文页签走同一套解析校验,手写的四个能力键原样生效, 表单页签的内容不会覆盖这里的编辑。
- 内置定义的只读弹窗也列出它的能力(技能 / MCP / 知识源 / 记忆档位)—— 否则「为什么这个 agent 够不到我的 Notion」无从排查。 「复制为系统级」完整携带四个维度,不做静默丢字段。
本期明确不做
回到
四个调度工具、四层发现与活动回放。
