@moxxy/plugin-subagents 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/dispatch-agent.d.ts +60 -0
- package/dist/dispatch-agent.d.ts.map +1 -0
- package/dist/dispatch-agent.js +193 -0
- package/dist/dispatch-agent.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/skills/dispatch-agents.md +114 -0
- package/src/discovery.test.ts +29 -0
- package/src/dispatch-agent.test.ts +344 -0
- package/src/dispatch-agent.ts +255 -0
- package/src/index.ts +86 -0
- package/src/resolve-spec.test.ts +150 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
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.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z, type AgentDef, type SubagentSpec } from '@moxxy/sdk';
|
|
2
|
+
declare const agentSpecSchema: z.ZodObject<{
|
|
3
|
+
prompt: z.ZodString;
|
|
4
|
+
agentType: z.ZodOptional<z.ZodString>;
|
|
5
|
+
label: z.ZodOptional<z.ZodString>;
|
|
6
|
+
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
7
|
+
model: z.ZodOptional<z.ZodString>;
|
|
8
|
+
mode: z.ZodOptional<z.ZodString>;
|
|
9
|
+
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
prompt: string;
|
|
12
|
+
agentType?: string | undefined;
|
|
13
|
+
label?: string | undefined;
|
|
14
|
+
systemPrompt?: string | undefined;
|
|
15
|
+
model?: string | undefined;
|
|
16
|
+
mode?: string | undefined;
|
|
17
|
+
allowedTools?: string[] | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
prompt: string;
|
|
20
|
+
agentType?: string | undefined;
|
|
21
|
+
label?: string | undefined;
|
|
22
|
+
systemPrompt?: string | undefined;
|
|
23
|
+
model?: string | undefined;
|
|
24
|
+
mode?: string | undefined;
|
|
25
|
+
allowedTools?: string[] | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
export type AgentSpecInput = z.infer<typeof agentSpecSchema>;
|
|
28
|
+
export interface DispatchAgentDeps {
|
|
29
|
+
/** Live lookup against the session's agent registry. Closure-bound at
|
|
30
|
+
* plugin construction so handler reads see fresh state. */
|
|
31
|
+
readonly getAgent: (name: string) => AgentDef | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Live snapshot of the parent's tool names. When wired, a child that
|
|
34
|
+
* neither the caller nor its kind restricts is defaulted to the parent's
|
|
35
|
+
* tools MINUS `dispatch_agent`, so a model can't drive an unbounded
|
|
36
|
+
* recursive fan-out (8^N concurrent sessions / fork-bomb). Omit (the
|
|
37
|
+
* default) to preserve full unrestricted inheritance — useful for
|
|
38
|
+
* standalone tests/scripts that don't wire a session.
|
|
39
|
+
*/
|
|
40
|
+
readonly getToolNames?: () => ReadonlyArray<string>;
|
|
41
|
+
}
|
|
42
|
+
/** Built-in "default" kind — surfaced when the model omits agentType or
|
|
43
|
+
* passes an unknown one. Never registered in the AgentRegistry so
|
|
44
|
+
* plugins can override it cleanly via `replace()` if they want. */
|
|
45
|
+
export declare const DEFAULT_AGENT: AgentDef;
|
|
46
|
+
export declare function buildDispatchAgentTool(deps: DispatchAgentDeps): import("@moxxy/sdk").ToolDef;
|
|
47
|
+
/** Position of this spec within a multi-agent batch — used to disambiguate
|
|
48
|
+
* fallback labels and (informationally) reason about fan-out width. */
|
|
49
|
+
export interface ResolveSpecPosition {
|
|
50
|
+
readonly index: number;
|
|
51
|
+
readonly total: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Merge a model-supplied spec with the registered agent kind. Caller
|
|
55
|
+
* fields win over kind defaults; omitted caller fields fall back to
|
|
56
|
+
* the kind, which falls back to the built-in DEFAULT.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveSpec(input: AgentSpecInput, deps: DispatchAgentDeps, position?: ResolveSpecPosition): SubagentSpec;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=dispatch-agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-agent.d.ts","sourceRoot":"","sources":["../src/dispatch-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,CAAC,EACD,KAAK,QAAQ,EAEb,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAqBpB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;EAgDnB,CAAC;AASH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC;gEAC4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,QAAQ,GAAG,SAAS,CAAC;IAC1D;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;CACrD;AAED;;oEAEoE;AACpE,eAAO,MAAM,aAAa,EAAE,QAI3B,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,gCA6E7D;AAED;wEACwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,iBAAiB,EACvB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,YAAY,CA2Bd"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { defineTool, MoxxyError, z, } from '@moxxy/sdk';
|
|
2
|
+
/** Upper bound on a single child prompt. Children seed their session with this
|
|
3
|
+
* verbatim and send it to the provider; an unbounded string at this trust
|
|
4
|
+
* boundary lets a runaway/injected model fan out N multi-megabyte completions. */
|
|
5
|
+
const MAX_PROMPT_CHARS = 20_000;
|
|
6
|
+
/** Upper bound on a per-child system-prompt override. */
|
|
7
|
+
const MAX_SYSTEM_PROMPT_CHARS = 8_000;
|
|
8
|
+
/**
|
|
9
|
+
* Aggregate text budget across the WHOLE batch (sum of every spec's prompt +
|
|
10
|
+
* systemPrompt). The per-field caps bound one child, but 8 specs each at the
|
|
11
|
+
* field ceiling still total ~224K chars, dispatched as 8 *concurrent*
|
|
12
|
+
* completions — a sudden cost/context/memory spike reachable from untrusted
|
|
13
|
+
* model output. This caps the simultaneous fan-out payload below that
|
|
14
|
+
* worst case while staying generously above any real multi-agent call.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_BATCH_TEXT_CHARS = 60_000;
|
|
17
|
+
/** This tool's own name — never re-granted to children by default so a model
|
|
18
|
+
* can't drive an unbounded recursive fan-out (8^N sessions). */
|
|
19
|
+
const DISPATCH_AGENT_TOOL_NAME = 'dispatch_agent';
|
|
20
|
+
const agentSpecSchema = z.object({
|
|
21
|
+
prompt: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(MAX_PROMPT_CHARS)
|
|
25
|
+
.describe('The task the sub-agent should perform. Phrase as a focused, self-contained request.'),
|
|
26
|
+
agentType: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Named agent kind to spawn (e.g. "researcher", "code-reviewer"). Looked ' +
|
|
30
|
+
'up in the agent registry contributed by installed plugins. Omit, or ' +
|
|
31
|
+
'pass "default", for a generic tool-use agent. Unknown types fall back ' +
|
|
32
|
+
'to default — the request never fails over a missing kind. List of ' +
|
|
33
|
+
'currently-registered kinds is visible via the /agents command.'),
|
|
34
|
+
label: z
|
|
35
|
+
.string()
|
|
36
|
+
.max(60)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Short label shown in progress events (e.g. "research-deps", "lint-fix-A").'),
|
|
39
|
+
systemPrompt: z
|
|
40
|
+
.string()
|
|
41
|
+
.max(MAX_SYSTEM_PROMPT_CHARS)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Override the kind\'s system prompt. Use to set persona, constraints, ' +
|
|
44
|
+
'or hand off upstream artifacts the child needs as context.'),
|
|
45
|
+
model: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Model id override; defaults to the kind\'s model, then the parent\'s. ' +
|
|
49
|
+
'Omit unless the user explicitly requested a specific model — do NOT ' +
|
|
50
|
+
'invent model ids. Unknown ids fall back to the parent\'s model with a warning.'),
|
|
51
|
+
mode: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Loop strategy override. Valid values: "default", "goal", ' +
|
|
55
|
+
'"research". OMIT for the kind\'s default — do NOT invent names.'),
|
|
56
|
+
allowedTools: z
|
|
57
|
+
.array(z.string())
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Restrict the child to these tool names. Overrides the kind\'s allowlist if set.'),
|
|
60
|
+
});
|
|
61
|
+
/** Built-in "default" kind — surfaced when the model omits agentType or
|
|
62
|
+
* passes an unknown one. Never registered in the AgentRegistry so
|
|
63
|
+
* plugins can override it cleanly via `replace()` if they want. */
|
|
64
|
+
export const DEFAULT_AGENT = {
|
|
65
|
+
name: 'default',
|
|
66
|
+
description: 'Generic tool-use loop. Inherits the parent\'s full tool registry; no system prompt override.',
|
|
67
|
+
};
|
|
68
|
+
export function buildDispatchAgentTool(deps) {
|
|
69
|
+
return defineTool({
|
|
70
|
+
name: 'dispatch_agent',
|
|
71
|
+
description: 'Spawn one or more focused sub-agents in parallel. Use when a task fans out ' +
|
|
72
|
+
'into independent subtasks (multi-source research, per-file refactor, ' +
|
|
73
|
+
'multi-perspective review). Each child runs in isolation and returns its ' +
|
|
74
|
+
'final message; children stream their progress so you see what each one is ' +
|
|
75
|
+
'doing in real time. Pass `agentType` to pick a specialized kind from the ' +
|
|
76
|
+
'agent registry (see /agents); omit for the default generic agent. Unknown ' +
|
|
77
|
+
'kinds fall back to the default instead of erroring.',
|
|
78
|
+
inputSchema: z.object({
|
|
79
|
+
agents: z
|
|
80
|
+
.array(agentSpecSchema)
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(8)
|
|
83
|
+
.superRefine((specs, ctx) => {
|
|
84
|
+
// Bound the *aggregate* concurrent fan-out payload, not just each
|
|
85
|
+
// child. Reject the whole batch before any session is spawned.
|
|
86
|
+
let total = 0;
|
|
87
|
+
for (const s of specs)
|
|
88
|
+
total += s.prompt.length + (s.systemPrompt?.length ?? 0);
|
|
89
|
+
if (total > MAX_BATCH_TEXT_CHARS) {
|
|
90
|
+
ctx.addIssue({
|
|
91
|
+
code: z.ZodIssueCode.custom,
|
|
92
|
+
message: `dispatch_agent batch payload too large: ${total} chars across ${specs.length} ` +
|
|
93
|
+
`agent(s) exceeds the ${MAX_BATCH_TEXT_CHARS}-char aggregate limit. Spawn fewer ` +
|
|
94
|
+
`agents per call or shorten the prompts/systemPrompts.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.describe('Specs for the agents to spawn. Run in parallel; results returned in order.'),
|
|
99
|
+
}),
|
|
100
|
+
handler: async (input, ctx) => {
|
|
101
|
+
if (!ctx.subagents) {
|
|
102
|
+
throw new MoxxyError({
|
|
103
|
+
code: 'INTERNAL',
|
|
104
|
+
message: 'dispatch_agent: no subagent spawner available — this tool must be invoked from a run-turn loop.',
|
|
105
|
+
hint: 'Invoke dispatch_agent from within a run-turn loop so the subagent spawner is wired into the tool context.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const agents = input.agents;
|
|
109
|
+
const specs = agents.map((s, i) => resolveSpec(s, deps, { index: i, total: agents.length }));
|
|
110
|
+
// The core spawner runs children with Promise.all, which rejects on the
|
|
111
|
+
// FIRST child's setup throw (model/log/provider lookup before the
|
|
112
|
+
// per-child try) and discards every sibling's result. Degrade to
|
|
113
|
+
// per-child errors here so one child's failure doesn't abort the batch
|
|
114
|
+
// and orphan the rest — the model still gets the specs that succeeded.
|
|
115
|
+
let results;
|
|
116
|
+
try {
|
|
117
|
+
results = await ctx.subagents.spawnAll(specs);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
return {
|
|
122
|
+
results: specs.map((s) => ({
|
|
123
|
+
label: s.label ?? s.agentType ?? 'default',
|
|
124
|
+
childSessionId: '',
|
|
125
|
+
text: '',
|
|
126
|
+
stopReason: 'error',
|
|
127
|
+
error: message,
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
results: results.map((r) => ({
|
|
133
|
+
label: r.label,
|
|
134
|
+
childSessionId: String(r.childSessionId),
|
|
135
|
+
text: r.text,
|
|
136
|
+
stopReason: r.stopReason,
|
|
137
|
+
...(r.error ? { error: r.error.message } : {}),
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Merge a model-supplied spec with the registered agent kind. Caller
|
|
145
|
+
* fields win over kind defaults; omitted caller fields fall back to
|
|
146
|
+
* the kind, which falls back to the built-in DEFAULT.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveSpec(input, deps, position) {
|
|
149
|
+
const requested = input.agentType ?? 'default';
|
|
150
|
+
const def = deps.getAgent(requested) ?? DEFAULT_AGENT;
|
|
151
|
+
const systemPrompt = input.systemPrompt ?? def.systemPrompt;
|
|
152
|
+
const model = input.model ?? def.model;
|
|
153
|
+
const mode = input.mode ?? def.mode;
|
|
154
|
+
// maxIterations only comes from the AgentDef now (the input schema
|
|
155
|
+
// doesn't expose it — see comment in agentSpecSchema above).
|
|
156
|
+
const allowedTools = resolveAllowedTools(input, def, deps);
|
|
157
|
+
// When the caller omits `label`, multiple agents of the same kind would all
|
|
158
|
+
// collapse to the kind's name (e.g. five "default" labels), making the live
|
|
159
|
+
// progress display and the returned results indistinguishable. Suffix a
|
|
160
|
+
// 1-based index in that multi-agent case; keep explicit/single labels clean.
|
|
161
|
+
const fallbackLabel = position && position.total > 1 ? `${def.name}-${position.index + 1}` : def.name;
|
|
162
|
+
const merged = {
|
|
163
|
+
prompt: input.prompt,
|
|
164
|
+
label: input.label ?? fallbackLabel,
|
|
165
|
+
// The requested kind, surfaced in subagent_* payloads for group rendering.
|
|
166
|
+
agentType: requested,
|
|
167
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
168
|
+
...(model !== undefined && { model }),
|
|
169
|
+
...(mode !== undefined && { mode }),
|
|
170
|
+
...(def.maxIterations !== undefined && { maxIterations: def.maxIterations }),
|
|
171
|
+
...(allowedTools !== undefined && { allowedTools }),
|
|
172
|
+
};
|
|
173
|
+
return merged;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Decide the child's tool allowlist. Caller `allowedTools` wins, then the
|
|
177
|
+
* kind's, then — when a parent tool snapshot is wired (`deps.getToolNames`)
|
|
178
|
+
* and neither restricts — the parent's tools MINUS `dispatch_agent`. That
|
|
179
|
+
* last default is the recursion cut: without it, an unrestricted child
|
|
180
|
+
* inherits `dispatch_agent` and each level can spawn another full batch,
|
|
181
|
+
* giving 8^depth concurrent sessions reachable from untrusted model output.
|
|
182
|
+
* A kind that genuinely needs to recurse can re-grant `dispatch_agent`
|
|
183
|
+
* explicitly via its `allowedTools`.
|
|
184
|
+
*/
|
|
185
|
+
function resolveAllowedTools(input, def, deps) {
|
|
186
|
+
const explicit = input.allowedTools ?? def.allowedTools;
|
|
187
|
+
if (explicit !== undefined)
|
|
188
|
+
return explicit;
|
|
189
|
+
if (!deps.getToolNames)
|
|
190
|
+
return undefined; // not wired → preserve full inheritance
|
|
191
|
+
return deps.getToolNames().filter((n) => n !== DISPATCH_AGENT_TOOL_NAME);
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=dispatch-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-agent.js","sourceRoot":"","sources":["../src/dispatch-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type AgentDef, type LifecycleHooks, type Plugin } from '@moxxy/sdk';
|
|
2
|
+
export { buildDispatchAgentTool, type DispatchAgentDeps } from './dispatch-agent.js';
|
|
3
|
+
export interface BuildSubagentsPluginOpts {
|
|
4
|
+
/**
|
|
5
|
+
* How the tool resolves an `agentType` name → AgentDef at handler
|
|
6
|
+
* time. Pass a closure that reads from your session's agent registry:
|
|
7
|
+
* `(name) => session.agents.get(name)`.
|
|
8
|
+
*
|
|
9
|
+
* Defaults to "no agents registered" (always falls back to the
|
|
10
|
+
* built-in default kind). Useful for standalone tests / scripts that
|
|
11
|
+
* don't want to wire a session.
|
|
12
|
+
*/
|
|
13
|
+
readonly getAgent?: (name: string) => AgentDef | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Live snapshot of the parent's tool names, e.g.
|
|
16
|
+
* `() => session.tools.list().map((t) => t.name)`. When provided, a child
|
|
17
|
+
* that neither the caller nor its kind restricts is defaulted to the
|
|
18
|
+
* parent's tools MINUS `dispatch_agent` — cutting unbounded recursive
|
|
19
|
+
* fan-out (8^N sessions). Omit to keep full unrestricted inheritance.
|
|
20
|
+
*/
|
|
21
|
+
readonly getToolNames?: () => ReadonlyArray<string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `@moxxy/plugin-subagents` — adds the dispatch_agent tool + the
|
|
25
|
+
* auto-detection skill ("dispatch-agents") that triggers on fan-out
|
|
26
|
+
* patterns. Without this plugin the model can't spawn subagents — the
|
|
27
|
+
* normal single-loop flow runs as usual.
|
|
28
|
+
*
|
|
29
|
+
* Other plugins can ship `AgentDef` kinds via their own
|
|
30
|
+
* `PluginSpec.agents`. This plugin's tool resolves them at runtime, so
|
|
31
|
+
* a freshly-installed agent kind becomes available the next time the
|
|
32
|
+
* model calls dispatch_agent — no restart needed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildSubagentsPlugin(opts?: BuildSubagentsPluginOpts, hooks?: LifecycleHooks): Plugin;
|
|
35
|
+
/**
|
|
36
|
+
* Discovery-loadable default export: resolves the `agents` + `tools` registries
|
|
37
|
+
* from the inter-plugin service registry in `onInit` (the host publishes them),
|
|
38
|
+
* so `dispatch_agent` looks up agent kinds + the live parent-tool snapshot from
|
|
39
|
+
* the session without a host-injected closure. Both reads are lazy (at tool-call
|
|
40
|
+
* time, after all registration), and degrade to the standalone defaults if the
|
|
41
|
+
* host hasn't published them.
|
|
42
|
+
*/
|
|
43
|
+
export declare const subagentsPlugin: Plugin;
|
|
44
|
+
export default subagentsPlugin;
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,cAAc,EAEnB,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,sBAAsB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAErF,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,QAAQ,GAAG,SAAS,CAAC;IAC3D;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;CACrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,GAAE,wBAA6B,EACnC,KAAK,CAAC,EAAE,cAAc,GACrB,MAAM,CAWR;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,MAgB1B,CAAC;AAEL,eAAe,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { definePlugin, } from '@moxxy/sdk';
|
|
2
|
+
import { buildDispatchAgentTool } from './dispatch-agent.js';
|
|
3
|
+
export { buildDispatchAgentTool } from './dispatch-agent.js';
|
|
4
|
+
/**
|
|
5
|
+
* `@moxxy/plugin-subagents` — adds the dispatch_agent tool + the
|
|
6
|
+
* auto-detection skill ("dispatch-agents") that triggers on fan-out
|
|
7
|
+
* patterns. Without this plugin the model can't spawn subagents — the
|
|
8
|
+
* normal single-loop flow runs as usual.
|
|
9
|
+
*
|
|
10
|
+
* Other plugins can ship `AgentDef` kinds via their own
|
|
11
|
+
* `PluginSpec.agents`. This plugin's tool resolves them at runtime, so
|
|
12
|
+
* a freshly-installed agent kind becomes available the next time the
|
|
13
|
+
* model calls dispatch_agent — no restart needed.
|
|
14
|
+
*/
|
|
15
|
+
export function buildSubagentsPlugin(opts = {}, hooks) {
|
|
16
|
+
const deps = {
|
|
17
|
+
getAgent: opts.getAgent ?? (() => undefined),
|
|
18
|
+
...(opts.getToolNames ? { getToolNames: opts.getToolNames } : {}),
|
|
19
|
+
};
|
|
20
|
+
return definePlugin({
|
|
21
|
+
name: '@moxxy/plugin-subagents',
|
|
22
|
+
version: '0.0.0',
|
|
23
|
+
...(hooks ? { hooks } : {}),
|
|
24
|
+
tools: [buildDispatchAgentTool(deps)],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Discovery-loadable default export: resolves the `agents` + `tools` registries
|
|
29
|
+
* from the inter-plugin service registry in `onInit` (the host publishes them),
|
|
30
|
+
* so `dispatch_agent` looks up agent kinds + the live parent-tool snapshot from
|
|
31
|
+
* the session without a host-injected closure. Both reads are lazy (at tool-call
|
|
32
|
+
* time, after all registration), and degrade to the standalone defaults if the
|
|
33
|
+
* host hasn't published them.
|
|
34
|
+
*/
|
|
35
|
+
export const subagentsPlugin = (() => {
|
|
36
|
+
let agents = null;
|
|
37
|
+
let tools = null;
|
|
38
|
+
const hooks = {
|
|
39
|
+
onInit: (ctx) => {
|
|
40
|
+
agents = ctx.services.get('agents') ?? null;
|
|
41
|
+
tools = ctx.services.get('tools') ?? null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
return buildSubagentsPlugin({
|
|
45
|
+
getAgent: (name) => agents?.get(name),
|
|
46
|
+
getToolNames: () => (tools ? tools.list().map((t) => t.name) : []),
|
|
47
|
+
}, hooks);
|
|
48
|
+
})();
|
|
49
|
+
export default subagentsPlugin;
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,GAKb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,sBAAsB,EAA0B,MAAM,qBAAqB,CAAC;AAErF,OAAO,EAAE,sBAAsB,EAA0B,MAAM,qBAAqB,CAAC;AAuBrF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAiC,EAAE,EACnC,KAAsB;IAEtB,MAAM,IAAI,GAAsB;QAC9B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;QAC5C,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;IACF,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,yBAAyB;QAC/B,OAAO,EAAE,OAAO;QAChB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,KAAK,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;KACtC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAW,CAAC,GAAG,EAAE;IAC3C,IAAI,MAAM,GAAmC,IAAI,CAAC;IAClD,IAAI,KAAK,GAAoD,IAAI,CAAC;IAClE,MAAM,KAAK,GAAmB;QAC5B,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;YACd,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAA0B,QAAQ,CAAC,IAAI,IAAI,CAAC;YACrE,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAA2C,OAAO,CAAC,IAAI,IAAI,CAAC;QACtF,CAAC;KACF,CAAC;IACF,OAAO,oBAAoB,CACzB;QACE,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC;QACrC,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACnE,EACD,KAAK,CACN,CAAC;AACJ,CAAC,CAAC,EAAE,CAAC;AAEL,eAAe,eAAe,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moxxy/plugin-subagents",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Spawn typed subagents in parallel. Plugins contribute agent kinds via the AgentDef registry; this plugin exposes the dispatch_agent tool and an auto-detection skill.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"moxxy",
|
|
7
|
+
"agent",
|
|
8
|
+
"subagents",
|
|
9
|
+
"tools",
|
|
10
|
+
"fan-out"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://moxxy.ai",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/moxxy-ai/moxxy/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/moxxy-ai/moxxy.git",
|
|
19
|
+
"directory": "packages/plugin-subagents"
|
|
20
|
+
},
|
|
21
|
+
"author": "Michal Makowski <michal.makowski97@gmail.com>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./skills/*": "./skills/*"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"skills"
|
|
40
|
+
],
|
|
41
|
+
"moxxy": {
|
|
42
|
+
"plugin": {
|
|
43
|
+
"entry": "./dist/index.js",
|
|
44
|
+
"kind": [
|
|
45
|
+
"tools",
|
|
46
|
+
"agent"
|
|
47
|
+
],
|
|
48
|
+
"skills": "./skills"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"zod": "^3.24.0",
|
|
53
|
+
"@moxxy/sdk": "0.26.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.10.0",
|
|
57
|
+
"typescript": "^5.7.3",
|
|
58
|
+
"vitest": "^2.1.8",
|
|
59
|
+
"@moxxy/core": "0.26.0",
|
|
60
|
+
"@moxxy/mode-default": "0.26.0",
|
|
61
|
+
"@moxxy/testing": "0.0.44",
|
|
62
|
+
"@moxxy/tsconfig": "0.0.0",
|
|
63
|
+
"@moxxy/vitest-preset": "0.0.0"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsc -p tsconfig.json",
|
|
67
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:watch": "vitest",
|
|
70
|
+
"clean": "rm -rf dist .turbo"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dispatch-agents
|
|
3
|
+
description: Fan a task out to parallel subagents when the work breaks into N independent pieces.
|
|
4
|
+
triggers:
|
|
5
|
+
- "find 3"
|
|
6
|
+
- "find 5"
|
|
7
|
+
- "find N"
|
|
8
|
+
- "research 3"
|
|
9
|
+
- "research 5"
|
|
10
|
+
- "find multiple"
|
|
11
|
+
- "compare across"
|
|
12
|
+
- "compare these"
|
|
13
|
+
- "for each of"
|
|
14
|
+
- "in parallel"
|
|
15
|
+
- "fan out"
|
|
16
|
+
- "spawn agents"
|
|
17
|
+
- "spawn subagents"
|
|
18
|
+
- "audit each"
|
|
19
|
+
- "summarize N"
|
|
20
|
+
- "list 3"
|
|
21
|
+
- "list 5"
|
|
22
|
+
- "multi-source"
|
|
23
|
+
- "multiple sources"
|
|
24
|
+
allowed-tools: [dispatch_agent, memory_save, memory_recall, memory_update]
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Dispatch subagents for parallel work
|
|
28
|
+
|
|
29
|
+
When the user's request naturally decomposes into N **independent** subtasks
|
|
30
|
+
that each need their own tool calls / context, spawn one subagent per subtask
|
|
31
|
+
with `dispatch_agent`. Each child runs in isolation, returns a focused result,
|
|
32
|
+
and the parent (you) composes the final reply.
|
|
33
|
+
|
|
34
|
+
## When to dispatch
|
|
35
|
+
|
|
36
|
+
Yes — fan out:
|
|
37
|
+
- "Find 5 recent articles about X" → 5 agents, each researching one angle.
|
|
38
|
+
- "Compare React, Vue, Svelte, Solid, Qwik" → 5 agents, each profiling one library.
|
|
39
|
+
- "For each of these files, suggest a refactor" → N agents, one per file.
|
|
40
|
+
- "Audit auth, db, and API for security issues" → 3 agents, one per area.
|
|
41
|
+
|
|
42
|
+
No — keep sequential:
|
|
43
|
+
- The subtasks depend on each other (output of A feeds B).
|
|
44
|
+
- The task is small enough that one tool round-trip would handle it.
|
|
45
|
+
- The user explicitly asked for one consolidated investigation.
|
|
46
|
+
|
|
47
|
+
## How to call
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
dispatch_agent({
|
|
51
|
+
agents: [
|
|
52
|
+
{ prompt: "Find a recent article about X from a major US outlet", agentType: "researcher", label: "us-press" },
|
|
53
|
+
{ prompt: "Find a recent article about X from a European outlet", agentType: "researcher", label: "eu-press" },
|
|
54
|
+
{ prompt: "Find a recent academic paper on X", agentType: "researcher", label: "academic" }
|
|
55
|
+
]
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Rules:
|
|
60
|
+
- **One spec per independent subtask.** Don't bundle two requests into one prompt.
|
|
61
|
+
- **Use clear `label`s** — they appear in the user's progress display.
|
|
62
|
+
- **Pick `agentType` from the registered kinds** if any fit (see `/agents`).
|
|
63
|
+
Unknown kinds silently fall back to the default; that's safe but you
|
|
64
|
+
miss out on the specialized system prompt.
|
|
65
|
+
- **Omit `agentType` entirely** when no kind matches — the default
|
|
66
|
+
generic agent works for most tasks.
|
|
67
|
+
- **Cap at 8 agents per call.** If the task wants more, batch it.
|
|
68
|
+
|
|
69
|
+
## After the agents return
|
|
70
|
+
|
|
71
|
+
You receive one result per spec, in input order. Each has `text` (the
|
|
72
|
+
agent's final message), `stopReason`, and optional `error`. Compose a
|
|
73
|
+
single user-facing reply that:
|
|
74
|
+
|
|
75
|
+
1. Synthesizes the findings (don't dump raw agent outputs verbatim).
|
|
76
|
+
2. Cites which agent contributed what when sources differ.
|
|
77
|
+
3. Flags any agent that errored or returned an empty result.
|
|
78
|
+
|
|
79
|
+
If most agents failed, fall back to doing the work yourself in the
|
|
80
|
+
current loop instead of retrying the spawn — the user shouldn't pay
|
|
81
|
+
twice for the same fan-out.
|
|
82
|
+
|
|
83
|
+
## Persist findings worth carrying forward
|
|
84
|
+
|
|
85
|
+
Subagent transcripts die with the session unless you journal them. After
|
|
86
|
+
synthesizing the reply, save durable findings to long-term memory —
|
|
87
|
+
`memory_save` for new entries, or `memory_update` (after `memory_recall`
|
|
88
|
+
to avoid fragmenting) when extending an existing one. See the
|
|
89
|
+
`remember-this` skill for the full workflow.
|
|
90
|
+
|
|
91
|
+
Save when a subagent surfaced:
|
|
92
|
+
- A **code location** that was hard to find ("auth middleware lives in
|
|
93
|
+
`packages/auth/src/mw.ts`") → type `project`.
|
|
94
|
+
- An **external pointer** worth keeping ("staging metrics at
|
|
95
|
+
`grafana.internal/d/auth`") → type `reference`.
|
|
96
|
+
- A **convention or constraint** the fan-out revealed about the repo
|
|
97
|
+
("all webhook handlers must call `verifySig` before parsing body") →
|
|
98
|
+
type `project`.
|
|
99
|
+
|
|
100
|
+
Skip when:
|
|
101
|
+
- The finding is only useful for the current reply (one-shot debug output).
|
|
102
|
+
- It's already obvious from `git log`, README, or the code itself.
|
|
103
|
+
- It's a secret — that belongs in the vault, not memory.
|
|
104
|
+
|
|
105
|
+
One entry per durable finding, not one per subagent. If three subagents
|
|
106
|
+
all rediscovered the same thing, save it once.
|
|
107
|
+
|
|
108
|
+
## Don't
|
|
109
|
+
|
|
110
|
+
- Don't spawn a subagent just to make a single tool call. Call the tool
|
|
111
|
+
directly in the current loop — spawning has setup overhead.
|
|
112
|
+
- Don't fan out work that the user expected as a single deep
|
|
113
|
+
investigation ("dig into X" ≠ "find 5 articles about X").
|
|
114
|
+
- Don't invent agent kinds. Either use a registered one or omit the field.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { ServiceRegistry } from '@moxxy/sdk';
|
|
3
|
+
import { subagentsPlugin } from './index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The discovery-loadable default export resolves the `agents` + `tools`
|
|
7
|
+
* registries from the inter-plugin service registry in onInit instead of a
|
|
8
|
+
* `build*({ getAgent, getToolNames })` closure.
|
|
9
|
+
*/
|
|
10
|
+
describe('subagentsPlugin (discovery-loadable)', () => {
|
|
11
|
+
it('exposes dispatch_agent + an onInit hook', () => {
|
|
12
|
+
expect(subagentsPlugin.tools?.map((t) => t.name)).toEqual(['dispatch_agent']);
|
|
13
|
+
expect(typeof subagentsPlugin.hooks?.onInit).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('onInit resolves the agents + tools registries from the service registry', () => {
|
|
17
|
+
const reg = { get: () => undefined, list: () => [], has: () => false };
|
|
18
|
+
const get = vi.fn(() => reg);
|
|
19
|
+
const services = {
|
|
20
|
+
get,
|
|
21
|
+
require: () => reg,
|
|
22
|
+
has: () => true,
|
|
23
|
+
register: () => {},
|
|
24
|
+
} as unknown as ServiceRegistry;
|
|
25
|
+
subagentsPlugin.hooks!.onInit!({ services } as never);
|
|
26
|
+
expect(get).toHaveBeenCalledWith('agents');
|
|
27
|
+
expect(get).toHaveBeenCalledWith('tools');
|
|
28
|
+
});
|
|
29
|
+
});
|