@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 +21 -0
- package/README.md +74 -0
- package/lib/index.js +101 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.js +107 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +22 -0
- package/lib/types/list-agents.d.ts +17 -0
- package/lib/types/list-agents.js +153 -0
- package/package.json +67 -0
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 };
|
package/lib/invariant.js
ADDED
|
@@ -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
|
+
}
|