dsh-plugin-subagent-bridge 0.7.4 → 0.8.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.md +20 -0
- package/index.js +125 -7
- package/package.json +1 -1
- package/skills/deepseek-worker/SKILL.md +27 -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.1",
|
|
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": [
|
|
@@ -121,6 +121,33 @@ DSH **自带** `subagent` 工具(和这座桥无关,是它自己的能力,
|
|
|
121
121
|
|
|
122
122
|
DSH 自己的委派深度上限是 1:worker(depth 0)能开子 agent(depth 1),子 agent 不能再往下开。所以不会无限递归。
|
|
123
123
|
|
|
124
|
+
## 指定 worker 用哪个模型
|
|
125
|
+
|
|
126
|
+
`dsh_task`、`dsh_spawn`、`dsh_followup` 都支持 `model`。默认不传,worker 跟随会话默认模型。
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{ "prompt": "…", "cwd": "/repo", "model": "zai-coding-cn/glm-5.3", "reasoningEffort": "max" }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- 写法是 **`provider/model`**,中间一个斜杠;
|
|
133
|
+
- **只写模型名**(`"glm-5.3"`)会沿用当前默认 provider——省事但容易踩空,见下;
|
|
134
|
+
- `reasoningEffort` 可选,取值随模型(`low` / `medium` / `high` / `max` 等),模型不支持该档会直接报错;
|
|
135
|
+
- 不传 `model` 的调用行为完全不变,不要为了"保险"而乱传。
|
|
136
|
+
|
|
137
|
+
什么时候值得指定:
|
|
138
|
+
|
|
139
|
+
| 场景 | 做法 |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| 任务要长上下文 / 大文件 | 挑窗口大的模型 |
|
|
142
|
+
| 要跨模型复核同一件事 | 第二次派发换另一家的模型 |
|
|
143
|
+
| 便宜模型够用(改文档、跑格式) | 显式指定小模型省钱 |
|
|
144
|
+
| 用同一个 worker 干不同类型活 | `dsh_followup` 里换 `model` |
|
|
145
|
+
|
|
146
|
+
**只写模型名的坑**:`glm-5.3`、`deepseek-v4.1-flash` 这类模型同时挂在多个 provider 下(`zai-coding-cn`、`opencode-go`、`newapi`…),只写名字会落到当前默认 provider 上,可能和你想的不是同一家。**要么写全 `provider/model`,要么先确认当前默认是什么。**
|
|
147
|
+
|
|
148
|
+
写错了(`/glm-5.3`、`a/b/c`)桥会在**发 prompt 之前**报错,不会白跑一轮。
|
|
149
|
+
|
|
150
|
+
|
|
124
151
|
## 怎么写 prompt
|
|
125
152
|
|
|
126
153
|
worker **看不到你这段对话**,它只有 `prompt` 和 `cwd`。写成可以独立开工的工单:
|