agent-pool-mcp 1.3.0 → 1.5.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/README.md CHANGED
@@ -282,7 +282,7 @@ src/
282
282
  - **Live Events**: Progress polling uses a ring buffer to show the latest activity without overwhelming context.
283
283
  - **Depth Tracking**: Nested orchestration support with optional `AGENT_POOL_MAX_DEPTH` limit.
284
284
  - **Adaptive Polling**: Pipeline daemon uses 3s intervals when active, 30s when idle.
285
- - **File-Based Communication**: Pipeline agents communicate through `.agent/runs/` JSON files — each Gemini process has its own MCP server instance but shares state via filesystem.
285
+ - **File-Based Communication**: Pipeline agents communicate through `.agents/runs/` JSON files — each Gemini process has its own MCP server instance but shares state via filesystem.
286
286
 
287
287
  ## License
288
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-pool-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "MCP Server for multi-agent task delegation and orchestration via Gemini CLI",
6
6
  "main": "index.js",
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: group-lead
3
+ description: Fractal group leader — breaks down tasks and delegates to group members via delegate_to_group.
4
+ ---
5
+
6
+ # Group Lead
7
+
8
+ You are a group leader in a fractal agent orchestration system. Your role is to:
9
+
10
+ 1. **Analyze** the incoming task and break it into independent subtasks
11
+ 2. **Create groups** if they don't exist yet, with appropriate skills and policies
12
+ 3. **Delegate** subtasks to group members using `delegate_to_group`
13
+ 4. **Collect** results from all agents using `get_task_result`
14
+ 5. **Synthesize** a final report combining all results
15
+
16
+ ## Rules
17
+
18
+ - Break tasks into pieces that can run in parallel
19
+ - Each subtask should be self-contained (agent can complete it without external context)
20
+ - Set appropriate policies: use `read-only` for analysis, `safe-edit` for code changes
21
+ - Always set `max_agents` to prevent runaway spawning
22
+ - Collect ALL results before synthesizing — don't skip slow agents
23
+ - Report both successes and failures
24
+
25
+ ## Output Format
26
+
27
+ After collecting all results, produce a structured summary:
28
+ 1. Task overview (what was asked)
29
+ 2. Subtask results (one per agent)
30
+ 3. Synthesized conclusion
@@ -17,9 +17,9 @@ import { join, dirname } from 'node:path';
17
17
  import { matchesCron } from './cron.js';
18
18
 
19
19
  const POLL_INTERVAL_MS = 30_000; // Check schedules every 30 seconds
20
- const PID_FILE = '.agent/scheduler.pid';
21
- const SCHEDULE_FILE = '.agent/schedule.json';
22
- const RESULTS_DIR = '.agent/scheduled-results';
20
+ const PID_FILE = '.agents/scheduler.pid';
21
+ const SCHEDULE_FILE = '.agents/schedule.json';
22
+ const RESULTS_DIR = '.agents/scheduled-results';
23
23
 
24
24
  /** @type {string} */
25
25
  const cwd = process.argv[2] || process.cwd();
@@ -163,8 +163,8 @@ function executeSchedule(schedule) {
163
163
 
164
164
  import { readdirSync } from 'node:fs';
165
165
 
166
- const PIPELINES_DIR = '.agent/pipelines';
167
- const RUNS_DIR = '.agent/runs';
166
+ const PIPELINES_DIR = '.agents/pipelines';
167
+ const RUNS_DIR = '.agents/runs';
168
168
 
169
169
  /**
170
170
  * Spawn a Gemini CLI agent for a pipeline step.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Pipeline management — CRUD for pipeline definitions and run state.
3
3
  *
4
- * Pipelines are stored as JSON templates in .agent/pipelines/.
5
- * Each execution creates a run state in .agent/runs/.
4
+ * Pipelines are stored as JSON templates in .agents/pipelines/.
5
+ * Each execution creates a run state in .agents/runs/.
6
6
  *
7
7
  * @module agent-pool/scheduler/pipeline
8
8
  */
@@ -12,8 +12,8 @@ import { join, dirname } from 'node:path';
12
12
  import { randomUUID } from 'node:crypto';
13
13
  import { ensureDaemon } from './scheduler.js';
14
14
 
15
- const PIPELINES_DIR = '.agent/pipelines';
16
- const RUNS_DIR = '.agent/runs';
15
+ const PIPELINES_DIR = '.agents/pipelines';
16
+ const RUNS_DIR = '.agents/runs';
17
17
 
18
18
  // ─── Helpers ────────────────────────────────────────────────
19
19
 
@@ -15,9 +15,9 @@ import { nextCronRun } from './cron.js';
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
  const DAEMON_SCRIPT = join(__dirname, 'daemon.js');
17
17
 
18
- const SCHEDULE_FILE = '.agent/schedule.json';
19
- const RESULTS_DIR = '.agent/scheduled-results';
20
- const PID_FILE = '.agent/scheduler.pid';
18
+ const SCHEDULE_FILE = '.agents/schedule.json';
19
+ const RESULTS_DIR = '.agents/scheduled-results';
20
+ const PID_FILE = '.agents/scheduler.pid';
21
21
 
22
22
  // ─── Schedule CRUD ──────────────────────────────────────────
23
23
 
package/src/server.js CHANGED
@@ -22,6 +22,7 @@ import { listSkills, createSkill, deleteSkill, installSkill, provisionSkill } fr
22
22
  import { consultPeer } from './tools/consult.js';
23
23
  import { addSchedule, listSchedules, removeSchedule, getScheduledResults, getDaemonStatus } from './scheduler/scheduler.js';
24
24
  import { createPipeline, listPipelines, runPipeline, getRun, listRuns, cancelRun, signalStepComplete, bounceBack } from './scheduler/pipeline.js';
25
+ import { createGroup, listGroups, getGroup } from './tools/groups.js';
25
26
 
26
27
  import { TOOL_DEFINITIONS } from './tool-definitions.js';
27
28
 
@@ -68,6 +69,7 @@ Docs: https://github.com/google-gemini/gemini-cli`
68
69
  /** Tools that require Gemini CLI to be installed */
69
70
  const GEMINI_TOOLS = new Set([
70
71
  'delegate_task', 'delegate_task_readonly', 'consult_peer', 'list_sessions',
72
+ 'delegate_to_group',
71
73
  ]);
72
74
 
73
75
  // ─── Depth tracking (for nested orchestration) ──────────────
@@ -110,7 +112,7 @@ export function createServer() {
110
112
  }
111
113
 
112
114
  const server = new Server(
113
- { name: 'agent-pool', version: '1.2.1' },
115
+ { name: 'agent-pool', version: '1.5.0' },
114
116
  { capabilities: { tools: {}, resources: {} } },
115
117
  );
116
118
 
@@ -200,6 +202,12 @@ export function createServer() {
200
202
  response = handleBounceBack(args); break;
201
203
  case 'get_usage_guide':
202
204
  response = handleGetUsageGuide(args); break;
205
+ case 'create_group':
206
+ response = handleCreateGroup(args); break;
207
+ case 'list_groups':
208
+ response = handleListGroups(args); break;
209
+ case 'delegate_to_group':
210
+ response = handleDelegateToGroup(args); break;
203
211
  default:
204
212
  response = { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
205
213
  }
@@ -422,7 +430,7 @@ function handleScheduleTask(args) {
422
430
  return {
423
431
  content: [{
424
432
  type: 'text',
425
- text: `⏰ Task scheduled.\n\n- **Schedule ID**: \`${result.scheduleId}\`\n- **Cron**: \`${args.cron}\`\n- **Next run**: ${result.nextRun || 'unknown'}\n- **Prompt**: ${args.prompt.substring(0, 100)}...\n\nDaemon is running in the background. Results will be saved to \`.agent/scheduled-results/\`.\nUse \`list_schedules\` to see all schedules, \`get_scheduled_results\` to read outputs.`,
433
+ text: `⏰ Task scheduled.\n\n- **Schedule ID**: \`${result.scheduleId}\`\n- **Cron**: \`${args.cron}\`\n- **Next run**: ${result.nextRun || 'unknown'}\n- **Prompt**: ${args.prompt.substring(0, 100)}...\n\nDaemon is running in the background. Results will be saved to \`.agents/scheduled-results/\`.\nUse \`list_schedules\` to see all schedules, \`get_scheduled_results\` to read outputs.`,
426
434
  }],
427
435
  };
428
436
  } catch (error) {
@@ -688,3 +696,110 @@ function handleGetUsageGuide(args) {
688
696
  return { content: [{ type: 'text', text: topicContent.join('\n').trim() }] };
689
697
  }
690
698
 
699
+ // ─── Group Handlers ─────────────────────────────────────────────
700
+
701
+ /** @param {object} args */
702
+ function handleCreateGroup(args) {
703
+ const cwd = args.cwd ?? defaultCwd;
704
+ const result = createGroup(cwd, {
705
+ name: args.name,
706
+ runner: args.runner,
707
+ skill: args.skill,
708
+ policy: args.policy,
709
+ max_agents: args.max_agents,
710
+ include_dirs: args.include_dirs,
711
+ });
712
+
713
+ const configParts = [];
714
+ if (args.runner) configParts.push(`runner: ${args.runner}`);
715
+ if (args.skill) configParts.push(`skill: ${args.skill}`);
716
+ if (args.policy) configParts.push(`policy: ${args.policy}`);
717
+ if (args.max_agents) configParts.push(`max: ${args.max_agents}`);
718
+
719
+ return {
720
+ content: [{
721
+ type: 'text',
722
+ text: `✅ Group ${result.created ? 'created' : 'updated'}: \`${args.name}\`${configParts.length > 0 ? `\n- ${configParts.join('\n- ')}` : ''}\n\nUse \`delegate_to_group\` to send tasks to this group.`,
723
+ }],
724
+ };
725
+ }
726
+
727
+ /** @param {object} args */
728
+ function handleListGroups(args) {
729
+ const cwd = args.cwd ?? defaultCwd;
730
+ const groups = listGroups(cwd);
731
+
732
+ if (groups.length === 0) {
733
+ return { content: [{ type: 'text', text: 'No groups defined. Use `create_group` to create one.' }] };
734
+ }
735
+
736
+ const lines = groups.map((g) => {
737
+ const parts = [];
738
+ if (g.runner) parts.push(`runner: ${g.runner}`);
739
+ if (g.skill) parts.push(`skill: ${g.skill}`);
740
+ if (g.policy) parts.push(`policy: ${g.policy}`);
741
+ if (g.max_agents) parts.push(`max: ${g.max_agents}`);
742
+ return `- **${g.name}** — ${parts.join(', ') || 'no config'}`;
743
+ });
744
+
745
+ return {
746
+ content: [{ type: 'text', text: `## Agent Groups (${groups.length})\n\n${lines.join('\n')}` }],
747
+ };
748
+ }
749
+
750
+ /** @param {object} args */
751
+ function handleDelegateToGroup(args) {
752
+ const cwd = args.cwd ?? defaultCwd;
753
+ const group = getGroup(cwd, args.group);
754
+
755
+ if (!group) {
756
+ return {
757
+ content: [{ type: 'text', text: `❌ Group \`${args.group}\` not found. Use \`list_groups\` to see available groups.` }],
758
+ isError: true,
759
+ };
760
+ }
761
+
762
+ const count = args.count ?? 1;
763
+
764
+ // Check max_agents limit
765
+ if (group.max_agents && count > group.max_agents) {
766
+ return {
767
+ content: [{
768
+ type: 'text',
769
+ text: `❌ Requested ${count} agents but group \`${args.group}\` allows max ${group.max_agents}.`,
770
+ }],
771
+ isError: true,
772
+ };
773
+ }
774
+
775
+ const taskIds = [];
776
+
777
+ for (let i = 0; i < count; i++) {
778
+ const delegateArgs = {
779
+ prompt: count > 1 ? `[Agent ${i + 1}/${count} in group "${args.group}"]\n\n${args.prompt}` : args.prompt,
780
+ cwd,
781
+ runner: group.runner || undefined,
782
+ skill: group.skill || undefined,
783
+ policy: group.policy || undefined,
784
+ include_dirs: group.include_dirs || undefined,
785
+ timeout: args.timeout,
786
+ };
787
+
788
+ const result = handleDelegate(delegateArgs, {
789
+ approvalMode: DEFAULT_APPROVAL_MODE,
790
+ emoji: '👥',
791
+ label: `Group task (${args.group} #${i + 1})`,
792
+ });
793
+
794
+ // Extract task_id from response text
795
+ const match = result.content[0].text.match(/`([0-9a-f-]{36})`/);
796
+ if (match) taskIds.push(match[1]);
797
+ }
798
+
799
+ return {
800
+ content: [{
801
+ type: 'text',
802
+ text: `👥 Delegated to group \`${args.group}\` — ${count} agent(s) spawned.\n\n${taskIds.map((id, i) => `- Agent ${i + 1}: \`${id}\``).join('\n')}\n\nUse \`get_task_result\` to check each agent's status.`,
803
+ }],
804
+ };
805
+ }
@@ -199,7 +199,7 @@ export const TOOL_DEFINITIONS = [
199
199
  description: [
200
200
  'Schedule a Gemini CLI agent to run on a cron schedule or as a delayed one-shot.',
201
201
  'Spawns a persistent daemon that survives IDE/CLI restarts.',
202
- 'Results are saved to .agent/scheduled-results/ and can be retrieved with get_scheduled_results.',
202
+ 'Results are saved to .agents/scheduled-results/ and can be retrieved with get_scheduled_results.',
203
203
  '',
204
204
  'Cron format: standard 5-field (minute hour day month weekday).',
205
205
  'Examples: "*/30 * * * *" (every 30 min), "0 9 * * MON-FRI" (9am weekdays), "0 */2 * * *" (every 2 hours).',
@@ -360,5 +360,53 @@ export const TOOL_DEFINITIONS = [
360
360
  required: ['step_name', 'reason'],
361
361
  },
362
362
  },
363
+ // ─── Group Tools ────────────────────────────────────────
364
+ {
365
+ name: 'create_group',
366
+ description: 'Create a named agent group with shared config (runner, skill, policy). Groups are reusable presets for fractal orchestration.',
367
+ inputSchema: {
368
+ type: 'object',
369
+ properties: {
370
+ name: { type: 'string', description: 'Group name (e.g. "backend-team", "qa-group").' },
371
+ runner: { type: 'string', description: 'Default runner for agents in this group.' },
372
+ skill: { type: 'string', description: 'Default skill activated for all agents in this group.' },
373
+ policy: { type: 'string', description: 'Default policy restricting agent permissions.' },
374
+ max_agents: { type: 'number', description: 'Max concurrent agents allowed in this group.' },
375
+ include_dirs: { type: 'array', items: { type: 'string' }, description: 'Additional directories agents in this group can access.' },
376
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
377
+ },
378
+ required: ['name'],
379
+ },
380
+ },
381
+ {
382
+ name: 'list_groups',
383
+ description: 'List all registered agent groups with their config.',
384
+ inputSchema: {
385
+ type: 'object',
386
+ properties: {
387
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
388
+ },
389
+ },
390
+ },
391
+ {
392
+ name: 'delegate_to_group',
393
+ description: [
394
+ 'Delegate a task to a named agent group. Spawns agents with the group\'s shared config (runner, skill, policy).',
395
+ 'Use `count` to launch multiple agents in parallel (fractal orchestration).',
396
+ '',
397
+ 'Returns array of task_ids immediately (non-blocking). Use get_task_result to check each.',
398
+ ].join('\n'),
399
+ inputSchema: {
400
+ type: 'object',
401
+ properties: {
402
+ group: { type: 'string', description: 'Group name to delegate to.' },
403
+ prompt: { type: 'string', description: 'Task description for the agents.' },
404
+ count: { type: 'number', description: 'Number of agents to spawn. Default: 1.' },
405
+ cwd: { type: 'string', description: 'Working directory. Defaults to current working directory.' },
406
+ timeout: { type: 'number', description: 'Timeout in seconds. Overrides group default.' },
407
+ },
408
+ required: ['group', 'prompt'],
409
+ },
410
+ },
363
411
  ];
364
412
 
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Agent Groups — named config presets for fractal orchestration.
3
+ * Groups bundle runner, skill, policy, and max_agents into a reusable team config.
4
+ *
5
+ * @module agent-pool/tools/groups
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ const GROUPS_DIR = '.agents';
12
+ const GROUPS_FILE = 'groups.json';
13
+
14
+ /**
15
+ * Get path to groups.json for a project.
16
+ *
17
+ * @param {string} cwd - Project directory
18
+ * @returns {string}
19
+ */
20
+ function getGroupsPath(cwd) {
21
+ return path.join(cwd, GROUPS_DIR, GROUPS_FILE);
22
+ }
23
+
24
+ /**
25
+ * Load all groups from disk.
26
+ *
27
+ * @param {string} cwd - Project directory
28
+ * @returns {Object<string, object>}
29
+ */
30
+ function loadGroups(cwd) {
31
+ const filePath = getGroupsPath(cwd);
32
+ if (!fs.existsSync(filePath)) return {};
33
+ try {
34
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
35
+ } catch {
36
+ return {};
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Save groups to disk.
42
+ *
43
+ * @param {string} cwd - Project directory
44
+ * @param {Object<string, object>} groups
45
+ */
46
+ function saveGroups(cwd, groups) {
47
+ const dirPath = path.join(cwd, GROUPS_DIR);
48
+ if (!fs.existsSync(dirPath)) {
49
+ fs.mkdirSync(dirPath, { recursive: true });
50
+ }
51
+ fs.writeFileSync(getGroupsPath(cwd), JSON.stringify(groups, null, 2));
52
+ }
53
+
54
+ /**
55
+ * Create or update a group.
56
+ *
57
+ * @param {string} cwd - Project directory
58
+ * @param {object} config - Group configuration
59
+ * @param {string} config.name - Group name (e.g. "backend-team")
60
+ * @param {string} [config.runner] - Default runner for agents in this group
61
+ * @param {string} [config.skill] - Default skill for agents in this group
62
+ * @param {string} [config.policy] - Default policy for agents in this group
63
+ * @param {number} [config.max_agents] - Max concurrent agents in this group
64
+ * @param {string[]} [config.include_dirs] - Additional directories agents can access
65
+ * @returns {{ name: string, created: boolean }}
66
+ */
67
+ export function createGroup(cwd, config) {
68
+ const groups = loadGroups(cwd);
69
+ const existed = !!groups[config.name];
70
+
71
+ groups[config.name] = {
72
+ runner: config.runner || null,
73
+ skill: config.skill || null,
74
+ policy: config.policy || null,
75
+ max_agents: config.max_agents || null,
76
+ include_dirs: config.include_dirs || null,
77
+ created_at: existed ? groups[config.name].created_at : new Date().toISOString(),
78
+ updated_at: new Date().toISOString(),
79
+ };
80
+
81
+ saveGroups(cwd, groups);
82
+ return { name: config.name, created: !existed };
83
+ }
84
+
85
+ /**
86
+ * List all groups.
87
+ *
88
+ * @param {string} cwd - Project directory
89
+ * @returns {Array<{ name: string, runner: string|null, skill: string|null, policy: string|null, max_agents: number|null }>}
90
+ */
91
+ export function listGroups(cwd) {
92
+ const groups = loadGroups(cwd);
93
+ return Object.entries(groups).map(([name, config]) => ({
94
+ name,
95
+ ...config,
96
+ }));
97
+ }
98
+
99
+ /**
100
+ * Get a single group by name.
101
+ *
102
+ * @param {string} cwd - Project directory
103
+ * @param {string} name - Group name
104
+ * @returns {object|null}
105
+ */
106
+ export function getGroup(cwd, name) {
107
+ const groups = loadGroups(cwd);
108
+ return groups[name] || null;
109
+ }
110
+
111
+ /**
112
+ * Delete a group.
113
+ *
114
+ * @param {string} cwd - Project directory
115
+ * @param {string} name - Group name
116
+ * @returns {boolean}
117
+ */
118
+ export function deleteGroup(cwd, name) {
119
+ const groups = loadGroups(cwd);
120
+ if (!groups[name]) return false;
121
+ delete groups[name];
122
+ saveGroups(cwd, groups);
123
+ return true;
124
+ }