UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

729 lines 33.5 kB
/** * Coordination MCP Tools for CLI * * V2 Compatibility - Swarm coordination and orchestration tools * * ⚠️ IMPORTANT: These tools provide LOCAL STATE MANAGEMENT. * - Topology/consensus state is tracked locally * - No actual distributed coordination * - Useful for single-machine workflow orchestration */ import { getProjectCwd } from './types.js'; import { validateIdentifier, validateText } from './validate-input.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; // Storage paths const STORAGE_DIR = '.claude-flow'; const COORD_DIR = 'coordination'; const COORD_FILE = 'store.json'; function getCoordDir() { return join(getProjectCwd(), STORAGE_DIR, COORD_DIR); } function getCoordPath() { return join(getCoordDir(), COORD_FILE); } function ensureCoordDir() { const dir = getCoordDir(); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } function loadCoordStore() { try { const path = getCoordPath(); if (existsSync(path)) { return JSON.parse(readFileSync(path, 'utf-8')); } } catch { // Return default store } return { topology: { type: 'hierarchical', maxNodes: 15, redundancy: 2, consensusAlgorithm: 'raft', }, loadBalance: { algorithm: 'adaptive', weights: {}, healthCheck: true, }, sync: { lastSync: new Date().toISOString(), syncCount: 0, conflicts: 0, pendingChanges: 0, }, nodes: {}, version: '3.0.0', }; } function saveCoordStore(store) { ensureCoordDir(); writeFileSync(getCoordPath(), JSON.stringify(store, null, 2), 'utf-8'); } export const coordinationTools = [ { name: 'coordination_topology', description: 'Configure swarm topology Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'set', 'optimize'], description: 'Action to perform' }, type: { type: 'string', enum: ['mesh', 'hierarchical', 'ring', 'star', 'hybrid', 'hierarchical-mesh'], description: 'Topology type' }, maxNodes: { type: 'number', description: 'Maximum nodes' }, redundancy: { type: 'number', description: 'Redundancy level' }, consensusAlgorithm: { type: 'string', enum: ['raft', 'byzantine', 'gossip', 'crdt'], description: 'Consensus algorithm' }, }, }, handler: async (input) => { const store = loadCoordStore(); const action = input.action || 'get'; if (action === 'get') { return { success: true, topology: store.topology, nodes: Object.keys(store.nodes).length, status: 'active', }; } if (action === 'set') { if (input.type) store.topology.type = input.type; if (input.maxNodes) store.topology.maxNodes = input.maxNodes; if (input.redundancy) store.topology.redundancy = input.redundancy; if (input.consensusAlgorithm) store.topology.consensusAlgorithm = input.consensusAlgorithm; saveCoordStore(store); return { success: true, action: 'updated', topology: store.topology, }; } if (action === 'optimize') { // Analyze current state and suggest optimal topology const nodeCount = Object.keys(store.nodes).length; let recommended = 'hierarchical'; if (nodeCount <= 5) { recommended = 'mesh'; } else if (nodeCount <= 15) { recommended = 'hierarchical'; } else { recommended = 'hybrid'; } return { success: true, action: 'optimize', current: store.topology.type, recommended, reason: nodeCount <= 5 ? 'Small cluster benefits from full mesh connectivity' : nodeCount <= 15 ? 'Medium cluster works well with hierarchical coordination' : 'Large cluster needs hybrid approach for scalability', }; } return { success: false, error: 'Unknown action' }; }, }, { name: 'coordination_load_balance', description: 'Configure load balancing Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'set', 'distribute'], description: 'Action to perform' }, algorithm: { type: 'string', enum: ['round-robin', 'least-connections', 'weighted', 'adaptive'], description: 'Algorithm' }, weights: { type: 'object', description: 'Node weights' }, task: { type: 'string', description: 'Task to distribute' }, }, }, handler: async (input) => { if (input.task) { const vTask = validateText(input.task, 'task'); if (!vTask.valid) return { success: false, error: vTask.error }; } const store = loadCoordStore(); const action = input.action || 'get'; if (action === 'get') { const nodes = Object.values(store.nodes); const avgLoad = nodes.length > 0 ? nodes.reduce((sum, n) => sum + n.load, 0) / nodes.length : 0; return { success: true, loadBalance: store.loadBalance, metrics: { nodeCount: nodes.length, avgLoad, maxLoad: nodes.length > 0 ? Math.max(...nodes.map(n => n.load)) : 0, minLoad: nodes.length > 0 ? Math.min(...nodes.map(n => n.load)) : 0, }, }; } if (action === 'set') { if (input.algorithm) store.loadBalance.algorithm = input.algorithm; if (input.weights) store.loadBalance.weights = input.weights; saveCoordStore(store); return { success: true, action: 'updated', loadBalance: store.loadBalance, }; } if (action === 'distribute') { const task = input.task; const nodes = Object.values(store.nodes).filter(n => n.status === 'active'); if (nodes.length === 0) { return { success: false, error: 'No active nodes available' }; } // Select node based on algorithm let selectedNode; const algorithm = store.loadBalance.algorithm; if (algorithm === 'least-connections' || algorithm === 'adaptive') { selectedNode = nodes.reduce((min, n) => n.load < min.load ? n : min); } else if (algorithm === 'weighted') { const weights = store.loadBalance.weights; selectedNode = nodes.reduce((max, n) => (weights[n.id] || 1) > (weights[max.id] || 1) ? n : max); } else { // Round robin - just pick first active selectedNode = nodes[0]; } // Update load selectedNode.load += 1; saveCoordStore(store); return { success: true, action: 'distributed', task, assignedTo: selectedNode.id, algorithm, nodeLoad: selectedNode.load, }; } return { success: false, error: 'Unknown action' }; }, }, { name: 'coordination_sync', description: 'Synchronize state across nodes Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['status', 'trigger', 'resolve'], description: 'Action to perform' }, force: { type: 'boolean', description: 'Force synchronization' }, conflictResolution: { type: 'string', enum: ['latest', 'merge', 'manual'], description: 'Conflict resolution strategy' }, }, }, handler: async (input) => { const store = loadCoordStore(); const action = input.action || 'status'; if (action === 'status') { const timeSinceSync = Date.now() - new Date(store.sync.lastSync).getTime(); return { success: true, sync: store.sync, timeSinceSync: `${Math.floor(timeSinceSync / 1000)}s`, status: store.sync.conflicts > 0 ? 'conflicts' : store.sync.pendingChanges > 0 ? 'pending' : 'synced', }; } if (action === 'trigger') { store.sync.syncCount++; store.sync.lastSync = new Date().toISOString(); store.sync.pendingChanges = 0; // Simulate sync await new Promise(resolve => setTimeout(resolve, 50)); saveCoordStore(store); return { success: true, action: 'synchronized', syncCount: store.sync.syncCount, syncedAt: store.sync.lastSync, nodesSync: Object.keys(store.nodes).length, }; } if (action === 'resolve') { const strategy = input.conflictResolution || 'latest'; if (store.sync.conflicts > 0) { const resolved = store.sync.conflicts; store.sync.conflicts = 0; saveCoordStore(store); return { success: true, action: 'resolved', strategy, conflictsResolved: resolved, }; } return { success: true, action: 'resolve', message: 'No conflicts to resolve', }; } return { success: false, error: 'Unknown action' }; }, }, { name: 'coordination_node', description: 'Manage coordination nodes Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'add', 'remove', 'heartbeat'], description: 'Action to perform' }, nodeId: { type: 'string', description: 'Node ID' }, status: { type: 'string', description: 'Node status' }, }, }, handler: async (input) => { if (input.nodeId) { const vNode = validateIdentifier(input.nodeId, 'nodeId'); if (!vNode.valid) return { success: false, error: vNode.error }; } const store = loadCoordStore(); const action = input.action || 'list'; if (action === 'list') { const nodes = Object.values(store.nodes); return { success: true, nodes: nodes.map(n => ({ id: n.id, status: n.status, load: n.load, lastHeartbeat: n.lastHeartbeat, })), total: nodes.length, active: nodes.filter(n => n.status === 'active').length, }; } if (action === 'add') { const nodeId = input.nodeId || `node-${Date.now()}`; store.nodes[nodeId] = { id: nodeId, status: 'active', load: 0, lastHeartbeat: new Date().toISOString(), }; saveCoordStore(store); return { success: true, action: 'added', nodeId, totalNodes: Object.keys(store.nodes).length, }; } if (action === 'remove') { const nodeId = input.nodeId; if (!store.nodes[nodeId]) { return { success: false, error: 'Node not found' }; } delete store.nodes[nodeId]; saveCoordStore(store); return { success: true, action: 'removed', nodeId, totalNodes: Object.keys(store.nodes).length, }; } if (action === 'heartbeat') { const nodeId = input.nodeId; if (store.nodes[nodeId]) { store.nodes[nodeId].lastHeartbeat = new Date().toISOString(); store.nodes[nodeId].status = 'active'; saveCoordStore(store); } return { success: true, action: 'heartbeat', nodeId, timestamp: new Date().toISOString(), }; } return { success: false, error: 'Unknown action' }; }, }, { name: 'coordination_consensus', description: 'Manage consensus protocol with BFT, Raft, or Quorum strategies Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['status', 'propose', 'vote', 'commit'], description: 'Action to perform' }, proposal: { type: 'object', description: 'Proposal data (for propose)' }, proposalId: { type: 'string', description: 'Proposal ID (for vote/commit/status)' }, vote: { type: 'string', enum: ['accept', 'reject'], description: 'Vote' }, voterId: { type: 'string', description: 'Voter node ID' }, strategy: { type: 'string', enum: ['bft', 'raft', 'quorum'], description: 'Consensus strategy (default: raft)' }, quorumPreset: { type: 'string', enum: ['unanimous', 'majority', 'supermajority'], description: 'Quorum threshold preset (default: majority)' }, term: { type: 'number', description: 'Term number (for raft strategy)' }, }, }, handler: async (input) => { if (input.proposalId) { const vProp = validateIdentifier(input.proposalId, 'proposalId'); if (!vProp.valid) return { success: false, error: vProp.error }; } if (input.voterId) { const vVoter = validateIdentifier(input.voterId, 'voterId'); if (!vVoter.valid) return { success: false, error: vVoter.error }; } const store = loadCoordStore(); const action = input.action || 'status'; const strategy = input.strategy || 'raft'; const nodeCount = Object.keys(store.nodes).length || 1; // Initialize consensus storage in the coordination store if missing if (!store.consensus) { store.consensus = { pending: [], history: [] }; } const consensus = store.consensus; function calcRequired(strat, total, preset) { if (total <= 0) return 1; if (strat === 'bft') return Math.floor((total * 2) / 3) + 1; if (strat === 'quorum') { if (preset === 'unanimous') return total; if (preset === 'supermajority') return Math.floor((total * 2) / 3) + 1; } return Math.floor(total / 2) + 1; } if (action === 'status') { if (input.proposalId) { // Status for specific proposal const p = consensus.pending.find(x => x.proposalId === input.proposalId); if (p) { const votesFor = Object.values(p.votes).filter(v => v).length; const votesAgainst = Object.values(p.votes).filter(v => !v).length; return { success: true, proposalId: p.proposalId, strategy: p.strategy, status: p.status, votesFor, votesAgainst, required: calcRequired(p.strategy, nodeCount, p.quorumPreset), totalNodes: nodeCount, resolved: false, }; } const h = consensus.history.find(x => x.proposalId === input.proposalId); if (h) return { success: true, ...h, resolved: true, historical: true }; return { success: false, error: 'Proposal not found' }; } const quorum = calcRequired(strategy, nodeCount); return { success: true, algorithm: store.topology.consensusAlgorithm, strategy, nodes: nodeCount, quorum, pendingProposals: consensus.pending.length, resolvedProposals: consensus.history.length, status: nodeCount >= quorum ? 'operational' : 'degraded', }; } if (action === 'propose') { const proposalId = `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const quorumPreset = input.quorumPreset || 'majority'; const term = input.term || 1; const required = calcRequired(strategy, nodeCount, quorumPreset); // Raft: one pending proposal per term if (strategy === 'raft') { const existing = consensus.pending.find(p => p.strategy === 'raft' && p.term === term); if (existing) { return { success: false, error: `Raft term ${term} already has pending proposal: ${existing.proposalId}`, existingProposalId: existing.proposalId, }; } } consensus.pending.push({ proposalId, type: 'coordination', proposal: input.proposal, proposedBy: input.voterId || 'system', proposedAt: new Date().toISOString(), votes: {}, status: 'pending', strategy, term: strategy === 'raft' ? term : undefined, quorumPreset: strategy === 'quorum' ? quorumPreset : undefined, byzantineVoters: strategy === 'bft' ? [] : undefined, }); saveCoordStore(store); return { success: true, action: 'proposed', proposalId, proposal: input.proposal, strategy, status: 'pending', required, totalNodes: nodeCount, term: strategy === 'raft' ? term : undefined, }; } if (action === 'vote') { const p = consensus.pending.find(x => x.proposalId === input.proposalId); if (!p) return { success: false, error: 'Proposal not found or already resolved' }; const voterId = input.voterId; if (!voterId) return { success: false, error: 'voterId is required' }; const voteValue = input.vote === 'accept'; const pStrategy = p.strategy || 'raft'; const required = calcRequired(pStrategy, nodeCount, p.quorumPreset); // Double-vote prevention if (voterId in p.votes) { if (pStrategy === 'bft' && p.votes[voterId] !== voteValue) { if (!p.byzantineVoters) p.byzantineVoters = []; if (!p.byzantineVoters.includes(voterId)) p.byzantineVoters.push(voterId); delete p.votes[voterId]; saveCoordStore(store); return { success: false, byzantineDetected: true, message: `Byzantine behavior: voter ${voterId} attempted conflicting vote. Vote invalidated.`, byzantineVoters: p.byzantineVoters, }; } return { success: false, error: `Voter ${voterId} has already voted on this proposal` }; } // BFT cross-proposal conflict check if (pStrategy === 'bft') { for (const other of consensus.pending) { if (other.proposalId === p.proposalId) continue; if (voterId in other.votes && other.votes[voterId] !== voteValue) { if (!p.byzantineVoters) p.byzantineVoters = []; if (!p.byzantineVoters.includes(voterId)) p.byzantineVoters.push(voterId); saveCoordStore(store); return { success: false, byzantineDetected: true, message: `Byzantine behavior: voter ${voterId} cast conflicting votes across proposals.`, byzantineVoters: p.byzantineVoters, }; } } } p.votes[voterId] = voteValue; const votesFor = Object.values(p.votes).filter(v => v).length; const votesAgainst = Object.values(p.votes).filter(v => !v).length; // Resolution check let resolved = false; let result; if (votesFor >= required) { resolved = true; result = 'approved'; } else if (votesAgainst >= required) { resolved = true; result = 'rejected'; } else if (pStrategy === 'quorum' && p.quorumPreset === 'unanimous' && votesAgainst > 0) { resolved = true; result = 'rejected'; } if (resolved && result) { p.status = result; consensus.history.push({ proposalId: p.proposalId, result, votes: { for: votesFor, against: votesAgainst }, decidedAt: new Date().toISOString(), strategy: pStrategy, term: p.term, byzantineDetected: p.byzantineVoters?.length ? p.byzantineVoters : undefined, }); consensus.pending = consensus.pending.filter(x => x.proposalId !== p.proposalId); } saveCoordStore(store); return { success: true, action: 'voted', proposalId: p.proposalId, voterId, vote: input.vote, strategy: pStrategy, votesFor, votesAgainst, required, totalNodes: nodeCount, resolved, result: resolved ? result : undefined, status: p.status, }; } if (action === 'commit') { // Commit is a no-op confirmation for already-resolved proposals if (input.proposalId) { const h = consensus.history.find(x => x.proposalId === input.proposalId); if (h) { return { success: true, action: 'committed', proposalId: input.proposalId, result: h.result, committedAt: new Date().toISOString(), }; } return { success: false, error: 'Proposal not found in resolved history. Vote must reach quorum first.' }; } return { success: false, error: 'proposalId is required for commit' }; } return { success: false, error: 'Unknown action' }; }, }, { name: 'coordination_orchestrate', description: 'Orchestrate multi-agent coordination Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { task: { type: 'string', description: 'Task to orchestrate' }, agents: { type: 'array', items: { type: 'string' }, description: 'Agent IDs to coordinate' }, strategy: { type: 'string', enum: ['parallel', 'sequential', 'pipeline', 'broadcast'], description: 'Orchestration strategy' }, timeout: { type: 'number', description: 'Timeout in ms' }, }, required: ['task'], }, handler: async (input) => { const vTask = validateText(input.task, 'task'); if (!vTask.valid) return { success: false, error: vTask.error }; if (input.agents && Array.isArray(input.agents)) { for (const a of input.agents) { const vA = validateIdentifier(a, 'agents[]'); if (!vA.valid) return { success: false, error: vA.error }; } } const store = loadCoordStore(); const task = input.task; const agents = input.agents || Object.keys(store.nodes); const strategy = input.strategy || 'parallel'; const orchestrationId = `orch-${Date.now()}`; // ADR-093 F7: this tool only schedules an orchestration record — it // does not actually execute. Previously it returned a hardcoded // `estimatedCompletion: "50ms"` which was misleading. Now we return // an honest stub-status with a note pointing callers at agent_spawn // / Task tool / hive-mind tools for real orchestration. Persist the // record so callers can list/inspect what was scheduled. const orchestration = { id: orchestrationId, task, strategy, agents, status: 'scheduled', scheduledAt: new Date().toISOString(), topology: store.topology.type, }; const orchStore = store; if (!Array.isArray(orchStore.orchestrations)) orchStore.orchestrations = []; orchStore.orchestrations.push(orchestration); if (orchStore.orchestrations.length > 100) { orchStore.orchestrations = orchStore.orchestrations.slice(-100); } saveCoordStore(orchStore); return { success: true, orchestrationId, task, strategy, agents, status: 'scheduled', topology: store.topology.type, // Honest stub: no executor wired up yet. Don't lie about completion time. executor: 'none', _note: 'coordination_orchestrate currently records the orchestration request but does not execute it. For real multi-agent execution use agent_spawn + the Task tool, or hive-mind_spawn for queen-led coordination. Real executor tracked in issue #2140.', }; }, }, { name: 'coordination_metrics', description: 'Get coordination metrics Use when native Task is wrong because the work crosses multiple agents that need to vote/sync/load-balance — TodoWrite + a single Task cannot orchestrate consensus. For one-off subtask dispatch, native Task is fine.', category: 'coordination', inputSchema: { type: 'object', properties: { metric: { type: 'string', enum: ['all', 'latency', 'throughput', 'availability'], description: 'Metric type' }, timeRange: { type: 'string', description: 'Time range' }, }, }, handler: async (input) => { const store = loadCoordStore(); const metric = input.metric || 'all'; const nodes = Object.values(store.nodes); const activeNodes = nodes.filter(n => n.status === 'active'); const metrics = { latency: { avg: null, p50: null, p95: null, p99: null, unit: 'ms', _note: 'Real-time latency metrics not available — coordination is state-tracking only', }, throughput: { current: null, peak: null, avg: null, unit: 'ops/s', _note: 'Real-time throughput metrics not available — coordination is state-tracking only', }, availability: { uptime: null, _note: 'Uptime not tracked — coordination store has no persistent start time', activeNodes: activeNodes.length, totalNodes: nodes.length, syncCount: store.sync.syncCount, lastSync: store.sync.lastSync, conflicts: store.sync.conflicts, pendingChanges: store.sync.pendingChanges, syncStatus: store.sync.conflicts === 0 ? 'healthy' : 'conflicts', }, }; if (metric === 'all') { return { success: true, metrics }; } return { success: true, metric, data: metrics[metric], }; }, }, ]; //# sourceMappingURL=coordination-tools.js.map