pi-squad 0.1.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/README.md +244 -0
- package/package.json +30 -0
- package/src/agent-pool.ts +445 -0
- package/src/agents/_defaults/architect.json +9 -0
- package/src/agents/_defaults/backend.json +9 -0
- package/src/agents/_defaults/debugger.json +9 -0
- package/src/agents/_defaults/devops.json +9 -0
- package/src/agents/_defaults/docs.json +9 -0
- package/src/agents/_defaults/frontend.json +9 -0
- package/src/agents/_defaults/fullstack.json +9 -0
- package/src/agents/_defaults/planner.json +9 -0
- package/src/agents/_defaults/qa.json +9 -0
- package/src/agents/_defaults/researcher.json +9 -0
- package/src/agents/_defaults/security.json +9 -0
- package/src/index.ts +1121 -0
- package/src/monitor.ts +204 -0
- package/src/panel/message-view.ts +232 -0
- package/src/panel/squad-panel.ts +383 -0
- package/src/panel/task-list.ts +264 -0
- package/src/planner.ts +275 -0
- package/src/protocol.ts +265 -0
- package/src/router.ts +207 -0
- package/src/scheduler.ts +732 -0
- package/src/skills/collaboration/SKILL.md +39 -0
- package/src/skills/squad-protocol/SKILL.md +65 -0
- package/src/skills/verification/SKILL.md +64 -0
- package/src/store.ts +458 -0
- package/src/supervisor.ts +143 -0
- package/src/types.ts +210 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* router.ts — @mention parsing and cross-agent message delivery.
|
|
3
|
+
*
|
|
4
|
+
* Parses assistant text for @agentname patterns.
|
|
5
|
+
* Routes messages to target agents via steer() if running,
|
|
6
|
+
* or queues them for delivery on next spawn.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentPool } from "./agent-pool.js";
|
|
10
|
+
import type { TaskMessage } from "./types.js";
|
|
11
|
+
import * as store from "./store.js";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export type EscalationListener = (taskId: string, agentName: string, message: string) => void;
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Router
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export class Router {
|
|
24
|
+
private pool: AgentPool;
|
|
25
|
+
private squadId: string;
|
|
26
|
+
private escalationListeners: EscalationListener[] = [];
|
|
27
|
+
|
|
28
|
+
constructor(pool: AgentPool, squadId: string) {
|
|
29
|
+
this.pool = pool;
|
|
30
|
+
this.squadId = squadId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Subscribe to escalation events (agent blocked, needs human) */
|
|
34
|
+
onEscalation(listener: EscalationListener): () => void {
|
|
35
|
+
this.escalationListeners.push(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
const idx = this.escalationListeners.indexOf(listener);
|
|
38
|
+
if (idx !== -1) this.escalationListeners.splice(idx, 1);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Process an assistant message for signals:
|
|
44
|
+
* - @mentions → route to target agent
|
|
45
|
+
* - Block signals → detect and escalate
|
|
46
|
+
*/
|
|
47
|
+
processMessage(taskId: string, fromAgent: string, text: string): void {
|
|
48
|
+
// Parse @mentions
|
|
49
|
+
const mentions = this.parseMentions(text, fromAgent);
|
|
50
|
+
for (const mention of mentions) {
|
|
51
|
+
this.routeMention(taskId, fromAgent, mention.target, mention.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Detect block signals
|
|
55
|
+
if (this.isBlockSignal(text)) {
|
|
56
|
+
for (const listener of this.escalationListeners) {
|
|
57
|
+
listener(taskId, fromAgent, this.extractBlockReason(text));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Route a message from one agent to another.
|
|
64
|
+
*/
|
|
65
|
+
routeMention(
|
|
66
|
+
sourceTaskId: string,
|
|
67
|
+
fromAgent: string,
|
|
68
|
+
targetAgent: string,
|
|
69
|
+
message: string,
|
|
70
|
+
): void {
|
|
71
|
+
// Log the mention in the source task
|
|
72
|
+
store.appendMessage(this.squadId, sourceTaskId, {
|
|
73
|
+
ts: store.now(),
|
|
74
|
+
from: fromAgent,
|
|
75
|
+
type: "mention",
|
|
76
|
+
to: targetAgent,
|
|
77
|
+
text: message,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Find if target agent is running
|
|
81
|
+
const targetTaskId = this.pool.getTaskIdForAgent(targetAgent);
|
|
82
|
+
|
|
83
|
+
if (targetTaskId && this.pool.isRunning(targetTaskId)) {
|
|
84
|
+
// Target is running — steer them
|
|
85
|
+
const steerMessage = `[squad] Message from @${fromAgent} (working on ${sourceTaskId}):\n${message}`;
|
|
86
|
+
this.pool.steer(targetTaskId, steerMessage);
|
|
87
|
+
|
|
88
|
+
// Log in target task too
|
|
89
|
+
store.appendMessage(this.squadId, targetTaskId, {
|
|
90
|
+
ts: store.now(),
|
|
91
|
+
from: fromAgent,
|
|
92
|
+
type: "mention",
|
|
93
|
+
to: targetAgent,
|
|
94
|
+
text: message,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
// Target not running — queue for later
|
|
98
|
+
this.pool.queueMessage(targetAgent, {
|
|
99
|
+
ts: store.now(),
|
|
100
|
+
from: fromAgent,
|
|
101
|
+
type: "mention",
|
|
102
|
+
to: targetAgent,
|
|
103
|
+
text: message,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Route a human message to an agent.
|
|
110
|
+
*/
|
|
111
|
+
routeHumanMessage(taskId: string, message: string): void {
|
|
112
|
+
store.appendMessage(this.squadId, taskId, {
|
|
113
|
+
ts: store.now(),
|
|
114
|
+
from: "human",
|
|
115
|
+
type: "message",
|
|
116
|
+
text: message,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (this.pool.isRunning(taskId)) {
|
|
120
|
+
this.pool.steer(taskId, `[squad] Human: ${message}`);
|
|
121
|
+
} else {
|
|
122
|
+
const task = store.loadTask(this.squadId, taskId);
|
|
123
|
+
if (task) {
|
|
124
|
+
this.pool.queueMessage(task.agent, {
|
|
125
|
+
ts: store.now(),
|
|
126
|
+
from: "human",
|
|
127
|
+
type: "message",
|
|
128
|
+
text: message,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =========================================================================
|
|
135
|
+
// Parsing
|
|
136
|
+
// =========================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse @mentions from text.
|
|
140
|
+
* Matches @agentname followed by text until the next @mention or end of line.
|
|
141
|
+
*/
|
|
142
|
+
private parseMentions(
|
|
143
|
+
text: string,
|
|
144
|
+
fromAgent: string,
|
|
145
|
+
): Array<{ target: string; message: string }> {
|
|
146
|
+
const mentions: Array<{ target: string; message: string }> = [];
|
|
147
|
+
// Match @word at start of line or after whitespace, capture until next @mention or newline
|
|
148
|
+
const regex = /(?:^|\s)@(\w+)\s+([^\n@]*(?:\n(?!.*@\w).*)*)/gm;
|
|
149
|
+
|
|
150
|
+
for (const match of text.matchAll(regex)) {
|
|
151
|
+
const target = match[1];
|
|
152
|
+
const message = match[2].trim();
|
|
153
|
+
|
|
154
|
+
// Don't route self-mentions
|
|
155
|
+
if (target === fromAgent) continue;
|
|
156
|
+
|
|
157
|
+
// Don't route empty messages
|
|
158
|
+
if (!message) continue;
|
|
159
|
+
|
|
160
|
+
// Check if target is a known agent
|
|
161
|
+
const projectCwd = store.loadSquad(this.squadId)?.cwd;
|
|
162
|
+
const agentDef = store.loadAgentDef(target, projectCwd);
|
|
163
|
+
if (agentDef) {
|
|
164
|
+
mentions.push({ target, message });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return mentions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Detect if text indicates the agent is blocked.
|
|
173
|
+
*/
|
|
174
|
+
private isBlockSignal(text: string): boolean {
|
|
175
|
+
const lower = text.toLowerCase();
|
|
176
|
+
const blockPatterns = [
|
|
177
|
+
/\bi(?:'m| am) blocked\b/,
|
|
178
|
+
/\bcannot proceed\b/,
|
|
179
|
+
/\bcan't proceed\b/,
|
|
180
|
+
/\bneed .+ (?:before|to proceed|to continue)/,
|
|
181
|
+
/\bwaiting (?:for|on) .+ (?:input|decision|response)/,
|
|
182
|
+
/\bblocked(?:\s+because|\s+by|\s*:)/,
|
|
183
|
+
];
|
|
184
|
+
return blockPatterns.some((p) => p.test(lower));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract the block reason from text.
|
|
189
|
+
*/
|
|
190
|
+
private extractBlockReason(text: string): string {
|
|
191
|
+
// Try to find the line with the block signal
|
|
192
|
+
const lines = text.split("\n");
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const lower = line.toLowerCase();
|
|
195
|
+
if (
|
|
196
|
+
lower.includes("blocked") ||
|
|
197
|
+
lower.includes("cannot proceed") ||
|
|
198
|
+
lower.includes("can't proceed") ||
|
|
199
|
+
lower.includes("waiting for") ||
|
|
200
|
+
lower.includes("waiting on")
|
|
201
|
+
) {
|
|
202
|
+
return line.trim();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return text.slice(0, 200);
|
|
206
|
+
}
|
|
207
|
+
}
|