dsh-plugin-subagent-bridge 0.10.0 → 0.11.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 +22 -2
- package/index.js +35 -8
- package/package.json +1 -1
- package/skills/delegate-to-external-agent/SKILL.md +118 -0
- package/skills/delegate-to-codex/SKILL.md +0 -85
package/README.md
CHANGED
|
@@ -56,6 +56,26 @@ 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
|
+
返回里带 `mode` 字段,方便确认这次是排队还是打断。
|
|
78
|
+
|
|
59
79
|
## 指定模型
|
|
60
80
|
|
|
61
81
|
`dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
|
|
@@ -269,14 +289,14 @@ dsh-plugin-subagent-bridge(宿主进程内)
|
|
|
269
289
|
| Skill | 装给谁 | 作用 |
|
|
270
290
|
| --- | --- | --- |
|
|
271
291
|
| `deepseek-worker` | Codex | 让 Codex 把任务当 subagent 派给 DSH(`dsh_task`),一次调用等结果 |
|
|
272
|
-
| `delegate-to-
|
|
292
|
+
| `delegate-to-external-agent` | DSH | 让 DSH 的 agent 在需要独立视角/复核时派给外部 agent:Codex(`codex_dispatch`)或 Claude Code(`claude_dispatch`),含两者的差别与续接方式 |
|
|
273
293
|
|
|
274
294
|
```bash
|
|
275
295
|
# Codex 侧
|
|
276
296
|
mkdir -p ~/.codex/skills && cp -R skills/deepseek-worker ~/.codex/skills/
|
|
277
297
|
|
|
278
298
|
# DSH 侧
|
|
279
|
-
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-
|
|
299
|
+
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
|
|
280
300
|
```
|
|
281
301
|
|
|
282
302
|
两份都是标准 `SKILL.md`:YAML frontmatter(`name` + `description`)+ 正文。**触发靠 description**——模型平时只看到名字和描述,判断相关了才读正文,所以描述里写满了中英触发语和"不要用"的边界。改完立刻生效,不用重启:Codex 下一次会话、DSH 下一轮就能看到。
|
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,20 @@ 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)
|
|
855
881
|
const before = Number(sessions()?.get?.(targetId)?.seq ?? 0)
|
|
856
882
|
const finished = waitForTurnEnd(ctx, targetId, turnBefore, seconds * 1000)
|
|
857
883
|
const requestId = randomUUID()
|
|
858
884
|
const accepted = await service.prompt(
|
|
859
|
-
{ sessionId: targetId, requestId, mode:
|
|
885
|
+
{ sessionId: targetId, requestId, mode: deliveryMode, content: [{ type: 'text', text: prompt }] },
|
|
860
886
|
AbortSignal.timeout(30000),
|
|
861
887
|
)
|
|
862
888
|
state.dispatched.add(targetId)
|
|
@@ -895,6 +921,7 @@ export function createApi(ctx, state, settings) {
|
|
|
895
921
|
: {}),
|
|
896
922
|
...(appliedModel === undefined ? {} : { model: appliedModel }),
|
|
897
923
|
...(placed === undefined ? {} : { workspace: placed }),
|
|
924
|
+
mode: deliveryMode,
|
|
898
925
|
text: collected.text,
|
|
899
926
|
truncated: collected.truncated,
|
|
900
927
|
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.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": [
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delegate-to-external-agent
|
|
3
|
+
description: 交给外部 agent / 让另一个 agent 审一下 / review / 第二意见 / 交叉验证 / 用另一个模型验证 / 交给 Codex / 交给 Claude 实现时使用。用 codex_dispatch 或 claude_dispatch 把任务派给本机的 Codex 或 Claude Code,一次调用阻塞到它退出拿到回答,可用 resume 续同一会话。不用于几行改动,也不用于对应的 CLI 未安装或未登录时。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 把任务交给外部 agent
|
|
7
|
+
|
|
8
|
+
同一台机器上可能装着另一个编码代理:**Codex**(`codex_dispatch`)或 **Claude Code**(`claude_dispatch`)。两者都是「一次调用 → 阻塞到它退出 → 返回最终回答 + 可续接的会话 id」。
|
|
9
|
+
|
|
10
|
+
## 先确认可用
|
|
11
|
+
|
|
12
|
+
- 工具列表里有 `codex_dispatch` / `claude_dispatch` 才能用。没有 → 这个部署没装 `dsh-plugin-subagent-bridge`,或者对应二进制没找到。
|
|
13
|
+
- 返回 `codex executable not found` / `claude executable not found` → CLI 不在 PATH。告诉用户在插件配置 `codexBin` / `claudeBin` 里指绝对路径,不要反复重试。
|
|
14
|
+
- 返回 `exited with …` → 多半是未登录、额度用尽或模型名不被支持。把后面几行原样转述,别自己编原因。
|
|
15
|
+
|
|
16
|
+
## 选哪个
|
|
17
|
+
|
|
18
|
+
| 情况 | 选 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 用户点名了(「让 Codex」「叫 Claude 看看」) | 听用户的 |
|
|
21
|
+
| 只是要个独立视角/第二意见 | 优先选和自己**不同模型家族**的那个 |
|
|
22
|
+
| 其中一个不可用 | 用另一个,并在回报里说明 |
|
|
23
|
+
|
|
24
|
+
两个工具都**看不到这段对话**,prompt 的写法完全一样。
|
|
25
|
+
|
|
26
|
+
## 基本调用
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
// Codex
|
|
30
|
+
codex_dispatch({
|
|
31
|
+
"prompt": "自足的审查/实现任务 + 验收标准",
|
|
32
|
+
"cwd": "/Users/.../当前仓库",
|
|
33
|
+
"sandbox": "read-only" // 只审不改;要它改代码时用 workspace-write(默认)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Claude Code
|
|
37
|
+
claude_dispatch({
|
|
38
|
+
"prompt": "自足的审查/实现任务 + 验收标准",
|
|
39
|
+
"cwd": "/Users/.../当前仓库",
|
|
40
|
+
"model": "sonnet" // 可选,不传就用 CLI 自己的设置
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
返回(工具视图里会附带一行 `[codex thread …]` 或 `[claude session …]`):
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
// Codex
|
|
48
|
+
{ "ok": true, "text": "审完了,两个问题…", "threadId": "01a1…", "exitCode": 0, "durationMs": 34000 }
|
|
49
|
+
|
|
50
|
+
// Claude
|
|
51
|
+
{ "ok": true, "text": "审完了,两个问题…", "sessionId": "b47f…", "exitCode": 0, "durationMs": 5000 }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 两者的差别
|
|
55
|
+
|
|
56
|
+
| | Codex | Claude Code |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| 权限控制 | `sandbox`:`read-only` / `workspace-write` / `danger-full-access` | 没有这个参数,用 Claude 自己的权限设置 |
|
|
59
|
+
| 续接参数 | `resume: "<threadId>"` | `resume: "<sessionId>"` |
|
|
60
|
+
| 继续时的限制 | `cwd` 与 `sandbox` **不生效**(沿用线程创建时的设置) | `cwd` 照常生效 |
|
|
61
|
+
| 失败判定 | `turn.failed` / 错误事件 | `is_error` 或 `subtype ≠ success` |
|
|
62
|
+
|
|
63
|
+
Codex 的 sandbox 选择:
|
|
64
|
+
|
|
65
|
+
| 目的 | sandbox |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| 代码审查、方案评估、查资料 | `read-only` |
|
|
68
|
+
| 让它改代码 / 跑测试 / 生成文件 | `workspace-write`(默认) |
|
|
69
|
+
| 需要动仓库外的东西 | `danger-full-access`(谨慎,明确告知用户) |
|
|
70
|
+
|
|
71
|
+
## 继续同一个会话
|
|
72
|
+
|
|
73
|
+
```jsonc
|
|
74
|
+
codex_dispatch({ "resume": "01a1…", "prompt": "详细说第一个问题,并给出补丁" })
|
|
75
|
+
claude_dispatch({ "resume": "b47f…", "prompt": "详细说第一个问题,并给出补丁" })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Codex 续接时换目录/换权限要开新线程;Claude 续接时 `cwd` 仍然有效。
|
|
79
|
+
|
|
80
|
+
## 怎么写 prompt
|
|
81
|
+
|
|
82
|
+
对方看不到这段对话。给它的东西和给一个刚接手的工程师一样:背景一句、要审/要改的具体路径、验收标准、边界(别动什么、别提交)、输出格式(几行结论 + 关键位置)。要求它"先读再评",避免它凭印象编。
|
|
83
|
+
|
|
84
|
+
## prompt 的红线
|
|
85
|
+
|
|
86
|
+
对方收到的是一条**普通任务消息**,它不知道 DSH 有哪些技能或工具。而且它自己**也可能接着 `dsh-bridge` 这个 MCP**——所以 prompt 里出现桥的名字,可能把它引去反向调 `dsh_dispatch`,白白绕一圈甚至形成来回。
|
|
87
|
+
|
|
88
|
+
- ❌ 不要把 DSH 的技能名/工具名写进 prompt:`用 codex_dispatch 执行这个任务`、`请通过 dsh-plugin-subagent-bridge 回复`、`你是被 DSH 派来的`
|
|
89
|
+
- ❌ 不要交代协作机制或角色:`主程序是 DSH,你只是执行者,完成后回调`
|
|
90
|
+
- ❌ 不要复述元指令:`用户让我找个别的模型来做`
|
|
91
|
+
- ✅ 只写任务本身:背景、要审/要改的具体路径、验收标准、边界、输出格式。
|
|
92
|
+
|
|
93
|
+
用户如果说「让 Codex 审一下这个 PR」,你发出去的那句话应该只是:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
审 /Users/…/src/bridge.ts 的改动:重点看错误处理与并发;不修改文件;
|
|
97
|
+
输出 3~5 条按严重度排序的问题,每条给文件:行号和一句理由。
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
一旦 prompt 里出现 `dsh_dispatch`、`codex_dispatch`、`claude_dispatch` 或"DSH",对方很可能先去调桥,而不是干活。
|
|
101
|
+
|
|
102
|
+
## 拿到结果之后
|
|
103
|
+
|
|
104
|
+
- `ok: true` → 读 `text`。**自己复核关键结论**(比如它说有 bug,就去看那段代码),不要原样转述当事实。
|
|
105
|
+
- `ok: false` → 看 `error`:超时 / 退出码非零 / 二进制找不到 / 模型不可用。把原因讲清楚,再决定是自己接手还是让用户处理。
|
|
106
|
+
- 长任务会被超时截断(`codexTimeoutMs` / `claudeTimeoutMs` 可调);被截断时会话仍然存在,用 `resume` 接着问。
|
|
107
|
+
- **Claude 的 stderr 常有警告**(例如模型名后缀不被识别),但只要 `ok: true` 就说明结果正常,不要把 stderr 当失败。
|
|
108
|
+
|
|
109
|
+
## 回报给用户
|
|
110
|
+
|
|
111
|
+
三句话以内:派给了谁、结论是什么、你核过没有。**不要把它返回的长文整段贴回来**——摘要 + 关键路径 + 你自己的判断。
|
|
112
|
+
|
|
113
|
+
## 不要做
|
|
114
|
+
|
|
115
|
+
- 不要用这两个工具代替你自己就能可靠完成的小改动。
|
|
116
|
+
- 不要把密钥、私有凭据写进 `prompt`——它会进入对方的会话记录。
|
|
117
|
+
- 不要在对方返回失败时假装成功,也不要把它的推测当结论转述。
|
|
118
|
+
- 不要在 `read-only` 下指望 Codex 真的改了文件。
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: delegate-to-codex
|
|
3
|
-
description: 让 Codex 审一下 / review / 用另一个模型验证 / 要第二意见 / 交叉验证 / 交给 Codex 实现 时使用。用 codex_dispatch 把任务派给 Codex,一次调用阻塞到它退出拿到回答,可用 resume 续同一线程。不用于几行改动,也不用于 Codex 未安装或未登录时。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 把任务交给 Codex
|
|
7
|
-
|
|
8
|
-
Codex 是同一台机器上的另一个编码代理。`codex_dispatch` 会跑一次 `codex exec`,**阻塞到它退出**,返回它的最终回答和线程 id。
|
|
9
|
-
|
|
10
|
-
## 先确认可用
|
|
11
|
-
|
|
12
|
-
- 工具列表里有 `codex_dispatch` 才能用。没有 → 说明这个部署没装 `dsh-plugin-subagent-bridge`,或者 Codex 二进制没找到。
|
|
13
|
-
- 调用返回 `codex executable not found` → Codex CLI 不在 PATH,也没有落在常见目录。告诉用户在插件 `config.codexBin` 里指一个绝对路径,不要反复重试。
|
|
14
|
-
- 用户机器上 Codex 未登录 / 额度用尽时,返回会是 `codex exited with …`,把后面那几行原样转述,别自己编原因。
|
|
15
|
-
|
|
16
|
-
## 基本调用
|
|
17
|
-
|
|
18
|
-
```jsonc
|
|
19
|
-
codex_dispatch({
|
|
20
|
-
"prompt": "自足的审查/实现任务 + 验收标准",
|
|
21
|
-
"cwd": "/Users/.../当前仓库",
|
|
22
|
-
"sandbox": "read-only" // 只审不改;要它改代码时用 workspace-write(默认)
|
|
23
|
-
})
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
返回(工具视图里会附带一行 `[codex thread <id> · <耗时>s]`):
|
|
27
|
-
|
|
28
|
-
```jsonc
|
|
29
|
-
{ "ok": true, "text": "审完了,两个问题…", "threadId": "01a1…", "exitCode": 0, "durationMs": 34000 }
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## 什么时候用哪种 sandbox
|
|
33
|
-
|
|
34
|
-
| 目的 | sandbox |
|
|
35
|
-
| --- | --- |
|
|
36
|
-
| 代码审查、方案评估、查资料 | `read-only` |
|
|
37
|
-
| 让它改代码 / 跑测试 / 生成文件 | `workspace-write`(默认) |
|
|
38
|
-
| 需要动仓库外的东西 | `danger-full-access`(谨慎,明确告知用户) |
|
|
39
|
-
|
|
40
|
-
## 继续同一个线程
|
|
41
|
-
|
|
42
|
-
```jsonc
|
|
43
|
-
codex_dispatch({ "resume": "01a1…", "prompt": "详细说第一个问题,并给出补丁" })
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
`resume` 时 **`cwd` 与 `sandbox` 不生效**——Codex 沿用该线程创建时的设置。需要换目录/换权限就开新线程。
|
|
47
|
-
|
|
48
|
-
## 怎么写 prompt
|
|
49
|
-
|
|
50
|
-
Codex 看不到这段对话。给它的东西和给一个刚接手的工程师一样:背景一句、要审/要改的具体路径、验收标准、边界(别动什么、别提交)、输出格式(几行结论 + 关键位置)。要求它"先读再评",避免它凭印象编。
|
|
51
|
-
|
|
52
|
-
## prompt 的红线
|
|
53
|
-
|
|
54
|
-
Codex 收到的是一条**普通任务消息**,它不知道 DSH 有哪些技能或工具。而且它自己**也接着 `dsh-bridge` 这个 MCP**——所以 prompt 里出现桥的名字,可能把它引去反向调 `dsh_dispatch`,白白绕一圈甚至形成来回。
|
|
55
|
-
|
|
56
|
-
- ❌ 不要把 DSH 的技能名/工具名写进 prompt:`用 codex_dispatch 执行这个任务`、`请通过 dsh-plugin-subagent-bridge 回复`、`你是被 DSH 派来的`
|
|
57
|
-
- ❌ 不要交代协作机制或角色:`主程序是 DSH,你只是执行者,完成后回调`
|
|
58
|
-
- ❌ 不要复述元指令:`用户让我找个别的模型来做`
|
|
59
|
-
- ✅ 只写任务本身:背景、要审/要改的具体路径、验收标准、边界、输出格式。
|
|
60
|
-
|
|
61
|
-
用户如果说「让 Codex 审一下这个 PR」,你发出去的那句话应该只是:
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
审 /Users/…/src/bridge.ts 的改动:重点看错误处理与并发;不修改文件;
|
|
65
|
-
输出 3~5 条按严重度排序的问题,每条给文件:行号和一句理由。
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
一旦 prompt 里出现 `dsh_dispatch`、`codex_dispatch` 或"DSH",Codex 很可能先去调桥,而不是干活。
|
|
69
|
-
|
|
70
|
-
## 拿到结果之后
|
|
71
|
-
|
|
72
|
-
- `ok: true` → 读 `text`。**自己复核关键结论**(比如它说有 bug,就去看那段代码),不要原样转述当事实。
|
|
73
|
-
- `ok: false` → 看 `error`:超时 / 退出码非零 / 二进制找不到。把原因讲清楚,再决定是自己接手还是让用户处理。
|
|
74
|
-
- 长任务(>5 分钟)会被超时截断,`codexTimeoutMs` 可调;被截断时线程仍然存在,用 `resume` 接着问。
|
|
75
|
-
|
|
76
|
-
## 回报给用户
|
|
77
|
-
|
|
78
|
-
三句话以内:派给了谁、结论是什么、你核过没有。**不要把 Codex 的长文整段贴回来**——摘要 + 关键路径 + 你自己的判断。
|
|
79
|
-
|
|
80
|
-
## 不要做
|
|
81
|
-
|
|
82
|
-
- 不要用 `codex_dispatch` 代替你自己就能可靠完成的小改动。
|
|
83
|
-
- 不要把密钥、私有凭据写进 `prompt`——它会进入 Codex 的会话记录。
|
|
84
|
-
- 不要在 Codex 返回失败时假装成功,也不要把它的推测当结论转述。
|
|
85
|
-
- 不要在 `read-only` 下指望它真的改了文件。
|