dsh-plugin-subagent-bridge 0.10.1 → 0.11.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 +22 -0
  2. package/index.js +39 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,6 +56,28 @@ dsh_task({ prompt: "…", cwd: "/repo/clawer" })
56
56
  - 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
57
57
  - 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
58
58
 
59
+ ## 排队还是立刻打断(`mode`)
60
+
61
+ DSH 的 prompt 有两种投递模式,桥把它们交给调用方选:
62
+
63
+ | `mode` | 行为 |
64
+ | --- | --- |
65
+ | `queue`(默认) | 等当前回合跑完再处理这条消息 |
66
+ | `steer` | **立刻打断**当前回合,把这条消息插进去 |
67
+
68
+ `dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
69
+
70
+ ```js
71
+ dsh_followup({ worker: "w1", message: "别改了,先把现在的结果汇报一下", mode: "steer" })
72
+ dsh_task({ sessionId: "session-…", prompt: "停一下,改成只跑单测", mode: "steer" })
73
+ ```
74
+
75
+ 和 `dsh_dispatch` 的关系:`dsh_dispatch` 一直有 `mode`,但它只投递、不等待;`dsh_task` 会阻塞到回合结束,所以以前对正在运行的会话直接拒绝。现在传 `mode: "steer"` 就允许插进运行中的会话——**它会先打断当前回合**,然后等这一轮结束才返回。
76
+
77
+ 一个容易踩的细节:`steer` 的消息会被**当前回合直接吸收**(不会新开一轮),所以等待条件不能是"下一个回合结束"。早期实现就是这么写的,结果答案已经回来了、调用却一直等到 120 秒超时才知道。现在 steer 等的是「当前回合结束」。
78
+
79
+ 返回里带 `mode` 字段,方便确认这次是排队还是打断。
80
+
59
81
  ## 指定模型
60
82
 
61
83
  `dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
package/index.js CHANGED
@@ -115,6 +115,13 @@ const TOOLS = [
115
115
  description:
116
116
  '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.',
117
117
  },
118
+ mode: {
119
+ type: 'string',
120
+ enum: ['queue', 'steer'],
121
+ description:
122
+ 'delivery: "queue" (default) waits for the running turn to finish, "steer" interrupts it and inserts this message now.',
123
+ },
124
+
118
125
  reasoningEffort: {
119
126
  type: 'string',
120
127
  description: 'Reasoning level for `model`, if that model offers levels (low, medium, high, max...).',
@@ -151,6 +158,13 @@ const TOOLS = [
151
158
  description:
152
159
  '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.',
153
160
  },
161
+ mode: {
162
+ type: 'string',
163
+ enum: ['queue', 'steer'],
164
+ description:
165
+ 'delivery: "queue" (default) waits for the running turn to finish, "steer" interrupts it and inserts this message now.',
166
+ },
167
+
154
168
  reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
155
169
  },
156
170
  required: ['task_name', 'message'],
@@ -171,6 +185,12 @@ const TOOLS = [
171
185
  description: 'Switch this worker to another model before the turn, as "provider/model". Omit to keep its current one.',
172
186
  },
173
187
  reasoningEffort: { type: 'string', description: 'Reasoning level for `model`, when that model offers levels.' },
188
+ mode: {
189
+ type: 'string',
190
+ enum: ['queue', 'steer'],
191
+ description:
192
+ 'delivery: "queue" (default) waits for the running turn to finish, "steer" interrupts it and inserts this message now.',
193
+ },
174
194
  },
175
195
  required: ['worker', 'message'],
176
196
  additionalProperties: false,
@@ -590,6 +610,7 @@ async function callTool(id, params, api, signal) {
590
610
  from: args.from,
591
611
  cwd: args.cwd,
592
612
  workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
613
+ mode: args.mode === 'steer' ? 'steer' : undefined,
593
614
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
594
615
  reasoningEffort:
595
616
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -606,6 +627,7 @@ async function callTool(id, params, api, signal) {
606
627
  await api.followup({
607
628
  worker,
608
629
  message,
630
+ mode: args.mode === 'steer' ? 'steer' : undefined,
609
631
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
610
632
  reasoningEffort:
611
633
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -644,6 +666,7 @@ async function callTool(id, params, api, signal) {
644
666
  sessionId,
645
667
  agentPreset: typeof args.agentPreset === 'string' && args.agentPreset !== '' ? args.agentPreset : undefined,
646
668
  workspace: args.workspace === true || (typeof args.workspace === 'string' && args.workspace !== '') ? args.workspace : undefined,
669
+ mode: args.mode === 'steer' ? 'steer' : undefined,
647
670
  model: typeof args.model === 'string' && args.model !== '' ? args.model : undefined,
648
671
  reasoningEffort:
649
672
  typeof args.reasoningEffort === 'string' && args.reasoningEffort !== '' ? args.reasoningEffort : undefined,
@@ -718,7 +741,7 @@ export function createApi(ctx, state, settings) {
718
741
  })
719
742
  },
720
743
 
721
- async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort, workspace }) {
744
+ async spawn({ taskName, message, forkTurns, from, cwd, model, reasoningEffort, workspace, mode }) {
722
745
  const service = controller()
723
746
  if (service === undefined || service === null) throw new Error('session controller is unavailable')
724
747
  if (state.workers.has(taskName)) throw new Error(`task_name ${JSON.stringify(taskName)} is already in use`)
@@ -750,7 +773,7 @@ export function createApi(ctx, state, settings) {
750
773
  const before = Number(sessions()?.get?.(sessionId)?.seq ?? 0)
751
774
  const requestId = randomUUID()
752
775
  const accepted = await service.prompt(
753
- { sessionId, requestId, mode: 'queue', content: [{ type: 'text', text: message }] },
776
+ { sessionId, requestId, mode: mode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: message }] },
754
777
  AbortSignal.timeout(30000),
755
778
  )
756
779
  state.dispatched.add(sessionId)
@@ -775,7 +798,7 @@ export function createApi(ctx, state, settings) {
775
798
  }
776
799
  },
777
800
 
778
- async followup({ worker, message, model, reasoningEffort }) {
801
+ async followup({ worker, message, model, reasoningEffort, mode }) {
779
802
  const target = resolveWorker(state, worker)
780
803
  if (target === undefined) throw new Error(`unknown worker ${JSON.stringify(worker)}`)
781
804
  const service = controller()
@@ -785,7 +808,7 @@ export function createApi(ctx, state, settings) {
785
808
  const before = Number(sessions()?.get?.(target.sessionId)?.seq ?? 0)
786
809
  const requestId = randomUUID()
787
810
  const accepted = await service.prompt(
788
- { sessionId: target.sessionId, requestId, mode: 'queue', content: [{ type: 'text', text: message }] },
811
+ { sessionId: target.sessionId, requestId, mode: mode === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: message }] },
789
812
  AbortSignal.timeout(30000),
790
813
  )
791
814
  state.baseline.set(target.sessionId, before)
@@ -824,7 +847,7 @@ export function createApi(ctx, state, settings) {
824
847
  return rows
825
848
  },
826
849
 
827
- async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort, workspace }) {
850
+ async task({ prompt, cwd, sessionId, agentPreset, timeoutSeconds, model, reasoningEffort, workspace, mode }) {
828
851
  const service = controller()
829
852
  if (service === undefined || service === null) throw new Error('session controller is unavailable')
830
853
  const seconds = Math.min(
@@ -846,17 +869,23 @@ export function createApi(ctx, state, settings) {
846
869
  }
847
870
  if (typeof targetId !== 'string' || targetId === '') throw new Error('session controller did not return a session id')
848
871
  created = true
849
- } else if (ctx.get('agents')?.get?.(targetId)?.status === 'running') {
850
- throw new Error('that session is running; use dsh_dispatch plus dsh_read_result instead')
872
+ } else if (mode !== 'steer' && ctx.get('agents')?.get?.(targetId)?.status === 'running') {
873
+ /* Steering exists precisely to reach a running session; queueing into one would
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')
851
876
  }
852
877
 
853
878
  const appliedModel = await applyModelSpec(ctx, targetId, model, reasoningEffort)
879
+ const deliveryMode = mode === 'steer' ? 'steer' : 'queue'
854
880
  const turnBefore = lastTurn(ctx, targetId)
881
+ /* A steered message is absorbed by the turn that is already running, so waiting for
882
+ * a strictly newer turn would hang until the timeout even after the answer landed. */
883
+ const waitAfter = deliveryMode === 'steer' ? Math.max(0, turnBefore - 1) : turnBefore
855
884
  const before = Number(sessions()?.get?.(targetId)?.seq ?? 0)
856
- const finished = waitForTurnEnd(ctx, targetId, turnBefore, seconds * 1000)
885
+ const finished = waitForTurnEnd(ctx, targetId, waitAfter, seconds * 1000)
857
886
  const requestId = randomUUID()
858
887
  const accepted = await service.prompt(
859
- { sessionId: targetId, requestId, mode: 'queue', content: [{ type: 'text', text: prompt }] },
888
+ { sessionId: targetId, requestId, mode: deliveryMode, content: [{ type: 'text', text: prompt }] },
860
889
  AbortSignal.timeout(30000),
861
890
  )
862
891
  state.dispatched.add(targetId)
@@ -895,6 +924,7 @@ export function createApi(ctx, state, settings) {
895
924
  : {}),
896
925
  ...(appliedModel === undefined ? {} : { model: appliedModel }),
897
926
  ...(placed === undefined ? {} : { workspace: placed }),
927
+ mode: deliveryMode,
898
928
  text: collected.text,
899
929
  truncated: collected.truncated,
900
930
  turns: lastTurn(ctx, targetId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subagent-bridge",
3
- "version": "0.10.1",
3
+ "version": "0.11.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": [