dsh-plugin-subagent-bridge 0.12.2 → 0.13.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
@@ -56,6 +56,53 @@ dsh_task({ prompt: "…", cwd: "/repo/clawer" })
56
56
  - 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
57
57
  - 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
58
58
 
59
+ ## 排队还是插话(`mode`)
60
+
61
+ DSH 的「繁忙时的发送行为」只有两种,桥把它们原样交给调用方:
62
+
63
+ | `mode` | DSH 里的名字 | 行为 |
64
+ | --- | --- | --- |
65
+ | `queue`(默认) | 排队发送 | 等当前回合跑完,再作为一个新回合处理 |
66
+ | `steer` | 插话发送 | 并入当前回合,模型在下一步就能看到 |
67
+
68
+ `dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
69
+
70
+ ```js
71
+ dsh_followup({ worker: "w1", message: "接下来只改 src 下的文件", mode: "steer" })
72
+ dsh_task({ sessionId: "session-…", prompt: "停一下,改成只跑单测", mode: "steer" })
73
+ ```
74
+
75
+ 两点实测结论:
76
+
77
+ 1. **`steer` 不打断正在执行的工具**。实测 worker 正在跑 `sleep 90`,`steer` 的消息投递进去了,但要等那次工具调用返回、模型走到下一步才被看到——整整 70 秒。真想立刻停,是 DSH 界面上另一个动作(取消),不是投递模式。
78
+ 2. **`steer` 不新开回合**(turn 编号不变),所以等待条件必须是「当前回合结束」。早期实现等的是「下一个回合」,答案都回来了、调用还在等,最后 120 秒超时。
79
+
80
+ `dsh_task` 对正在运行的会话默认会拒绝,错误信息里会提示改用 `steer`。
81
+
82
+ ## 创建项目(workspace)
83
+
84
+ DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
85
+
86
+ `dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
87
+
88
+ ```js
89
+ // 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
90
+ dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
91
+
92
+ // 用指定标题创建
93
+ dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
94
+
95
+ // 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
96
+ dsh_task({ prompt: "…", cwd: "/repo/clawer" })
97
+ ```
98
+
99
+ 行为细节:
100
+
101
+ - 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
102
+ - 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
103
+ - 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
104
+ - 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
105
+
59
106
  ## 排队还是立刻打断(`mode`)
60
107
 
61
108
  DSH 的 prompt 有两种投递模式,桥把它们交给调用方选:
package/index.js CHANGED
@@ -117,9 +117,9 @@ const TOOLS = [
117
117
  },
118
118
  mode: {
119
119
  type: 'string',
120
- enum: ['queue', 'steer', 'interrupt'],
120
+ enum: ['queue', 'steer'],
121
121
  description:
122
- 'delivery: "queue" (default) waits for the running turn to finish; "steer" inserts the message into that turn (its current step still completes); "interrupt" cancels the running turn first, then sends.',
122
+ 'How a busy session receives this: "queue" (default) waits for the running turn to finish, "steer" inserts the message into that turn so the model sees it at the next step.',
123
123
  },
124
124
 
125
125
  reasoningEffort: {
@@ -160,9 +160,9 @@ const TOOLS = [
160
160
  },
161
161
  mode: {
162
162
  type: 'string',
163
- enum: ['queue', 'steer', 'interrupt'],
163
+ enum: ['queue', 'steer'],
164
164
  description:
165
- 'delivery: "queue" (default) waits for the running turn to finish; "steer" inserts the message into that turn (its current step still completes); "interrupt" cancels the running turn first, then sends.',
165
+ 'How a busy session receives this: "queue" (default) waits for the running turn to finish, "steer" inserts the message into that turn so the model sees it at the next step.',
166
166
  },
167
167
 
168
168
  reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
@@ -187,9 +187,9 @@ const TOOLS = [
187
187
  reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
188
188
  mode: {
189
189
  type: 'string',
190
- enum: ['queue', 'steer', 'interrupt'],
190
+ enum: ['queue', 'steer'],
191
191
  description:
192
- 'delivery: "queue" (default) waits for the running turn to finish; "steer" inserts the message into that turn (its current step still completes); "interrupt" cancels the running turn first, then sends.',
192
+ 'How a busy session receives this: "queue" (default) waits for the running turn to finish, "steer" inserts the message into that turn so the model sees it at the next step.',
193
193
  },
194
194
  },
195
195
  required: ['worker', 'message'],
@@ -610,7 +610,7 @@ async function callTool(id, params, api, signal) {
610
610
  from: args.from,
611
611
  cwd: args.cwd,
612
612
  workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
613
- mode: args.mode === 'steer' || args.mode === 'interrupt' ? args.mode : undefined,
613
+ mode: args.mode === 'steer' ? 'steer' : undefined,
614
614
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
615
615
  reasoningEffort:
616
616
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -627,7 +627,7 @@ async function callTool(id, params, api, signal) {
627
627
  await api.followup({
628
628
  worker,
629
629
  message,
630
- mode: args.mode === 'steer' || args.mode === 'interrupt' ? args.mode : undefined,
630
+ mode: args.mode === 'steer' ? 'steer' : undefined,
631
631
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
632
632
  reasoningEffort:
633
633
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -666,7 +666,7 @@ async function callTool(id, params, api, signal) {
666
666
  sessionId,
667
667
  agentPreset: typeof args.agentPreset === 'string' && args.agentPreset !== '' ? args.agentPreset : undefined,
668
668
  workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
669
- mode: args.mode === 'steer' || args.mode === 'interrupt' ? args.mode : undefined,
669
+ mode: args.mode === 'steer' ? 'steer' : undefined,
670
670
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
671
671
  reasoningEffort:
672
672
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -869,30 +869,16 @@ export function createApi(ctx, state, settings) {
869
869
  }
870
870
  if (typeof targetId !== 'string' || targetId === '') throw new Error('session controller did not return a session id')
871
871
  created = true
872
- } else if (mode !== 'steer' && mode !== 'interrupt' && ctx.get('agents')?.get?.(targetId)?.status === 'running') {
872
+ } else if (mode !== 'steer' && ctx.get('agents')?.get?.(targetId)?.status === 'running') {
873
873
  /* Steering exists precisely to reach a running session; queueing into one would
874
874
  * silently wait for a turn this call is not tracking. */
875
- throw new Error('that session is running; pass mode:"steer" (insert into the turn) or mode:"interrupt" (cancel it first), or use dsh_dispatch plus dsh_read_result')
875
+ throw new Error('that session is running; pass mode:"steer" to insert into that turn, or use dsh_dispatch plus dsh_read_result')
876
876
  }
877
877
 
878
878
  const appliedModel = await applyModelSpec(ctx, targetId, model, reasoningEffort)
879
- const deliveryMode = mode === 'steer' ? 'steer' : mode === 'interrupt' ? 'interrupt' : 'queue'
879
+ /* DSH delivers a prompt either queued behind the running turn or steered into it. */
880
+ const deliveryMode = mode === 'steer' ? 'steer' : 'queue'
880
881
  const turnBefore = lastTurn(ctx, targetId)
881
-
882
- /* `steer` inserts the message into the running turn, but that turn still finishes its
883
- * current step first — an in-flight tool call keeps running. `interrupt` is the hard
884
- * stop: cancel the turn, then send, so the worker drops what it was doing. */
885
- if (deliveryMode === 'interrupt' && ctx.get('agents')?.get?.(targetId)?.status === 'running') {
886
- try {
887
- await service.cancel({ sessionId: targetId })
888
- } catch {
889
- /* Cancelling is best effort: the send below still delivers the message. */
890
- }
891
- /* The abort closes the *current* turn, so accept that number: waiting for a
892
- * strictly newer turn burned the whole grace period before every send. */
893
- await waitForTurnEnd(ctx, targetId, Math.max(0, turnBefore - 1), 15000)
894
- }
895
-
896
882
  /* A steered message is absorbed by the turn that is already running, so waiting for
897
883
  * a strictly newer turn would hang until the timeout even after the answer landed. */
898
884
  const waitAfter = deliveryMode === 'steer' ? Math.max(0, turnBefore - 1) : turnBefore
@@ -900,7 +886,7 @@ export function createApi(ctx, state, settings) {
900
886
  const finished = waitForTurnEnd(ctx, targetId, waitAfter, seconds * 1000)
901
887
  const requestId = randomUUID()
902
888
  const accepted = await service.prompt(
903
- { sessionId: targetId, requestId, mode: deliveryMode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: prompt }] },
889
+ { sessionId: targetId, requestId, mode: deliveryMode, content: [{ type: 'text', text: prompt }] },
904
890
  AbortSignal.timeout(30000),
905
891
  )
906
892
  state.dispatched.add(targetId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subagent-bridge",
3
- "version": "0.12.2",
3
+ "version": "0.13.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": [
@@ -224,34 +224,26 @@ dsh_followup({ "worker": "audit_api", "message": "把第 3 条展开,给出补
224
224
 
225
225
  `dsh_followup` 复用该 worker 的会话,比新开一个便宜。也可以用 `dsh_task({ "sessionId": …, "prompt": … })` 做同样的事(阻塞版)。
226
226
 
227
- ## 改主意时:排队还是立刻打断
227
+ ## 改主意时:排队还是插话
228
228
 
229
- 默认 `dsh_followup` / `dsh_task` 是**排队**:等 worker 当前这一轮跑完再处理。但有时候用户就是要它立刻停——改方向、别继续跑、先汇报。这时用 `mode`:
229
+ 默认是**排队发送**:等 worker 当前这一轮跑完,再作为新的一轮处理。DSH 的「繁忙时的发送行为」还提供**插话发送**,桥用 `mode` 暴露它:
230
230
 
231
231
  ```jsonc
232
- // 排进队列,等当前回合结束(默认)
232
+ // 排队(默认):等这一轮结束
233
233
  dsh_followup({ "worker": "w1", "message": "跑完顺手看一下测试覆盖率" })
234
234
 
235
- // 插进当前回合:当前那一步工具调用仍会跑完
236
- dsh_followup({ "worker": "w1", "message": "补充一句:只改 src 下的文件", "mode": "steer" })
237
-
238
- // 立刻打断:先取消当前回合,再发消息开新回合
239
- dsh_followup({ "worker": "w1", "message": "停,先汇报现在的结果", "mode": "interrupt" })
235
+ // 插话:并入当前这一轮,模型下一步就能看到
236
+ dsh_followup({ "worker": "w1", "message": "接下来只改 src 下的文件,别动 packages", "mode": "steer" })
240
237
  ```
241
238
 
242
239
  | `mode` | 行为 | 什么时候用 |
243
240
  | --- | --- | --- |
244
- | `queue`(默认) | 等这一轮结束 | 补充要求,不急着停 |
245
- | `steer` | 并入当前回合,**当前那一步还会跑完** | 想到补充,能等这一下 |
246
- | `interrupt` | 先 `cancel`,再发消息 | 真的要停 |
247
-
248
- 实测差别(worker 正在跑 `sleep 120`):
249
-
250
- - `steer` → 消息要等那一步工具调用结束才被看到,**70 秒**才回应;
251
- - `interrupt` → 先 aborted 当前回合,再开新回合,**10 秒左右**返回。
241
+ | `queue`(默认) | 等这一轮结束,新开一轮 | 「顺便」「另外」 |
242
+ | `steer` | 并入当前一轮,**turn 不重开** | 「接下来注意…」「只改某某」 |
252
243
 
253
- 用户说「停下来」「别跑了」「先别管这个」时用 `interrupt`;说「顺便」「另外」时用默认 `queue` 就够了。`dsh_task` 对**正在运行**的会话默认会拒绝,拒绝信息里也会提示这两个选项。
244
+ **`steer` 不会打断正在执行的工具**:worker 若正卡在 `sleep 90` 这类调用里,消息要等它返回到下一步才被看到(实测 70 秒)。要它立刻停,那是用户在 DSH 界面上按取消,不是这里的投递模式——不要用 `steer` 假装能"打断"。
254
245
 
246
+ 用户说「停下来/别做了」时,正确反应是:告诉用户这会让当前这轮中止,或者用 `dsh_wait` 等它自然结束——不要依赖 `mode` 去实现"立即停"。
255
247
 
256
248
  ## 查现在有哪些 worker
257
249