@hydraharness/harness-tool-subagent-control 0.1.1-rc.6

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
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 ADDED
@@ -0,0 +1,74 @@
1
+ # @hydraharness/harness-tool-subagent-control
2
+
3
+ The optional, globally named `send_message`, `interrupt_agent`, and `list_agents` tools are thin adapters over `ctx.subagents`. Provider-bound `@hydraharness/harness-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers shared control tools once, so multiple delegation tools never register duplicate global controls. The root plugin registers `send_message` and `interrupt_agent` and requires only `subagents`; the separately loadable `./list-agents` plugin registers `list_agents` and declares `subagents` plus `agents` as load-time dependencies. Its catalog reads additionally require the session store and projection registry at call time, but no query service. A deployment can keep the root tools while omitting the list tool. No tool's presence determines whether a delegation tool starts continuable work. These tools own only the parent-to-child direction; the independently installed [`@hydraharness/harness-tool-subagent-report`](../tool-subagent-report/README.md) owns the child-to-parent direction.
4
+
5
+ The tool performs no lifecycle routing — residency and cold resume belong to the subagent service. It passes `exec.agent` as the exact live parent that authorizes delivery and records every message source as `{ kind: 'coordinator', senderSessionId: parent.id }`, which the service retains but never treats as authority. Every message becomes the subagent's next FIFO turn through `Agent.followup()`: if the child is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The tool forwards its execution signal, which owns admission only until inbox acceptance; once the child accepts the message the accepted turn cannot be cancelled through this tool. This call returns no child reply — its transcript by that id is the source of what it did — and a child with `report` sends content on its own initiative as a separate parent message. A delivery failure becomes an errored tool result stating the message was not delivered.
6
+
7
+ `interrupt_agent(agent_id)` passes `exec.agent` as the exact live ancestor authority for `ctx.subagents.interrupt()`: the target may be a direct child or a deeper descendant, and the service — never this tool — verifies the caller against the target Activation's recorded lineage. Only the target's current turn stops (`keepInbox`): queued messages stay parked until a later `send_message`, published descendants keep running, and the child stays available for follow-ups. The call returns as soon as the stop request is accepted, without waiting for target quiescence; an absent or already-settled target is an accepted no-op, while self, sibling, stale, and non-ancestor callers become errored results.
8
+
9
+ `list_agents` takes one optional `scope` argument, derives the root id from the calling agent, and projects the service catalog to continuable children without a cursor. The default `children` scope reads `ctx.subagents.listChildren()`; `descendants` reads `ctx.subagents.listDescendants()`, whose one-corpus walk crosses ordinary sessions and one-shot children and renders surviving rows in stable pre-order with `parent=<id> depth=<n>`. The `parent` annotation is the durable direct-parent session id and may name an ordinary session omitted from the output. For the calling agent, only depth-1 child entries are `send_message` candidates; deeper child entries are `interrupt_agent` candidates only. Status comes from the live Agent registry: `running` (active driver), `idle` (resident between turns, possibly waiting on agents it started), or `ready` (storage only and resumable rather than terminal). The service result also contains one-shot session-backed subagents for consumers such as a UI, but those entries are omitted from this model tool because they cannot accept `send_message`. Diagnostics remain visible, with positions in the descendants scope. Durable identity and mode come from each child's descriptor, while delivery-time authority and Activation ownership checks remain the service's.
10
+
11
+ ## Model Experience
12
+
13
+ ### Tool schema
14
+
15
+ #### What the model sees
16
+
17
+ The generated [schemas](../../../docs/tool-catalog.md#hydraharness-tool-subagent-control): `send_message` takes `subagent_id` and `message`, describing that the message becomes the subagent's next turn, that this call returns no answer from the subagent, and that a failure means the message was not delivered; `interrupt_agent` takes `agent_id`, describing that only the current turn stops, queued messages park, descendants keep running, and acceptance precedes the actual stop; `list_agents` takes the optional `scope` enum.
18
+
19
+ #### Token effect
20
+
21
+ Fixed schema cost per parent request.
22
+
23
+ #### KV Cache effect
24
+
25
+ Prefix-stable; the schema does not change at runtime.
26
+
27
+ ### Interrupt result
28
+
29
+ #### What the model sees
30
+
31
+ `interrupt requested for agent <agent_id>` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; an absent or settled target still renders the acceptance line.
32
+
33
+ #### Token effect
34
+
35
+ One short acknowledgement per call; the interrupted turn's abort is visible only in the child's own transcript.
36
+
37
+ #### KV Cache effect
38
+
39
+ Append-only; each result follows the reusable request prefix.
40
+
41
+ ### Delivery result
42
+
43
+ #### What the model sees
44
+
45
+ `message queued as the next turn for subagent <subagent_id>` on acceptance; the canonical output carries the accepted `messageId`. A failure — an unauthorized or unknown child, a descriptor-less child that cannot be resumed, or admission rejected — is an errored result whose message states the message was not delivered.
46
+
47
+ #### Token effect
48
+
49
+ One short acknowledgement per call; the child's response never returns through this call. A separately granted `report` may append selected content to parent history.
50
+
51
+ #### KV Cache effect
52
+
53
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
54
+
55
+ ### Listing result
56
+
57
+ #### What the model sees
58
+
59
+ One line per continuable child in stable catalog order: `<id> [<status>] — <label>` (`running` = active driver, `idle` = resident between turns, `ready` = storage only; resumable rather than terminal, not a result waiting to be collected — a direct child in that state can be resumed by `send_message`), plus `<id> [diagnostic: <reason>]` for a candidate that could not be read (`corrupt`, `unsupported`, or `unavailable`). The `descendants` scope inserts ` parent=<id> depth=<n>` before the label dash on every line, in pre-order. One-shot children are intentionally absent; `(no subagents)` means no continuable child or diagnostic survived the projection. Diagnostics never expose descriptor contents.
60
+
61
+ #### Token effect
62
+
63
+ Grows linearly with the listed continuable children — the whole tree under the `descendants` scope; there is no cursor or cap, so long-lived parents with many persisted children pay the full list each call.
64
+
65
+ #### KV Cache effect
66
+
67
+ Append-only; each result follows the reusable request prefix.
68
+
69
+ ## Known Limitations and Deferred Work
70
+
71
+ - **A queued message has no independent result** — acceptance returns only its inbox `messageId`; the child's work lands in the durable child Session and is never collected through this tool. A child granted `report` may send selected content back separately, but that message is not this call's result.
72
+ - **No steering of the current turn** — every message opens a later FIFO turn, so a message sent while the child is working runs only after its current turn finishes and cannot redirect it.
73
+ - **Listing is a snapshot, not a delivery promise** — it may race publication, disposal, or a later message, and another process may activate a child this process reports as `ready`; cross-process accuracy requires a shared lease. `interrupt_agent` performs the authoritative live-lineage check itself, so discovery staleness cannot grant authority.
74
+ - **No pagination or deletion** — the complete stably ordered set is returned, and persisted children remain listed for as long as their sessions remain in persistence; a service-level bound or delete operation is a later product decision.
package/lib/index.js ADDED
@@ -0,0 +1,101 @@
1
+ import { defineTool } from "@hydraharness/harness-tools";
2
+ import { SessionId } from "@hydraharness/harness-session";
3
+ //#region lib/types/index.js
4
+ /**
5
+ * The globally named `send_message` and `interrupt_agent` tools: thin
6
+ * model-facing adapters over `ctx.subagents.followup()` and
7
+ * `ctx.subagents.interrupt()`. They perform no lifecycle routing of their own —
8
+ * residency, cold resume, and interrupt authorization belong to the subagent
9
+ * service — and they live apart from the provider-bound
10
+ * `@hydraharness/harness-tool-subagent` instances so multiple delegation tools share
11
+ * one control API.
12
+ * @module @hydraharness/harness-tool-subagent-control
13
+ */
14
+ const name = "tool-subagent-control";
15
+ const inject = ["tools", "subagents"];
16
+ /**
17
+ * Register the `send_message` and `interrupt_agent` tools.
18
+ * @param ctx - context carrying the tool registry and subagent service.
19
+ */
20
+ function apply(ctx) {
21
+ ctx.tools.register(defineTool({
22
+ name: "send_message",
23
+ description: "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.",
24
+ parameters: {
25
+ subagent_id: {
26
+ type: "string",
27
+ required: true,
28
+ description: "The subagent id returned when the background subagent was started."
29
+ },
30
+ message: {
31
+ type: "string",
32
+ required: true,
33
+ description: "The message to deliver to the subagent."
34
+ }
35
+ },
36
+ output: {
37
+ schema: {
38
+ type: "object",
39
+ additionalProperties: false,
40
+ properties: { messageId: {
41
+ type: "string",
42
+ required: true
43
+ } }
44
+ },
45
+ render: (args, _value) => [{
46
+ type: "text",
47
+ text: `message queued as the next turn for subagent ${args.subagent_id}`
48
+ }]
49
+ },
50
+ async execute(args, exec) {
51
+ const parent = exec.agent;
52
+ if (!parent) throw new Error("send_message requires a calling agent (exec.agent was undefined)");
53
+ const message = [{
54
+ type: "text",
55
+ text: args.message
56
+ }];
57
+ return { messageId: await ctx.subagents.followup(parent, SessionId(args.subagent_id), message, {
58
+ source: {
59
+ kind: "coordinator",
60
+ form: "relay",
61
+ senderSessionId: parent.id
62
+ },
63
+ signal: exec.signal
64
+ }) };
65
+ }
66
+ }));
67
+ ctx.tools.register(defineTool({
68
+ name: "interrupt_agent",
69
+ description: "Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.",
70
+ parameters: { agent_id: {
71
+ type: "string",
72
+ required: true,
73
+ description: "The agent id of the running agent to interrupt."
74
+ } },
75
+ output: {
76
+ schema: {
77
+ type: "object",
78
+ additionalProperties: false,
79
+ properties: { accepted: {
80
+ type: "boolean",
81
+ required: true
82
+ } }
83
+ },
84
+ render: (args, _value) => [{
85
+ type: "text",
86
+ text: `interrupt requested for agent ${args.agent_id}`
87
+ }]
88
+ },
89
+ execute(args, exec) {
90
+ const caller = exec.agent;
91
+ if (!caller) throw new Error("interrupt_agent requires a calling agent (exec.agent was undefined)");
92
+ ctx.subagents.interrupt(SessionId(args.agent_id), {
93
+ kind: "ancestor",
94
+ agent: caller
95
+ });
96
+ return Promise.resolve({ accepted: true });
97
+ }
98
+ }));
99
+ }
100
+ //#endregion
101
+ export { apply, inject, name };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@hydraharness/harness-tool-subagent-control`.
4
+ * @module @hydraharness/harness-tool-subagent-control/invariant
5
+ */
6
+ const PACKAGE_NAME = "@hydraharness/harness-tool-subagent-control";
7
+ /** Cordis companion plugin name. */
8
+ const name = "tool-subagent-control-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this model-facing adapter has no independent lifecycle stream; delivery
13
+ * and activation relations are owned by the subagent service it calls.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The globally named `send_message` and `interrupt_agent` tools: thin
3
+ * model-facing adapters over `ctx.subagents.followup()` and
4
+ * `ctx.subagents.interrupt()`. They perform no lifecycle routing of their own —
5
+ * residency, cold resume, and interrupt authorization belong to the subagent
6
+ * service — and they live apart from the provider-bound
7
+ * `@hydraharness/harness-tool-subagent` instances so multiple delegation tools share
8
+ * one control API.
9
+ * @module @hydraharness/harness-tool-subagent-control
10
+ */
11
+ import type { Context } from '@hydraharness/cordis';
12
+ export declare const name = "tool-subagent-control";
13
+ export declare const inject: string[];
14
+ /**
15
+ * Register the `send_message` and `interrupt_agent` tools.
16
+ * @param ctx - context carrying the tool registry and subagent service.
17
+ */
18
+ export declare function apply(ctx: Context): void;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The globally named `send_message` and `interrupt_agent` tools: thin
3
+ * model-facing adapters over `ctx.subagents.followup()` and
4
+ * `ctx.subagents.interrupt()`. They perform no lifecycle routing of their own —
5
+ * residency, cold resume, and interrupt authorization belong to the subagent
6
+ * service — and they live apart from the provider-bound
7
+ * `@hydraharness/harness-tool-subagent` instances so multiple delegation tools share
8
+ * one control API.
9
+ * @module @hydraharness/harness-tool-subagent-control
10
+ */
11
+ import { defineTool } from '@hydraharness/harness-tools';
12
+ import { SessionId } from '@hydraharness/harness-session';
13
+ export const name = 'tool-subagent-control';
14
+ export const inject = ['tools', 'subagents'];
15
+ /**
16
+ * Register the `send_message` and `interrupt_agent` tools.
17
+ * @param ctx - context carrying the tool registry and subagent service.
18
+ */
19
+ export function apply(ctx) {
20
+ ctx.tools.register(defineTool({
21
+ name: 'send_message',
22
+ description: 'Send a message to a background subagent by its subagent id, continuing the same conversation. It '
23
+ + 'becomes the subagent\'s next turn: if it is still working, the message waits until its current turn '
24
+ + 'finishes, so it cannot redirect work already underway. This call returns no answer from the '
25
+ + 'subagent — only confirmation that the message was delivered — so use it to give it more work. A '
26
+ + 'failure means the message was NOT delivered.',
27
+ parameters: {
28
+ subagent_id: {
29
+ type: 'string',
30
+ required: true,
31
+ description: 'The subagent id returned when the background subagent was started.',
32
+ },
33
+ message: {
34
+ type: 'string',
35
+ required: true,
36
+ description: 'The message to deliver to the subagent.',
37
+ },
38
+ },
39
+ output: {
40
+ schema: {
41
+ type: 'object',
42
+ additionalProperties: false,
43
+ properties: {
44
+ messageId: { type: 'string', required: true },
45
+ },
46
+ },
47
+ render: (args, _value) => [{
48
+ type: 'text',
49
+ text: `message queued as the next turn for subagent ${args.subagent_id}`,
50
+ }],
51
+ },
52
+ async execute(args, exec) {
53
+ const parent = exec.agent;
54
+ if (!parent) {
55
+ // Parent authority requires an exact live calling agent.
56
+ throw new Error('send_message requires a calling agent (exec.agent was undefined)');
57
+ }
58
+ const message = [{ type: 'text', text: args.message }];
59
+ const messageId = await ctx.subagents.followup(parent, SessionId(args.subagent_id), message, {
60
+ source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
61
+ signal: exec.signal,
62
+ });
63
+ return { messageId };
64
+ },
65
+ }));
66
+ ctx.tools.register(defineTool({
67
+ name: 'interrupt_agent',
68
+ description: 'Request cancellation of a background agent\'s current turn by its agent id. The target may be your '
69
+ + 'direct child or a deeper agent created under you. Only the current turn stops: messages already '
70
+ + 'queued for the agent stay parked until a later send_message, agents it started keep running, and '
71
+ + 'the agent itself stays available for follow-ups. This call returns as soon as the stop request is '
72
+ + 'accepted, so the target may keep running briefly; interrupting an agent that already finished is '
73
+ + 'an accepted no-op.',
74
+ parameters: {
75
+ agent_id: {
76
+ type: 'string',
77
+ required: true,
78
+ description: 'The agent id of the running agent to interrupt.',
79
+ },
80
+ },
81
+ output: {
82
+ schema: {
83
+ type: 'object',
84
+ additionalProperties: false,
85
+ properties: {
86
+ accepted: { type: 'boolean', required: true },
87
+ },
88
+ },
89
+ render: (args, _value) => [{
90
+ type: 'text',
91
+ text: `interrupt requested for agent ${args.agent_id}`,
92
+ }],
93
+ },
94
+ execute(args, exec) {
95
+ const caller = exec.agent;
96
+ if (!caller) {
97
+ // Ancestor authority requires an exact live calling agent.
98
+ throw new Error('interrupt_agent requires a calling agent (exec.agent was undefined)');
99
+ }
100
+ // The service authorizes the exact live caller against the target's
101
+ // recorded lineage; the tool adds no authority of its own.
102
+ ctx.subagents.interrupt(SessionId(args.agent_id), { kind: 'ancestor', agent: caller });
103
+ return Promise.resolve({ accepted: true });
104
+ },
105
+ }));
106
+ }
107
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hydraharness/harness-tool-subagent-control`.
3
+ * @module @hydraharness/harness-tool-subagent-control/invariant
4
+ */
5
+ import type { Context } from '@hydraharness/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "tool-subagent-control-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hydraharness/harness-tool-subagent-control`.
3
+ * @module @hydraharness/harness-tool-subagent-control/invariant
4
+ */
5
+ const PACKAGE_NAME = '@hydraharness/harness-tool-subagent-control';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'tool-subagent-control-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: this model-facing adapter has no independent lifecycle stream; delivery
12
+ * and activation relations are owned by the subagent service it calls.
13
+ */
14
+ const install = () => { };
15
+ /**
16
+ * Register this package's invariant companion.
17
+ * @param ctx - Cordis context carrying the invariant service.
18
+ * @returns the installed registration's disposer after setup succeeds.
19
+ */
20
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
21
+ /* jscpd:ignore-end */
22
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The globally named `list_agents` tool: a thin model-facing adapter over
3
+ * the continuable projection of `ctx.subagents.listChildren()` and, for the
4
+ * `descendants` scope, `ctx.subagents.listDescendants()`. It stays separately
5
+ * loadable from the root `send_message` plugin so a deployment can register
6
+ * continuation delivery without exposing discovery.
7
+ * @module @hydraharness/harness-tool-subagent-control/list-agents
8
+ */
9
+ import type { Context } from '@hydraharness/cordis';
10
+ export declare const name = "tool-subagent-list-agents";
11
+ export declare const inject: string[];
12
+ /**
13
+ * Register the `list_agents` tool.
14
+ * @param ctx - context carrying the tool registry, subagent service, and live Agent registry.
15
+ */
16
+ export declare function apply(ctx: Context): void;
17
+ //# sourceMappingURL=list-agents.d.ts.map
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The globally named `list_agents` tool: a thin model-facing adapter over
3
+ * the continuable projection of `ctx.subagents.listChildren()` and, for the
4
+ * `descendants` scope, `ctx.subagents.listDescendants()`. It stays separately
5
+ * loadable from the root `send_message` plugin so a deployment can register
6
+ * continuation delivery without exposing discovery.
7
+ * @module @hydraharness/harness-tool-subagent-control/list-agents
8
+ */
9
+ import { defineTool } from '@hydraharness/harness-tools';
10
+ import { assertNever } from '@hydraharness/harness-llm';
11
+ export const name = 'tool-subagent-list-agents';
12
+ export const inject = ['tools', 'subagents', 'agents'];
13
+ /** Resolve the optional model request into an internal required-scope spec. */
14
+ function resolveListAgentsRequest(request) {
15
+ return { scope: request.scope ?? 'children' };
16
+ }
17
+ /**
18
+ * Refine one candidate's status through the live Agent registry: `running`
19
+ * for an active driver, `idle` for a resident Agent between turns (possibly
20
+ * waiting on agents it started), and `ready` when no live Agent remains.
21
+ * `ready` preserves resumability without presenting an inactive conversation
22
+ * as a terminal result to collect.
23
+ */
24
+ function statusOf(agents, id) {
25
+ const agent = agents.get(id);
26
+ if (agent === undefined)
27
+ return 'ready';
28
+ return agent.status === 'running' ? 'running' : 'idle';
29
+ }
30
+ /** Project one service row into the model-facing entry, or omit a one-shot child. */
31
+ function project(agents, entry, position) {
32
+ const at = position === undefined ? {} : { parent: position.parentId, depth: position.depth };
33
+ if (entry.kind === 'diagnostic') {
34
+ return { kind: 'diagnostic', id: entry.id, reason: entry.reason, ...at };
35
+ }
36
+ // One-shot children cannot be continued by send_message, so the model
37
+ // never selects them; discovery still traversed them for descendants.
38
+ if (entry.mode !== 'continuable')
39
+ return undefined;
40
+ return {
41
+ kind: 'child',
42
+ id: entry.id,
43
+ label: entry.label,
44
+ status: statusOf(agents, entry.id),
45
+ ...at,
46
+ };
47
+ }
48
+ /**
49
+ * Register the `list_agents` tool.
50
+ * @param ctx - context carrying the tool registry, subagent service, and live Agent registry.
51
+ */
52
+ export function apply(ctx) {
53
+ ctx.tools.register(defineTool({
54
+ name: 'list_agents',
55
+ description: 'List your continuable background subagents by durable id and label. Use it to recall which ones '
56
+ + 'you started, not to poll for completion — you are told when one finishes. Status comes from the live '
57
+ + 'registry: running means the agent is working right now, idle means it is loaded but between turns '
58
+ + '(it may be waiting on agents it started), and ready means it exists only in storage — resumable, not '
59
+ + 'terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same '
60
+ + 'conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery '
61
+ + 'promise — `send_message` performs the authoritative check and may still fail. Children that could '
62
+ + 'not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` '
63
+ + 'walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent '
64
+ + 'session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are '
65
+ + 'candidates for `interrupt_agent` only.',
66
+ parameters: {
67
+ scope: {
68
+ type: 'string',
69
+ enum: ['children', 'descendants'],
70
+ description: 'children (default) lists direct children only; descendants walks the complete tree below you.',
71
+ },
72
+ },
73
+ output: {
74
+ schema: {
75
+ type: 'array',
76
+ items: {
77
+ oneOf: [
78
+ {
79
+ type: 'object',
80
+ additionalProperties: false,
81
+ properties: {
82
+ kind: { type: 'string', required: true, enum: ['child'] },
83
+ id: { type: 'string', required: true },
84
+ label: { type: 'string', required: true },
85
+ status: { type: 'string', required: true, enum: ['running', 'idle', 'ready'] },
86
+ parent: { type: 'string' },
87
+ depth: { type: 'number' },
88
+ },
89
+ },
90
+ {
91
+ type: 'object',
92
+ additionalProperties: false,
93
+ properties: {
94
+ kind: { type: 'string', required: true, enum: ['diagnostic'] },
95
+ id: { type: 'string', required: true },
96
+ reason: { type: 'string', required: true, enum: ['corrupt', 'unsupported', 'unavailable'] },
97
+ parent: { type: 'string' },
98
+ depth: { type: 'number' },
99
+ },
100
+ },
101
+ ],
102
+ },
103
+ },
104
+ render: (args, entries) => {
105
+ const request = resolveListAgentsRequest(args);
106
+ return [{
107
+ type: 'text',
108
+ text: entries.length === 0
109
+ ? '(no subagents)'
110
+ : entries.map((entry) => {
111
+ // A descendants row always carries its position; children rows
112
+ // never render it. String() spans the schema-optional shape
113
+ // without a dead fallback branch.
114
+ const at = request.scope === 'descendants'
115
+ ? ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
116
+ : '';
117
+ return entry.kind === 'child'
118
+ ? `${entry.id} [${entry.status}]${at} — ${entry.label}`
119
+ : `${entry.id} [diagnostic: ${entry.reason}]${at}`;
120
+ }).join('\n'),
121
+ }];
122
+ },
123
+ },
124
+ async execute(args, exec) {
125
+ const parent = exec.agent;
126
+ if (!parent) {
127
+ // Non-agent callers have no session whose children could be listed.
128
+ throw new Error('list_agents requires a calling agent (exec.agent was undefined)');
129
+ }
130
+ const request = resolveListAgentsRequest(args);
131
+ // The registry drains started tool bodies, so the scan must observe the
132
+ // call's signal rather than finish a slow catalog after cancellation.
133
+ switch (request.scope) {
134
+ case 'children': {
135
+ const entries = await ctx.subagents.listChildren(parent.id, exec.signal);
136
+ return entries
137
+ .map(entry => project(ctx.agents, entry))
138
+ .filter(entry => entry !== undefined);
139
+ }
140
+ case 'descendants': {
141
+ const entries = await ctx.subagents.listDescendants(parent.id, exec.signal);
142
+ return entries
143
+ .map(entry => project(ctx.agents, entry, entry))
144
+ .filter(entry => entry !== undefined);
145
+ }
146
+ /* v8 ignore next 2 -- the resolver normalizes the schema-validated closed scope before dispatch. */
147
+ default:
148
+ return assertNever(request.scope, 'list_agents scope');
149
+ }
150
+ },
151
+ }));
152
+ }
153
+ //# sourceMappingURL=list-agents.js.map
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@hydraharness/harness-tool-subagent-control",
3
+ "description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations",
4
+ "hydra": {
5
+ "plugin": {
6
+ "application": "Let the agent list child agents, send follow-up messages, or interrupt their work."
7
+ }
8
+ },
9
+ "version": "0.1.1-rc.6",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
16
+ "directory": "packages/subagent/tool-subagent-control"
17
+ },
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "types": "lib/types/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/types/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ },
26
+ "./invariant": {
27
+ "types": "./lib/types/invariant.d.ts",
28
+ "default": "./lib/invariant.js"
29
+ },
30
+ "./list-agents": {
31
+ "types": "./lib/types/list-agents.d.ts",
32
+ "default": "./lib/types/list-agents.js"
33
+ },
34
+ "./src/*": "./src/*",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "lib/index.js",
39
+ "lib/invariant.js",
40
+ "lib/types/**/*.js",
41
+ "lib/types/**/*.d.ts"
42
+ ],
43
+ "license": "MIT",
44
+ "peerDependencies": {
45
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
46
+ "@hydraharness/harness-llm": "^0.1.1-rc.6",
47
+ "@hydraharness/harness-session": "^0.1.1-rc.6",
48
+ "@hydraharness/cordis": "^4.0.2",
49
+ "@hydraharness/harness-tools": "^0.1.1-rc.6",
50
+ "@hydraharness/harness-subagent": "^0.1.1-rc.6"
51
+ },
52
+ "devDependencies": {
53
+ "@hydraharness/harness-agent": "^0.1.1-rc.6",
54
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
55
+ "@hydraharness/harness-agent-loop": "^0.1.1-rc.6",
56
+ "@hydraharness/harness-llm": "^0.1.1-rc.6",
57
+ "@hydraharness/harness-agent-loop-testkit": "^0.1.1-rc.6",
58
+ "@hydraharness/harness-session": "^0.1.1-rc.6",
59
+ "@hydraharness/harness-session-persistence": "^0.1.1-rc.6",
60
+ "@hydraharness/harness-session-persistence-jsonl": "^0.1.1-rc.6",
61
+ "@hydraharness/harness-session-projection": "^0.1.1-rc.6",
62
+ "@hydraharness/harness-subagent-spawn-in-process": "^0.1.1-rc.6",
63
+ "@hydraharness/harness-tools": "^0.1.1-rc.6",
64
+ "@hydraharness/cordis": "^4.0.2",
65
+ "@hydraharness/harness-subagent": "^0.1.1-rc.6"
66
+ }
67
+ }