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.
@@ -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
+ };
@@ -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
+ }