dsh-agent-foreman 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +235 -3
- package/README.zh.md +250 -0
- package/cordis.patch.yml +11 -0
- package/docs/settings-roster.png +0 -0
- package/icon.svg +8 -0
- package/lib/catalog.js +137 -0
- package/lib/client.js +433 -0
- package/lib/command.js +101 -0
- package/lib/engine.js +1292 -0
- package/lib/index.js +173 -0
- package/lib/page.js +881 -0
- package/lib/routes.js +525 -0
- package/lib/skill.js +79 -0
- package/lib/store.js +705 -0
- package/lib/team.js +1102 -0
- package/lib/tools.js +506 -0
- package/lib/whitelist.js +320 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dclef
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,235 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Foreman · dsh-agent-foreman
|
|
2
|
+
|
|
3
|
+
> Parent agent dispatches, sub-agents do the work, the parent reviews.
|
|
4
|
+
> No preset roles, no scripted phases: **you decide who works and with which model**.
|
|
5
|
+
|
|
6
|
+
Chinese docs: [README.zh.md](README.zh.md)
|
|
7
|
+
|
|
8
|
+
## The problem
|
|
9
|
+
|
|
10
|
+
DSH's built-in Agent Team lets you give each teammate a name and a prompt — but **not a model**.
|
|
11
|
+
Real work is mixed: some of it needs an expensive model, some of it is a two-line edit.
|
|
12
|
+
Inheriting the parent's model for every pair of hands is both slow and expensive.
|
|
13
|
+
|
|
14
|
+
Foreman splits it in two:
|
|
15
|
+
|
|
16
|
+
- **Contractors** — the crew you define in the settings page, named by trade like a real
|
|
17
|
+
construction site: 力工 (laborer — does the business work) and, optionally, 安全员 (safety officer
|
|
18
|
+
— reviews and rejects). The foreman itself is the parent agent: it splits the work and signs off,
|
|
19
|
+
and is never in the roster. Want specialties? **Hire several instances** (力工 · frontend,
|
|
20
|
+
力工 · render), each with its own model and **its own prompt** (the 👁 button next to the name),
|
|
21
|
+
and its own concurrency cap: default 1, so one instance never works two orders at once; set it to
|
|
22
|
+
2 and it can. That prompt is prepended to every order the contractor receives. **The minimal setup
|
|
23
|
+
is a single 力工.**
|
|
24
|
+
- **Orders** — the work the parent agent splits out. Each order carries a task and acceptance
|
|
25
|
+
criteria, and may name a contractor or rotate automatically.
|
|
26
|
+
|
|
27
|
+
The parent only splits work and reviews results. Everything in between happens in isolated child
|
|
28
|
+
sessions, and the products come back as one shared board.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
dsh plugin add E:/path/to/dsh-agent-foreman
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Manual (dev) install:
|
|
37
|
+
|
|
38
|
+
1. Copy the whole directory to `<profile>/node_modules/dsh-agent-foreman`
|
|
39
|
+
2. Add `"dsh-agent-foreman"` to `dsh.profile.bundles` in `<profile>/package.json`
|
|
40
|
+
3. Restart DSH
|
|
41
|
+
|
|
42
|
+
Then open **Settings → 包工头** and add contractors.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
Type `/foreman` (registered at runtime through `ctx.skills.register()`) followed by the work:
|
|
49
|
+
`/foreman extract GraphBlocked into shared logic; add regression tests`. Semicolons or newlines
|
|
50
|
+
split orders. The skill body walks the parent through roster → split → one dispatch → review, so no
|
|
51
|
+
standing system prompt is needed.
|
|
52
|
+
|
|
53
|
+
| Tool | Purpose |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `foreman_roster` | List contractors with route, prompt length, concurrency cap, and the current policy |
|
|
56
|
+
| `foreman_dispatch` | Dispatch a batch of orders, wait for all of them, return the board |
|
|
57
|
+
| `foreman_review` | Pass / fail each order; a failed one is re-dispatched with feedback |
|
|
58
|
+
| `foreman_status` | Re-read a board; omit `runId` for the latest one in this session |
|
|
59
|
+
|
|
60
|
+
### Where members show up
|
|
61
|
+
|
|
62
|
+
**Settings → 包工头 → 实时工地** is a polling board over `api/live`: whether the foreman is
|
|
63
|
+
working, each contractor's role and model, and every order's status / dependencies / error /
|
|
64
|
+
artifact path for the current round. It pauses itself while the page is hidden.
|
|
65
|
+
|
|
66
|
+
The important part: the board shows both the **declared model** (what the roster says) and the
|
|
67
|
+
**actual model** (read back from the child session's header). When they differ the row is
|
|
68
|
+
flagged — that is the direct answer to "which model is actually running this order". In team
|
|
69
|
+
mode the actual model comes from the host's `listMembers()` model field.
|
|
70
|
+
|
|
71
|
+
In team mode a contractor **is** a real Agent Teams member, so the native `Agent Teams` button in
|
|
72
|
+
the session header lists them too. The settings page keeps the full roster editor.
|
|
73
|
+
|
|
74
|
+
Roster notes:
|
|
75
|
+
|
|
76
|
+
- A contractor is a name, an optional role (a lightweight trade label for routing, not a fixed
|
|
77
|
+
persona), an optional prompt, a provider, a model, an effort level and a concurrency cap.
|
|
78
|
+
**Leave the provider on "follow the current session" and the child simply runs on whatever model
|
|
79
|
+
the parent session is using.**
|
|
80
|
+
- To use a different model: pick the provider first — **the model dropdown is derived from it** and
|
|
81
|
+
only lists that provider's models (providers with no models never show up).
|
|
82
|
+
- Settings page collapses policy behind "派单策略"; the roster table keeps name / provider / model /
|
|
83
|
+
effort / concurrency / enabled, and 👁 opens that contractor's prompt editor.
|
|
84
|
+
|
|
85
|
+
### The roster *is* the whitelist
|
|
86
|
+
|
|
87
|
+
The host keeps its own **subagent model whitelist** (`subagent-model-selection`), which decides
|
|
88
|
+
which provider/model routes child agents may use. That list and the roster are configured
|
|
89
|
+
separately, so when they drift apart the roster says `trae` while the children quietly run
|
|
90
|
+
something else — with no error anywhere.
|
|
91
|
+
|
|
92
|
+
This plugin folds that into one place: **every enabled contractor with an explicit provider+model
|
|
93
|
+
is collected into the host whitelist automatically.** You only maintain the roster.
|
|
94
|
+
|
|
95
|
+
- Synced once at plugin startup, again after saving the roster, and on demand via
|
|
96
|
+
`POST /api/whitelist/sync`.
|
|
97
|
+
- If no contractor declares an explicit route (all "follow the current session"), the host
|
|
98
|
+
configuration is left untouched — an empty list makes the host throw.
|
|
99
|
+
- Applies to **new sessions only**. The host snapshots the whitelist into a session when that
|
|
100
|
+
session is first created, so existing sessions keep the old list; open a new session after
|
|
101
|
+
changing the roster.
|
|
102
|
+
- A failed sync never blocks dispatch — it only logs. `/api/state` exposes the live `whitelist`
|
|
103
|
+
field.
|
|
104
|
+
|
|
105
|
+
Dispatch notes:
|
|
106
|
+
|
|
107
|
+
- `orders[].task` **must be self-contained** — the child cannot see the parent's context.
|
|
108
|
+
- `orders[].contractor` accepts a contractor **name or id**, or a `role` trade label; name/id
|
|
109
|
+
matching takes precedence over role matching. Multiple instances with that role rotate among
|
|
110
|
+
themselves, an omitted contractor rotates through the whole roster, and an unknown selector
|
|
111
|
+
reports an error instead of silently reassigning the work.
|
|
112
|
+
- `orders[].dependsOn` only references orders in the same batch. An order becomes ready as soon
|
|
113
|
+
as **all its dependencies complete successfully**; it does not wait for an entire dependency
|
|
114
|
+
layer. Unrelated work still runs concurrently, up to the global default of **4** (maximum 8)
|
|
115
|
+
and each contractor's own concurrency cap.
|
|
116
|
+
- Failure, timeout or cancellation of an order marks its downstream orders **skipped**, and a
|
|
117
|
+
skipped dependency blocks the rest of its chain. The board records the blocking error rather
|
|
118
|
+
than executing downstream work with missing inputs.
|
|
119
|
+
- `waitSeconds` caps the whole batch; unfinished orders are aborted rather than silently reported done.
|
|
120
|
+
- Live status exposes dependency relationships, errors and artifact paths alongside progress;
|
|
121
|
+
it is a status view, not a separate real-time DAG editor.
|
|
122
|
+
|
|
123
|
+
## Policy
|
|
124
|
+
|
|
125
|
+
| Field | Default | Meaning |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `concurrency` | 4 | Contractors working at once (1–8); the per-instance cap lives in the roster |
|
|
128
|
+
| `timeoutSeconds` | 900 | Wall-clock cap per order |
|
|
129
|
+
| `maxRework` | 1 | How many times an order may be sent back |
|
|
130
|
+
| `maxDepth` | 1 | Whether a child may dispatch further children |
|
|
131
|
+
| `provider` | `spawn` | Which subagents provider to use (`spawn` / `fork` / `cursor`) |
|
|
132
|
+
|
|
133
|
+
## Data
|
|
134
|
+
|
|
135
|
+
- Windows: `%LOCALAPPDATA%/dsh-agent-foreman/`
|
|
136
|
+
- Elsewhere: `<baseDir>/data/dsh-agent-foreman/`
|
|
137
|
+
|
|
138
|
+
`roster.json` stores contractors (including roles) and policy. Run snapshots live under `runs/`.
|
|
139
|
+
After restarting, you can query and review a **settled** board from its saved snapshot; work that
|
|
140
|
+
was interrupted is not automatically re-dispatched or resumed. Recovery restores explicit status
|
|
141
|
+
for inspection and review, not a background execution engine. Full delivery text, when available,
|
|
142
|
+
is stored separately under `artifacts/` and linked from the board by its path.
|
|
143
|
+
|
|
144
|
+
A corrupt roster never blocks startup — it is treated as empty and logged.
|
|
145
|
+
|
|
146
|
+
## Security
|
|
147
|
+
|
|
148
|
+
- Every HTTP route accepts same-origin, loopback requests only; cross-site calls get 403.
|
|
149
|
+
- The roster stores provider/model/effort names — never credentials.
|
|
150
|
+
- Contractors run in their own child sessions and inherit DSH's effective sandbox.
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm install --ignore-scripts
|
|
156
|
+
npm test
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Install development dependencies first: the schema package is both a runtime peer dependency and
|
|
160
|
+
a development dependency used by the test suite. `test/harness.test.mjs` runs the real host half
|
|
161
|
+
against a fake host, so no DSH instance is needed. The suite covers tools, engine, routes, CSRF,
|
|
162
|
+
ledger and compatibility regressions.
|
|
163
|
+
|
|
164
|
+
## Compared to
|
|
165
|
+
|
|
166
|
+
| | Agent Team (built-in) | dsh-agent-teams | Foreman |
|
|
167
|
+
| --- | --- | --- | --- |
|
|
168
|
+
| Per-member model | no | no | **yes** — `agentOptions` locked at creation |
|
|
169
|
+
| Cross-round memory | no | yes — previousResult | yes — history in prompt |
|
|
170
|
+
| Native team tab | yes | yes | yes — shared tasks |
|
|
171
|
+
| Self-report recovery | no | yes — update_task | yes — `foreman_report` |
|
|
172
|
+
| Real route echo | no | no | **yes** — declared vs actual model |
|
|
173
|
+
| Whitelist sync | no | no | **yes** — roster is the single source |
|
|
174
|
+
| Job site panel | no | yes — activity panel | **yes** — header button + real model |
|
|
175
|
+
| Dep scheduling + failure block | no | yes — DAG | yes — ready scheduling + skipped |
|
|
176
|
+
| Rework loop | no | manual | yes — auto with feedback |
|
|
177
|
+
| Settings page | yes | no | yes — roster + model catalog |
|
|
178
|
+
|
|
179
|
+
**Key difference**: Foreman's **per-member model selection + real route echo** is unique.
|
|
180
|
+
dsh-agent-teams excels at full task DAG (claim/attempt/quality gates) for complex multi-phase
|
|
181
|
+
projects; Foreman focuses on **model-based dispatch and per-order review** with fewer tools
|
|
182
|
+
(5 vs 14) and faster onboarding.
|
|
183
|
+
|
|
184
|
+
## Team mode (experimental)
|
|
185
|
+
|
|
186
|
+
Off by default. Tick **团队模式** under Settings → 包工头 → 派单策略 and each
|
|
187
|
+
contractor becomes a real member of the host's **agent team**: the plugin spawns
|
|
188
|
+
it with `agentTeams.spawnTeammate`, switches its session model with
|
|
189
|
+
`sessionController.selectModel`, sends the work order as a team message, then reads the
|
|
190
|
+
delivery back from the member's session events.
|
|
191
|
+
|
|
192
|
+
Members therefore show up in the team area and can change model in their own
|
|
193
|
+
session. Every step depends on host team services, so **any failure falls back to
|
|
194
|
+
the plain activation path** and the board's `teamMode` field says which one ran.
|
|
195
|
+
|
|
196
|
+
**Why delivery waits for `active`**: the host's `sendMessage` throws
|
|
197
|
+
`active teammate "worker" not found` until the member leaves `provisioning` and becomes
|
|
198
|
+
`active`. The plugin therefore polls `listMembers()` before delivering; if the member never
|
|
199
|
+
gets there (or reports `failed`), it falls back to the plain path instead of surfacing an error.
|
|
200
|
+
Known trade-offs:
|
|
201
|
+
|
|
202
|
+
- Member names must be ASCII (`lower-kebab-case`), so Chinese contractor names all
|
|
203
|
+
become `worker`, `worker-2`, … — the Chinese name and model live in the member
|
|
204
|
+
description, which the team area shows.
|
|
205
|
+
- A member's first turn runs on the parent's model (`spawnTeammate` accepts no model field —
|
|
206
|
+
hence the one-line handshake).
|
|
207
|
+
- The member's **actual** model is echoed by `listMembers()`, written into the board's
|
|
208
|
+
`teamMode` note; if the roster model differs it is shown side by side.
|
|
209
|
+
- Switching is bounded by the host's per-subagent model allow-list; values outside
|
|
210
|
+
it are refused and the board shows `model switch: failed · reason`.
|
|
211
|
+
- Members stay in the team after the round.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## Changelog
|
|
215
|
+
|
|
216
|
+
### 0.2.0 (2026-10-28)
|
|
217
|
+
|
|
218
|
+
- **Self-member system**: `subagents.startActivation` + `agentOptions` locks model at creation,
|
|
219
|
+
no longer relying on `spawnTeammate` (which cannot specify models)
|
|
220
|
+
- **Cross-round memory**: history concatenated into prompt (like agent-teams' previousResult)
|
|
221
|
+
- **`foreman_report` self-report tool**: product recovery without reading member sessions
|
|
222
|
+
- **Real route echo**: `actualRoute` / `routeMismatch` — declared vs actual model comparison
|
|
223
|
+
- **Whitelist sync**: roster auto-syncs to host subagent model whitelist
|
|
224
|
+
- **Job site panel**: "🏗 工地" button in session header with live model and order status
|
|
225
|
+
- **Dependency-ready scheduling**: no layer barriers, failure blocks downstream as `skipped`
|
|
226
|
+
- **Snapshot recovery**: `runs/` atomic snapshots, queryable after restart, never auto-reruns
|
|
227
|
+
- **Roster roles**: `role` field supports role-based order rotation
|
|
228
|
+
|
|
229
|
+
### 0.1.0
|
|
230
|
+
|
|
231
|
+
- Initial release: roster management, dispatch engine, board, settings page
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# 包工头 · dsh-agent-foreman
|
|
2
|
+
|
|
3
|
+
> 父智能体派单,子智能体干活,父智能体验收。
|
|
4
|
+
> 不预置角色、不写剧本:**谁干活、用什么模型,由你把关**。
|
|
5
|
+
|
|
6
|
+
## 它解决的问题
|
|
7
|
+
|
|
8
|
+
DSH 自带的「智能体团队」只能给每个成员一个名字和一段提示词——**成员用哪个模型选不了**。
|
|
9
|
+
而实际干活时,同一批任务里既有需要贵模型把关的部分,也有随手就能做的小改,全部继承
|
|
10
|
+
父会话的模型既费钱也慢。
|
|
11
|
+
|
|
12
|
+
包工头把这件事拆成两半:
|
|
13
|
+
|
|
14
|
+
- **承包商(contractor)**:你在设置页里定义的人,名字只是个标签,真正决定"谁干活"的是
|
|
15
|
+
`provider + 模型 + 思考等级`。想按专长分工就多雇几个实例、各起各的名字:便宜的配便宜模型
|
|
16
|
+
跑搬砖活,贵的专门挑错,还有一个直接跟父会话同模型。每个承包商还能带一段**自己的提示词**
|
|
17
|
+
(设置页里点名字后面的 👁 编辑),派单时原样塞进它的工单里。
|
|
18
|
+
- **工单(order)**:父智能体拆出来的活。每张工单指定任务与验收标准,可以点名某个承包商,
|
|
19
|
+
也可以自动轮转分配。
|
|
20
|
+
|
|
21
|
+
父智能体只做两件事:拆单、验收。中间的过程全部由承包商在各自独立的子会话里完成,
|
|
22
|
+
产物回收成一块共享看板。
|
|
23
|
+
|
|
24
|
+
## 装上它
|
|
25
|
+
|
|
26
|
+
把仓库放到任意目录,然后在 DSH 里:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
dsh plugin add E:/path/to/dsh-agent-foreman
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
或者手动安装(dev 用法):
|
|
33
|
+
|
|
34
|
+
1. 把整个目录拷到 `<profile>/node_modules/dsh-agent-foreman`
|
|
35
|
+
2. 在 `<profile>/package.json` 的 `dsh.profile.bundles` 里加上 `"dsh-agent-foreman"`
|
|
36
|
+
3. 重启 DSH
|
|
37
|
+
|
|
38
|
+
装好后打开 **设置 → 包工头**,添加承包商。
|
|
39
|
+
|
|
40
|
+
## 用起来
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
1. 设置 → 包工头 → 添加承包商(名字、provider、模型、思考等级、并发各一格,👁 写提示词)
|
|
44
|
+
力工-前端 trae deepseek-v4.1-flash 默认 开 1
|
|
45
|
+
力工-渲染 workbuddy-global deepseek-v4.1-flash high 开 2
|
|
46
|
+
安全员 deepseek-official deepseek-flash 默认 关 1
|
|
47
|
+
|
|
48
|
+
分工靠名字:**包工头(父模型)拆单、力工干活、安全员挑错(可选)**。名册里只放你雇的人,
|
|
49
|
+
包工头就是驱动这一切的父智能体,不占名册。
|
|
50
|
+
|
|
51
|
+
**想要专长就多雇几个实例**(两个力工各用各的模型、各写各的提示词)。派单时 `contractor`
|
|
52
|
+
写名字就点某一个人,省略就全名册轮转。
|
|
53
|
+
每个实例还带一个并发上限:默认 1,也就是同一个人串行干、不会自己跟自己抢文件;
|
|
54
|
+
调成 2 它就能同时接两张单。
|
|
55
|
+
|
|
56
|
+
**最简形态只有一个人:力工。** 所有工单都给它,靠它的并发上限和策略里的总并发控制节奏。
|
|
57
|
+
|
|
58
|
+
2. 在对话里直接说:"用包工头把这三件事分下去:……";或者用 `/foreman` 一句话派单(见下)
|
|
59
|
+
|
|
60
|
+
3. 父智能体调用 foreman_dispatch 派单 → 等全部落定 → 拿回看板 → 逐单打分
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `/foreman`:一句话派单
|
|
64
|
+
|
|
65
|
+
插件在宿主里注册了一个运行时 skill(`lib/skill.js`,`ctx.skills.register()`)。输入框里敲 `/`
|
|
66
|
+
能看到 **foreman**,选中或直接打 `/foreman` 加任务即可:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
/foreman 把 GraphBlocked 抽成共享逻辑;给新逻辑补回归测试
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
分号或换行分段,一段一张工单。skill 正文规定四步:看名册 → 拆单(补齐背景/文件/验收)→
|
|
73
|
+
一次 `foreman_dispatch` → 逐单 `foreman_review`。这条路径不靠常驻系统提示词,只在用到时
|
|
74
|
+
注入一次,比每轮都挂着一段守则省。
|
|
75
|
+
|
|
76
|
+
父智能体手里的四个工具:
|
|
77
|
+
|
|
78
|
+
| 工具 | 作用 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `foreman_roster` | 看名册:有哪些承包商、什么模型、提示词多少字、并发几张单、当前策略 |
|
|
81
|
+
| `foreman_dispatch` | 派一轮工单,等全部落定,返回看板(含每单交付摘要) |
|
|
82
|
+
| `foreman_review` | 逐单 pass / fail;fail 且还有额度就带着反馈重派给同一个人 |
|
|
83
|
+
| `foreman_status` | 回看某一轮看板;省略 runId 取本会话最近一轮 |
|
|
84
|
+
|
|
85
|
+
### 成员在哪看
|
|
86
|
+
|
|
87
|
+
**设置 → 包工头 → 实时工地**:一块轮询 `api/live` 的看板,显示包工头在不在跑、
|
|
88
|
+
每个承包商的岗位与模型、本轮每张工单的状态 / 依赖 / 错误 / 产物路径。页面切到后台
|
|
89
|
+
就自动停表,不占资源。
|
|
90
|
+
|
|
91
|
+
关键一点:看板同时显示**声明模型**和**真实模型**。声明的是名册里配的那套,真实的是
|
|
92
|
+
从子会话的 header 里读回来的实际路由。两者不一致时该行会标出来——这正好回答了
|
|
93
|
+
"它到底跑的是谁的模型"。团队模式下则读宿主 `listMembers()` 回显的成员实际模型。
|
|
94
|
+
|
|
95
|
+
团队模式下承包商本身就是宿主的「智能体团队」成员,点会话头的 `智能体团队` 也能看到。
|
|
96
|
+
|
|
97
|
+
### 名册要点
|
|
98
|
+
|
|
99
|
+
- 承包商 = 名字 + 可选岗位(`role`,只用于派单路由,不是固定角色体系)+ 提示词 + provider +
|
|
100
|
+
模型 + 思考等级 + 并发上限。**provider 选「跟随当前会话」时,子智能体直接跑父会话当前的模型**,
|
|
101
|
+
不需要知道它是什么。
|
|
102
|
+
- 想换模型就分两步:先选 provider,**模型下拉只列这个 provider 自己的模型**(由 provider 推出来,
|
|
103
|
+
没有模型的 provider 不会出现在下拉里)。
|
|
104
|
+
- 设置页把策略收进了「派单策略」折叠区,名册只留 名字 / 岗位 / provider / 模型 / 思考等级 / 并发 /
|
|
105
|
+
启用,点名字后面的 👁 编辑这个承包商的提示词(派单时塞进工单,和工单冲突时以工单为准)。
|
|
106
|
+
|
|
107
|
+
### 名册就是白名单
|
|
108
|
+
|
|
109
|
+
宿主自己有一份**子智能体模型白名单**(设置项 `subagent-model-selection`),它决定子智能体
|
|
110
|
+
被允许跑哪些 provider/模型。这份名单和名册是两处独立配置,一旦不一致就会出现"名册里写着
|
|
111
|
+
trae,实际却在跑别的模型"——而且没有任何报错。
|
|
112
|
+
|
|
113
|
+
本插件把这件事收成一处:**名册里所有启用且配了 provider+model 的承包商,会自动汇总进宿主
|
|
114
|
+
白名单**。你只需要维护名册。
|
|
115
|
+
|
|
116
|
+
- 插件启动时同步一次;设置页保存名册后再同步一次;也可 `POST /api/whitelist/sync` 手动触发。
|
|
117
|
+
- 名册里没有任何显式路由(都用「跟随当前会话」)时**不动**宿主原配置——空名单会让宿主报错。
|
|
118
|
+
- 只影响**新会话**。宿主的白名单在会话首次建立时就被快照进该会话了,已经开着的会话改不了,
|
|
119
|
+
所以改完名册要**开新会话**才生效。
|
|
120
|
+
- 同步失败不会影响派单,只记日志;`/api/state` 的 `whitelist` 字段能看当前真实名单。
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
### 派单参数要点
|
|
125
|
+
|
|
126
|
+
- `orders[].task` **必须自足**:子智能体看不到父会话的任何上下文,背景、要动的文件、
|
|
127
|
+
期望结果都要写进去。
|
|
128
|
+
- `orders[].contractor` 可写承包商**名字或 id**,也可写 `role` 岗位名;名字/id 匹配优先于岗位匹配。
|
|
129
|
+
同岗位的多个实例之间轮转,省略时全名册轮转;不存在的名字、id 或岗位会直接报错,不会悄悄换人。
|
|
130
|
+
- `orders[].dependsOn` 只能引用本轮内的工单。所有依赖都成功完成后,这张工单就会开工,
|
|
131
|
+
**不需要等整张拓扑层跑完**;无依赖关系的工单仍可并行,受总并发默认 **4**(上限 8)和单人并发上限约束。
|
|
132
|
+
- 某张工单失败、超时或被取消后,它的下游工单标记为 **skipped**;被跳过的依赖继续阻断更下游,
|
|
133
|
+
不会在缺少上游产物时硬跑。看板会保留阻断原因,便于验收和排查。
|
|
134
|
+
- `waitSeconds` 是这一轮的总墙钟上限,到点会中止没跑完的工单并把现状返回——不会让父智能体
|
|
135
|
+
拿到一份假的"全部完成"。
|
|
136
|
+
- 实时状态会显示依赖关系、错误和产物路径;它是状态视图,不另造一套实时 DAG 编辑界面。
|
|
137
|
+
|
|
138
|
+
### 验收与返工
|
|
139
|
+
|
|
140
|
+
`foreman_review` 打回时,反馈会原样进入下一次派单的提示词("上一轮被打回,原因:……")。
|
|
141
|
+
返工额度由策略里的 `maxRework` 控制(默认 1 次),额度用完后打回只会记录结论、不再重派,
|
|
142
|
+
这一轮转入 `closed`。
|
|
143
|
+
|
|
144
|
+
## 策略
|
|
145
|
+
|
|
146
|
+
设置页可调,也可以直接改数据文件里的 `policy`:
|
|
147
|
+
|
|
148
|
+
| 字段 | 默认 | 说明 |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| `concurrency` | 4 | 同时在场的承包商数(1–8);单人的上限在名册里按人设 |
|
|
151
|
+
| `timeoutSeconds` | 900 | 单张工单的墙钟上限 |
|
|
152
|
+
| `maxRework` | 1 | 每张工单最多被返工几次 |
|
|
153
|
+
| `maxDepth` | 1 | 子智能体还能不能再派子智能体 |
|
|
154
|
+
| `provider` | `spawn` | 走哪个 subagents provider(`spawn` / `fork` / `cursor`) |
|
|
155
|
+
|
|
156
|
+
## 数据放在哪
|
|
157
|
+
|
|
158
|
+
- Windows:`%LOCALAPPDATA%/dsh-agent-foreman/`
|
|
159
|
+
- 其它平台:`<baseDir>/data/dsh-agent-foreman/`
|
|
160
|
+
|
|
161
|
+
`roster.json` 保存名册(含岗位)与策略;运行快照保存在 `runs/`。重启后可以查询、验收已经落定的
|
|
162
|
+
看板快照,但**中断的工作不会自动重新派发或续跑**。这里的显式恢复是恢复可检查、可验收的状态,
|
|
163
|
+
不是偷偷挂一个后台执行引擎。完整交付正文在可用时另存于 `artifacts/`,看板通过产物路径关联。
|
|
164
|
+
|
|
165
|
+
名册坏了不会阻断启动:读不出来的名册会被当成空名册并记一条日志。
|
|
166
|
+
|
|
167
|
+
## 安全边界
|
|
168
|
+
|
|
169
|
+
- 所有 HTTP 接口只接受同源、且落在回环地址上的请求;跨站请求一律 403(有 CSRF 面)。
|
|
170
|
+
- 名册只保存 `provider` / `model` / 思考等级等配置,不保存任何密钥。
|
|
171
|
+
- 承包商在独立子会话里跑,沙箱与权限沿用 DSH 当前生效的那一套。
|
|
172
|
+
|
|
173
|
+
## 开发
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
node --test test/*.test.mjs # 37 条用例:工具、引擎、路由、CSRF、台账、并发上限
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
测试用假宿主跑真实的插件半边(`test/harness.test.mjs`),不需要启动 DSH。
|
|
180
|
+
|
|
181
|
+
## 团队模式(实验)
|
|
182
|
+
|
|
183
|
+
默认关闭。在「设置 → 包工头 → 派单策略」勾上**团队模式**后,承包商不再走
|
|
184
|
+
`subagents.startActivation`,而是:
|
|
185
|
+
|
|
186
|
+
1. `agentTeams.spawnTeammate` 把它加成宿主的**智能体团队**成员(自己的会话);
|
|
187
|
+
2. `sessionController.selectModel` 把成员会话的模型换成名册里配的那套;
|
|
188
|
+
3. `agentTeams.sendMessage` 把工单当团队消息发过去;
|
|
189
|
+
4. 等它那一轮跑完,从它的会话事件里把交付正文捞回来进看板。
|
|
190
|
+
|
|
191
|
+
这样承包商既能在「智能体团队」区看见,也能在它自己的会话里换模型。
|
|
192
|
+
|
|
193
|
+
**为什么必须等成员 active 再投递**:宿主 `agent-teams` 的 `sendMessage` 在成员还没
|
|
194
|
+
进入 `active` 阶段时会直接抛 `active teammate "worker" not found`。成员 spawn 之后
|
|
195
|
+
要先经历 `provisioning`(初始 prompt 被确认),才转 `active`。本插件在投递前会轮询
|
|
196
|
+
`listMembers()` 等这个状态;等不到(宿主没有该接口 / 成员 failed)就退回普通模式,
|
|
197
|
+
不把失败抛给用户。
|
|
198
|
+
|
|
199
|
+
这条链路依赖宿主的团队服务,因此**任何一步不可用都会自动退回普通模式**,
|
|
200
|
+
看板每单的 `teamMode` 字段会写明是「团队模式」还是「失败退回」。已知取舍:
|
|
201
|
+
|
|
202
|
+
- 成员名只能是 ASCII(宿主只收 `lower-kebab-case`),所以中文承包商统一落成
|
|
203
|
+
`worker`、`worker-2`…,中文名与模型写在成员的描述里;
|
|
204
|
+
- 成员第一轮用的是父会话的模型(`spawnTeammate` 本身不接受 model 字段,建成员时给不了),
|
|
205
|
+
切模型从第二轮才生效——所以建成员时只发一句「待命」,工单随后再发;
|
|
206
|
+
- 成员的**实际模型**由宿主 `listMembers()` 的 `model` 字段回显,会写进看板的 `teamMode`
|
|
207
|
+
备注;名册配的模型与它不一致时会并列写出;
|
|
208
|
+
- 切模型受宿主给子会话的**模型白名单**限制,白名单里没有的 provider/模型会被拒,
|
|
209
|
+
这种单在看板上会写明「模型切换:failed · 原因」;
|
|
210
|
+
- 每轮派单结束时成员会留在团队里(可继续用,也可手动删除);
|
|
211
|
+
- 并发上限仍按名册的「并发」和策略的「同时在场承包商」卡。
|
|
212
|
+
|
|
213
|
+
## 与其它方案的区别
|
|
214
|
+
|
|
215
|
+
| | 智能体团队(内置) | dsh-agent-teams | 包工头 |
|
|
216
|
+
| --- | --- | --- | --- |
|
|
217
|
+
| 成员可选模型 | ✗ | ✗ | **✓** `agentOptions` 创建时锁定 |
|
|
218
|
+
| 跨轮记忆 | ✗ | ✓ previousResult | ✓ 历史拼 prompt |
|
|
219
|
+
| 原生团队标签页 | ✓ | ✓ | ✓ 共享任务 |
|
|
220
|
+
| 自报回收 | ✗ | ✓ update_task | ✓ `foreman_report` |
|
|
221
|
+
| 真实路由回显 | ✗ | ✗ | **✓** 声明 vs 实际模型对照 |
|
|
222
|
+
| 白名单同步 | ✗ | ✗ | **✓** 名册 = 宿主白名单唯一来源 |
|
|
223
|
+
| 工地看板 | ✗ | ✓ 活动面板 | **✓** 会话头「工地」+ 真实模型 |
|
|
224
|
+
| 依赖调度 + 失败阻断 | ✗ | ✓ DAG | ✓ 就绪调度 + skipped 递归阻断 |
|
|
225
|
+
| 返工闭环 | ✗ | 手动 | ✓ 带反馈自动重派 |
|
|
226
|
+
| 设置页 | ✓ | ✗ | ✓ 名册 + 模型目录 |
|
|
227
|
+
|
|
228
|
+
**核心差异**:包工头的**模型可选 + 真实路由回显**是其它方案没有的。dsh-agent-teams 的强项是完整任务 DAG(claim/attempt/quality gates),适合复杂多阶段项目;包工头侧重**按模型拆单、逐单验收**,工具更少(5 vs 14),上手更快。
|
|
229
|
+
|
|
230
|
+
## 版本说明
|
|
231
|
+
|
|
232
|
+
### 0.2.0(2026-10-28)
|
|
233
|
+
|
|
234
|
+
- **自建成员系统**:`subagents.startActivation` + `agentOptions` 锁定模型,不再依赖 `spawnTeammate`(后者无法指定模型)
|
|
235
|
+
- **跨轮记忆**:历史产出拼进 prompt(同 agent-teams 的 previousResult)
|
|
236
|
+
- **`foreman_report` 自报工具**:产物回收不依赖读成员会话(同 agent-teams 的 update_task)
|
|
237
|
+
- **真实路由回显**:`actualRoute` / `routeMismatch`,声明模型 vs 实际模型对照
|
|
238
|
+
- **白名单同步**:名册自动同步宿主子智能体模型白名单
|
|
239
|
+
- **工地看板**:会话头「🏗 工地」按钮,实时显示承包商模型与工单状态
|
|
240
|
+
- **依赖就绪调度**:不整层等待,失败递归阻断为 `skipped`
|
|
241
|
+
- **快照恢复**:`runs/` 原子快照,重启后可查可验收,绝不自动重跑
|
|
242
|
+
- **名册岗位**:`role` 字段支持按岗位轮转派单
|
|
243
|
+
|
|
244
|
+
### 0.1.0
|
|
245
|
+
|
|
246
|
+
- 初始版本:名册管理、派单引擎、看板、设置页
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 包工头(dsh-agent-foreman)
|
|
2
|
+
#
|
|
3
|
+
# 只插一行:整块能力都长在这一个插件上——名册、派单引擎、看板、设置页都由
|
|
4
|
+
# lib/index.js 挂载。分包商(承包商)是运行时数据,不是 cordis 插件行,所以
|
|
5
|
+
# 这里没有第二个 id。
|
|
6
|
+
#
|
|
7
|
+
# 客户端半边(设置页里的名册编辑器)由 package.json 的 dsh.client 声明,
|
|
8
|
+
# 宿主自动加载 lib/client.js,无需在此重复。
|
|
9
|
+
- insert:
|
|
10
|
+
- id: dsh-agent-foreman
|
|
11
|
+
name: dsh-agent-foreman
|
|
Binary file
|
package/icon.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Foreman">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#1f2937"/>
|
|
3
|
+
<path d="M12 40h40v5a3 3 0 0 1-3 3H15a3 3 0 0 1-3-3v-5z" fill="#f59e0b"/>
|
|
4
|
+
<path d="M18 40V30a14 14 0 0 1 28 0v10H18z" fill="#fbbf24"/>
|
|
5
|
+
<path d="M28 18h8v22h-8z" fill="#fde68a"/>
|
|
6
|
+
<path d="M30.5 20h3v20h-3z" fill="#f59e0b" opacity=".55"/>
|
|
7
|
+
<circle cx="32" cy="47" r="2.6" fill="#1f2937"/>
|
|
8
|
+
</svg>
|
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包工头 · 模型目录读取。
|
|
3
|
+
*
|
|
4
|
+
* 承包商的名字只是标签,真正决定"谁干活"的是 provider + 模型。设置页要能下拉
|
|
5
|
+
* 出宿主机真实在用的目录,所以这里直接问 `llm` 服务,不维护第二份清单。
|
|
6
|
+
*
|
|
7
|
+
* 三个已知的宿主约束,这里的代码都绕开了:
|
|
8
|
+
*
|
|
9
|
+
* 1. `listModels()` 不带推理等级——等级只在 `resolveModelInfo()` 里,而且那是
|
|
10
|
+
* 一次按模型的异步查询。所以等级走单独端点,等用户选了模型才查。
|
|
11
|
+
* 2. 某个 provider 的目录可能整个失败(没配 key、网关不通)。一个 provider 挂掉
|
|
12
|
+
* 不应该让整个下拉框变空,因此逐个捕获,把错误挂在该 provider 上返回。
|
|
13
|
+
* 3. `llm` 可能根本不存在(无头 profile)。这时返回空目录而不是抛错,设置页会
|
|
14
|
+
* 退化成手填输入框。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-agent-foreman/catalog
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 取 LLM 服务;缺失时返回 undefined(`get()` 不会因未 inject 而抛)。
|
|
21
|
+
* @param ctx - cordis 上下文。
|
|
22
|
+
*/
|
|
23
|
+
function llmOf(ctx) {
|
|
24
|
+
try {
|
|
25
|
+
const llm = ctx.get('llm');
|
|
26
|
+
return typeof llm?.listProviders === 'function' ? llm : undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 列出宿主当前注册的 provider。
|
|
35
|
+
* @param ctx - cordis 上下文。
|
|
36
|
+
* @returns `{ id, name }[]`,缺失服务时为空数组。
|
|
37
|
+
*/
|
|
38
|
+
export function readProviders(ctx) {
|
|
39
|
+
const llm = llmOf(ctx);
|
|
40
|
+
if (llm === undefined)
|
|
41
|
+
return [];
|
|
42
|
+
try {
|
|
43
|
+
const providers = llm.listProviders();
|
|
44
|
+
return Array.isArray(providers)
|
|
45
|
+
? providers.map(provider => ({ id: String(provider.id), name: String(provider.name ?? provider.id) }))
|
|
46
|
+
: [];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 读取完整目录:每个 provider 及其模型。
|
|
55
|
+
*
|
|
56
|
+
* 串行而不是并发,因为适配器的 `listModels` 大多要打一次远端接口,而目录读取
|
|
57
|
+
* 只在打开设置页时发生一次——并发省不下多少时间,却会把某个坏网关的错误放大
|
|
58
|
+
* 成一片超时。
|
|
59
|
+
*
|
|
60
|
+
* @param ctx - cordis 上下文。
|
|
61
|
+
* @returns `{ providers: [{ id, name, models, error? }], fetchedAt }`。
|
|
62
|
+
*/
|
|
63
|
+
export async function readCatalog(ctx) {
|
|
64
|
+
const llm = llmOf(ctx);
|
|
65
|
+
if (llm === undefined)
|
|
66
|
+
return { providers: [], fetchedAt: new Date().toISOString(), unavailable: true };
|
|
67
|
+
const providers = readProviders(ctx);
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const provider of providers) {
|
|
70
|
+
try {
|
|
71
|
+
const models = await llm.listModels(provider.id);
|
|
72
|
+
out.push({
|
|
73
|
+
...provider,
|
|
74
|
+
models: Array.isArray(models)
|
|
75
|
+
? models.map(model => ({ id: String(model.id), name: String(model.name ?? model.id) }))
|
|
76
|
+
: [],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
out.push({ ...provider, models: [], error: error instanceof Error ? error.message : String(error) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { providers: out, fetchedAt: new Date().toISOString() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 读取单个模型的推理等级。等级随模型而异,不能拿父会话的等级硬套。
|
|
88
|
+
* @param ctx - cordis 上下文。
|
|
89
|
+
* @param provider - provider id。
|
|
90
|
+
* @param model - 模型 id。
|
|
91
|
+
* @returns `{ efforts: [{ id, name }], defaultEffort? }`;查不到时为空。
|
|
92
|
+
*/
|
|
93
|
+
export async function readEfforts(ctx, provider, model) {
|
|
94
|
+
const llm = llmOf(ctx);
|
|
95
|
+
if (llm === undefined || typeof llm.resolveModelInfo !== 'function')
|
|
96
|
+
return { efforts: [] };
|
|
97
|
+
try {
|
|
98
|
+
const info = await llm.resolveModelInfo(provider, model, undefined);
|
|
99
|
+
const efforts = Array.isArray(info?.reasoning?.efforts)
|
|
100
|
+
? info.reasoning.efforts.map(effort => ({ id: String(effort.id), name: String(effort.name ?? effort.id) }))
|
|
101
|
+
: [];
|
|
102
|
+
const defaultEffort = info?.reasoning?.defaultEffort;
|
|
103
|
+
return typeof defaultEffort === 'string' ? { efforts, defaultEffort } : { efforts };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { efforts: [] };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 宿主实际注册的 subagents provider 名。
|
|
112
|
+
*
|
|
113
|
+
* `SubagentRuntime.providers` 是公开的 Map(审计工具也这么读),但服务可能尚未
|
|
114
|
+
* 就绪;读不到就返回空数组,调用方据此决定是否回退到配置里的默认名。
|
|
115
|
+
*
|
|
116
|
+
* @param ctx - cordis 上下文。
|
|
117
|
+
* @returns provider 名数组。
|
|
118
|
+
*/
|
|
119
|
+
export function readSubagentProviders(ctx) {
|
|
120
|
+
try {
|
|
121
|
+
const service = ctx.get('subagents');
|
|
122
|
+
const providers = service?.providers;
|
|
123
|
+
if (providers instanceof Map)
|
|
124
|
+
return [...providers.keys()].map(String);
|
|
125
|
+
if (typeof service?.getProvider === 'function') {
|
|
126
|
+
const names = [];
|
|
127
|
+
for (const candidate of ['spawn', 'cursor', 'acp', 'external'])
|
|
128
|
+
if (service.getProvider(candidate) !== undefined)
|
|
129
|
+
names.push(candidate);
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|