pi-subagents-router 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/AgentConfig.d.ts +52 -0
- package/dist/AgentConfig.js +7 -0
- package/dist/BuiltinAgents.d.ts +10 -0
- package/dist/BuiltinAgents.js +156 -0
- package/dist/Supvisor.d.ts +33 -0
- package/dist/Supvisor.js +310 -0
- package/dist/agent-router.d.ts +16 -0
- package/dist/agent-router.js +209 -0
- package/package.json +54 -0
- package/src/AgentConfig.ts +59 -0
- package/src/BuiltinAgents.ts +165 -0
- package/src/Supvisor.ts +462 -0
- package/src/agent-router.ts +243 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BuiltinAgents — The 4 hardcoded agents extracted from agent-router.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentConfig } from "./AgentConfig.ts";
|
|
6
|
+
|
|
7
|
+
export const BUILTIN_AGENTS: AgentConfig[] = [
|
|
8
|
+
{
|
|
9
|
+
name: "documenter",
|
|
10
|
+
description:
|
|
11
|
+
"Documentation specialist responsible for creating and maintaining project documentation",
|
|
12
|
+
thinking: "high",
|
|
13
|
+
tools: [
|
|
14
|
+
"read",
|
|
15
|
+
"grep",
|
|
16
|
+
"find",
|
|
17
|
+
"ls",
|
|
18
|
+
"bash",
|
|
19
|
+
"edit",
|
|
20
|
+
"write",
|
|
21
|
+
"contact_supvisor",
|
|
22
|
+
],
|
|
23
|
+
systemPromptMode: "replace",
|
|
24
|
+
inheritProjectContext: true,
|
|
25
|
+
inheritSkills: false,
|
|
26
|
+
triggers: [
|
|
27
|
+
"documentation",
|
|
28
|
+
"docs",
|
|
29
|
+
"write docs",
|
|
30
|
+
"documentation update",
|
|
31
|
+
"README",
|
|
32
|
+
],
|
|
33
|
+
useWhen: [
|
|
34
|
+
"documentation creation",
|
|
35
|
+
"knowledge documentation",
|
|
36
|
+
"technical writing",
|
|
37
|
+
],
|
|
38
|
+
avoidWhen: [
|
|
39
|
+
"implementation",
|
|
40
|
+
"testing",
|
|
41
|
+
"deployment operations",
|
|
42
|
+
],
|
|
43
|
+
capabilities: ["documentation", "knowledge", "writing"],
|
|
44
|
+
produces: ["docs", "readme", "technical-writing"],
|
|
45
|
+
source: "builtin",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "researcher",
|
|
49
|
+
description:
|
|
50
|
+
"Research specialist responsible for information gathering, analysis, and evidence-based insights",
|
|
51
|
+
thinking: "high",
|
|
52
|
+
tools: [
|
|
53
|
+
"read",
|
|
54
|
+
"grep",
|
|
55
|
+
"find",
|
|
56
|
+
"ls",
|
|
57
|
+
"bash",
|
|
58
|
+
"web_search",
|
|
59
|
+
"fetch_content",
|
|
60
|
+
],
|
|
61
|
+
systemPromptMode: "replace",
|
|
62
|
+
inheritProjectContext: true,
|
|
63
|
+
inheritSkills: true,
|
|
64
|
+
triggers: [
|
|
65
|
+
"research",
|
|
66
|
+
"investigate",
|
|
67
|
+
"find information",
|
|
68
|
+
"look up",
|
|
69
|
+
],
|
|
70
|
+
useWhen: [
|
|
71
|
+
"information gathering",
|
|
72
|
+
"market research",
|
|
73
|
+
"technical research",
|
|
74
|
+
"competitive analysis",
|
|
75
|
+
],
|
|
76
|
+
avoidWhen: ["implementation", "debugging", "testing"],
|
|
77
|
+
capabilities: ["research", "analysis", "information-gathering"],
|
|
78
|
+
produces: ["research-report", "findings", "evidence"],
|
|
79
|
+
source: "builtin",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "software-engineer",
|
|
83
|
+
description: "Senior software engineering implementation specialist",
|
|
84
|
+
thinking: "medium",
|
|
85
|
+
tools: [
|
|
86
|
+
"read",
|
|
87
|
+
"grep",
|
|
88
|
+
"find",
|
|
89
|
+
"ls",
|
|
90
|
+
"bash",
|
|
91
|
+
"edit",
|
|
92
|
+
"write",
|
|
93
|
+
"contact_supvisor",
|
|
94
|
+
],
|
|
95
|
+
systemPromptMode: "replace",
|
|
96
|
+
inheritProjectContext: true,
|
|
97
|
+
inheritSkills: true,
|
|
98
|
+
triggers: ["code", "implement", "build", "develop", "program"],
|
|
99
|
+
useWhen: [
|
|
100
|
+
"software development",
|
|
101
|
+
"coding",
|
|
102
|
+
"implementation",
|
|
103
|
+
"feature development",
|
|
104
|
+
],
|
|
105
|
+
avoidWhen: [
|
|
106
|
+
"documentation",
|
|
107
|
+
"research",
|
|
108
|
+
"testing",
|
|
109
|
+
],
|
|
110
|
+
capabilities: ["implementation", "coding", "software-development"],
|
|
111
|
+
produces: ["code", "source-files", "implementation"],
|
|
112
|
+
source: "builtin",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "reviewer",
|
|
116
|
+
description: "Code quality reviewer responsible for validating implementation quality",
|
|
117
|
+
thinking: "medium",
|
|
118
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
119
|
+
systemPromptMode: "replace",
|
|
120
|
+
inheritProjectContext: true,
|
|
121
|
+
inheritSkills: true,
|
|
122
|
+
triggers: ["review", "audit", "check", "validate"],
|
|
123
|
+
useWhen: [
|
|
124
|
+
"code review",
|
|
125
|
+
"quality assurance",
|
|
126
|
+
"architecture review",
|
|
127
|
+
"security review",
|
|
128
|
+
],
|
|
129
|
+
avoidWhen: ["implementation", "documentation", "research"],
|
|
130
|
+
capabilities: ["review", "quality-assurance", "auditing"],
|
|
131
|
+
produces: ["review-report", "quality-feedback", "audit"],
|
|
132
|
+
source: "builtin",
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Simple keyword → capability mapping for built-in routing.
|
|
138
|
+
* Used when capabilities are not explicitly declared on agents.
|
|
139
|
+
*/
|
|
140
|
+
export const KEYWORD_TO_CAPABILITY: Record<string, string[]> = {
|
|
141
|
+
// Documentation
|
|
142
|
+
documentation: ["documentation", "knowledge", "writing"],
|
|
143
|
+
docs: ["documentation", "knowledge", "writing"],
|
|
144
|
+
"write docs": ["documentation", "knowledge", "writing"],
|
|
145
|
+
readme: ["documentation", "knowledge", "writing"],
|
|
146
|
+
|
|
147
|
+
// Research
|
|
148
|
+
research: ["research", "analysis", "information-gathering"],
|
|
149
|
+
investigate: ["research", "analysis", "information-gathering"],
|
|
150
|
+
"find information": ["research", "analysis", "information-gathering"],
|
|
151
|
+
"look up": ["research", "analysis", "information-gathering"],
|
|
152
|
+
|
|
153
|
+
// Engineering
|
|
154
|
+
implement: ["implementation", "coding", "software-development"],
|
|
155
|
+
build: ["implementation", "coding", "software-development"],
|
|
156
|
+
develop: ["implementation", "coding", "software-development"],
|
|
157
|
+
program: ["implementation", "coding", "software-development"],
|
|
158
|
+
code: ["implementation", "coding", "software-development"],
|
|
159
|
+
|
|
160
|
+
// Review
|
|
161
|
+
review: ["review", "quality-assurance", "auditing"],
|
|
162
|
+
audit: ["review", "quality-assurance", "auditing"],
|
|
163
|
+
check: ["review", "quality-assurance", "auditing"],
|
|
164
|
+
validate: ["review", "quality-assurance", "auditing"],
|
|
165
|
+
};
|
package/src/Supvisor.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supvisor — The agent routing supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the agent management logic from agent-router.ts and provides
|
|
5
|
+
* two entry points:
|
|
6
|
+
* - routeAgent() — single-agent routing (same behavior as old)
|
|
7
|
+
* - planAndRoute() — multi-agent workflow (NEW)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AgentConfig,
|
|
12
|
+
RoutingDecision,
|
|
13
|
+
RouteResult,
|
|
14
|
+
} from "./AgentConfig.ts";
|
|
15
|
+
import { BUILTIN_AGENTS, KEYWORD_TO_CAPABILITY } from "./BuiltinAgents.ts";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Helper: Route to a sub-agent via RPC (extracted from agent-router.ts)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
interface SpawnOptions {
|
|
22
|
+
thinking?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
inheritContext?: boolean;
|
|
25
|
+
inheritSkills?: boolean;
|
|
26
|
+
run_in_background?: boolean;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SpawnRequest {
|
|
31
|
+
requestId: string;
|
|
32
|
+
type: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
options: SpawnOptions;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function spawnSubAgent(
|
|
38
|
+
pi: any,
|
|
39
|
+
subagentType: string,
|
|
40
|
+
prompt: string,
|
|
41
|
+
description: string,
|
|
42
|
+
runInBackground: boolean = false,
|
|
43
|
+
options: {
|
|
44
|
+
thinking?: string;
|
|
45
|
+
model?: string;
|
|
46
|
+
inheritContext?: boolean;
|
|
47
|
+
inheritSkills?: boolean;
|
|
48
|
+
} = {}
|
|
49
|
+
): Promise<{ success: boolean; agentId?: string; error?: string }> {
|
|
50
|
+
const PI_SUBAGENTS_REPO =
|
|
51
|
+
"https://github.com/tintinweb/pi-subagents";
|
|
52
|
+
const requestId = `router-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let unsub: (() => void) | null = null;
|
|
56
|
+
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
if (unsub) unsub();
|
|
59
|
+
resolve({
|
|
60
|
+
success: false,
|
|
61
|
+
error: `Timed out waiting for pi-subagents RPC reply. Is ${PI_SUBAGENTS_REPO} installed and loaded?`,
|
|
62
|
+
});
|
|
63
|
+
}, 10_000);
|
|
64
|
+
|
|
65
|
+
let emitError: string | undefined;
|
|
66
|
+
try {
|
|
67
|
+
pi.events.emit("subagents:rpc:spawn", {
|
|
68
|
+
requestId,
|
|
69
|
+
type: subagentType,
|
|
70
|
+
prompt,
|
|
71
|
+
options: {
|
|
72
|
+
description,
|
|
73
|
+
run_in_background: runInBackground,
|
|
74
|
+
thinking: options.thinking,
|
|
75
|
+
model: options.model,
|
|
76
|
+
inherit_context: options.inheritContext,
|
|
77
|
+
inherit_skills: options.inheritSkills,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
} catch (err: any) {
|
|
81
|
+
emitError = err.message || "Unknown emit error";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (emitError) {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
resolve({
|
|
87
|
+
success: false,
|
|
88
|
+
error: `Failed to emit spawn event to pi-subagents: ${emitError}`,
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
unsub = pi.events.on(
|
|
94
|
+
`subagents:rpc:spawn:reply:${requestId}`,
|
|
95
|
+
(reply: any) => {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
unsub!();
|
|
98
|
+
|
|
99
|
+
if (!reply?.success) {
|
|
100
|
+
resolve({
|
|
101
|
+
success: false,
|
|
102
|
+
error: reply?.error || "Unknown pi-subagents RPC error",
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
resolve({ success: true, agentId: reply.data?.id });
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Helper: Analyze task description and determine best agent
|
|
115
|
+
// (Same as the old analyzeTaskDescription, extracted and made static)
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
export function analyzeTaskDescription(
|
|
119
|
+
taskDescription: string,
|
|
120
|
+
agents: AgentConfig[]
|
|
121
|
+
): RoutingDecision {
|
|
122
|
+
const lowerTask = taskDescription.toLowerCase();
|
|
123
|
+
|
|
124
|
+
// FIX: Check ALL triggers across ALL agents, then pick earliest position in input text
|
|
125
|
+
let bestTriggerAgent: {
|
|
126
|
+
name: string;
|
|
127
|
+
trigger: string;
|
|
128
|
+
position: number;
|
|
129
|
+
} | null = null;
|
|
130
|
+
for (const agent of agents) {
|
|
131
|
+
if (agent.triggers) {
|
|
132
|
+
for (const trigger of agent.triggers) {
|
|
133
|
+
const position = lowerTask.indexOf(trigger.toLowerCase());
|
|
134
|
+
if (position >= 0) {
|
|
135
|
+
if (
|
|
136
|
+
!bestTriggerAgent ||
|
|
137
|
+
position < bestTriggerAgent.position
|
|
138
|
+
) {
|
|
139
|
+
bestTriggerAgent = {
|
|
140
|
+
name: agent.name,
|
|
141
|
+
trigger,
|
|
142
|
+
position,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (bestTriggerAgent) {
|
|
151
|
+
return {
|
|
152
|
+
agentType: bestTriggerAgent.name,
|
|
153
|
+
reason: `Trigger match: "${bestTriggerAgent.trigger}"`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check useWhen criteria
|
|
158
|
+
const useWhenScores: {
|
|
159
|
+
agent: string;
|
|
160
|
+
score: number;
|
|
161
|
+
matches: string[];
|
|
162
|
+
}[] = [];
|
|
163
|
+
for (const agent of agents) {
|
|
164
|
+
if (agent.useWhen) {
|
|
165
|
+
const matches = agent.useWhen.filter((criterion) =>
|
|
166
|
+
lowerTask.includes(criterion.toLowerCase())
|
|
167
|
+
);
|
|
168
|
+
if (matches.length > 0) {
|
|
169
|
+
useWhenScores.push({
|
|
170
|
+
agent: agent.name,
|
|
171
|
+
score: matches.length,
|
|
172
|
+
matches: matches,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check avoidWhen penalties
|
|
179
|
+
const avoidWhenPenalties: {
|
|
180
|
+
agent: string;
|
|
181
|
+
penalties: string[];
|
|
182
|
+
}[] = [];
|
|
183
|
+
for (const agent of agents) {
|
|
184
|
+
if (agent.avoidWhen) {
|
|
185
|
+
const penalties = agent.avoidWhen.filter(
|
|
186
|
+
(criterion) => lowerTask.includes(criterion.toLowerCase())
|
|
187
|
+
);
|
|
188
|
+
if (penalties.length > 0) {
|
|
189
|
+
avoidWhenPenalties.push({ agent: agent.name, penalties });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Find best match with second-best fallback
|
|
195
|
+
if (useWhenScores.length > 0) {
|
|
196
|
+
useWhenScores.sort((a, b) => b.score - a.score);
|
|
197
|
+
|
|
198
|
+
const topCandidate = useWhenScores[0];
|
|
199
|
+
const topPenalty = avoidWhenPenalties.find(
|
|
200
|
+
(p) => p.agent === topCandidate.agent
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!topPenalty) {
|
|
204
|
+
return {
|
|
205
|
+
agentType: topCandidate.agent,
|
|
206
|
+
reason: `useWhen match: "${topCandidate.matches.join(
|
|
207
|
+
", "
|
|
208
|
+
)}" (score: ${topCandidate.score})`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Second-best fallback — try next best non-penalized agent
|
|
213
|
+
for (let i = 1; i < useWhenScores.length; i++) {
|
|
214
|
+
const candidate = useWhenScores[i];
|
|
215
|
+
const candidatePenalty = avoidWhenPenalties.find(
|
|
216
|
+
(p) => p.agent === candidate.agent
|
|
217
|
+
);
|
|
218
|
+
if (!candidatePenalty) {
|
|
219
|
+
return {
|
|
220
|
+
agentType: candidate.agent,
|
|
221
|
+
reason: `useWhen match: "${candidate.matches.join(
|
|
222
|
+
", "
|
|
223
|
+
)}" (score: ${candidate.score}), fallback from penalized "${topCandidate.agent}"`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// No non-penalized match — fall back to second-best
|
|
229
|
+
const secondBest = useWhenScores[1];
|
|
230
|
+
if (secondBest) {
|
|
231
|
+
return {
|
|
232
|
+
agentType: secondBest.agent,
|
|
233
|
+
reason: `FALLBACK: "${topCandidate.agent}" penalized (${topPenalty.penalties.join(", ")}), using "${secondBest.agent}"`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
agentType: "general-purpose",
|
|
240
|
+
reason: "No clear match - using general-purpose agent",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Supervisor — The main agent routing supervisor class
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
export class Supervisor {
|
|
249
|
+
private agents: AgentConfig[];
|
|
250
|
+
private pi: any;
|
|
251
|
+
|
|
252
|
+
constructor(agents: AgentConfig[], pi: any) {
|
|
253
|
+
this.agents = agents;
|
|
254
|
+
this.pi = pi;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ========================================================================
|
|
258
|
+
// Single-Agent Routing (UNCHANGED behavior from old code)
|
|
259
|
+
// ========================================================================
|
|
260
|
+
|
|
261
|
+
async routeAgent(
|
|
262
|
+
taskDescription: string,
|
|
263
|
+
forceAgent?: string,
|
|
264
|
+
runInBackground?: boolean,
|
|
265
|
+
thinking?: string,
|
|
266
|
+
model?: string
|
|
267
|
+
): Promise<RouteResult> {
|
|
268
|
+
// If forceAgent is specified, use it directly
|
|
269
|
+
if (forceAgent) {
|
|
270
|
+
const agent = this.agents.find((a) => a.name === forceAgent);
|
|
271
|
+
if (agent) {
|
|
272
|
+
const spawnResult = await spawnSubAgent(
|
|
273
|
+
this.pi,
|
|
274
|
+
agent.name,
|
|
275
|
+
taskDescription,
|
|
276
|
+
taskDescription.substring(0, 50) +
|
|
277
|
+
(taskDescription.length > 50 ? "..." : ""),
|
|
278
|
+
runInBackground || false,
|
|
279
|
+
{ thinking: thinking || agent.thinking, model }
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (spawnResult.success) {
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
agentType: forceAgent,
|
|
286
|
+
agentId: spawnResult.agentId,
|
|
287
|
+
reason: "Forced by user",
|
|
288
|
+
};
|
|
289
|
+
} else {
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
error: `Failed to spawn agent '${forceAgent}': ${spawnResult.error}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: `Agent not found: ${forceAgent}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Analyze task description against agent criteria
|
|
304
|
+
const routingDecision = analyzeTaskDescription(
|
|
305
|
+
taskDescription,
|
|
306
|
+
this.agents
|
|
307
|
+
);
|
|
308
|
+
const selectedAgent = this.agents.find(
|
|
309
|
+
(a) => a.name === routingDecision.agentType
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (!selectedAgent) {
|
|
313
|
+
return {
|
|
314
|
+
success: true,
|
|
315
|
+
agentType: "general-purpose",
|
|
316
|
+
reason: routingDecision.reason,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Spawn the sub-agent via pi-subagents RPC
|
|
321
|
+
const spawnResult = await spawnSubAgent(
|
|
322
|
+
this.pi,
|
|
323
|
+
selectedAgent.name,
|
|
324
|
+
taskDescription,
|
|
325
|
+
taskDescription.substring(0, 50) +
|
|
326
|
+
(taskDescription.length > 50 ? "..." : ""),
|
|
327
|
+
runInBackground || false,
|
|
328
|
+
{ thinking: thinking || selectedAgent.thinking, model }
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (spawnResult.success) {
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
agentType: selectedAgent.name,
|
|
335
|
+
reason: routingDecision.reason,
|
|
336
|
+
agentId: spawnResult.agentId,
|
|
337
|
+
};
|
|
338
|
+
} else {
|
|
339
|
+
return {
|
|
340
|
+
success: false,
|
|
341
|
+
error: `Failed to spawn agent '${selectedAgent.name}': ${spawnResult.error}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ========================================================================
|
|
347
|
+
// Multi-Agent Routing (NEW — planAndRoute)
|
|
348
|
+
// ========================================================================
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Plan and route a workflow for multiple agents.
|
|
352
|
+
* Uses keyword matching to find relevant agents, then executes
|
|
353
|
+
* them in parallel (where dependencies allow).
|
|
354
|
+
*/
|
|
355
|
+
async planAndRoute(
|
|
356
|
+
goal: string,
|
|
357
|
+
options?: { forceAgents?: string[]; background?: boolean }
|
|
358
|
+
): Promise<{
|
|
359
|
+
success: boolean;
|
|
360
|
+
agents?: string[]; // selected agents
|
|
361
|
+
steps?: string[]; // executed steps
|
|
362
|
+
results?: Record<string, any>; // results keyed by agent name
|
|
363
|
+
error?: string;
|
|
364
|
+
}> {
|
|
365
|
+
// Step 1: Find relevant agents via keyword matching
|
|
366
|
+
const relevantAgents = options?.forceAgents
|
|
367
|
+
? this.agents.filter((a) => options!.forceAgents!.includes(a.name))
|
|
368
|
+
: this._findRelevantAgents(goal);
|
|
369
|
+
|
|
370
|
+
if (relevantAgents.length === 0) {
|
|
371
|
+
return { success: false, error: "No agents matched for goal" };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Step 2: Execute agents in parallel (no dependencies in v2.0)
|
|
375
|
+
const results: Record<string, any> = {};
|
|
376
|
+
const steps: string[] = [];
|
|
377
|
+
|
|
378
|
+
const promises = relevantAgents.map(async (agent) => {
|
|
379
|
+
const spawnResult = await spawnSubAgent(
|
|
380
|
+
this.pi,
|
|
381
|
+
agent.name,
|
|
382
|
+
goal,
|
|
383
|
+
goal.substring(0, 50) + (goal.length > 50 ? "..." : ""),
|
|
384
|
+
options?.background || false,
|
|
385
|
+
{ model: undefined }
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (spawnResult.success) {
|
|
389
|
+
results[agent.name] = { success: true, agentId: spawnResult.agentId };
|
|
390
|
+
steps.push(`✅ ${agent.name}: spawned (id: ${spawnResult.agentId})`);
|
|
391
|
+
} else {
|
|
392
|
+
results[agent.name] = { success: false, error: spawnResult.error };
|
|
393
|
+
steps.push(`❌ ${agent.name}: failed (${spawnResult.error})`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await Promise.all(promises);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
success: true,
|
|
401
|
+
agents: relevantAgents.map((a) => a.name),
|
|
402
|
+
results,
|
|
403
|
+
steps,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ========================================================================
|
|
408
|
+
// Internal: Find relevant agents by keyword matching
|
|
409
|
+
// ========================================================================
|
|
410
|
+
|
|
411
|
+
private _findRelevantAgents(goal: string): AgentConfig[] {
|
|
412
|
+
const lowerGoal = goal.toLowerCase();
|
|
413
|
+
const scores: Map<string, number> = new Map();
|
|
414
|
+
|
|
415
|
+
for (const agent of this.agents) {
|
|
416
|
+
let score = 0;
|
|
417
|
+
|
|
418
|
+
// Check triggers (highest priority)
|
|
419
|
+
if (agent.triggers) {
|
|
420
|
+
for (const trigger of agent.triggers) {
|
|
421
|
+
if (lowerGoal.includes(trigger.toLowerCase())) {
|
|
422
|
+
score += 10;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Check useWhen
|
|
428
|
+
if (agent.useWhen) {
|
|
429
|
+
for (const when of agent.useWhen) {
|
|
430
|
+
if (lowerGoal.includes(when.toLowerCase())) {
|
|
431
|
+
score += 5;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Subtract for avoidWhen
|
|
437
|
+
if (agent.avoidWhen) {
|
|
438
|
+
for (const when of agent.avoidWhen) {
|
|
439
|
+
if (lowerGoal.includes(when.toLowerCase())) {
|
|
440
|
+
score -= 3;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (score > 0) scores.set(agent.name, score);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Return agents with score > 0, sorted by score descending
|
|
449
|
+
return Array.from(scores.entries())
|
|
450
|
+
.sort((a, b) => b[1] - a[1])
|
|
451
|
+
.map(([name]) => this.agents.find((a) => a.name === name)!)
|
|
452
|
+
.filter(Boolean);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ========================================================================
|
|
456
|
+
// Helper: Get all agents (builtin + filesystem)
|
|
457
|
+
// ========================================================================
|
|
458
|
+
|
|
459
|
+
static async getAllAgents(): Promise<AgentConfig[]> {
|
|
460
|
+
return BUILTIN_AGENTS;
|
|
461
|
+
}
|
|
462
|
+
}
|