dsh-acp-enhanced 0.2.1 → 0.3.1
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 +210 -0
- package/README.md +119 -92
- package/lib/codec.js +34 -0
- package/lib/index.js +166 -5
- package/package.json +3 -2
- package/scripts/dsh-acp-zed.sh +64 -0
- package/README-en.md +0 -215
package/README-zh.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
**[English](README.md) | 中文**
|
|
2
|
+
|
|
3
|
+
# dsh-acp-enhanced
|
|
4
|
+
|
|
5
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的增强版
|
|
6
|
+
[Agent Client Protocol](https://agentclientprotocol.com)(ACP)服务器,为 **Zed** 等 ACP
|
|
7
|
+
编辑器设计。它是官方 `@deepseek-ai/dsh-acp` 桥接器的即插即用替代品:官方桥只做纯文本
|
|
8
|
+
输出,本桥把 Web GUI 的能力(流式、遥测、模型/权限控制、会话管理、MCP)全部暴露到
|
|
9
|
+
ACP 线上。
|
|
10
|
+
|
|
11
|
+
## 特性
|
|
12
|
+
|
|
13
|
+
### 输出与遥测
|
|
14
|
+
|
|
15
|
+
- **块级流式 + 推理流式**:文本块与思考过程实时到达(`agent_message_chunk` /
|
|
16
|
+
`agent_thought_chunk`),取消/重试不留半截输出
|
|
17
|
+
- **完整遥测**:上下文用量环 + 缓存命中率 / TPS / 输入-输出-推理 token / 工具耗时 /
|
|
18
|
+
轮次计数(`usage_update._meta` 携带全量明细)
|
|
19
|
+
|
|
20
|
+
### 模型与权限
|
|
21
|
+
|
|
22
|
+
- **模型切换**:实时 `provider/model` 目录下拉(按 ACP 规范分组线格式)
|
|
23
|
+
- **推理强度**:`reasoning_effort` 下拉——仅当当前路由暴露可选 efforts 时出现
|
|
24
|
+
- **权限预设**:read-only / workspace-write / full-access 三种会话模式
|
|
25
|
+
- **审批**:工具调用弹出原生 allow-once / reject-once 审批
|
|
26
|
+
|
|
27
|
+
### Zed 深度集成
|
|
28
|
+
|
|
29
|
+
- **工具卡片**:展开可见每次调用的完整参数与结果预览(`rawInput` / `rawOutput`),
|
|
30
|
+
按工具类型渲染图标
|
|
31
|
+
- **Zed 文件与终端**:`zed_read_text_file` / `zed_write_text_file` / `zed_terminal` 把
|
|
32
|
+
文件编辑放进 Zed 的"编辑文件"区(diff + 接受/拒绝)、命令跑在 Zed 真实终端
|
|
33
|
+
- **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答
|
|
34
|
+
- **Plan 面板**:plan mode 开关 → Zed 底部"规划中"状态条
|
|
35
|
+
|
|
36
|
+
### 会话
|
|
37
|
+
|
|
38
|
+
- **恢复与归档**:`session/load` 恢复历史线程(完整回放);`session/list` /
|
|
39
|
+
`session/delete` 管理线程归档(带标题、按更新时间排序);标题实时推送
|
|
40
|
+
|
|
41
|
+
### 命令
|
|
42
|
+
|
|
43
|
+
- **Slash 命令**:输入 `/` 即可见命令列表(`available_commands_update`):`/status`
|
|
44
|
+
查看路由与遥测、`/model` 列出或切换模型(列表以等宽代码块排版,一眼全见),其余
|
|
45
|
+
(`/compact` `/goal` `/permission` `/plan`…)直通 harness 命令注册表,全部**不经过
|
|
46
|
+
模型 turn** 即时执行;未解析的 slash 放行给模型(`/skill-name` 技能手势)
|
|
47
|
+
|
|
48
|
+
### MCP
|
|
49
|
+
|
|
50
|
+
- **MCP servers**:`session/new` 的 `mcpServers` 挂载任意 MCP server(stdio +
|
|
51
|
+
streamable HTTP),工具以 `mcp__<server>__<tool>` 注入;失败的 server 不会拖垮会话
|
|
52
|
+
|
|
53
|
+
## 效果预览
|
|
54
|
+
|
|
55
|
+
在 Zed 的 AI Agent 面板中选择 **dsh-acp-enhanced** 后:
|
|
56
|
+
|
|
57
|
+
<img src="assets/screenshots/approval-config-context.png" width="560">
|
|
58
|
+
|
|
59
|
+
<img src="assets/screenshots/tool-cards-elicitation.png" width="560">
|
|
60
|
+
|
|
61
|
+
## 快速开始
|
|
62
|
+
|
|
63
|
+
本包遵循 dsh 官方插件规范(声明了 `dsh.bundle`),安装与官方组合包一致:**一条命令**
|
|
64
|
+
完成,自动初始化 profile、安装包、追加 bundle 层,全程无需手写 profile YAML。
|
|
65
|
+
|
|
66
|
+
### 安装(2 步)
|
|
67
|
+
|
|
68
|
+
**第 1 步:安装**(从 npm registry,无需下载源码)
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
dsh plugin --profile acp-enhanced add dsh-acp-enhanced
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> 开发/改源码时用 `link:` 指向本地 checkout(改动实时生效):
|
|
75
|
+
> `dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced"`
|
|
76
|
+
|
|
77
|
+
**第 2 步:注册进 Zed**(在 `~/.config/zed/settings.json` 的 `agent_servers` 里注册;
|
|
78
|
+
Zed 会用极简 PATH 拉起 agent,因此用随附启动器 `scripts/dsh-acp-zed.sh` 定位
|
|
79
|
+
`node`/`dsh`)
|
|
80
|
+
|
|
81
|
+
> **启动器随包发布**,绝对路径取决于第 1 步的安装方式:
|
|
82
|
+
> - **npm 安装(默认)**:`$HOME/.dsh/profiles/acp-enhanced/node_modules/dsh-acp-enhanced/scripts/dsh-acp-zed.sh`。Zed 不会展开 `~` 或环境变量,请把 `$HOME` 换成你的用户目录(如 `/Users/you`)后写全绝对路径。
|
|
83
|
+
> - **`link:` 开发安装**:`<你的 checkout 路径>/scripts/dsh-acp-zed.sh`。
|
|
84
|
+
|
|
85
|
+
#### 最常见:DeepSeek 官方 API(默认路由)
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
{
|
|
89
|
+
// ...你已有的设置...
|
|
90
|
+
"agent_servers": {
|
|
91
|
+
"dsh-acp-enhanced": {
|
|
92
|
+
"type": "custom",
|
|
93
|
+
"command": "/bin/bash",
|
|
94
|
+
"args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
|
|
95
|
+
"env": {
|
|
96
|
+
"DSH_ACP_PROVIDER": "deepseek-official", // 官方 provider id
|
|
97
|
+
"DSH_ACP_MODEL": "deepseek-v4-flash" // 官方模型 id
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> 这两项 env 与包自带 patch 的缺省值一致,**省略也能工作**——显式写上只是让路由意图
|
|
105
|
+
> 一目了然。API key 不必写进 Zed:存入 `~/.dsh/.credentials.yaml`
|
|
106
|
+
> (`DEEPSEEK_API_KEY`)由 dsh 凭据服务解析即可;启动脚本还会兜底继承正在运行的
|
|
107
|
+
> `dsh web` 进程的 key。
|
|
108
|
+
|
|
109
|
+
可选:固定面板默认项(都可随时在面板里改):
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
"dsh-acp-enhanced": {
|
|
113
|
+
// ...上面的 type/command/args/env...
|
|
114
|
+
"default_config_options": {
|
|
115
|
+
"model": "deepseek-official/deepseek-v4-flash",
|
|
116
|
+
"plan_mode": false,
|
|
117
|
+
"reasoning_effort": "high"
|
|
118
|
+
},
|
|
119
|
+
"favorite_config_option_values": {
|
|
120
|
+
"model": ["deepseek-official/deepseek-v4-flash", "deepseek-official/deepseek-v4-pro"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### 扩展:走 OpenAI-Responses 网关(如公司内部模型网关)
|
|
126
|
+
|
|
127
|
+
同一安装路径,只是 env 换成网关暴露的 provider/model 与它要求的 key 环境变量名:
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
"dsh-acp-enhanced": {
|
|
131
|
+
"type": "custom",
|
|
132
|
+
"command": "/bin/bash",
|
|
133
|
+
"args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
|
|
134
|
+
"env": {
|
|
135
|
+
"DSH_ACP_PROVIDER": "<gateway-provider-id>", // 网关暴露的 provider id
|
|
136
|
+
"DSH_ACP_MODEL": "<gateway-model-id>", // 网关暴露的 model id
|
|
137
|
+
"<KEY_ENV_NAME>": "<key>" // 网关声明读取的 key 环境变量名
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> `<KEY_ENV_NAME>` 也可以省掉,把 key 存进 `~/.dsh/.credentials.yaml` 统一管理。
|
|
143
|
+
|
|
144
|
+
Zed 会热重载设置。打开 **AI Agent 面板**(`Cmd+Shift+A`)→ agent 选择器选
|
|
145
|
+
**dsh-acp-enhanced** → 输入第一条消息即可:回复实时流式返回,状态栏显示上下文用量,
|
|
146
|
+
面板顶部有 Model / Permission preset / Plan mode 配置项与三种模式,线程归档可恢复
|
|
147
|
+
历史会话。
|
|
148
|
+
|
|
149
|
+
本地验证(无需 Zed):
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
node scripts/acp-client.mjs # 官方默认路由,无需 env;期望 ALL CHECKS PASSED
|
|
153
|
+
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # 自定义路由时再传
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 可选:web_search 走同一个网关
|
|
157
|
+
|
|
158
|
+
若网关实现 OpenAI Responses 的 `web_search` 服务端工具,可把搜索也路由到网关(复用
|
|
159
|
+
同一凭据)。装子包并给 profile 的 `cordis.patch.yml` 追加两段:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
- id: web
|
|
167
|
+
config:
|
|
168
|
+
searchProvider: openai-responses # 子包注册在 ctx.web 上的搜索 provider id(固定值)
|
|
169
|
+
|
|
170
|
+
- insert:
|
|
171
|
+
- id: web-search-openrouter
|
|
172
|
+
name: 'dsh-web-search-openrouter'
|
|
173
|
+
config:
|
|
174
|
+
enabled: true
|
|
175
|
+
baseURL: http://<gateway-host>:<port>/v1
|
|
176
|
+
model: <your-model-id>
|
|
177
|
+
apiKeyEnv: <KEY_ENV_NAME>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> ⚠️ `searchProvider` 必须**精确等于** `openai-responses`——这是
|
|
181
|
+
> `dsh-web-search-openrouter` 注册在 `ctx.web` 上的搜索 provider id,**不是**网关的
|
|
182
|
+
> LLM provider id(即上面 `DSH_ACP_PROVIDER` 填的那个)。web 插件按 id 精确匹配,
|
|
183
|
+
> 填错时配置期不会报错,直到首次搜索才抛 `WEB_PROVIDER_CONFIGURED_MISSING`。
|
|
184
|
+
|
|
185
|
+
## 故障排查
|
|
186
|
+
|
|
187
|
+
| 症状 | 处理 |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `exec: dsh: not found`(status 127) | 用随附 `dsh-acp-zed.sh` 启动器(自定位 node/dsh) |
|
|
190
|
+
| `no API key for provider route "xxx"` | 写入 `~/.dsh/.credentials.yaml`,或在 agent_servers 里设 `env.DEEPSEEK_API_KEY` |
|
|
191
|
+
| 无法切换模型 / 上下文用量不显示 | 选到了不可路由的"幽灵 provider";本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
192
|
+
| 需要详细诊断 | `ACP_DEBUG=1 dsh --profile acp-enhanced`(stderr 生命周期 trace) |
|
|
193
|
+
|
|
194
|
+
## 开发
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
node scripts/acp-client.mjs # 端到端冒烟(需要 API key)
|
|
198
|
+
node scripts/acp-client-tools.mjs # 客户端工具测试(模拟 Zed 的 fs/terminal/elicitation/plan)
|
|
199
|
+
node scripts/acp-mcp-test.mjs # MCP 挂载测试(无模型调用)
|
|
200
|
+
node scripts/acp-smoke-keyless.mjs # keyless 冒烟(CI 用)
|
|
201
|
+
node scripts/acp-resume-test.mjs # 会话恢复测试
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 已知限制
|
|
205
|
+
|
|
206
|
+
仅 baseline prompt(无图片/音频附件)、不支持 `additionalDirectories`、文本按块粒度
|
|
207
|
+
流式、每会话同时一个 in-flight prompt。MCP 支持 stdio 与 streamable HTTP(不声明
|
|
208
|
+
legacy SSE / `acp` 传输)。`session/close` / `session/fork` / `session/resume` 未实现
|
|
209
|
+
(不声明能力,合规客户端不会调用);`session/delete` 因 dsh 持久化无官方删除 API,
|
|
210
|
+
采用直接删除后端目录的方式。
|
package/README.md
CHANGED
|
@@ -1,111 +1,127 @@
|
|
|
1
|
-
**[
|
|
1
|
+
**[中文](README-zh.md) | English**
|
|
2
2
|
|
|
3
3
|
# dsh-acp-enhanced
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
An enhanced [Agent Client Protocol](https://agentclientprotocol.com) (ACP) server for
|
|
6
|
+
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh), built for ACP
|
|
7
|
+
editors like **Zed**. It is a drop-in replacement for the official `@deepseek-ai/dsh-acp`
|
|
8
|
+
bridge: the official bridge only streams plain text, this one exposes the Web GUI's
|
|
9
|
+
capabilities — streaming, telemetry, model/permission control, session management, MCP —
|
|
10
|
+
over the ACP wire.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Features
|
|
12
13
|
|
|
13
|
-
###
|
|
14
|
+
### Output & telemetry
|
|
14
15
|
|
|
15
|
-
-
|
|
16
|
-
`agent_thought_chunk
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
- **Block + reasoning streaming**: text blocks and the model's thinking arrive live
|
|
17
|
+
(`agent_message_chunk` / `agent_thought_chunk`); cancelled/retried attempts never leak
|
|
18
|
+
torn output
|
|
19
|
+
- **Full telemetry**: context usage ring plus cache hit rate / TPS / input-output-reasoning
|
|
20
|
+
tokens / tool timing / turn counts (`usage_update._meta` carries the full breakdown)
|
|
19
21
|
|
|
20
|
-
###
|
|
22
|
+
### Model & permissions
|
|
21
23
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
-
|
|
24
|
+
- **Model switching**: live `provider/model` catalog dropdown (ACP grouped-select wire shape)
|
|
25
|
+
- **Reasoning effort**: `reasoning_effort` dropdown — only when the routed model exposes
|
|
26
|
+
selectable efforts
|
|
27
|
+
- **Permission presets**: read-only / workspace-write / full-access session modes
|
|
28
|
+
- **Approval**: native allow-once / reject-once prompts per tool call
|
|
26
29
|
|
|
27
|
-
### Zed
|
|
30
|
+
### Zed deep integration
|
|
28
31
|
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
- **Zed
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **
|
|
32
|
+
- **Tool cards**: expand to see each call's full arguments and result preview
|
|
33
|
+
(`rawInput` / `rawOutput`), with per-kind icons
|
|
34
|
+
- **Zed files & terminal**: `zed_read_text_file` / `zed_write_text_file` / `zed_terminal`
|
|
35
|
+
put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a
|
|
36
|
+
real Zed terminal
|
|
37
|
+
- **Native form questions**: `ask_user_question` → `elicitation/create` form, click an
|
|
38
|
+
option, no typing
|
|
39
|
+
- **Plan panel**: plan mode toggle → "planning" status bar in Zed
|
|
35
40
|
|
|
36
|
-
###
|
|
41
|
+
### Sessions
|
|
37
42
|
|
|
38
|
-
-
|
|
39
|
-
`session/delete`
|
|
43
|
+
- **Resume & archive**: `session/load` restores past threads (full replay);
|
|
44
|
+
`session/list` / `session/delete` manage the thread archive (titled, sorted by last
|
|
45
|
+
activity); live title updates
|
|
40
46
|
|
|
41
|
-
###
|
|
47
|
+
### Commands
|
|
42
48
|
|
|
43
|
-
- **
|
|
44
|
-
|
|
49
|
+
- **Slash commands**: typing `/` reveals the command list (`available_commands_update`):
|
|
50
|
+
`/status` shows the route and telemetry, `/model` lists or switches the model (listings
|
|
51
|
+
render as monospace code blocks — readable at a glance), everything
|
|
52
|
+
else (`/compact` `/goal` `/permission` `/plan`…) runs straight through the harness
|
|
53
|
+
command registry — all executed **without a model turn**; unresolved slashes fall
|
|
54
|
+
through to the model (the `/skill-name` skill gesture)
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
### MCP
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
- **MCP servers**: `session/new` `mcpServers` mount any MCP server (stdio + streamable
|
|
59
|
+
HTTP); tools join as `mcp__<server>__<tool>`; a failing server never takes the session
|
|
60
|
+
down
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
## Preview
|
|
51
63
|
|
|
52
|
-
-
|
|
53
|
-
Plan mode 配置项与上下文用量环。
|
|
64
|
+
After picking **dsh-acp-enhanced** in Zed's AI Agent panel:
|
|
54
65
|
|
|
55
|
-
<img src="assets/screenshots/
|
|
66
|
+
<img src="assets/screenshots/approval-config-context.png" width="560">
|
|
56
67
|
|
|
57
|
-
-
|
|
58
|
-
弹出,选项即点即答。
|
|
68
|
+
<img src="assets/screenshots/tool-cards-elicitation.png" width="560">
|
|
59
69
|
|
|
60
|
-
##
|
|
70
|
+
## Quick start
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
This package follows the official dsh plugin conventions (it declares `dsh.bundle`), so
|
|
73
|
+
installation matches any official bundle: **one command** — auto-initializes the profile,
|
|
74
|
+
installs the package, appends the bundle layer; no profile YAML to write.
|
|
64
75
|
|
|
65
|
-
###
|
|
76
|
+
### Install (2 steps)
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
**Step 1 — install** (from the npm registry; no source checkout needed):
|
|
68
79
|
|
|
69
80
|
```sh
|
|
70
81
|
dsh plugin --profile acp-enhanced add dsh-acp-enhanced
|
|
71
82
|
```
|
|
72
83
|
|
|
73
|
-
>
|
|
84
|
+
> When hacking on the code, use `link:` to a local checkout instead (live edits):
|
|
74
85
|
> `dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced"`
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
Zed
|
|
78
|
-
`node`/`dsh
|
|
87
|
+
**Step 2 — register in Zed** (under `agent_servers` in `~/.config/zed/settings.json`;
|
|
88
|
+
Zed spawns agents with a minimal PATH, so use the shipped launcher
|
|
89
|
+
`scripts/dsh-acp-zed.sh`, which locates `node`/`dsh` itself)
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
> **The launcher ships with the package.** Its absolute path depends on how you
|
|
92
|
+
> installed in Step 1:
|
|
93
|
+
> - **npm install (default)**: `$HOME/.dsh/profiles/acp-enhanced/node_modules/dsh-acp-enhanced/scripts/dsh-acp-zed.sh` — replace `$HOME` with your home directory (e.g. `/Users/you`); Zed does not expand `~` or env vars, so write the full literal path.
|
|
94
|
+
> - **`link:` dev install**: `<your checkout>/scripts/dsh-acp-zed.sh`.
|
|
95
|
+
|
|
96
|
+
#### Most common: DeepSeek official API (the default route)
|
|
81
97
|
|
|
82
98
|
```jsonc
|
|
83
99
|
{
|
|
84
|
-
//
|
|
100
|
+
// ...your existing settings...
|
|
85
101
|
"agent_servers": {
|
|
86
102
|
"dsh-acp-enhanced": {
|
|
87
103
|
"type": "custom",
|
|
88
104
|
"command": "/bin/bash",
|
|
89
105
|
"args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
|
|
90
106
|
"env": {
|
|
91
|
-
"DSH_ACP_PROVIDER": "deepseek-official", //
|
|
92
|
-
"DSH_ACP_MODEL": "deepseek-v4-flash" //
|
|
107
|
+
"DSH_ACP_PROVIDER": "deepseek-official", // the official provider id
|
|
108
|
+
"DSH_ACP_MODEL": "deepseek-v4-flash" // the official model id
|
|
93
109
|
}
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
```
|
|
98
114
|
|
|
99
|
-
>
|
|
100
|
-
>
|
|
101
|
-
>
|
|
102
|
-
> `dsh web`
|
|
115
|
+
> Both env vars match the shipped patch's defaults, so **they can be omitted entirely** —
|
|
116
|
+
> writing them out just makes the route explicit. The API key does not have to live in Zed:
|
|
117
|
+
> store it in `~/.dsh/.credentials.yaml` (`DEEPSEEK_API_KEY`) and the dsh credentials
|
|
118
|
+
> service resolves it; the launcher also falls back to a running `dsh web` process's key.
|
|
103
119
|
|
|
104
|
-
|
|
120
|
+
Optional: pin the panel's default config options (all still changeable in the panel):
|
|
105
121
|
|
|
106
122
|
```jsonc
|
|
107
123
|
"dsh-acp-enhanced": {
|
|
108
|
-
//
|
|
124
|
+
// ...the type/command/args/env above...
|
|
109
125
|
"default_config_options": {
|
|
110
126
|
"model": "deepseek-official/deepseek-v4-flash",
|
|
111
127
|
"plan_mode": false,
|
|
@@ -117,9 +133,10 @@ Zed 会用极简 PATH 拉起 agent,因此用随附启动器 `scripts/dsh-acp-z
|
|
|
117
133
|
}
|
|
118
134
|
```
|
|
119
135
|
|
|
120
|
-
####
|
|
136
|
+
#### Extended: route through an OpenAI-Responses gateway (e.g. a company model gateway)
|
|
121
137
|
|
|
122
|
-
|
|
138
|
+
Same install path; only the env values change to the provider/model the gateway exposes
|
|
139
|
+
plus the key env var it requires:
|
|
123
140
|
|
|
124
141
|
```jsonc
|
|
125
142
|
"dsh-acp-enhanced": {
|
|
@@ -127,31 +144,34 @@ Zed 会用极简 PATH 拉起 agent,因此用随附启动器 `scripts/dsh-acp-z
|
|
|
127
144
|
"command": "/bin/bash",
|
|
128
145
|
"args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
|
|
129
146
|
"env": {
|
|
130
|
-
"DSH_ACP_PROVIDER": "<gateway-provider-id>", //
|
|
131
|
-
"DSH_ACP_MODEL": "<gateway-model-id>", //
|
|
132
|
-
"<KEY_ENV_NAME>": "<key>" //
|
|
147
|
+
"DSH_ACP_PROVIDER": "<gateway-provider-id>", // provider id exposed by the gateway
|
|
148
|
+
"DSH_ACP_MODEL": "<gateway-model-id>", // model id exposed by the gateway
|
|
149
|
+
"<KEY_ENV_NAME>": "<key>" // the key env var the gateway reads
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
```
|
|
136
153
|
|
|
137
|
-
> `<KEY_ENV_NAME>`
|
|
154
|
+
> `<KEY_ENV_NAME>` can also be omitted and the key stored in
|
|
155
|
+
> `~/.dsh/.credentials.yaml` instead.
|
|
138
156
|
|
|
139
|
-
Zed
|
|
140
|
-
**dsh-acp-enhanced** →
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
Zed hot-reloads settings. Open the **AI Agent panel** (`Cmd+Shift+A`) → pick
|
|
158
|
+
**dsh-acp-enhanced** in the agent selector → send your first message: replies stream in
|
|
159
|
+
real time, the status bar shows context usage, the panel exposes Model / Permission preset
|
|
160
|
+
/ Plan mode options plus three modes, and the thread archive lists and resumes past
|
|
161
|
+
sessions.
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
Verify locally (no Zed needed):
|
|
145
164
|
|
|
146
165
|
```sh
|
|
147
|
-
node scripts/acp-client.mjs #
|
|
148
|
-
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs #
|
|
166
|
+
node scripts/acp-client.mjs # official default route, no env; expect ALL CHECKS PASSED
|
|
167
|
+
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # only for a custom route
|
|
149
168
|
```
|
|
150
169
|
|
|
151
|
-
###
|
|
170
|
+
### Optional: route web_search through the same gateway
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
If the gateway implements the OpenAI Responses `web_search` server tool, you can route
|
|
173
|
+
search through it too (reusing the same credential). Install the sub-package and append
|
|
174
|
+
two blocks to the profile's `cordis.patch.yml`:
|
|
155
175
|
|
|
156
176
|
```sh
|
|
157
177
|
dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
@@ -160,7 +180,7 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
160
180
|
```yaml
|
|
161
181
|
- id: web
|
|
162
182
|
config:
|
|
163
|
-
searchProvider:
|
|
183
|
+
searchProvider: openai-responses # the search provider id this sub-package registers on ctx.web (fixed value)
|
|
164
184
|
|
|
165
185
|
- insert:
|
|
166
186
|
- id: web-search-openrouter
|
|
@@ -172,29 +192,36 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
172
192
|
apiKeyEnv: <KEY_ENV_NAME>
|
|
173
193
|
```
|
|
174
194
|
|
|
175
|
-
|
|
195
|
+
> ⚠️ `searchProvider` must be **exactly** `openai-responses` — the search provider id
|
|
196
|
+
> `dsh-web-search-openrouter` registers on `ctx.web`. It is **not** your gateway's LLM
|
|
197
|
+
> provider id (the one you put in `DSH_ACP_PROVIDER` above). The `web` plugin matches it
|
|
198
|
+
> exactly, so a wrong value produces no error at config time and only fails at the first
|
|
199
|
+
> search with `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
200
|
+
|
|
201
|
+
## Troubleshooting
|
|
176
202
|
|
|
177
|
-
|
|
|
203
|
+
| Symptom | Fix |
|
|
178
204
|
|---|---|
|
|
179
|
-
| `exec: dsh: not found
|
|
180
|
-
| `no API key for provider route "xxx"` |
|
|
181
|
-
|
|
|
182
|
-
|
|
|
205
|
+
| `exec: dsh: not found` (status 127) | Use the shipped `dsh-acp-zed.sh` launcher (locates node/dsh itself) |
|
|
206
|
+
| `no API key for provider route "xxx"` | Write `~/.dsh/.credentials.yaml`, or set `env.DEEPSEEK_API_KEY` on the agent_servers entry |
|
|
207
|
+
| Cannot switch models / 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 |
|
|
208
|
+
| Need detailed diagnostics | `ACP_DEBUG=1 dsh --profile acp-enhanced` (stderr lifecycle trace) |
|
|
183
209
|
|
|
184
|
-
##
|
|
210
|
+
## Development
|
|
185
211
|
|
|
186
212
|
```sh
|
|
187
|
-
node scripts/acp-client.mjs #
|
|
188
|
-
node scripts/acp-client-tools.mjs #
|
|
189
|
-
node scripts/acp-mcp-test.mjs # MCP
|
|
190
|
-
node scripts/acp-smoke-keyless.mjs # keyless
|
|
191
|
-
node scripts/acp-resume-test.mjs #
|
|
213
|
+
node scripts/acp-client.mjs # end-to-end smoke (needs an API key)
|
|
214
|
+
node scripts/acp-client-tools.mjs # client-tool tests (mocks Zed fs/terminal/elicitation/plan)
|
|
215
|
+
node scripts/acp-mcp-test.mjs # MCP mount test (no model calls)
|
|
216
|
+
node scripts/acp-smoke-keyless.mjs # keyless boot smoke (CI)
|
|
217
|
+
node scripts/acp-resume-test.mjs # session resume test
|
|
192
218
|
```
|
|
193
219
|
|
|
194
|
-
##
|
|
220
|
+
## Known limitations
|
|
195
221
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
legacy SSE / `acp`
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
Baseline prompts only (no image/audio attachments), no `additionalDirectories`, text
|
|
223
|
+
streams at block granularity, one in-flight prompt per session. MCP supports stdio and
|
|
224
|
+
streamable HTTP (legacy SSE / `acp` transports are not advertised).
|
|
225
|
+
`session/close` / `session/fork` / `session/resume` are not implemented (capabilities
|
|
226
|
+
undeclared, compliant clients will not call them); `session/delete` removes the persisted
|
|
227
|
+
directory directly because dsh persistence has no official delete API.
|
package/lib/codec.js
CHANGED
|
@@ -57,6 +57,40 @@ export function promptHasUnsupportedContent(prompt) {
|
|
|
57
57
|
return prompt.some((block) => block.type !== 'text' && block.type !== 'resource_link')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Kramdown attribute-style inline markup (SiYuan exports), including
|
|
61
|
+
* truncation-damaged tails — titles are byte-budgeted upstream, so a cut
|
|
62
|
+
* can land mid-attribute (unterminated `"` or no closing `]`):
|
|
63
|
+
* `[resource_link name="..." url="..."]`, `[file_link ...]`, `[ref ...]`. */
|
|
64
|
+
const KRAMDOWN_ATTR_MARKUP = /\[[^\]]*="[^\]]*\]|\[[^\]]*="[^\]]*$/g
|
|
65
|
+
|
|
66
|
+
/** Tight markdown link `[label](url)` — kept as the label only. */
|
|
67
|
+
const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)\s]+)\)/g
|
|
68
|
+
|
|
69
|
+
/** Human-facing attribute kept when stripping kramdown markup; the closing
|
|
70
|
+
* quote is optional so a value truncated at the byte budget still survives. */
|
|
71
|
+
const ATTR_NAME_OR_TEXT = /(?:name|text)="([^"]*)"?/i
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sanitize a session title for display on the ACP wire. Titles derive from
|
|
75
|
+
* raw first-prompt text, which often carries pasted markup — SiYuan's
|
|
76
|
+
* kramdown `[resource_link ...]` / `[file_link ...]` / `[ref ...]` inline
|
|
77
|
+
* syntax (possibly truncated mid-markup), or plain markdown links. Keeps the
|
|
78
|
+
* human-facing name/text (or the markdown label) and collapses whitespace
|
|
79
|
+
* to a single line.
|
|
80
|
+
* @param input - untrusted title text (raw or upstream-normalized).
|
|
81
|
+
* @returns the cleaned title; `''` when nothing visible remains.
|
|
82
|
+
*/
|
|
83
|
+
export function sanitizeWireTitle(input) {
|
|
84
|
+
return String(input ?? '')
|
|
85
|
+
.replace(MARKDOWN_LINK, '$1')
|
|
86
|
+
.replace(KRAMDOWN_ATTR_MARKUP, (match) => {
|
|
87
|
+
const keep = ATTR_NAME_OR_TEXT.exec(match)
|
|
88
|
+
return keep === null ? '' : keep[1]
|
|
89
|
+
})
|
|
90
|
+
.replace(/\s+/g, ' ')
|
|
91
|
+
.trim()
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
/**
|
|
61
95
|
* Compute the usage telemetry snapshot for one provider-reported usage sample.
|
|
62
96
|
* @param usage - TokenUsage { inputTokens, outputTokens, cacheReadTokens?, cacheWriteTokens?, reasoningTokens? }.
|
package/lib/index.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import { randomUUID } from 'node:crypto'
|
|
33
33
|
import { rm } from 'node:fs/promises'
|
|
34
|
+
import { createRequire } from 'node:module'
|
|
34
35
|
import { isAbsolute, dirname } from 'node:path'
|
|
35
36
|
import { Readable, Writable } from 'node:stream'
|
|
36
37
|
import Schema from '@deepseek-ai/schemastery'
|
|
@@ -39,11 +40,15 @@ import { createUserMessage, errorChain, ReasoningEffortId } from '@deepseek-ai/d
|
|
|
39
40
|
import { installModelSelection } from '@deepseek-ai/dsh-agent'
|
|
40
41
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
41
42
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
42
|
-
import { acpPromptToText, promptHasUnsupportedContent, turnEndToStopReason, usageTelemetry } from './codec.js'
|
|
43
|
+
import { acpPromptToText, promptHasUnsupportedContent, sanitizeWireTitle, turnEndToStopReason, usageTelemetry } from './codec.js'
|
|
44
|
+
|
|
45
|
+
/** Agent version advertised on the ACP wire — read from package.json so the
|
|
46
|
+
* handshake can never drift from the released package version. */
|
|
47
|
+
const AGENT_VERSION = createRequire(import.meta.url)('../package.json').version
|
|
43
48
|
|
|
44
49
|
export const name = 'acp-enhanced'
|
|
45
50
|
/** The bridge creates and owns agents; every other concern is carried by the composition. */
|
|
46
|
-
export const inject = ['agents', 'llm', 'approval', 'tools']
|
|
51
|
+
export const inject = ['agents', 'llm', 'approval', 'tools', 'commands']
|
|
47
52
|
|
|
48
53
|
export const Config = Schema.object({
|
|
49
54
|
/** Initial provider route for every created agent. */
|
|
@@ -121,6 +126,7 @@ export function apply(ctx, config) {
|
|
|
121
126
|
const llm = ctx.llm
|
|
122
127
|
const approval = ctx.approval
|
|
123
128
|
const tools = ctx.tools
|
|
129
|
+
const commands = ctx.commands
|
|
124
130
|
const logger = ctx.logger
|
|
125
131
|
/** The user-questions service (mounted by dsh-base); absent in minimal deployments. */
|
|
126
132
|
const userQuestions = ctx.get('userQuestions')
|
|
@@ -281,7 +287,10 @@ export function apply(ctx, config) {
|
|
|
281
287
|
if (event.data.contextWindow !== undefined) record.contextWindow = event.data.contextWindow
|
|
282
288
|
break
|
|
283
289
|
case 'session/title': {
|
|
284
|
-
|
|
290
|
+
// Titles derive from raw first-prompt text; sanitize before the wire
|
|
291
|
+
// so pasted markup (e.g. SiYuan `[resource_link ...]`) can't leak
|
|
292
|
+
// into Zed's thread title.
|
|
293
|
+
const title = sanitizeWireTitle(event.data.title)
|
|
285
294
|
if (typeof title === 'string' && title.length > 0) {
|
|
286
295
|
record.title = title
|
|
287
296
|
publishSessionInfo(record, {
|
|
@@ -1001,6 +1010,111 @@ export function apply(ctx, config) {
|
|
|
1001
1010
|
}
|
|
1002
1011
|
}
|
|
1003
1012
|
|
|
1013
|
+
// ── slash commands (adapter built-ins + harness command registry) ────────
|
|
1014
|
+
|
|
1015
|
+
/** Adapter-level commands, always first in the advertised list. */
|
|
1016
|
+
const BUILTIN_COMMANDS = [
|
|
1017
|
+
{ name: 'status', description: 'Show session status: model route, context usage, telemetry.' },
|
|
1018
|
+
{ name: 'model', description: 'List the model catalog, or switch with /model <provider/model | substring>.' },
|
|
1019
|
+
]
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Advertise the full command surface for a session as ACP
|
|
1023
|
+
* `available_commands_update`: adapter built-ins plus whatever the harness
|
|
1024
|
+
* command registry (compact/goal/permission/plan/…) serves for the agent.
|
|
1025
|
+
*/
|
|
1026
|
+
async function publishCommands(record) {
|
|
1027
|
+
const list = [...BUILTIN_COMMANDS]
|
|
1028
|
+
const seen = new Set(list.map((command) => command.name))
|
|
1029
|
+
try {
|
|
1030
|
+
for (const descriptor of commands.list(record.agent)) {
|
|
1031
|
+
if (seen.has(descriptor.name)) continue
|
|
1032
|
+
seen.add(descriptor.name)
|
|
1033
|
+
list.push({
|
|
1034
|
+
name: descriptor.name,
|
|
1035
|
+
description: descriptor.description,
|
|
1036
|
+
...descriptor.input === undefined ? {} : { input: descriptor.input },
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
logger.warn(`acp-enhanced: command listing failed: ${String(error)}`)
|
|
1041
|
+
}
|
|
1042
|
+
notify({
|
|
1043
|
+
sessionId: record.agent.session.id,
|
|
1044
|
+
update: { sessionUpdate: 'available_commands_update', availableCommands: list },
|
|
1045
|
+
})
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Advertise the command surface *after* the current request response is
|
|
1050
|
+
* written. A fresh `session/new` id is generated server-side, so the client
|
|
1051
|
+
* cannot route session notifications for it until the response arrives; an
|
|
1052
|
+
* `available_commands_update` queued before that response is dropped and
|
|
1053
|
+
* the slash menu stays empty ("Available commands: none").
|
|
1054
|
+
*/
|
|
1055
|
+
function publishCommandsAfterResponse(record) {
|
|
1056
|
+
setImmediate(() => {
|
|
1057
|
+
publishCommands(record).catch((error) => {
|
|
1058
|
+
logger.warn(`acp-enhanced: command broadcast failed: ${String(error)}`)
|
|
1059
|
+
})
|
|
1060
|
+
})
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Text summary of one session: route, turns, last usage telemetry. */
|
|
1064
|
+
async function statusText(record) {
|
|
1065
|
+
const selection = record.selection.current
|
|
1066
|
+
const last = record.lastUsage
|
|
1067
|
+
const lines = [
|
|
1068
|
+
`route ${selection.provider ?? '?'}/${selection.model ?? '?'}`,
|
|
1069
|
+
`turns ${record.turnCount}`,
|
|
1070
|
+
...last === undefined ? [] : [
|
|
1071
|
+
`context ${last.used}/${last.size}`,
|
|
1072
|
+
`last usage ${last.meta.inputTokens ?? '?'}/${last.meta.outputTokens ?? '?'} in/out, ${last.meta.reasoningTokens ?? '?'} reasoning, cache hit ${last.meta.cacheHitRate ?? '?'}%, ${last.meta.tps ?? '?'} tps`,
|
|
1073
|
+
],
|
|
1074
|
+
]
|
|
1075
|
+
// Monospace code block: aligned columns at a glance, no interaction needed.
|
|
1076
|
+
return ['```', ...lines, '```'].join('\n')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** The /model command: list the live catalog or switch by exact id/substring. */
|
|
1080
|
+
async function modelCommandText(record, query) {
|
|
1081
|
+
const catalog = await modelCatalog()
|
|
1082
|
+
const current = record.selection.current
|
|
1083
|
+
if (query.trim().length === 0) {
|
|
1084
|
+
const lines = catalog.flatMap((group) => group.models.map((model) => {
|
|
1085
|
+
const mark = `${group.id}/${model.id}` === `${current.provider}/${current.model}` ? '* ' : ' '
|
|
1086
|
+
return `${mark}${group.id}/${model.id}`
|
|
1087
|
+
}))
|
|
1088
|
+
// Monospace code block: aligned catalog, no interaction needed.
|
|
1089
|
+
return lines.length > 0 ? ['```', ...lines, '```'].join('\n') : 'no models available'
|
|
1090
|
+
}
|
|
1091
|
+
const needle = query.trim().toLowerCase()
|
|
1092
|
+
const matches = catalog.flatMap((group) => group.models.map((model) => ({ provider: group.id, model: model.id })))
|
|
1093
|
+
.filter((candidate) => `${candidate.provider}/${candidate.model}`.toLowerCase().includes(needle) || candidate.model.toLowerCase().includes(needle))
|
|
1094
|
+
if (matches.length === 0) return `no model matches "${query}"`
|
|
1095
|
+
if (matches.length > 1) return `ambiguous: ${matches.map((m) => `${m.provider}/${m.model}`).join(', ')}`
|
|
1096
|
+
await applySelection(record, { provider: matches[0].provider, model: matches[0].model })
|
|
1097
|
+
return `switched to ${matches[0].provider}/${matches[0].model}`
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Refresh every client-visible surface a command may have mutated. */
|
|
1101
|
+
function refreshAfterCommand(record) {
|
|
1102
|
+
const permission = permissionPresets()
|
|
1103
|
+
if (permission !== undefined) {
|
|
1104
|
+
notify({
|
|
1105
|
+
sessionId: record.agent.session.id,
|
|
1106
|
+
update: {
|
|
1107
|
+
sessionUpdate: 'current_mode_update',
|
|
1108
|
+
currentModeId: permission.current(record.agent.session.events),
|
|
1109
|
+
},
|
|
1110
|
+
})
|
|
1111
|
+
}
|
|
1112
|
+
broadcastConfig(record).catch((error) => {
|
|
1113
|
+
logger.warn(`acp-enhanced: config rebroadcast after command failed: ${String(error)}`)
|
|
1114
|
+
})
|
|
1115
|
+
publishCommands(record)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1004
1118
|
// ── session records + history replay ─────────────────────────────────────
|
|
1005
1119
|
|
|
1006
1120
|
/** Build the bridge-owned protocol record for a fresh or resumed agent. */
|
|
@@ -1066,7 +1180,9 @@ export function apply(ctx, config) {
|
|
|
1066
1180
|
// Malformed line — skip; titles live on well-formed rows.
|
|
1067
1181
|
}
|
|
1068
1182
|
}
|
|
1069
|
-
|
|
1183
|
+
// Stored titles are the raw upstream text (the sanitizer runs on the
|
|
1184
|
+
// live path only); clean them here so replay shows the same wire shape.
|
|
1185
|
+
return last === undefined ? undefined : sanitizeWireTitle(last)
|
|
1070
1186
|
} catch {
|
|
1071
1187
|
return undefined
|
|
1072
1188
|
}
|
|
@@ -1202,7 +1318,7 @@ export function apply(ctx, config) {
|
|
|
1202
1318
|
syncClientTools()
|
|
1203
1319
|
return Promise.resolve({
|
|
1204
1320
|
protocolVersion: PROTOCOL_VERSION,
|
|
1205
|
-
agentInfo: { name: 'deepseek-harness-acp-enhanced', version:
|
|
1321
|
+
agentInfo: { name: 'deepseek-harness-acp-enhanced', version: AGENT_VERSION },
|
|
1206
1322
|
agentCapabilities: {
|
|
1207
1323
|
loadSession: true,
|
|
1208
1324
|
sessionCapabilities: { list: {}, delete: {} },
|
|
@@ -1238,6 +1354,7 @@ export function apply(ctx, config) {
|
|
|
1238
1354
|
const record = makeRecord(handle)
|
|
1239
1355
|
sessions.set(sessionId, record)
|
|
1240
1356
|
await syncMcpServers(params.mcpServers, params.cwd)
|
|
1357
|
+
publishCommandsAfterResponse(record)
|
|
1241
1358
|
const permission = permissionPresets()
|
|
1242
1359
|
return {
|
|
1243
1360
|
sessionId,
|
|
@@ -1299,6 +1416,7 @@ export function apply(ctx, config) {
|
|
|
1299
1416
|
// Zed inserts the thread before the load RPC completes; replay the
|
|
1300
1417
|
// conversation history as notifications so the thread renders.
|
|
1301
1418
|
await replayHistory(record)
|
|
1419
|
+
publishCommandsAfterResponse(record)
|
|
1302
1420
|
const permission = permissionPresets()
|
|
1303
1421
|
return {
|
|
1304
1422
|
...permission === undefined ? {} : {
|
|
@@ -1329,6 +1447,49 @@ export function apply(ctx, config) {
|
|
|
1329
1447
|
}
|
|
1330
1448
|
const text = acpPromptToText(params.prompt)
|
|
1331
1449
|
if (text.trim().length === 0) throw invalidParams('empty prompt')
|
|
1450
|
+
|
|
1451
|
+
// Insurance: by the first prompt the client is guaranteed to know the
|
|
1452
|
+
// session, so re-advertise the command surface (idempotent) — covers
|
|
1453
|
+
// any client that missed the post-session/new broadcast.
|
|
1454
|
+
publishCommands(record)
|
|
1455
|
+
|
|
1456
|
+
// Adapter-level slash commands never reach the model: /status and
|
|
1457
|
+
// /model are built in, any other registered slash (compact/goal/
|
|
1458
|
+
// permission/plan/…) runs through the harness command registry
|
|
1459
|
+
// without a model turn. An unresolved slash falls through — the
|
|
1460
|
+
// /skill-name gesture is claimed inside the agent's next step.
|
|
1461
|
+
const trimmed = text.trim()
|
|
1462
|
+
const commandMatch = trimmed.match(/^\/(\w[\w-]*)\b/)
|
|
1463
|
+
const respond = (reply) => {
|
|
1464
|
+
notify({
|
|
1465
|
+
sessionId: record.agent.session.id,
|
|
1466
|
+
update: {
|
|
1467
|
+
sessionUpdate: 'agent_message_chunk',
|
|
1468
|
+
messageId: randomUUID(),
|
|
1469
|
+
content: { type: 'text', text: reply },
|
|
1470
|
+
},
|
|
1471
|
+
})
|
|
1472
|
+
return { stopReason: 'end_turn' }
|
|
1473
|
+
}
|
|
1474
|
+
if (commandMatch?.[1] === 'status') return respond(await statusText(record))
|
|
1475
|
+
if (commandMatch?.[1] === 'model') {
|
|
1476
|
+
return respond(await modelCommandText(record, trimmed.slice(commandMatch[0].length).trim()))
|
|
1477
|
+
}
|
|
1478
|
+
if (commandMatch !== null && commandMatch[1] !== undefined) {
|
|
1479
|
+
let execution
|
|
1480
|
+
try {
|
|
1481
|
+
execution = await commands.execute(record.agent, trimmed, new AbortController().signal)
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
return respond(`⚠ /${commandMatch[1]} failed: ${error.message ?? String(error)}`)
|
|
1484
|
+
}
|
|
1485
|
+
if (execution !== undefined) {
|
|
1486
|
+
const { result } = execution
|
|
1487
|
+
const reply = result.text ?? (result.kind === 'success' ? `/${commandMatch[1]} ✓` : `/${commandMatch[1]} failed`)
|
|
1488
|
+
refreshAfterCommand(record)
|
|
1489
|
+
return respond(result.kind === 'error' ? `⚠ ${reply}` : reply)
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1332
1493
|
if (ctx.agents.get(record.agent.id) !== record.agent) {
|
|
1333
1494
|
throw internalError('prompt was not queued: the agent was disposed outside the bridge')
|
|
1334
1495
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-acp-enhanced",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Enhanced ACP server for DeepSeek Harness: block-level streaming, usage/stat telemetry (cache hit rate, token speed, input/output tokens, context length, turns, tool timing), model & reasoning-effort switching, and permission-preset control over the ACP wire (Zed-friendly)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"lib",
|
|
25
25
|
"profile",
|
|
26
|
+
"scripts/dsh-acp-zed.sh",
|
|
26
27
|
"cordis.patch.yml",
|
|
27
28
|
"README.md",
|
|
28
|
-
"README-
|
|
29
|
+
"README-zh.md",
|
|
29
30
|
"LICENSE"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Zed launcher for dsh-acp-enhanced.
|
|
3
|
+
#
|
|
4
|
+
# Zed (a GUI app) spawns agent processes with a minimal PATH that usually does
|
|
5
|
+
# NOT include node or dsh, so this wrapper locates both itself:
|
|
6
|
+
# - node: PATH, /opt/homebrew/bin, /usr/local/bin, ~/.nvm/versions/node/*
|
|
7
|
+
# - dsh: PATH, the npx cache (~/.npm/_npx/*/node_modules/.bin), the global
|
|
8
|
+
# npm prefix bin dir, /opt/homebrew/bin, /usr/local/bin
|
|
9
|
+
# and prepends the node dir to PATH so dsh's `#!/usr/bin/env node` shebang
|
|
10
|
+
# resolves.
|
|
11
|
+
#
|
|
12
|
+
# The profile resolves DEEPSEEK_API_KEY through the dsh credentials service
|
|
13
|
+
# (~/.dsh/.credentials.yaml), so no environment plumbing is required; an
|
|
14
|
+
# explicit DEEPSEEK_API_KEY from Zed's agent_servers.env wins, and a running
|
|
15
|
+
# `dsh web` process is a final fallback source.
|
|
16
|
+
#
|
|
17
|
+
# stdout stays the ACP JSON-RPC wire; diagnostics go to stderr.
|
|
18
|
+
set -u
|
|
19
|
+
|
|
20
|
+
NODE_BIN="$(command -v node 2>/dev/null || true)"
|
|
21
|
+
if [ -z "${NODE_BIN}" ]; then
|
|
22
|
+
for candidate in \
|
|
23
|
+
/opt/homebrew/bin/node \
|
|
24
|
+
/usr/local/bin/node \
|
|
25
|
+
"$HOME"/.nvm/versions/node/*/bin/node; do
|
|
26
|
+
if [ -x "${candidate}" ]; then
|
|
27
|
+
NODE_BIN="${candidate}"
|
|
28
|
+
break
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
fi
|
|
32
|
+
if [ -n "${NODE_BIN}" ]; then
|
|
33
|
+
export PATH="$(dirname "${NODE_BIN}"):${PATH}"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
DASH_BIN="$(command -v dsh 2>/dev/null || true)"
|
|
37
|
+
if [ -z "${DASH_BIN}" ]; then
|
|
38
|
+
for candidate in \
|
|
39
|
+
"$HOME"/.npm/_npx/*/node_modules/.bin/dsh \
|
|
40
|
+
"$(npm prefix -g 2>/dev/null)/bin/dsh" \
|
|
41
|
+
/opt/homebrew/bin/dsh \
|
|
42
|
+
/usr/local/bin/dsh; do
|
|
43
|
+
if [ -x "${candidate}" ]; then
|
|
44
|
+
DASH_BIN="${candidate}"
|
|
45
|
+
break
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
fi
|
|
49
|
+
if [ -z "${NODE_BIN}" ] || [ -z "${DASH_BIN}" ]; then
|
|
50
|
+
echo "dsh-acp-zed: cannot locate node and/or dsh (node='${NODE_BIN}' dsh='${DASH_BIN}'); install them or set PATH" >&2
|
|
51
|
+
exit 127
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
|
|
55
|
+
WEB_PID="$(pgrep -f 'dsh web' | head -n 1)"
|
|
56
|
+
if [ -n "${WEB_PID}" ]; then
|
|
57
|
+
KEY="$(ps eww "${WEB_PID}" 2>/dev/null | tr ' ' '\n' | grep '^DEEPSEEK_API_KEY=' | cut -d= -f2-)"
|
|
58
|
+
if [ -n "${KEY}" ]; then
|
|
59
|
+
export DEEPSEEK_API_KEY="${KEY}"
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
exec "${DASH_BIN}" --profile acp-enhanced "$@"
|
package/README-en.md
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
**[中文](README.md) | English**
|
|
2
|
-
|
|
3
|
-
# dsh-acp-enhanced
|
|
4
|
-
|
|
5
|
-
An enhanced [Agent Client Protocol](https://agentclientprotocol.com) (ACP) server for
|
|
6
|
-
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh), built for ACP
|
|
7
|
-
editors like **Zed**. It is a drop-in replacement for the official `@deepseek-ai/dsh-acp`
|
|
8
|
-
bridge: the official bridge only streams plain text, this one exposes the Web GUI's
|
|
9
|
-
capabilities — streaming, telemetry, model/permission control, session management, MCP —
|
|
10
|
-
over the ACP wire.
|
|
11
|
-
|
|
12
|
-
## Features
|
|
13
|
-
|
|
14
|
-
### Output & telemetry
|
|
15
|
-
|
|
16
|
-
- **Block + reasoning streaming**: text blocks and the model's thinking arrive live
|
|
17
|
-
(`agent_message_chunk` / `agent_thought_chunk`); cancelled/retried attempts never leak
|
|
18
|
-
torn output
|
|
19
|
-
- **Full telemetry**: context usage ring plus cache hit rate / TPS / input-output-reasoning
|
|
20
|
-
tokens / tool timing / turn counts (`usage_update._meta` carries the full breakdown)
|
|
21
|
-
|
|
22
|
-
### Model & permissions
|
|
23
|
-
|
|
24
|
-
- **Model switching**: live `provider/model` catalog dropdown (ACP grouped-select wire shape)
|
|
25
|
-
- **Reasoning effort**: `reasoning_effort` dropdown — only when the routed model exposes
|
|
26
|
-
selectable efforts
|
|
27
|
-
- **Permission presets**: read-only / workspace-write / full-access session modes
|
|
28
|
-
- **Approval**: native allow-once / reject-once prompts per tool call
|
|
29
|
-
|
|
30
|
-
### Zed deep integration
|
|
31
|
-
|
|
32
|
-
- **Tool cards**: expand to see each call's full arguments and result preview
|
|
33
|
-
(`rawInput` / `rawOutput`), with per-kind icons
|
|
34
|
-
- **Zed files & terminal**: `zed_read_text_file` / `zed_write_text_file` / `zed_terminal`
|
|
35
|
-
put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a
|
|
36
|
-
real Zed terminal
|
|
37
|
-
- **Native form questions**: `ask_user_question` → `elicitation/create` form, click an
|
|
38
|
-
option, no typing
|
|
39
|
-
- **Plan panel**: plan mode toggle → "planning" status bar in Zed
|
|
40
|
-
|
|
41
|
-
### Sessions
|
|
42
|
-
|
|
43
|
-
- **Resume & archive**: `session/load` restores past threads (full replay);
|
|
44
|
-
`session/list` / `session/delete` manage the thread archive (titled, sorted by last
|
|
45
|
-
activity); live title updates
|
|
46
|
-
|
|
47
|
-
### MCP
|
|
48
|
-
|
|
49
|
-
- **MCP servers**: `session/new` `mcpServers` mount any MCP server (stdio + streamable
|
|
50
|
-
HTTP); tools join as `mcp__<server>__<tool>`; a failing server never takes the session
|
|
51
|
-
down
|
|
52
|
-
|
|
53
|
-
## Preview
|
|
54
|
-
|
|
55
|
-
After picking **dsh-acp-enhanced** in Zed's AI Agent panel:
|
|
56
|
-
|
|
57
|
-
<img src="assets/screenshots/approval-config-context.png" alt="Approval popup, model/reasoning-effort switches, context ring" width="560">
|
|
58
|
-
|
|
59
|
-
- Tool calls that need permission pop a **native approval prompt**; below the input box sit
|
|
60
|
-
the model, reasoning effort, permission preset, plan mode options and the context usage
|
|
61
|
-
ring.
|
|
62
|
-
|
|
63
|
-
<img src="assets/screenshots/tool-cards-elicitation.png" alt="Tool call inputs and outputs, native Zed question form" width="320">
|
|
64
|
-
|
|
65
|
-
- **Tool cards** expand to show full arguments and result previews; when dsh needs your
|
|
66
|
-
confirmation or a choice, the question arrives as a **native Zed form** — click an
|
|
67
|
-
option, no typing.
|
|
68
|
-
|
|
69
|
-
## Quick start
|
|
70
|
-
|
|
71
|
-
This package follows the official dsh plugin conventions (it declares `dsh.bundle`), so
|
|
72
|
-
installation matches any official bundle: **one command** — auto-initializes the profile,
|
|
73
|
-
installs the package, appends the bundle layer; no profile YAML to write.
|
|
74
|
-
|
|
75
|
-
### Install (2 steps)
|
|
76
|
-
|
|
77
|
-
**Step 1 — install** (from the npm registry; no source checkout needed):
|
|
78
|
-
|
|
79
|
-
```sh
|
|
80
|
-
dsh plugin --profile acp-enhanced add dsh-acp-enhanced
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
> When hacking on the code, use `link:` to a local checkout instead (live edits):
|
|
84
|
-
> `dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced"`
|
|
85
|
-
|
|
86
|
-
**Step 2 — register in Zed** (under `agent_servers` in `~/.config/zed/settings.json`;
|
|
87
|
-
Zed spawns agents with a minimal PATH, so use the shipped launcher
|
|
88
|
-
`scripts/dsh-acp-zed.sh`, which locates `node`/`dsh` itself)
|
|
89
|
-
|
|
90
|
-
#### Most common: DeepSeek official API (the default route)
|
|
91
|
-
|
|
92
|
-
```jsonc
|
|
93
|
-
{
|
|
94
|
-
// ...your existing settings...
|
|
95
|
-
"agent_servers": {
|
|
96
|
-
"dsh-acp-enhanced": {
|
|
97
|
-
"type": "custom",
|
|
98
|
-
"command": "/bin/bash",
|
|
99
|
-
"args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
|
|
100
|
-
"env": {
|
|
101
|
-
"DSH_ACP_PROVIDER": "deepseek-official", // the official provider id
|
|
102
|
-
"DSH_ACP_MODEL": "deepseek-v4-flash" // the official model id
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
> Both env vars match the shipped patch's defaults, so **they can be omitted entirely** —
|
|
110
|
-
> writing them out just makes the route explicit. The API key does not have to live in Zed:
|
|
111
|
-
> store it in `~/.dsh/.credentials.yaml` (`DEEPSEEK_API_KEY`) and the dsh credentials
|
|
112
|
-
> service resolves it; the launcher also falls back to a running `dsh web` process's key.
|
|
113
|
-
|
|
114
|
-
Optional: pin the panel's default config options (all still changeable in the panel):
|
|
115
|
-
|
|
116
|
-
```jsonc
|
|
117
|
-
"dsh-acp-enhanced": {
|
|
118
|
-
// ...the type/command/args/env above...
|
|
119
|
-
"default_config_options": {
|
|
120
|
-
"model": "deepseek-official/deepseek-v4-flash",
|
|
121
|
-
"plan_mode": false,
|
|
122
|
-
"reasoning_effort": "high"
|
|
123
|
-
},
|
|
124
|
-
"favorite_config_option_values": {
|
|
125
|
-
"model": ["deepseek-official/deepseek-v4-flash", "deepseek-official/deepseek-v4-pro"]
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
#### Extended: route through an OpenAI-Responses gateway (e.g. a company model gateway)
|
|
131
|
-
|
|
132
|
-
Same install path; only the env values change to the provider/model the gateway exposes
|
|
133
|
-
plus the key env var it requires:
|
|
134
|
-
|
|
135
|
-
```jsonc
|
|
136
|
-
"dsh-acp-enhanced": {
|
|
137
|
-
"type": "custom",
|
|
138
|
-
"command": "/bin/bash",
|
|
139
|
-
"args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
|
|
140
|
-
"env": {
|
|
141
|
-
"DSH_ACP_PROVIDER": "<gateway-provider-id>", // provider id exposed by the gateway
|
|
142
|
-
"DSH_ACP_MODEL": "<gateway-model-id>", // model id exposed by the gateway
|
|
143
|
-
"<KEY_ENV_NAME>": "<key>" // the key env var the gateway reads
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
> `<KEY_ENV_NAME>` can also be omitted and the key stored in
|
|
149
|
-
> `~/.dsh/.credentials.yaml` instead.
|
|
150
|
-
|
|
151
|
-
Zed hot-reloads settings. Open the **AI Agent panel** (`Cmd+Shift+A`) → pick
|
|
152
|
-
**dsh-acp-enhanced** in the agent selector → send your first message: replies stream in
|
|
153
|
-
real time, the status bar shows context usage, the panel exposes Model / Permission preset
|
|
154
|
-
/ Plan mode options plus three modes, and the thread archive lists and resumes past
|
|
155
|
-
sessions.
|
|
156
|
-
|
|
157
|
-
Verify locally (no Zed needed):
|
|
158
|
-
|
|
159
|
-
```sh
|
|
160
|
-
node scripts/acp-client.mjs # official default route, no env; expect ALL CHECKS PASSED
|
|
161
|
-
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # only for a custom route
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Optional: route web_search through the same gateway
|
|
165
|
-
|
|
166
|
-
If the gateway implements the OpenAI Responses `web_search` server tool, you can route
|
|
167
|
-
search through it too (reusing the same credential). Install the sub-package and append
|
|
168
|
-
two blocks to the profile's `cordis.patch.yml`:
|
|
169
|
-
|
|
170
|
-
```sh
|
|
171
|
-
dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
```yaml
|
|
175
|
-
- id: web
|
|
176
|
-
config:
|
|
177
|
-
searchProvider: <provider>
|
|
178
|
-
|
|
179
|
-
- insert:
|
|
180
|
-
- id: web-search-openrouter
|
|
181
|
-
name: 'dsh-web-search-openrouter'
|
|
182
|
-
config:
|
|
183
|
-
enabled: true
|
|
184
|
-
baseURL: http://<gateway-host>:<port>/v1
|
|
185
|
-
model: <your-model-id>
|
|
186
|
-
apiKeyEnv: <KEY_ENV_NAME>
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
## Troubleshooting
|
|
190
|
-
|
|
191
|
-
| Symptom | Fix |
|
|
192
|
-
|---|---|
|
|
193
|
-
| `exec: dsh: not found` (status 127) | Use the shipped `dsh-acp-zed.sh` launcher (locates node/dsh itself) |
|
|
194
|
-
| `no API key for provider route "xxx"` | Write `~/.dsh/.credentials.yaml`, or set `env.DEEPSEEK_API_KEY` on the agent_servers entry |
|
|
195
|
-
| Cannot switch models / 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 |
|
|
196
|
-
| Need detailed diagnostics | `ACP_DEBUG=1 dsh --profile acp-enhanced` (stderr lifecycle trace) |
|
|
197
|
-
|
|
198
|
-
## Development
|
|
199
|
-
|
|
200
|
-
```sh
|
|
201
|
-
node scripts/acp-client.mjs # end-to-end smoke (needs an API key)
|
|
202
|
-
node scripts/acp-client-tools.mjs # client-tool tests (mocks Zed fs/terminal/elicitation/plan)
|
|
203
|
-
node scripts/acp-mcp-test.mjs # MCP mount test (no model calls)
|
|
204
|
-
node scripts/acp-smoke-keyless.mjs # keyless boot smoke (CI)
|
|
205
|
-
node scripts/acp-resume-test.mjs # session resume test
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
## Known limitations
|
|
209
|
-
|
|
210
|
-
Baseline prompts only (no image/audio attachments), no `additionalDirectories`, text
|
|
211
|
-
streams at block granularity, one in-flight prompt per session. MCP supports stdio and
|
|
212
|
-
streamable HTTP (legacy SSE / `acp` transports are not advertised).
|
|
213
|
-
`session/close` / `session/fork` / `session/resume` are not implemented (capabilities
|
|
214
|
-
undeclared, compliant clients will not call them); `session/delete` removes the persisted
|
|
215
|
-
directory directly because dsh persistence has no official delete API.
|