dsh-plugin-subagent-bridge 0.7.4 → 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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/index.js +125 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
4
4
  [![dsh](https://img.shields.io/badge/DSH-%5E0.2.1--0-2f6fed)](https://github.com/deepseek-ai/deepseek-harness)
5
+ <!-- M8ven ownership proof — keep this line in place; it verifies the publisher. -->
6
+ [![M8ven Score](https://m8ven.ai/badge/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?v=f15f3e56c3fa22e8aafb88cf5428501f)](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(id, text(await api.spawn({ taskName, message, forkTurns, from: args.from, cwd: args.cwd })))
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(id, text(await api.followup({ worker, message })))
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 { task_name: taskName, sessionId, accepted: accepted?.accepted === true, fork_turns: forkTurns }
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 { worker: target.taskName ?? target.sessionId, sessionId: target.sessionId, accepted: accepted?.accepted === true }
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.7.4",
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": [