dsh-subagent-control 1.0.3 → 1.0.5
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 +19 -0
- package/README.md +29 -18
- package/components/budget/index.js +93 -0
- package/components/budget/locale/en.json +6 -0
- package/components/budget/locale/zh.json +6 -0
- package/components/budget/package.json +5 -0
- package/components/drain/index.js +56 -0
- package/components/drain/locale/en.json +6 -0
- package/components/drain/locale/zh.json +6 -0
- package/components/drain/package.json +5 -0
- package/components/relay/index.js +108 -0
- package/components/relay/locale/en.json +6 -0
- package/components/relay/locale/zh.json +6 -0
- package/components/relay/package.json +5 -0
- package/components/shared.js +37 -0
- package/components/wait/index.js +81 -0
- package/components/wait/locale/en.json +6 -0
- package/components/wait/locale/zh.json +6 -0
- package/components/wait/package.json +5 -0
- package/cordis.patch.yml +14 -2
- package/package.json +15 -4
- package/extras.js +0 -343
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.5
|
|
4
|
+
|
|
5
|
+
- The `subagent_budget` tool says "not running" instead of "inactive", and its line now lists how
|
|
6
|
+
many subagents sit on each level (`levels 1:3 2:1`).
|
|
7
|
+
- The `drain_subagents` description no longer claims a released child can never be woken again.
|
|
8
|
+
Drain drops the activation and gives the run back; the session stays continuable, so a later
|
|
9
|
+
message cold-resumes it.
|
|
10
|
+
|
|
11
|
+
## 1.0.4
|
|
12
|
+
|
|
13
|
+
- The single optional `extras` row is split into four independent components, each with its own
|
|
14
|
+
switch in the plugin panel and its own localized title and description: Cross-layer relay,
|
|
15
|
+
Wait for subagents, Release subagents, Delegation budget. Enabling one registers exactly one
|
|
16
|
+
tool instead of all four.
|
|
17
|
+
- Removed the `dsh-subagent-control/extras` export in favour of `./relay`, `./wait`, `./drain`
|
|
18
|
+
and `./budget`, each also exporting `package.json` and `locale/*.json` for the panel.
|
|
19
|
+
- Unchanged: the base row still registers exactly the three original tools, and every component
|
|
20
|
+
still starts disabled.
|
|
21
|
+
|
|
3
22
|
## 1.0.3
|
|
4
23
|
|
|
5
24
|
- `list_subagents` marks Team members, which are continuable subagents: each row carries
|
package/README.md
CHANGED
|
@@ -82,23 +82,32 @@ The three new names were chosen precisely because they do **not** collide with t
|
|
|
82
82
|
|
|
83
83
|
Remove or disable the bundle in the plugin panel. Nothing is left behind.
|
|
84
84
|
|
|
85
|
-
### Optional
|
|
85
|
+
### Optional components (each off by default)
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
(`
|
|
89
|
-
|
|
87
|
+
Four extra tools ship as **separate components**, each with its own row and switch in the plugin
|
|
88
|
+
panel (`Cross-layer relay`, `Wait for subagents`, `Release subagents`, `Delegation budget`).
|
|
89
|
+
All four start disabled; turning one on registers exactly one tool, so you can enable only what
|
|
90
|
+
you need.
|
|
90
91
|
|
|
91
|
-
| Tool | What it does | Risk |
|
|
92
|
-
| --- | --- | --- |
|
|
93
|
-
| `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 |
|
|
94
|
-
| `wait_subagents` | Wait for descendants (bounded by `timeout_seconds`) instead of ending your turn first | blocks up to the timeout |
|
|
95
|
-
| `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 |
|
|
96
|
-
| `subagent_budget` | Read-only report: depth limit, how deep the tree goes, running/inactive counts | none |
|
|
92
|
+
| Component | Tool | What it does | Risk |
|
|
93
|
+
| --- | --- | --- | --- |
|
|
94
|
+
| Cross-layer relay | `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 |
|
|
95
|
+
| Wait for subagents | `wait_subagents` | Wait for descendants (bounded by `timeout_seconds`) instead of ending your turn first | blocks up to the timeout |
|
|
96
|
+
| Release subagents | `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 |
|
|
97
|
+
| Delegation budget | `subagent_budget` | Read-only report: depth limit, how deep the tree goes, running/inactive counts | none |
|
|
97
98
|
|
|
98
99
|
A separate companion bundle, [`dsh-team-limits`](https://github.com/SpaceRelay/dsh-subagent-control/tree/main/team-limits),
|
|
99
100
|
raises the Agent Team limits (members, tasks, message bytes). It is a separate install on
|
|
100
101
|
purpose: it overrides the shipped `agent-team` row, and that should never happen implicitly.
|
|
101
102
|
|
|
103
|
+
A second companion bundle, [`dsh-human-wait`](https://github.com/SpaceRelay/dsh-subagent-control/tree/main/human-wait),
|
|
104
|
+
stops the PTC `run_code` deadline from counting the time you spend answering a question, reviewing
|
|
105
|
+
a plan or approving a request. PTC mode wraps the whole program — nested tool and approval waits
|
|
106
|
+
included — in one 120 s wall clock, so a slow answer used to surface as
|
|
107
|
+
`Error: code run failed (timeout): execution deadline reached (120000ms)`. With the bundle
|
|
108
|
+
installed that wait no longer burns the budget (default cap 24 h, `-1` = unlimited) while a plain
|
|
109
|
+
runaway program is still stopped at the normal budget.
|
|
110
|
+
|
|
102
111
|
### License
|
|
103
112
|
|
|
104
113
|
MIT — see [LICENSE](./LICENSE). Issues and source live at
|
|
@@ -180,19 +189,21 @@ plugin_manager install_bundle target "dsh-subagent-control@1.0.0"
|
|
|
180
189
|
|
|
181
190
|
在插件面板里移除或停用即可,不会留下任何残留。
|
|
182
191
|
|
|
183
|
-
###
|
|
192
|
+
### 可选组件(各自默认关闭)
|
|
184
193
|
|
|
185
|
-
|
|
194
|
+
另外四个工具是**四个独立组件**,在插件面板里各有自己的一行和一个开关(中文界面显示:跨层补话 / 等待子代理 / 释放子代理 / 委派预算)。四个都默认关闭,打开哪一个就只注册那一个工具——只开你需要的。
|
|
186
195
|
|
|
187
|
-
| 工具 | 作用 | 风险 |
|
|
188
|
-
| --- | --- | --- |
|
|
189
|
-
| `relay_subagent_message` |
|
|
190
|
-
| `wait_subagents` | 等后代结束再收工(受 `timeout_seconds` 限制) | 会阻塞到超时 |
|
|
191
|
-
| `drain_subagents` | 停止**并释放**直接子级;`interrupt_subagent` 只停当前回合 | 被释放的子级不能再靠消息唤醒 |
|
|
192
|
-
| `subagent_budget` | 只读报告:深度上限、树有多深、运行/空闲数量 | 无 |
|
|
196
|
+
| 组件 | 工具 | 作用 | 风险 |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| 跨层补话 | `relay_subagent_message` | 把消息投给第一跳并请它继续向下转发 | 中间层可能改写,**不保证送达** |
|
|
199
|
+
| 等待子代理 | `wait_subagents` | 等后代结束再收工(受 `timeout_seconds` 限制) | 会阻塞到超时 |
|
|
200
|
+
| 释放子代理 | `drain_subagents` | 停止**并释放**直接子级;`interrupt_subagent` 只停当前回合 | 被释放的子级不能再靠消息唤醒 |
|
|
201
|
+
| 委派预算 | `subagent_budget` | 只读报告:深度上限、树有多深、运行/空闲数量 | 无 |
|
|
193
202
|
|
|
194
203
|
另有一个独立的同伴包 [`dsh-team-limits`](https://github.com/SpaceRelay/dsh-subagent-control/tree/main/team-limits),用来提高 Agent Team 的成员数、任务数与消息字节上限。**它刻意做成单独安装**:它覆盖的是宿主自带的 `agent-team` 行,这种事不该偷偷发生。
|
|
195
204
|
|
|
205
|
+
还有一个独立的同伴包 [`dsh-human-wait`](https://github.com/SpaceRelay/dsh-subagent-control/tree/main/human-wait),用来让 PTC 模式 `run_code` 的执行预算在「等你回答提问 / 审议计划 / 审批」期间暂停。PTC 会把整个程序——**包括嵌套的工具调用与审批等待**——包在一个 120 秒墙钟里,所以回答慢一点就会看到 `Error: code run failed (timeout): execution deadline reached (120000ms)`;装上之后这段等待不再计入预算(默认上限 24 小时,`-1` 表示不限),而普通的死循环程序仍会在原预算处被终止。**同样刻意做成单独安装**:它拦截的是宿主 `ptc-runtime` 的执行预算。
|
|
206
|
+
|
|
196
207
|
### 许可
|
|
197
208
|
|
|
198
209
|
MIT,见 [LICENSE](./LICENSE)。源码与问题反馈:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation budget: a read-only look at what the caller has below it.
|
|
3
|
+
*
|
|
4
|
+
* Use it before planning a fan-out: how deep the chain already goes, how many
|
|
5
|
+
* agents sit on each level, and how many are running. It never starts, stops or
|
|
6
|
+
* messages anything.
|
|
7
|
+
*/
|
|
8
|
+
import { childrenOf, requireAgent } from '../shared.js'
|
|
9
|
+
|
|
10
|
+
export const name = 'subagent-control-budget'
|
|
11
|
+
|
|
12
|
+
export const inject = ['tools', 'subagents', 'agents']
|
|
13
|
+
|
|
14
|
+
const BUDGET_SCHEMA = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
required: ['running', 'inactive'],
|
|
18
|
+
properties: {
|
|
19
|
+
maxDepth: { type: 'number' },
|
|
20
|
+
running: { type: 'number' },
|
|
21
|
+
inactive: { type: 'number' },
|
|
22
|
+
deepest: { type: 'number' },
|
|
23
|
+
levels: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
items: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
required: ['depth', 'count'],
|
|
29
|
+
properties: {
|
|
30
|
+
depth: { type: 'number' },
|
|
31
|
+
count: { type: 'number' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function apply(ctx) {
|
|
39
|
+
ctx.tools.register({
|
|
40
|
+
name: 'subagent_budget',
|
|
41
|
+
description:
|
|
42
|
+
'Report the delegation budget this agent is working with: the configured depth limit, ' +
|
|
43
|
+
'how deep the agents below it already go, how many sit on each level, and how many are running. ' +
|
|
44
|
+
'Read-only; it never starts, stops or messages anything.',
|
|
45
|
+
parameters: { type: 'object', additionalProperties: false, properties: {} },
|
|
46
|
+
output: {
|
|
47
|
+
schema: BUDGET_SCHEMA,
|
|
48
|
+
render: (_args, budget) => {
|
|
49
|
+
const parts = []
|
|
50
|
+
if (budget.maxDepth !== undefined) parts.push(`depth limit ${budget.maxDepth}`)
|
|
51
|
+
parts.push(`${budget.running} running`, `${budget.inactive} not running`)
|
|
52
|
+
if (budget.deepest !== undefined) parts.push(`deepest ${budget.deepest}`)
|
|
53
|
+
if (budget.levels !== undefined && budget.levels.length > 0) {
|
|
54
|
+
parts.push(`levels ${budget.levels.map((level) => `${level.depth}:${level.count}`).join(' ')}`)
|
|
55
|
+
}
|
|
56
|
+
return [{ type: 'text', text: parts.join(', ') }]
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
async execute(_args, exec) {
|
|
60
|
+
const caller = requireAgent(exec, 'subagent_budget')
|
|
61
|
+
let rows = []
|
|
62
|
+
try {
|
|
63
|
+
rows = await ctx.subagents.listDescendants(caller.id, exec.signal)
|
|
64
|
+
} catch {
|
|
65
|
+
rows = []
|
|
66
|
+
}
|
|
67
|
+
const levels = new Map()
|
|
68
|
+
let running = 0
|
|
69
|
+
let inactive = 0
|
|
70
|
+
let deepest = 0
|
|
71
|
+
for (const row of childrenOf(rows)) {
|
|
72
|
+
levels.set(row.depth, (levels.get(row.depth) ?? 0) + 1)
|
|
73
|
+
if (row.depth > deepest) deepest = row.depth
|
|
74
|
+
if (ctx.agents.get(row.id)?.status === 'running') running += 1
|
|
75
|
+
else inactive += 1
|
|
76
|
+
}
|
|
77
|
+
const budget = { running, inactive }
|
|
78
|
+
if (deepest > 0) budget.deepest = deepest
|
|
79
|
+
if (levels.size > 0) {
|
|
80
|
+
budget.levels = [...levels.entries()]
|
|
81
|
+
.sort((left, right) => left[0] - right[0])
|
|
82
|
+
.map(([depth, count]) => ({ depth, count }))
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const limit = ctx.subagents.resolveMaxDepth?.()
|
|
86
|
+
if (typeof limit === 'number') budget.maxDepth = limit
|
|
87
|
+
} catch {
|
|
88
|
+
/* the limit stays unknown rather than failing the report */
|
|
89
|
+
}
|
|
90
|
+
return budget
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drain: stop and release direct children, freeing their run.
|
|
3
|
+
*
|
|
4
|
+
* interrupt_subagent only ends the current turn and keeps the activation, so a
|
|
5
|
+
* stopped subagent stays resident. Drain drops the activation and gives the run
|
|
6
|
+
* back; the session itself stays continuable, so a later message cold-resumes it.
|
|
7
|
+
*/
|
|
8
|
+
import { requireAgent } from '../shared.js'
|
|
9
|
+
|
|
10
|
+
export const name = 'subagent-control-drain'
|
|
11
|
+
|
|
12
|
+
export const inject = ['tools', 'subagents', 'agents']
|
|
13
|
+
|
|
14
|
+
const DRAIN_SCHEMA = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
required: ['released'],
|
|
18
|
+
properties: {
|
|
19
|
+
released: { type: 'array', items: { type: 'string' } },
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function apply(ctx) {
|
|
24
|
+
ctx.tools.register({
|
|
25
|
+
name: 'drain_subagents',
|
|
26
|
+
description:
|
|
27
|
+
'Stop subagents you started without waiting for them to finish and free the run ' +
|
|
28
|
+
'they occupy. Direct children only; a later message still wakes a released child. ' +
|
|
29
|
+
'Use interrupt_subagent when you only want to stop the current work.',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
required: ['subagent_ids'],
|
|
34
|
+
properties: {
|
|
35
|
+
subagent_ids: {
|
|
36
|
+
type: 'array',
|
|
37
|
+
items: { type: 'string' },
|
|
38
|
+
description: 'Direct child ids to release, from list_subagents.',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
output: {
|
|
43
|
+
schema: DRAIN_SCHEMA,
|
|
44
|
+
render: (_args, result) => [{ type: 'text', text: `released ${result.released.length} subagent(s)` }],
|
|
45
|
+
},
|
|
46
|
+
async execute(args, exec) {
|
|
47
|
+
const caller = requireAgent(exec, 'drain_subagents')
|
|
48
|
+
const ids = args.subagent_ids
|
|
49
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
50
|
+
throw new Error('drain_subagents needs at least one subagent id')
|
|
51
|
+
}
|
|
52
|
+
await ctx.subagents.drainChildren(caller, ids)
|
|
53
|
+
return { released: ids }
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-layer relay: hand a message to the first hop and ask it to forward.
|
|
3
|
+
*
|
|
4
|
+
* The subagent service only accepts messages for a direct child, so a deeper
|
|
5
|
+
* descendant is reached by a prompt-level relay. It is best effort by nature:
|
|
6
|
+
* the middle agent has to cooperate, and may summarize instead of forwarding.
|
|
7
|
+
*/
|
|
8
|
+
import { childrenOf, pathToTarget, requireAgent } from '../shared.js'
|
|
9
|
+
|
|
10
|
+
export const name = 'subagent-control-relay'
|
|
11
|
+
|
|
12
|
+
export const inject = ['tools', 'subagents', 'agents']
|
|
13
|
+
|
|
14
|
+
const RELAY_SCHEMA = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
required: ['delivered', 'hops', 'nextHop'],
|
|
18
|
+
properties: {
|
|
19
|
+
delivered: { type: 'boolean' },
|
|
20
|
+
hops: { type: 'number' },
|
|
21
|
+
nextHop: { type: 'string' },
|
|
22
|
+
remainingDepth: { type: 'number' },
|
|
23
|
+
messageId: { type: 'string' },
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function apply(ctx) {
|
|
28
|
+
ctx.tools.register({
|
|
29
|
+
name: 'relay_subagent_message',
|
|
30
|
+
description:
|
|
31
|
+
'Send a message to a deeper descendant that you cannot reach directly. ' +
|
|
32
|
+
'The subagent service only accepts messages for a direct child, so this tool delivers ' +
|
|
33
|
+
'the message to the first hop with a request to forward it further down. ' +
|
|
34
|
+
'Delivery below the first hop is best effort: each intermediate agent has to forward it, ' +
|
|
35
|
+
'and may summarize instead. Use send_subagent_message when the target is a direct child.',
|
|
36
|
+
parameters: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
required: ['subagent_id', 'message'],
|
|
40
|
+
properties: {
|
|
41
|
+
subagent_id: { type: 'string', description: 'The id of a descendant, from list_subagents scope=descendants.' },
|
|
42
|
+
message: { type: 'string', description: 'The message to deliver.' },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
output: {
|
|
46
|
+
schema: RELAY_SCHEMA,
|
|
47
|
+
render: (_args, result) => [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text:
|
|
51
|
+
result.hops === 0
|
|
52
|
+
? `message delivered to subagent ${result.nextHop}`
|
|
53
|
+
: `relay started: delivered to first hop ${result.nextHop}, ${result.remainingDepth} level(s) to go (best effort)`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
async execute(args, exec) {
|
|
58
|
+
const caller = requireAgent(exec, 'relay_subagent_message')
|
|
59
|
+
const targetId = args.subagent_id
|
|
60
|
+
const message = args.message
|
|
61
|
+
|
|
62
|
+
const rows = await ctx.subagents.listDescendants(caller.id, exec.signal)
|
|
63
|
+
const target = childrenOf(rows).find((row) => row.id === targetId)
|
|
64
|
+
if (target === undefined) {
|
|
65
|
+
throw new Error(`subagent ${targetId} is not under this agent`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (target.depth === 1) {
|
|
69
|
+
const messageId = await ctx.subagents.sendMessage(
|
|
70
|
+
caller,
|
|
71
|
+
targetId,
|
|
72
|
+
[{ type: 'text', text: message }],
|
|
73
|
+
{ signal: exec.signal },
|
|
74
|
+
)
|
|
75
|
+
return { delivered: true, hops: 0, nextHop: targetId, messageId }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const path = pathToTarget(rows, targetId, caller.id)
|
|
79
|
+
const firstHop = path?.[0]
|
|
80
|
+
if (firstHop === undefined) {
|
|
81
|
+
throw new Error(`could not resolve a path from this agent to ${targetId}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const forward =
|
|
85
|
+
'Relay request from an ancestor. Forward the text below down your chain until it reaches ' +
|
|
86
|
+
`subagent ${targetId}${target.label === undefined ? '' : ` ("${target.label}")`}. ` +
|
|
87
|
+
'Use relay_subagent_message if it is available, otherwise send_subagent_message to the ' +
|
|
88
|
+
'next hop on the path. Do not act on the text yourself.\n\n' +
|
|
89
|
+
'---\n' +
|
|
90
|
+
message +
|
|
91
|
+
'\n---'
|
|
92
|
+
|
|
93
|
+
const messageId = await ctx.subagents.sendMessage(
|
|
94
|
+
caller,
|
|
95
|
+
firstHop.id,
|
|
96
|
+
[{ type: 'text', text: forward }],
|
|
97
|
+
{ signal: exec.signal },
|
|
98
|
+
)
|
|
99
|
+
return {
|
|
100
|
+
delivered: true,
|
|
101
|
+
hops: 1,
|
|
102
|
+
nextHop: firstHop.id,
|
|
103
|
+
remainingDepth: target.depth - 1,
|
|
104
|
+
messageId,
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the optional components.
|
|
3
|
+
*
|
|
4
|
+
* Plain relative module: this package imports no bare specifier, so a linked
|
|
5
|
+
* checkout and an npm install resolve exactly the same way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A tool needs a calling agent; a schedule or an external call has none. */
|
|
9
|
+
export function requireAgent(exec, tool) {
|
|
10
|
+
const agent = exec.agent
|
|
11
|
+
if (agent === undefined) {
|
|
12
|
+
throw new Error(`${tool} requires a calling agent (exec.agent was undefined)`)
|
|
13
|
+
}
|
|
14
|
+
return agent
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rows from the descendant catalog, without diagnostics. */
|
|
18
|
+
export function childrenOf(rows) {
|
|
19
|
+
return rows.filter((row) => row.kind === 'child')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Build the chain from the caller's direct child down to the target. */
|
|
23
|
+
export function pathToTarget(rows, targetId, callerId) {
|
|
24
|
+
const byId = new Map()
|
|
25
|
+
for (const row of childrenOf(rows)) byId.set(row.id, row)
|
|
26
|
+
let cursor = byId.get(targetId)
|
|
27
|
+
if (cursor === undefined) return undefined
|
|
28
|
+
const path = [cursor]
|
|
29
|
+
for (let guard = 0; guard < 16; guard += 1) {
|
|
30
|
+
if (cursor.depth === 1) break
|
|
31
|
+
const parent = byId.get(cursor.parentId)
|
|
32
|
+
if (parent === undefined || parent.id === callerId) break
|
|
33
|
+
path.unshift(parent)
|
|
34
|
+
cursor = parent
|
|
35
|
+
}
|
|
36
|
+
return path
|
|
37
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait for subagents inside the current turn.
|
|
3
|
+
*
|
|
4
|
+
* The wait is bounded and never cancels anything: it only decides whether this
|
|
5
|
+
* turn keeps going or reports a timeout with what is still running.
|
|
6
|
+
*/
|
|
7
|
+
import { childrenOf, requireAgent } from '../shared.js'
|
|
8
|
+
|
|
9
|
+
export const name = 'subagent-control-wait'
|
|
10
|
+
|
|
11
|
+
export const inject = ['tools', 'subagents', 'agents']
|
|
12
|
+
|
|
13
|
+
const WAIT_SCHEMA = {
|
|
14
|
+
type: 'object',
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
required: ['joined', 'timedOut', 'running'],
|
|
17
|
+
properties: {
|
|
18
|
+
joined: { type: 'boolean' },
|
|
19
|
+
timedOut: { type: 'boolean' },
|
|
20
|
+
running: { type: 'number' },
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function apply(ctx) {
|
|
25
|
+
ctx.tools.register({
|
|
26
|
+
name: 'wait_subagents',
|
|
27
|
+
description:
|
|
28
|
+
'Wait for your subagents to finish instead of ending your turn first. ' +
|
|
29
|
+
'The wait is bounded by timeout_seconds and never cancels them. ' +
|
|
30
|
+
'Useful when you want to merge their results in this same turn.',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
properties: {
|
|
35
|
+
timeout_seconds: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
description: 'Maximum time to wait, 5 to 600 seconds (default 120).',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
output: {
|
|
42
|
+
schema: WAIT_SCHEMA,
|
|
43
|
+
render: (_args, result) => [
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: result.timedOut
|
|
47
|
+
? `wait timed out, ${result.running} subagent(s) still running`
|
|
48
|
+
: `all observed subagents settled, ${result.running} still marked running`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
async execute(args, exec) {
|
|
53
|
+
const caller = requireAgent(exec, 'wait_subagents')
|
|
54
|
+
const requested = typeof args.timeout_seconds === 'number' ? args.timeout_seconds : 120
|
|
55
|
+
const timeoutMs = Math.min(Math.max(requested, 5), 600) * 1000
|
|
56
|
+
|
|
57
|
+
let timedOut = false
|
|
58
|
+
let timer
|
|
59
|
+
const wait = ctx.subagents.waitForChildren(caller)
|
|
60
|
+
await Promise.race([
|
|
61
|
+
wait.then(() => undefined),
|
|
62
|
+
new Promise((resolve) => {
|
|
63
|
+
timer = setTimeout(() => {
|
|
64
|
+
timedOut = true
|
|
65
|
+
resolve(undefined)
|
|
66
|
+
}, timeoutMs)
|
|
67
|
+
}),
|
|
68
|
+
])
|
|
69
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
70
|
+
|
|
71
|
+
let rows = []
|
|
72
|
+
try {
|
|
73
|
+
rows = await ctx.subagents.listDescendants(caller.id, exec.signal)
|
|
74
|
+
} catch {
|
|
75
|
+
rows = []
|
|
76
|
+
}
|
|
77
|
+
const running = childrenOf(rows).filter((row) => ctx.agents.get(row.id)?.status === 'running').length
|
|
78
|
+
return { joined: !timedOut, timedOut, running }
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
# Four opt-in components, each with its own switch in the plugin panel.
|
|
2
|
+
# The base row is the three core tools; every component below starts disabled,
|
|
3
|
+
# so a default install registers nothing beyond those three.
|
|
1
4
|
- insert:
|
|
2
5
|
- id: subagent-control
|
|
3
6
|
name: 'dsh-subagent-control'
|
|
4
|
-
- id: subagent-control-
|
|
5
|
-
name: 'dsh-subagent-control/
|
|
7
|
+
- id: subagent-control-relay
|
|
8
|
+
name: 'dsh-subagent-control/relay'
|
|
9
|
+
disabled: true
|
|
10
|
+
- id: subagent-control-wait
|
|
11
|
+
name: 'dsh-subagent-control/wait'
|
|
12
|
+
disabled: true
|
|
13
|
+
- id: subagent-control-drain
|
|
14
|
+
name: 'dsh-subagent-control/drain'
|
|
15
|
+
disabled: true
|
|
16
|
+
- id: subagent-control-budget
|
|
17
|
+
name: 'dsh-subagent-control/budget'
|
|
6
18
|
disabled: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subagent-control",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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,14 +12,25 @@
|
|
|
12
12
|
"icon": "./icon.svg",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js",
|
|
15
|
-
"./extras": "./extras.js",
|
|
16
15
|
"./package.json": "./package.json",
|
|
17
16
|
"./locale/*.json": "./locale/*.json",
|
|
18
|
-
"./icon": "./icon.svg"
|
|
17
|
+
"./icon": "./icon.svg",
|
|
18
|
+
"./relay": "./components/relay/index.js",
|
|
19
|
+
"./relay/package.json": "./components/relay/package.json",
|
|
20
|
+
"./relay/locale/*.json": "./components/relay/locale/*.json",
|
|
21
|
+
"./wait": "./components/wait/index.js",
|
|
22
|
+
"./wait/package.json": "./components/wait/package.json",
|
|
23
|
+
"./wait/locale/*.json": "./components/wait/locale/*.json",
|
|
24
|
+
"./drain": "./components/drain/index.js",
|
|
25
|
+
"./drain/package.json": "./components/drain/package.json",
|
|
26
|
+
"./drain/locale/*.json": "./components/drain/locale/*.json",
|
|
27
|
+
"./budget": "./components/budget/index.js",
|
|
28
|
+
"./budget/package.json": "./components/budget/package.json",
|
|
29
|
+
"./budget/locale/*.json": "./components/budget/locale/*.json"
|
|
19
30
|
},
|
|
20
31
|
"files": [
|
|
21
32
|
"index.js",
|
|
22
|
-
"
|
|
33
|
+
"components",
|
|
23
34
|
"cordis.patch.yml",
|
|
24
35
|
"locale/*.json",
|
|
25
36
|
"icon.svg",
|
package/extras.js
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
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
|
-
}
|