@evomap/evolver 1.28.1

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.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +290 -0
  3. package/README.zh-CN.md +236 -0
  4. package/SKILL.md +132 -0
  5. package/assets/gep/capsules.json +79 -0
  6. package/assets/gep/events.jsonl +7 -0
  7. package/assets/gep/genes.json +108 -0
  8. package/index.js +530 -0
  9. package/package.json +38 -0
  10. package/src/canary.js +13 -0
  11. package/src/evolve.js +1704 -0
  12. package/src/gep/a2a.js +173 -0
  13. package/src/gep/a2aProtocol.js +736 -0
  14. package/src/gep/analyzer.js +35 -0
  15. package/src/gep/assetCallLog.js +130 -0
  16. package/src/gep/assetStore.js +297 -0
  17. package/src/gep/assets.js +36 -0
  18. package/src/gep/bridge.js +71 -0
  19. package/src/gep/candidates.js +142 -0
  20. package/src/gep/contentHash.js +65 -0
  21. package/src/gep/deviceId.js +209 -0
  22. package/src/gep/envFingerprint.js +83 -0
  23. package/src/gep/hubReview.js +206 -0
  24. package/src/gep/hubSearch.js +237 -0
  25. package/src/gep/issueReporter.js +262 -0
  26. package/src/gep/llmReview.js +92 -0
  27. package/src/gep/memoryGraph.js +771 -0
  28. package/src/gep/memoryGraphAdapter.js +203 -0
  29. package/src/gep/mutation.js +186 -0
  30. package/src/gep/narrativeMemory.js +108 -0
  31. package/src/gep/paths.js +113 -0
  32. package/src/gep/personality.js +355 -0
  33. package/src/gep/prompt.js +566 -0
  34. package/src/gep/questionGenerator.js +212 -0
  35. package/src/gep/reflection.js +127 -0
  36. package/src/gep/sanitize.js +67 -0
  37. package/src/gep/selector.js +250 -0
  38. package/src/gep/signals.js +417 -0
  39. package/src/gep/skillDistiller.js +499 -0
  40. package/src/gep/solidify.js +1681 -0
  41. package/src/gep/strategy.js +126 -0
  42. package/src/gep/taskReceiver.js +528 -0
  43. package/src/gep/validationReport.js +55 -0
  44. package/src/ops/cleanup.js +80 -0
  45. package/src/ops/commentary.js +60 -0
  46. package/src/ops/health_check.js +106 -0
  47. package/src/ops/index.js +11 -0
  48. package/src/ops/innovation.js +67 -0
  49. package/src/ops/lifecycle.js +168 -0
  50. package/src/ops/self_repair.js +72 -0
  51. package/src/ops/skills_monitor.js +143 -0
  52. package/src/ops/trigger.js +33 -0
@@ -0,0 +1,126 @@
1
+ // Evolution Strategy Presets (v1.1)
2
+ // Controls the balance between repair, optimize, and innovate intents.
3
+ //
4
+ // Usage: set EVOLVE_STRATEGY env var to one of: balanced, innovate, harden, repair-only,
5
+ // early-stabilize, steady-state, or "auto" for adaptive selection.
6
+ // Default: balanced (or auto-detected based on cycle count / saturation signals)
7
+ //
8
+ // Each strategy defines:
9
+ // repair/optimize/innovate - target allocation ratios (inform the LLM prompt)
10
+ // repairLoopThreshold - repair ratio in last 8 cycles that triggers forced innovation
11
+ // label - human-readable name injected into the GEP prompt
12
+
13
+ var fs = require('fs');
14
+ var path = require('path');
15
+
16
+ var STRATEGIES = {
17
+ 'balanced': {
18
+ repair: 0.20,
19
+ optimize: 0.30,
20
+ innovate: 0.50,
21
+ repairLoopThreshold: 0.50,
22
+ label: 'Balanced',
23
+ description: 'Normal operation. Steady growth with stability.',
24
+ },
25
+ 'innovate': {
26
+ repair: 0.05,
27
+ optimize: 0.15,
28
+ innovate: 0.80,
29
+ repairLoopThreshold: 0.30,
30
+ label: 'Innovation Focus',
31
+ description: 'System is stable. Maximize new features and capabilities.',
32
+ },
33
+ 'harden': {
34
+ repair: 0.40,
35
+ optimize: 0.40,
36
+ innovate: 0.20,
37
+ repairLoopThreshold: 0.70,
38
+ label: 'Hardening',
39
+ description: 'After a big change. Focus on stability and robustness.',
40
+ },
41
+ 'repair-only': {
42
+ repair: 0.80,
43
+ optimize: 0.20,
44
+ innovate: 0.00,
45
+ repairLoopThreshold: 1.00,
46
+ label: 'Repair Only',
47
+ description: 'Emergency. Fix everything before doing anything else.',
48
+ },
49
+ 'early-stabilize': {
50
+ repair: 0.60,
51
+ optimize: 0.25,
52
+ innovate: 0.15,
53
+ repairLoopThreshold: 0.80,
54
+ label: 'Early Stabilization',
55
+ description: 'First cycles. Prioritize fixing existing issues before innovating.',
56
+ },
57
+ 'steady-state': {
58
+ repair: 0.60,
59
+ optimize: 0.30,
60
+ innovate: 0.10,
61
+ repairLoopThreshold: 0.90,
62
+ label: 'Steady State',
63
+ description: 'Evolution saturated. Maintain existing capabilities. Minimal innovation.',
64
+ },
65
+ };
66
+
67
+ // Read evolution_state.json to get the current cycle count for auto-detection.
68
+ function _readCycleCount() {
69
+ try {
70
+ // evolver/memory/evolution_state.json (local to the skill)
71
+ var localPath = path.resolve(__dirname, '..', '..', 'memory', 'evolution_state.json');
72
+ // workspace/memory/evolution/evolution_state.json (canonical path used by evolve.js)
73
+ var workspacePath = path.resolve(__dirname, '..', '..', '..', '..', 'memory', 'evolution', 'evolution_state.json');
74
+ var candidates = [localPath, workspacePath];
75
+ for (var i = 0; i < candidates.length; i++) {
76
+ if (fs.existsSync(candidates[i])) {
77
+ var data = JSON.parse(fs.readFileSync(candidates[i], 'utf8'));
78
+ return data && Number.isFinite(data.cycleCount) ? data.cycleCount : 0;
79
+ }
80
+ }
81
+ } catch (e) {}
82
+ return 0;
83
+ }
84
+
85
+ function resolveStrategy(opts) {
86
+ var signals = (opts && Array.isArray(opts.signals)) ? opts.signals : [];
87
+ var name = String(process.env.EVOLVE_STRATEGY || 'balanced').toLowerCase().trim();
88
+
89
+ // Backward compatibility: FORCE_INNOVATION=true maps to 'innovate'
90
+ if (!process.env.EVOLVE_STRATEGY) {
91
+ var fi = String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase();
92
+ if (fi === 'true') name = 'innovate';
93
+ }
94
+
95
+ // Auto-detection: when no explicit strategy is set (defaults to 'balanced'),
96
+ // apply heuristics inspired by Echo-MingXuan's "fix first, innovate later" pattern.
97
+ var isDefault = !process.env.EVOLVE_STRATEGY || name === 'balanced' || name === 'auto';
98
+
99
+ if (isDefault) {
100
+ // Early-stabilize: first 5 cycles should focus on fixing existing issues.
101
+ var cycleCount = _readCycleCount();
102
+ if (cycleCount > 0 && cycleCount <= 5) {
103
+ name = 'early-stabilize';
104
+ }
105
+
106
+ // Saturation detection: if saturation signals are present, switch to steady-state.
107
+ if (signals.indexOf('force_steady_state') !== -1) {
108
+ name = 'steady-state';
109
+ } else if (signals.indexOf('evolution_saturation') !== -1) {
110
+ name = 'steady-state';
111
+ }
112
+ }
113
+
114
+ // Explicit "auto" maps to whatever was auto-detected above (or balanced if no heuristic fired).
115
+ if (name === 'auto') name = 'balanced';
116
+
117
+ var strategy = STRATEGIES[name] || STRATEGIES['balanced'];
118
+ strategy.name = name;
119
+ return strategy;
120
+ }
121
+
122
+ function getStrategyNames() {
123
+ return Object.keys(STRATEGIES);
124
+ }
125
+
126
+ module.exports = { resolveStrategy, getStrategyNames, STRATEGIES };
@@ -0,0 +1,528 @@
1
+ // ---------------------------------------------------------------------------
2
+ // taskReceiver -- pulls external tasks from Hub, auto-claims, and injects
3
+ // them as high-priority signals into the evolution loop.
4
+ //
5
+ // v2: Smart task selection with difficulty-aware ROI scoring and capability
6
+ // matching via memory graph history.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const { getNodeId, buildHubHeaders } = require('./a2aProtocol');
10
+
11
+ const HUB_URL = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || 'https://evomap.ai';
12
+
13
+ function buildAuthHeaders() {
14
+ return buildHubHeaders();
15
+ }
16
+
17
+ const TASK_STRATEGY = String(process.env.TASK_STRATEGY || 'balanced').toLowerCase();
18
+ const TASK_MIN_CAPABILITY_MATCH = Number(process.env.TASK_MIN_CAPABILITY_MATCH) || 0.1;
19
+
20
+ // Scoring weights by strategy
21
+ const STRATEGY_WEIGHTS = {
22
+ greedy: { roi: 0.10, capability: 0.05, completion: 0.05, bounty: 0.80 },
23
+ balanced: { roi: 0.35, capability: 0.30, completion: 0.20, bounty: 0.15 },
24
+ conservative: { roi: 0.25, capability: 0.45, completion: 0.25, bounty: 0.05 },
25
+ };
26
+
27
+ /**
28
+ * Fetch available tasks from Hub via the A2A fetch endpoint.
29
+ * Optionally piggybacks proactive questions in the payload for Hub to create bounties.
30
+ *
31
+ * @param {object} [opts]
32
+ * @param {Array<{ question: string, amount?: number, signals?: string[] }>} [opts.questions]
33
+ * @returns {{ tasks: Array, questions_created?: Array }}
34
+ */
35
+ async function fetchTasks(opts) {
36
+ const o = opts || {};
37
+ const nodeId = getNodeId();
38
+ if (!nodeId) return { tasks: [] };
39
+
40
+ try {
41
+ const payload = {
42
+ asset_type: null,
43
+ include_tasks: true,
44
+ };
45
+
46
+ if (Array.isArray(o.questions) && o.questions.length > 0) {
47
+ payload.questions = o.questions;
48
+ }
49
+
50
+ const msg = {
51
+ protocol: 'gep-a2a',
52
+ protocol_version: '1.0.0',
53
+ message_type: 'fetch',
54
+ message_id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
55
+ sender_id: nodeId,
56
+ timestamp: new Date().toISOString(),
57
+ payload,
58
+ };
59
+
60
+ const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/fetch`;
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), 8000);
63
+
64
+ const res = await fetch(url, {
65
+ method: 'POST',
66
+ headers: buildAuthHeaders(),
67
+ body: JSON.stringify(msg),
68
+ signal: controller.signal,
69
+ });
70
+ clearTimeout(timer);
71
+
72
+ if (!res.ok) return { tasks: [] };
73
+
74
+ const data = await res.json();
75
+ const respPayload = data.payload || data;
76
+ const tasks = Array.isArray(respPayload.tasks) ? respPayload.tasks : [];
77
+ const result = { tasks };
78
+
79
+ if (respPayload.questions_created) {
80
+ result.questions_created = respPayload.questions_created;
81
+ }
82
+
83
+ // LessonL: extract relevant lessons from Hub response
84
+ if (Array.isArray(respPayload.relevant_lessons) && respPayload.relevant_lessons.length > 0) {
85
+ result.relevant_lessons = respPayload.relevant_lessons;
86
+ }
87
+
88
+ return result;
89
+ } catch (err) {
90
+ console.warn("[TaskReceiver] fetchTasks failed:", err && err.message ? err.message : err);
91
+ return { tasks: [] };
92
+ }
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Capability matching: how well this agent's history matches a task's signals
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function parseSignals(raw) {
100
+ if (!raw) return [];
101
+ return String(raw).split(',').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
102
+ }
103
+
104
+ function jaccard(a, b) {
105
+ if (!a.length || !b.length) return 0;
106
+ var setA = new Set(a);
107
+ var setB = new Set(b);
108
+ var inter = 0;
109
+ for (var v of setB) { if (setA.has(v)) inter++; }
110
+ return inter / (setA.size + setB.size - inter);
111
+ }
112
+
113
+ /**
114
+ * Estimate how well this agent can handle a task based on memory graph history.
115
+ * Returns 0.0 - 1.0 where 1.0 = strong match with high success rate.
116
+ *
117
+ * @param {object} task - task from Hub (has .signals field)
118
+ * @param {Array} memoryEvents - from tryReadMemoryGraphEvents()
119
+ * @returns {number}
120
+ */
121
+ function estimateCapabilityMatch(task, memoryEvents) {
122
+ if (!Array.isArray(memoryEvents) || memoryEvents.length === 0) return 0.5;
123
+
124
+ var taskSignals = parseSignals(task.signals || task.title);
125
+ if (taskSignals.length === 0) return 0.5;
126
+
127
+ var successBySignalKey = {};
128
+ var totalBySignalKey = {};
129
+ var allSignals = {};
130
+
131
+ for (var i = 0; i < memoryEvents.length; i++) {
132
+ var ev = memoryEvents[i];
133
+ if (!ev || ev.type !== 'MemoryGraphEvent' || ev.kind !== 'outcome') continue;
134
+
135
+ var sigs = (ev.signal && Array.isArray(ev.signal.signals)) ? ev.signal.signals : [];
136
+ var key = (ev.signal && ev.signal.key) ? String(ev.signal.key) : '';
137
+ var status = (ev.outcome && ev.outcome.status) ? String(ev.outcome.status) : '';
138
+
139
+ for (var j = 0; j < sigs.length; j++) {
140
+ allSignals[sigs[j].toLowerCase()] = true;
141
+ }
142
+
143
+ if (!key) continue;
144
+ if (!totalBySignalKey[key]) { totalBySignalKey[key] = 0; successBySignalKey[key] = 0; }
145
+ totalBySignalKey[key]++;
146
+ if (status === 'success') successBySignalKey[key]++;
147
+ }
148
+
149
+ // Jaccard overlap between task signals and all signals this agent has worked with
150
+ var allSigArr = Object.keys(allSignals);
151
+ var overlapScore = jaccard(taskSignals, allSigArr);
152
+
153
+ // Weighted success rate across matching signal keys
154
+ var weightedSuccess = 0;
155
+ var weightSum = 0;
156
+ for (var sk in totalBySignalKey) {
157
+ // Reconstruct signals from the key for comparison
158
+ var skParts = sk.split('|').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
159
+ var sim = jaccard(taskSignals, skParts);
160
+ if (sim < 0.15) continue;
161
+
162
+ var total = totalBySignalKey[sk];
163
+ var succ = successBySignalKey[sk] || 0;
164
+ var rate = (succ + 1) / (total + 2); // Laplace smoothing
165
+ weightedSuccess += rate * sim;
166
+ weightSum += sim;
167
+ }
168
+
169
+ var successScore = weightSum > 0 ? (weightedSuccess / weightSum) : 0.5;
170
+
171
+ // Combine: 60% success rate history + 40% signal overlap
172
+ return Math.min(1, overlapScore * 0.4 + successScore * 0.6);
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Local fallback difficulty estimation when Hub doesn't provide complexity_score
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function localDifficultyEstimate(task) {
180
+ var signals = parseSignals(task.signals);
181
+ var signalFactor = Math.min(signals.length / 8, 1);
182
+
183
+ var titleWords = (task.title || '').split(/\s+/).filter(Boolean).length;
184
+ var titleFactor = Math.min(titleWords / 15, 1);
185
+
186
+ return Math.min(1, signalFactor * 0.6 + titleFactor * 0.4);
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Commitment deadline estimation -- based on task difficulty
191
+ // ---------------------------------------------------------------------------
192
+
193
+ const MIN_COMMITMENT_MS = 5 * 60 * 1000; // 5 min (Hub minimum)
194
+ const MAX_COMMITMENT_MS = 24 * 60 * 60 * 1000; // 24 h (Hub maximum)
195
+
196
+ const DIFFICULTY_DURATION_MAP = [
197
+ { threshold: 0.3, durationMs: 15 * 60 * 1000 }, // low: 15 min
198
+ { threshold: 0.5, durationMs: 30 * 60 * 1000 }, // medium: 30 min
199
+ { threshold: 0.7, durationMs: 60 * 60 * 1000 }, // high: 60 min
200
+ { threshold: 1.0, durationMs: 120 * 60 * 1000 }, // very high: 120 min
201
+ ];
202
+
203
+ /**
204
+ * Estimate a reasonable commitment deadline for a task.
205
+ * Returns an ISO-8601 date string or null if estimation fails.
206
+ *
207
+ * @param {object} task - task from Hub
208
+ * @returns {string|null}
209
+ */
210
+ function estimateCommitmentDeadline(task) {
211
+ if (!task) return null;
212
+
213
+ var difficulty = (task.complexity_score != null)
214
+ ? Number(task.complexity_score)
215
+ : localDifficultyEstimate(task);
216
+
217
+ var durationMs = DIFFICULTY_DURATION_MAP[DIFFICULTY_DURATION_MAP.length - 1].durationMs;
218
+ for (var i = 0; i < DIFFICULTY_DURATION_MAP.length; i++) {
219
+ if (difficulty <= DIFFICULTY_DURATION_MAP[i].threshold) {
220
+ durationMs = DIFFICULTY_DURATION_MAP[i].durationMs;
221
+ break;
222
+ }
223
+ }
224
+
225
+ durationMs = Math.max(MIN_COMMITMENT_MS, Math.min(MAX_COMMITMENT_MS, durationMs));
226
+
227
+ var deadline = new Date(Date.now() + durationMs);
228
+
229
+ if (task.expires_at) {
230
+ var expiresAt = new Date(task.expires_at);
231
+ if (!isNaN(expiresAt.getTime()) && expiresAt < deadline) {
232
+ var remaining = expiresAt.getTime() - Date.now();
233
+ if (remaining < MIN_COMMITMENT_MS) return null;
234
+ var adjusted = new Date(expiresAt.getTime() - 60000);
235
+ if (adjusted.getTime() - Date.now() < MIN_COMMITMENT_MS) return null;
236
+ deadline = adjusted;
237
+ }
238
+ }
239
+
240
+ return deadline.toISOString();
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Score a single task for this agent
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /**
248
+ * @param {object} task - task from Hub
249
+ * @param {number} capabilityMatch - from estimateCapabilityMatch()
250
+ * @returns {{ composite: number, factors: object }}
251
+ */
252
+ function scoreTask(task, capabilityMatch) {
253
+ var w = STRATEGY_WEIGHTS[TASK_STRATEGY] || STRATEGY_WEIGHTS.balanced;
254
+
255
+ var difficulty = (task.complexity_score != null) ? task.complexity_score : localDifficultyEstimate(task);
256
+ var bountyAmount = task.bounty_amount || 0;
257
+ var completionRate = (task.historical_completion_rate != null) ? task.historical_completion_rate : 0.5;
258
+
259
+ // ROI: bounty per unit difficulty (higher = better value)
260
+ var roiRaw = bountyAmount / (difficulty + 0.1);
261
+ var roiNorm = Math.min(roiRaw / 200, 1); // normalize: 200-credit ROI = max
262
+
263
+ // Bounty absolute: normalize against a reference max
264
+ var bountyNorm = Math.min(bountyAmount / 100, 1);
265
+
266
+ var composite =
267
+ w.roi * roiNorm +
268
+ w.capability * capabilityMatch +
269
+ w.completion * completionRate +
270
+ w.bounty * bountyNorm;
271
+
272
+ return {
273
+ composite: Math.round(composite * 1000) / 1000,
274
+ factors: {
275
+ roi: Math.round(roiNorm * 100) / 100,
276
+ capability: Math.round(capabilityMatch * 100) / 100,
277
+ completion: Math.round(completionRate * 100) / 100,
278
+ bounty: Math.round(bountyNorm * 100) / 100,
279
+ difficulty: Math.round(difficulty * 100) / 100,
280
+ },
281
+ };
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Enhanced task selection with scoring
286
+ // ---------------------------------------------------------------------------
287
+
288
+ /**
289
+ * Pick the best task from a list using composite scoring.
290
+ * @param {Array} tasks
291
+ * @param {Array} [memoryEvents] - from tryReadMemoryGraphEvents()
292
+ * @returns {object|null}
293
+ */
294
+ function selectBestTask(tasks, memoryEvents) {
295
+ if (!Array.isArray(tasks) || tasks.length === 0) return null;
296
+
297
+ var nodeId = getNodeId();
298
+
299
+ // Already-claimed tasks for this node always take top priority (resume work)
300
+ var myClaimedTask = tasks.find(function(t) {
301
+ return t.status === 'claimed' && t.claimed_by === nodeId;
302
+ });
303
+ if (myClaimedTask) return myClaimedTask;
304
+
305
+ // Filter to open tasks only
306
+ var open = tasks.filter(function(t) { return t.status === 'open'; });
307
+ if (open.length === 0) return null;
308
+
309
+ // Legacy greedy mode: preserve old behavior exactly
310
+ if (TASK_STRATEGY === 'greedy' && (!memoryEvents || memoryEvents.length === 0)) {
311
+ var bountyTasks = open.filter(function(t) { return t.bounty_id; });
312
+ if (bountyTasks.length > 0) {
313
+ bountyTasks.sort(function(a, b) { return (b.bounty_amount || 0) - (a.bounty_amount || 0); });
314
+ return bountyTasks[0];
315
+ }
316
+ return open[0];
317
+ }
318
+
319
+ // Score all open tasks
320
+ var scored = open.map(function(t) {
321
+ var cap = estimateCapabilityMatch(t, memoryEvents || []);
322
+ var result = scoreTask(t, cap);
323
+ return { task: t, composite: result.composite, factors: result.factors, capability: cap };
324
+ });
325
+
326
+ // Filter by minimum capability match (unless conservative skipping is off)
327
+ if (TASK_MIN_CAPABILITY_MATCH > 0) {
328
+ var filtered = scored.filter(function(s) { return s.capability >= TASK_MIN_CAPABILITY_MATCH; });
329
+ if (filtered.length > 0) scored = filtered;
330
+ }
331
+
332
+ scored.sort(function(a, b) { return b.composite - a.composite; });
333
+
334
+ // Log top 3 candidates for debugging
335
+ var top3 = scored.slice(0, 3);
336
+ for (var i = 0; i < top3.length; i++) {
337
+ var s = top3[i];
338
+ console.log('[TaskStrategy] #' + (i + 1) + ' "' + (s.task.title || s.task.task_id || '').slice(0, 50) + '" score=' + s.composite + ' ' + JSON.stringify(s.factors));
339
+ }
340
+
341
+ return scored[0] ? scored[0].task : null;
342
+ }
343
+
344
+ /**
345
+ * Claim a task on the Hub.
346
+ * @param {string} taskId
347
+ * @param {{ commitment_deadline?: string }} [opts]
348
+ * @returns {boolean} true if claim succeeded
349
+ */
350
+ async function claimTask(taskId, opts) {
351
+ const nodeId = getNodeId();
352
+ if (!nodeId || !taskId) return false;
353
+
354
+ try {
355
+ const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/claim`;
356
+ const controller = new AbortController();
357
+ const timer = setTimeout(() => controller.abort(), 5000);
358
+
359
+ const body = { task_id: taskId, node_id: nodeId };
360
+ if (opts && opts.commitment_deadline) {
361
+ body.commitment_deadline = opts.commitment_deadline;
362
+ }
363
+
364
+ const res = await fetch(url, {
365
+ method: 'POST',
366
+ headers: buildAuthHeaders(),
367
+ body: JSON.stringify(body),
368
+ signal: controller.signal,
369
+ });
370
+ clearTimeout(timer);
371
+
372
+ return res.ok;
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Complete a task on the Hub with the result asset ID.
380
+ * @param {string} taskId
381
+ * @param {string} assetId
382
+ * @returns {boolean}
383
+ */
384
+ async function completeTask(taskId, assetId) {
385
+ const nodeId = getNodeId();
386
+ if (!nodeId || !taskId || !assetId) return false;
387
+
388
+ try {
389
+ const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/complete`;
390
+ const controller = new AbortController();
391
+ const timer = setTimeout(() => controller.abort(), 5000);
392
+
393
+ const res = await fetch(url, {
394
+ method: 'POST',
395
+ headers: buildAuthHeaders(),
396
+ body: JSON.stringify({ task_id: taskId, asset_id: assetId, node_id: nodeId }),
397
+ signal: controller.signal,
398
+ });
399
+ clearTimeout(timer);
400
+
401
+ return res.ok;
402
+ } catch {
403
+ return false;
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Extract signals from a task to inject into evolution cycle.
409
+ * @param {object} task
410
+ * @returns {string[]} signals array
411
+ */
412
+ function taskToSignals(task) {
413
+ if (!task) return [];
414
+ const signals = [];
415
+ if (task.signals) {
416
+ const parts = String(task.signals).split(',').map(s => s.trim()).filter(Boolean);
417
+ signals.push(...parts);
418
+ }
419
+ if (task.title) {
420
+ const words = String(task.title).toLowerCase().split(/\s+/).filter(w => w.length >= 3);
421
+ for (const w of words.slice(0, 5)) {
422
+ if (!signals.includes(w)) signals.push(w);
423
+ }
424
+ }
425
+ signals.push('external_task');
426
+ if (task.bounty_id) signals.push('bounty_task');
427
+ return signals;
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Worker Pool task operations (POST /a2a/work/*)
432
+ // These use a separate API from bounty tasks and return assignment objects.
433
+ // ---------------------------------------------------------------------------
434
+
435
+ async function claimWorkerTask(taskId) {
436
+ const nodeId = getNodeId();
437
+ if (!nodeId || !taskId) return null;
438
+
439
+ try {
440
+ const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/work/claim`;
441
+ const controller = new AbortController();
442
+ const timer = setTimeout(() => controller.abort(), 5000);
443
+
444
+ const res = await fetch(url, {
445
+ method: 'POST',
446
+ headers: buildAuthHeaders(),
447
+ body: JSON.stringify({ task_id: taskId, node_id: nodeId }),
448
+ signal: controller.signal,
449
+ });
450
+ clearTimeout(timer);
451
+
452
+ if (!res.ok) return null;
453
+ return await res.json();
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+
459
+ async function completeWorkerTask(assignmentId, resultAssetId) {
460
+ const nodeId = getNodeId();
461
+ if (!nodeId || !assignmentId || !resultAssetId) return false;
462
+
463
+ try {
464
+ const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/work/complete`;
465
+ const controller = new AbortController();
466
+ const timer = setTimeout(() => controller.abort(), 5000);
467
+
468
+ const res = await fetch(url, {
469
+ method: 'POST',
470
+ headers: buildAuthHeaders(),
471
+ body: JSON.stringify({ assignment_id: assignmentId, node_id: nodeId, result_asset_id: resultAssetId }),
472
+ signal: controller.signal,
473
+ });
474
+ clearTimeout(timer);
475
+
476
+ return res.ok;
477
+ } catch {
478
+ return false;
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Atomic claim+complete for deferred worker tasks.
484
+ * Called from solidify after a successful evolution cycle so we never hold
485
+ * an assignment that might expire before completion.
486
+ *
487
+ * @param {string} taskId
488
+ * @param {string} resultAssetId - sha256:... of the published capsule
489
+ * @returns {{ ok: boolean, assignment_id?: string, error?: string }}
490
+ */
491
+ async function claimAndCompleteWorkerTask(taskId, resultAssetId) {
492
+ const nodeId = getNodeId();
493
+ if (!nodeId || !taskId || !resultAssetId) {
494
+ return { ok: false, error: 'missing_params' };
495
+ }
496
+
497
+ const assignment = await claimWorkerTask(taskId);
498
+ if (!assignment) {
499
+ return { ok: false, error: 'claim_failed' };
500
+ }
501
+
502
+ const assignmentId = assignment.id || assignment.assignment_id;
503
+ if (!assignmentId) {
504
+ return { ok: false, error: 'no_assignment_id' };
505
+ }
506
+
507
+ const completed = await completeWorkerTask(assignmentId, resultAssetId);
508
+ if (!completed) {
509
+ console.warn(`[WorkerPool] Claimed assignment ${assignmentId} but complete failed -- will expire on Hub`);
510
+ return { ok: false, error: 'complete_failed', assignment_id: assignmentId };
511
+ }
512
+
513
+ return { ok: true, assignment_id: assignmentId };
514
+ }
515
+
516
+ module.exports = {
517
+ fetchTasks,
518
+ selectBestTask,
519
+ estimateCapabilityMatch,
520
+ scoreTask,
521
+ claimTask,
522
+ completeTask,
523
+ taskToSignals,
524
+ claimWorkerTask,
525
+ completeWorkerTask,
526
+ claimAndCompleteWorkerTask,
527
+ estimateCommitmentDeadline,
528
+ };