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
package/dist/Supvisor.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
import { BUILTIN_AGENTS } from "./BuiltinAgents.js";
|
|
10
|
+
async function spawnSubAgent(pi, subagentType, prompt, description, runInBackground = false, options = {}) {
|
|
11
|
+
const PI_SUBAGENTS_REPO = "https://github.com/tintinweb/pi-subagents";
|
|
12
|
+
const requestId = `router-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
let unsub = null;
|
|
15
|
+
const timeout = setTimeout(() => {
|
|
16
|
+
if (unsub)
|
|
17
|
+
unsub();
|
|
18
|
+
resolve({
|
|
19
|
+
success: false,
|
|
20
|
+
error: `Timed out waiting for pi-subagents RPC reply. Is ${PI_SUBAGENTS_REPO} installed and loaded?`,
|
|
21
|
+
});
|
|
22
|
+
}, 10_000);
|
|
23
|
+
let emitError;
|
|
24
|
+
try {
|
|
25
|
+
pi.events.emit("subagents:rpc:spawn", {
|
|
26
|
+
requestId,
|
|
27
|
+
type: subagentType,
|
|
28
|
+
prompt,
|
|
29
|
+
options: {
|
|
30
|
+
description,
|
|
31
|
+
run_in_background: runInBackground,
|
|
32
|
+
thinking: options.thinking,
|
|
33
|
+
model: options.model,
|
|
34
|
+
inherit_context: options.inheritContext,
|
|
35
|
+
inherit_skills: options.inheritSkills,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
emitError = err.message || "Unknown emit error";
|
|
41
|
+
}
|
|
42
|
+
if (emitError) {
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
resolve({
|
|
45
|
+
success: false,
|
|
46
|
+
error: `Failed to emit spawn event to pi-subagents: ${emitError}`,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
unsub();
|
|
53
|
+
if (!reply?.success) {
|
|
54
|
+
resolve({
|
|
55
|
+
success: false,
|
|
56
|
+
error: reply?.error || "Unknown pi-subagents RPC error",
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
resolve({ success: true, agentId: reply.data?.id });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Helper: Analyze task description and determine best agent
|
|
66
|
+
// (Same as the old analyzeTaskDescription, extracted and made static)
|
|
67
|
+
// ============================================================================
|
|
68
|
+
export function analyzeTaskDescription(taskDescription, agents) {
|
|
69
|
+
const lowerTask = taskDescription.toLowerCase();
|
|
70
|
+
// FIX: Check ALL triggers across ALL agents, then pick earliest position in input text
|
|
71
|
+
let bestTriggerAgent = null;
|
|
72
|
+
for (const agent of agents) {
|
|
73
|
+
if (agent.triggers) {
|
|
74
|
+
for (const trigger of agent.triggers) {
|
|
75
|
+
const position = lowerTask.indexOf(trigger.toLowerCase());
|
|
76
|
+
if (position >= 0) {
|
|
77
|
+
if (!bestTriggerAgent ||
|
|
78
|
+
position < bestTriggerAgent.position) {
|
|
79
|
+
bestTriggerAgent = {
|
|
80
|
+
name: agent.name,
|
|
81
|
+
trigger,
|
|
82
|
+
position,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (bestTriggerAgent) {
|
|
90
|
+
return {
|
|
91
|
+
agentType: bestTriggerAgent.name,
|
|
92
|
+
reason: `Trigger match: "${bestTriggerAgent.trigger}"`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Check useWhen criteria
|
|
96
|
+
const useWhenScores = [];
|
|
97
|
+
for (const agent of agents) {
|
|
98
|
+
if (agent.useWhen) {
|
|
99
|
+
const matches = agent.useWhen.filter((criterion) => lowerTask.includes(criterion.toLowerCase()));
|
|
100
|
+
if (matches.length > 0) {
|
|
101
|
+
useWhenScores.push({
|
|
102
|
+
agent: agent.name,
|
|
103
|
+
score: matches.length,
|
|
104
|
+
matches: matches,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Check avoidWhen penalties
|
|
110
|
+
const avoidWhenPenalties = [];
|
|
111
|
+
for (const agent of agents) {
|
|
112
|
+
if (agent.avoidWhen) {
|
|
113
|
+
const penalties = agent.avoidWhen.filter((criterion) => lowerTask.includes(criterion.toLowerCase()));
|
|
114
|
+
if (penalties.length > 0) {
|
|
115
|
+
avoidWhenPenalties.push({ agent: agent.name, penalties });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Find best match with second-best fallback
|
|
120
|
+
if (useWhenScores.length > 0) {
|
|
121
|
+
useWhenScores.sort((a, b) => b.score - a.score);
|
|
122
|
+
const topCandidate = useWhenScores[0];
|
|
123
|
+
const topPenalty = avoidWhenPenalties.find((p) => p.agent === topCandidate.agent);
|
|
124
|
+
if (!topPenalty) {
|
|
125
|
+
return {
|
|
126
|
+
agentType: topCandidate.agent,
|
|
127
|
+
reason: `useWhen match: "${topCandidate.matches.join(", ")}" (score: ${topCandidate.score})`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Second-best fallback — try next best non-penalized agent
|
|
131
|
+
for (let i = 1; i < useWhenScores.length; i++) {
|
|
132
|
+
const candidate = useWhenScores[i];
|
|
133
|
+
const candidatePenalty = avoidWhenPenalties.find((p) => p.agent === candidate.agent);
|
|
134
|
+
if (!candidatePenalty) {
|
|
135
|
+
return {
|
|
136
|
+
agentType: candidate.agent,
|
|
137
|
+
reason: `useWhen match: "${candidate.matches.join(", ")}" (score: ${candidate.score}), fallback from penalized "${topCandidate.agent}"`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// No non-penalized match — fall back to second-best
|
|
142
|
+
const secondBest = useWhenScores[1];
|
|
143
|
+
if (secondBest) {
|
|
144
|
+
return {
|
|
145
|
+
agentType: secondBest.agent,
|
|
146
|
+
reason: `FALLBACK: "${topCandidate.agent}" penalized (${topPenalty.penalties.join(", ")}), using "${secondBest.agent}"`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
agentType: "general-purpose",
|
|
152
|
+
reason: "No clear match - using general-purpose agent",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Supervisor — The main agent routing supervisor class
|
|
157
|
+
// ============================================================================
|
|
158
|
+
export class Supervisor {
|
|
159
|
+
agents;
|
|
160
|
+
pi;
|
|
161
|
+
constructor(agents, pi) {
|
|
162
|
+
this.agents = agents;
|
|
163
|
+
this.pi = pi;
|
|
164
|
+
}
|
|
165
|
+
// ========================================================================
|
|
166
|
+
// Single-Agent Routing (UNCHANGED behavior from old code)
|
|
167
|
+
// ========================================================================
|
|
168
|
+
async routeAgent(taskDescription, forceAgent, runInBackground, thinking, model) {
|
|
169
|
+
// If forceAgent is specified, use it directly
|
|
170
|
+
if (forceAgent) {
|
|
171
|
+
const agent = this.agents.find((a) => a.name === forceAgent);
|
|
172
|
+
if (agent) {
|
|
173
|
+
const spawnResult = await spawnSubAgent(this.pi, agent.name, taskDescription, taskDescription.substring(0, 50) +
|
|
174
|
+
(taskDescription.length > 50 ? "..." : ""), runInBackground || false, { thinking: thinking || agent.thinking, model });
|
|
175
|
+
if (spawnResult.success) {
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
agentType: forceAgent,
|
|
179
|
+
agentId: spawnResult.agentId,
|
|
180
|
+
reason: "Forced by user",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
error: `Failed to spawn agent '${forceAgent}': ${spawnResult.error}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
error: `Agent not found: ${forceAgent}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Analyze task description against agent criteria
|
|
198
|
+
const routingDecision = analyzeTaskDescription(taskDescription, this.agents);
|
|
199
|
+
const selectedAgent = this.agents.find((a) => a.name === routingDecision.agentType);
|
|
200
|
+
if (!selectedAgent) {
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
agentType: "general-purpose",
|
|
204
|
+
reason: routingDecision.reason,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Spawn the sub-agent via pi-subagents RPC
|
|
208
|
+
const spawnResult = await spawnSubAgent(this.pi, selectedAgent.name, taskDescription, taskDescription.substring(0, 50) +
|
|
209
|
+
(taskDescription.length > 50 ? "..." : ""), runInBackground || false, { thinking: thinking || selectedAgent.thinking, model });
|
|
210
|
+
if (spawnResult.success) {
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
agentType: selectedAgent.name,
|
|
214
|
+
reason: routingDecision.reason,
|
|
215
|
+
agentId: spawnResult.agentId,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
error: `Failed to spawn agent '${selectedAgent.name}': ${spawnResult.error}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ========================================================================
|
|
226
|
+
// Multi-Agent Routing (NEW — planAndRoute)
|
|
227
|
+
// ========================================================================
|
|
228
|
+
/**
|
|
229
|
+
* Plan and route a workflow for multiple agents.
|
|
230
|
+
* Uses keyword matching to find relevant agents, then executes
|
|
231
|
+
* them in parallel (where dependencies allow).
|
|
232
|
+
*/
|
|
233
|
+
async planAndRoute(goal, options) {
|
|
234
|
+
// Step 1: Find relevant agents via keyword matching
|
|
235
|
+
const relevantAgents = options?.forceAgents
|
|
236
|
+
? this.agents.filter((a) => options.forceAgents.includes(a.name))
|
|
237
|
+
: this._findRelevantAgents(goal);
|
|
238
|
+
if (relevantAgents.length === 0) {
|
|
239
|
+
return { success: false, error: "No agents matched for goal" };
|
|
240
|
+
}
|
|
241
|
+
// Step 2: Execute agents in parallel (no dependencies in v2.0)
|
|
242
|
+
const results = {};
|
|
243
|
+
const steps = [];
|
|
244
|
+
const promises = relevantAgents.map(async (agent) => {
|
|
245
|
+
const spawnResult = await spawnSubAgent(this.pi, agent.name, goal, goal.substring(0, 50) + (goal.length > 50 ? "..." : ""), options?.background || false, { model: undefined });
|
|
246
|
+
if (spawnResult.success) {
|
|
247
|
+
results[agent.name] = { success: true, agentId: spawnResult.agentId };
|
|
248
|
+
steps.push(`✅ ${agent.name}: spawned (id: ${spawnResult.agentId})`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
results[agent.name] = { success: false, error: spawnResult.error };
|
|
252
|
+
steps.push(`❌ ${agent.name}: failed (${spawnResult.error})`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
await Promise.all(promises);
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
agents: relevantAgents.map((a) => a.name),
|
|
259
|
+
results,
|
|
260
|
+
steps,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// ========================================================================
|
|
264
|
+
// Internal: Find relevant agents by keyword matching
|
|
265
|
+
// ========================================================================
|
|
266
|
+
_findRelevantAgents(goal) {
|
|
267
|
+
const lowerGoal = goal.toLowerCase();
|
|
268
|
+
const scores = new Map();
|
|
269
|
+
for (const agent of this.agents) {
|
|
270
|
+
let score = 0;
|
|
271
|
+
// Check triggers (highest priority)
|
|
272
|
+
if (agent.triggers) {
|
|
273
|
+
for (const trigger of agent.triggers) {
|
|
274
|
+
if (lowerGoal.includes(trigger.toLowerCase())) {
|
|
275
|
+
score += 10;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Check useWhen
|
|
280
|
+
if (agent.useWhen) {
|
|
281
|
+
for (const when of agent.useWhen) {
|
|
282
|
+
if (lowerGoal.includes(when.toLowerCase())) {
|
|
283
|
+
score += 5;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Subtract for avoidWhen
|
|
288
|
+
if (agent.avoidWhen) {
|
|
289
|
+
for (const when of agent.avoidWhen) {
|
|
290
|
+
if (lowerGoal.includes(when.toLowerCase())) {
|
|
291
|
+
score -= 3;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (score > 0)
|
|
296
|
+
scores.set(agent.name, score);
|
|
297
|
+
}
|
|
298
|
+
// Return agents with score > 0, sorted by score descending
|
|
299
|
+
return Array.from(scores.entries())
|
|
300
|
+
.sort((a, b) => b[1] - a[1])
|
|
301
|
+
.map(([name]) => this.agents.find((a) => a.name === name))
|
|
302
|
+
.filter(Boolean);
|
|
303
|
+
}
|
|
304
|
+
// ========================================================================
|
|
305
|
+
// Helper: Get all agents (builtin + filesystem)
|
|
306
|
+
// ========================================================================
|
|
307
|
+
static async getAllAgents() {
|
|
308
|
+
return BUILTIN_AGENTS;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-subagents — Agent Router Extension
|
|
3
|
+
*
|
|
4
|
+
* v2.0: Supervisor-based routing (refactored from monolithic agent-router.ts)
|
|
5
|
+
*
|
|
6
|
+
* Entry point that wires the Supervisor into the VS Code extension host.
|
|
7
|
+
* Exposes:
|
|
8
|
+
* - "route_agent" tool (single-agent, same behavior as v1)
|
|
9
|
+
* - "plan_and_route" tool (multi-agent workflow, NEW)
|
|
10
|
+
* - "/assign-agent" command (backward-compatible shortcut)
|
|
11
|
+
* - "list_agents" tool (list all available agents)
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/tintinweb/pi-subagents
|
|
14
|
+
*/
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
export default function (pi: ExtensionAPI): Promise<void>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-subagents — Agent Router Extension
|
|
3
|
+
*
|
|
4
|
+
* v2.0: Supervisor-based routing (refactored from monolithic agent-router.ts)
|
|
5
|
+
*
|
|
6
|
+
* Entry point that wires the Supervisor into the VS Code extension host.
|
|
7
|
+
* Exposes:
|
|
8
|
+
* - "route_agent" tool (single-agent, same behavior as v1)
|
|
9
|
+
* - "plan_and_route" tool (multi-agent workflow, NEW)
|
|
10
|
+
* - "/assign-agent" command (backward-compatible shortcut)
|
|
11
|
+
* - "list_agents" tool (list all available agents)
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/tintinweb/pi-subagents
|
|
14
|
+
*/
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import { Supervisor } from "./Supvisor.js";
|
|
17
|
+
import { BUILTIN_AGENTS } from "./BuiltinAgents.js";
|
|
18
|
+
const PI_SUBAGENTS_REPO = "https://github.com/tintinweb/pi-subagents";
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Entry point
|
|
21
|
+
// ============================================================================
|
|
22
|
+
export default async function (pi) {
|
|
23
|
+
// Build the supervisor with builtin agents
|
|
24
|
+
const supervisor = new Supervisor([...BUILTIN_AGENTS], pi);
|
|
25
|
+
// ---- Register route_agent tool (UNCHANGED behavior) ------------------
|
|
26
|
+
pi.registerTool({
|
|
27
|
+
name: "route_agent",
|
|
28
|
+
label: "Route Agent",
|
|
29
|
+
description: `Route tasks to appropriate sub-agents via pi-subagents (${PI_SUBAGENTS_REPO})`,
|
|
30
|
+
parameters: Type.Object({
|
|
31
|
+
taskDescription: Type.String({ description: "Description of the task to be performed" }),
|
|
32
|
+
forceAgent: Type.Optional(Type.String({ description: "Optionally force a specific agent type" })),
|
|
33
|
+
runInBackground: Type.Optional(Type.Boolean({ description: "Run the agent in background (non-blocking)" })),
|
|
34
|
+
thinking: Type.Optional(Type.String({ description: "Thinking level" })),
|
|
35
|
+
model: Type.Optional(Type.String({ description: "Model to use" })),
|
|
36
|
+
}),
|
|
37
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
38
|
+
const { taskDescription, forceAgent, runInBackground, thinking, model } = params;
|
|
39
|
+
// If forceAgent is specified, use it directly
|
|
40
|
+
if (forceAgent) {
|
|
41
|
+
const result = await supervisor.routeAgent(taskDescription, forceAgent, runInBackground, thinking, model);
|
|
42
|
+
if (result.success) {
|
|
43
|
+
const agent = BUILTIN_AGENTS.find((a) => a.name === result.agentType);
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: `Routed to agent: ${result.agentType} via pi-subagents\n\n${agent?.description ?? ""}\n\nAgent ID: ${result.agentId ?? "foreground"}\n\nExecuted by: ${PI_SUBAGENTS_REPO}`,
|
|
48
|
+
}],
|
|
49
|
+
details: {
|
|
50
|
+
agentType: result.agentType,
|
|
51
|
+
reason: result.reason ?? "Forced by user",
|
|
52
|
+
subagentRepo: PI_SUBAGENTS_REPO,
|
|
53
|
+
agentId: result.agentId,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Failed to spawn agent '${forceAgent}': ${result.error}`,
|
|
62
|
+
}],
|
|
63
|
+
details: { error: result.error },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Otherwise, analyze and route normally
|
|
68
|
+
const result = await supervisor.routeAgent(taskDescription, undefined, runInBackground, thinking, model);
|
|
69
|
+
if (result.success) {
|
|
70
|
+
const agent = BUILTIN_AGENTS.find((a) => a.name === result.agentType);
|
|
71
|
+
return {
|
|
72
|
+
content: [{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: `Routed to agent: ${result.agentType} via pi-subagents\n\nDescription: ${agent?.description ?? ""}\n\nReason: ${result.reason ?? ""}\n\nAgent ID: ${result.agentId ?? "foreground"}\n\nExecuted by: ${PI_SUBAGENTS_REPO}`,
|
|
75
|
+
}],
|
|
76
|
+
details: {
|
|
77
|
+
agentType: result.agentType,
|
|
78
|
+
reason: result.reason,
|
|
79
|
+
subagentRepo: PI_SUBAGENTS_REPO,
|
|
80
|
+
agentId: result.agentId,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
return {
|
|
86
|
+
content: [{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: `Failed to spawn agent: ${result.error}`,
|
|
89
|
+
}],
|
|
90
|
+
details: { error: result.error },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
// ---- Register plan_and_route tool (NEW — multi-agent workflow) --------
|
|
96
|
+
pi.registerTool({
|
|
97
|
+
name: "plan_and_route",
|
|
98
|
+
label: "Plan and Route",
|
|
99
|
+
description: `Plan a multi-agent workflow via pi-subagents (${PI_SUBAGENTS_REPO}). Launches multiple agents in parallel based on goal keywords.`,
|
|
100
|
+
parameters: Type.Object({
|
|
101
|
+
goal: Type.String({ description: "The goal description for multi-agent execution" }),
|
|
102
|
+
forceAgents: Type.Optional(Type.Array(Type.String({ description: "Agent names to force" }))),
|
|
103
|
+
runInBackground: Type.Optional(Type.Boolean({ description: "Run agents in background" })),
|
|
104
|
+
}),
|
|
105
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
106
|
+
const { goal, forceAgents, runInBackground } = params;
|
|
107
|
+
const result = await supervisor.planAndRoute(goal, { forceAgents, background: runInBackground });
|
|
108
|
+
if (result.success) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: `Multi-agent workflow launched successfully!\n\nAgents: ${(result.agents ?? []).join(", ")}\nSteps:\n${(result.steps ?? []).join("\n")}`,
|
|
113
|
+
}],
|
|
114
|
+
details: {
|
|
115
|
+
agents: result.agents,
|
|
116
|
+
results: result.results,
|
|
117
|
+
subagentRepo: PI_SUBAGENTS_REPO,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `Failed to plan workflow: ${result.error}`,
|
|
126
|
+
}],
|
|
127
|
+
details: { error: result.error },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
// ---- Register /assign-agent command (UNCHANGED behavior) --------------
|
|
133
|
+
pi.registerCommand("assign-agent", {
|
|
134
|
+
description: `Route tasks to appropriate sub-agents via pi-subagents (${PI_SUBAGENTS_REPO})`,
|
|
135
|
+
handler: async (input, ctx) => {
|
|
136
|
+
if (!input) {
|
|
137
|
+
ctx.ui.notify("Please provide a task description", "error");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const result = await supervisor.routeAgent(input, undefined, false);
|
|
141
|
+
if (result.success) {
|
|
142
|
+
ctx.ui.notify(`Routed to ${result.agentType}: ${result.reason ?? ""} (via pi-subagents)`, "info");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
ctx.ui.notify(`Failed to spawn agent: ${result.error}`, "error");
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
// ---- Register list_agents tool (UNCHANGED behavior) ------------------
|
|
150
|
+
pi.registerTool({
|
|
151
|
+
name: "list_agents",
|
|
152
|
+
label: "List Agents",
|
|
153
|
+
description: `List all available agents and their routing criteria (${PI_SUBAGENTS_REPO})`,
|
|
154
|
+
parameters: Type.Object({}),
|
|
155
|
+
async execute() {
|
|
156
|
+
const agents = [
|
|
157
|
+
...BUILTIN_AGENTS,
|
|
158
|
+
// TODO: add filesystem agents here when implemented
|
|
159
|
+
];
|
|
160
|
+
let response = `Available Agents (routed via pi-subagents):\n\n`;
|
|
161
|
+
agents.forEach((agent, index) => {
|
|
162
|
+
response += `${index + 1}. **${agent.name}**\n`;
|
|
163
|
+
response += ` Description: ${agent.description}\n`;
|
|
164
|
+
response += ` Thinking: ${agent.thinking ?? "medium"}\n`;
|
|
165
|
+
response += ` Triggers: ${agent.triggers?.join(", ") ?? "none"}\n`;
|
|
166
|
+
response += ` Use When: ${agent.useWhen?.join(", ") ?? "any"}\n`;
|
|
167
|
+
response += ` Avoid When: ${agent.avoidWhen?.join(", ") ?? "none"}\n`;
|
|
168
|
+
response += ` Tools: ${agent.tools?.join(", ") ?? "default"}\n\n`;
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: response }],
|
|
172
|
+
details: {
|
|
173
|
+
agents: agents.map((a) => ({
|
|
174
|
+
name: a.name,
|
|
175
|
+
description: a.description,
|
|
176
|
+
useWhen: a.useWhen,
|
|
177
|
+
avoidWhen: a.avoidWhen,
|
|
178
|
+
})),
|
|
179
|
+
subagentRepo: PI_SUBAGENTS_REPO,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
// ---- Intercept input (UNCHANGED behavior from old code) --------------
|
|
185
|
+
pi.on("input", async (event, ctx) => {
|
|
186
|
+
// Skip commands / special prefixes
|
|
187
|
+
if (event.text.startsWith("/") || event.text.startsWith("!")) {
|
|
188
|
+
return { action: "continue" };
|
|
189
|
+
}
|
|
190
|
+
// Analyze and route
|
|
191
|
+
const result = await supervisor.routeAgent(event.text, undefined, true);
|
|
192
|
+
if (result.success) {
|
|
193
|
+
ctx.ui.notify(`Auto-routed to ${result.agentType}: ${result.reason ?? ""} (via pi-subagents)`, "info");
|
|
194
|
+
return { action: "handled" };
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Fallback to text transformation instead of suppressing
|
|
198
|
+
ctx.ui.notify(`Auto-routing failed: ${result.error}`, "warning");
|
|
199
|
+
return {
|
|
200
|
+
action: "transform",
|
|
201
|
+
text: `[ROUTED] ${event.text}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// ---- Session start notification (UNCHANGED) --------------------------
|
|
206
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
207
|
+
ctx.ui.notify(`${PI_SUBAGENTS_REPO} engine loaded — route_agent and plan_and_route available`, "info");
|
|
208
|
+
});
|
|
209
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-subagents-router",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A Pi extension that routes agents based on task descriptions using useWhen and avoidWhen criteria",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/agent-router.js",
|
|
7
|
+
"types": "./dist/agent-router.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/agent-router.d.ts",
|
|
12
|
+
"default": "./dist/agent-router.js"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"test": "node --require ts-node/register test/test-supervisor.ts",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pi-package",
|
|
28
|
+
"pi-extension",
|
|
29
|
+
"pi",
|
|
30
|
+
"extension",
|
|
31
|
+
"agent",
|
|
32
|
+
"routing",
|
|
33
|
+
"ai"
|
|
34
|
+
],
|
|
35
|
+
"author": "Asdrubal",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"private": false,
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "latest",
|
|
43
|
+
"typebox": "^1.3.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"ts-node": "^10.9.2",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
48
|
+
},
|
|
49
|
+
"pi": {
|
|
50
|
+
"extensions": [
|
|
51
|
+
"./dist/agent-router.js"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentConfig — Shared type for all agents (builtin + filesystem).
|
|
3
|
+
*
|
|
4
|
+
* This file extracts the AgentConfig interface from the monolithic
|
|
5
|
+
* agent-router.ts so it can be imported by all modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type AgentSource = "builtin" | "filesystem";
|
|
9
|
+
|
|
10
|
+
export interface AgentConfig {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
thinking?: string;
|
|
14
|
+
tools?: string[];
|
|
15
|
+
systemPromptMode?: string;
|
|
16
|
+
inheritProjectContext?: boolean;
|
|
17
|
+
inheritSkills?: boolean;
|
|
18
|
+
triggers?: string[];
|
|
19
|
+
useWhen?: string[];
|
|
20
|
+
avoidWhen?: string[];
|
|
21
|
+
|
|
22
|
+
// NEW (Phase 1+): capability-based routing
|
|
23
|
+
capabilities?: string[]; // capabilities this agent provides
|
|
24
|
+
consumes?: string[]; // dependencies (inputs needed)
|
|
25
|
+
produces?: string[]; // dependencies (outputs produced)
|
|
26
|
+
dependsOn?: string[]; // agents this agent depends on
|
|
27
|
+
source?: AgentSource; // "builtin" | "filesystem"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple routing decision
|
|
32
|
+
*/
|
|
33
|
+
export interface RoutingDecision {
|
|
34
|
+
agentType: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Route result — single agent
|
|
40
|
+
*/
|
|
41
|
+
export interface RouteResult {
|
|
42
|
+
success: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
agentId?: string;
|
|
45
|
+
agentType?: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
details?: Record<string, any>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Plan result — multi-agent workflow
|
|
52
|
+
*/
|
|
53
|
+
export interface PlanResult {
|
|
54
|
+
success: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
agents?: string[]; // names of agents to execute
|
|
57
|
+
steps?: string[]; // executed step descriptions
|
|
58
|
+
results?: Record<string, any>; // results keyed by agent name
|
|
59
|
+
}
|