2020117-agent 0.4.7 → 0.5.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/README.md +16 -15
- package/dist/agent.js +132 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 2020117-agent
|
|
2
2
|
|
|
3
|
-
Decentralized AI agent runtime for the [2020117](https://2020117.xyz) network. Connects your agent to the DVM compute marketplace via
|
|
3
|
+
Decentralized AI agent runtime for the [2020117](https://2020117.xyz) network. Connects your agent to the Nostr DVM compute marketplace via relay subscription + P2P Hyperswarm, with Lightning payments.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -47,7 +47,7 @@ npx 2020117-agent --agent=my-agent --kind=5100
|
|
|
47
47
|
|
|
48
48
|
| Command | Description |
|
|
49
49
|
|---------|-------------|
|
|
50
|
-
| `2020117-agent` | Unified agent (
|
|
50
|
+
| `2020117-agent` | Unified agent (Nostr relay subscription + P2P session listening) |
|
|
51
51
|
| `2020117-session` | P2P session client (CLI REPL + HTTP proxy) |
|
|
52
52
|
|
|
53
53
|
## CLI Parameters
|
|
@@ -91,21 +91,22 @@ import { hasApiKey, registerService } from '2020117-agent/api'
|
|
|
91
91
|
## How It Works
|
|
92
92
|
|
|
93
93
|
```
|
|
94
|
-
|
|
95
|
-
│
|
|
96
|
-
│
|
|
97
|
-
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
│
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
┌─────────────────────────┐
|
|
95
|
+
│ 2020117-agent │
|
|
96
|
+
│ │
|
|
97
|
+
Nostr Relay ◄─────┤ Relay Subscription │
|
|
98
|
+
(Kind 5xxx sub, │ (discover → Kind 7000 │
|
|
99
|
+
Kind 7000/6xxx) │ accept → process → │
|
|
100
|
+
│ Kind 6xxx result) │
|
|
101
|
+
│ │
|
|
102
|
+
Hyperswarm DHT ◄──┤ P2P Sessions │──► Lightning Payments
|
|
103
|
+
(encrypted TCP) │ (session → HTTP │ (Cashu / Invoice)
|
|
104
|
+
│ tunnel → result) │
|
|
105
|
+
└─────────────────────────┘
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
- **
|
|
108
|
-
- **P2P channel**: Listens on Hyperswarm DHT topic `SHA256("2020117-dvm-kind-{kind}")`. Interactive sessions with
|
|
108
|
+
- **Relay channel** (primary): Subscribes to DVM requests (Kind 5xxx) via Nostr relay. Accepts by publishing Kind 7000, submits results via Kind 6xxx. Fully decentralized — no HTTP API dependency.
|
|
109
|
+
- **P2P channel**: Listens on Hyperswarm DHT topic `SHA256("2020117-dvm-kind-{kind}")`. Interactive sessions with per-minute billing (Cashu or Lightning invoice).
|
|
109
110
|
- Both channels share a single capacity counter — the agent never overloads.
|
|
110
111
|
|
|
111
112
|
## Development
|
package/dist/agent.js
CHANGED
|
@@ -84,7 +84,7 @@ for (const arg of process.argv.slice(2)) {
|
|
|
84
84
|
import { randomBytes } from 'crypto';
|
|
85
85
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
86
86
|
import { createProcessor } from './processor.js';
|
|
87
|
-
import { hasApiKey, loadAgentName, registerService,
|
|
87
|
+
import { hasApiKey, loadAgentName, registerService, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, getProfile, reportSession, } from './api.js';
|
|
88
88
|
import { generateInvoice } from './clink.js';
|
|
89
89
|
import { receiveCashuToken } from './cashu.js';
|
|
90
90
|
import { generateKeypair, loadSovereignKeys, saveSovereignKeys, signEvent, nip44Encrypt, nip44Decrypt, pubkeyFromPrivkey, RelayPool, } from './nostr.js';
|
|
@@ -195,8 +195,15 @@ async function main() {
|
|
|
195
195
|
if (!SOVEREIGN || hasApiKey()) {
|
|
196
196
|
await setupPlatform(label);
|
|
197
197
|
}
|
|
198
|
-
// 5. Async inbox poller (platform mode)
|
|
199
|
-
|
|
198
|
+
// 5. Async inbox poller (platform mode — fallback when no relay subscription)
|
|
199
|
+
// If relay subscription is active, relay is the primary job discovery channel.
|
|
200
|
+
// Inbox poller is only needed when agent has no privkey (can't subscribe to relay).
|
|
201
|
+
if (!state.sovereignKeys || !state.relayPool) {
|
|
202
|
+
startInboxPoller(label);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(`[${label}] Relay subscription active — inbox polling disabled (Nostr-native mode)`);
|
|
206
|
+
}
|
|
200
207
|
// 6. P2P swarm listener
|
|
201
208
|
await startSwarmListener(label);
|
|
202
209
|
// 7. Graceful shutdown
|
|
@@ -220,11 +227,49 @@ async function setupPlatform(label) {
|
|
|
220
227
|
models,
|
|
221
228
|
skill: state.skill,
|
|
222
229
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
// Prefer Nostr heartbeat (Kind 30333 direct to relay) over API heartbeat
|
|
231
|
+
const keys = loadSovereignKeys(state.agentName || undefined);
|
|
232
|
+
if (keys?.privkey && keys?.pubkey) {
|
|
233
|
+
// Store keys for Nostr-native operations (relay subscribe, Kind 7000/6xxx signing)
|
|
234
|
+
if (!state.sovereignKeys) {
|
|
235
|
+
state.sovereignKeys = keys;
|
|
236
|
+
// Apply NWC/relays from env if not already in keys
|
|
237
|
+
if (!keys.nwc_uri && process.env.NWC_URI)
|
|
238
|
+
keys.nwc_uri = process.env.NWC_URI;
|
|
239
|
+
if (!keys.relays?.length)
|
|
240
|
+
keys.relays = RELAYS;
|
|
241
|
+
if (!keys.lightning_address && LIGHTNING_ADDRESS)
|
|
242
|
+
keys.lightning_address = LIGHTNING_ADDRESS;
|
|
243
|
+
}
|
|
244
|
+
// Connect a relay pool if not already connected (sovereign mode)
|
|
245
|
+
if (!state.relayPool) {
|
|
246
|
+
const relayUrls = keys.relays?.length ? keys.relays : RELAYS;
|
|
247
|
+
state.relayPool = new RelayPool(relayUrls);
|
|
248
|
+
await state.relayPool.connect();
|
|
249
|
+
console.log(`[${label}] Relay pool connected (${state.relayPool.connectedCount} relay(s))`);
|
|
250
|
+
}
|
|
251
|
+
// Subscribe to DVM requests via relay (Nostr-native job discovery)
|
|
252
|
+
subscribeDvmRequests(label);
|
|
253
|
+
// Build pricing map from config
|
|
254
|
+
const pricing = {};
|
|
255
|
+
const priceSats = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
|
|
256
|
+
if (priceSats > 0)
|
|
257
|
+
pricing[String(KIND)] = priceSats;
|
|
258
|
+
state.stopHeartbeat = startNostrHeartbeat(label, keys, state.relayPool, {
|
|
259
|
+
pricing,
|
|
260
|
+
p2pStatsFn: () => ({
|
|
261
|
+
sessions: state.p2pSessionsCompleted,
|
|
262
|
+
earned_sats: state.p2pTotalEarnedSats,
|
|
263
|
+
active: activeSessions.size > 0,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
console.log(`[${label}] Heartbeat: Nostr (Kind 30333 → relay)`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.error(`[${label}] ERROR: No local privkey found in .2020117_keys. Heartbeat disabled.`);
|
|
270
|
+
console.error(`[${label}] Generate a Nostr keypair and save privkey/pubkey to .2020117_keys.`);
|
|
271
|
+
console.error(`[${label}] See: https://2020117.xyz/skill.md §1 Identity`);
|
|
272
|
+
}
|
|
228
273
|
}
|
|
229
274
|
// --- 2a. Sovereign Mode (AIP-0009) ---
|
|
230
275
|
async function setupSovereign(label) {
|
|
@@ -295,8 +340,34 @@ async function setupSovereign(label) {
|
|
|
295
340
|
subscribeNipXX(label);
|
|
296
341
|
// 8. Subscribe to DVM requests (Kind 5xxx) directly from relay
|
|
297
342
|
subscribeDvmRequests(label);
|
|
298
|
-
// 9.
|
|
299
|
-
|
|
343
|
+
// 9. Print startup summary
|
|
344
|
+
const relays = (keys.relays || RELAYS).join(', ');
|
|
345
|
+
console.log('');
|
|
346
|
+
console.log(`═══════════════════════════════════════════════`);
|
|
347
|
+
console.log(` Agent ready: ${agentName}`);
|
|
348
|
+
console.log(` Pubkey: ${keys.pubkey}`);
|
|
349
|
+
console.log(` Kind: ${KIND}`);
|
|
350
|
+
console.log(` Relays: ${relays}`);
|
|
351
|
+
console.log(` Lightning: ${LIGHTNING_ADDRESS || '(not set)'}`);
|
|
352
|
+
console.log(` NWC wallet: ${state.nwcParsed ? 'connected' : '(not set)'}`);
|
|
353
|
+
console.log(` Processor: ${state.processor?.name || 'none'}`);
|
|
354
|
+
console.log(`═══════════════════════════════════════════════`);
|
|
355
|
+
console.log('');
|
|
356
|
+
// 10. Start sovereign heartbeat (Kind 30333 to relay)
|
|
357
|
+
if (state.sovereignKeys && state.relayPool) {
|
|
358
|
+
const pricing = {};
|
|
359
|
+
const priceSats = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
|
|
360
|
+
if (priceSats > 0)
|
|
361
|
+
pricing[String(KIND)] = priceSats;
|
|
362
|
+
state.stopHeartbeat = startNostrHeartbeat(label, state.sovereignKeys, state.relayPool, {
|
|
363
|
+
pricing,
|
|
364
|
+
p2pStatsFn: () => ({
|
|
365
|
+
sessions: state.p2pSessionsCompleted,
|
|
366
|
+
earned_sats: state.p2pTotalEarnedSats,
|
|
367
|
+
active: activeSessions.size > 0,
|
|
368
|
+
}),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
300
371
|
}
|
|
301
372
|
async function publishAiInfo(label) {
|
|
302
373
|
if (!state.sovereignKeys || !state.relayPool)
|
|
@@ -380,9 +451,13 @@ function subscribeNipXX(label) {
|
|
|
380
451
|
});
|
|
381
452
|
console.log(`[${label}] Subscribed to ai.prompt (Kind 25802)`);
|
|
382
453
|
}
|
|
454
|
+
let dvmSubscribed = false;
|
|
383
455
|
function subscribeDvmRequests(label) {
|
|
384
456
|
if (!state.sovereignKeys || !state.relayPool)
|
|
385
457
|
return;
|
|
458
|
+
if (dvmSubscribed)
|
|
459
|
+
return; // prevent double-subscribe (sovereign + platform both call this)
|
|
460
|
+
dvmSubscribed = true;
|
|
386
461
|
// Subscribe to all DVM requests of our kind (broadcast + directed)
|
|
387
462
|
state.relayPool.subscribe({ kinds: [KIND] }, (event) => {
|
|
388
463
|
handleDvmRequest(label, event).catch(e => {
|
|
@@ -448,6 +523,12 @@ async function handleAiPrompt(label, event) {
|
|
|
448
523
|
async function handleDvmRequest(label, event) {
|
|
449
524
|
if (!state.sovereignKeys || !state.relayPool || !state.processor)
|
|
450
525
|
return;
|
|
526
|
+
// Skip own events
|
|
527
|
+
if (event.pubkey === state.sovereignKeys.pubkey)
|
|
528
|
+
return;
|
|
529
|
+
// Dedup: skip already-seen events
|
|
530
|
+
if (!markSeen(event.id))
|
|
531
|
+
return;
|
|
451
532
|
if (!acquireSlot())
|
|
452
533
|
return;
|
|
453
534
|
try {
|
|
@@ -541,28 +622,41 @@ async function publishAiError(clientPubkey, promptId, code, message) {
|
|
|
541
622
|
}, state.sovereignKeys.privkey);
|
|
542
623
|
await state.relayPool.publish(event).catch(() => { });
|
|
543
624
|
}
|
|
544
|
-
function
|
|
545
|
-
if (!state.sovereignKeys || !state.relayPool)
|
|
546
|
-
return;
|
|
625
|
+
function startNostrHeartbeat(label, keys, pool, opts) {
|
|
547
626
|
async function publishHeartbeat() {
|
|
548
|
-
|
|
549
|
-
|
|
627
|
+
const tags = [
|
|
628
|
+
['d', keys.pubkey],
|
|
629
|
+
['status', 'online'],
|
|
630
|
+
['capacity', String(getAvailableCapacity())],
|
|
631
|
+
['kinds', String(KIND)],
|
|
632
|
+
];
|
|
633
|
+
// Add pricing tag (format: "5100:50,5200:100")
|
|
634
|
+
if (opts?.pricing && Object.keys(opts.pricing).length > 0) {
|
|
635
|
+
tags.push(['price', Object.entries(opts.pricing).map(([k, v]) => `${k}:${v}`).join(',')]);
|
|
636
|
+
}
|
|
637
|
+
// Add p2p_stats tag (JSON)
|
|
638
|
+
if (opts?.p2pStatsFn) {
|
|
639
|
+
const stats = opts.p2pStatsFn();
|
|
640
|
+
tags.push(['p2p_stats', JSON.stringify(stats)]);
|
|
641
|
+
}
|
|
550
642
|
const event = signEvent({
|
|
551
643
|
kind: 30333,
|
|
552
|
-
tags
|
|
553
|
-
['d', state.sovereignKeys.pubkey],
|
|
554
|
-
['status', 'online'],
|
|
555
|
-
['capacity', String(getAvailableCapacity())],
|
|
556
|
-
['kinds', String(KIND)],
|
|
557
|
-
],
|
|
644
|
+
tags,
|
|
558
645
|
content: '',
|
|
559
|
-
},
|
|
560
|
-
const ok = await
|
|
646
|
+
}, keys.privkey);
|
|
647
|
+
const ok = await pool.publish(event);
|
|
561
648
|
if (ok)
|
|
562
649
|
console.log(`[${label}] Heartbeat published to relay`);
|
|
563
650
|
}
|
|
564
651
|
publishHeartbeat();
|
|
565
|
-
setInterval(publishHeartbeat,
|
|
652
|
+
const timer = setInterval(publishHeartbeat, 60_000);
|
|
653
|
+
return () => clearInterval(timer);
|
|
654
|
+
}
|
|
655
|
+
/** @deprecated Use startNostrHeartbeat. Kept for backward compat. */
|
|
656
|
+
function startSovereignHeartbeat(label) {
|
|
657
|
+
if (!state.sovereignKeys || !state.relayPool)
|
|
658
|
+
return;
|
|
659
|
+
startNostrHeartbeat(label, state.sovereignKeys, state.relayPool);
|
|
566
660
|
}
|
|
567
661
|
// --- 3. Async Inbox Poller ---
|
|
568
662
|
function startInboxPoller(label) {
|
|
@@ -685,6 +779,20 @@ async function delegateAPI(kind, input, bidSats, provider) {
|
|
|
685
779
|
throw new Error(`Sub-task ${jobId} timed out after 120s`);
|
|
686
780
|
}
|
|
687
781
|
const activeSessions = new Map();
|
|
782
|
+
// Dedup: track recently seen DVM request event IDs (prevent double-processing from relay + inbox)
|
|
783
|
+
const seenEventIds = new Set();
|
|
784
|
+
const MAX_SEEN = 500;
|
|
785
|
+
function markSeen(eventId) {
|
|
786
|
+
if (seenEventIds.has(eventId))
|
|
787
|
+
return false;
|
|
788
|
+
seenEventIds.add(eventId);
|
|
789
|
+
if (seenEventIds.size > MAX_SEEN) {
|
|
790
|
+
const first = seenEventIds.values().next().value;
|
|
791
|
+
if (first)
|
|
792
|
+
seenEventIds.delete(first);
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
688
796
|
/** Send a billing tick to the customer — Cashu: request amount, Invoice: send bolt11 */
|
|
689
797
|
async function sendBillingTick(node, session, amount, label) {
|
|
690
798
|
const tickId = randomBytes(4).toString('hex');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2020117-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "2020117 agent runtime —
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Cashu/Lightning payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"2020117-agent": "./dist/agent.js",
|