dsh-ace
Evolvable Markdown playbook for DSH agents — retrospectives distill lessons, consulted before tasks.
- Stars
- 1
- Language
- JavaScript
- Created
- Oct 21, 2026
- Updated
- Oct 26, 2026
Introduction
title: dsh-ace 使用手册 owner: dsh-ace 维护者 status: current last_reviewed: 2026-10-21
dsh-ace —— ACE 操作手册插件
结论:装上即得一套「可进化的操作手册」——agent 完成任务后自动复盘沉淀经验,下次任务前翻手册照做;手册是纯 Markdown,人随时可以翻看和直接编辑。
dsh-ace 把 ACE(Agentic Context Engineering, arXiv 2510.04618)的方法做成 DSH 原生插件:不训练模型、不改权重,纯靠提示词与一个 Markdown 文件(playbook)。核心循环:
- 读:任务开始前,agent 用
ace_playbook查手册里有没有命中经验; - 盘:完成一个有实质工作量的任务后,agent 调
ace_reflect,由 LLM 复盘轨迹、提炼候选经验(条件/做法/依据); - 并:
ace_curate把候选经验增量并入手册——增/改/删单条,LLM 去重,绝不整本重写(防止旧经验细节被磨平)。
安装
结论:一条命令安装,无需 Python、无需构建、无第三方 npm 依赖。
在终端(DSH 可用环境中)执行:
dsh plugin add github:ADkun/dsh-ace
也可以写完整地址:dsh plugin add https://github.com/ADkun/dsh-ace.git
安装后重启会话即可生效(插件注册三个工具和一段系统提示)。卸载:dsh plugin remove dsh-ace。
首次使用
结论:什么都不用配置——第一次调用会自动创建本机共享手册** <DSH home>/ace/playbook.md(Windows 上通常是 C:\Users\<你>\.dsh\ace\playbook.md),所有工作区共用这一本。**
手册位置由 playbookPath 决定,三种形态:
| 填什么 | 落在哪 | 用途 |
|---|---|---|
| 留空(默认) | <DSH home>/ace/playbook.md | 本机共享,跨工作区共用一本 |
相对路径,如 .ace/playbook.md | 相对会话工作目录 join | 该工作区私有,可随仓库进 git |
绝对路径,可 ~ 开头 | 原样使用 | 点名某一份手册 |
同一时刻只有一本手册被读写(不做「共享 + 项目私有」两层合并读:两层编号会撞、容量会互相吃、刚写进去的条目下次未必读得到)。
- 正常给 agent 派活;完成一个像样的任务后,agent 会(被系统提示提醒)调用
ace_reflect复盘,再把有价值的候选经验用ace_curate并入手册; - 想人工沉淀经验,直接告诉 agent「把这条经验加进手册」,或自己编辑上面那本手册文件(条目格式见下);
- 下次遇到同类任务,agent 开始前会先查手册。
手册条目格式(人可直接编辑,机器序列化同款):
### [ace-001] 编辑前先读目标文件
- 条件:要对既有文件做任何修改之前
- 做法:先用 read 完整读一遍,确认结构后再动手
- 依据:盲改导致过两次返工
- 元数据:created=2026-02-07T09:12:33.412+08:00 updated=2026-02-07T09:12:33.412+08:00 hits=0 lastHit=
条目编号单调递增:删掉 ace-003 之后,下一个新增是 ace-004,不会重用那个号。手册文件顶部有一行 HTML 注释记着「历史最高编号」(<!-- ace:id-highwater: 3 -->),机器写、人可直接看也可删。唯一的例外:你同时删掉最高编号的条目和这行标记时,历史最大值就没法从文件本身恢复了,那个编号可能被复用一次(之后标记会被重写成当前最大编号,也就是刚被复用的那个号,因此只会复用这一次)。只删标记不删条目的话,下一个新增仍会大于手册里现存的最高编号;只删条目不删标记的话,编号照旧不复用。
配置
结论:全部字段有默认值,零配置可用;想覆盖复盘用哪个模型、改手册位置时才需要动。
改配置有两条路:
- 设置页(推荐):DSH 网页端 → 侧边栏底部 Settings → 导航里的「ACE 手册」。页面是固定暗色卡片、JetBrains Maple Mono 等宽字体,改动即保存(写入宿主配置,重启会话生效);
- 页面是三页标签导航(横排标签栏在标题下方、内容之上):「统计」/「设置」/「记录」,默认停在「统计」;切页不记忆——刷新回到「统计」页。标签栏支持方向键 ←/→ 循环切换、Home/End 跳首尾;
- 三个页面始终都在 DOM 里,未选中的用
[hidden]隐藏而不是卸载,所以切页不会丢状态:设置页填了一半的内容、记录页的搜索词与页码都留在原地; - 其中 复盘 provider / model 两个覆盖字段是可搜索的分组下拉(不再是自由文本):点开即可搜索(对 name / id / description 做大小写不敏感子串匹配),↑/↓ 移动高亮、Enter 选中、Esc 或点面板外部关闭;model 下拉按 provider 分组,已选 provider 时只列该组;
- 两行各自都能清空(面板底部「跟随会话模型(清空)」),清空即回到跟随会话模型;
- provider 与 model 必须两项都填才生效:只填一项时页面会在 model 行下方给出橙色警告,
resolveRoute也会静默回落到会话模型; - 下拉的数据来自宿主的
GET /api/dsh-ace/catalog;宿主还没重启到带这个接口的版本时,下拉里只显示一行「模型目录读取失败」,其余字段照常可用; - 「统计」页是一块只读的「手册统计」:条目数 / 容量占用(如
35 / 100)、正文字符数、文件字节数、手册绝对路径与最后写入时间,最近 14 天的更新分布(纯 CSS 条形图),自动归档(auto=true)占比,以及最近更新的 5 条。数据来自宿主的GET /api/dsh-ace/stats(只读、不写盘),打开页面拉一次、点区块里的「刷新」或保存任一设置后再拉一次;宿主还没重启到带这个接口的版本时,这一块只显示一行提示,其余字段照常可用; - 「记录」页是手册全量条目清单:搜索框对 id / 标题 / 条件 / 做法 / 依据 五个字段做大小写不敏感子串匹配;点列头排序(编号列按数字比较、更新日期列按日期比较,点同列反转方向,列头显示 ▲/▼);每页 20 条,带页码与「第 X / Y 页」;点整行展开该条的条件/做法/依据全文(可同时展开多条);顶部显示「共 N 条 / 上限 M 条」。数据来自宿主的
GET /api/dsh-ace/entries(只读、不写盘),第一次切到该页才发请求,切走再回来不重复请求,点区块里的「刷新」才重拉;宿主还没重启到带这个接口的版本时,该页只显示一行提示,其余两页照常可用;
- 补丁层:直接在 profile 的
cordis.patch.yml里 overridedsh-ace条目的 config 字段。
| 字段 | 类型 / 默认值 | 说明 |
|---|---|---|
playbookPath | string / 空 | 手册文件路径。留空 = 本机共享手册 <DSH home>/ace/playbook.md(跨工作区共用一本);绝对路径(支持 ~ / ~/ / ~\ 前缀)原样使用;相对路径相对会话工作目录解析,即该工作区私有 |
autoReflect | boolean / true | 是否在系统提示里提醒 agent 任务完成后主动复盘 |
reflectProvider | string / 空 | 复盘与策展 LLM 的 provider 覆盖(设置页为可搜索下拉,可清空);留空跟随会话模型 |
reflectModel | string / 空 | 复盘与策展 LLM 的 model 覆盖(设置页为按 provider 分组的可搜索下拉,可清空);留空跟随会话模型。必须与 reflectProvider 同时非空才生效 |
maxEntries | number / 100 | 手册最大条目数(可填 1–1000);超出后先请 LLM 把相近的两条合并成一条,合并后仍超上限就按命中次数从少到多淘汰(命中多的留着;同命中次数时创建更早的先走),被淘汰与被合并的条目搬进同目录的 archive.md(永不被物理删除);候选不会因满额被整批拒绝 |
evictProtectDays | number / 14 | 新条目保护天数(可填 0–365)。created 距今不足 N 天的条目先不参与淘汰:只要不受保护的条目够腾位就不动它们;不够时仍按既有顺序(命中少的 → 创建早的 → 编号小的)从受保护条目里补齐,所以永远不会因为「全都在保护期」而死锁或整批拒绝候选。填 0 = 不保护(与旧行为一致);默认值 14 同样只在该键缺失 / 为 null / 为空串或纯空白 / 不是有限数字时生效,显式写下的 0 不会被兜成 14 |
injectTokenBudget | number / 900 | 每回合自动注入块的 token 估算上限(可填 100–8000;估算 = ASCII 字符 ×0.4 + 非 ASCII ×1.0 的粗略折算)。调小 = 注入更少内容、更省 token 与上下文——超预算的条目不注入(不截断半条);调大 = 一次多注几条。第一条无条件取,之后每加一条先试算整块 token,超了才停 |
injectMaxEntries | number / 5 | 每回合自动注入最多几条经验(可填 1–20)。调小 = 更少注入更省 token;调大 = 一次看到更多经验。它只管注入,不影响手册容量(那是 maxEntries)。注入块首行写的「最多 N 条」跟着这个值走(第 37 轮修正:此前固定写 5,配置成别的值就是文案在说谎) |
autoRecap | boolean / true | 自动归档经验:主代理回合结束后,插件在 agent 空闲下来时后台跑复盘并写入手册,不依赖模型主动调用。复盘输入包含本回合真实轨迹(用户消息、工具调用与工具结果),不再只是一个计数 |
autoRecapThreshold | number / 1 | 触发自动归档所需的最小 mutating 工具调用次数(write/edit/pwsh/bash/run_code/present)。填 0 表示每次都触发(本回合一次工具都没动也照样复盘);默认值 1 只在该键缺失 / 为 null / 为空串或纯空白 / 不是有限数字时生效,显式写下的 0 不会被兜成 1 |
autoRecapCooldownTurns | number / 2 | 两次自动归档之间的最小回合间隔,避免每轮都跑。填 0 表示不冷却(每个回合结束都允许复盘);默认值 2 同样只在该键缺失 / 为 null / 为空串或纯空白 / 不是有限数字时生效,显式写下的 0 不会被兜成 2。设置页两个输入框的可填范围都是 0–1000 |
autoReviewInbox | boolean / false | 预留,当前不生效:自动复审「待合并」条目的开关。实际复审入口是设置页的「待合并复审」区块与对话里的 /ace review |
自动归档与「待合并」复审
结论:经验采集在后台自动完成,人只需在设置页看一眼「待合并复审」区块、偶尔输入一次 /ace review。
主代理一个回合真的干完、且本回合有足够 mutating 工具调用时,插件会在后台跑 Reflector + Curator,把新经验以增量方式写入手册(条目带 auto 标记)。子代理的回合永不触发——只有主代理采集。
「足够」与「隔几轮」都可以填 0:autoRecapThreshold 填 0 就是每个回合结束都复盘(哪怕这一回合一次工具都没动),autoRecapCooldownTurns 填 0 就是完全不冷却(上一回合刚复盘过,这一回合照样复盘)。只有没填(键缺失、null、空串、纯空白或不是有限数字)才会用默认的 1 / 2——显式写下的 0 不会被当成「没填」。
复盘输入包含本回合真实轨迹:插件会把本回合模型可见的对话历史(你的要求、助手的动作、每一次工具调用与工具结果,含失败结果)渲染成一段紧凑文本喂给复盘模型,而不是只告诉它「本回合改了几次文件」。理由很直接:只喂计数时,复盘模型拿不到任务目标、工具序列和错误细节,实测一条经验也提炼不出来;喂真实轨迹才有可泛化的材料。轨迹有体量上限(默认最近 60 条消息、12000 字符;超出时首尾都保留——开头留最多 3000 字符保证你的任务目标不被切掉,中间标一行「…(中间 N 字符已省略)」,结尾吃剩余预算),系统提示词与思维链不参与。工具结果会带上工具名(结果(read): …),这样复盘模型不会把并行调用的结果张冠李戴。轨迹取不到(宿主接口不可用或会话为空)时直接跳过这次自动复盘、不调模型,宁可不写也不白花一次调用;跳过不算复盘过一次——这次占用的冷却窗口与计数会还回去,下一轮该采集照常采集(若在这段等待里你已经跑完下一轮、攒下了新的待办,那份待办与它的计数一律不受影响)。真实失败(模型调用失败、写手册失败)时只把计数还回状态、冷却仍算已消耗(那一轮确实尝试过),同样只还「仍属于这次的那一份」:这段等待里若已有更新的待办(哪怕它已被另一次空闲触发的复盘取走),旧回合的额度就不再还回,以免重复计入。
触发时机在「回合真正结束、agent 空闲下来」之后,不是在回合结束的那一刻(也不再要求「此刻没有别的子代理在跑」——那条判定曾让几乎每一轮都被拦掉):宿主只允许在 agent 空闲时跑这类后台维护任务(回合进行中强行跑会直接失败),所以回合结束时插件先把这次复盘记成待办,等 agent 报空闲才真正执行。如果空闲那一刻 agent 恰好又被新任务拉走,这次复盘会顺延到下一个空闲时刻再试(最多试 3 次);连续冲突才放弃并提醒你一次。顺延、放弃、失败回滚都只作用于「自己取出的那一份」待办:等待期间如果你又结束了一轮并挂上更新的待办,迟到的旧待办既不会把它顶掉、也不会把自己那份的 mutating 计数重复还回去——否则新待办会被静默吞掉、额度凭空蒸发,而新那一轮的冷却已经白吃。反过来也一样——如果顺延的那份待办还在队列里等着,紧接着你又干完一轮并再次命中触发条件,两笔会合并成一份(计数相加,例如上一笔 3 次 + 这一笔 2 次 = 5 次一起复盘),不会只剩后一轮那几次;合并后若因「没材料」或失败要回滚,冷却还回的是最早那一笔之前的值,前几回合攒下的额度与采集机会仍然照算。还有更靠后的一种相撞:那份待办已经被空闲边沿取走、正跑在 await 窗口里,而你在那几秒里又结束了一轮并挂上更新的待办。 此时旧那份已经不在队列里,它迟到的回滚(跳过 / 撞忙 / 失败)会判出「不归当前这一代」。插件不会因此把它丢掉:旧那份的 mutating 计数会被过继给当前状态——队列里还压着更新的待办就加进它的计数,队列已被另一个空闲边沿取空就还回状态里的计数;跳过这一路还会把冷却基准一并过继、取两者中更早的那个(两笔推进合并且都没执行,放弃时就该退回到最早那次推进之前)。过继只发生一次,且已经被合并吸收过的旧待办绝不再过继——否则同一笔额度会被加两遍。至于经验材料本身通常不会丢:复盘取的是会话尾部最近若干条消息,旧那一轮的动作多半仍在新的那份待办的视野里;真正靠这条规则救回来的是额度记账,以及撞忙时「这一轮确实没跑成」这个事实(日志里会明说,不再静默)。
Curator 判为「新条目但与某条旧条目相似度偏高」(BM25 自评分相对阈值 50%)时,这条对子会写进手册同目录的 merge-inbox(封顶 20 条,满员后自动归档暂停新增)。人的两条出口:
- 设置页「待合并复审」区块:逐对显示两条条目的 id、标题与相似度;区块不可用时(宿主未重启)只显示一行提示,其余字段照常可用;
/ace review(/ace的 review 参数):把待并对子注入对话(超过 8 条只注入前 8 条),模型据此用ace_playbook按 id 读出两条全文,能合并就走ace_curate的 update + remove,不能合并就原样跳过。
另有 /ace 命令作为手动兜底:立即跑一次 Reflector + Curator,写入手册(撞上并发冲突时,被拦下的条目会明确回报给你),不等自动触发。如果这会儿 agent 正忙,它会回你一句「agent 正在忙,稍后重试」——稍后再发一次即可。
手册文件存在哪(默认本机共享)
结论:手册默认一本、放在 <DSH home>/ace/playbook.md,与工作区无关;填相对路径才变成该工作区私有。
<DSH home> 的解析链(与设置文件同范式,见下节):profileContext.home → DSH_HOME → ~/.dsh。目录不存在会在第一次写手册时自动创建(共享层的 ace 子目录同理)。想给某个仓库一份私有手册,把它设成相对路径 .ace/playbook.md 即可。
手册文件本身是首次读取时即创建的:第一次调用 ace_playbook(或 ace_reflect)读到「文件不存在」就会建出一份带文档头的空手册,父目录也一并建好。这一步在写锁内完成(与其它写入同一把锁),因此不会和并发写入打架。
多实例同时写同一本手册
结论:同一本手册上的「读-改-写」是串行的;别人在你复盘期间改了手册,你会收到一条明确报错,而不是覆盖掉对方的写。
- 写入统一经
withExclusiveLock(同进程按路径串行 + 跨进程<file>.lock文件锁)。锁区间内只有纯计算——Curator 的 LLM 去重在锁外跑,不占用别人的锁。 - 跨进程锁不会抢活着的持锁者:持锁进程还在就等着,最多等 30 秒;超时报错失败,而不是抢过来把对方的写盖掉。只有持锁进程已经崩溃(被强杀)留下的锁才会被立刻回收。
- 如果复盘期间别人改了同一条目,冲突按条判定:只有真正冲突的那几条被拦下,同批其余安全改动照常落盘;被拦下的条目会明确回报给你(
ace_curate的conflicts字段、自动复盘与/ace默认复盘分支的一条提示;/ace review只是把「待合并」条目注入对话,不写手册、不做冲突判定),说明「手册在复盘期间被别的进程改过,这几条没并入,重试即可」——不静默丢弃、不覆盖对方的写。
已知限制
结论:下面几条是当前设计的边界,不是待修的缺陷——写在这里免得被当成意外。
- 并发新增只拦「重复」,不拦「不同」。 两个实例同时往同一本手册加条目时,冲突检测也参与 add:只拦与「自快照以来新出现或被改名的条目」构成重复的那些(内容完全相同、标题相同、或 BM25 相似度超阈值),其余照常落盘。与快照里本就存在、且未被改名的条目重复不算冲突——那是 Curator 去重(LLM 判 new/update/drop)的职责,不在冲突层重复拦。近似重复那一层只比正文(条件/做法/依据,不含标题)、相似度用对称比率并封顶 100%,所以「同领域的不同经验」不会被误判成重复。
- 编号可能被复用一次。 条目编号单调递增,但人同时删掉最高编号的条目和文档头的高水位标记时,历史最大值无法从文件本身恢复,那个编号可能被复用一次(之后标记会被重写成当前最大编号,也就是刚被复用的那个号,因此只会复用这一次)。这是已知边界,不是并发写导致的。
- 旧工作区里的手册不会自动搬迁。 升级前放在
<workspace>/.ace/playbook.md的手册原样保留、不会被搬到共享层,也不会被合并——共享层是另一本独立的手册。想继续用旧的,把playbookPath设成那个相对路径即可。 - 多实例共享一本手册时,后写者仍可能覆盖先写者。 锁只保证「读-改-写」区间不重叠(不并发写坏文件),不保证跨实例的语义不冲突;后者靠上面的逐条冲突检测挑出来、回报给你重试。
设置文件存在哪(跟随 profile)
结论:设置文件落在当前 profile 目录下——<profileDir>/dsh-ace.json,每个 profile 各自一份;首次升级会自动从旧的 $DSH_HOME/dsh-ace.json 复制过来。
设置页保存的值(不是插件行 Config)存在这个 JSON 文件里。解析链按优先级从高到低:
ctx.get('profileContext').dir—— 当前 profile 的目录($DSH_HOME/profiles/<name>,官方惯例;API 出处dsh-app-boot\lib\types\profile-context.d.ts:10-25);DSH_PROFILE_DIR环境变量;DSH_HOME环境变量;~/.dsh。
前一级缺失或为空串才落到下一级。升级说明:旧版本把设置写在 $DSH_HOME/dsh-ace.json(所有 profile 共用一份)。新版本第一次启动时,若新位置还没有文件而旧文件存在,会把旧内容复制到新位置并记一条日志;旧文件原样保留,不会删除。新位置已有文件时绝不覆盖——你在新位置写过的设置才是权威。因此升级后各 profile 会先继承旧设置一次,之后各写各的。
工具一览
结论:三个模型可见工具,全部围绕手册一个文件,互不越界。
| 工具 | 作用 | 改手册? |
|---|---|---|
ace_playbook | 读手册(整本 / 按条目 id / 按短关键词过滤——query 传 1-3 个短关键词,命中任一片段即返回、按相关度(BM25)排序;matched=0 时改用整本读取) | 否 |
ace_reflect | 复盘任务轨迹,产出结构化候选经验 | 否(只产出候选) |
ace_curate | 候选经验增量并入(add/update/remove + LLM 去重 + 容量护栏) | 是(仅增量) |
深入阅读
- 架构与不变量:DESIGN.md
- 测试与验证:TESTING.md
- 面向 agent 的约定:AGENTS.md