dsh-plugin-subagent-bridge 0.7.3 → 0.8.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.md +20 -0
- package/index.js +125 -7
- package/package.json +1 -1
- package/skills/deepseek-worker/SKILL.md +17 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
5
|
+
<!-- M8ven ownership proof — keep this line in place; it verifies the publisher. -->
|
|
6
|
+
[](https://m8ven.ai/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?s=readme)
|
|
5
7
|
|
|
6
8
|
在 DeepSeek Harness 宿主内部提供**本机回环 MCP 端点**,让另一个智能体(如 Codex)把任务派给 DSH 会话并取回结果。
|
|
7
9
|
|
|
@@ -9,6 +11,23 @@
|
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
14
|
+
## 指定模型
|
|
15
|
+
|
|
16
|
+
`dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
dsh_task({ prompt: "…", model: "zai-coding-cn/glm-5.3", reasoningEffort: "max" })
|
|
20
|
+
dsh_spawn({ task_name: "review", message: "…", model: "opencode-go/deepseek-v4.1-flash" })
|
|
21
|
+
dsh_followup({ worker: "review", message: "…", model: "deepseek-account/deepseek-flash" })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 写法是 `provider/model`;**只写模型名**时沿用当前默认 provider(`model: "glm-5.3"`);
|
|
25
|
+
- `reasoningEffort` 可选,取值随模型(`low` / `medium` / `high` / `max` 等);
|
|
26
|
+
- 不传 `model` 就完全不动会话,行为和以前一致;
|
|
27
|
+
- 实测(Host 侧 `sessionController.selectModel`):请求头里的 provider/model 确实变成指定的那组,且**不会改写 profile 的默认模型**。
|
|
28
|
+
|
|
29
|
+
实现上就是在建好会话、发第一个 prompt **之前**调一次 `selectModel`,所以 worker 的第一轮就已经在新模型上跑。
|
|
30
|
+
|
|
12
31
|
## 工具
|
|
13
32
|
|
|
14
33
|
| 工具 | 作用 |
|
|
@@ -246,6 +265,7 @@ node --test "test/*.test.mjs"
|
|
|
246
265
|
| [dsh-plugin-composer-history](https://github.com/Dogtiti/dsh-plugin-composer-history) | 输入框中按 `↑` / `↓` 回填历史消息 |
|
|
247
266
|
| **dsh-plugin-subagent-bridge**(本仓库) | 让 Codex 通过本机 MCP 把 DSH 当 subagent 用(spawn / wait / followup),反向也能把任务派给 Codex |
|
|
248
267
|
| [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) | 在模型选择器旁显示供应商套餐额度余量(目前 GLM Coding Plan),点开是明细浮层 |
|
|
268
|
+
| [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | 在模型名旁显示供应商品牌 logo(24 个厂商,支持前缀匹配) |
|
|
249
269
|
|
|
250
270
|
|
|
251
271
|
## 许可
|
package/index.js
CHANGED
|
@@ -94,6 +94,15 @@ const TOOLS = [
|
|
|
94
94
|
description: 'Continue an existing idle session (multi-turn worker) instead of opening a new one.',
|
|
95
95
|
},
|
|
96
96
|
agentPreset: { type: 'string', description: 'Agent preset for a new session.' },
|
|
97
|
+
model: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description:
|
|
100
|
+
'Model to run this worker on, as "provider/model" (for example "zai-coding-cn/glm-5.3"). A bare model id keeps the current default provider. Omit to use whatever the session already uses.',
|
|
101
|
+
},
|
|
102
|
+
reasoningEffort: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Reasoning level for `model`, if that model offers levels (low, medium, high, max...).',
|
|
105
|
+
},
|
|
97
106
|
timeoutSeconds: { type: 'number', description: 'How long to wait for the turn to finish. Default 300, max 600.' },
|
|
98
107
|
},
|
|
99
108
|
required: ['prompt'],
|
|
@@ -116,6 +125,12 @@ const TOOLS = [
|
|
|
116
125
|
},
|
|
117
126
|
from: { type: 'string', description: 'Session id to fork when fork_turns is "all".' },
|
|
118
127
|
cwd: { type: 'string', description: 'Working directory for a fresh worker.' },
|
|
128
|
+
model: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description:
|
|
131
|
+
'Model for this worker, as "provider/model" (for example "opencode-go/deepseek-v4.1-flash"). A bare model id keeps the current default provider.',
|
|
132
|
+
},
|
|
133
|
+
reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
|
|
119
134
|
},
|
|
120
135
|
required: ['task_name', 'message'],
|
|
121
136
|
additionalProperties: false,
|
|
@@ -130,6 +145,11 @@ const TOOLS = [
|
|
|
130
145
|
properties: {
|
|
131
146
|
worker: { type: 'string', description: 'task_name from dsh_spawn, or a session id.' },
|
|
132
147
|
message: { type: 'string', description: 'Follow-up work order.' },
|
|
148
|
+
model: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'Switch this worker to another model before the turn, as "provider/model". Omit to keep its current one.',
|
|
151
|
+
},
|
|
152
|
+
reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
|
|
133
153
|
},
|
|
134
154
|
required: ['worker', 'message'],
|
|
135
155
|
additionalProperties: false,
|
|
@@ -483,12 +503,37 @@ async function callTool(id, params, api, signal) {
|
|
|
483
503
|
if (forkTurns === 'all' && (typeof args.from !== 'string' || args.from === '')) {
|
|
484
504
|
throw new Error('from is required when fork_turns is "all"')
|
|
485
505
|
}
|
|
486
|
-
return ok(
|
|
506
|
+
return ok(
|
|
507
|
+
id,
|
|
508
|
+
text(
|
|
509
|
+
await api.spawn({
|
|
510
|
+
taskName,
|
|
511
|
+
message,
|
|
512
|
+
forkTurns,
|
|
513
|
+
from: args.from,
|
|
514
|
+
cwd: args.cwd,
|
|
515
|
+
model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
|
|
516
|
+
reasoningEffort:
|
|
517
|
+
typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
|
|
518
|
+
}),
|
|
519
|
+
),
|
|
520
|
+
)
|
|
487
521
|
}
|
|
488
522
|
case 'dsh_followup': {
|
|
489
523
|
const worker = requireString(args.worker, 'worker')
|
|
490
524
|
const message = requireString(args.message, 'message')
|
|
491
|
-
return ok(
|
|
525
|
+
return ok(
|
|
526
|
+
id,
|
|
527
|
+
text(
|
|
528
|
+
await api.followup({
|
|
529
|
+
worker,
|
|
530
|
+
message,
|
|
531
|
+
model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
|
|
532
|
+
reasoningEffort:
|
|
533
|
+
typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
|
|
534
|
+
}),
|
|
535
|
+
),
|
|
536
|
+
)
|
|
492
537
|
}
|
|
493
538
|
case 'dsh_close': {
|
|
494
539
|
const worker = requireString(args.worker, 'worker')
|
|
@@ -520,6 +565,9 @@ async function callTool(id, params, api, signal) {
|
|
|
520
565
|
cwd: typeof args.cwd === 'string' && args.cwd !== '' ? args.cwd : undefined,
|
|
521
566
|
sessionId,
|
|
522
567
|
agentPreset: typeof args.agentPreset === 'string' && args.agentPreset !== '' ? args.agentPreset : undefined,
|
|
568
|
+
model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
|
|
569
|
+
reasoningEffort:
|
|
570
|
+
typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
|
|
523
571
|
timeoutSeconds,
|
|
524
572
|
}),
|
|
525
573
|
),
|
|
@@ -591,7 +639,7 @@ export function createApi(ctx, state, settings) {
|
|
|
591
639
|
})
|
|
592
640
|
},
|
|
593
641
|
|
|
594
|
-
async spawn({ taskName, message, forkTurns, from, cwd }) {
|
|
642
|
+
async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort }) {
|
|
595
643
|
const service = controller()
|
|
596
644
|
if (service === undefined || service === null) throw new Error('session controller is unavailable')
|
|
597
645
|
if (state.workers.has(taskName)) throw new Error(`task_name ${JSON.stringify(taskName)} is already in use`)
|
|
@@ -616,6 +664,7 @@ export function createApi(ctx, state, settings) {
|
|
|
616
664
|
if (typeof sessionId !== 'string' || sessionId === '') throw new Error('session controller did not return a session id')
|
|
617
665
|
}
|
|
618
666
|
|
|
667
|
+
const appliedModel = await applyModelSpec(ctx, sessionId, model, reasoningEffort)
|
|
619
668
|
const before = Number(sessions()?.get?.(sessionId)?.seq ?? 0)
|
|
620
669
|
const requestId = randomUUID()
|
|
621
670
|
const accepted = await service.prompt(
|
|
@@ -634,15 +683,22 @@ export function createApi(ctx, state, settings) {
|
|
|
634
683
|
updatedAt: Date.now(),
|
|
635
684
|
})
|
|
636
685
|
await saveWorkers(settings, state)
|
|
637
|
-
return {
|
|
686
|
+
return {
|
|
687
|
+
task_name: taskName,
|
|
688
|
+
sessionId,
|
|
689
|
+
accepted: accepted?.accepted === true,
|
|
690
|
+
fork_turns: forkTurns,
|
|
691
|
+
...(appliedModel === undefined ? {} : { model: appliedModel }),
|
|
692
|
+
}
|
|
638
693
|
},
|
|
639
694
|
|
|
640
|
-
async followup({ worker, message }) {
|
|
695
|
+
async followup({ worker, message, model, reasoningEffort }) {
|
|
641
696
|
const target = resolveWorker(state, worker)
|
|
642
697
|
if (target === undefined) throw new Error(`unknown worker ${JSON.stringify(worker)}`)
|
|
643
698
|
const service = controller()
|
|
644
699
|
if (service === undefined || service === null) throw new Error('session controller is unavailable')
|
|
645
700
|
takeDispatchSlot(state, settings)
|
|
701
|
+
const appliedModel = await applyModelSpec(ctx, target.sessionId, model, reasoningEffort)
|
|
646
702
|
const before = Number(sessions()?.get?.(target.sessionId)?.seq ?? 0)
|
|
647
703
|
const requestId = randomUUID()
|
|
648
704
|
const accepted = await service.prompt(
|
|
@@ -656,7 +712,12 @@ export function createApi(ctx, state, settings) {
|
|
|
656
712
|
target.record.updatedAt = Date.now()
|
|
657
713
|
}
|
|
658
714
|
await saveWorkers(settings, state)
|
|
659
|
-
return {
|
|
715
|
+
return {
|
|
716
|
+
worker: target.taskName ?? target.sessionId,
|
|
717
|
+
sessionId: target.sessionId,
|
|
718
|
+
accepted: accepted?.accepted === true,
|
|
719
|
+
...(appliedModel === undefined ? {} : { model: appliedModel }),
|
|
720
|
+
}
|
|
660
721
|
},
|
|
661
722
|
|
|
662
723
|
async close({ worker }) {
|
|
@@ -680,7 +741,7 @@ export function createApi(ctx, state, settings) {
|
|
|
680
741
|
return rows
|
|
681
742
|
},
|
|
682
743
|
|
|
683
|
-
async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds }) {
|
|
744
|
+
async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort }) {
|
|
684
745
|
const service = controller()
|
|
685
746
|
if (service === undefined || service === null) throw new Error('session controller is unavailable')
|
|
686
747
|
const seconds = Math.min(
|
|
@@ -703,6 +764,7 @@ export function createApi(ctx, state, settings) {
|
|
|
703
764
|
throw new Error('that session is running; use dsh_dispatch plus dsh_read_result instead')
|
|
704
765
|
}
|
|
705
766
|
|
|
767
|
+
const appliedModel = await applyModelSpec(ctx, targetId, model, reasoningEffort)
|
|
706
768
|
const turnBefore = lastTurn(ctx, targetId)
|
|
707
769
|
const before = Number(sessions()?.get?.(targetId)?.seq ?? 0)
|
|
708
770
|
const finished = waitForTurnEnd(ctx, targetId, turnBefore, seconds * 1000)
|
|
@@ -745,6 +807,7 @@ export function createApi(ctx, state, settings) {
|
|
|
745
807
|
next: `the worker is still running; do not end the turn. Call dsh_wait({targets:["${targetId}"],timeoutMs:${String(MAX_BLOCKING_SECONDS * 1000)}}) and repeat until it is not "running".`,
|
|
746
808
|
}
|
|
747
809
|
: {}),
|
|
810
|
+
...(appliedModel === undefined ? {} : { model: appliedModel }),
|
|
748
811
|
text: collected.text,
|
|
749
812
|
truncated: collected.truncated,
|
|
750
813
|
turns: lastTurn(ctx, targetId),
|
|
@@ -1014,6 +1077,61 @@ function callerDepth(state, sessionId) {
|
|
|
1014
1077
|
}
|
|
1015
1078
|
|
|
1016
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Read a `provider/model` spec. A bare model name keeps the provider of the current
|
|
1082
|
+
* default selection, so callers can say `model: "glm-5.3"` without repeating the
|
|
1083
|
+
* provider they already run.
|
|
1084
|
+
* @param ctx - Host context.
|
|
1085
|
+
* @param spec - `provider/model` or a bare model id.
|
|
1086
|
+
* @returns {{ provider: string, model: string }} resolved pair.
|
|
1087
|
+
*/
|
|
1088
|
+
function parseModelSpec(ctx, spec) {
|
|
1089
|
+
const raw = String(spec).trim()
|
|
1090
|
+
if (raw === '') throw new Error('model must not be empty')
|
|
1091
|
+
if (raw.startsWith('/') || raw.endsWith('/') || raw.includes('//')) {
|
|
1092
|
+
throw new Error(`model must look like "provider/model", got ${JSON.stringify(spec)}`)
|
|
1093
|
+
}
|
|
1094
|
+
const slash = raw.indexOf('/')
|
|
1095
|
+
if (slash > 0) {
|
|
1096
|
+
const provider = raw.slice(0, slash).trim()
|
|
1097
|
+
const model = raw.slice(slash + 1).trim()
|
|
1098
|
+
if (provider === '' || model === '') throw new Error(`model must look like "provider/model", got ${JSON.stringify(spec)}`)
|
|
1099
|
+
return { provider, model }
|
|
1100
|
+
}
|
|
1101
|
+
const current = ctx.get('agentDefaultModel')?.currentSelection?.()
|
|
1102
|
+
const provider = typeof current?.provider === 'string' ? current.provider : ''
|
|
1103
|
+
if (provider === '') throw new Error('model without a provider needs a default selection; pass "provider/model"')
|
|
1104
|
+
return { provider, model: raw }
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Install a model selection on one session before its next turn.
|
|
1109
|
+
* `sessionController.selectModel` validates the pair and applies it to the next request.
|
|
1110
|
+
* @param ctx - Host context.
|
|
1111
|
+
* @param sessionId - session to switch.
|
|
1112
|
+
* @param spec - `provider/model` or a bare model id.
|
|
1113
|
+
* @param reasoningEffort - optional effort level.
|
|
1114
|
+
* @returns {{ provider: string, model: string, reasoningEffort?: string } | undefined} applied selection.
|
|
1115
|
+
*/
|
|
1116
|
+
async function applyModelSpec(ctx, sessionId, spec, reasoningEffort) {
|
|
1117
|
+
if (typeof spec !== 'string' || spec.trim() === '') return undefined
|
|
1118
|
+
const service = ctx.get('sessionController')
|
|
1119
|
+
if (service === undefined || service === null) throw new Error('session controller is unavailable')
|
|
1120
|
+
if (typeof service.selectModel !== 'function') throw new Error('this Host does not expose selectModel; update DeepSeek Harness')
|
|
1121
|
+
const { provider, model } = parseModelSpec(ctx, spec)
|
|
1122
|
+
try {
|
|
1123
|
+
const result = await service.selectModel({
|
|
1124
|
+
sessionId,
|
|
1125
|
+
provider,
|
|
1126
|
+
model,
|
|
1127
|
+
...(typeof reasoningEffort === 'string' && reasoningEffort !== '' ? { reasoningEffort } : {}),
|
|
1128
|
+
})
|
|
1129
|
+
return result?.selected ?? { provider, model, ...(reasoningEffort ? { reasoningEffort } : {}) }
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
throw new Error(`cannot switch ${String(sessionId)} to ${provider}/${model}: ${String(error?.message ?? error)}`)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1017
1135
|
/**
|
|
1018
1136
|
* Place a freshly created session in the workspace its directory belongs to.
|
|
1019
1137
|
* `sessionController.create` only records a working directory; the sidebar groups
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subagent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Subagent bridge: lets Codex delegate a task to DeepSeek Harness the way it would spawn a subagent (one call, waits for the answer), plus a codex_dispatch tool for the reverse direction.",
|
|
6
6
|
"keywords": [
|
|
@@ -104,6 +104,23 @@ dsh_spawn({ "task_name": "continue_here", "fork_turns": "all",
|
|
|
104
104
|
`fork_turns: "all"` 会 fork 那个会话作为 worker 的起点(继承到最近一个已完成 turn),worker 的后续改动**不会**污染原会话。默认 `"none"` 是全新空会话。
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
## 让它自己并行(DSH 也有 subagent)
|
|
108
|
+
|
|
109
|
+
DSH **自带** `subagent` 工具(和这座桥无关,是它自己的能力,worker 在深度 0,可以开子 agent)。但它**默认不会用**——不提示就一个人串行干完。
|
|
110
|
+
|
|
111
|
+
任务能拆成互不依赖的几块时,在 prompt 末尾加一句:
|
|
112
|
+
|
|
113
|
+
> 这几块互不依赖,可以用你自己的 subagent 并行处理,最后汇总。
|
|
114
|
+
|
|
115
|
+
实测:4 个互不依赖的乘法 + 这句提示 → 它开了 **4 个 subagent 并行**,43 秒返回汇总,结果全对。不加这句,同样的活它串行做完。
|
|
116
|
+
|
|
117
|
+
判断标准和 Codex 的 proactive 策略一致:
|
|
118
|
+
|
|
119
|
+
- ✅ 子任务互不依赖、并行能省时间或提高质量 → 加这句
|
|
120
|
+
- ❌ 有先后依赖、要改同一批文件、验收标准还没想清 → 不要加,串行更稳
|
|
121
|
+
|
|
122
|
+
DSH 自己的委派深度上限是 1:worker(depth 0)能开子 agent(depth 1),子 agent 不能再往下开。所以不会无限递归。
|
|
123
|
+
|
|
107
124
|
## 怎么写 prompt
|
|
108
125
|
|
|
109
126
|
worker **看不到你这段对话**,它只有 `prompt` 和 `cwd`。写成可以独立开工的工单:
|