dsh-plugin-subagent-bridge 0.8.0 → 0.9.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 CHANGED
@@ -11,6 +11,30 @@
11
11
 
12
12
  ---
13
13
 
14
+ ## 创建项目(workspace)
15
+
16
+ DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
17
+
18
+ `dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
19
+
20
+ ```js
21
+ // 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
22
+ dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
23
+
24
+ // 用指定标题创建
25
+ dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
26
+
27
+ // 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
28
+ dsh_task({ prompt: "…", cwd: "/repo/clawer" })
29
+ ```
30
+
31
+ 行为细节:
32
+
33
+ - 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
34
+ - 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
35
+ - 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
36
+ - 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
37
+
14
38
  ## 指定模型
15
39
 
16
40
  `dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
package/index.js CHANGED
@@ -99,6 +99,11 @@ const TOOLS = [
99
99
  description:
100
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
101
  },
102
+ workspace: {
103
+ type: ['boolean', 'string'],
104
+ description:
105
+ 'Grouping for the new session. true creates a DSH project (workspace) for `cwd`, titled after its last path segment, when none owns that directory yet; a string uses that title instead. Omit (or false) to only reuse an existing project, leaving the session ungrouped when the directory has none.',
106
+ },
102
107
  reasoningEffort: {
103
108
  type: 'string',
104
109
  description: 'Reasoning level for `model`, if that model offers levels (low, medium, high, max...).',
@@ -130,6 +135,11 @@ const TOOLS = [
130
135
  description:
131
136
  '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
137
  },
138
+ workspace: {
139
+ type: ['boolean', 'string'],
140
+ description:
141
+ 'Grouping for the new worker. true creates a DSH project (workspace) for `cwd` when none owns it; a string uses that title. Omit to only reuse an existing project.',
142
+ },
133
143
  reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
134
144
  },
135
145
  required: ['task_name', 'message'],
@@ -512,6 +522,7 @@ async function callTool(id, params, api, signal) {
512
522
  forkTurns,
513
523
  from: args.from,
514
524
  cwd: args.cwd,
525
+ workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
515
526
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
516
527
  reasoningEffort:
517
528
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -565,6 +576,7 @@ async function callTool(id, params, api, signal) {
565
576
  cwd: typeof args.cwd === 'string' && args.cwd !== '' ? args.cwd : undefined,
566
577
  sessionId,
567
578
  agentPreset: typeof args.agentPreset === 'string' && args.agentPreset !== '' ? args.agentPreset : undefined,
579
+ workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
568
580
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
569
581
  reasoningEffort:
570
582
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -639,7 +651,7 @@ export function createApi(ctx, state, settings) {
639
651
  })
640
652
  },
641
653
 
642
- async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort }) {
654
+ async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort, workspace }) {
643
655
  const service = controller()
644
656
  if (service === undefined || service === null) throw new Error('session controller is unavailable')
645
657
  if (state.workers.has(taskName)) throw new Error(`task_name ${JSON.stringify(taskName)} is already in use`)
@@ -648,6 +660,7 @@ export function createApi(ctx, state, settings) {
648
660
  takeDispatchSlot(state, settings)
649
661
  assertCapacity(state, settings, root)
650
662
 
663
+ let placed
651
664
  let sessionId
652
665
  if (forkTurns === 'all') {
653
666
  const parent = sessions()?.get?.(from)
@@ -660,7 +673,9 @@ export function createApi(ctx, state, settings) {
660
673
  } else {
661
674
  const opened = await service.create(cwd === undefined ? {} : { cwd })
662
675
  sessionId = opened?.sessionId
663
- if (typeof sessionId === 'string' && sessionId !== '') await attachToWorkspace(ctx, sessionId, cwd ?? undefined)
676
+ if (typeof sessionId === 'string' && sessionId !== '') {
677
+ placed = await attachToWorkspace(ctx, sessionId, cwd ?? undefined, workspaceOptions(workspace))
678
+ }
664
679
  if (typeof sessionId !== 'string' || sessionId === '') throw new Error('session controller did not return a session id')
665
680
  }
666
681
 
@@ -689,6 +704,7 @@ export function createApi(ctx, state, settings) {
689
704
  accepted: accepted?.accepted === true,
690
705
  fork_turns: forkTurns,
691
706
  ...(appliedModel === undefined ? {} : { model: appliedModel }),
707
+ ...(placed === undefined ? {} : { workspace: placed }),
692
708
  }
693
709
  },
694
710
 
@@ -741,7 +757,7 @@ export function createApi(ctx, state, settings) {
741
757
  return rows
742
758
  },
743
759
 
744
- async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort }) {
760
+ async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort, workspace }) {
745
761
  const service = controller()
746
762
  if (service === undefined || service === null) throw new Error('session controller is unavailable')
747
763
  const seconds = Math.min(
@@ -752,12 +768,15 @@ export function createApi(ctx, state, settings) {
752
768
  takeDispatchSlot(state, settings)
753
769
  const started = Date.now()
754
770
 
771
+ let placed
755
772
  let targetId = sessionId
756
773
  let created = false
757
774
  if (targetId === undefined) {
758
775
  const opened = await service.create(cwd === undefined ? {} : { cwd })
759
776
  targetId = opened?.sessionId
760
- if (typeof targetId === 'string' && targetId !== '') await attachToWorkspace(ctx, targetId, cwd ?? undefined)
777
+ if (typeof targetId === 'string' && targetId !== '') {
778
+ placed = await attachToWorkspace(ctx, targetId, cwd ?? undefined, workspaceOptions(workspace))
779
+ }
761
780
  if (typeof targetId !== 'string' || targetId === '') throw new Error('session controller did not return a session id')
762
781
  created = true
763
782
  } else if (ctx.get('agents')?.get?.(targetId)?.status === 'running') {
@@ -808,6 +827,7 @@ export function createApi(ctx, state, settings) {
808
827
  }
809
828
  : {}),
810
829
  ...(appliedModel === undefined ? {} : { model: appliedModel }),
830
+ ...(placed === undefined ? {} : { workspace: placed }),
811
831
  text: collected.text,
812
832
  truncated: collected.truncated,
813
833
  turns: lastTurn(ctx, targetId),
@@ -1132,6 +1152,20 @@ async function applyModelSpec(ctx, sessionId, spec, reasoningEffort) {
1132
1152
  }
1133
1153
  }
1134
1154
 
1155
+ /**
1156
+ * Translate the `workspace` argument into attach options.
1157
+ * - absent / `false` → never create, only reuse an existing project for the directory
1158
+ * - `true` → create a project titled after the directory when none owns it
1159
+ * - a string → same, with that title
1160
+ * @param workspace - raw argument from the tool call.
1161
+ * @returns options for {@link attachToWorkspace}.
1162
+ */
1163
+ function workspaceOptions(workspace) {
1164
+ if (workspace === true) return { create: true }
1165
+ if (typeof workspace === 'string' && workspace.trim() !== '') return { create: true, title: workspace.trim() }
1166
+ return {}
1167
+ }
1168
+
1135
1169
  /**
1136
1170
  * Place a freshly created session in the workspace its directory belongs to.
1137
1171
  * `sessionController.create` only records a working directory; the sidebar groups
@@ -1140,14 +1174,32 @@ async function applyModelSpec(ctx, sessionId, spec, reasoningEffort) {
1140
1174
  * @param sessionId - session just created.
1141
1175
  * @param cwd - directory the session was opened in.
1142
1176
  */
1143
- async function attachToWorkspace(ctx, sessionId, cwd) {
1144
- if (typeof cwd !== 'string' || cwd === '') return
1177
+ async function attachToWorkspace(ctx, sessionId, cwd, options = {}) {
1178
+ if (typeof cwd !== 'string' || cwd === '') return undefined
1145
1179
  try {
1146
1180
  const registry = ctx.get('workspaceRegistry')
1147
- const workspace = await registry?.resolveByPath?.(cwd)
1181
+ if (registry === undefined || registry === null) return undefined
1182
+
1183
+ /* Only create when the caller asked for it: an existing project is reused either way,
1184
+ * because `create` returns the record that already owns the canonical path. */
1185
+ let workspace
1186
+ if (options.create === true && typeof registry.create === 'function') {
1187
+ /* The registry rejects a directory that does not exist yet, and a brand-new project
1188
+ * is exactly that, so the path is materialised first. */
1189
+ await mkdir(cwd, { recursive: true }).catch(() => {})
1190
+ const title = typeof options.title === 'string' && options.title.trim() !== '' ? options.title.trim() : undefined
1191
+ workspace = await registry.create(cwd, title)
1192
+ } else {
1193
+ workspace = await registry.resolveByPath?.(cwd)
1194
+ }
1195
+
1148
1196
  await workspace?.attachSession?.(sessionId)
1197
+ return workspace === undefined || workspace === null
1198
+ ? undefined
1199
+ : { id: workspace.id, title: workspace.title, path: workspace.path }
1149
1200
  } catch {
1150
1201
  /* Grouping is cosmetic: a failure here must not fail the dispatch. */
1202
+ return undefined
1151
1203
  }
1152
1204
  }
1153
1205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subagent-bridge",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": [
@@ -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`。写成可以独立开工的工单: