Skip to content

Latest commit

 

History

History
162 lines (121 loc) · 11.1 KB

plugin-spec.md

File metadata and controls

162 lines (121 loc) · 11.1 KB

插件架构规格

这份规格管「边界与权威」:哪一层负责什么、两个面的设置谁说了算、注入怎么进提示词。 各部分的细节在各自的规格里,这里只链接,不复制(改这一条要有理由):

细节 在哪
记录格式、精细度档位、check/lint 判定 worklog-spec.md
配置卡片的三个页签、每个字段的归属、写回通路 settings-spec.md
全局记忆的目录、来源分档、索引、dream、inbox memory-spec.md
目标.md 的两层表 goals-spec.md

一、四层,以及每层的依赖方向

① 技能层    skills/                    无 DSH 依赖 —— 能在没有 DSH 的环境里单独跑
                 ↓ 引擎是 journal.py(stdlib only)
② 记忆层    ~/dsh-lore-memory/        跨工作区事实 —— 在**所有**工作区之外(`memoryDir` 可改)
                 ↓ 写:CLI / 读:插件工具
③ 运行时层  lib/index.js(宿主半边)     provider / 工具 / 注入 / 只读状态端点
            lib/client.js(浏览器半边)  本 bundle 那一页上的配置卡片
                 ↓
④ 配置层    Config.volatile()          官方通路(profile patch;headless 退回 settings.json)

依赖只能向下:技能层不知道 DSH 存在;记忆层不知道插件存在(它只是文件); 运行时层是唯一同时知道技能与记忆的地方。

门面怎么说:包根 README.md 面向使用者,把这张图讲成三块能力 —— 技能层 / 记忆层 / 插件层(运行时层 + 配置层合称插件层)。 那是同一张图的粗粒度说法,架构判断一律以本节的四层为准; 两处若不一致,改的是 README,不是这一节。

为什么这条不能破:① 之所以要能独立跑,是因为记录体系对无 DSH 的用户同样有价值 (skill-only/ + dist/ 那条发行线在独立技能仓库 project-work-log 里,就是为它存在的)。一旦技能层依赖插件, 那条线就死了。

二、记忆为什么读写在两个地方

在哪 为什么
写 journal.py(CLI) 写要经过来源分档、索引重建、lint。这些是技能层的能力,而且必须在无 DSH 时也能做
读 插件工具 worklog_memory 结构化、不依赖 shell、也不要求技能在场;没有它,代理就只能靠 pwsh 去 cat 那些文件
取件(信箱) 同一工具的 inbox 操作 见 §六:把消息整个搬进收件方工作区,正文以文件形式到达
发送(信箱) 同一工具的 send 操作 薄包装 journal.py inbox put(子进程):写路径只有 CLI 一条;文件必须装进信里,不许直接放进对方目录
会话清单 同一工具的 sessions 操作 默认只列调用方 Agent 的 cwd 下的会话;显式 scope: "all" 才跨工作区(只读,每行标注 @工作区);send 仍只接受本工作区会话(见 memory-spec §十一 ①)
主动送达授权 同一工具的 subscribe 操作 只给自己的工作区开/关(边界同样是调用方 cwd)。沙箱会话里 CLI 写不了记忆根,这条是主路;发件方替别人开不了

2026-09-30 实测纠正:这张表原先写着"代理自己的 read/grep 被 ctx.workspaceFiles 限制在工作区内,碰不到 $DSH_HOME"。在当前策略(workspace-write)下不成立 —— read 工具能直接读 ~/.dsh/memory/INDEX.md(读不限、写受限)。所以插件的价值不是"唯一的路", 而是唯一不离开会话、不靠 shell、也不依赖技能在场的路;对"连读也受限"的策略它同样是唯一的路。 这条口径错误来自上一轮的源码阅读,没有在跑起来的宿主里验过 —— 记在这里,别再当事实用。

这条不对称是有意的,但它的代价要认:模型能用工具读记忆,却要去 shell 里跑 CLI 才能写。 所以插件另提供薄包装:send 起 journal.py inbox put 子进程,判定与落盘仍在 CLI —— 工具不自己发明格式(那会变成第二份实现,漂了以后两边都绿;见 inboxSplit 那次夹具事故)。 真正写记忆条目的路依旧只有 CLI。

三、设置的两个面,以及谁说了算

2026-09-29 起,配置只有一个事实源:宿主的设置通道。 分层是宿主的:

profile patch(用户层,卡片写这里)  >  本包的行配置  >  内置默认

没有 configEditor 的宿主(headless)退回插件自己的文件,此时是插件自己合:

settings.json(手工编辑)  >  row config(cordis.patch.yml)  >  内置默认

为什么第二种里"文件赢"而不是"row 赢":bundle 补丁里写死了 skillDir 与三个开关(modelInvocable / userInvocable / verbose)。如果 row 赢,那个文件就是死的。

Config 与 settings.json 的关系(已变):Config 是唯一的声明处 —— 字段必须先在那里声明,才有官方通道可走。settings.json 只是没有 configEditor 的宿主的退路,不是第二个声明处。

要守住的一条:新增字段时只挑一个家。 一个可配置字段必须先进 Config 并标 .volatile()(否则官方通道看不见它),同时它的名字必须出现在客户端 lib/client.js 的 FORM_FIELDS 里 —— 否则卡片认领不到自己的 namespace, 整页静默消失。tests/audit-client-runtime.mjs 从 lib/index.js 的源码重新解析 Config 的 .volatile() 字段与 FORM_FIELDS 对账。

四、注入:两套接口,按易变性分工

这是本规格唯一新增的机制。宿主提供两个接口,选择依据不是内容重要与否,而是它变不变:

注入什么 接口 性质 代价
经验层资源提示(2026-10-19 简化) ctx.systemPrompt.section({…}) 稳定;进系统提示词当固定前缀,不随 memoryEnabled 关闭 约 265 字 / ~185 tokens/请求;文本自带「若存在 work_log/」的条件
全局记忆索引 ctx.systemPrompt.section({…}) 稳定;进系统提示词当固定前缀 在每个请求里,所以按字符预算限制
待收 N 条 ctx.systemPrompt.context({…}) 可变;尾注入在保留历史之后 只在内容变化时追加,所以空信箱零成本

经验层为什么要死注入:它替代的是工作区 AGENTS.md 钩子——那段提示必须每轮都在,否则 「工作区有历史记录和经验」这件事只在模型主动加载技能时才可见。做成插件段之后,新工作区不必再建文件; 具体流程(现象词搜索、_inbox.md、收尾检查)在 project-work-log 技能与 work_log/README.md, 固定段只做资源提示,不再复述操作手册。skill-only 场景仍用 AGENTS.md(见 templates.md §10)。

为什么索引用 section 而不是 context:索引变动很少(只在 memory collect / index 之后),而它每次都是同一段文本 —— section 进的是可缓存前缀。 为什么待收条数用 context:它每条消息都变;放在前缀里会让整个提示词缓存失效, 而它的正确位置是"排在保留历史之后",那正是 context 的定义。

两条硬规矩:

  1. 索引只放 id 与一句话,正文永远不进注入 —— 按字符计(软 800 / 硬 1500, 超硬上限报错而不是截断)。要正文就让模型调工具。
  2. 空的东西不产生任何输出。待收 0 条时 context 返回空串;索引不存在时 section 返回空串。一条"这里是空的"的提示本身就是噪声,而且它会日复一日地消耗每个请求。

一个已知限制(不许假装没有):规格原本说索引"按本工作区的 applies-to 取交集", 但装配期拿不到工作区根 —— 实测 process.cwd() 是 profile 目录, sandboxPolicy.resolve({}) 无会话时回同一个兜底根。所以第一版的 section 注入全集;索引本身按设计极小(只有 id 与一句话),全集是可接受的。 要真正按工作区过滤,得先证明装配上下文里能拿到工作区——那是另一件事,不做就不写。

五、记忆对模型是读得到的,写要靠工具

模型要能把新的教训写回记忆,否则"一个工作区学到的东西能被别处用到"就只对已经写进去的 内容成立。写入的判定(来源分档、索引重建、lint)在 journal.py 里,工具不重写那套判定, 只是把它搬过来。

这不违反 §一:技能层仍然能独立跑(CLI 一直在),插件只是多给模型一条不用离开会话 的写路。两边共用同一套规则,因为规则的定义处只有一个。

六、跨工作区传递:目标是「传过去」,不是「做一套邮政」

投递机制用了「邮件 / 信箱」这类词,但那只是形状之一。

准则:跨工作区的目标只有一个——把信息从 A 传到 B。任何具体的载体都是手段。 所以不要为了一个通道去建一套邮政系统:不搞收件人表、不搞优先级、不搞会话管理, 除非有真实的用法要求它。

由此推出两条:

  1. 传信息的正路是全局记忆。 一条在 A 里确证过的教训,写进记忆后所有工作区都能读到, 而它同时有来源分档、lint、dream 收敛与索引注入。为一次性传话而建的第二套机制, 绝大多数情况下是多余的——inbox 只在"这消息对投递方以外的任何人都不该存在"时才用。
  2. inbox 刻意保持小:暂存、已读即删、有过期清扫、有上限。它不承担检索、不承担归档、 不承担会话管理。不要在它上面加语义;需要检索或长期价值的内容应该进记忆。

已知的一次例外(2026-10-17):主动送达要一个固定收件会话(memory-spec §十一 规矩 3) 和它的收束规则(规矩 6)。那是"通道"的一部分,不是一个会话管理器:不按消息建会话、 不列对方的会话、不管理对方会话的生命周期(旧会话留着,由用户归档)。

插件在这里的角色是「通道」:ctx.workspaceFiles 把代理自己的 read/grep 限制在工作区内, 而工具体在宿主进程里跑,所以它是唯一能碰 $DSH_HOME 的路。要确认的是这条通道通不通, 不是它长得像不像邮箱。

七、空承诺是缺陷,不是待办

设置页上任何能点但没有效果的控件,按缺陷处理:要么实现它,要么从页面上撤掉, 要么显式标注"尚未实现"。

理由不是洁癖:这个项目的失效模式反复是**"界面说会做,实际不做,而且不报错"**。 一个诚实的"没做"比一个看起来在工作的开关好得多——后者会让人以为功能坏了, 或者更糟:以为它在保护他。