@bolloon/bolloon-agent 0.1.38 → 0.1.39
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/dist/agents/constraint-layer.js +1 -2
- package/dist/web/client.js +2861 -3746
- package/dist/web/components/p2p/index.js +226 -264
- package/dist/web/ui/message-renderer.js +323 -434
- package/dist/web/ui/step-timeline.js +255 -351
- package/package.json +1 -1
- package/dist/bollharness-integration/llm/pi-ai.js +0 -397
- package/dist/bollharness-integration/pi-ecosystem-colony/index.js +0 -365
- package/dist/bollharness-integration/pi-ecosystem-goals/index.js +0 -458
- package/dist/bollharness-integration/pi-ecosystem-judgment/decision.js +0 -300
- package/dist/bollharness-integration/pi-ecosystem-judgment/distillation.js +0 -291
- package/dist/bollharness-integration/pi-ecosystem-judgment/index.js +0 -445
- package/dist/bollharness-integration/pi-ecosystem-mcp/index.js +0 -331
- package/dist/bollharness-integration/pi-ecosystem-subagents/index.js +0 -303
- package/dist/constraints/commands.js +0 -100
- package/dist/constraints/permissions.js +0 -37
- package/dist/constraints/runtime.js +0 -135
- package/dist/constraints/session.js +0 -48
- package/dist/constraints/system-init.js +0 -51
- package/dist/constraints/tools.js +0 -104
- package/dist/llm/minimax-provider.js +0 -46
- package/dist/llm/minimax.js +0 -45
- package/dist/pi-ecosystem-colony/index.js +0 -365
- package/dist/runtime/context/minimax-prompt.js +0 -178
- package/dist/runtime/context/sys-prompt.js +0 -1
- package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
- package/dist/social/ant-colony/PheromoneEngine.js +0 -227
- package/dist/social/ant-colony/index.js +0 -6
- package/dist/social/ant-colony/types.js +0 -24
- package/dist/utils/double.js +0 -6
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -657
- package/dist/web/components/p2p/p2p-tools.js +0 -248
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi MCP Adapter Integration for Bolloon
|
|
3
|
-
*
|
|
4
|
-
* Bridges MCP (Model Context Protocol) servers with Bolloon's tool system.
|
|
5
|
-
* Based on the pi-mcp-adapter philosophy: on-demand tool loading with minimal token overhead.
|
|
6
|
-
*
|
|
7
|
-
* Key differences from Claude Code MCP:
|
|
8
|
-
* - White-box design: every schema, parameter, and return value is visible
|
|
9
|
-
* - Minimal primitives: only read, write, edit, bash under the hood
|
|
10
|
-
* - No schema validation black boxes - interfaces are simple by design
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from 'fs/promises';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import { spawn } from 'child_process';
|
|
15
|
-
import { EventEmitter } from 'events';
|
|
16
|
-
// MCP adapter state
|
|
17
|
-
let tools = new Map();
|
|
18
|
-
let servers = new Map();
|
|
19
|
-
let initialized = false;
|
|
20
|
-
let toolCallLog = [];
|
|
21
|
-
// Event emitter for MCP events
|
|
22
|
-
class McpEventEmitter extends EventEmitter {
|
|
23
|
-
}
|
|
24
|
-
const mcpEvents = new McpEventEmitter();
|
|
25
|
-
/**
|
|
26
|
-
* Discover MCP servers from standard config locations
|
|
27
|
-
*/
|
|
28
|
-
export async function discoverMcpServers() {
|
|
29
|
-
const configs = [];
|
|
30
|
-
const locations = [
|
|
31
|
-
path.join(process.env.HOME || '/tmp', '.mcp.json'),
|
|
32
|
-
path.join(process.env.HOME || '/tmp', '.config', 'mcp', 'mcp.json'),
|
|
33
|
-
path.join(process.env.HOME || '/tmp', '.config', 'mcp.json'),
|
|
34
|
-
'.mcp.json',
|
|
35
|
-
];
|
|
36
|
-
for (const loc of locations) {
|
|
37
|
-
try {
|
|
38
|
-
const content = await fs.readFile(loc, 'utf-8');
|
|
39
|
-
const mcpJson = JSON.parse(content);
|
|
40
|
-
if (mcpJson.mcpServers) {
|
|
41
|
-
for (const [name, config] of Object.entries(mcpJson.mcpServers)) {
|
|
42
|
-
const serverConfig = config;
|
|
43
|
-
configs.push({
|
|
44
|
-
name,
|
|
45
|
-
command: serverConfig.command,
|
|
46
|
-
args: serverConfig.args,
|
|
47
|
-
env: serverConfig.env,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (mcpJson['mcpServers']) {
|
|
52
|
-
for (const [name, config] of Object.entries(mcpJson['mcpServers'])) {
|
|
53
|
-
const serverConfig = config;
|
|
54
|
-
configs.push({
|
|
55
|
-
name,
|
|
56
|
-
command: serverConfig.command,
|
|
57
|
-
args: serverConfig.args,
|
|
58
|
-
env: serverConfig.env,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// File doesn't exist, skip
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return configs;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Initialize the MCP adapter
|
|
71
|
-
*/
|
|
72
|
-
export async function initializeMcpAdapter() {
|
|
73
|
-
if (initialized)
|
|
74
|
-
return;
|
|
75
|
-
const discoveredServers = await discoverMcpServers();
|
|
76
|
-
console.log(`[McpAdapter] Discovered ${discoveredServers.length} MCP servers`);
|
|
77
|
-
for (const server of discoveredServers) {
|
|
78
|
-
registerServer(server);
|
|
79
|
-
}
|
|
80
|
-
initialized = true;
|
|
81
|
-
mcpEvents.emit('initialized');
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Register an MCP server configuration
|
|
85
|
-
*/
|
|
86
|
-
export function registerServer(config) {
|
|
87
|
-
servers.set(config.name, { config, process: null, running: false });
|
|
88
|
-
console.log(`[McpAdapter] Registered server: ${config.name}`);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Register an MCP tool
|
|
92
|
-
*/
|
|
93
|
-
export function registerTool(tool) {
|
|
94
|
-
tools.set(tool.name, tool);
|
|
95
|
-
mcpEvents.emit('toolRegistered', tool);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* List all available MCP tools
|
|
99
|
-
*/
|
|
100
|
-
export function listTools() {
|
|
101
|
-
return Array.from(tools.values());
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Check if a tool exists
|
|
105
|
-
*/
|
|
106
|
-
export function hasTool(name) {
|
|
107
|
-
return tools.has(name);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Get a specific tool
|
|
111
|
-
*/
|
|
112
|
-
export function getTool(name) {
|
|
113
|
-
return tools.get(name);
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Get tool call log for debugging
|
|
117
|
-
*/
|
|
118
|
-
export function getToolCallLog() {
|
|
119
|
-
return [...toolCallLog];
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Clear tool call log
|
|
123
|
-
*/
|
|
124
|
-
export function clearToolCallLog() {
|
|
125
|
-
toolCallLog = [];
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Execute an MCP tool via JSON-RPC protocol
|
|
129
|
-
*/
|
|
130
|
-
export async function executeTool(name, args) {
|
|
131
|
-
const tool = tools.get(name);
|
|
132
|
-
if (!tool) {
|
|
133
|
-
return { success: false, error: `Unknown tool: ${name}` };
|
|
134
|
-
}
|
|
135
|
-
const server = servers.get(tool.serverName);
|
|
136
|
-
if (!server) {
|
|
137
|
-
return { success: false, error: `Server not found: ${tool.serverName}` };
|
|
138
|
-
}
|
|
139
|
-
// Log the call for debugging
|
|
140
|
-
const logEntry = {
|
|
141
|
-
timestamp: new Date().toISOString(),
|
|
142
|
-
tool: name,
|
|
143
|
-
args,
|
|
144
|
-
result: null,
|
|
145
|
-
};
|
|
146
|
-
console.log(`[McpAdapter] Executing tool: ${name} with args:`, JSON.stringify(args, null, 2));
|
|
147
|
-
try {
|
|
148
|
-
const result = await sendMcpRequest(tool.serverName, 'tools/call', {
|
|
149
|
-
name,
|
|
150
|
-
arguments: args,
|
|
151
|
-
});
|
|
152
|
-
logEntry.result = result;
|
|
153
|
-
toolCallLog.push(logEntry);
|
|
154
|
-
// Keep log size manageable
|
|
155
|
-
if (toolCallLog.length > 100) {
|
|
156
|
-
toolCallLog = toolCallLog.slice(-50);
|
|
157
|
-
}
|
|
158
|
-
if (result && typeof result === 'object' && 'content' in result) {
|
|
159
|
-
return {
|
|
160
|
-
success: true,
|
|
161
|
-
content: result.content,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return { success: true, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
165
|
-
}
|
|
166
|
-
catch (e) {
|
|
167
|
-
logEntry.result = { error: String(e) };
|
|
168
|
-
toolCallLog.push(logEntry);
|
|
169
|
-
return { success: false, error: String(e) };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Send MCP request to a server (simplified - uses stdin/stdout JSON-RPC)
|
|
174
|
-
*/
|
|
175
|
-
async function sendMcpRequest(serverName, method, params) {
|
|
176
|
-
const server = servers.get(serverName);
|
|
177
|
-
if (!server) {
|
|
178
|
-
throw new Error(`Server not registered: ${serverName}`);
|
|
179
|
-
}
|
|
180
|
-
// For now, return a placeholder - real implementation would use MCP protocol
|
|
181
|
-
// The actual protocol typically uses stdio or HTTP+streamableHTTP
|
|
182
|
-
console.log(`[McpAdapter] Would send ${method} to ${serverName}:`, params);
|
|
183
|
-
// Simulate successful response for development
|
|
184
|
-
return {
|
|
185
|
-
content: [
|
|
186
|
-
{
|
|
187
|
-
type: 'text',
|
|
188
|
-
text: `[Simulated] Tool ${method} executed with params: ${JSON.stringify(params)}`,
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Start an MCP server process
|
|
195
|
-
*/
|
|
196
|
-
export async function startServer(serverName) {
|
|
197
|
-
const server = servers.get(serverName);
|
|
198
|
-
if (!server) {
|
|
199
|
-
console.error(`[McpAdapter] Server not found: ${serverName}`);
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
if (server.running && server.process) {
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
try {
|
|
206
|
-
const child = spawn(server.config.command, server.config.args || [], {
|
|
207
|
-
env: { ...process.env, ...server.config.env },
|
|
208
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
209
|
-
});
|
|
210
|
-
child.stdout?.on('data', (data) => {
|
|
211
|
-
console.log(`[McpAdapter][${serverName}] stdout:`, data.toString());
|
|
212
|
-
});
|
|
213
|
-
child.stderr?.on('data', (data) => {
|
|
214
|
-
console.error(`[McpAdapter][${serverName}] stderr:`, data.toString());
|
|
215
|
-
});
|
|
216
|
-
child.on('error', (err) => {
|
|
217
|
-
console.error(`[McpAdapter][${serverName}] error:`, err);
|
|
218
|
-
server.running = false;
|
|
219
|
-
server.process = null;
|
|
220
|
-
});
|
|
221
|
-
child.on('exit', (code) => {
|
|
222
|
-
console.log(`[McpAdapter][${serverName}] exited with code:`, code);
|
|
223
|
-
server.running = false;
|
|
224
|
-
server.process = null;
|
|
225
|
-
});
|
|
226
|
-
server.process = child;
|
|
227
|
-
server.running = true;
|
|
228
|
-
console.log(`[McpAdapter] Started server: ${serverName}`);
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
catch (e) {
|
|
232
|
-
console.error(`[McpAdapter] Failed to start ${serverName}:`, e);
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Stop an MCP server
|
|
238
|
-
*/
|
|
239
|
-
export function stopServer(serverName) {
|
|
240
|
-
const server = servers.get(serverName);
|
|
241
|
-
if (server && server.process) {
|
|
242
|
-
server.process.kill();
|
|
243
|
-
server.running = false;
|
|
244
|
-
server.process = null;
|
|
245
|
-
console.log(`[McpAdapter] Stopped server: ${serverName}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Discover tools from a server using MCP protocol
|
|
250
|
-
*/
|
|
251
|
-
export async function discoverTools(serverName) {
|
|
252
|
-
try {
|
|
253
|
-
const result = await sendMcpRequest(serverName, 'tools/list');
|
|
254
|
-
const toolsList = [];
|
|
255
|
-
if (result && typeof result === 'object' && 'tools' in result) {
|
|
256
|
-
const toolsData = result.tools;
|
|
257
|
-
for (const tool of toolsData) {
|
|
258
|
-
const t = tool;
|
|
259
|
-
toolsList.push({
|
|
260
|
-
name: t.name,
|
|
261
|
-
description: t.description || '',
|
|
262
|
-
inputSchema: t.inputSchema || {},
|
|
263
|
-
serverName,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return toolsList;
|
|
268
|
-
}
|
|
269
|
-
catch (e) {
|
|
270
|
-
console.error(`[McpAdapter] Failed to discover tools from ${serverName}:`, e);
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Create a Tavily search tool (common MCP integration)
|
|
276
|
-
*/
|
|
277
|
-
export function createTavilyTool(apiKey) {
|
|
278
|
-
return {
|
|
279
|
-
name: 'tavily_search',
|
|
280
|
-
description: 'Search the web using Tavily',
|
|
281
|
-
inputSchema: {
|
|
282
|
-
type: 'object',
|
|
283
|
-
properties: {
|
|
284
|
-
query: { type: 'string', description: 'Search query' },
|
|
285
|
-
maxResults: { type: 'number', description: 'Maximum results', default: 5 },
|
|
286
|
-
},
|
|
287
|
-
required: ['query'],
|
|
288
|
-
},
|
|
289
|
-
serverName: 'tavily',
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Create an Amap maps tool
|
|
294
|
-
*/
|
|
295
|
-
export function createAmapTool(apiKey) {
|
|
296
|
-
return {
|
|
297
|
-
name: 'amap_weather',
|
|
298
|
-
description: 'Get weather information from Amap',
|
|
299
|
-
inputSchema: {
|
|
300
|
-
type: 'object',
|
|
301
|
-
properties: {
|
|
302
|
-
city: { type: 'string', description: 'City name or code' },
|
|
303
|
-
},
|
|
304
|
-
required: ['city'],
|
|
305
|
-
},
|
|
306
|
-
serverName: 'amap-maps',
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Get adapter status
|
|
311
|
-
*/
|
|
312
|
-
export function getAdapterStatus() {
|
|
313
|
-
return {
|
|
314
|
-
initialized,
|
|
315
|
-
serverCount: servers.size,
|
|
316
|
-
toolCount: tools.size,
|
|
317
|
-
servers: Array.from(servers.keys()),
|
|
318
|
-
runningServers: Array.from(servers.entries())
|
|
319
|
-
.filter(([, s]) => s.running)
|
|
320
|
-
.map(([name]) => name),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Event subscription
|
|
325
|
-
*/
|
|
326
|
-
export function on(event, callback) {
|
|
327
|
-
mcpEvents.on(event, callback);
|
|
328
|
-
}
|
|
329
|
-
export function off(event, callback) {
|
|
330
|
-
mcpEvents.off(event, callback);
|
|
331
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi Subagents Integration for Bolloon
|
|
3
|
-
*
|
|
4
|
-
* Lightweight subagent implementation based on tmux multi-session.
|
|
5
|
-
* Based on pi-subagents philosophy: simple task delegation and result collection.
|
|
6
|
-
*
|
|
7
|
-
* Key differences from Claude Code subagents:
|
|
8
|
-
* - tmux-based sessions (not black-box background processes)
|
|
9
|
-
* - Simple task委托 and result回收
|
|
10
|
-
* - Full visibility into what each subagent is doing
|
|
11
|
-
* - No complex scheduling - suitable for single task splitting
|
|
12
|
-
*/
|
|
13
|
-
import { spawn } from 'child_process';
|
|
14
|
-
import * as fs from 'fs/promises';
|
|
15
|
-
import * as path from 'path';
|
|
16
|
-
// Session management
|
|
17
|
-
const sessions = new Map();
|
|
18
|
-
const tmuxSessions = new Map();
|
|
19
|
-
const SESSION_PREFIX = 'bolloon-subagent-';
|
|
20
|
-
const STATE_DIR = path.join(process.env.HOME || '/tmp', '.bolloon', 'subagents');
|
|
21
|
-
/**
|
|
22
|
-
* Generate a unique subagent ID
|
|
23
|
-
*/
|
|
24
|
-
function generateId() {
|
|
25
|
-
return `sa-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Create a new tmux session for a subagent
|
|
29
|
-
*/
|
|
30
|
-
export async function createSubagent(task, options = {}) {
|
|
31
|
-
const id = generateId();
|
|
32
|
-
const sessionId = `${SESSION_PREFIX}${id}`;
|
|
33
|
-
const name = options.name || `subagent-${id.substring(0, 8)}`;
|
|
34
|
-
const subagent = {
|
|
35
|
-
id,
|
|
36
|
-
name,
|
|
37
|
-
task,
|
|
38
|
-
status: 'created',
|
|
39
|
-
sessionId,
|
|
40
|
-
createdAt: new Date().toISOString(),
|
|
41
|
-
};
|
|
42
|
-
sessions.set(id, subagent);
|
|
43
|
-
await fs.mkdir(STATE_DIR, { recursive: true });
|
|
44
|
-
await fs.writeFile(path.join(STATE_DIR, `${id}.json`), JSON.stringify(subagent, null, 2));
|
|
45
|
-
console.log(`[PiSubagents] Created subagent: ${id} - ${name}`);
|
|
46
|
-
return subagent;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Start a subagent in a tmux session
|
|
50
|
-
*/
|
|
51
|
-
export async function startSubagent(id, command, options = {}) {
|
|
52
|
-
const subagent = sessions.get(id);
|
|
53
|
-
if (!subagent) {
|
|
54
|
-
return { subagentId: id, success: false, error: 'Subagent not found' };
|
|
55
|
-
}
|
|
56
|
-
if (subagent.status === 'running') {
|
|
57
|
-
return { subagentId: id, success: false, error: 'Subagent already running' };
|
|
58
|
-
}
|
|
59
|
-
subagent.status = 'running';
|
|
60
|
-
subagent.startedAt = new Date().toISOString();
|
|
61
|
-
await saveSubagent(subagent);
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
// Create a detached tmux session running the command
|
|
64
|
-
const tmuxCmd = spawn('tmux', [
|
|
65
|
-
'new-session',
|
|
66
|
-
'-d',
|
|
67
|
-
'-s',
|
|
68
|
-
subagent.sessionId,
|
|
69
|
-
command,
|
|
70
|
-
], {
|
|
71
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
72
|
-
});
|
|
73
|
-
tmuxSessions.set(id, tmuxCmd);
|
|
74
|
-
const timeoutMs = options.timeoutMs || 5 * 60 * 1000;
|
|
75
|
-
let timedOut = false;
|
|
76
|
-
const timeout = setTimeout(() => {
|
|
77
|
-
timedOut = true;
|
|
78
|
-
terminateSubagent(id);
|
|
79
|
-
subagent.status = 'failed';
|
|
80
|
-
subagent.error = 'Task timeout';
|
|
81
|
-
resolve({
|
|
82
|
-
subagentId: id,
|
|
83
|
-
success: false,
|
|
84
|
-
error: 'Task timeout',
|
|
85
|
-
});
|
|
86
|
-
}, timeoutMs);
|
|
87
|
-
tmuxCmd.on('error', (err) => {
|
|
88
|
-
clearTimeout(timeout);
|
|
89
|
-
subagent.status = 'failed';
|
|
90
|
-
subagent.error = String(err);
|
|
91
|
-
sessions.set(id, subagent);
|
|
92
|
-
resolve({ subagentId: id, success: false, error: String(err) });
|
|
93
|
-
});
|
|
94
|
-
tmuxCmd.on('exit', async (code) => {
|
|
95
|
-
clearTimeout(timeout);
|
|
96
|
-
subagent.completedAt = new Date().toISOString();
|
|
97
|
-
if (timedOut)
|
|
98
|
-
return;
|
|
99
|
-
if (code === 0) {
|
|
100
|
-
subagent.status = 'completed';
|
|
101
|
-
// Try to capture output
|
|
102
|
-
try {
|
|
103
|
-
const output = await captureTmuxOutput(subagent.sessionId);
|
|
104
|
-
subagent.result = output;
|
|
105
|
-
resolve({
|
|
106
|
-
subagentId: id,
|
|
107
|
-
success: true,
|
|
108
|
-
result: output,
|
|
109
|
-
duration: subagent.startedAt
|
|
110
|
-
? Date.now() - new Date(subagent.startedAt).getTime()
|
|
111
|
-
: undefined,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
resolve({
|
|
116
|
-
subagentId: id,
|
|
117
|
-
success: true,
|
|
118
|
-
duration: subagent.startedAt
|
|
119
|
-
? Date.now() - new Date(subagent.startedAt).getTime()
|
|
120
|
-
: undefined,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
subagent.status = 'failed';
|
|
126
|
-
subagent.error = `Exit code: ${code}`;
|
|
127
|
-
resolve({
|
|
128
|
-
subagentId: id,
|
|
129
|
-
success: false,
|
|
130
|
-
error: `Exit code: ${code}`,
|
|
131
|
-
duration: subagent.startedAt
|
|
132
|
-
? Date.now() - new Date(subagent.startedAt).getTime()
|
|
133
|
-
: undefined,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
sessions.set(id, subagent);
|
|
137
|
-
await saveSubagent(subagent);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Delegate a task to a new subagent and start it
|
|
143
|
-
*/
|
|
144
|
-
export async function delegateTask(task, command, options = {}) {
|
|
145
|
-
const subagent = await createSubagent(task, options);
|
|
146
|
-
return startSubagent(subagent.id, command, options);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Capture output from a tmux session
|
|
150
|
-
*/
|
|
151
|
-
async function captureTmuxOutput(sessionId) {
|
|
152
|
-
return new Promise((resolve, reject) => {
|
|
153
|
-
const capture = spawn('tmux', ['capture-pane', '-t', sessionId, '-p']);
|
|
154
|
-
let output = '';
|
|
155
|
-
capture.stdout.on('data', (data) => {
|
|
156
|
-
output += data.toString();
|
|
157
|
-
});
|
|
158
|
-
capture.on('error', reject);
|
|
159
|
-
capture.on('close', () => {
|
|
160
|
-
resolve(output.trim());
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Send input to a running subagent
|
|
166
|
-
*/
|
|
167
|
-
export async function sendToSubagent(id, input) {
|
|
168
|
-
const subagent = sessions.get(id);
|
|
169
|
-
if (!subagent) {
|
|
170
|
-
throw new Error('Subagent not found');
|
|
171
|
-
}
|
|
172
|
-
return new Promise((resolve, reject) => {
|
|
173
|
-
const tmux = spawn('tmux', ['send-keys', '-t', subagent.sessionId, input, 'Enter']);
|
|
174
|
-
tmux.on('error', reject);
|
|
175
|
-
tmux.on('close', () => resolve());
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Get subagent status
|
|
180
|
-
*/
|
|
181
|
-
export function getSubagent(id) {
|
|
182
|
-
return sessions.get(id);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* List all subagents
|
|
186
|
-
*/
|
|
187
|
-
export function listSubagents() {
|
|
188
|
-
return Array.from(sessions.values());
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* List running subagents
|
|
192
|
-
*/
|
|
193
|
-
export function listRunningSubagents() {
|
|
194
|
-
return Array.from(sessions.values()).filter((s) => s.status === 'running');
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Terminate a subagent
|
|
198
|
-
*/
|
|
199
|
-
export async function terminateSubagent(id) {
|
|
200
|
-
const subagent = sessions.get(id);
|
|
201
|
-
if (!subagent)
|
|
202
|
-
return;
|
|
203
|
-
// Kill tmux session
|
|
204
|
-
spawn('tmux', ['kill-session', '-t', subagent.sessionId]);
|
|
205
|
-
const tmuxProcess = tmuxSessions.get(id);
|
|
206
|
-
if (tmuxProcess) {
|
|
207
|
-
tmuxProcess.kill();
|
|
208
|
-
tmuxSessions.delete(id);
|
|
209
|
-
}
|
|
210
|
-
subagent.status = 'terminated';
|
|
211
|
-
subagent.completedAt = new Date().toISOString();
|
|
212
|
-
sessions.set(id, subagent);
|
|
213
|
-
await saveSubagent(subagent);
|
|
214
|
-
console.log(`[PiSubagents] Terminated subagent: ${id}`);
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Save subagent state to disk
|
|
218
|
-
*/
|
|
219
|
-
async function saveSubagent(subagent) {
|
|
220
|
-
try {
|
|
221
|
-
await fs.mkdir(STATE_DIR, { recursive: true });
|
|
222
|
-
await fs.writeFile(path.join(STATE_DIR, `${subagent.id}.json`), JSON.stringify(subagent, null, 2));
|
|
223
|
-
}
|
|
224
|
-
catch (e) {
|
|
225
|
-
console.warn('[PiSubagents] Failed to save subagent:', e);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Load subagents from disk
|
|
230
|
-
*/
|
|
231
|
-
export async function loadSubagents() {
|
|
232
|
-
try {
|
|
233
|
-
await fs.mkdir(STATE_DIR, { recursive: true });
|
|
234
|
-
const files = await fs.readdir(STATE_DIR);
|
|
235
|
-
for (const file of files) {
|
|
236
|
-
if (file.endsWith('.json')) {
|
|
237
|
-
const data = await fs.readFile(path.join(STATE_DIR, file), 'utf-8');
|
|
238
|
-
const subagent = JSON.parse(data);
|
|
239
|
-
sessions.set(subagent.id, subagent);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
console.log(`[PiSubagents] Loaded ${sessions.size} subagents`);
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Directory doesn't exist
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Get subagent statistics
|
|
250
|
-
*/
|
|
251
|
-
export function getStats() {
|
|
252
|
-
const all = Array.from(sessions.values());
|
|
253
|
-
return {
|
|
254
|
-
total: all.length,
|
|
255
|
-
running: all.filter((s) => s.status === 'running').length,
|
|
256
|
-
completed: all.filter((s) => s.status === 'completed').length,
|
|
257
|
-
failed: all.filter((s) => s.status === 'failed').length,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Clean up terminated subagents
|
|
262
|
-
*/
|
|
263
|
-
export async function cleanupSubagents() {
|
|
264
|
-
const terminated = Array.from(sessions.values()).filter((s) => s.status === 'terminated' || s.status === 'completed' || s.status === 'failed');
|
|
265
|
-
for (const subagent of terminated) {
|
|
266
|
-
try {
|
|
267
|
-
await fs.unlink(path.join(STATE_DIR, `${subagent.id}.json`));
|
|
268
|
-
sessions.delete(subagent.id);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
// File doesn't exist
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (terminated.length > 0) {
|
|
275
|
-
console.log(`[PiSubagents] Cleaned up ${terminated.length} subagents`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Execute a task in parallel using multiple subagents
|
|
280
|
-
*/
|
|
281
|
-
export async function parallelDelegate(tasks, commandFactory, options = {}) {
|
|
282
|
-
const promises = tasks.map((task) => delegateTask(task, commandFactory(task), options));
|
|
283
|
-
return Promise.all(promises);
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Split a long-form task across multiple subagents
|
|
287
|
-
* (e.g., writing and researching simultaneously)
|
|
288
|
-
*/
|
|
289
|
-
export async function splitTask(mainTask, subtasks, commandFactory, options = {}) {
|
|
290
|
-
const results = new Map();
|
|
291
|
-
// Create all subagents
|
|
292
|
-
const subagents = await Promise.all(subtasks.map((st) => createSubagent(`${mainTask}: ${st}`, options)));
|
|
293
|
-
// Start all in parallel
|
|
294
|
-
const promises = subagents.map((sa, i) => startSubagent(sa.id, commandFactory(subtasks[i]), options).then((r) => ({
|
|
295
|
-
subtask: subtasks[i],
|
|
296
|
-
result: r,
|
|
297
|
-
})));
|
|
298
|
-
const settled = await Promise.all(promises);
|
|
299
|
-
for (const { subtask, result } of settled) {
|
|
300
|
-
results.set(subtask, result);
|
|
301
|
-
}
|
|
302
|
-
return results;
|
|
303
|
-
}
|