@evomap/evolver 1.29.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/LICENSE +22 -0
- package/README.md +290 -0
- package/README.zh-CN.md +236 -0
- package/SKILL.md +132 -0
- package/assets/gep/capsules.json +79 -0
- package/assets/gep/events.jsonl +7 -0
- package/assets/gep/genes.json +108 -0
- package/index.js +479 -0
- package/package.json +38 -0
- package/src/canary.js +13 -0
- package/src/evolve.js +1704 -0
- package/src/gep/a2a.js +173 -0
- package/src/gep/a2aProtocol.js +736 -0
- package/src/gep/analyzer.js +35 -0
- package/src/gep/assetCallLog.js +130 -0
- package/src/gep/assetStore.js +297 -0
- package/src/gep/assets.js +36 -0
- package/src/gep/bridge.js +71 -0
- package/src/gep/candidates.js +142 -0
- package/src/gep/contentHash.js +65 -0
- package/src/gep/deviceId.js +209 -0
- package/src/gep/envFingerprint.js +68 -0
- package/src/gep/hubReview.js +206 -0
- package/src/gep/hubSearch.js +237 -0
- package/src/gep/issueReporter.js +262 -0
- package/src/gep/llmReview.js +92 -0
- package/src/gep/memoryGraph.js +771 -0
- package/src/gep/memoryGraphAdapter.js +203 -0
- package/src/gep/mutation.js +186 -0
- package/src/gep/narrativeMemory.js +108 -0
- package/src/gep/paths.js +113 -0
- package/src/gep/personality.js +355 -0
- package/src/gep/prompt.js +566 -0
- package/src/gep/questionGenerator.js +212 -0
- package/src/gep/reflection.js +127 -0
- package/src/gep/sanitize.js +67 -0
- package/src/gep/selector.js +250 -0
- package/src/gep/signals.js +417 -0
- package/src/gep/skillDistiller.js +499 -0
- package/src/gep/solidify.js +1681 -0
- package/src/gep/strategy.js +126 -0
- package/src/gep/taskReceiver.js +528 -0
- package/src/gep/validationReport.js +55 -0
- package/src/ops/cleanup.js +80 -0
- package/src/ops/commentary.js +60 -0
- package/src/ops/health_check.js +106 -0
- package/src/ops/index.js +11 -0
- package/src/ops/innovation.js +67 -0
- package/src/ops/lifecycle.js +168 -0
- package/src/ops/self_repair.js +72 -0
- package/src/ops/skills_monitor.js +143 -0
- 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
|
+
};
|