dsh-plugin-subagent-bridge 0.11.1 → 0.12.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.
Files changed (3) hide show
  1. package/README.md +8 -2
  2. package/index.js +28 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,8 @@ DSH 的 prompt 有两种投递模式,桥把它们交给调用方选:
63
63
  | `mode` | 行为 |
64
64
  | --- | --- |
65
65
  | `queue`(默认) | 等当前回合跑完再处理这条消息 |
66
- | `steer` | **立刻打断**当前回合,把这条消息插进去 |
66
+ | `steer` | 把消息**插进当前回合**(当前那一步仍会跑完) |
67
+ | `interrupt` | **先取消当前回合**,再发送,真正立刻停下 |
67
68
 
68
69
  `dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
69
70
 
@@ -74,7 +75,12 @@ dsh_task({ sessionId: "session-…", prompt: "停一下,改成只跑单测", m
74
75
 
75
76
  和 `dsh_dispatch` 的关系:`dsh_dispatch` 一直有 `mode`,但它只投递、不等待;`dsh_task` 会阻塞到回合结束,所以以前对正在运行的会话直接拒绝。现在传 `mode: "steer"` 就允许插进运行中的会话——**它会先打断当前回合**,然后等这一轮结束才返回。
76
77
 
77
- 一个容易踩的细节:`steer` 的消息会被**当前回合直接吸收**(不会新开一轮),所以等待条件不能是"下一个回合结束"。早期实现就是这么写的,结果答案已经回来了、调用却一直等到 120 秒超时才知道。现在 steer 等的是「当前回合结束」。
78
+ 两个实测出来的细节:
79
+
80
+ 1. **`steer` 不会打断正在执行的工具**。实测:worker 正在跑 `sleep 90`,`steer` 进去后消息被**并入当前回合**,但要等那一步工具调用结束才会被模型看到——整整等了 70 秒。原因是 `steer` 只是投递,`cancel` 才是中止。
81
+ 2. **`steer` 不新开回合**,所以等待条件不能是"下一个回合结束"。早期实现那样写,答案已经回来了、调用还在等,最后 120 秒超时。现在 steer 等的是「当前回合结束」。
82
+
83
+ 所以真要"按下就停",用 `interrupt`:它先 `cancel` 掉当前回合(最多等 15 秒确认),再以普通排队模式发消息,开启新回合。
78
84
 
79
85
  返回里带 `mode` 字段,方便确认这次是排队还是打断。
80
86
 
package/index.js CHANGED
@@ -117,9 +117,9 @@ const TOOLS = [
117
117
  },
118
118
  mode: {
119
119
  type: 'string',
120
- enum: ['queue', 'steer'],
120
+ enum: ['queue', 'steer', 'interrupt'],
121
121
  description:
122
- 'delivery: "queue" (default) waits for the running turn to finish, "steer" interrupts it and inserts this message now.',
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.',
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'],
163
+ enum: ['queue', 'steer', 'interrupt'],
164
164
  description:
165
- 'delivery: "queue" (default) waits for the running turn to finish, "steer" interrupts it and inserts this message now.',
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.',
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'],
190
+ enum: ['queue', 'steer', 'interrupt'],
191
191
  description:
192
- 'delivery: "queue" (default) waits for the running turn to finish, "steer" interrupts it and inserts this message now.',
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.',
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' ? 'steer' : undefined,
613
+ mode: args.mode === 'steer' || args.mode === 'interrupt' ? args.mode : 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' ? 'steer' : undefined,
630
+ mode: args.mode === 'steer' || args.mode === 'interrupt' ? args.mode : 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' ? 'steer' : undefined,
669
+ mode: args.mode === 'steer' || args.mode === 'interrupt' ? args.mode : 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,15 +869,30 @@ 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' && ctx.get('agents')?.get?.(targetId)?.status === 'running') {
872
+ } else if (mode !== 'steer' && mode !== 'interrupt' && 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" to interrupt it, or use dsh_dispatch plus dsh_read_result')
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')
876
876
  }
877
877
 
878
878
  const appliedModel = await applyModelSpec(ctx, targetId, model, reasoningEffort)
879
- const deliveryMode = mode === 'steer' ? 'steer' : 'queue'
879
+ const deliveryMode = mode === 'steer' ? 'steer' : mode === 'interrupt' ? 'interrupt' : 'queue'
880
880
  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
+
881
896
  /* A steered message is absorbed by the turn that is already running, so waiting for
882
897
  * a strictly newer turn would hang until the timeout even after the answer landed. */
883
898
  const waitAfter = deliveryMode === 'steer' ? Math.max(0, turnBefore - 1) : turnBefore
@@ -885,7 +900,7 @@ export function createApi(ctx, state, settings) {
885
900
  const finished = waitForTurnEnd(ctx, targetId, waitAfter, seconds * 1000)
886
901
  const requestId = randomUUID()
887
902
  const accepted = await service.prompt(
888
- { sessionId: targetId, requestId, mode: deliveryMode, content: [{ type: 'text', text: prompt }] },
903
+ { sessionId: targetId, requestId, mode: deliveryMode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: prompt }] },
889
904
  AbortSignal.timeout(30000),
890
905
  )
891
906
  state.dispatched.add(targetId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subagent-bridge",
3
- "version": "0.11.1",
3
+ "version": "0.12.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": [