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.
- package/README.md +22 -0
- package/index.js +39 -9
- 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
|
-
|
|
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,
|
|
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:
|
|
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.
|
|
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": [
|