dsh-acp-enhanced 0.2.1 → 0.3.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-en.md +8 -0
- package/README.md +7 -0
- package/lib/index.js +130 -1
- package/package.json +1 -1
package/README-en.md
CHANGED
|
@@ -44,6 +44,14 @@ over the ACP wire.
|
|
|
44
44
|
`session/list` / `session/delete` manage the thread archive (titled, sorted by last
|
|
45
45
|
activity); live title updates
|
|
46
46
|
|
|
47
|
+
### Commands
|
|
48
|
+
|
|
49
|
+
- **Slash commands**: typing `/` reveals the command list (`available_commands_update`):
|
|
50
|
+
`/status` shows the route and telemetry, `/model` lists or switches the model, everything
|
|
51
|
+
else (`/compact` `/goal` `/permission` `/plan`…) runs straight through the harness
|
|
52
|
+
command registry — all executed **without a model turn**; unresolved slashes fall
|
|
53
|
+
through to the model (the `/skill-name` skill gesture)
|
|
54
|
+
|
|
47
55
|
### MCP
|
|
48
56
|
|
|
49
57
|
- **MCP servers**: `session/new` `mcpServers` mount any MCP server (stdio + streamable
|
package/README.md
CHANGED
|
@@ -38,6 +38,13 @@ ACP 线上。
|
|
|
38
38
|
- **恢复与归档**:`session/load` 恢复历史线程(完整回放);`session/list` /
|
|
39
39
|
`session/delete` 管理线程归档(带标题、按更新时间排序);标题实时推送
|
|
40
40
|
|
|
41
|
+
### 命令
|
|
42
|
+
|
|
43
|
+
- **Slash 命令**:输入 `/` 即可见命令列表(`available_commands_update`):`/status`
|
|
44
|
+
查看路由与遥测、`/model` 列出或切换模型,其余(`/compact` `/goal` `/permission`
|
|
45
|
+
`/plan`…)直通 harness 命令注册表,全部**不经过模型 turn** 即时执行;未解析的
|
|
46
|
+
slash 放行给模型(`/skill-name` 技能手势)
|
|
47
|
+
|
|
41
48
|
### MCP
|
|
42
49
|
|
|
43
50
|
- **MCP servers**:`session/new` 的 `mcpServers` 挂载任意 MCP server(stdio +
|
package/lib/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import { acpPromptToText, promptHasUnsupportedContent, turnEndToStopReason, usag
|
|
|
43
43
|
|
|
44
44
|
export const name = 'acp-enhanced'
|
|
45
45
|
/** The bridge creates and owns agents; every other concern is carried by the composition. */
|
|
46
|
-
export const inject = ['agents', 'llm', 'approval', 'tools']
|
|
46
|
+
export const inject = ['agents', 'llm', 'approval', 'tools', 'commands']
|
|
47
47
|
|
|
48
48
|
export const Config = Schema.object({
|
|
49
49
|
/** Initial provider route for every created agent. */
|
|
@@ -121,6 +121,7 @@ export function apply(ctx, config) {
|
|
|
121
121
|
const llm = ctx.llm
|
|
122
122
|
const approval = ctx.approval
|
|
123
123
|
const tools = ctx.tools
|
|
124
|
+
const commands = ctx.commands
|
|
124
125
|
const logger = ctx.logger
|
|
125
126
|
/** The user-questions service (mounted by dsh-base); absent in minimal deployments. */
|
|
126
127
|
const userQuestions = ctx.get('userQuestions')
|
|
@@ -1001,6 +1002,94 @@ export function apply(ctx, config) {
|
|
|
1001
1002
|
}
|
|
1002
1003
|
}
|
|
1003
1004
|
|
|
1005
|
+
// ── slash commands (adapter built-ins + harness command registry) ────────
|
|
1006
|
+
|
|
1007
|
+
/** Adapter-level commands, always first in the advertised list. */
|
|
1008
|
+
const BUILTIN_COMMANDS = [
|
|
1009
|
+
{ name: 'status', description: 'Show session status: model route, context usage, telemetry.' },
|
|
1010
|
+
{ name: 'model', description: 'List the model catalog, or switch with /model <provider/model | substring>.' },
|
|
1011
|
+
]
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Advertise the full command surface for a session as ACP
|
|
1015
|
+
* `available_commands_update`: adapter built-ins plus whatever the harness
|
|
1016
|
+
* command registry (compact/goal/permission/plan/…) serves for the agent.
|
|
1017
|
+
*/
|
|
1018
|
+
async function publishCommands(record) {
|
|
1019
|
+
const list = [...BUILTIN_COMMANDS]
|
|
1020
|
+
const seen = new Set(list.map((command) => command.name))
|
|
1021
|
+
try {
|
|
1022
|
+
for (const descriptor of commands.list(record.agent)) {
|
|
1023
|
+
if (seen.has(descriptor.name)) continue
|
|
1024
|
+
seen.add(descriptor.name)
|
|
1025
|
+
list.push({
|
|
1026
|
+
name: descriptor.name,
|
|
1027
|
+
description: descriptor.description,
|
|
1028
|
+
...descriptor.input === undefined ? {} : { input: descriptor.input },
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
logger.warn(`acp-enhanced: command listing failed: ${String(error)}`)
|
|
1033
|
+
}
|
|
1034
|
+
notify({
|
|
1035
|
+
sessionId: record.agent.session.id,
|
|
1036
|
+
update: { sessionUpdate: 'available_commands_update', availableCommands: list },
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** Text summary of one session: route, turns, last usage telemetry. */
|
|
1041
|
+
async function statusText(record) {
|
|
1042
|
+
const selection = record.selection.current
|
|
1043
|
+
const last = record.lastUsage
|
|
1044
|
+
const lines = [
|
|
1045
|
+
`route: ${selection.provider ?? '?'}/${selection.model ?? '?'}`,
|
|
1046
|
+
`turns: ${record.turnCount}`,
|
|
1047
|
+
...last === undefined ? [] : [
|
|
1048
|
+
`context: ${last.used}/${last.size}`,
|
|
1049
|
+
`last usage: in ${last.meta.inputTokens ?? '?'} / out ${last.meta.outputTokens ?? '?'} / reasoning ${last.meta.reasoningTokens ?? '?'} tokens, cache hit ${last.meta.cacheHitRate ?? '?'}%, tps ${last.meta.tps ?? '?'}`,
|
|
1050
|
+
],
|
|
1051
|
+
]
|
|
1052
|
+
return lines.join('\n')
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** The /model command: list the live catalog or switch by exact id/substring. */
|
|
1056
|
+
async function modelCommandText(record, query) {
|
|
1057
|
+
const catalog = await modelCatalog()
|
|
1058
|
+
const current = record.selection.current
|
|
1059
|
+
if (query.trim().length === 0) {
|
|
1060
|
+
const lines = catalog.flatMap((group) => group.models.map((model) => {
|
|
1061
|
+
const mark = `${group.id}/${model.id}` === `${current.provider}/${current.model}` ? '* ' : ' '
|
|
1062
|
+
return `${mark}${group.id}/${model.id}`
|
|
1063
|
+
}))
|
|
1064
|
+
return lines.length > 0 ? lines.join('\n') : 'no models available'
|
|
1065
|
+
}
|
|
1066
|
+
const needle = query.trim().toLowerCase()
|
|
1067
|
+
const matches = catalog.flatMap((group) => group.models.map((model) => ({ provider: group.id, model: model.id })))
|
|
1068
|
+
.filter((candidate) => `${candidate.provider}/${candidate.model}`.toLowerCase().includes(needle) || candidate.model.toLowerCase().includes(needle))
|
|
1069
|
+
if (matches.length === 0) return `no model matches "${query}"`
|
|
1070
|
+
if (matches.length > 1) return `ambiguous: ${matches.map((m) => `${m.provider}/${m.model}`).join(', ')}`
|
|
1071
|
+
await applySelection(record, { provider: matches[0].provider, model: matches[0].model })
|
|
1072
|
+
return `switched to ${matches[0].provider}/${matches[0].model}`
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Refresh every client-visible surface a command may have mutated. */
|
|
1076
|
+
function refreshAfterCommand(record) {
|
|
1077
|
+
const permission = permissionPresets()
|
|
1078
|
+
if (permission !== undefined) {
|
|
1079
|
+
notify({
|
|
1080
|
+
sessionId: record.agent.session.id,
|
|
1081
|
+
update: {
|
|
1082
|
+
sessionUpdate: 'current_mode_update',
|
|
1083
|
+
currentModeId: permission.current(record.agent.session.events),
|
|
1084
|
+
},
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
broadcastConfig(record).catch((error) => {
|
|
1088
|
+
logger.warn(`acp-enhanced: config rebroadcast after command failed: ${String(error)}`)
|
|
1089
|
+
})
|
|
1090
|
+
publishCommands(record)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1004
1093
|
// ── session records + history replay ─────────────────────────────────────
|
|
1005
1094
|
|
|
1006
1095
|
/** Build the bridge-owned protocol record for a fresh or resumed agent. */
|
|
@@ -1238,6 +1327,7 @@ export function apply(ctx, config) {
|
|
|
1238
1327
|
const record = makeRecord(handle)
|
|
1239
1328
|
sessions.set(sessionId, record)
|
|
1240
1329
|
await syncMcpServers(params.mcpServers, params.cwd)
|
|
1330
|
+
publishCommands(record)
|
|
1241
1331
|
const permission = permissionPresets()
|
|
1242
1332
|
return {
|
|
1243
1333
|
sessionId,
|
|
@@ -1299,6 +1389,7 @@ export function apply(ctx, config) {
|
|
|
1299
1389
|
// Zed inserts the thread before the load RPC completes; replay the
|
|
1300
1390
|
// conversation history as notifications so the thread renders.
|
|
1301
1391
|
await replayHistory(record)
|
|
1392
|
+
publishCommands(record)
|
|
1302
1393
|
const permission = permissionPresets()
|
|
1303
1394
|
return {
|
|
1304
1395
|
...permission === undefined ? {} : {
|
|
@@ -1329,6 +1420,44 @@ export function apply(ctx, config) {
|
|
|
1329
1420
|
}
|
|
1330
1421
|
const text = acpPromptToText(params.prompt)
|
|
1331
1422
|
if (text.trim().length === 0) throw invalidParams('empty prompt')
|
|
1423
|
+
|
|
1424
|
+
// Adapter-level slash commands never reach the model: /status and
|
|
1425
|
+
// /model are built in, any other registered slash (compact/goal/
|
|
1426
|
+
// permission/plan/…) runs through the harness command registry
|
|
1427
|
+
// without a model turn. An unresolved slash falls through — the
|
|
1428
|
+
// /skill-name gesture is claimed inside the agent's next step.
|
|
1429
|
+
const trimmed = text.trim()
|
|
1430
|
+
const commandMatch = trimmed.match(/^\/(\w[\w-]*)\b/)
|
|
1431
|
+
const respond = (reply) => {
|
|
1432
|
+
notify({
|
|
1433
|
+
sessionId: record.agent.session.id,
|
|
1434
|
+
update: {
|
|
1435
|
+
sessionUpdate: 'agent_message_chunk',
|
|
1436
|
+
messageId: randomUUID(),
|
|
1437
|
+
content: { type: 'text', text: reply },
|
|
1438
|
+
},
|
|
1439
|
+
})
|
|
1440
|
+
return { stopReason: 'end_turn' }
|
|
1441
|
+
}
|
|
1442
|
+
if (commandMatch?.[1] === 'status') return respond(await statusText(record))
|
|
1443
|
+
if (commandMatch?.[1] === 'model') {
|
|
1444
|
+
return respond(await modelCommandText(record, trimmed.slice(commandMatch[0].length).trim()))
|
|
1445
|
+
}
|
|
1446
|
+
if (commandMatch !== null && commandMatch[1] !== undefined) {
|
|
1447
|
+
let execution
|
|
1448
|
+
try {
|
|
1449
|
+
execution = await commands.execute(record.agent, trimmed, new AbortController().signal)
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
return respond(`⚠ /${commandMatch[1]} failed: ${error.message ?? String(error)}`)
|
|
1452
|
+
}
|
|
1453
|
+
if (execution !== undefined) {
|
|
1454
|
+
const { result } = execution
|
|
1455
|
+
const reply = result.text ?? (result.kind === 'success' ? `/${commandMatch[1]} ✓` : `/${commandMatch[1]} failed`)
|
|
1456
|
+
refreshAfterCommand(record)
|
|
1457
|
+
return respond(result.kind === 'error' ? `⚠ ${reply}` : reply)
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1332
1461
|
if (ctx.agents.get(record.agent.id) !== record.agent) {
|
|
1333
1462
|
throw internalError('prompt was not queued: the agent was disposed outside the bridge')
|
|
1334
1463
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-acp-enhanced",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|