agent-pool-mcp 1.7.1 → 1.7.3
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/README.md +15 -8
- package/package.json +1 -1
- package/src/runner/gemini-runner.js +2 -1
- package/src/tools/results.js +97 -1
package/README.md
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# agent-pool-mcp
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
> Developed by [RND-PRO](https://rnd-pro.com)
|
|
7
|
+
Multi-agent orchestration via [Gemini CLI](https://github.com/google-gemini/gemini-cli) — parallel task delegation, sequential pipelines, cron scheduling, and cross-model peer review.
|
|
10
8
|
|
|
11
9
|
Compatible with [Antigravity](https://antigravity.dev), Cursor, Windsurf, Claude Code, and any MCP-enabled coding agent.
|
|
12
10
|
|
|
@@ -185,12 +183,20 @@ See [parallel-work guide](examples/parallel-work.md) and built-in `orchestrator`
|
|
|
185
183
|
|
|
186
184
|
## MCP Ecosystem
|
|
187
185
|
|
|
188
|
-
Best used
|
|
186
|
+
Best used as part of [**mcp-agent-portal**](https://github.com/rnd-pro/mcp-agent-portal) — a unified MCP aggregator that bundles agent-pool, project-graph, and other servers behind a single config entry:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"mcpServers": {
|
|
191
|
+
"agent-portal": {
|
|
192
|
+
"command": "npx",
|
|
193
|
+
"args": ["-y", "mcp-agent-portal"]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
|-------|---------------|-------------------|
|
|
192
|
-
| **Primary IDE agent** | Delegates tasks, consults peer | Navigates codebase, runs analysis |
|
|
193
|
-
| **Gemini CLI workers** | Executes delegated tasks | Available as MCP tool inside workers |
|
|
199
|
+
Also works standalone alongside [**project-graph-mcp**](https://www.npmjs.com/package/project-graph-mcp) — AST-based codebase analysis:
|
|
194
200
|
|
|
195
201
|
```json
|
|
196
202
|
{
|
|
@@ -216,6 +222,7 @@ Best used together with [**project-graph-mcp**](https://www.npmjs.com/package/pr
|
|
|
216
222
|
- [examples/parallel-work.md](examples/parallel-work.md) — Delegation patterns and best practices
|
|
217
223
|
|
|
218
224
|
## Related Projects
|
|
225
|
+
- [mcp-agent-portal](https://github.com/rnd-pro/mcp-agent-portal) — Unified MCP aggregator + web dashboard + AI agent runtime
|
|
219
226
|
- [project-graph-mcp](https://github.com/rnd-pro/project-graph-mcp) — AST-based codebase analysis for AI agents
|
|
220
227
|
- [Symbiote.js](https://github.com/symbiotejs/symbiote.js) — Isomorphic Reactive Web Components framework
|
|
221
228
|
- [JSDA-Kit](https://github.com/rnd-pro/jsda-kit) — SSG/SSR toolkit for modern web applications
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ import { spawn, execFile } from 'node:child_process';
|
|
|
10
10
|
import { trackChild, killGroup, untrackChild } from './process-manager.js';
|
|
11
11
|
import { getRunner, loadConfig } from './config.js';
|
|
12
12
|
import { buildSshSpawn, parseRemotePid } from './ssh.js';
|
|
13
|
-
import { setTaskPid, updateTaskResult, pushTaskEvent } from '../tools/results.js';
|
|
13
|
+
import { setTaskPid, updateTaskResult, pushTaskEvent, pushTaskStderr } from '../tools/results.js';
|
|
14
14
|
|
|
15
15
|
const DEFAULT_TIMEOUT_SEC = 600;
|
|
16
16
|
const DEFAULT_APPROVAL_MODE = 'yolo';
|
|
@@ -160,6 +160,7 @@ export function runGeminiStreaming({ prompt, cwd, model, approvalMode, timeout,
|
|
|
160
160
|
|
|
161
161
|
child.stderr.on('data', (chunk) => {
|
|
162
162
|
stderrData += chunk.toString();
|
|
163
|
+
if (taskId) pushTaskStderr(taskId, chunk.toString());
|
|
163
164
|
});
|
|
164
165
|
|
|
165
166
|
child.on('close', (code) => {
|
package/src/tools/results.js
CHANGED
|
@@ -77,6 +77,8 @@ export function createTask(taskId, prompt, waitHint, approvalMode) {
|
|
|
77
77
|
waitHint: waitHint ?? null,
|
|
78
78
|
pid: null,
|
|
79
79
|
liveEvents: [],
|
|
80
|
+
lastEventAt: null,
|
|
81
|
+
stderr: '',
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -90,6 +92,7 @@ export function pushTaskEvent(taskId, event) {
|
|
|
90
92
|
const entry = taskStore.get(taskId);
|
|
91
93
|
if (entry && entry.status === 'running') {
|
|
92
94
|
entry.liveEvents.push(event);
|
|
95
|
+
entry.lastEventAt = Date.now();
|
|
93
96
|
// Ring buffer: keep only the last MAX_LIVE_EVENTS
|
|
94
97
|
if (entry.liveEvents.length > MAX_LIVE_EVENTS) {
|
|
95
98
|
entry.liveEvents = entry.liveEvents.slice(-MAX_LIVE_EVENTS);
|
|
@@ -97,6 +100,23 @@ export function pushTaskEvent(taskId, event) {
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Append stderr data to a task (for diagnostics).
|
|
105
|
+
*
|
|
106
|
+
* @param {string} taskId
|
|
107
|
+
* @param {string} chunk - stderr chunk
|
|
108
|
+
*/
|
|
109
|
+
export function pushTaskStderr(taskId, chunk) {
|
|
110
|
+
const entry = taskStore.get(taskId);
|
|
111
|
+
if (entry && entry.status === 'running') {
|
|
112
|
+
entry.stderr += chunk;
|
|
113
|
+
// Keep only last 2KB of stderr
|
|
114
|
+
if (entry.stderr.length > 2048) {
|
|
115
|
+
entry.stderr = entry.stderr.slice(-2048);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* Associate a PID with a task (called after spawn).
|
|
102
122
|
*
|
|
@@ -316,6 +336,68 @@ export function formatTaskResult(taskId) {
|
|
|
316
336
|
progress = '\n\n⏳ *Cold start — Gemini CLI initialization takes ~15-20s*';
|
|
317
337
|
}
|
|
318
338
|
|
|
339
|
+
// Time since last event — key diagnostic
|
|
340
|
+
let activityInfo = '';
|
|
341
|
+
if (entry.lastEventAt) {
|
|
342
|
+
const silentSec = ((Date.now() - entry.lastEventAt) / 1000).toFixed(0);
|
|
343
|
+
if (parseInt(silentSec) > 60) {
|
|
344
|
+
activityInfo = `\n⏱️ Last event ${silentSec}s ago — model may be thinking or rate-limited.`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Stderr diagnostics — parse rate limits, extract actionable info
|
|
349
|
+
let stderrInfo = '';
|
|
350
|
+
if (entry.stderr) {
|
|
351
|
+
const raw = entry.stderr;
|
|
352
|
+
|
|
353
|
+
// Count rate limit occurrences
|
|
354
|
+
const rateLimitCount = (raw.match(/429|Too Many Requests|RESOURCE_EXHAUSTED/gi) || []).length;
|
|
355
|
+
|
|
356
|
+
// Forward-compatible: extract retry delay if present in stderr.
|
|
357
|
+
// Currently Google SDK does NOT include retryDelayMs in console.error output,
|
|
358
|
+
// but if Gemini CLI or SDK adds it in the future, this parser will pick it up.
|
|
359
|
+
// Supported formats:
|
|
360
|
+
// retryDelay: '42s' (Google API RetryInfo)
|
|
361
|
+
// retryDelayMs: 42000 (Gemini CLI error object)
|
|
362
|
+
// Retry-After: 30 (HTTP header)
|
|
363
|
+
let retrySeconds = null;
|
|
364
|
+
const msMatch = raw.match(/retryDelayMs[:"'\s]*(\d+)/i);
|
|
365
|
+
const secMatch = raw.match(/retryDelay[:"'\s]*'?(\d+)s'?/i);
|
|
366
|
+
const headerMatch = raw.match(/Retry-After[:"'\s]*(\d+)/i);
|
|
367
|
+
if (msMatch) retrySeconds = Math.ceil(parseInt(msMatch[1]) / 1000);
|
|
368
|
+
else if (secMatch) retrySeconds = parseInt(secMatch[1]);
|
|
369
|
+
else if (headerMatch) retrySeconds = parseInt(headerMatch[1]);
|
|
370
|
+
|
|
371
|
+
if (rateLimitCount > 0) {
|
|
372
|
+
const parts = [`⚡ Rate limited (429 × ${rateLimitCount})`];
|
|
373
|
+
if (retrySeconds) {
|
|
374
|
+
const resetAt = new Date(Date.now() + retrySeconds * 1000);
|
|
375
|
+
const resetTime = resetAt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
376
|
+
parts.push(`resets ~${resetTime} (${retrySeconds}s)`);
|
|
377
|
+
}
|
|
378
|
+
stderrInfo = `\n${parts.join(' — ')}`;
|
|
379
|
+
} else {
|
|
380
|
+
// Show other errors (non-rate-limit)
|
|
381
|
+
const lines = raw.trim().split('\n')
|
|
382
|
+
.filter((l) => !l.includes('IDEClient') && !l.includes('YOLO mode') && !l.includes('Loaded cached') && l.trim())
|
|
383
|
+
.slice(-3);
|
|
384
|
+
if (lines.length > 0) {
|
|
385
|
+
stderrInfo = `\n📋 stderr: ${lines.join(' | ').substring(0, 200)}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// PID alive check
|
|
391
|
+
let pidStatus = '';
|
|
392
|
+
if (entry.pid) {
|
|
393
|
+
try {
|
|
394
|
+
process.kill(entry.pid, 0); // signal 0 = check alive
|
|
395
|
+
pidStatus = ' (alive)';
|
|
396
|
+
} catch {
|
|
397
|
+
pidStatus = ' (dead ⚠️)';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
319
401
|
// System load awareness during polling
|
|
320
402
|
const load = getSystemLoad();
|
|
321
403
|
const loadInfo = load.warning ? `\n\n${load.warning}` : '';
|
|
@@ -325,7 +407,7 @@ export function formatTaskResult(taskId) {
|
|
|
325
407
|
return {
|
|
326
408
|
content: [{
|
|
327
409
|
type: 'text',
|
|
328
|
-
text: `⏳ Task is still running (${elapsed}s elapsed, ${entry.liveEvents.length} events).\n\n- **Prompt**: ${entry.prompt.substring(0, 100)}...\n- **Mode**: ${modeLabel}${progress}\n\n💡 **${hint}**${loadInfo}\n\nCheck again later with \`get_task_result\`.`,
|
|
410
|
+
text: `⏳ Task is still running (${elapsed}s elapsed, ${entry.liveEvents.length} events).\n\n- **Prompt**: ${entry.prompt.substring(0, 100)}...\n- **Mode**: ${modeLabel}\n- **PID**: ${entry.pid ?? 'unknown'}${pidStatus}${activityInfo}${stderrInfo}${progress}\n\n💡 **${hint}**${loadInfo}\n\nCheck again later with \`get_task_result\`.`,
|
|
329
411
|
}],
|
|
330
412
|
};
|
|
331
413
|
}
|
|
@@ -371,6 +453,20 @@ export function formatTaskResult(taskId) {
|
|
|
371
453
|
if (result.errors?.length > 0) {
|
|
372
454
|
sections.push(`### Errors\n\n${result.errors.join('\n')}`);
|
|
373
455
|
}
|
|
456
|
+
// Include stderr diagnostics (rate limits, etc.)
|
|
457
|
+
if (entry.stderr) {
|
|
458
|
+
const rateLimitCount = (entry.stderr.match(/429|Too Many Requests|RESOURCE_EXHAUSTED/gi) || []).length;
|
|
459
|
+
if (rateLimitCount > 0) {
|
|
460
|
+
sections.push(`### Cause: Rate Limit\n\n⚡ **${rateLimitCount} rate limit errors (429)** during execution. Task failed because API quota was exhausted.\n\n💡 Wait a few minutes for quota to reset, then retry.`);
|
|
461
|
+
} else {
|
|
462
|
+
const stderrLines = entry.stderr.trim().split('\n')
|
|
463
|
+
.filter((l) => !l.includes('IDEClient') && !l.includes('YOLO mode') && !l.includes('Loaded cached') && l.trim())
|
|
464
|
+
.slice(-5);
|
|
465
|
+
if (stderrLines.length > 0) {
|
|
466
|
+
sections.push(`### Stderr\n\n\`\`\`\n${stderrLines.join('\n')}\n\`\`\``);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
374
470
|
const errorSignal = (result.errors ?? []).join(' ');
|
|
375
471
|
sections.push(`### Recovery\n\n${classifyError(errorSignal || `exit code ${result.exitCode}`)}`);
|
|
376
472
|
}
|