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 +47 -0
- package/index.js +14 -28
- package/package.json +1 -1
- package/skills/deepseek-worker/SKILL.md +9 -17
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'
|
|
120
|
+
enum: ['queue', 'steer'],
|
|
121
121
|
description:
|
|
122
|
-
'
|
|
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'
|
|
163
|
+
enum: ['queue', 'steer'],
|
|
164
164
|
description:
|
|
165
|
-
'
|
|
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'
|
|
190
|
+
enum: ['queue', 'steer'],
|
|
191
191
|
description:
|
|
192
|
-
'
|
|
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'
|
|
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'
|
|
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'
|
|
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' &&
|
|
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"
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
244
|
+
**`steer` 不会打断正在执行的工具**:worker 若正卡在 `sleep 90` 这类调用里,消息要等它返回到下一步才被看到(实测 70 秒)。要它立刻停,那是用户在 DSH 界面上按取消,不是这里的投递模式——不要用 `steer` 假装能"打断"。
|
|
254
245
|
|
|
246
|
+
用户说「停下来/别做了」时,正确反应是:告诉用户这会让当前这轮中止,或者用 `dsh_wait` 等它自然结束——不要依赖 `mode` 去实现"立即停"。
|
|
255
247
|
|
|
256
248
|
## 查现在有哪些 worker
|
|
257
249
|
|