// --------------------------------------------------------------------------- // taskReceiver -- pulls external tasks from Hub, auto-claims, and injects // them as high-priority signals into the evolution loop. // --------------------------------------------------------------------------- const { getNodeId } = require('./a2aProtocol'); const HUB_URL = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || 'https://evomap.ai'; /** * Fetch available tasks from Hub via the A2A fetch endpoint. * @returns {Array} Array of task objects, or empty array on failure. */ async function fetchTasks() { const nodeId = getNodeId(); if (!nodeId) return []; try { const msg = { protocol: 'gep-a2a', protocol_version: '1.0.0', message_type: 'fetch', message_id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, sender_id: nodeId, timestamp: new Date().toISOString(), payload: { asset_type: null, include_tasks: true, }, }; const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/fetch`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 8000); const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(msg), signal: controller.signal, }); clearTimeout(timer); if (!res.ok) return []; const data = await res.json(); const payload = data.payload || data; return Array.isArray(payload.tasks) ? payload.tasks : []; } catch { return []; } } /** * Pick the best task from a list. Priority: * 1. Bounty tasks (higher amount first) * 2. Tasks already claimed by this node * 3. Open tasks (newest first) * @param {Array} tasks * @returns {object|null} */ function selectBestTask(tasks) { if (!Array.isArray(tasks) || tasks.length === 0) return null; const nodeId = getNodeId(); // Already-claimed tasks for this node take top priority (resume work) const myClaimedTask = tasks.find( t => t.status === 'claimed' && t.claimed_by_node_id === nodeId ); if (myClaimedTask) return myClaimedTask; // Filter to open tasks only const open = tasks.filter(t => t.status === 'open'); if (open.length === 0) return null; // Prefer bounty tasks, sorted by amount descending const bountyTasks = open.filter(t => t.bounty_id); if (bountyTasks.length > 0) { bountyTasks.sort((a, b) => (b.bounty_amount || 0) - (a.bounty_amount || 0)); return bountyTasks[0]; } // Fallback: newest open task return open[0]; } /** * Claim a task on the Hub. * @param {string} taskId * @returns {boolean} true if claim succeeded */ async function claimTask(taskId) { const nodeId = getNodeId(); if (!nodeId || !taskId) return false; try { const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/claim`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, node_id: nodeId }), signal: controller.signal, }); clearTimeout(timer); return res.ok; } catch { return false; } } /** * Complete a task on the Hub with the result asset ID. * @param {string} taskId * @param {string} assetId * @returns {boolean} */ async function completeTask(taskId, assetId) { const nodeId = getNodeId(); if (!nodeId || !taskId || !assetId) return false; try { const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/complete`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, asset_id: assetId, node_id: nodeId }), signal: controller.signal, }); clearTimeout(timer); return res.ok; } catch { return false; } } /** * Extract signals from a task to inject into evolution cycle. * @param {object} task * @returns {string[]} signals array */ function taskToSignals(task) { if (!task) return []; const signals = []; if (task.signals) { const parts = String(task.signals).split(',').map(s => s.trim()).filter(Boolean); signals.push(...parts); } if (task.title) { const words = String(task.title).toLowerCase().split(/\s+/).filter(w => w.length >= 3); for (const w of words.slice(0, 5)) { if (!signals.includes(w)) signals.push(w); } } signals.push('external_task'); if (task.bounty_id) signals.push('bounty_task'); return signals; } module.exports = { fetchTasks, selectBestTask, claimTask, completeTask, taskToSignals, };