175 lines
4.7 KiB
JavaScript
175 lines
4.7 KiB
JavaScript
// ---------------------------------------------------------------------------
|
|
// 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,
|
|
};
|