dsh-acp-enhanced 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh.md +117 -23
- package/README.md +155 -26
- package/lib/codec.js +38 -0
- package/lib/index.js +377 -118
- package/lib/terminal-codec.js +6 -1
- package/package.json +32 -31
- package/scripts/dsh-acp-zed.sh +94 -19
package/README-zh.md
CHANGED
|
@@ -51,7 +51,9 @@ ACP 线上。
|
|
|
51
51
|
状态机为进行中 → 完成/失败
|
|
52
52
|
- **Zed 文件与终端**:`zed_read_text_file` / `zed_write_text_file` / `zed_terminal` 把
|
|
53
53
|
文件编辑放进 Zed 的"编辑文件"区(diff + 接受/拒绝)、命令跑在 Zed 真实终端
|
|
54
|
-
- **原生表单提问**:`ask_user_question` → `elicitation/create`
|
|
54
|
+
- **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答;
|
|
55
|
+
选项带描述展示,每个带选项的问题附一个"自定义答案"输入框——选项都不合适时可自由输入,
|
|
56
|
+
单选时自定义答案覆盖所选、多选时与所选并存(与 dsh 原生提问卡片语义一致)
|
|
55
57
|
- **Plan 面板**:plan mode 开关 → Zed 底部"规划中"状态条
|
|
56
58
|
|
|
57
59
|
### 会话
|
|
@@ -70,7 +72,9 @@ ACP 线上。
|
|
|
70
72
|
(列表以等宽代码块排版,一眼全见),其余(`/compact` `/goal` `/permission`
|
|
71
73
|
`/plan`…)直通 harness 命令注册表,全部**不经过模型 turn** 即时执行。所有
|
|
72
74
|
userInvocable 技能也会作为命令广播,`/ask-matt`、`/code-review`、`/tdd` 等能被
|
|
73
|
-
编辑器放行到达桥,技能正文按 dsh-tool-skill
|
|
75
|
+
编辑器放行到达桥,技能正文按 dsh-tool-skill 的用户调用方式注入消息。斜杠命令
|
|
76
|
+
旁粘贴的图片会作为命令附件随行(例如 `/goal` 目标的参考截图),与 Web 端
|
|
77
|
+
composer 的提交方式一致
|
|
74
78
|
|
|
75
79
|
### MCP
|
|
76
80
|
|
|
@@ -183,34 +187,107 @@ node scripts/acp-client.mjs # 官方默认路由,无需 env
|
|
|
183
187
|
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # 自定义路由时再传
|
|
184
188
|
```
|
|
185
189
|
|
|
186
|
-
###
|
|
190
|
+
### Web 搜索
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
bridge 自身不携带、也不推荐任何搜索 provider:模型侧 `web_search` 工具走 `web`
|
|
193
|
+
seam 的 `searchProvider`,往 profile 里挂任意 `ctx.web` provider 即可——带
|
|
194
|
+
`dsh.bundle` 的包用 `dsh plugin --profile acp-enhanced add <package>` 安装,普通包
|
|
195
|
+
走用户层 `insert` 挂载(见下节)。你的 dsh 部署里有哪些 provider 是 profile 层的
|
|
196
|
+
事,与 bridge 无关。
|
|
197
|
+
|
|
198
|
+
### 管理 profile 的插件
|
|
199
|
+
|
|
200
|
+
dsh-acp-enhanced 跑在**独立的 profile** 里——`acp-enhanced`(由上面的安装命令创建于
|
|
201
|
+
`~/.dsh/profiles/acp-enhanced/`),与 `dsh web` 背后的 `web` profile 完全隔离,
|
|
202
|
+
在这里增删改插件不会影响 web 侧的任何配置。
|
|
203
|
+
|
|
204
|
+
profile 的插件树由三层组合而成,后层修补前层:
|
|
205
|
+
|
|
206
|
+
1. **bundle 层**:profile `package.json` 的 `dsh.profile.bundles`——模板自带的
|
|
207
|
+
`@deepseek-ai/dsh-base` 在前,随后是每个声明了 `dsh.bundle` 的已安装包(如
|
|
208
|
+
`dsh-acp-enhanced`),按数组顺序排列。
|
|
209
|
+
2. **用户层**:`~/.dsh/profiles/acp-enhanced/cordis.patch.yml`——按 id 定位的行配置
|
|
210
|
+
覆写、`disabled: true` 行禁用,以及 `insert` 挂载(无 `dsh.bundle` 的包——如手工
|
|
211
|
+
挂载的自写 provider——就靠它装配)。
|
|
212
|
+
3. **临时覆盖**:`dsh --profile acp-enhanced --patch extra.yml`。
|
|
213
|
+
|
|
214
|
+
调整插件集:
|
|
190
215
|
|
|
191
216
|
```sh
|
|
192
|
-
dsh plugin --profile acp-enhanced add dsh
|
|
217
|
+
dsh plugin --profile acp-enhanced add <package> # 安装;声明 dsh.bundle 的包自动加入层栈
|
|
218
|
+
dsh plugin --profile acp-enhanced remove <package> # 卸载;自动退出层栈
|
|
219
|
+
dsh plugin --profile acp-enhanced update [package] # 更新一个/全部并 reconcile
|
|
220
|
+
dsh --profile acp-enhanced --dump-config # 查看组合后的完整树(标注每行来自哪一层)
|
|
193
221
|
```
|
|
194
222
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
223
|
+
`dsh plugin` 本质是在 profile 目录里转发 pnpm,并在每次运行后按安装状态 reconcile
|
|
224
|
+
`dsh.profile.bundles`。两个值得知道的推论:
|
|
225
|
+
|
|
226
|
+
- **靠从 `bundles` 里删条目来禁用 bundle 是禁不住的**——包仍是已安装依赖,下一次
|
|
227
|
+
`dsh plugin` 运行会原样加回来。想不禁载地禁用某一行,请在用户层按**行 id**(不是
|
|
228
|
+
包名,id 可在 `--dump-config` 输出里查)定位:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
- id: mnemon
|
|
232
|
+
disabled: true
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- **无 `dsh.bundle` 的包自身不会装配**——它只作为普通依赖安装(带一次性警告),需要
|
|
236
|
+
自己在用户层 `insert` 挂载;要改已有行的配置,用 `- id: <行>` + `config:` 覆写——
|
|
237
|
+
patch 条目是整行替换、不做合并。
|
|
238
|
+
|
|
239
|
+
改动在**下一个**进程生效:Zed 为每个 agent 线程拉起一个全新的
|
|
240
|
+
`dsh --profile acp-enhanced`,编辑 profile 后新开 agent 线程(或重启 Zed)即可。
|
|
241
|
+
|
|
242
|
+
## 兼容性
|
|
243
|
+
|
|
244
|
+
同一个桥可运行在 **0.1.0-rc.6** 至 **0.1.2-rc.1** 的每一代 harness 上。0.1.2 线重写了本桥消费的多个
|
|
245
|
+
API,桥在运行期同时吸收各代——不分叉、不加版本开关:
|
|
246
|
+
|
|
247
|
+
| API | ≤ 0.1.1-rc.2(旧代) | ≥ 0.1.2-alpha.2(projection 代) | 桥的做法 |
|
|
248
|
+
|---|---|---|---|
|
|
249
|
+
| 会话的运行中 preset | `resolveSessionPreset({header, events})` 导出 | 导出已移除;`agentPreset` session projection | 自行折叠事件日志(最后一个 `agent-preset/selected` 胜出、header 兜底)——两代语义一致 |
|
|
250
|
+
| preset 解析失败 | `UnknownPresetError` / `PresetMountError` | `RemoteError`,错误码 `agent-preset/*` | `isPresetClientError`:RemoteError 按 `isDSHRemoteError` + `code` 鸭子类型识别;旧类按 `presetId` 结构识别(绝不跨副本 `instanceof`) |
|
|
251
|
+
| `permissionPresets.current(x)` | `current(events)` | `current(session)`(经 `permissionState`) | `currentPermissionMode` 每次调用前探测服务实例 |
|
|
252
|
+
| 会话事件日志读取 | 同步 `session.events` 数组 | `session.events` 已移除(0.1.2-rc.1);改为 `snapshotEvents()` / `ownEvents()` / `eventAt()` | `sessionEventsOf`:有 `snapshotEvents()` 用之,否则用活数组 |
|
|
253
|
+
| 注册表 `execute` 签名 | `execute(agent, line, signal)` | `execute(agent, line, images, signal)`(images 插在 line 与 signal 之间,0.1.1-rc.1 起) | `executeRegistryCommand` 按声明参数个数探测(`Remote` 装饰器不包裹方法) |
|
|
254
|
+
| `userQuestions` 注册 | `registerProvider({ask})` | `user-questions/request` Cordis waterfall(0.1.2-alpha.2 起) | 探测服务实例;waterfall 监听只应答本桥会话、其余经 `next()` 传递 |
|
|
255
|
+
|
|
256
|
+
两条不变量保证其安全性(openma 的 `deepseek-harness-acp` 适配器独立得出了同样结论):**只值导入纯
|
|
257
|
+
helper**(`createUserMessage`、`ReasoningEffortId`、`SessionId`、`defineTool`…… 外来副本功能等价);**服务的代际问题按服务实例探测回答**——决定服务代际的是启动它的 CLI,不是本包的依赖范围。`dsh-agent-presets`
|
|
258
|
+
按 *命名空间* 导入:0.1.2-alpha.1 删除了其命名导出,命名导入会在 ESM 链接期直接失败。
|
|
259
|
+
|
|
260
|
+
从干净依赖树校验两代:
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
node scripts/compat-check.mjs # 分别安装 0.1.0-rc.6 与 0.1.2-alpha.2+ 两套,逐一导入本桥
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 开发检出:仓库锁定 CLI + 独立 home
|
|
267
|
+
|
|
268
|
+
启动器**从检出目录**(`link:` 安装)运行时,按以下顺序解析 dsh CLI:
|
|
269
|
+
|
|
270
|
+
1. `$DSH_PATH` —— 显式指定的 dsh 二进制,或其 `node_modules/.bin/dsh` 内含 dsh 的目录
|
|
271
|
+
2. 仓库锁定的 CLI —— `<repo>/node_modules/.bin/dsh`(本包的 `@deepseek-ai/dsh`
|
|
272
|
+
devDependency,当前 0.1.2-rc.1)
|
|
273
|
+
3. 全局兜底 —— PATH / npx 缓存 / npm 前缀 里的 `dsh`(旧行为;未 `pnpm install` 的全新检出退化为它)
|
|
274
|
+
|
|
275
|
+
命中 (1) 或 (2) 时,profile 在**独立 home**(`DSH_ACP_HOME`,默认 `~/.dsh-acp`)下启动:dsh
|
|
276
|
+
每次启动都会把自身依赖闭包 heal 进 `$DSH_HOME/profiles/node_modules`——该目录被同 home 下所有
|
|
277
|
+
profile 共享、内容随最后启动的 CLI 翻转——因此第二个 CLI 代际不得与运行中的 `dsh web`
|
|
278
|
+
等共享 home。此路径永不触碰默认 home。harness 注入到子进程的 `DSH_HOME=$HOME/.dsh`
|
|
279
|
+
(dsh 会向每个 agent/工具进程导出它)会被识别并覆盖而非沿用;只有指向默认 home 之外的
|
|
280
|
+
`DSH_HOME` 才被尊重;确要将锁定 CLI 跑在默认 home 上,请显式设 `DSH_ACP_HOME=$HOME/.dsh`。
|
|
281
|
+
|
|
282
|
+
一次性引导独立 home(建**不含** `dsh-mnemon` 的 profile——它不支持 0.1.2-alpha harness——并逐字移植旧
|
|
283
|
+
profile 的用户层行、迁移凭据/设置、挂 0.1.2-alpha `standard` preset 所需的
|
|
284
|
+
`subagent-model-selection-settings` 宿主服务、关闭 DeepSeek 插件清单上报):
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
scripts/init-acp-home.sh # 幂等;重跑不会覆盖你的文件
|
|
208
288
|
```
|
|
209
289
|
|
|
210
|
-
|
|
211
|
-
> `dsh-web-search-openrouter` 注册在 `ctx.web` 上的搜索 provider id,**不是**网关的
|
|
212
|
-
> LLM provider id(即上面 `DSH_ACP_PROVIDER` 填的那个)。web 插件按 id 精确匹配,
|
|
213
|
-
> 填错时配置期不会报错,直到首次搜索才抛 `WEB_PROVIDER_CONFIGURED_MISSING`。
|
|
290
|
+
两代 harness 都把会话持久化在 `$DSH_HOME/sessions/<slug>/<id>/session.jsonl.zstd`,且新代可读旧代日志(已验证:历史回放与 preset 折叠跨代工作)。因此旧线程只需把会话历史拷到新 home——`scripts/init-acp-home.sh` 会打印这条命令(或加 `--copy-sessions`);默认不拷贝,因为默认 home 的目录里还有全部 web profile 会话。
|
|
214
291
|
|
|
215
292
|
## 故障排查
|
|
216
293
|
|
|
@@ -218,6 +295,9 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
218
295
|
|---|---|
|
|
219
296
|
| `exec: dsh: not found`(status 127) | 用随附 `dsh-acp-zed.sh` 启动器(自定位 node/dsh) |
|
|
220
297
|
| `no API key for provider route "xxx"` | 写入 `~/.dsh/.credentials.yaml`,或在 agent_servers 里设 `env.DEEPSEEK_API_KEY` |
|
|
298
|
+
| `SyntaxError: ... 'PresetMountError'` | 你在 0.1.2-alpha 宿主上运行 0.7.0 之前的桥副本——升级本包 |
|
|
299
|
+
| `modelSelectionSettings requires ... in the Host scope` | 0.1.2-alpha 宿主缺少 `subagent-model-selection-settings` 行——运行 `scripts/init-acp-home.sh`(或按脚本模板在用户层补 insert 行) |
|
|
300
|
+
| 宿主升级后旧线程变空白 | 会话存放在 `$DSH_HOME/sessions/<slug>/`;把旧 home 的历史拷进独立 home(`scripts/init-acp-home.sh --copy-sessions`)即可继续 |
|
|
221
301
|
| 无法切换模型 | 保存的 `reasoning_effort` 默认值(或会话当前 effort)被带到新模型上。0.3.6 起本桥按模型记住上次使用的强度(随 profile 持久化):不被新模型支持的 effort 会被该模型记忆值替换——没有记忆则回退其默认值,再无默认则取第一个可选值,既不会切换失败也不会出现 "unknown"。另检查:是否选到了不可路由的"幽灵 provider"——本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
222
302
|
| 上下文用量不显示 | 选到了不可路由的"幽灵 provider";本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
223
303
|
| 需要详细诊断 | `ACP_DEBUG=1 dsh --profile acp-enhanced`(stderr 生命周期 trace) |
|
|
@@ -225,6 +305,7 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
225
305
|
## 开发
|
|
226
306
|
|
|
227
307
|
```sh
|
|
308
|
+
node scripts/compat-check.mjs # 跨代链接检查(0.1.0-rc.6 + 0.1.2-alpha.2+ 临时安装)
|
|
228
309
|
node scripts/acp-client.mjs # 端到端冒烟(需要 API key)
|
|
229
310
|
node scripts/acp-client-tools.mjs # 客户端工具测试(模拟 Zed 的 fs/terminal/elicitation/plan)
|
|
230
311
|
node scripts/acp-mcp-test.mjs # MCP 挂载测试(无模型调用)
|
|
@@ -233,8 +314,21 @@ node scripts/acp-resume-test.mjs # 会话恢复测试
|
|
|
233
314
|
node scripts/codec-image-test.mjs # 图片编解码单元测试(无网络,假 store)
|
|
234
315
|
node scripts/terminal-codec-test.mjs # 终端卡片编解码单元测试(无网络)
|
|
235
316
|
node scripts/acp-image-e2e.mjs # 图片能力端到端(vision 模型段需 API key)
|
|
317
|
+
scripts/init-acp-home.sh # 引导/刷新独立 home(~/.dsh-acp)
|
|
236
318
|
```
|
|
237
319
|
|
|
320
|
+
harness 包的 devDependency 与锁定的 `@deepseek-ai/dsh` CLI 声明相同的 range(如
|
|
321
|
+
`^0.1.2-rc.1`),让仓库依赖树与全新 CLI 安装解析出同一个连贯家族——在此用精确 patch
|
|
322
|
+
锁定、与 CLI 的 range 闭包混存会得到分裂闭包(同名包两个版本),profile 启动时报
|
|
323
|
+
export-not-found。改这些锁定后务必整体重建 lockfile(`rm -rf node_modules pnpm-lock.yaml
|
|
324
|
+
&& pnpm install`):原地增量安装既会留下污染 profile heal 的残留 store 条目,还会保留
|
|
325
|
+
lockfile 里的陈旧 peer 解析——从 0.1.2-alpha.2 原地升到 0.1.2-rc.1 时,rc.1 各包的
|
|
326
|
+
snapshot 里仍挂着 `dsh-session-persistence@0.1.2-alpha.3`(旧代 peer),boot 与
|
|
327
|
+
session/new 全部通过,直到第一个 turn 才以 `TypeError: Cannot read properties of
|
|
328
|
+
undefined (reading 'length')`(PersistenceCoordinator)崩掉。`pnpm-workspace.yaml` 放行
|
|
329
|
+
了 CLI 闭包的构建脚本(node-pty prebuild、koffi)——仓库 CLI 启动 profile 时它们就是
|
|
330
|
+
运行时依赖。
|
|
331
|
+
|
|
238
332
|
## 已知限制
|
|
239
333
|
|
|
240
334
|
不支持音频附件(不声明 audio 能力)、文本按块粒度流式、每会话同时一个 in-flight
|
package/README.md
CHANGED
|
@@ -59,7 +59,10 @@ over the ACP wire.
|
|
|
59
59
|
put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a
|
|
60
60
|
real Zed terminal
|
|
61
61
|
- **Native form questions**: `ask_user_question` → `elicitation/create` form, click an
|
|
62
|
-
option
|
|
62
|
+
option — or type a custom answer when none of them fit: options render with their
|
|
63
|
+
descriptions, each option-backed question gets a free-text "Custom answer" field, and a
|
|
64
|
+
custom answer replaces the single selection / accompanies a multi-select (same semantics
|
|
65
|
+
as dsh's native question card)
|
|
63
66
|
- **Plan panel**: plan mode toggle → "planning" status bar in Zed
|
|
64
67
|
|
|
65
68
|
### Sessions
|
|
@@ -82,7 +85,9 @@ over the ACP wire.
|
|
|
82
85
|
through the harness command registry — all executed **without a model turn**. Every
|
|
83
86
|
user-invocable skill is advertised as a command too, so `/ask-matt`, `/code-review`,
|
|
84
87
|
`/tdd`, … reach the bridge instead of being rejected by the editor, and the skill's
|
|
85
|
-
instructions are injected into the message (dsh-tool-skill-style user invocation)
|
|
88
|
+
instructions are injected into the message (dsh-tool-skill-style user invocation).
|
|
89
|
+
Images pasted next to a slash line ride along as command attachments (e.g. reference
|
|
90
|
+
screenshots for a `/goal` objective), the same way the Web composer submits them
|
|
86
91
|
|
|
87
92
|
### MCP
|
|
88
93
|
|
|
@@ -151,6 +156,21 @@ Zed spawns agents with a minimal PATH, so use the shipped launcher
|
|
|
151
156
|
> store it in `~/.dsh/.credentials.yaml` (`DEEPSEEK_API_KEY`) and the dsh credentials
|
|
152
157
|
> service resolves it; the launcher also falls back to a running `dsh web` process's key.
|
|
153
158
|
|
|
159
|
+
Debugging a stalled turn (is it the model request or the tool?):
|
|
160
|
+
|
|
161
|
+
```jsonc
|
|
162
|
+
"env": {
|
|
163
|
+
// ...existing vars...
|
|
164
|
+
"ACP_LOG": "/Users/you/.dsh/dsh-acp-enhanced.trace.jsonl" // append-only JSONL event trace
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Each line is one session event with wall-clock `time` (ms epoch); a turn that appears to
|
|
169
|
+
hang is attributable afterwards: a **model request stall** shows a long gap between
|
|
170
|
+
`step/start` and the first `assistant/chunk`, while a **tool-execution stall** shows a
|
|
171
|
+
long gap between `tool/call` and `tool/result` (the result line carries `elapsedMs`).
|
|
172
|
+
`prompt/settled` lines cover the full user-message round trip (stopReason + elapsed).
|
|
173
|
+
|
|
154
174
|
Optional: pin the panel's default config options (all still changeable in the panel):
|
|
155
175
|
|
|
156
176
|
```jsonc
|
|
@@ -202,36 +222,125 @@ node scripts/acp-client.mjs # official default route, no env;
|
|
|
202
222
|
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # only for a custom route
|
|
203
223
|
```
|
|
204
224
|
|
|
205
|
-
###
|
|
225
|
+
### Web search
|
|
226
|
+
|
|
227
|
+
The bridge ships no search provider and takes no position on which one you use: the
|
|
228
|
+
model-facing `web_search` tool rides on the `web` seam's `searchProvider`, so mount any
|
|
229
|
+
`ctx.web` provider into the profile — a package with `dsh.bundle` via
|
|
230
|
+
`dsh plugin --profile acp-enhanced add <package>`, or a plain package via your user-layer
|
|
231
|
+
`insert` rows (see below). Which provider exists in your dsh deployment is a profile
|
|
232
|
+
concern, not a bridge one.
|
|
233
|
+
|
|
234
|
+
### Managing the profile's plugins
|
|
235
|
+
|
|
236
|
+
dsh-acp-enhanced runs in its **own profile** — `acp-enhanced`, created at
|
|
237
|
+
`~/.dsh/profiles/acp-enhanced/` by the install command above — fully separate from the
|
|
238
|
+
`web` profile behind `dsh web`, so plugin changes here never affect your web setup.
|
|
239
|
+
|
|
240
|
+
The profile composes its plugin tree from three sources, each layer patching the ones
|
|
241
|
+
before it:
|
|
242
|
+
|
|
243
|
+
1. **Bundle layers** — `dsh.profile.bundles` in the profile's `package.json`: the
|
|
244
|
+
template's `@deepseek-ai/dsh-base` first, then every installed package that declares
|
|
245
|
+
`dsh.bundle` (like `dsh-acp-enhanced`), in array order.
|
|
246
|
+
2. **Your user layer** — `~/.dsh/profiles/acp-enhanced/cordis.patch.yml`: id-targeted
|
|
247
|
+
row config overrides, `disabled: true` row disables, and `insert` lists (how a
|
|
248
|
+
package without `dsh.bundle` — e.g. a hand-mounted custom provider — gets
|
|
249
|
+
mounted).
|
|
250
|
+
3. **Per-run overlays** — `dsh --profile acp-enhanced --patch extra.yml`.
|
|
206
251
|
|
|
207
|
-
|
|
208
|
-
search through it too (reusing the same credential). Install the sub-package and append
|
|
209
|
-
two blocks to the profile's `cordis.patch.yml`:
|
|
252
|
+
Adjust the set with:
|
|
210
253
|
|
|
211
254
|
```sh
|
|
212
|
-
dsh plugin --profile acp-enhanced add dsh-
|
|
255
|
+
dsh plugin --profile acp-enhanced add <package> # install; a dsh.bundle package auto-joins the layer stack
|
|
256
|
+
dsh plugin --profile acp-enhanced remove <package> # uninstall; auto-leaves the stack
|
|
257
|
+
dsh plugin --profile acp-enhanced update [package] # update one/all, then reconcile
|
|
258
|
+
dsh --profile acp-enhanced --dump-config # inspect the composed tree (per-layer provenance)
|
|
213
259
|
```
|
|
214
260
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
261
|
+
`dsh plugin` is a thin pnpm forwarder (run inside the profile directory) that
|
|
262
|
+
reconciles `dsh.profile.bundles` against the installed state after every run. Two
|
|
263
|
+
consequences worth knowing:
|
|
264
|
+
|
|
265
|
+
- **Disabling a bundle by deleting it from `bundles` does not stick** — the package is
|
|
266
|
+
still an installed dependency, and the next `dsh plugin` run appends it right back.
|
|
267
|
+
To disable a single row without uninstalling, target it in the user layer by its
|
|
268
|
+
**row id** (not the package name — find ids in the `--dump-config` output):
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
- id: mnemon
|
|
272
|
+
disabled: true
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- **A package without `dsh.bundle` loads nothing by itself** — it installs as a plain
|
|
276
|
+
dependency (with a one-time warning) and needs your own `insert` entry in the user
|
|
277
|
+
layer. To change an existing row's config, override it with `- id: <row>` + `config:`
|
|
278
|
+
— patch entries replace the whole row config, they do not merge.
|
|
279
|
+
|
|
280
|
+
Changes take effect in the **next** process: Zed spawns a fresh
|
|
281
|
+
`dsh --profile acp-enhanced` for every agent thread, so open a new agent thread (or
|
|
282
|
+
restart Zed) after editing the profile.
|
|
283
|
+
|
|
284
|
+
## Compatibility
|
|
285
|
+
|
|
286
|
+
One bridge binary runs against every harness generation from **0.1.0-rc.6** through
|
|
287
|
+
**0.1.2-rc.1**. The 0.1.2 line rewrote three APIs this bridge consumes, and the
|
|
288
|
+
bridge absorbs every generation at runtime — no fork, no version flag:
|
|
289
|
+
|
|
290
|
+
| API | ≤ 0.1.1-rc.2 (legacy) | ≥ 0.1.2-alpha.2 (projection) | Bridge behavior |
|
|
291
|
+
|---|---|---|---|
|
|
292
|
+
| running preset of a session | `resolveSessionPreset({header, events})` export | export removed; `agentPreset` session projection | folds the log itself (last `agent-preset/selected` wins, header fallback) — identical semantics in both |
|
|
293
|
+
| preset resolution failure | `UnknownPresetError` / `PresetMountError` | `RemoteError`, codes `agent-preset/*` | `isPresetClientError`: RemoteError duck-typed by `isDSHRemoteError` + `code`, legacy classes identified structurally by `presetId` (never cross-copy `instanceof`) |
|
|
294
|
+
| `permissionPresets.current(x)` | `current(events)` | `current(session)` (via `permissionState`) | `currentPermissionMode` probes the service instance per call |
|
|
295
|
+
| session event log reads | synchronous `session.events` array | `session.events` removed (0.1.2-rc.1); `snapshotEvents()` / `ownEvents()` / `eventAt()` | `sessionEventsOf` reads `snapshotEvents()` when present, the live array otherwise |
|
|
296
|
+
| registry `execute` signature | `execute(agent, line, signal)` | `execute(agent, line, images, signal)` (images between line and signal, 0.1.1-rc.1+) | `executeRegistryCommand` probes the declared arity (the `Remote` decorator never wraps the method) |
|
|
297
|
+
| `userQuestions` registration | `registerProvider({ask})` | `user-questions/request` Cordis waterfall (0.1.2-alpha.2+) | probes the service instance; waterfall listener answers bridge-owned requests and delegates via `next()` |
|
|
298
|
+
|
|
299
|
+
Two invariants make this safe (same conclusions the openma `deepseek-harness-acp` adapter
|
|
300
|
+
reached independently): **value-import pure helpers only** (`createUserMessage`,
|
|
301
|
+
`ReasoningEffortId`, `SessionId`, `defineTool`, … — a foreign copy is functionally
|
|
302
|
+
equivalent), and **service-generation questions are answered by probing the service
|
|
303
|
+
instance**, because the booting CLI — not this package's dependency range — decides the
|
|
304
|
+
service generation. `dsh-agent-presets` is imported as a *namespace*: 0.1.2-alpha.1
|
|
305
|
+
removed its named exports, and a named import would fail at ESM link time.
|
|
306
|
+
|
|
307
|
+
Check both generations from a clean tree:
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
node scripts/compat-check.mjs # installs 0.1.0-rc.6 + 0.1.2-alpha.2+ sets, imports the bridge from each
|
|
228
311
|
```
|
|
229
312
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
313
|
+
### Dev checkout: repo-pinned CLI, isolated home
|
|
314
|
+
|
|
315
|
+
When the launcher runs **from a checkout** (`link:` install), it resolves the dsh CLI in
|
|
316
|
+
this order:
|
|
317
|
+
|
|
318
|
+
1. `$DSH_PATH` — an explicit dsh binary, or a directory whose `node_modules/.bin/dsh` holds one
|
|
319
|
+
2. the repo-pinned CLI — `<repo>/node_modules/.bin/dsh` (this package's `@deepseek-ai/dsh`
|
|
320
|
+
devDependency, currently 0.1.2-rc.1)
|
|
321
|
+
3. global fallback — `dsh` on PATH / npx cache / npm prefix (the legacy behavior; a fresh
|
|
322
|
+
clone without `pnpm install` degrades to it)
|
|
323
|
+
|
|
324
|
+
Whenever (1) or (2) wins, the profile boots under an **isolated home**
|
|
325
|
+
(`DSH_ACP_HOME`, default `~/.dsh-acp`): dsh heals its whole dependency closure into
|
|
326
|
+
`$DSH_HOME/profiles/node_modules` on every boot — a dir shared by every profile under that
|
|
327
|
+
home whose content flips to whichever CLI booted last — so a second CLI generation must not
|
|
328
|
+
share a home with e.g. a running `dsh web`. The default home is never touched by this path.
|
|
329
|
+
Harness-injected `DSH_HOME=$HOME/.dsh` in the child environment (dsh exports it into every
|
|
330
|
+
agent/tool process) is detected and overridden, not honored — only a `DSH_HOME` pointing
|
|
331
|
+
away from the default home is respected; to force the pinned CLI onto the default home,
|
|
332
|
+
set `DSH_ACP_HOME=$HOME/.dsh` deliberately.
|
|
333
|
+
|
|
334
|
+
Bootstrap the isolated home once (profile without `dsh-mnemon` — it does not support the
|
|
335
|
+
0.1.2-alpha harness — plus your old profile's user rows ported verbatim and credentials/settings
|
|
336
|
+
migration, the `subagent-model-selection-settings` host service the 0.1.2-alpha `standard`
|
|
337
|
+
preset requires, and the DeepSeek plugin-package inventory reporter disabled):
|
|
338
|
+
|
|
339
|
+
```sh
|
|
340
|
+
scripts/init-acp-home.sh # idempotent; re-runs never clobber your files
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Both harness generations persist sessions under `$DSH_HOME/sessions/<slug>/<id>/session.jsonl.zstd`, and the new generation reads old-generation logs (verified: history replay and the preset fold work cross-generation). Old threads therefore only need their session history copied to the new home — `scripts/init-acp-home.sh` prints the one-liner (or pass `--copy-sessions`); it copies nothing by default, because the default home's tree also holds every web-profile session.
|
|
235
344
|
|
|
236
345
|
## Troubleshooting
|
|
237
346
|
|
|
@@ -239,6 +348,9 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
239
348
|
|---|---|
|
|
240
349
|
| `exec: dsh: not found` (status 127) | Use the shipped `dsh-acp-zed.sh` launcher (locates node/dsh itself) |
|
|
241
350
|
| `no API key for provider route "xxx"` | Write `~/.dsh/.credentials.yaml`, or set `env.DEEPSEEK_API_KEY` on the agent_servers entry |
|
|
351
|
+
| `SyntaxError: ... 'PresetMountError'` | You are running a pre-0.7.0 bridge copy against a 0.1.2-alpha host — update this package |
|
|
352
|
+
| `modelSelectionSettings requires ... in the Host scope` | A 0.1.2-alpha host without the `subagent-model-selection-settings` row — run `scripts/init-acp-home.sh` (or add the insert row to your user layer, see the script) |
|
|
353
|
+
| Old threads start empty after a host upgrade | The sessions live under `$DSH_HOME/sessions/<slug>/`; copy the old home's history to the isolated home (`scripts/init-acp-home.sh --copy-sessions`) and the new host resumes them |
|
|
242
354
|
| Cannot switch models | The saved `reasoning_effort` default (or the session's current effort) is carried onto the new model. Since 0.3.6 the bridge remembers the last effort per model (per-profile JSON): an unsupported carried effort is replaced by that model's remembered effort, else its own default, else its first offered effort — never an "unknown" dropdown, never a failed switch. Also check: a "phantom provider" route was picked — this bridge filters them by default (only `config.provider`'s models are advertised), so point the profile's provider at a real route |
|
|
243
355
|
| Context usage missing | A "phantom provider" route was picked; this bridge filters them by default (only `config.provider`'s models are advertised) — point the profile's provider at a real route |
|
|
244
356
|
| Need detailed diagnostics | `ACP_DEBUG=1 dsh --profile acp-enhanced` (stderr lifecycle trace) |
|
|
@@ -246,16 +358,33 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
246
358
|
## Development
|
|
247
359
|
|
|
248
360
|
```sh
|
|
361
|
+
node scripts/compat-check.mjs # cross-generation link check (0.1.0-rc.6 + 0.1.2-alpha.2+ scratch installs)
|
|
249
362
|
node scripts/acp-client.mjs # end-to-end smoke (needs an API key)
|
|
250
363
|
node scripts/acp-client-tools.mjs # client-tool tests (mocks Zed fs/terminal/elicitation/plan)
|
|
251
364
|
node scripts/acp-mcp-test.mjs # MCP mount test (no model calls)
|
|
252
365
|
node scripts/acp-smoke-keyless.mjs # keyless boot smoke (CI)
|
|
253
366
|
node scripts/acp-resume-test.mjs # session resume test
|
|
254
367
|
node scripts/codec-image-test.mjs # image-codec unit tests (no network, fake store)
|
|
255
|
-
node scripts/terminal-codec-test.mjs
|
|
368
|
+
node scripts/terminal-codec-test.mjs # terminal-card codec unit tests (no network)
|
|
256
369
|
node scripts/acp-image-e2e.mjs # image capability e2e (vision-model leg needs an API key)
|
|
370
|
+
scripts/init-acp-home.sh # bootstrap/refresh the isolated home (~/.dsh-acp)
|
|
257
371
|
```
|
|
258
372
|
|
|
373
|
+
DevDependency pins for the harness packages use the same ranges the pinned
|
|
374
|
+
`@deepseek-ai/dsh` CLI declares (e.g. `^0.1.2-rc.1`), so the repo's tree and a fresh
|
|
375
|
+
CLI install resolve one coherent family — exact patch pins here mixed with the CLI's
|
|
376
|
+
range-resolved closure produce a split closure (two versions of one name) that breaks
|
|
377
|
+
profile boots with export-not-found errors. After changing those pins, regenerate the
|
|
378
|
+
whole lockfile (`rm -rf node_modules pnpm-lock.yaml && pnpm install`): an incremental
|
|
379
|
+
install both leaves stale store entries poisoning the profile heal AND retains stale
|
|
380
|
+
lockfile peer resolutions — bumping 0.1.2-alpha.2 → 0.1.2-rc.1 in place left
|
|
381
|
+
`dsh-session-persistence@0.1.2-alpha.3` (old-generation peers) wired into the rc.1
|
|
382
|
+
packages' snapshots, which passes boot and session/new and only breaks the first turn
|
|
383
|
+
with `TypeError: Cannot read properties of undefined (reading 'length')` from
|
|
384
|
+
PersistenceCoordinator.
|
|
385
|
+
`pnpm-workspace.yaml` approves the CLI closure's build scripts (node-pty prebuilds, koffi)
|
|
386
|
+
— they are runtime requirements when the repo CLI boots the profile.
|
|
387
|
+
|
|
259
388
|
## Known limitations
|
|
260
389
|
|
|
261
390
|
Audio attachments are not supported (audio capability is not advertised), text streams at
|
package/lib/codec.js
CHANGED
|
@@ -57,6 +57,44 @@ export function promptHasUnsupportedContent(prompt) {
|
|
|
57
57
|
return prompt.some((block) => block.type !== 'text' && block.type !== 'resource_link')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Flatten an ACP prompt's text blocks to the line a composer submits: text
|
|
62
|
+
* blocks concatenate verbatim in wire order, while images and resource links
|
|
63
|
+
* are composer attachments, not input-box text. This is the line slash
|
|
64
|
+
* commands are parsed from — an image pasted next to a slash line must neither
|
|
65
|
+
* prefix it nor pollute its arguments (the Web composer's `matchEnter`
|
|
66
|
+
* contract: line text plus a separate images payload).
|
|
67
|
+
* @param prompt - ACP `session/prompt` content, in wire order.
|
|
68
|
+
* @returns the concatenated text-block content.
|
|
69
|
+
*/
|
|
70
|
+
export function acpPromptLineText(prompt) {
|
|
71
|
+
return (prompt ?? []).flatMap((block) => block?.type === 'text' && typeof block.text === 'string' ? [block.text] : []).join('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the composer's image payload for a slash-command submission from an
|
|
76
|
+
* ACP prompt: one `{ data, mediaType, name? }` upload object per image block,
|
|
77
|
+
* in wire order — the registry `execute` images slot's exact shape (the
|
|
78
|
+
* composer serializes `{ type: 'image', mediaType, data, name }`, and the
|
|
79
|
+
* command service's `admitEncodedImages` reads `data`/`mediaType`/`name`).
|
|
80
|
+
* The service admits the payload into the attachment store itself; the store
|
|
81
|
+
* is content-addressed, so an image `convertPrompt` already saved resolves to
|
|
82
|
+
* the same reference and nothing is duplicated. An image whose media type is
|
|
83
|
+
* not a raster cannot reach a command (`convertPrompt` rejects it before
|
|
84
|
+
* dispatch), so a non-raster here is simply skipped.
|
|
85
|
+
* @param prompt - ACP `session/prompt` content, in wire order.
|
|
86
|
+
* @returns upload objects ready for `commands.execute`'s images slot.
|
|
87
|
+
*/
|
|
88
|
+
export function acpPromptCommandImages(prompt) {
|
|
89
|
+
return (prompt ?? []).flatMap((block) => {
|
|
90
|
+
if (block?.type !== 'image' || typeof block.data !== 'string' || block.data.length === 0) return []
|
|
91
|
+
const mediaType = canonicalImageMediaType(block.mimeType)
|
|
92
|
+
if (mediaType === undefined) return []
|
|
93
|
+
const name = imageName(block.uri)
|
|
94
|
+
return [{ data: block.data, mediaType, ...name === undefined ? {} : { name } }]
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
/** Raster media types the harness attachment seam admits (dsh-attachment). */
|
|
61
99
|
const IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
|
|
62
100
|
|