@evomap/evolver 1.70.0 → 1.74.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/assets/gep/candidates.jsonl +1 -6
- package/index.js +123 -7
- package/package.json +1 -1
- package/scripts/validate-suite.js +21 -6
- package/src/adapters/hookAdapter.js +3 -1
- package/src/adapters/kiro.js +203 -0
- package/src/adapters/scripts/evolver-session-start.js +62 -0
- package/src/atp/atpExecute.js +285 -0
- package/src/atp/atpTaskPickup.js +233 -0
- package/src/atp/autoBuyer.js +12 -6
- package/src/atp/autoDeliver.js +199 -0
- package/src/atp/cliAutobuyPrompt.js +4 -3
- package/src/atp/hubClient.js +20 -0
- package/src/atp/index.js +10 -1
- package/src/atp/questionComposer.js +133 -0
- package/src/evolve.js +1 -1
- package/src/gep/.integrity +0 -0
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/integrityCheck.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/shield.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/validator/sandboxExecutor.js +11 -2
- package/src/proxy/lifecycle/manager.js +5 -1
- package/src/proxy/mailbox/store.js +5 -0
- package/src/proxy/server/http.js +47 -4
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// ATP end-to-end task completer.
|
|
2
|
+
//
|
|
3
|
+
// Invoked from the `atp-complete` subcommand (index.js). A spawned Cursor
|
|
4
|
+
// sub-session answers an ATP task, writes the answer to disk, then runs:
|
|
5
|
+
//
|
|
6
|
+
// node index.js atp-complete \
|
|
7
|
+
// --task-id=<tid> --order-id=<oid> --answer-file=<path> [--summary="..."]
|
|
8
|
+
//
|
|
9
|
+
// This module takes that answer and drives the full settlement path:
|
|
10
|
+
// 1. Synthesize a minimal Gene + Capsule bundle wrapping the answer.
|
|
11
|
+
// 2. POST /a2a/publish to register the Capsule asset on the Hub (signed).
|
|
12
|
+
// 3. POST /a2a/task/complete to bind the resultAssetId to the claimed task.
|
|
13
|
+
// 4. POST /a2a/atp/deliver to submit the proof_payload (asset_id + result).
|
|
14
|
+
//
|
|
15
|
+
// Autoverify (verifyMode=auto) on the Hub treats `payload.asset_id` and
|
|
16
|
+
// `payload.result` as has_result=true with pass_rate=1.0, so a valid answer
|
|
17
|
+
// immediately progresses the DeliveryProof from pending -> verified -> settled.
|
|
18
|
+
//
|
|
19
|
+
// Failures are returned as { ok:false, stage:..., error:... } so the caller can
|
|
20
|
+
// retry per-stage without duplicating upstream effects (Gene/Capsule asset_ids
|
|
21
|
+
// are deterministic content-hashes, so republish of the same bundle is
|
|
22
|
+
// idempotent server-side).
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const http = require('http');
|
|
26
|
+
const https = require('https');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
const { computeAssetId } = require('../gep/contentHash');
|
|
30
|
+
const {
|
|
31
|
+
getNodeId,
|
|
32
|
+
getHubUrl,
|
|
33
|
+
getHubNodeSecret,
|
|
34
|
+
buildHubHeaders,
|
|
35
|
+
sendHelloToHub,
|
|
36
|
+
} = require('../gep/a2aProtocol');
|
|
37
|
+
const { submitDelivery } = require('./hubClient');
|
|
38
|
+
|
|
39
|
+
const MAX_ANSWER_CHARS = 32000; // cap capsule.content to protect Hub payload limits
|
|
40
|
+
const PUBLISH_TIMEOUT_MS = 15000;
|
|
41
|
+
|
|
42
|
+
function _readAnswer(answerFile) {
|
|
43
|
+
const raw = fs.readFileSync(answerFile, 'utf8');
|
|
44
|
+
const trimmed = String(raw || '').trim();
|
|
45
|
+
if (!trimmed) throw new Error('answer file is empty');
|
|
46
|
+
if (trimmed.length > MAX_ANSWER_CHARS) {
|
|
47
|
+
return trimmed.slice(0, MAX_ANSWER_CHARS - 40) + '\n...[TRUNCATED]...';
|
|
48
|
+
}
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _buildGene(capabilities, signals) {
|
|
53
|
+
const caps = Array.isArray(capabilities) && capabilities.length > 0
|
|
54
|
+
? capabilities.slice(0, 8)
|
|
55
|
+
: ['general'];
|
|
56
|
+
const sig = Array.isArray(signals) && signals.length > 0
|
|
57
|
+
? signals.slice(0, 8)
|
|
58
|
+
: ['atp_task'];
|
|
59
|
+
const gene = {
|
|
60
|
+
type: 'Gene',
|
|
61
|
+
schema_version: '1.0',
|
|
62
|
+
id: 'gene_atp_answer_' + caps.sort().join('_').slice(0, 40),
|
|
63
|
+
summary: 'Deliver an ATP task answer for capabilities: ' + caps.join(', '),
|
|
64
|
+
signals_match: sig,
|
|
65
|
+
category: 'innovate',
|
|
66
|
+
strategy: [
|
|
67
|
+
'Read the buyer question carefully and identify the requested capability.',
|
|
68
|
+
'Produce a concrete, actionable answer addressing the question directly.',
|
|
69
|
+
'Return the answer as Capsule content for verifiable delivery.',
|
|
70
|
+
],
|
|
71
|
+
validation: [
|
|
72
|
+
'Answer is non-empty and directly addresses the buyer question.',
|
|
73
|
+
'Answer references the requested capabilities where relevant.',
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
gene.asset_id = computeAssetId(gene);
|
|
77
|
+
return gene;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _buildCapsule({ gene, answer, summary, orderId, taskId, capabilities, signals }) {
|
|
81
|
+
const caps = Array.isArray(capabilities) ? capabilities.slice(0, 8) : [];
|
|
82
|
+
const sig = Array.isArray(signals) && signals.length > 0 ? signals.slice(0, 8) : ['atp_task'];
|
|
83
|
+
const confidence = 0.9; // merchant self-attested; buyer verify may override
|
|
84
|
+
const capsuleSummary = String(summary || '').trim()
|
|
85
|
+
|| 'ATP merchant delivery for order ' + String(orderId || '').slice(0, 24);
|
|
86
|
+
const capsule = {
|
|
87
|
+
type: 'Capsule',
|
|
88
|
+
schema_version: '1.0',
|
|
89
|
+
id: 'capsule_atp_' + String(orderId || taskId || Date.now()).replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 40),
|
|
90
|
+
trigger: sig,
|
|
91
|
+
gene: gene.id,
|
|
92
|
+
summary: capsuleSummary.slice(0, 200),
|
|
93
|
+
confidence,
|
|
94
|
+
blast_radius: { files: 0, lines: Math.min(1000, answer.split('\n').length) },
|
|
95
|
+
outcome: { status: 'success', score: confidence },
|
|
96
|
+
env_fingerprint: { platform: process.platform, arch: process.arch, runtime: 'evolver-atp' },
|
|
97
|
+
content: answer,
|
|
98
|
+
source_type: 'atp_task_executor',
|
|
99
|
+
atp: {
|
|
100
|
+
order_id: orderId || null,
|
|
101
|
+
task_id: taskId || null,
|
|
102
|
+
capabilities: caps,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
capsule.asset_id = computeAssetId(capsule);
|
|
106
|
+
return capsule;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _publishUrl() {
|
|
110
|
+
const base = String(getHubUrl() || '').replace(/\/+$/, '');
|
|
111
|
+
if (!base) throw new Error('hub url not configured');
|
|
112
|
+
return base + '/a2a/publish';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _postJson(urlStr, body, timeoutMs) {
|
|
116
|
+
return new Promise(function (resolve) {
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = new URL(urlStr);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
resolve({ ok: false, error: 'invalid_url: ' + (e && e.message) });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const isHttps = parsed.protocol === 'https:';
|
|
125
|
+
const lib = isHttps ? https : http;
|
|
126
|
+
const payload = JSON.stringify(body || {});
|
|
127
|
+
const headers = Object.assign(
|
|
128
|
+
{ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
129
|
+
buildHubHeaders() || {},
|
|
130
|
+
);
|
|
131
|
+
const req = lib.request(
|
|
132
|
+
{
|
|
133
|
+
hostname: parsed.hostname,
|
|
134
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
135
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: headers,
|
|
138
|
+
timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
|
|
139
|
+
},
|
|
140
|
+
function (res) {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
res.on('data', function (c) { chunks.push(c); });
|
|
143
|
+
res.on('end', function () {
|
|
144
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
145
|
+
let data = null;
|
|
146
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = { raw: text }; }
|
|
147
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
148
|
+
resolve({ ok: true, status: res.statusCode, data });
|
|
149
|
+
} else {
|
|
150
|
+
resolve({ ok: false, status: res.statusCode, data, error: 'http_' + res.statusCode });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
req.on('timeout', function () { req.destroy(new Error('timeout')); });
|
|
156
|
+
req.on('error', function (err) { resolve({ ok: false, error: err.message }); });
|
|
157
|
+
req.write(payload);
|
|
158
|
+
req.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function _ensureNodeSecret() {
|
|
163
|
+
if (getHubNodeSecret()) return true;
|
|
164
|
+
try {
|
|
165
|
+
const hello = await sendHelloToHub();
|
|
166
|
+
return !!(hello && hello.ok);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function _publishBundle(gene, capsule) {
|
|
173
|
+
const nodeSecret = getHubNodeSecret();
|
|
174
|
+
if (!nodeSecret) return { ok: false, error: 'missing_node_secret_after_hello' };
|
|
175
|
+
const signatureInput = [gene.asset_id, capsule.asset_id].sort().join('|');
|
|
176
|
+
const signature = crypto.createHmac('sha256', nodeSecret).update(signatureInput).digest('hex');
|
|
177
|
+
const msg = {
|
|
178
|
+
protocol: 'gep-a2a',
|
|
179
|
+
protocol_version: '1.0.0',
|
|
180
|
+
message_type: 'publish',
|
|
181
|
+
message_id: 'msg_atp_' + crypto.randomBytes(8).toString('hex'),
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
sender_id: getNodeId(),
|
|
184
|
+
payload: { assets: [gene, capsule], signature: signature },
|
|
185
|
+
};
|
|
186
|
+
return _postJson(_publishUrl(), msg, PUBLISH_TIMEOUT_MS);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function _completeTaskOnHub(taskId, assetId) {
|
|
190
|
+
const base = String(getHubUrl() || '').replace(/\/+$/, '');
|
|
191
|
+
if (!base) return { ok: false, error: 'hub_url_missing' };
|
|
192
|
+
return _postJson(base + '/a2a/task/complete', {
|
|
193
|
+
task_id: taskId,
|
|
194
|
+
asset_id: assetId,
|
|
195
|
+
node_id: getNodeId(),
|
|
196
|
+
}, PUBLISH_TIMEOUT_MS);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* End-to-end ATP task completion driver.
|
|
201
|
+
*
|
|
202
|
+
* @param {object} opts
|
|
203
|
+
* @param {string} opts.taskId - Hub task row id (required)
|
|
204
|
+
* @param {string} opts.orderId - ATP DeliveryProof id (required)
|
|
205
|
+
* @param {string} opts.answerFile - Path to file holding the merchant answer (required)
|
|
206
|
+
* @param {string} [opts.summary] - Short summary for capsule.summary
|
|
207
|
+
* @param {string[]} [opts.capabilities] - Listing capabilities (metadata only)
|
|
208
|
+
* @param {string[]} [opts.signals] - Task signals (metadata only)
|
|
209
|
+
* @returns {Promise<{ok:boolean, stage?:string, error?:string, assetId?:string}>}
|
|
210
|
+
*/
|
|
211
|
+
async function completeAtpTask(opts) {
|
|
212
|
+
const taskId = opts && opts.taskId;
|
|
213
|
+
const orderId = opts && opts.orderId;
|
|
214
|
+
const answerFile = opts && opts.answerFile;
|
|
215
|
+
if (!taskId || !orderId || !answerFile) {
|
|
216
|
+
return { ok: false, stage: 'input', error: 'taskId, orderId, answerFile are required' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let answer;
|
|
220
|
+
try {
|
|
221
|
+
answer = _readAnswer(answerFile);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { ok: false, stage: 'read_answer', error: e && e.message };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const handshakeOk = await _ensureNodeSecret();
|
|
227
|
+
if (!handshakeOk) {
|
|
228
|
+
return { ok: false, stage: 'hello', error: 'failed to register with hub; node_secret missing' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const gene = _buildGene(opts.capabilities, opts.signals);
|
|
232
|
+
const capsule = _buildCapsule({
|
|
233
|
+
gene,
|
|
234
|
+
answer,
|
|
235
|
+
summary: opts.summary,
|
|
236
|
+
orderId,
|
|
237
|
+
taskId,
|
|
238
|
+
capabilities: opts.capabilities,
|
|
239
|
+
signals: opts.signals,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const pub = await _publishBundle(gene, capsule);
|
|
243
|
+
if (!pub.ok) {
|
|
244
|
+
return { ok: false, stage: 'publish', error: pub.error || 'publish_failed', details: pub };
|
|
245
|
+
}
|
|
246
|
+
const decision = pub.data && pub.data.payload && pub.data.payload.decision;
|
|
247
|
+
if (decision && decision !== 'accept') {
|
|
248
|
+
const reason = pub.data.payload.reason || 'unknown';
|
|
249
|
+
return { ok: false, stage: 'publish', error: 'publish_rejected: ' + reason, details: pub.data };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const complete = await _completeTaskOnHub(taskId, capsule.asset_id);
|
|
253
|
+
if (!complete.ok) {
|
|
254
|
+
return { ok: false, stage: 'complete', error: complete.error || 'complete_failed', details: complete };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const proofPayload = {
|
|
258
|
+
asset_id: capsule.asset_id,
|
|
259
|
+
result: capsule.summary,
|
|
260
|
+
content_hash: capsule.asset_id,
|
|
261
|
+
pass_rate: 1.0,
|
|
262
|
+
delivered_by: getNodeId(),
|
|
263
|
+
task_id: taskId,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const delivery = await submitDelivery(orderId, proofPayload);
|
|
267
|
+
if (!delivery || !delivery.ok) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
stage: 'deliver',
|
|
271
|
+
error: (delivery && delivery.error) || 'deliver_failed',
|
|
272
|
+
assetId: capsule.asset_id,
|
|
273
|
+
details: delivery,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { ok: true, assetId: capsule.asset_id, deliveryId: delivery.data && delivery.data.proof_id };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = {
|
|
281
|
+
completeAtpTask,
|
|
282
|
+
// exported for tests
|
|
283
|
+
_buildGene,
|
|
284
|
+
_buildCapsule,
|
|
285
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// ATP Task Pickup (merchant-side)
|
|
2
|
+
//
|
|
3
|
+
// When a buyer places an ATP order, the Hub creates a Task row in status
|
|
4
|
+
// "claimed" already bound to a target merchant node (see orderRouterService).
|
|
5
|
+
// That Task never appears in /a2a/fetch (which only returns status="open"
|
|
6
|
+
// tasks), so without this module the merchant's Evolver runtime never knows
|
|
7
|
+
// it has work to do, no resultAssetId is ever written, autoDeliver never
|
|
8
|
+
// runs, and the DeliveryProof expires after 7 days.
|
|
9
|
+
//
|
|
10
|
+
// This module bridges the gap by:
|
|
11
|
+
// 1. Polling /a2a/task/my for tasks with atp_order_id set and no
|
|
12
|
+
// result_asset_id yet (the "merchant owes work" shape).
|
|
13
|
+
// 2. Producing a renderable sessions_spawn(...) prompt that the main loop
|
|
14
|
+
// can emit to stdout. The Evolver wrapper (Cursor/Claude Code hook)
|
|
15
|
+
// picks that up and launches a sub-session that answers the question
|
|
16
|
+
// and runs `node index.js atp-complete` to settle the order.
|
|
17
|
+
// 3. Deduping via a local ledger so the same task is never spawned twice,
|
|
18
|
+
// even across restarts.
|
|
19
|
+
//
|
|
20
|
+
// The module never *itself* prints sessions_spawn. It only PROVIDES the
|
|
21
|
+
// spawn string to whoever orchestrates stdout (evolve.js main loop), so the
|
|
22
|
+
// existing "one sessions_spawn per cycle" contract with the wrapper is
|
|
23
|
+
// preserved and evolve's normal bridge is not interfered with.
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const { getMemoryDir } = require('../gep/paths');
|
|
29
|
+
const { renderSessionsSpawnCall } = require('../gep/bridge');
|
|
30
|
+
const hubClient = require('./hubClient');
|
|
31
|
+
|
|
32
|
+
const LEDGER_FILENAME = 'atp-pickup-ledger.json';
|
|
33
|
+
const LEDGER_MAX_ENTRIES = 500;
|
|
34
|
+
const SPAWN_COOLDOWN_MS = 5 * 60 * 1000; // do not respawn the same task within 5 min
|
|
35
|
+
const MAX_ANSWER_PROMPT_CHARS = 12000;
|
|
36
|
+
|
|
37
|
+
function _isEnabled() {
|
|
38
|
+
const raw = (process.env.EVOLVER_ATP_PICKUP || 'on').toLowerCase().trim();
|
|
39
|
+
return raw !== 'off' && raw !== '0' && raw !== 'false';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _ledgerPath() {
|
|
43
|
+
return path.join(getMemoryDir(), LEDGER_FILENAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _emptyLedger() {
|
|
47
|
+
return { version: 1, spawned: {} };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _readLedger() {
|
|
51
|
+
try {
|
|
52
|
+
const p = _ledgerPath();
|
|
53
|
+
if (!fs.existsSync(p)) return _emptyLedger();
|
|
54
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.spawned) return _emptyLedger();
|
|
57
|
+
return parsed;
|
|
58
|
+
} catch (_) {
|
|
59
|
+
return _emptyLedger();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _writeLedger(ledger) {
|
|
64
|
+
try {
|
|
65
|
+
const dir = getMemoryDir();
|
|
66
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
const entries = Object.entries(ledger.spawned || {});
|
|
68
|
+
if (entries.length > LEDGER_MAX_ENTRIES) {
|
|
69
|
+
ledger.spawned = Object.fromEntries(entries.slice(-LEDGER_MAX_ENTRIES));
|
|
70
|
+
}
|
|
71
|
+
const tmp = _ledgerPath() + '.tmp';
|
|
72
|
+
fs.writeFileSync(tmp, JSON.stringify(ledger, null, 2));
|
|
73
|
+
fs.renameSync(tmp, _ledgerPath());
|
|
74
|
+
} catch (_) {
|
|
75
|
+
// Non-fatal: next tick will re-read Hub state. Stale ledger at worst
|
|
76
|
+
// causes a duplicate spawn, which the Hub will 409 on already-completed
|
|
77
|
+
// tasks without side effects.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _isEligible(task) {
|
|
82
|
+
if (!task || typeof task !== 'object') return false;
|
|
83
|
+
if (!task.atp_order_id) return false;
|
|
84
|
+
if (task.result_asset_id) return false;
|
|
85
|
+
if (task.status && task.status !== 'claimed' && task.status !== 'open') return false;
|
|
86
|
+
if (!task.id) return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _recentlySpawned(ledger, taskId) {
|
|
91
|
+
const entry = ledger.spawned && ledger.spawned[taskId];
|
|
92
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
93
|
+
const ts = Number(entry.at) || 0;
|
|
94
|
+
return Date.now() - ts < SPAWN_COOLDOWN_MS;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _clipQuestion(q) {
|
|
98
|
+
const s = String(q || '').trim();
|
|
99
|
+
if (!s) return '(buyer did not provide a question body)';
|
|
100
|
+
if (s.length <= MAX_ANSWER_PROMPT_CHARS) return s;
|
|
101
|
+
return s.slice(0, MAX_ANSWER_PROMPT_CHARS - 40) + '\n...[TRUNCATED]...';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _answerFilePath(taskId) {
|
|
105
|
+
const safe = String(taskId || 'task').replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 64);
|
|
106
|
+
return path.join(getMemoryDir(), 'atp_answer_' + safe + '.md');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _buildSpawnTask(task, opts) {
|
|
110
|
+
const capabilities = Array.isArray(task.capabilities) ? task.capabilities.slice(0, 8) : [];
|
|
111
|
+
const signalsCsv = task.signals ? String(task.signals) : '';
|
|
112
|
+
const answerPath = _answerFilePath(task.id);
|
|
113
|
+
const question = _clipQuestion(task.user_question_body || task.description || task.title);
|
|
114
|
+
|
|
115
|
+
const evolverExec = opts && opts.evolverExec ? opts.evolverExec : 'node index.js';
|
|
116
|
+
|
|
117
|
+
const lines = [
|
|
118
|
+
'You are an ATP merchant sub-agent. A buyer has paid credits for your node to answer their request.',
|
|
119
|
+
'',
|
|
120
|
+
'# Task',
|
|
121
|
+
'- Task ID: ' + task.id,
|
|
122
|
+
'- ATP Order ID: ' + task.atp_order_id,
|
|
123
|
+
'- Title: ' + String(task.title || '(no title)').slice(0, 200),
|
|
124
|
+
'- Capabilities requested: ' + (capabilities.length ? capabilities.join(', ') : '(none)'),
|
|
125
|
+
'- Signals: ' + (signalsCsv || '(none)'),
|
|
126
|
+
'',
|
|
127
|
+
'# Buyer question',
|
|
128
|
+
question,
|
|
129
|
+
'',
|
|
130
|
+
'# Your job',
|
|
131
|
+
'1. Produce a concrete, useful answer to the buyer question above.',
|
|
132
|
+
' - Use your existing tools (web search, code read, reasoning) as appropriate.',
|
|
133
|
+
' - Keep the answer focused and actionable; do not invent facts.',
|
|
134
|
+
' - If the question is ambiguous, answer the most reasonable interpretation and state your assumption.',
|
|
135
|
+
'2. Write the full answer to this file (plain text or markdown):',
|
|
136
|
+
' ' + answerPath,
|
|
137
|
+
'3. Settle the order by running EXACTLY this command from the Evolver install dir:',
|
|
138
|
+
' ' + evolverExec + ' atp-complete \\',
|
|
139
|
+
' --task-id=' + task.id + ' \\',
|
|
140
|
+
' --order-id=' + task.atp_order_id + ' \\',
|
|
141
|
+
' --answer-file=' + answerPath +
|
|
142
|
+
(capabilities.length ? ' \\\n --capabilities=' + capabilities.join(',') : '') +
|
|
143
|
+
(signalsCsv ? ' \\\n --signals=' + signalsCsv : ''),
|
|
144
|
+
'4. If atp-complete prints "[ATP-Complete] OK asset_id=...", you are done.',
|
|
145
|
+
' If it prints "FAILED", read the stage= field. Safe to retry the same command.',
|
|
146
|
+
'',
|
|
147
|
+
'# Hard rules',
|
|
148
|
+
'- Do NOT commit or push any repo changes -- this is a per-order side task, not a code evolution.',
|
|
149
|
+
'- Do NOT run `node index.js solidify` or `node index.js run`.',
|
|
150
|
+
'- Do NOT fabricate the answer; if you cannot answer, still run atp-complete with a short',
|
|
151
|
+
' honest explanation so the buyer is not left waiting for 7 days.',
|
|
152
|
+
'- Keep the answer under 12k characters.',
|
|
153
|
+
];
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fetch a pickup action if one is due. Idempotent -- safe to call from the
|
|
159
|
+
* main loop every cycle.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} [opts]
|
|
162
|
+
* @param {number} [opts.limit=5] -- how many Hub tasks to consider per call
|
|
163
|
+
* @param {string} [opts.evolverExec] -- how the wrapper should invoke Evolver
|
|
164
|
+
* @returns {Promise<null | { spawnCall: string, task: object }>}
|
|
165
|
+
* null when there is nothing to do; otherwise a sessions_spawn() string
|
|
166
|
+
* the caller SHOULD print to stdout on its next cycle output and the task
|
|
167
|
+
* we picked so the caller can log it.
|
|
168
|
+
*/
|
|
169
|
+
async function pickOne(opts) {
|
|
170
|
+
if (!_isEnabled()) return null;
|
|
171
|
+
const limit = Math.max(1, Math.min(20, Number(opts && opts.limit) || 5));
|
|
172
|
+
|
|
173
|
+
let listResult;
|
|
174
|
+
try {
|
|
175
|
+
listResult = await hubClient.listMyTasks(limit);
|
|
176
|
+
} catch (_) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!listResult || !listResult.ok) return null;
|
|
180
|
+
|
|
181
|
+
const tasks = (listResult.data && Array.isArray(listResult.data.tasks))
|
|
182
|
+
? listResult.data.tasks
|
|
183
|
+
: (Array.isArray(listResult.data) ? listResult.data : []);
|
|
184
|
+
if (!tasks.length) return null;
|
|
185
|
+
|
|
186
|
+
const ledger = _readLedger();
|
|
187
|
+
let picked = null;
|
|
188
|
+
for (const t of tasks) {
|
|
189
|
+
if (!_isEligible(t)) continue;
|
|
190
|
+
if (_recentlySpawned(ledger, t.id)) continue;
|
|
191
|
+
picked = t;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
if (!picked) return null;
|
|
195
|
+
|
|
196
|
+
const spawnTask = _buildSpawnTask(picked, opts);
|
|
197
|
+
const spawnCall = renderSessionsSpawnCall({
|
|
198
|
+
task: spawnTask,
|
|
199
|
+
agentId: 'atp_pickup',
|
|
200
|
+
cleanup: 'delete',
|
|
201
|
+
label: 'atp_pickup_' + String(picked.id).slice(0, 32),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
ledger.spawned = ledger.spawned || {};
|
|
205
|
+
ledger.spawned[picked.id] = { at: Date.now(), order_id: picked.atp_order_id };
|
|
206
|
+
_writeLedger(ledger);
|
|
207
|
+
|
|
208
|
+
return { spawnCall, task: picked };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Forget a previously-spawned task so the main loop will retry it next cycle.
|
|
213
|
+
* Called by callers that detected the spawn channel was unavailable (e.g.
|
|
214
|
+
* wrapper not attached) so we do not burn the cooldown on a no-op spawn.
|
|
215
|
+
*/
|
|
216
|
+
function forget(taskId) {
|
|
217
|
+
if (!taskId) return;
|
|
218
|
+
const ledger = _readLedger();
|
|
219
|
+
if (ledger.spawned && ledger.spawned[taskId]) {
|
|
220
|
+
delete ledger.spawned[taskId];
|
|
221
|
+
_writeLedger(ledger);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
pickOne,
|
|
227
|
+
forget,
|
|
228
|
+
_isEnabled,
|
|
229
|
+
_isEligible,
|
|
230
|
+
_buildSpawnTask,
|
|
231
|
+
_recentlySpawned,
|
|
232
|
+
_answerFilePath,
|
|
233
|
+
};
|
package/src/atp/autoBuyer.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
// ATP Auto-Buyer (opt-
|
|
1
|
+
// ATP Auto-Buyer (opt-out, default ON as of ATP liquidity unlock)
|
|
2
2
|
// Converts capability gaps into ATP orders with strict budget caps and
|
|
3
|
-
// 24h question-level deduplication.
|
|
4
|
-
// EVOLVER_ATP_AUTOBUY=
|
|
3
|
+
// 24h question-level deduplication. Disable by setting
|
|
4
|
+
// EVOLVER_ATP_AUTOBUY=off. Budget caps:
|
|
5
5
|
// ATP_AUTOBUY_DAILY_CAP_CREDITS (default 50)
|
|
6
6
|
// ATP_AUTOBUY_PER_ORDER_CAP_CREDITS (default 10)
|
|
7
7
|
// Cold-start safety: the first 5 minutes after process start use a half-cap
|
|
8
8
|
// to protect against misconfiguration loops on restart storms.
|
|
9
9
|
//
|
|
10
10
|
// Integration contract:
|
|
11
|
-
// 1) Call start({ dailyCap, perOrderCap }) once
|
|
11
|
+
// 1) Call start({ dailyCap, perOrderCap }) once at Evolver boot. The
|
|
12
|
+
// evolve loop does this at the top of every cycle; start() is
|
|
13
|
+
// idempotent so the repeated call is a no-op.
|
|
12
14
|
// 2) Call considerOrder({ signals, question, capabilities, budget, ... })
|
|
13
15
|
// from the evolve loop whenever a capability gap is detected.
|
|
14
16
|
// 3) Result shape: { ok, skipped?, reason?, data?, error? }.
|
|
@@ -48,8 +50,12 @@ function _todayKey(now) {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function _isEnabled() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// Default ON: the evolve loop starts autoBuyer at the top of every cycle
|
|
54
|
+
// so new users get ATP buyer routing out of the box. Disable by setting
|
|
55
|
+
// EVOLVER_ATP_AUTOBUY=off. Budget caps (DAILY_CAP + PER_ORDER_CAP) keep
|
|
56
|
+
// the downside bounded even when this is on.
|
|
57
|
+
const raw = (process.env.EVOLVER_ATP_AUTOBUY || 'on').toLowerCase().trim();
|
|
58
|
+
return raw !== 'off' && raw !== '0' && raw !== 'false';
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
function _emptyLedger() {
|