dsh-subagent-control 1.0.0 → 1.0.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/CHANGELOG.md +17 -0
- package/README.md +108 -34
- package/cordis.patch.yml +3 -0
- package/extras.js +343 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
- New optional `extras` component (`dsh-subagent-control/extras`), shipped **disabled**:
|
|
6
|
+
`relay_subagent_message`, `wait_subagents`, `drain_subagents`, `subagent_budget`.
|
|
7
|
+
- The default install is unchanged: still exactly the three original tools while extras stays off.
|
|
8
|
+
- New companion bundle `dsh-team-limits`, published separately, for raising the Agent Team
|
|
9
|
+
member/task/message limits by overriding the `agent-team` row.
|
|
10
|
+
|
|
11
|
+
## 1.0.1
|
|
12
|
+
|
|
13
|
+
- Documentation: bilingual README (English + 简体中文) with usage examples, the
|
|
14
|
+
security model, and the name-collision note.
|
|
15
|
+
- Release: publishing now runs on GitHub Actions with npm trusted publishing
|
|
16
|
+
(OIDC) and provenance. No npm token is stored in the repository.
|
|
17
|
+
- No runtime change: the three tools and their JSON Schemas are identical to
|
|
18
|
+
1.0.0.
|
|
19
|
+
|
|
3
20
|
## 1.0.0
|
|
4
21
|
|
|
5
22
|
First release.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-subagent-control
|
|
2
2
|
|
|
3
|
+
**English** · [简体中文](#简体中文)
|
|
4
|
+
|
|
3
5
|
Three tools that let any DSH agent — **including a team member** — list, steer and stop the subagents it started.
|
|
4
6
|
|
|
5
7
|
`list_subagents` · `send_subagent_message` · `interrupt_subagent`
|
|
@@ -10,112 +12,184 @@ Three tools that let any DSH agent — **including a team member** — list, ste
|
|
|
10
12
|
|
|
11
13
|
### The problem
|
|
12
14
|
|
|
13
|
-
The Harness ships `list_agents`, `send_message` and `interrupt_agent`. When the Agent Team bundle is active it registers tools with **those same three names** in every team member's exact Agent scope, and the more specific registration wins. Inside a teammate scope the team versions take over: they resolve `target` against the team roster only, and interrupting is a Lead-only operation.
|
|
15
|
+
The Harness ships `list_agents`, `send_message` and `interrupt_agent`. When the Agent Team bundle is active, it registers tools with **those same three names** in every team member's exact Agent scope, and the more specific registration wins. Inside a teammate scope the team versions take over: they resolve `target` against the team roster only, and interrupting is a Lead-only operation.
|
|
14
16
|
|
|
15
|
-
The result: a teammate can start a subagent with `subagent`, but cannot list it, follow up with it, or stop it — even though `ctx.subagents`
|
|
17
|
+
The result: a teammate can start a subagent with `subagent`, but cannot list it, follow up with it, or stop it — even though the subagent service (`ctx.subagents`) has supported all three for any live agent all along, and its authority rules already allow an ancestor to manage its own descendants.
|
|
16
18
|
|
|
17
|
-
This bundle does not fight over the taken names. It registers three **new** names, globally, so the Lead and every teammate get them at once.
|
|
19
|
+
This bundle does not fight over the taken names. It registers three **new** names, globally, so the Lead and every teammate get them at once.
|
|
18
20
|
|
|
19
21
|
### Tools
|
|
20
22
|
|
|
21
|
-
| Tool | Parameters |
|
|
23
|
+
| Tool | Parameters | Underlying call |
|
|
22
24
|
| --- | --- | --- |
|
|
23
25
|
| `list_subagents` | `scope?`: `"children"` \| `"descendants"` (default `children`) | `ctx.subagents.listChildren()` / `listDescendants()` |
|
|
24
26
|
| `send_subagent_message` | `subagent_id`, `message` (both required) | `ctx.subagents.sendMessage()` |
|
|
25
27
|
| `interrupt_subagent` | `subagent_id` (required) | `ctx.subagents.interrupt()` with ancestor authority |
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
### Usage
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
list_subagents
|
|
33
|
+
→ 3b0f9c1e running "review the patch" depth=1
|
|
34
|
+
|
|
35
|
+
list_subagents scope=descendants
|
|
36
|
+
→ 3b0f9c1e running "review the patch" depth=1
|
|
37
|
+
7d2a44b0 inactive "check the tests" depth=2
|
|
38
|
+
|
|
39
|
+
send_subagent_message subagent_id=3b0f9c1e message="also look at locale/zh.json"
|
|
40
|
+
→ messageId=...
|
|
41
|
+
|
|
42
|
+
interrupt_subagent subagent_id=3b0f9c1e
|
|
43
|
+
→ accepted=true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each listing line is `id status "label" depth=`. `status` is `running` or `inactive` — **inactive means no turn is executing, not that the subagent is gone**. Use `send_subagent_message` to wake it.
|
|
28
47
|
|
|
29
48
|
### Behaviour and limits
|
|
30
49
|
|
|
31
|
-
- One-shot subagents are
|
|
32
|
-
- A child whose
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- Every failure
|
|
50
|
+
- **One-shot subagents are hidden.** They cannot accept a follow-up message, so listing them would only invite a call that is guaranteed to fail.
|
|
51
|
+
- **A broken record is reported, not thrown.** A child whose state cannot be read comes back as a diagnostic entry (`corrupt` / `unsupported` / `unavailable`) instead of turning the whole listing into an error.
|
|
52
|
+
- **`send_subagent_message` reaches direct children only.** That is the service's own rule; the plugin does not widen it.
|
|
53
|
+
- **`interrupt_subagent` reaches descendants**, so a team Lead can also stop a grandchild.
|
|
54
|
+
- **Every failure names its cause**: no calling agent, unknown id, or an id that is not yours.
|
|
55
|
+
- **No config, no storage.** The bundle writes nothing and reads nothing outside the three services.
|
|
36
56
|
|
|
37
57
|
### Install
|
|
38
58
|
|
|
39
|
-
Through the plugin
|
|
59
|
+
Through the plugin panel / market, or by asking the agent:
|
|
40
60
|
|
|
41
61
|
```
|
|
42
62
|
plugin_manager install_bundle target "dsh-subagent-control@1.0.0"
|
|
43
63
|
```
|
|
44
64
|
|
|
45
|
-
From a checkout, pass the directory path instead of the npm spec. The bundle applies immediately — no restart.
|
|
65
|
+
From a checkout, pass the **directory path** instead of the npm spec. The bundle applies immediately — no restart needed. Afterwards the three tool names are visible and the plugin row is enabled.
|
|
46
66
|
|
|
47
67
|
### Compatibility
|
|
48
68
|
|
|
49
69
|
- DSH with the `tools`, `subagents` and `agents` services (0.2.x and later).
|
|
50
70
|
- Node `^22.19.0 || >=24.0.0`.
|
|
51
|
-
-
|
|
71
|
+
- **Zero dependencies** — no runtime dependency, no peer dependency, not a single `import`. The tools are plain tool-table objects with explicit JSON Schemas, so they install and behave identically from npm or from a linked checkout.
|
|
72
|
+
|
|
73
|
+
### Name collisions
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
`dsh-subagent-library` also registers a global `list_subagents` tool. Two global registrations of one name cannot both win: install one of them, or remember that the other may shadow this one.
|
|
54
76
|
|
|
55
|
-
|
|
77
|
+
The three new names were chosen precisely because they do **not** collide with the built-in `list_agents` / `send_message` / `interrupt_agent`.
|
|
56
78
|
|
|
57
79
|
### Uninstall
|
|
58
80
|
|
|
59
|
-
Remove or disable the bundle in the plugin panel. Nothing
|
|
81
|
+
Remove or disable the bundle in the plugin panel. Nothing is left behind.
|
|
82
|
+
|
|
83
|
+
### Optional extras (off by default)
|
|
84
|
+
|
|
85
|
+
The package also ships a second, **disabled-by-default** row: `subagent-control-extras`
|
|
86
|
+
(`dsh-subagent-control/extras`). Nothing changes until you enable it in the plugin panel.
|
|
87
|
+
When enabled it adds four tools:
|
|
88
|
+
|
|
89
|
+
| Tool | What it does | Risk |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `relay_subagent_message` | Best-effort message to a deeper descendant: delivered to the first hop with a request to forward it down | intermediate agents may summarize; not guaranteed |
|
|
92
|
+
| `wait_subagents` | Wait for descendants (bounded by `timeout_seconds`) instead of ending your turn first | blocks up to the timeout |
|
|
93
|
+
| `drain_subagents` | Stop **and release** direct children, unlike `interrupt_subagent`, which only stops the current turn | a released child cannot be woken by a message |
|
|
94
|
+
| `subagent_budget` | Read-only report: depth limit, how deep the tree goes, running/inactive counts | none |
|
|
95
|
+
|
|
96
|
+
A separate companion bundle, [`dsh-team-limits`](https://github.com/SpaceRelay/dsh-subagent-control/tree/main/team-limits),
|
|
97
|
+
raises the Agent Team limits (members, tasks, message bytes). It is a separate install on
|
|
98
|
+
purpose: it overrides the shipped `agent-team` row, and that should never happen implicitly.
|
|
60
99
|
|
|
61
100
|
### License
|
|
62
101
|
|
|
63
|
-
MIT — see [LICENSE](./LICENSE).
|
|
102
|
+
MIT — see [LICENSE](./LICENSE). Issues and source live at
|
|
103
|
+
[github.com/SpaceRelay/dsh-subagent-control](https://github.com/SpaceRelay/dsh-subagent-control).
|
|
64
104
|
|
|
65
105
|
---
|
|
66
106
|
|
|
67
|
-
##
|
|
107
|
+
## 简体中文
|
|
68
108
|
|
|
69
109
|
### 它解决什么问题
|
|
70
110
|
|
|
71
111
|
Harness 自带 `list_agents`、`send_message`、`interrupt_agent`。开启 Agent Team 后,团队插件会把**同名的三个工具**注册进每个团队成员的精确作用域,而更具体的作用域优先。于是在成员作用域里生效的是团队版:`target` 只认队友名,打断只有组长能做。
|
|
72
112
|
|
|
73
|
-
结果是:团队成员能用 `subagent`
|
|
113
|
+
结果是:团队成员能用 `subagent` 派出子代理,却列不出、追问不了、也打断不了它。而底层的子代理服务(`ctx.subagents`)本来就支持这三件事,权限规则也允许祖先管理自己的后代。
|
|
74
114
|
|
|
75
|
-
|
|
115
|
+
本插件不去抢那三个被占用的名字,而是全局注册三个**新名字**,组长和每个组员立刻都能用。
|
|
76
116
|
|
|
77
117
|
### 工具
|
|
78
118
|
|
|
79
|
-
| 工具 | 参数 |
|
|
119
|
+
| 工具 | 参数 | 底层调用 |
|
|
80
120
|
| --- | --- | --- |
|
|
81
121
|
| `list_subagents` | `scope?`:`"children"` 或 `"descendants"`,默认 `children` | `ctx.subagents.listChildren()` / `listDescendants()` |
|
|
82
|
-
| `send_subagent_message` | `subagent_id`、`message
|
|
122
|
+
| `send_subagent_message` | `subagent_id`、`message`(均必填) | `ctx.subagents.sendMessage()` |
|
|
83
123
|
| `interrupt_subagent` | `subagent_id`(必填) | `ctx.subagents.interrupt()`(祖先权限) |
|
|
84
124
|
|
|
85
|
-
|
|
125
|
+
### 用法
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
list_subagents
|
|
129
|
+
→ 3b0f9c1e running "review the patch" depth=1
|
|
130
|
+
|
|
131
|
+
list_subagents scope=descendants
|
|
132
|
+
→ 3b0f9c1e running "review the patch" depth=1
|
|
133
|
+
7d2a44b0 inactive "check the tests" depth=2
|
|
134
|
+
|
|
135
|
+
send_subagent_message subagent_id=3b0f9c1e message="顺手看一下 locale/zh.json"
|
|
136
|
+
→ messageId=...
|
|
137
|
+
|
|
138
|
+
interrupt_subagent subagent_id=3b0f9c1e
|
|
139
|
+
→ accepted=true
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
每行格式是 `id status "label" depth=`。`status` 为 `running` 或 `inactive`——**inactive 表示当前没有在跑,不代表子代理已经消失**;用 `send_subagent_message` 可以把它叫醒。
|
|
86
143
|
|
|
87
144
|
### 行为与边界
|
|
88
145
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
146
|
+
- **一次性子代理不会被列出。** 它们无法接收后续消息,列出来只会诱导一次注定失败的调用。
|
|
147
|
+
- **记录损坏时报告,而不是抛错。** 某个子代理状态读不出来,会作为诊断条目(`corrupt` / `unsupported` / `unavailable`)返回,而不是让整个列表失败。
|
|
148
|
+
- **`send_subagent_message` 只能发给直接子代理。** 这是服务本身的规则,插件没有放宽。
|
|
149
|
+
- **`interrupt_subagent` 覆盖所有后代**,所以组长也能打断孙代。
|
|
150
|
+
- **任何失败都说明原因**:没有调用者、id 不存在、或者不是你的子代理。
|
|
151
|
+
- **不写配置、不落存储。** 除了这三个服务,插件不读写任何东西。
|
|
94
152
|
|
|
95
153
|
### 安装
|
|
96
154
|
|
|
97
|
-
|
|
155
|
+
在插件面板 / 市场中安装,或者让智能体执行:
|
|
98
156
|
|
|
99
157
|
```
|
|
100
158
|
plugin_manager install_bundle target "dsh-subagent-control@1.0.0"
|
|
101
159
|
```
|
|
102
160
|
|
|
103
|
-
从本地目录安装时,把 npm
|
|
161
|
+
从本地目录安装时,把 npm 规格换成**目录路径**。安装即时生效,无需重启;装完后三个工具名可见、插件行为已启用。
|
|
104
162
|
|
|
105
163
|
### 兼容性
|
|
106
164
|
|
|
107
165
|
- 具备 `tools`、`subagents`、`agents` 三个服务的 DSH(0.2.x 及以上)。
|
|
108
166
|
- Node `^22.19.0 || >=24.0.0`。
|
|
109
|
-
-
|
|
167
|
+
- **零依赖**:没有运行时依赖、没有 peer 依赖,模块里一行 `import` 都没有。工具是带显式 JSON Schema 的普通定义对象,所以 npm 安装与软链安装行为完全一致。
|
|
110
168
|
|
|
111
|
-
###
|
|
169
|
+
### 撞名
|
|
112
170
|
|
|
113
171
|
`dsh-subagent-library` 也注册了全局的 `list_subagents`。同一个名字的两个全局注册不可能同时生效——二选一安装,或记住其中一个可能被另一个覆盖。
|
|
114
172
|
|
|
173
|
+
这三个新名字是特意挑的,**不会**和内置的 `list_agents` / `send_message` / `interrupt_agent` 冲突。
|
|
174
|
+
|
|
115
175
|
### 卸载
|
|
116
176
|
|
|
117
|
-
|
|
177
|
+
在插件面板里移除或停用即可,不会留下任何残留。
|
|
178
|
+
|
|
179
|
+
### 可选扩展(默认关闭)
|
|
180
|
+
|
|
181
|
+
包内还带第二行、**默认禁用**的组件:`subagent-control-extras`(`dsh-subagent-control/extras`)。不启用它就没有任何变化;启用后多出四个工具:
|
|
182
|
+
|
|
183
|
+
| 工具 | 作用 | 风险 |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `relay_subagent_message` | 跨层补话:把消息投给第一跳并请其向下转发 | 中间层可能改写,**不保证送达** |
|
|
186
|
+
| `wait_subagents` | 等后代结束再收工(受 `timeout_seconds` 限制) | 会阻塞到超时 |
|
|
187
|
+
| `drain_subagents` | 停止**并释放**直接子级;`interrupt_subagent` 只停当前回合 | 被释放的子级不能再靠消息唤醒 |
|
|
188
|
+
| `subagent_budget` | 只读报告:深度上限、树有多深、运行/空闲数量 | 无 |
|
|
189
|
+
|
|
190
|
+
另有一个独立的同伴包 [`dsh-team-limits`](https://github.com/SpaceRelay/dsh-subagent-control/tree/main/team-limits),用来提高 Agent Team 的成员数、任务数与消息字节上限。**它刻意做成单独安装**:它覆盖的是宿主自带的 `agent-team` 行,这种事不该偷偷发生。
|
|
118
191
|
|
|
119
192
|
### 许可
|
|
120
193
|
|
|
121
|
-
MIT,见 [LICENSE](./LICENSE)
|
|
194
|
+
MIT,见 [LICENSE](./LICENSE)。源码与问题反馈:
|
|
195
|
+
[github.com/SpaceRelay/dsh-subagent-control](https://github.com/SpaceRelay/dsh-subagent-control)。
|
package/cordis.patch.yml
CHANGED
package/extras.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional extras for dsh-subagent-control.
|
|
3
|
+
*
|
|
4
|
+
* This module is shipped as a disabled bundle row (`subagent-control-extras`).
|
|
5
|
+
* Nothing here runs until a user enables that row in the plugin panel, so the
|
|
6
|
+
* default install keeps exactly the three tools of the main module.
|
|
7
|
+
*
|
|
8
|
+
* Four tools:
|
|
9
|
+
* relay_subagent_message - best-effort message to a deeper descendant,
|
|
10
|
+
* delivered to the first hop with a forward request
|
|
11
|
+
* wait_subagents - wait for descendants instead of ending the turn
|
|
12
|
+
* drain_subagents - stop and release direct children (not "pause")
|
|
13
|
+
* subagent_budget - read-only report of the caller's delegation budget
|
|
14
|
+
*
|
|
15
|
+
* Like the main module this file imports nothing: the subagent service is
|
|
16
|
+
* reached through `ctx`, and the tool definitions are plain registry-ready
|
|
17
|
+
* objects with explicit JSON Schemas.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const name = 'subagent-control-extras'
|
|
21
|
+
|
|
22
|
+
export const inject = ['tools', 'subagents', 'agents']
|
|
23
|
+
|
|
24
|
+
/** A tool needs a calling agent; a schedule or an external call has none. */
|
|
25
|
+
function requireAgent(exec, tool) {
|
|
26
|
+
const agent = exec.agent
|
|
27
|
+
if (agent === undefined) {
|
|
28
|
+
throw new Error(`${tool} requires a calling agent (exec.agent was undefined)`)
|
|
29
|
+
}
|
|
30
|
+
return agent
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Rows from the descendant catalog, without diagnostics. */
|
|
34
|
+
function childrenOf(rows) {
|
|
35
|
+
return rows.filter((row) => row.kind === 'child')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build the chain from the caller's direct child down to the target. */
|
|
39
|
+
function pathToTarget(rows, targetId, callerId) {
|
|
40
|
+
const byId = new Map()
|
|
41
|
+
for (const row of childrenOf(rows)) byId.set(row.id, row)
|
|
42
|
+
let cursor = byId.get(targetId)
|
|
43
|
+
if (cursor === undefined) return undefined
|
|
44
|
+
const path = [cursor]
|
|
45
|
+
for (let guard = 0; guard < 16; guard += 1) {
|
|
46
|
+
if (cursor.depth === 1) break
|
|
47
|
+
const parent = byId.get(cursor.parentId)
|
|
48
|
+
if (parent === undefined || parent.id === callerId) break
|
|
49
|
+
path.unshift(parent)
|
|
50
|
+
cursor = parent
|
|
51
|
+
}
|
|
52
|
+
return path
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const BUDGET_SCHEMA = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
required: ['running', 'inactive'],
|
|
59
|
+
properties: {
|
|
60
|
+
maxDepth: { type: 'number' },
|
|
61
|
+
running: { type: 'number' },
|
|
62
|
+
inactive: { type: 'number' },
|
|
63
|
+
deepest: { type: 'number' },
|
|
64
|
+
levels: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
items: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
required: ['depth', 'count'],
|
|
70
|
+
properties: {
|
|
71
|
+
depth: { type: 'number' },
|
|
72
|
+
count: { type: 'number' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const RELAY_SCHEMA = {
|
|
80
|
+
type: 'object',
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
required: ['delivered', 'hops', 'nextHop'],
|
|
83
|
+
properties: {
|
|
84
|
+
delivered: { type: 'boolean' },
|
|
85
|
+
hops: { type: 'number' },
|
|
86
|
+
nextHop: { type: 'string' },
|
|
87
|
+
remainingDepth: { type: 'number' },
|
|
88
|
+
messageId: { type: 'string' },
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const WAIT_SCHEMA = {
|
|
93
|
+
type: 'object',
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
required: ['joined', 'timedOut', 'running'],
|
|
96
|
+
properties: {
|
|
97
|
+
joined: { type: 'boolean' },
|
|
98
|
+
timedOut: { type: 'boolean' },
|
|
99
|
+
running: { type: 'number' },
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DRAIN_SCHEMA = {
|
|
104
|
+
type: 'object',
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
required: ['released'],
|
|
107
|
+
properties: {
|
|
108
|
+
released: { type: 'array', items: { type: 'string' } },
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function apply(ctx) {
|
|
113
|
+
/* ------------------------------------------------------------- budget */
|
|
114
|
+
|
|
115
|
+
ctx.tools.register({
|
|
116
|
+
name: 'subagent_budget',
|
|
117
|
+
description:
|
|
118
|
+
'Report the delegation budget this agent is working with: the configured depth limit, ' +
|
|
119
|
+
'how deep the agents below it already go, and how many are running. ' +
|
|
120
|
+
'Read-only; it never starts, stops or messages anything.',
|
|
121
|
+
parameters: { type: 'object', additionalProperties: false, properties: {} },
|
|
122
|
+
output: {
|
|
123
|
+
schema: BUDGET_SCHEMA,
|
|
124
|
+
render: (_args, budget) => {
|
|
125
|
+
const parts = []
|
|
126
|
+
if (budget.maxDepth !== undefined) parts.push(`depth limit ${budget.maxDepth}`)
|
|
127
|
+
parts.push(`${budget.running} running`, `${budget.inactive} inactive`)
|
|
128
|
+
if (budget.deepest !== undefined) parts.push(`deepest ${budget.deepest}`)
|
|
129
|
+
return [{ type: 'text', text: parts.join(', ') }]
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
async execute(_args, exec) {
|
|
133
|
+
const caller = requireAgent(exec, 'subagent_budget')
|
|
134
|
+
let rows = []
|
|
135
|
+
try {
|
|
136
|
+
rows = await ctx.subagents.listDescendants(caller.id, exec.signal)
|
|
137
|
+
} catch {
|
|
138
|
+
rows = []
|
|
139
|
+
}
|
|
140
|
+
const levels = new Map()
|
|
141
|
+
let running = 0
|
|
142
|
+
let inactive = 0
|
|
143
|
+
let deepest = 0
|
|
144
|
+
for (const row of childrenOf(rows)) {
|
|
145
|
+
levels.set(row.depth, (levels.get(row.depth) ?? 0) + 1)
|
|
146
|
+
if (row.depth > deepest) deepest = row.depth
|
|
147
|
+
if (ctx.agents.get(row.id)?.status === 'running') running += 1
|
|
148
|
+
else inactive += 1
|
|
149
|
+
}
|
|
150
|
+
const budget = { running, inactive }
|
|
151
|
+
if (deepest > 0) budget.deepest = deepest
|
|
152
|
+
if (levels.size > 0) {
|
|
153
|
+
budget.levels = [...levels.entries()]
|
|
154
|
+
.sort((left, right) => left[0] - right[0])
|
|
155
|
+
.map(([depth, count]) => ({ depth, count }))
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const limit = ctx.subagents.resolveMaxDepth?.()
|
|
159
|
+
if (typeof limit === 'number') budget.maxDepth = limit
|
|
160
|
+
} catch {
|
|
161
|
+
/* the limit stays unknown rather than failing the report */
|
|
162
|
+
}
|
|
163
|
+
return budget
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/* -------------------------------------------------------------- relay */
|
|
168
|
+
|
|
169
|
+
ctx.tools.register({
|
|
170
|
+
name: 'relay_subagent_message',
|
|
171
|
+
description:
|
|
172
|
+
'Send a message to a deeper descendant that you cannot reach directly. ' +
|
|
173
|
+
'The subagent service only accepts messages for a direct child, so this tool delivers ' +
|
|
174
|
+
'the message to the first hop with a request to forward it further down. ' +
|
|
175
|
+
'Delivery below the first hop is best effort: each intermediate agent has to forward it, ' +
|
|
176
|
+
'and may summarize instead. Use send_subagent_message when the target is a direct child.',
|
|
177
|
+
parameters: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
additionalProperties: false,
|
|
180
|
+
required: ['subagent_id', 'message'],
|
|
181
|
+
properties: {
|
|
182
|
+
subagent_id: { type: 'string', description: 'The id of a descendant, from list_subagents scope=descendants.' },
|
|
183
|
+
message: { type: 'string', description: 'The message to deliver.' },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
output: {
|
|
187
|
+
schema: RELAY_SCHEMA,
|
|
188
|
+
render: (args, result) => [
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
text:
|
|
192
|
+
result.hops === 0
|
|
193
|
+
? `message delivered to subagent ${result.nextHop}`
|
|
194
|
+
: `relay started: delivered to first hop ${result.nextHop}, ${result.remainingDepth} level(s) to go (best effort)`,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
async execute(args, exec) {
|
|
199
|
+
const caller = requireAgent(exec, 'relay_subagent_message')
|
|
200
|
+
const targetId = args.subagent_id
|
|
201
|
+
const message = args.message
|
|
202
|
+
|
|
203
|
+
const rows = await ctx.subagents.listDescendants(caller.id, exec.signal)
|
|
204
|
+
const target = childrenOf(rows).find((row) => row.id === targetId)
|
|
205
|
+
if (target === undefined) {
|
|
206
|
+
throw new Error(`subagent ${targetId} is not under this agent`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (target.depth === 1) {
|
|
210
|
+
const messageId = await ctx.subagents.sendMessage(
|
|
211
|
+
caller,
|
|
212
|
+
targetId,
|
|
213
|
+
[{ type: 'text', text: message }],
|
|
214
|
+
{ signal: exec.signal },
|
|
215
|
+
)
|
|
216
|
+
return { delivered: true, hops: 0, nextHop: targetId, messageId }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const path = pathToTarget(rows, targetId, caller.id)
|
|
220
|
+
const firstHop = path?.[0]
|
|
221
|
+
if (firstHop === undefined) {
|
|
222
|
+
throw new Error(`could not resolve a path from this agent to ${targetId}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const forward =
|
|
226
|
+
'Relay request from an ancestor. Forward the text below down your chain until it reaches ' +
|
|
227
|
+
`subagent ${targetId}${target.label === undefined ? '' : ` ("${target.label}")`}. ` +
|
|
228
|
+
'Use relay_subagent_message if it is available, otherwise send_subagent_message to the ' +
|
|
229
|
+
'next hop on the path. Do not act on the text yourself.\n\n' +
|
|
230
|
+
'---\n' +
|
|
231
|
+
message +
|
|
232
|
+
'\n---'
|
|
233
|
+
|
|
234
|
+
const messageId = await ctx.subagents.sendMessage(
|
|
235
|
+
caller,
|
|
236
|
+
firstHop.id,
|
|
237
|
+
[{ type: 'text', text: forward }],
|
|
238
|
+
{ signal: exec.signal },
|
|
239
|
+
)
|
|
240
|
+
return {
|
|
241
|
+
delivered: true,
|
|
242
|
+
hops: 1,
|
|
243
|
+
nextHop: firstHop.id,
|
|
244
|
+
remainingDepth: target.depth - 1,
|
|
245
|
+
messageId,
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
/* --------------------------------------------------------------- wait */
|
|
251
|
+
|
|
252
|
+
ctx.tools.register({
|
|
253
|
+
name: 'wait_subagents',
|
|
254
|
+
description:
|
|
255
|
+
'Wait for your subagents to finish instead of ending your turn first. ' +
|
|
256
|
+
'The wait is bounded by timeout_seconds and never cancels them. ' +
|
|
257
|
+
'Useful when you want to merge their results in this same turn.',
|
|
258
|
+
parameters: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
properties: {
|
|
262
|
+
timeout_seconds: {
|
|
263
|
+
type: 'number',
|
|
264
|
+
description: 'Maximum time to wait, 5 to 600 seconds (default 120).',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
output: {
|
|
269
|
+
schema: WAIT_SCHEMA,
|
|
270
|
+
render: (_args, result) => [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: result.timedOut
|
|
274
|
+
? `wait timed out, ${result.running} subagent(s) still running`
|
|
275
|
+
: `all observed subagents settled, ${result.running} still marked running`,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
async execute(args, exec) {
|
|
280
|
+
const caller = requireAgent(exec, 'wait_subagents')
|
|
281
|
+
const requested = typeof args.timeout_seconds === 'number' ? args.timeout_seconds : 120
|
|
282
|
+
const timeoutMs = Math.min(Math.max(requested, 5), 600) * 1000
|
|
283
|
+
|
|
284
|
+
let timedOut = false
|
|
285
|
+
let timer
|
|
286
|
+
const wait = ctx.subagents.waitForChildren(caller)
|
|
287
|
+
await Promise.race([
|
|
288
|
+
wait.then(() => undefined),
|
|
289
|
+
new Promise((resolve) => {
|
|
290
|
+
timer = setTimeout(() => {
|
|
291
|
+
timedOut = true
|
|
292
|
+
resolve(undefined)
|
|
293
|
+
}, timeoutMs)
|
|
294
|
+
}),
|
|
295
|
+
])
|
|
296
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
297
|
+
|
|
298
|
+
let rows = []
|
|
299
|
+
try {
|
|
300
|
+
rows = await ctx.subagents.listDescendants(caller.id, exec.signal)
|
|
301
|
+
} catch {
|
|
302
|
+
rows = []
|
|
303
|
+
}
|
|
304
|
+
const running = childrenOf(rows).filter((row) => ctx.agents.get(row.id)?.status === 'running').length
|
|
305
|
+
return { joined: !timedOut, timedOut, running }
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
/* -------------------------------------------------------------- drain */
|
|
310
|
+
|
|
311
|
+
ctx.tools.register({
|
|
312
|
+
name: 'drain_subagents',
|
|
313
|
+
description:
|
|
314
|
+
'Stop and release subagents you started, freeing their run instead of just interrupting ' +
|
|
315
|
+
'the current turn. Accepts direct children only; a released child cannot be woken again ' +
|
|
316
|
+
'with a message, so use interrupt_subagent when you only want to stop the current work.',
|
|
317
|
+
parameters: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
additionalProperties: false,
|
|
320
|
+
required: ['subagent_ids'],
|
|
321
|
+
properties: {
|
|
322
|
+
subagent_ids: {
|
|
323
|
+
type: 'array',
|
|
324
|
+
items: { type: 'string' },
|
|
325
|
+
description: 'Direct child ids to release, from list_subagents.',
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
output: {
|
|
330
|
+
schema: DRAIN_SCHEMA,
|
|
331
|
+
render: (_args, result) => [{ type: 'text', text: `released ${result.released.length} subagent(s)` }],
|
|
332
|
+
},
|
|
333
|
+
async execute(args, exec) {
|
|
334
|
+
const caller = requireAgent(exec, 'drain_subagents')
|
|
335
|
+
const ids = args.subagent_ids
|
|
336
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
337
|
+
throw new Error('drain_subagents needs at least one subagent id')
|
|
338
|
+
}
|
|
339
|
+
await ctx.subagents.drainChildren(caller, ids)
|
|
340
|
+
return { released: ids }
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subagent-control",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Three tools — list_subagents, send_subagent_message, interrupt_subagent — that let any agent, including a team member, list, steer and stop the subagents it started.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
"icon": "./icon.svg",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js",
|
|
15
|
+
"./extras": "./extras.js",
|
|
15
16
|
"./package.json": "./package.json",
|
|
16
17
|
"./locale/*.json": "./locale/*.json",
|
|
17
18
|
"./icon": "./icon.svg"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"index.js",
|
|
22
|
+
"extras.js",
|
|
21
23
|
"cordis.patch.yml",
|
|
22
24
|
"locale/*.json",
|
|
23
25
|
"icon.svg",
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"node": "^22.19.0 || >=24.0.0"
|
|
40
42
|
},
|
|
41
43
|
"scripts": {
|
|
42
|
-
"test": "node test/smoke.mjs"
|
|
44
|
+
"test": "node test/smoke.mjs && node test/smoke-extras.mjs"
|
|
43
45
|
},
|
|
44
46
|
"dsh": {
|
|
45
47
|
"bundle": {
|