dsh-plugin-subagent-bridge 0.15.0 → 0.15.2
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 +188 -329
- package/README.zh-CN.md +314 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,297 +1,203 @@
|
|
|
1
1
|
# dsh-plugin-subagent-bridge
|
|
2
2
|
|
|
3
|
+
> Let Codex or Claude Code drive DeepSeek Harness sessions as subagents — and let DSH drive them back.
|
|
4
|
+
|
|
3
5
|
[](LICENSE)
|
|
4
6
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
5
7
|
<!-- M8ven ownership proof — keep this line in place; it verifies the publisher. -->
|
|
6
8
|
[](https://m8ven.ai/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?s=readme)
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
插件运行在宿主进程内,直接使用公开的 `sessionController` 服务 —— 不需要读取 `~/.dsh/.credentials.yaml`,也不需要伪造 cookie。
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 让 DSH 派活给 Claude
|
|
15
|
-
|
|
16
|
-
反向那一侧以前只有 Codex。现在多了 `claude_dispatch`:DSH 里的 agent 可以把任务交给 Claude Code,拿回最终回答。
|
|
10
|
+
[中文说明](README.zh-CN.md)
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
claude_dispatch({ prompt: "审一下这个 PR 的风险", cwd: "/repo", resume: "a4925654-…", model: "sonnet" })
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
| 参数 | 说明 |
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| `prompt` | 必填,任务正文 |
|
|
25
|
-
| `cwd` | 工作目录;默认用当前会话目录 |
|
|
26
|
-
| `resume` | Claude Code 的 session id,续接同一段对话 |
|
|
27
|
-
| `model` | 模型别名或 id;不传则用配置,再退回 Claude 自己的设置 |
|
|
12
|
+
The plugin runs inside the Harness host and exposes a loopback MCP endpoint. It uses public host services (`sessionController`, `sessions`, `workspaceRegistry`, `userQuestions`) — no credential files, no cookie forgery.
|
|
28
13
|
|
|
29
|
-
|
|
14
|
+
## Highlights
|
|
30
15
|
|
|
31
|
-
|
|
16
|
+
- **One tool call, one answer.** `dsh_task` opens a session, delivers the message, blocks until the turn ends, and returns only the final assistant text.
|
|
17
|
+
- **Parallel workers.** `dsh_spawn` returns immediately; `dsh_wait` blocks until any of them settles.
|
|
18
|
+
- **Per-call model and project.** Pin a worker to `provider/model` and file it under a named workspace without touching profile defaults.
|
|
19
|
+
- **Queue or steer.** Choose whether a follow-up waits for the running turn or is inserted into it.
|
|
20
|
+
- **Unattended by default.** Workers are told to assume a reasonable default instead of stopping to ask.
|
|
21
|
+
- **Reverse direction.** `codex_dispatch` and `claude_dispatch` hand work to Codex or Claude Code, with resumable threads.
|
|
22
|
+
- **Loop guard.** Depth and rate limits stop the two agents from delegating to each other in circles.
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
## Requirements
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
|
|
38
|
-
|
|
39
|
-
`dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
// 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
|
|
43
|
-
dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
|
|
44
|
-
|
|
45
|
-
// 用指定标题创建
|
|
46
|
-
dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
|
|
47
|
-
|
|
48
|
-
// 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
|
|
49
|
-
dsh_task({ prompt: "…", cwd: "/repo/clawer" })
|
|
50
|
-
```
|
|
26
|
+
- DeepSeek Harness `^0.2.1-0`.
|
|
27
|
+
- Codex CLI (`codex`) and/or Claude Code CLI (`claude`) on `PATH` for the reverse direction.
|
|
51
28
|
|
|
52
|
-
|
|
29
|
+
## Install
|
|
53
30
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
|
|
57
|
-
- 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
|
|
58
|
-
|
|
59
|
-
## 没人值守:让它自己拿主意(`assumeDefaults`)
|
|
60
|
-
|
|
61
|
-
worker 卡在 `ask_user_question` 上时,需要有人在界面上点一下;但调用方是另一个 agent 时,那个人往往不在。
|
|
62
|
-
|
|
63
|
-
所以桥在派发时会**自动追加一条常驻指令**:
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
(如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,
|
|
67
|
-
不要把选择权抛回给用户、不要调用 ask_user_question 等回答;
|
|
68
|
-
在结果里用一行说明你假设了什么。)
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/Dogtiti/dsh-plugin-subagent-bridge
|
|
69
33
|
```
|
|
70
34
|
|
|
71
|
-
|
|
72
|
-
- `dsh_task`、`dsh_spawn`、`dsh_followup` 三条路径都会加;
|
|
73
|
-
- prompt 里本来就提到 `ask_user_question` 时不追加,避免和调用方的明确意图打架。
|
|
74
|
-
|
|
75
|
-
效果:worker 不会停下来等人,而是选一个合理默认做法做完,并在结果里交代假设,调用方据此复核。
|
|
76
|
-
|
|
77
|
-
## 回答 worker 的问题(`dsh_answer`)
|
|
35
|
+
Open **Plugins → Add** in DSH and pick that directory. On start the plugin listens on `127.0.0.1:47821` and writes the endpoint file:
|
|
78
36
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
桥把这种情况暴露出来:
|
|
82
|
-
|
|
83
|
-
```jsonc
|
|
84
|
-
// dsh_wait 的返回
|
|
37
|
+
```json
|
|
85
38
|
{
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
"id": "mode",
|
|
93
|
-
"question": "用哪种模式?",
|
|
94
|
-
"header": "Choose Mode",
|
|
95
|
-
"options": [{ "label": "快速", "description": "只跑单测" }, { "label": "完整" }]
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
"next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
|
|
39
|
+
"url": "http://127.0.0.1:47821/mcp/<token>",
|
|
40
|
+
"port": 47821,
|
|
41
|
+
"token": "<token>",
|
|
42
|
+
"pid": 12345,
|
|
43
|
+
"startedAt": "2026-10-22T12:30:00.000Z"
|
|
100
44
|
}
|
|
101
45
|
```
|
|
102
46
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```jsonc
|
|
106
|
-
dsh_answer({
|
|
107
|
-
"worker": "w1",
|
|
108
|
-
"answers": [{ "id": "mode", "selected": ["快速"] }] // custom 可用于自由文本
|
|
109
|
-
})
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
细节:
|
|
113
|
-
|
|
114
|
-
- 一个问题的 id 必须**恰好出现一次**,否则底层会报 `BAD_ANSWER`;
|
|
115
|
-
- `selected` 是选项 label 数组,多选时给多个;跳过一个问题就给空数组;
|
|
116
|
-
- 回答是插进正在等待的那个回合的(底层用 `steer`),worker 立刻继续;
|
|
117
|
-
- 只有真卡在 `ask_user_question` 上才算「等待中」——工具一旦返回结果就不再报。
|
|
118
|
-
|
|
119
|
-
## 排队还是插话(`mode`)
|
|
47
|
+
`~/.dsh/bridge/endpoint.json`, directory `0700`, file `0600`.
|
|
120
48
|
|
|
121
|
-
|
|
49
|
+
## Connect
|
|
122
50
|
|
|
123
|
-
|
|
124
|
-
| --- | --- | --- |
|
|
125
|
-
| `queue`(默认) | 排队发送 | 等当前回合跑完,再作为一个新回合处理 |
|
|
126
|
-
| `steer` | 插话发送 | 并入当前回合,模型在下一步就能看到 |
|
|
127
|
-
|
|
128
|
-
`dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
|
|
51
|
+
### Codex
|
|
129
52
|
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
|
|
53
|
+
```bash
|
|
54
|
+
codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
|
|
55
|
+
codex mcp list # verify
|
|
56
|
+
codex mcp remove dsh-bridge # remove
|
|
133
57
|
```
|
|
134
58
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
1. **`steer` 不打断正在执行的工具**。实测 worker 正在跑 `sleep 90`,`steer` 的消息投递进去了,但要等那次工具调用返回、模型走到下一步才被看到——整整 70 秒。真想立刻停,是 DSH 界面上另一个动作(取消),不是投递模式。
|
|
138
|
-
2. **`steer` 不新开回合**(turn 编号不变),所以等待条件必须是「当前回合结束」。早期实现等的是「下一个回合」,答案都回来了、调用还在等,最后 120 秒超时。
|
|
139
|
-
|
|
140
|
-
`dsh_task` 对正在运行的会话默认会拒绝,错误信息里会提示改用 `steer`。
|
|
141
|
-
|
|
142
|
-
第三个细节是返回值:插话的回复和原任务在**同一个回合**里,所以不能简单取"这一轮最后一条 assistant 文本"——那可能是原任务后面的步骤。桥会在插话后定位到**自己那条 user/message**,只取它之后的 assistant 文本作为回答。
|
|
59
|
+
The desktop app and the CLI share `~/.codex/config.toml`, so both see it. The token lives in the URL path, so no environment variable is needed — the desktop app cannot receive custom ones when launched from Finder. `Authorization: Bearer <token>` works too.
|
|
143
60
|
|
|
144
|
-
|
|
61
|
+
### Claude Code
|
|
145
62
|
|
|
146
|
-
|
|
63
|
+
```bash
|
|
64
|
+
claude mcp add --transport http dsh-bridge "<the url above>" --scope user
|
|
65
|
+
```
|
|
147
66
|
|
|
148
|
-
|
|
67
|
+
## Usage
|
|
149
68
|
|
|
150
69
|
```js
|
|
151
|
-
//
|
|
152
|
-
dsh_task({ prompt: "
|
|
70
|
+
// Synchronous: wait for the turn, get the answer
|
|
71
|
+
dsh_task({ prompt: "Review the error handling in src/bridge.ts", cwd: "/repo" })
|
|
153
72
|
|
|
154
|
-
//
|
|
155
|
-
dsh_spawn({ task_name: "
|
|
73
|
+
// Asynchronous: hand out work, then wait for all of it
|
|
74
|
+
dsh_spawn({ task_name: "audit_api", message: "…", cwd: "/repo" })
|
|
75
|
+
dsh_spawn({ task_name: "audit_ui", message: "…", cwd: "/repo" })
|
|
76
|
+
dsh_wait({ targets: ["audit_api", "audit_ui"], timeoutMs: 240000 })
|
|
156
77
|
|
|
157
|
-
//
|
|
158
|
-
|
|
78
|
+
// Continue an existing worker
|
|
79
|
+
dsh_followup({ worker: "audit_api", message: "Expand item 3 and attach a patch" })
|
|
159
80
|
```
|
|
160
81
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
|
|
164
|
-
- 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
|
|
165
|
-
- 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
|
|
166
|
-
- 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
|
|
82
|
+
## Tools
|
|
167
83
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
DSH 的 prompt 有两种投递模式,桥把它们交给调用方选:
|
|
171
|
-
|
|
172
|
-
| `mode` | 行为 |
|
|
84
|
+
| Tool | Purpose |
|
|
173
85
|
| --- | --- |
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
86
|
+
| `dsh_task` | Synchronous dispatch: create session, deliver, block to end of turn, return the final answer |
|
|
87
|
+
| `dsh_spawn` | Asynchronous dispatch: returns `task_name` + `sessionId` immediately |
|
|
88
|
+
| `dsh_followup` | Another round on an existing worker, reusing its context |
|
|
89
|
+
| `dsh_wait` | Wait on several workers at once; returns when any reaches a terminal state |
|
|
90
|
+
| `dsh_close` | Close a worker and free its slot (the session stays) |
|
|
91
|
+
| `dsh_answer` | Answer a worker's question (limited — see below) |
|
|
92
|
+
| `dsh_list_workers` | List workers started by this bridge and their live status |
|
|
93
|
+
| `dsh_dispatch` | Deliver one message to any session; returns `requestId` and `seqAtDispatch` |
|
|
94
|
+
| `dsh_read_result` | Long-poll the assistant text produced after `seqAtDispatch` |
|
|
95
|
+
| `dsh_list_sessions` | List session metadata only (id, title, cwd, running, last activity) |
|
|
177
96
|
|
|
178
|
-
`
|
|
97
|
+
`dsh_list_sessions` never returns message content, and `dsh_read_result` only works for sessions this bridge has dispatched to.
|
|
98
|
+
|
|
99
|
+
### Model
|
|
179
100
|
|
|
180
101
|
```js
|
|
181
|
-
|
|
182
|
-
|
|
102
|
+
dsh_task({ prompt: "…", model: "zai-coding-cn/glm-5.3", reasoningEffort: "max" })
|
|
103
|
+
dsh_spawn({ task_name: "review", message: "…", model: "opencode-go/deepseek-v4.1-flash" })
|
|
183
104
|
```
|
|
184
105
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
1. **`steer` 不会打断正在执行的工具**。实测:worker 正在跑 `sleep 90`,`steer` 进去后消息被**并入当前回合**,但要等那一步工具调用结束才会被模型看到——整整等了 70 秒。原因是 `steer` 只是投递,`cancel` 才是中止。
|
|
190
|
-
2. **`steer` 不新开回合**,所以等待条件不能是"下一个回合结束"。早期实现那样写,答案已经回来了、调用还在等,最后 120 秒超时。现在 steer 等的是「当前回合结束」。
|
|
191
|
-
|
|
192
|
-
所以真要"按下就停",用 `interrupt`:它先 `cancel` 掉当前回合(最多等 15 秒确认),再以普通排队模式发消息,开启新回合。
|
|
193
|
-
|
|
194
|
-
返回里带 `mode` 字段,方便确认这次是排队还是打断。
|
|
106
|
+
- `provider/model`; a bare model id keeps the current default provider.
|
|
107
|
+
- `reasoningEffort` is optional and model-dependent.
|
|
108
|
+
- Omitted means the session is untouched.
|
|
109
|
+
- The switch happens before the first prompt, so the worker's first turn already runs on it, and the profile default model is not modified.
|
|
195
110
|
|
|
196
|
-
|
|
111
|
+
### Workspace
|
|
197
112
|
|
|
198
|
-
|
|
113
|
+
DSH groups sessions into projects; a new session lands under "Ungrouped" unless its directory is already a project.
|
|
199
114
|
|
|
200
115
|
```js
|
|
201
|
-
dsh_task({ prompt: "…",
|
|
202
|
-
dsh_spawn({ task_name: "
|
|
203
|
-
|
|
116
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true }) // create, titled after the directory
|
|
117
|
+
dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
|
|
118
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer" }) // reuse only
|
|
204
119
|
```
|
|
205
120
|
|
|
206
|
-
|
|
207
|
-
- `reasoningEffort` 可选,取值随模型(`low` / `medium` / `high` / `max` 等);
|
|
208
|
-
- 不传 `model` 就完全不动会话,行为和以前一致;
|
|
209
|
-
- 实测(Host 侧 `sessionController.selectModel`):请求头里的 provider/model 确实变成指定的那组,且**不会改写 profile 的默认模型**。
|
|
121
|
+
Missing directories are created first (registration requires an existing directory). An existing project is reused without retitling. The result carries `workspace: { id, title, path }`. A failure here never fails the dispatch.
|
|
210
122
|
|
|
211
|
-
|
|
123
|
+
### Queue or steer
|
|
212
124
|
|
|
213
|
-
|
|
125
|
+
DSH's "sending while busy" has exactly two behaviours:
|
|
214
126
|
|
|
215
|
-
|
|
|
127
|
+
| `mode` | Behaviour |
|
|
216
128
|
| --- | --- |
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `dsh_followup` | 给已有 worker 再派一轮(复用它的上下文) |
|
|
220
|
-
| `dsh_close` | 关闭 worker,释放并发名额(不删除会话) |
|
|
221
|
-
| `dsh_list_workers` | 列出本桥开的 worker 及其实时状态 |
|
|
222
|
-
| `dsh_wait` | 一次等多批 worker:`targets` + `timeoutMs`,任一到达终态即返回 `{completed}` / `{errored}` / `"running"` / `"not_found"` |
|
|
223
|
-
| `dsh_dispatch` | 向指定会话投递一条用户消息并开始一轮;返回 `requestId` 与 `seqAtDispatch` |
|
|
224
|
-
| `dsh_read_result` | **长轮询**读进度:返回该会话在 `seqAtDispatch` 之后的**全部**助手文本(用于看过程;要结果用 `dsh_wait`) |
|
|
225
|
-
| `dsh_list_sessions` | 列出会话**元数据**(id、标题、工作目录、是否运行、最后活动时间) |
|
|
226
|
-
|
|
227
|
-
## 反向:DSH → Codex
|
|
129
|
+
| `queue` (default) | Wait for the current turn, then deliver as a new turn |
|
|
130
|
+
| `steer` | Insert into the current turn; the model sees it at the next step |
|
|
228
131
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
| --- | --- | --- |
|
|
233
|
-
| `codex_dispatch` | `prompt`(必填)、`cwd`、`resume`(线程 id)、`sandbox` | `{ ok, text, threadId, exitCode, durationMs, error? }` |
|
|
234
|
-
|
|
235
|
-
它执行 `codex exec [resume <id>] --json -o <临时文件>`,从事件流取 `thread.started`、从 `-o` 文件取最终消息;工具结果里会带 `[codex thread <id> · 12s]`,把那个 id 传给 `resume` 就能继续同一个 Codex 线程(实测能记住上一轮的回复)。
|
|
132
|
+
```js
|
|
133
|
+
dsh_followup({ worker: "w1", message: "Only touch files under src from here", mode: "steer" })
|
|
134
|
+
```
|
|
236
135
|
|
|
237
|
-
|
|
136
|
+
Two measured facts:
|
|
238
137
|
|
|
239
|
-
|
|
138
|
+
1. **`steer` does not interrupt a running tool call.** A worker inside `sleep 90` only sees the message when that call returns (70s in our test). Stopping it is the UI's cancel action, not a delivery mode.
|
|
139
|
+
2. **`steer` does not open a new turn** — the turn number stays the same, so the bridge waits for the *current* turn to end. Waiting for the next one blocks until the timeout.
|
|
240
140
|
|
|
241
|
-
|
|
242
|
-
- `dsh_read_result` 只对**本桥派发过**的会话生效;从未派发过的会话一律拒绝。
|
|
141
|
+
`dsh_task` refuses a running session unless `mode: "steer"` is given, and the error message says so. A steered reply shares the turn with the original task, so the bridge anchors the answer on its own user message rather than the turn's last assistant text.
|
|
243
142
|
|
|
244
|
-
|
|
143
|
+
### Unattended workers
|
|
245
144
|
|
|
246
|
-
|
|
145
|
+
A worker that calls `ask_user_question` waits for a human. When the caller is another agent, nobody is there. So the bridge appends a standing instruction to every delegated task:
|
|
247
146
|
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
|
|
147
|
+
```
|
|
148
|
+
(如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,
|
|
149
|
+
不要把选择权抛回给用户、不要调用 ask_user_question 等回答;
|
|
150
|
+
在结果里用一行说明你假设了什么。)
|
|
251
151
|
```
|
|
252
152
|
|
|
253
|
-
|
|
153
|
+
On by default (`assumeDefaults: false` disables it). It is skipped when the prompt already mentions `ask_user_question`.
|
|
254
154
|
|
|
255
|
-
|
|
155
|
+
### Questions (`pendingQuestion`)
|
|
256
156
|
|
|
257
|
-
|
|
157
|
+
If a worker does ask, `dsh_wait` reports it:
|
|
158
|
+
|
|
159
|
+
```jsonc
|
|
258
160
|
{
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
161
|
+
"status": { "w1": "running" },
|
|
162
|
+
"pendingQuestion": {
|
|
163
|
+
"callId": "call_abc",
|
|
164
|
+
"questions": [{ "id": "mode", "question": "Which mode?", "options": [{ "label": "Fast" }] }]
|
|
165
|
+
},
|
|
166
|
+
"next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
|
|
264
167
|
}
|
|
265
168
|
```
|
|
266
169
|
|
|
267
|
-
|
|
170
|
+
`dsh_answer({ worker, answers: [{ id: "mode", selected: ["Fast"] }] })` submits a batch.
|
|
268
171
|
|
|
269
|
-
|
|
172
|
+
**Known limit:** DSH's `userQuestions.answer()` only accepts questions that have already timed out into the `continued` state. A question still blocking inside the tool call belongs to the web UI's answerer, and the bridge cannot answer it (`answered: false` in practice). Prefer `assumeDefaults` over waiting for a question.
|
|
270
173
|
|
|
271
|
-
|
|
272
|
-
codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
|
|
273
|
-
```
|
|
174
|
+
## Reverse direction
|
|
274
175
|
|
|
275
|
-
|
|
176
|
+
Both tools are registered for agents of this deployment.
|
|
276
177
|
|
|
277
|
-
|
|
278
|
-
codex mcp list
|
|
279
|
-
codex mcp get dsh-bridge
|
|
280
|
-
```
|
|
178
|
+
### `codex_dispatch`
|
|
281
179
|
|
|
282
|
-
|
|
180
|
+
| Parameter | Description |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `prompt` | Required task text |
|
|
183
|
+
| `cwd` | Working directory; defaults to the calling session's |
|
|
184
|
+
| `resume` | Codex thread id to continue |
|
|
185
|
+
| `sandbox` | `read-only` / `workspace-write` / `danger-full-access` |
|
|
283
186
|
|
|
284
|
-
>
|
|
187
|
+
Returns `{ ok, text, threadId, exitCode, durationMs }`. Runs `codex exec [resume <id>] --json -o <tmpfile>`, reads `thread.started` from the event stream and the final message from the `-o` file. **`sandbox` and `cwd` are ignored on `resume`** — `codex exec resume` accepts neither.
|
|
285
188
|
|
|
286
|
-
|
|
189
|
+
### `claude_dispatch`
|
|
287
190
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
191
|
+
| Parameter | Description |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `prompt` | Required task text |
|
|
194
|
+
| `cwd` | Working directory |
|
|
195
|
+
| `resume` | Claude Code session id |
|
|
196
|
+
| `model` | Model alias or id; falls back to config, then to the CLI's own setting |
|
|
291
197
|
|
|
292
|
-
|
|
198
|
+
Returns `{ ok, text, sessionId, durationMs }`. Runs `claude -p --output-format json` and reads the single JSON object on stdout. **stderr is advisory** — the CLI warns about an unrecognised model suffix while still succeeding.
|
|
293
199
|
|
|
294
|
-
##
|
|
200
|
+
## Configuration
|
|
295
201
|
|
|
296
202
|
```yaml
|
|
297
203
|
- insert:
|
|
@@ -301,155 +207,108 @@ codex mcp remove dsh-bridge
|
|
|
301
207
|
port: 47821
|
|
302
208
|
```
|
|
303
209
|
|
|
304
|
-
|
|
|
210
|
+
| Key | Default | Description |
|
|
305
211
|
| --- | --- | --- |
|
|
306
|
-
| `enabled` | `true` |
|
|
307
|
-
| `port` | `47821` |
|
|
308
|
-
| `endpointFile` | `~/.dsh/bridge/endpoint.json` |
|
|
309
|
-
| `maxResultChars` | `20000` |
|
|
310
|
-
| `taskTimeoutSeconds` | `300` | `dsh_task`
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
**① 深度(`maxHops`,默认 1)**
|
|
335
|
-
|
|
336
|
-
- 每次都计算这次派发会产生的深度:`max(当前在飞的链路深度, 调用方深度) + 1`;
|
|
337
|
-
- 调用方是桥建的 worker 时按其深度 1 起算;
|
|
338
|
-
- `codex_dispatch` 运行期间会把链路标记为"在飞",嵌套回来的调用继承该深度;
|
|
339
|
-
- 超限直接报错,措辞可执行:
|
|
340
|
-
`codex_dispatch would create delegation depth 2, but maxHops is 1. Carry the work out directly instead of delegating again.`
|
|
212
|
+
| `enabled` | `true` | Master switch; when off, no listener and no endpoint file |
|
|
213
|
+
| `port` | `47821` | Loopback port; fixed so the client URL survives restarts |
|
|
214
|
+
| `endpointFile` | `~/.dsh/bridge/endpoint.json` | Where the endpoint and token are written |
|
|
215
|
+
| `maxResultChars` | `20000` | Cap on a single result payload (the head is kept) |
|
|
216
|
+
| `taskTimeoutSeconds` | `300` | `dsh_task` wait cap (client may override, max 240) |
|
|
217
|
+
| `assumeDefaults` | `true` | Append the decide-and-continue instruction |
|
|
218
|
+
| `workersFile` | `~/.dsh/bridge/workers.json` | Worker registry (`0600`) |
|
|
219
|
+
| `maxHops` | `1` | Delegation depth limit |
|
|
220
|
+
| `dispatchPerMinute` | `10` | Dispatches allowed per window |
|
|
221
|
+
| `dispatchWindowMs` | `60000` | Window length |
|
|
222
|
+
| `maxWorkersPerRoot` | `4` | Workers per working directory |
|
|
223
|
+
| `maxWorkers` | `12` | Global worker cap |
|
|
224
|
+
| `workerTtlMs` | `86400000` | Records stop counting once stale |
|
|
225
|
+
| `waitTimeoutMs` | `240000` | Default `dsh_wait` timeout |
|
|
226
|
+
| `readWaitSeconds` | `25` | Default `dsh_read_result` long-poll |
|
|
227
|
+
| `codexBin` | `codex` | Codex executable; a value with `/` is resolved strictly |
|
|
228
|
+
| `codexSandbox` | `workspace-write` | Default sandbox for `codex_dispatch` |
|
|
229
|
+
| `codexTimeoutMs` | `300000` | Per-run timeout |
|
|
230
|
+
| `claudeBin` | `claude` | Claude Code executable |
|
|
231
|
+
| `claudeModel` | *(empty)* | Empty means the CLI's own setting |
|
|
232
|
+
| `claudeTimeoutMs` | `300000` | Per-run timeout |
|
|
233
|
+
|
|
234
|
+
A busy port is reported, not silently replaced: the endpoint file gets an `error` field and the host log prints `subagent-bridge: could not start on port …`.
|
|
235
|
+
|
|
236
|
+
## Guardrails
|
|
237
|
+
|
|
238
|
+
**Depth.** Both agents can see the same bridge, so a circle is possible: DSH worker → `codex_dispatch` → that Codex run → `dsh_spawn` → … Each dispatch computes `max(in-flight depth, caller depth) + 1` and refuses beyond `maxHops`:
|
|
341
239
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
240
|
+
```
|
|
241
|
+
codex_dispatch would create delegation depth 2, but maxHops is 1.
|
|
242
|
+
Carry the work out directly instead of delegating again.
|
|
243
|
+
```
|
|
345
244
|
|
|
346
|
-
|
|
245
|
+
**Rate.** `dispatchPerMinute` (default 10) catches bursts and tight retry loops:
|
|
347
246
|
|
|
348
247
|
```
|
|
349
248
|
rate limit: 10 dispatches per 60s reached; retry in 23s
|
|
350
249
|
```
|
|
351
250
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
worker 名额**按工作目录分桶**,调用方不用传任何 owner:
|
|
355
|
-
|
|
356
|
-
- 每个 `cwd` 默认 **4 个**(`maxWorkersPerRoot`);
|
|
357
|
-
- 全局默认 **12 个**(`maxWorkers`),跨项目兜底;
|
|
358
|
-
- **完成但没 `dsh_close` 的仍占名额**(与 Codex 一致);
|
|
359
|
-
- 记录 **24 小时**没更新就自动不再占名额(`workerTtlMs`),不需要手动清理;
|
|
360
|
-
- 阻塞式 `dsh_task` 不计入名额,只登记。
|
|
361
|
-
|
|
362
|
-
为什么不是"每个会话 4 个":MCP 端点是共享的,请求里没有会话或线程身份(Codex 的 `CODEX_THREAD_ID` 只存在于 shell 环境)。`cwd` 是唯一由调用方稳定提供、且语义上对应"一个项目"的维度。
|
|
363
|
-
|
|
364
|
-
## 跨重启
|
|
365
|
-
|
|
366
|
-
worker 注册表(`task_name` / `sessionId` / 终态结果)和每轮的时间基线都落在 `~/.dsh/bridge/workers.json`:
|
|
251
|
+
**Slots.** 4 workers per `cwd`, 12 globally. A finished worker keeps its slot until `dsh_close`; records older than 24h stop counting. Blocking `dsh_task` calls are registered but do not consume a slot. Buckets are keyed by `cwd` because the MCP endpoint is shared and carries no session identity.
|
|
367
252
|
|
|
368
|
-
|
|
369
|
-
- **重启后 `dsh_list_workers` 仍能看到之前的 worker**;
|
|
370
|
-
- 已经结束的 worker,`dsh_wait` 直接返回缓存里的最终回答(不必重新打开会话);
|
|
371
|
-
- 重启时还在跑的 worker,会话未打开时状态是 `offline`(结果未知,去 DSH 侧边栏打开那个会话即可查看);
|
|
372
|
-
- `dsh_followup` 会给 worker 开新一轮,同时**清掉**缓存的旧回答;
|
|
373
|
-
- 文件最多保留 200 条,超出时先淘汰最旧的 **已关闭** worker。
|
|
253
|
+
**Persistence.** `workers.json` keeps the registry and each turn's baseline: workers survive a restart, finished ones replay their cached answer, cold sessions report `offline`, and the file keeps at most 200 records.
|
|
374
254
|
|
|
375
|
-
|
|
255
|
+
**Security.** Loopback only; every request needs the token in the path or a bearer header, compared in constant time; the token is generated once and persisted. Holding the endpoint file is equivalent to being able to send messages to sessions — keep its `0600`/`0700` permissions. DSH's own sandbox and approval rules still apply to anything delivered.
|
|
376
256
|
|
|
377
|
-
|
|
378
|
-
- 请求必须携带令牌:路径 `/mcp/<token>`,或 `Authorization: Bearer <token>`;比较使用定长时间比较。
|
|
379
|
-
- 令牌首次启动时生成并**持久化**(重启后复用),因此能力 URL 稳定。
|
|
380
|
-
- 能力等价于「向指定会话投递消息」:本机任意进程只要能读到端点文件就能派活。文件权限 0600、目录 0700;不要把它复制到共享位置。
|
|
381
|
-
- DSH 侧的权限策略不受影响:派发进去的消息仍按会话自身的 sandbox / 审批策略执行。
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
## 实现
|
|
257
|
+
## How it works
|
|
386
258
|
|
|
387
259
|
```
|
|
388
|
-
Codex /
|
|
389
|
-
│ POST /mcp/<token> JSON-RPC
|
|
260
|
+
Codex / any MCP client
|
|
261
|
+
│ POST /mcp/<token> JSON-RPC (stateless, single JSON response)
|
|
390
262
|
▼
|
|
391
|
-
dsh-plugin-subagent-bridge
|
|
392
|
-
· node:http
|
|
263
|
+
dsh-plugin-subagent-bridge (inside the host process)
|
|
264
|
+
· node:http on 127.0.0.1:47821
|
|
393
265
|
· initialize / tools/list / tools/call / ping
|
|
394
|
-
· sessionController.prompt({sessionId, requestId, mode, content})
|
|
395
|
-
· sessions.get(id).snapshotEvents()
|
|
266
|
+
· sessionController.prompt({ sessionId, requestId, mode, content })
|
|
267
|
+
· sessions.get(id).snapshotEvents() to read text back
|
|
396
268
|
```
|
|
397
269
|
|
|
398
|
-
|
|
270
|
+
The protocol is a stateless subset of streamable HTTP: each POST returns one `application/json` body and nothing is pushed, so results are collected by long polling. The server stops waiting the moment the client disconnects.
|
|
399
271
|
|
|
400
|
-
|
|
272
|
+
## Skills
|
|
401
273
|
|
|
402
|
-
|
|
274
|
+
Two installable skills turn this into behaviour a model follows rather than a tool it might forget:
|
|
403
275
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
| Skill | 装给谁 | 作用 |
|
|
276
|
+
| Skill | Install to | Purpose |
|
|
407
277
|
| --- | --- | --- |
|
|
408
|
-
| `deepseek-worker` | Codex |
|
|
409
|
-
| `delegate-to-external-agent` | DSH |
|
|
278
|
+
| `deepseek-worker` | Codex | When and how Codex should delegate to DSH |
|
|
279
|
+
| `delegate-to-external-agent` | DSH | When DSH should delegate to Codex or Claude Code |
|
|
410
280
|
|
|
411
281
|
```bash
|
|
412
|
-
# Codex 侧
|
|
413
282
|
mkdir -p ~/.codex/skills && cp -R skills/deepseek-worker ~/.codex/skills/
|
|
414
|
-
|
|
415
|
-
# DSH 侧
|
|
416
|
-
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
|
|
283
|
+
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
|
|
417
284
|
```
|
|
418
285
|
|
|
419
|
-
|
|
286
|
+
Both are plain `SKILL.md` files: YAML frontmatter (`name` + `description`) plus body. Triggering depends entirely on the description, so they carry bilingual trigger phrases and explicit "don't use" boundaries. Changes take effect without a restart.
|
|
420
287
|
|
|
421
|
-
|
|
288
|
+
## Compatibility
|
|
422
289
|
|
|
423
|
-
|
|
290
|
+
- DSH `^0.2.1-0` (`engines.dsh`).
|
|
291
|
+
- MCP protocol versions `2025-06-18`, `2025-03-26`, `2024-11-05`; unknown versions fall back to the newest.
|
|
292
|
+
- Requires the public services `sessionController`, `sessions`, `workspaceRegistry`, `userQuestions`. Missing ones surface as explicit tool errors.
|
|
424
293
|
|
|
425
|
-
|
|
426
|
-
- MCP 协议版本:`2025-06-18`、`2025-03-26`、`2024-11-05`(按客户端请求回显,未知版本回落到最新)。
|
|
427
|
-
- 依赖宿主公开服务 `sessionController`(`list` / `prompt`)与 `sessions.get()`;两者不可用时工具会返回明确错误。
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
## 开发
|
|
294
|
+
## Development
|
|
432
295
|
|
|
433
296
|
```bash
|
|
434
297
|
node --test "test/*.test.mjs"
|
|
435
298
|
```
|
|
436
299
|
|
|
437
|
-
|
|
438
|
-
`test/persistence.test.mjs` 单独覆盖持久化:文件权限 0600、缺失/损坏/畸形记录、spawn 返回时文件已落盘、完成即缓存、followup 清缓存、close 后重启仍为 closed、冷会话走缓存或 `offline`、并发写不串。
|
|
300
|
+
83 tests: handshake and version negotiation, tool list and argument validation, dispatch call shape, result baselines, long polling, real HTTP round trips (404/405/202 and bearer), `codex exec` event parsing, binary resolution, success/failure/timeout runs, resume argument split, final-answer-only behaviour, `dsh_wait` terminal states, slot limits and release, followup rebaselining, registry persistence and restart recovery, `maxHops` and rate limiting, per-`cwd` budgets, workspace creation and reuse, `queue`/`steer` delivery and waiting, anchors for steered answers, Claude output parsing and resume arguments, `assumeDefaults` injection, `pendingQuestion` and `dsh_answer`.
|
|
439
301
|
|
|
440
|
-
|
|
302
|
+
## Related
|
|
441
303
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
| 插件 | 说明 |
|
|
304
|
+
| Plugin | Description |
|
|
445
305
|
| --- | --- |
|
|
446
|
-
|
|
|
447
|
-
| [dsh-plugin-composer-
|
|
448
|
-
|
|
|
449
|
-
| [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) |
|
|
450
|
-
| [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) |
|
|
451
|
-
|
|
306
|
+
| **dsh-plugin-subagent-bridge** (this repo) | Let Codex drive DSH as a subagent, and DSH drive Codex / Claude Code |
|
|
307
|
+
| [dsh-plugin-composer-predict](https://github.com/Dogtiti/dsh-plugin-composer-predict) | Predicts the next instruction as placeholder text; `Tab` accepts |
|
|
308
|
+
| [dsh-plugin-composer-history](https://github.com/Dogtiti/dsh-plugin-composer-history) | `↑` / `↓` in the composer to recall previous messages |
|
|
309
|
+
| [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) | Provider quota next to the model picker |
|
|
310
|
+
| [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | Provider logos next to model names |
|
|
452
311
|
|
|
453
|
-
##
|
|
312
|
+
## License
|
|
454
313
|
|
|
455
314
|
MIT © [Dogtiti](https://github.com/Dogtiti)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# dsh-plugin-subagent-bridge
|
|
2
|
+
|
|
3
|
+
> 让 Codex 或 Claude Code 把 DeepSeek Harness 会话当作 subagent 来用 —— 反过来,DSH 也能把任务派给它们。
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
7
|
+
<!-- M8ven ownership proof — keep this line in place; it verifies the publisher. -->
|
|
8
|
+
[](https://m8ven.ai/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?s=readme)
|
|
9
|
+
|
|
10
|
+
[English](README.md)
|
|
11
|
+
|
|
12
|
+
插件运行在 Harness 宿主进程内,对外暴露一个回环 MCP 端点。它只用宿主公开服务(`sessionController`、`sessions`、`workspaceRegistry`、`userQuestions`)——不读凭据文件,也不伪造 cookie。
|
|
13
|
+
|
|
14
|
+
## 特性
|
|
15
|
+
|
|
16
|
+
- **一次调用,一个答案。** `dsh_task` 开会话、投递消息、阻塞到这一轮结束,只返回最终的助手文本。
|
|
17
|
+
- **并行 worker。** `dsh_spawn` 立即返回;`dsh_wait` 阻塞到其中任意一个到达终态。
|
|
18
|
+
- **按次指定模型与项目。** 把 worker 钉在 `provider/model` 上、归入指定项目,且不改动 profile 默认值。
|
|
19
|
+
- **排队或插话。** 由调用方决定后续消息是等当前回合,还是插进当前回合。
|
|
20
|
+
- **默认无人值守。** worker 被告知采用合理默认继续,而不是停下来提问。
|
|
21
|
+
- **反向能力。** `codex_dispatch` 与 `claude_dispatch` 把任务交给 Codex 或 Claude Code,且线程可续接。
|
|
22
|
+
- **防环。** 深度与速率限制阻止两个 agent 互相派活绕圈。
|
|
23
|
+
|
|
24
|
+
## 环境要求
|
|
25
|
+
|
|
26
|
+
- DeepSeek Harness `^0.2.1-0`。
|
|
27
|
+
- 反向方向需要 `PATH` 上有 Codex CLI(`codex`)和/或 Claude Code CLI(`claude`)。
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/Dogtiti/dsh-plugin-subagent-bridge
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
在 DSH 里打开 **Plugins → 添加**,选择该目录。启动后插件监听 `127.0.0.1:47821`,并写入端点文件:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"url": "http://127.0.0.1:47821/mcp/<token>",
|
|
40
|
+
"port": 47821,
|
|
41
|
+
"token": "<token>",
|
|
42
|
+
"pid": 12345,
|
|
43
|
+
"startedAt": "2026-10-22T12:30:00.000Z"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`~/.dsh/bridge/endpoint.json`,目录 `0700`,文件 `0600`。
|
|
48
|
+
|
|
49
|
+
## 连接
|
|
50
|
+
|
|
51
|
+
### Codex
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
|
|
55
|
+
codex mcp list # 验证
|
|
56
|
+
codex mcp remove dsh-bridge # 移除
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
桌面 App 与 CLI 共用 `~/.codex/config.toml`,两端都能看到。token 放在 URL 路径里,因此不需要环境变量——桌面 App 从 Finder 启动时拿不到自定义环境变量。也支持 `Authorization: Bearer <token>`。
|
|
60
|
+
|
|
61
|
+
### Claude Code
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
claude mcp add --transport http dsh-bridge "<上面那个 url>" --scope user
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 用法
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// 同步:等这一轮跑完,直接拿答案
|
|
71
|
+
dsh_task({ prompt: "审一下 src/bridge.ts 的错误处理", cwd: "/repo" })
|
|
72
|
+
|
|
73
|
+
// 异步:先派活,再一起等
|
|
74
|
+
dsh_spawn({ task_name: "audit_api", message: "…", cwd: "/repo" })
|
|
75
|
+
dsh_spawn({ task_name: "audit_ui", message: "…", cwd: "/repo" })
|
|
76
|
+
dsh_wait({ targets: ["audit_api", "audit_ui"], timeoutMs: 240000 })
|
|
77
|
+
|
|
78
|
+
// 续派:复用已有 worker 的上下文
|
|
79
|
+
dsh_followup({ worker: "audit_api", message: "把第 3 条展开,附上补丁" })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 工具
|
|
83
|
+
|
|
84
|
+
| 工具 | 作用 |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `dsh_task` | 同步派活:开会话、投递、阻塞到回合结束,返回最终回答 |
|
|
87
|
+
| `dsh_spawn` | 异步派活:立刻返回 `task_name` + `sessionId` |
|
|
88
|
+
| `dsh_followup` | 在已有 worker 上再派一轮,复用其上下文 |
|
|
89
|
+
| `dsh_wait` | 一次等多个 worker,任一到达终态即返回 |
|
|
90
|
+
| `dsh_close` | 关闭 worker 并释放名额(会话保留) |
|
|
91
|
+
| `dsh_answer` | 回答 worker 提出的问题(有已知限制,见下) |
|
|
92
|
+
| `dsh_list_workers` | 列出本桥开的 worker 及实时状态 |
|
|
93
|
+
| `dsh_dispatch` | 向任意会话投递一条消息;返回 `requestId` 与 `seqAtDispatch` |
|
|
94
|
+
| `dsh_read_result` | 长轮询读取 `seqAtDispatch` 之后产生的助手文本 |
|
|
95
|
+
| `dsh_list_sessions` | 只列会话元数据(id、标题、cwd、是否运行、最后活动时间) |
|
|
96
|
+
|
|
97
|
+
`dsh_list_sessions` 从不返回消息内容;`dsh_read_result` 只对本桥派发过的会话有效。
|
|
98
|
+
|
|
99
|
+
### 指定模型
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
dsh_task({ prompt: "…", model: "zai-coding-cn/glm-5.3", reasoningEffort: "max" })
|
|
103
|
+
dsh_spawn({ task_name: "review", message: "…", model: "opencode-go/deepseek-v4.1-flash" })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- 写法是 `provider/model`;只写模型名时沿用当前默认 provider。
|
|
107
|
+
- `reasoningEffort` 可选,取值随模型。
|
|
108
|
+
- 不传则完全不动会话。
|
|
109
|
+
- 切换发生在第一次 prompt 之前,所以 worker 第一轮就已经在新模型上跑,且不会改动 profile 的默认模型。
|
|
110
|
+
|
|
111
|
+
### 项目归属
|
|
112
|
+
|
|
113
|
+
DSH 把会话分组为项目;新建会话默认落在「未分组」,除非所在目录已经是项目。
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true }) // 创建,标题取目录名
|
|
117
|
+
dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
|
|
118
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer" }) // 只复用
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
目录不存在会先创建(登记要求目录已存在)。已是项目则复用且**不改标题**。返回里带 `workspace: { id, title, path }`。项目操作失败不会让派发失败。
|
|
122
|
+
|
|
123
|
+
### 排队或插话
|
|
124
|
+
|
|
125
|
+
DSH 的「繁忙时的发送行为」只有两种:
|
|
126
|
+
|
|
127
|
+
| `mode` | 行为 |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `queue`(默认) | 等当前回合结束,作为新回合投递 |
|
|
130
|
+
| `steer` | 插入当前回合,模型在下一步就能看到 |
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
dsh_followup({ worker: "w1", message: "接下来只改 src 下的文件", mode: "steer" })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
两个实测结论:
|
|
137
|
+
|
|
138
|
+
1. **`steer` 不打断正在执行的工具调用。** worker 若正卡在 `sleep 90` 里,要等那次调用返回才看得到消息(实测 70 秒)。真的停下是界面上的「取消」,不是投递模式。
|
|
139
|
+
2. **`steer` 不新开回合** —— turn 编号不变,所以桥等的是**当前**回合结束;等「下一个回合」会一直阻塞到超时。
|
|
140
|
+
|
|
141
|
+
`dsh_task` 对运行中的会话默认拒绝,除非传 `mode: "steer"`,错误信息里也这么写。插话的回复与原任务同一回合,所以桥以**自己那条用户消息**为锚点取回答,而不是这一轮最后一条助手文本。
|
|
142
|
+
|
|
143
|
+
### 无人值守
|
|
144
|
+
|
|
145
|
+
调用 `ask_user_question` 的 worker 会等人回答。调用方是另一个 agent 时,没有人在这。因此桥给每个派发的任务追加一条常驻指令:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
(如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,
|
|
149
|
+
不要把选择权抛回给用户、不要调用 ask_user_question 等回答;
|
|
150
|
+
在结果里用一行说明你假设了什么。)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
默认开启(`assumeDefaults: false` 可关闭)。prompt 里本来就提到 `ask_user_question` 时不追加。
|
|
154
|
+
|
|
155
|
+
### 问题上报
|
|
156
|
+
|
|
157
|
+
worker 真的提问时,`dsh_wait` 会把问题带出来:
|
|
158
|
+
|
|
159
|
+
```jsonc
|
|
160
|
+
{
|
|
161
|
+
"status": { "w1": "running" },
|
|
162
|
+
"pendingQuestion": {
|
|
163
|
+
"callId": "call_abc",
|
|
164
|
+
"questions": [{ "id": "mode", "question": "用哪种模式?", "options": [{ "label": "快速" }] }]
|
|
165
|
+
},
|
|
166
|
+
"next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`dsh_answer({ worker, answers: [{ id: "mode", selected: ["快速"] }] })` 提交答案。
|
|
171
|
+
|
|
172
|
+
**已知限制:** DSH 的 `userQuestions.answer()` 只接受已经超时进入 `continued` 状态的问题。仍阻塞在工具调用里的问题由 Web UI 的 answerer 接管,桥答不了(实测返回 `answered: false`)。实际使用中应依赖 `assumeDefaults`,而不是等 worker 提问。
|
|
173
|
+
|
|
174
|
+
## 反向方向
|
|
175
|
+
|
|
176
|
+
两个工具都注册给本部署的 Agent。
|
|
177
|
+
|
|
178
|
+
### `codex_dispatch`
|
|
179
|
+
|
|
180
|
+
| 参数 | 说明 |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `prompt` | 必填,任务正文 |
|
|
183
|
+
| `cwd` | 工作目录;默认用调用方会话的目录 |
|
|
184
|
+
| `resume` | Codex 线程 id,用于续接 |
|
|
185
|
+
| `sandbox` | `read-only` / `workspace-write` / `danger-full-access` |
|
|
186
|
+
|
|
187
|
+
返回 `{ ok, text, threadId, exitCode, durationMs }`。执行 `codex exec [resume <id>] --json -o <临时文件>`,从事件流读 `thread.started`,从 `-o` 文件读最终消息。**`resume` 时 `sandbox` 与 `cwd` 均不生效** —— `codex exec resume` 不接受这两个参数。
|
|
188
|
+
|
|
189
|
+
### `claude_dispatch`
|
|
190
|
+
|
|
191
|
+
| 参数 | 说明 |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `prompt` | 必填,任务正文 |
|
|
194
|
+
| `cwd` | 工作目录 |
|
|
195
|
+
| `resume` | Claude Code 的 session id |
|
|
196
|
+
| `model` | 模型别名或 id;不传则用配置,再退回 CLI 自己的设置 |
|
|
197
|
+
|
|
198
|
+
返回 `{ ok, text, sessionId, durationMs }`。执行 `claude -p --output-format json`,只读 stdout 上的单个 JSON 对象。**stderr 只是提示** —— 模型名后缀不被识别时 CLI 会警告,但结果仍然正常。
|
|
199
|
+
|
|
200
|
+
## 配置
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
- insert:
|
|
204
|
+
- id: subagent-bridge
|
|
205
|
+
name: 'dsh-plugin-subagent-bridge'
|
|
206
|
+
config:
|
|
207
|
+
port: 47821
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| 键 | 默认 | 说明 |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| `enabled` | `true` | 总开关;关闭后不监听、不写端点文件 |
|
|
213
|
+
| `port` | `47821` | 回环端口;固定端口让客户端 URL 重启后依然有效 |
|
|
214
|
+
| `endpointFile` | `~/.dsh/bridge/endpoint.json` | 端点与令牌写入位置 |
|
|
215
|
+
| `maxResultChars` | `20000` | 单次结果载荷上限(保留开头) |
|
|
216
|
+
| `taskTimeoutSeconds` | `300` | `dsh_task` 等待上限(客户端可覆盖,最大 240) |
|
|
217
|
+
| `assumeDefaults` | `true` | 追加「自行决策」指令 |
|
|
218
|
+
| `workersFile` | `~/.dsh/bridge/workers.json` | worker 注册表(`0600`) |
|
|
219
|
+
| `maxHops` | `1` | 委派深度上限 |
|
|
220
|
+
| `dispatchPerMinute` | `10` | 每个窗口允许的派发次数 |
|
|
221
|
+
| `dispatchWindowMs` | `60000` | 窗口长度 |
|
|
222
|
+
| `maxWorkersPerRoot` | `4` | 每个工作目录的 worker 上限 |
|
|
223
|
+
| `maxWorkers` | `12` | 全局 worker 上限 |
|
|
224
|
+
| `workerTtlMs` | `86400000` | 记录过期后不再占名额 |
|
|
225
|
+
| `waitTimeoutMs` | `240000` | `dsh_wait` 默认超时 |
|
|
226
|
+
| `readWaitSeconds` | `25` | `dsh_read_result` 默认长轮询时长 |
|
|
227
|
+
| `codexBin` | `codex` | Codex 可执行文件;含 `/` 时严格按路径解析 |
|
|
228
|
+
| `codexSandbox` | `workspace-write` | `codex_dispatch` 默认 sandbox |
|
|
229
|
+
| `codexTimeoutMs` | `300000` | 单次运行超时 |
|
|
230
|
+
| `claudeBin` | `claude` | Claude Code 可执行文件 |
|
|
231
|
+
| `claudeModel` | 空 | 空表示用 CLI 自己的设置 |
|
|
232
|
+
| `claudeTimeoutMs` | `300000` | 单次运行超时 |
|
|
233
|
+
|
|
234
|
+
端口被占用时会**报错而不是静默换端口**:端点文件里写入 `error` 字段,宿主日志打印 `subagent-bridge: could not start on port …`。
|
|
235
|
+
|
|
236
|
+
## 护栏
|
|
237
|
+
|
|
238
|
+
**深度。** 两个 agent 都能看到同一个桥,因此存在环路:DSH worker → `codex_dispatch` → 那次 Codex → `dsh_spawn` → … 每次派发都计算 `max(在飞链路深度, 调用方深度) + 1`,超过 `maxHops` 直接拒绝:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
codex_dispatch would create delegation depth 2, but maxHops is 1.
|
|
242
|
+
Carry the work out directly instead of delegating again.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**速率。** `dispatchPerMinute`(默认 10)拦截突发与紧循环重试:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
rate limit: 10 dispatches per 60s reached; retry in 23s
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**名额。** 每个 `cwd` 4 个,全局 12 个。完成的 worker 在 `dsh_close` 之前仍占名额;记录超过 24 小时自动不再计数。阻塞式 `dsh_task` 只登记、不占名额。按 `cwd` 分桶是因为 MCP 端点共享且请求里没有会话身份。
|
|
252
|
+
|
|
253
|
+
**持久化。** `workers.json` 保存注册表与每轮基线:重启后 worker 仍在,已完成的直接回放缓存答案,冷会话报告 `offline`,文件最多保留 200 条。
|
|
254
|
+
|
|
255
|
+
**安全。** 只监听回环;每个请求都要令牌(路径或 bearer 头),常量时间比较;令牌只生成一次并持久化。拿到端点文件等价于能向会话投递消息——保持 `0600`/`0700` 权限。DSH 自身的 sandbox 与审批规则对投递内容依然生效。
|
|
256
|
+
|
|
257
|
+
## 工作原理
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
Codex / 任意 MCP 客户端
|
|
261
|
+
│ POST /mcp/<token> JSON-RPC(无状态,单次 JSON 响应)
|
|
262
|
+
▼
|
|
263
|
+
dsh-plugin-subagent-bridge(宿主进程内)
|
|
264
|
+
· node:http 监听 127.0.0.1:47821
|
|
265
|
+
· initialize / tools/list / tools/call / ping
|
|
266
|
+
· sessionController.prompt({ sessionId, requestId, mode, content })
|
|
267
|
+
· sessions.get(id).snapshotEvents() 读回文本
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
协议是 streamable HTTP 的无状态子集:每个 POST 返回一个 `application/json`,没有任何推送,所以结果靠长轮询取。客户端一断开,服务端立刻停止等待。
|
|
271
|
+
|
|
272
|
+
## 配套 skill
|
|
273
|
+
|
|
274
|
+
两份可安装的 skill 把这些能力变成模型会遵循的行为,而不是它可能忘记的工具:
|
|
275
|
+
|
|
276
|
+
| Skill | 装给谁 | 作用 |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `deepseek-worker` | Codex | Codex 何时、如何把任务派给 DSH |
|
|
279
|
+
| `delegate-to-external-agent` | DSH | DSH 何时该派给 Codex 或 Claude Code |
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
mkdir -p ~/.codex/skills && cp -R skills/deepseek-worker ~/.codex/skills/
|
|
283
|
+
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
两份都是普通 `SKILL.md`:YAML frontmatter(`name` + `description`)加正文。触发全靠 description,所以里面写了中英触发语和明确的「不要用」边界。改完不用重启即刻生效。
|
|
287
|
+
|
|
288
|
+
## 兼容性
|
|
289
|
+
|
|
290
|
+
- DSH `^0.2.1-0`(`engines.dsh`)。
|
|
291
|
+
- MCP 协议版本 `2025-06-18`、`2025-03-26`、`2024-11-05`;未知版本回落到最新。
|
|
292
|
+
- 依赖公开服务 `sessionController`、`sessions`、`workspaceRegistry`、`userQuestions`;缺失时以明确的工具错误呈现。
|
|
293
|
+
|
|
294
|
+
## 开发
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
node --test "test/*.test.mjs"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
83 项测试:握手与版本协商、工具清单与参数校验、派发调用形状、结果基线、长轮询、真实 HTTP 往返(404/405/202 与 bearer)、`codex exec` 事件解析、二进制解析、成功/失败/超时、resume 参数分叉、只回最终回答、`dsh_wait` 终态、名额上限与释放、followup 重基线、注册表持久化与重启恢复、`maxHops` 与速率限制、按 `cwd` 的预算、项目创建与复用、`queue`/`steer` 投递与等待、插话回答的锚点、Claude 输出解析与 resume 参数、`assumeDefaults` 注入、`pendingQuestion` 与 `dsh_answer`。
|
|
301
|
+
|
|
302
|
+
## 相关插件
|
|
303
|
+
|
|
304
|
+
| 插件 | 说明 |
|
|
305
|
+
| --- | --- |
|
|
306
|
+
| **dsh-plugin-subagent-bridge**(本仓库) | 让 Codex 把 DSH 当 subagent 用,DSH 也能派给 Codex / Claude Code |
|
|
307
|
+
| [dsh-plugin-composer-predict](https://github.com/Dogtiti/dsh-plugin-composer-predict) | 预测下一条指令并显示为占位文字,`Tab` 采纳 |
|
|
308
|
+
| [dsh-plugin-composer-history](https://github.com/Dogtiti/dsh-plugin-composer-history) | 输入框中按 `↑` / `↓` 回填历史消息 |
|
|
309
|
+
| [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) | 在模型选择器旁显示供应商额度余量 |
|
|
310
|
+
| [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | 在模型名旁显示供应商品牌 logo |
|
|
311
|
+
|
|
312
|
+
## 许可
|
|
313
|
+
|
|
314
|
+
MIT © [Dogtiti](https://github.com/Dogtiti)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subagent-bridge",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
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": [
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"cordis.patch.yml",
|
|
29
29
|
"locale/*.json",
|
|
30
30
|
"icon.svg",
|
|
31
|
-
"skills/*/SKILL.md"
|
|
31
|
+
"skills/*/SKILL.md",
|
|
32
|
+
"README.md",
|
|
33
|
+
"README.zh-CN.md"
|
|
32
34
|
],
|
|
33
35
|
"engines": {
|
|
34
36
|
"dsh": "^0.2.1-0"
|