dsh-acp-enhanced 0.3.5 → 0.3.6
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 +5 -2
- package/README.md +4 -2
- package/lib/index.js +92 -17
- package/package.json +1 -1
- package/scripts/dsh-acp-zed.sh +11 -0
package/README-zh.md
CHANGED
|
@@ -20,7 +20,10 @@ ACP 线上。
|
|
|
20
20
|
### 模型与权限
|
|
21
21
|
|
|
22
22
|
- **模型切换**:实时 `provider/model` 目录下拉(按 ACP 规范分组线格式)
|
|
23
|
-
- **推理强度**:`reasoning_effort` 下拉——仅当当前路由暴露可选 efforts
|
|
23
|
+
- **推理强度**:`reasoning_effort` 下拉——仅当当前路由暴露可选 efforts 时出现;
|
|
24
|
+
每个模型都会记住它上次使用的强度(按 profile 持久化),切回时自动恢复,
|
|
25
|
+
首次切换的模型则回退到它自己的默认值——没有默认值时取第一个可选值,
|
|
26
|
+
绝不出现空的 "unknown" 选择
|
|
24
27
|
- **权限预设**:read-only / workspace-write / full-access 三种会话模式
|
|
25
28
|
- **审批**:工具调用弹出原生 allow-once / reject-once 审批
|
|
26
29
|
|
|
@@ -196,7 +199,7 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
196
199
|
|---|---|
|
|
197
200
|
| `exec: dsh: not found`(status 127) | 用随附 `dsh-acp-zed.sh` 启动器(自定位 node/dsh) |
|
|
198
201
|
| `no API key for provider route "xxx"` | 写入 `~/.dsh/.credentials.yaml`,或在 agent_servers 里设 `env.DEEPSEEK_API_KEY` |
|
|
199
|
-
| 无法切换模型 | 保存的 `reasoning_effort` 默认值(或会话当前 effort
|
|
202
|
+
| 无法切换模型 | 保存的 `reasoning_effort` 默认值(或会话当前 effort)被带到新模型上。0.3.6 起本桥按模型记住上次使用的强度(随 profile 持久化):不被新模型支持的 effort 会被该模型记忆值替换——没有记忆则回退其默认值,再无默认则取第一个可选值,既不会切换失败也不会出现 "unknown"。另检查:是否选到了不可路由的"幽灵 provider"——本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
200
203
|
| 上下文用量不显示 | 选到了不可路由的"幽灵 provider";本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
201
204
|
| 需要详细诊断 | `ACP_DEBUG=1 dsh --profile acp-enhanced`(stderr 生命周期 trace) |
|
|
202
205
|
|
package/README.md
CHANGED
|
@@ -23,7 +23,9 @@ over the ACP wire.
|
|
|
23
23
|
|
|
24
24
|
- **Model switching**: live `provider/model` catalog dropdown (ACP grouped-select wire shape)
|
|
25
25
|
- **Reasoning effort**: `reasoning_effort` dropdown — only when the routed model exposes
|
|
26
|
-
selectable efforts
|
|
26
|
+
selectable efforts; each model remembers the effort it last used (persisted per profile),
|
|
27
|
+
so switching back restores it, and a first-time model falls back to its own default — or
|
|
28
|
+
its first offered effort — instead of an empty "unknown" selection
|
|
27
29
|
- **Permission presets**: read-only / workspace-write / full-access session modes
|
|
28
30
|
- **Approval**: native allow-once / reject-once prompts per tool call
|
|
29
31
|
|
|
@@ -215,7 +217,7 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
215
217
|
|---|---|
|
|
216
218
|
| `exec: dsh: not found` (status 127) | Use the shipped `dsh-acp-zed.sh` launcher (locates node/dsh itself) |
|
|
217
219
|
| `no API key for provider route "xxx"` | Write `~/.dsh/.credentials.yaml`, or set `env.DEEPSEEK_API_KEY` on the agent_servers entry |
|
|
218
|
-
| Cannot switch models | The saved `reasoning_effort` default (or the session's current effort) is carried onto the new model
|
|
220
|
+
| 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 |
|
|
219
221
|
| 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 |
|
|
220
222
|
| Need detailed diagnostics | `ACP_DEBUG=1 dsh --profile acp-enhanced` (stderr lifecycle trace) |
|
|
221
223
|
|
package/lib/index.js
CHANGED
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { randomUUID } from 'node:crypto'
|
|
39
|
-
import {
|
|
39
|
+
import { readFileSync } from 'node:fs'
|
|
40
|
+
import { rm, writeFile } from 'node:fs/promises'
|
|
40
41
|
import { createRequire } from 'node:module'
|
|
41
|
-
import { isAbsolute, dirname, resolve } from 'node:path'
|
|
42
|
+
import { isAbsolute, dirname, join, resolve } from 'node:path'
|
|
42
43
|
import { Readable, Writable } from 'node:stream'
|
|
43
44
|
import Schema from '@deepseek-ai/schemastery'
|
|
44
45
|
import { AgentSideConnection, ndJsonStream, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk'
|
|
@@ -52,6 +53,55 @@ import { acpPromptToText, promptHasUnsupportedContent, sanitizeWireTitle, turnEn
|
|
|
52
53
|
* handshake can never drift from the released package version. */
|
|
53
54
|
const AGENT_VERSION = createRequire(import.meta.url)('../package.json').version
|
|
54
55
|
|
|
56
|
+
/** State-kept per-model reasoning-effort memory (`provider/model` → effort
|
|
57
|
+
* id). A model switch carries the session's current effort onto the new
|
|
58
|
+
* model; when the target does not offer it, the effort this bridge last
|
|
59
|
+
* successfully applied to that route is restored instead — and switching
|
|
60
|
+
* back restores the effort that model had before, so the editor's dropdown
|
|
61
|
+
* never falls back to an empty ("unknown") selection. Persisted as a small
|
|
62
|
+
* JSON file next to the profile (the shipped Zed launcher exports
|
|
63
|
+
* DSH_ACP_PROFILE_DIR); without that variable the memory simply lives for
|
|
64
|
+
* the process. */
|
|
65
|
+
const effortMemoryFile = process.env.DSH_ACP_PROFILE_DIR !== undefined && process.env.DSH_ACP_PROFILE_DIR.length > 0
|
|
66
|
+
? join(process.env.DSH_ACP_PROFILE_DIR, 'dsh-acp-enhanced-effort-memory.json')
|
|
67
|
+
: undefined
|
|
68
|
+
|
|
69
|
+
function loadEffortMemory() {
|
|
70
|
+
if (effortMemoryFile === undefined) return {}
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(readFileSync(effortMemoryFile, 'utf8'))
|
|
73
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error?.code !== 'ENOENT') {
|
|
76
|
+
console.warn(`acp-enhanced: effort memory unreadable (${effortMemoryFile}): ${String(error)}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const effortMemory = loadEffortMemory()
|
|
83
|
+
|
|
84
|
+
function writeEffortMemory() {
|
|
85
|
+
if (effortMemoryFile === undefined) return
|
|
86
|
+
writeFile(effortMemoryFile, JSON.stringify(effortMemory, null, 2)).catch((error) => {
|
|
87
|
+
console.warn(`acp-enhanced: effort memory write failed (${effortMemoryFile}): ${String(error)}`)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The effort last successfully applied to a route, when one was recorded. */
|
|
92
|
+
function rememberedEffort(provider, model) {
|
|
93
|
+
const effort = effortMemory[`${provider}/${model}`]
|
|
94
|
+
return typeof effort === 'string' && effort.length > 0 ? effort : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Record the effort applied to a route, persisting when possible. */
|
|
98
|
+
function rememberEffort(provider, model, effort) {
|
|
99
|
+
const key = `${provider}/${model}`
|
|
100
|
+
if (effortMemory[key] === effort) return
|
|
101
|
+
effortMemory[key] = effort
|
|
102
|
+
writeEffortMemory()
|
|
103
|
+
}
|
|
104
|
+
|
|
55
105
|
export const name = 'acp-enhanced'
|
|
56
106
|
/** The bridge creates and owns agents; every other concern is carried by the composition. */
|
|
57
107
|
export const inject = ['agents', 'llm', 'approval', 'tools', 'commands', 'systemPrompt']
|
|
@@ -858,7 +908,11 @@ export function apply(ctx, config) {
|
|
|
858
908
|
name: 'Reasoning effort',
|
|
859
909
|
description: 'Reasoning level applied to model requests for this session.',
|
|
860
910
|
category: 'thought_level',
|
|
861
|
-
|
|
911
|
+
// The chain mirrors applySelection: selected effort, the model's own
|
|
912
|
+
// default, then the first offered effort — a non-empty selection
|
|
913
|
+
// unless the model declares no reasoning at all. This backstop keeps
|
|
914
|
+
// the editor's dropdown from ever showing an "unknown" effort.
|
|
915
|
+
currentValue: selected.reasoningEffort ?? modelInfo?.reasoning?.defaultEffort ?? efforts[0]?.id ?? '',
|
|
862
916
|
options: efforts.map((effort) => ({
|
|
863
917
|
value: effort.id,
|
|
864
918
|
name: effort.name,
|
|
@@ -901,28 +955,49 @@ export function apply(ctx, config) {
|
|
|
901
955
|
|
|
902
956
|
/** Resolve and apply a new model/effort selection to the session. */
|
|
903
957
|
async function applySelection(record, next) {
|
|
904
|
-
|
|
958
|
+
// Per-model effort restoration. An explicit effort (Zed re-applies its
|
|
959
|
+
// saved default_config_options on every session, and a model switch
|
|
960
|
+
// carries the session's current effort onto the newly picked model) wins
|
|
961
|
+
// when the target offers it. Unsupported efforts are not just dropped:
|
|
962
|
+
// the effort last successfully applied to this route is restored, and a
|
|
963
|
+
// first-time route falls back to its own default — or, when it declares
|
|
964
|
+
// none, to the FIRST effort it offers — so the selection never carries an
|
|
965
|
+
// empty effort that renders as "unknown" in the editor's dropdown. A
|
|
966
|
+
// model without reasoning metadata resolves without an effort.
|
|
967
|
+
let info
|
|
905
968
|
try {
|
|
906
|
-
|
|
907
|
-
} catch
|
|
908
|
-
|
|
909
|
-
// re-applies its saved `default_config_options` on every session, and
|
|
910
|
-
// switching models carries the session's current effort onto the newly
|
|
911
|
-
// picked model, which may not offer it (glm-* and gpt-5.6-luna reject
|
|
912
|
-
// "max"). Rejecting the whole switch snapped Zed's dropdown back and
|
|
913
|
-
// made model switching itself appear broken, so drop the stale effort
|
|
914
|
-
// and resolve against the target model's own default instead. Any other
|
|
915
|
-
// failure (unknown route, missing credential) still surfaces.
|
|
916
|
-
if (error?.code !== 'UNSUPPORTED_REASONING_EFFORT' || next.reasoningEffort === undefined) throw error
|
|
917
|
-
logger.warn(`acp-enhanced: dropped reasoning effort "${next.reasoningEffort}" for ${next.provider}/${next.model} (unsupported); using the model default`)
|
|
918
|
-
resolved = await llm.resolveCallConfig({ ...next, reasoningEffort: undefined })
|
|
969
|
+
info = await llm.resolveModelInfo(next.provider, next.model)
|
|
970
|
+
} catch {
|
|
971
|
+
info = undefined
|
|
919
972
|
}
|
|
973
|
+
const efforts = info?.reasoning?.efforts ?? []
|
|
974
|
+
let candidate = next.reasoningEffort
|
|
975
|
+
if (candidate !== undefined && !efforts.some((effort) => effort.id === candidate)) {
|
|
976
|
+
logger.warn(`acp-enhanced: dropped reasoning effort "${candidate}" for ${next.provider}/${next.model} (unsupported)`)
|
|
977
|
+
candidate = undefined
|
|
978
|
+
}
|
|
979
|
+
if (candidate === undefined && efforts.length > 0) {
|
|
980
|
+
const remembered = rememberedEffort(next.provider, next.model)
|
|
981
|
+
candidate = remembered !== undefined && efforts.some((effort) => effort.id === remembered)
|
|
982
|
+
? remembered
|
|
983
|
+
: info?.reasoning?.defaultEffort ?? efforts[0].id
|
|
984
|
+
}
|
|
985
|
+
const resolved = await llm.resolveCallConfig(
|
|
986
|
+
candidate === undefined
|
|
987
|
+
? { provider: next.provider, model: next.model }
|
|
988
|
+
: { ...next, reasoningEffort: candidate },
|
|
989
|
+
)
|
|
920
990
|
const selected = {
|
|
921
991
|
provider: resolved.provider,
|
|
922
992
|
model: resolved.model,
|
|
923
993
|
...resolved.reasoningEffort === undefined ? {} : { reasoningEffort: resolved.reasoningEffort },
|
|
924
994
|
}
|
|
925
995
|
record.selection.current = selected
|
|
996
|
+
// Remember the effective effort for this route so a later switch back
|
|
997
|
+
// restores it (a model without reasoning metadata records nothing).
|
|
998
|
+
if (selected.reasoningEffort !== undefined) {
|
|
999
|
+
rememberEffort(selected.provider, selected.model, selected.reasoningEffort)
|
|
1000
|
+
}
|
|
926
1001
|
// Persist as the default only when the chosen route is one this bridge is
|
|
927
1002
|
// wired to serve. On a single-provider machine the default route is the
|
|
928
1003
|
// routable one, so switching within it always persists; a stray cross-provider
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-acp-enhanced",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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",
|
package/scripts/dsh-acp-zed.sh
CHANGED
|
@@ -61,4 +61,15 @@ if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
|
|
|
61
61
|
fi
|
|
62
62
|
fi
|
|
63
63
|
|
|
64
|
+
# Derive the profile root from this launcher's own location:
|
|
65
|
+
# …/profiles/<name>/node_modules/<pkg>/scripts/dsh-acp-zed.sh
|
|
66
|
+
# — three levels above this script is the profile dir. pwd -L keeps the
|
|
67
|
+
# logical path (pnpm lays out node_modules as symlinks), so the climb lands
|
|
68
|
+
# on the profile root. The bridge persists small per-model state (the last
|
|
69
|
+
# reasoning effort per model) next to the profile's own files.
|
|
70
|
+
if [ -z "${DSH_ACP_PROFILE_DIR:-}" ]; then
|
|
71
|
+
ACP_LAUNCHER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -L)"
|
|
72
|
+
export DSH_ACP_PROFILE_DIR="$(cd "${ACP_LAUNCHER_DIR}/../../.." && pwd -L)"
|
|
73
|
+
fi
|
|
74
|
+
|
|
64
75
|
exec "${DASH_BIN}" --profile acp-enhanced "$@"
|