@elisym/sdk 0.11.0 → 0.12.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/dist/index.cjs +247 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +247 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -320,7 +320,9 @@ declare class NostrPool {
|
|
|
320
320
|
publish(event: Event): Promise<void>;
|
|
321
321
|
/** Publish to all relays and wait for all to settle. Throws if none accepted. */
|
|
322
322
|
publishAll(event: Event): Promise<void>;
|
|
323
|
-
subscribe(filter: Filter, onEvent: (event: Event) => void
|
|
323
|
+
subscribe(filter: Filter, onEvent: (event: Event) => void, opts?: {
|
|
324
|
+
oneose?: () => void;
|
|
325
|
+
}): SubCloser;
|
|
324
326
|
/**
|
|
325
327
|
* Subscribe and wait until at least one relay confirms the subscription
|
|
326
328
|
* is active (EOSE). Resolves on the first relay that responds.
|
|
@@ -402,6 +404,43 @@ declare class DiscoveryService {
|
|
|
402
404
|
* provider.
|
|
403
405
|
*/
|
|
404
406
|
fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
|
|
407
|
+
/**
|
|
408
|
+
* Enrich an agent map with paid-job stats, feedback counters, and kind:0
|
|
409
|
+
* metadata, then return them sorted by `compareAgentsByRank`. Mutates the
|
|
410
|
+
* passed-in `Agent` objects in place.
|
|
411
|
+
*
|
|
412
|
+
* Shared between `fetchAgents` (one-shot) and `streamAgents` (post-EOSE
|
|
413
|
+
* second pass). The `signal` short-circuits the post-query work; in-flight
|
|
414
|
+
* pool queries are not cancellable today (they fall through to the standard
|
|
415
|
+
* timeout) and the caller drops the resolved value.
|
|
416
|
+
*/
|
|
417
|
+
private runEnrichment;
|
|
418
|
+
/**
|
|
419
|
+
* Stream elisym agents progressively as relays deliver events.
|
|
420
|
+
*
|
|
421
|
+
* Two live subscriptions:
|
|
422
|
+
* - kind:31990 (capability cards) - emits `onAgent(agent)` for every new or
|
|
423
|
+
* updated `(pubkey, d-tag)`. The emitted Agent is the merged view across
|
|
424
|
+
* all surviving cards for that author.
|
|
425
|
+
* - kind:6100 (default-offset job results) tagged `t=elisym` since 30d ago -
|
|
426
|
+
* emits `onPaidJob(pubkey, ts)` for each delivered result. Custom-kind
|
|
427
|
+
* results (offset != 100) are not on this stream; they enter the final
|
|
428
|
+
* ranking via the post-EOSE enrichment pass.
|
|
429
|
+
*
|
|
430
|
+
* After capabilities EOSE, an enrichment pass runs in parallel to the live
|
|
431
|
+
* subscriptions and produces a ranked snapshot via `onComplete`. The snapshot
|
|
432
|
+
* is a clone, so further live updates do not mutate it.
|
|
433
|
+
*
|
|
434
|
+
* `closer.close()` tears down both subscriptions and aborts an in-flight
|
|
435
|
+
* enrichment. If `opts.signal` is provided, aborting it does the same.
|
|
436
|
+
*/
|
|
437
|
+
streamAgents(network: Network, opts: {
|
|
438
|
+
onAgent: (agent: Agent) => void;
|
|
439
|
+
onPaidJob?: (pubkey: string, ts: number) => void;
|
|
440
|
+
onEose?: () => void;
|
|
441
|
+
onComplete?: (agents: Agent[]) => void;
|
|
442
|
+
signal?: AbortSignal;
|
|
443
|
+
}): SubCloser;
|
|
405
444
|
/**
|
|
406
445
|
* Publish a capability card (kind:31990) as a provider.
|
|
407
446
|
* Solana address is validated for Base58 format only - full decode
|
package/dist/index.d.ts
CHANGED
|
@@ -320,7 +320,9 @@ declare class NostrPool {
|
|
|
320
320
|
publish(event: Event): Promise<void>;
|
|
321
321
|
/** Publish to all relays and wait for all to settle. Throws if none accepted. */
|
|
322
322
|
publishAll(event: Event): Promise<void>;
|
|
323
|
-
subscribe(filter: Filter, onEvent: (event: Event) => void
|
|
323
|
+
subscribe(filter: Filter, onEvent: (event: Event) => void, opts?: {
|
|
324
|
+
oneose?: () => void;
|
|
325
|
+
}): SubCloser;
|
|
324
326
|
/**
|
|
325
327
|
* Subscribe and wait until at least one relay confirms the subscription
|
|
326
328
|
* is active (EOSE). Resolves on the first relay that responds.
|
|
@@ -402,6 +404,43 @@ declare class DiscoveryService {
|
|
|
402
404
|
* provider.
|
|
403
405
|
*/
|
|
404
406
|
fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
|
|
407
|
+
/**
|
|
408
|
+
* Enrich an agent map with paid-job stats, feedback counters, and kind:0
|
|
409
|
+
* metadata, then return them sorted by `compareAgentsByRank`. Mutates the
|
|
410
|
+
* passed-in `Agent` objects in place.
|
|
411
|
+
*
|
|
412
|
+
* Shared between `fetchAgents` (one-shot) and `streamAgents` (post-EOSE
|
|
413
|
+
* second pass). The `signal` short-circuits the post-query work; in-flight
|
|
414
|
+
* pool queries are not cancellable today (they fall through to the standard
|
|
415
|
+
* timeout) and the caller drops the resolved value.
|
|
416
|
+
*/
|
|
417
|
+
private runEnrichment;
|
|
418
|
+
/**
|
|
419
|
+
* Stream elisym agents progressively as relays deliver events.
|
|
420
|
+
*
|
|
421
|
+
* Two live subscriptions:
|
|
422
|
+
* - kind:31990 (capability cards) - emits `onAgent(agent)` for every new or
|
|
423
|
+
* updated `(pubkey, d-tag)`. The emitted Agent is the merged view across
|
|
424
|
+
* all surviving cards for that author.
|
|
425
|
+
* - kind:6100 (default-offset job results) tagged `t=elisym` since 30d ago -
|
|
426
|
+
* emits `onPaidJob(pubkey, ts)` for each delivered result. Custom-kind
|
|
427
|
+
* results (offset != 100) are not on this stream; they enter the final
|
|
428
|
+
* ranking via the post-EOSE enrichment pass.
|
|
429
|
+
*
|
|
430
|
+
* After capabilities EOSE, an enrichment pass runs in parallel to the live
|
|
431
|
+
* subscriptions and produces a ranked snapshot via `onComplete`. The snapshot
|
|
432
|
+
* is a clone, so further live updates do not mutate it.
|
|
433
|
+
*
|
|
434
|
+
* `closer.close()` tears down both subscriptions and aborts an in-flight
|
|
435
|
+
* enrichment. If `opts.signal` is provided, aborting it does the same.
|
|
436
|
+
*/
|
|
437
|
+
streamAgents(network: Network, opts: {
|
|
438
|
+
onAgent: (agent: Agent) => void;
|
|
439
|
+
onPaidJob?: (pubkey: string, ts: number) => void;
|
|
440
|
+
onEose?: () => void;
|
|
441
|
+
onComplete?: (agents: Agent[]) => void;
|
|
442
|
+
signal?: AbortSignal;
|
|
443
|
+
}): SubCloser;
|
|
405
444
|
/**
|
|
406
445
|
* Publish a capability card (kind:31990) as a provider.
|
|
407
446
|
* Solana address is validated for Base58 format only - full decode
|
package/dist/index.js
CHANGED
|
@@ -1007,6 +1007,7 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
|
|
|
1007
1007
|
var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
|
|
1008
1008
|
var RANKING_BUCKET_SIZE_SECS = 60;
|
|
1009
1009
|
var COLD_START_BUCKET = -Infinity;
|
|
1010
|
+
var NEVER_ABORTED_SIGNAL = new AbortController().signal;
|
|
1010
1011
|
function toDTag(name) {
|
|
1011
1012
|
const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
|
|
1012
1013
|
if (!tag) {
|
|
@@ -1036,13 +1037,63 @@ function compareAgentsByRank(a, b) {
|
|
|
1036
1037
|
}
|
|
1037
1038
|
return kb.lastSeen - ka.lastSeen;
|
|
1038
1039
|
}
|
|
1040
|
+
function parseCapabilityEvent(event, network) {
|
|
1041
|
+
if (!verifyEvent(event)) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
if (!event.content) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
let raw;
|
|
1048
|
+
try {
|
|
1049
|
+
raw = JSON.parse(event.content);
|
|
1050
|
+
} catch {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
if (!raw || typeof raw !== "object") {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
const candidate = raw;
|
|
1057
|
+
if (typeof candidate.name !== "string" || !candidate.name) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof candidate.description !== "string") {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
if (!Array.isArray(candidate.capabilities) || !candidate.capabilities.every((cap) => typeof cap === "string")) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
if (candidate.deleted) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
const card = candidate;
|
|
1070
|
+
if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
const agentNetwork = card.payment?.network ?? "devnet";
|
|
1077
|
+
if (agentNetwork !== network) {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
const kTags = event.tags.filter((tag) => tag[0] === "k").map((tag) => parseInt(tag[1] ?? "", 10)).filter((kind) => !isNaN(kind));
|
|
1081
|
+
return {
|
|
1082
|
+
pubkey: event.pubkey,
|
|
1083
|
+
npub: nip19.npubEncode(event.pubkey),
|
|
1084
|
+
cards: [card],
|
|
1085
|
+
eventId: event.id,
|
|
1086
|
+
supportedKinds: kTags,
|
|
1087
|
+
lastSeen: event.created_at
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1039
1090
|
function buildAgentsFromEvents(events, network) {
|
|
1040
1091
|
const latestByDTag = /* @__PURE__ */ new Map();
|
|
1041
1092
|
for (const event of events) {
|
|
1042
1093
|
if (!verifyEvent(event)) {
|
|
1043
1094
|
continue;
|
|
1044
1095
|
}
|
|
1045
|
-
const dTag = event.tags.find((
|
|
1096
|
+
const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
|
|
1046
1097
|
const key = `${event.pubkey}:${dTag}`;
|
|
1047
1098
|
const prev = latestByDTag.get(key);
|
|
1048
1099
|
if (!prev || event.created_at > prev.created_at) {
|
|
@@ -1051,82 +1102,51 @@ function buildAgentsFromEvents(events, network) {
|
|
|
1051
1102
|
}
|
|
1052
1103
|
const accumMap = /* @__PURE__ */ new Map();
|
|
1053
1104
|
for (const event of latestByDTag.values()) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (raw.deleted) {
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
const card = raw;
|
|
1075
|
-
if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
const agentNetwork = card.payment?.network ?? "devnet";
|
|
1082
|
-
if (agentNetwork !== network) {
|
|
1083
|
-
continue;
|
|
1084
|
-
}
|
|
1085
|
-
const kTags = event.tags.filter((t) => t[0] === "k").map((t) => parseInt(t[1] ?? "", 10)).filter((k) => !isNaN(k));
|
|
1086
|
-
const entry = { card, kTags, createdAt: event.created_at };
|
|
1087
|
-
const existing = accumMap.get(event.pubkey);
|
|
1088
|
-
if (existing) {
|
|
1089
|
-
const dupIndex = existing.entries.findIndex((e) => e.card.name === card.name);
|
|
1090
|
-
if (dupIndex >= 0) {
|
|
1091
|
-
if (entry.createdAt >= existing.entries[dupIndex].createdAt) {
|
|
1092
|
-
existing.entries[dupIndex] = entry;
|
|
1105
|
+
const parsed = parseCapabilityEvent(event, network);
|
|
1106
|
+
if (!parsed) {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const card = parsed.cards[0];
|
|
1110
|
+
const cardKinds = parsed.supportedKinds;
|
|
1111
|
+
const createdAt = parsed.lastSeen;
|
|
1112
|
+
const existing = accumMap.get(parsed.pubkey);
|
|
1113
|
+
if (existing) {
|
|
1114
|
+
const prevForName = existing.perCard.get(card.name);
|
|
1115
|
+
if (prevForName) {
|
|
1116
|
+
if (createdAt >= prevForName.createdAt) {
|
|
1117
|
+
const idx = existing.agent.cards.findIndex(
|
|
1118
|
+
(existingCard) => existingCard.name === card.name
|
|
1119
|
+
);
|
|
1120
|
+
if (idx >= 0) {
|
|
1121
|
+
existing.agent.cards[idx] = card;
|
|
1093
1122
|
}
|
|
1094
|
-
|
|
1095
|
-
existing.entries.push(entry);
|
|
1096
|
-
}
|
|
1097
|
-
if (event.created_at > existing.lastSeen) {
|
|
1098
|
-
existing.lastSeen = event.created_at;
|
|
1099
|
-
existing.eventId = event.id;
|
|
1123
|
+
existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
|
|
1100
1124
|
}
|
|
1101
1125
|
} else {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
npub: nip19.npubEncode(event.pubkey),
|
|
1105
|
-
entries: [entry],
|
|
1106
|
-
eventId: event.id,
|
|
1107
|
-
lastSeen: event.created_at
|
|
1108
|
-
});
|
|
1126
|
+
existing.agent.cards.push(card);
|
|
1127
|
+
existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
|
|
1109
1128
|
}
|
|
1110
|
-
|
|
1129
|
+
if (createdAt > existing.agent.lastSeen) {
|
|
1130
|
+
existing.agent.lastSeen = createdAt;
|
|
1131
|
+
existing.agent.eventId = parsed.eventId;
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
accumMap.set(parsed.pubkey, {
|
|
1135
|
+
agent: parsed,
|
|
1136
|
+
perCard: /* @__PURE__ */ new Map([[card.name, { createdAt, kTags: cardKinds }]])
|
|
1137
|
+
});
|
|
1111
1138
|
}
|
|
1112
1139
|
}
|
|
1113
1140
|
const agentMap = /* @__PURE__ */ new Map();
|
|
1114
1141
|
for (const [pubkey, acc] of accumMap) {
|
|
1115
1142
|
const kindsSet = /* @__PURE__ */ new Set();
|
|
1116
|
-
for (const
|
|
1117
|
-
for (const
|
|
1118
|
-
kindsSet.add(
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
agentMap.set(pubkey,
|
|
1123
|
-
pubkey: acc.pubkey,
|
|
1124
|
-
npub: acc.npub,
|
|
1125
|
-
cards: acc.entries.map((e) => e.card),
|
|
1126
|
-
eventId: acc.eventId,
|
|
1127
|
-
supportedKinds,
|
|
1128
|
-
lastSeen: acc.lastSeen
|
|
1129
|
-
});
|
|
1143
|
+
for (const { kTags } of acc.perCard.values()) {
|
|
1144
|
+
for (const kind of kTags) {
|
|
1145
|
+
kindsSet.add(kind);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
acc.agent.supportedKinds = [...kindsSet];
|
|
1149
|
+
agentMap.set(pubkey, acc.agent);
|
|
1130
1150
|
}
|
|
1131
1151
|
return agentMap;
|
|
1132
1152
|
}
|
|
@@ -1241,6 +1261,19 @@ var DiscoveryService = class {
|
|
|
1241
1261
|
const events = await this.pool.querySync(filter);
|
|
1242
1262
|
const agentMap = buildAgentsFromEvents(events, network);
|
|
1243
1263
|
const agents = Array.from(agentMap.values());
|
|
1264
|
+
return this.runEnrichment(agents, agentMap, NEVER_ABORTED_SIGNAL);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Enrich an agent map with paid-job stats, feedback counters, and kind:0
|
|
1268
|
+
* metadata, then return them sorted by `compareAgentsByRank`. Mutates the
|
|
1269
|
+
* passed-in `Agent` objects in place.
|
|
1270
|
+
*
|
|
1271
|
+
* Shared between `fetchAgents` (one-shot) and `streamAgents` (post-EOSE
|
|
1272
|
+
* second pass). The `signal` short-circuits the post-query work; in-flight
|
|
1273
|
+
* pool queries are not cancellable today (they fall through to the standard
|
|
1274
|
+
* timeout) and the caller drops the resolved value.
|
|
1275
|
+
*/
|
|
1276
|
+
async runEnrichment(agents, agentMap, signal) {
|
|
1244
1277
|
const agentPubkeys = Array.from(agentMap.keys());
|
|
1245
1278
|
if (agentPubkeys.length === 0) {
|
|
1246
1279
|
return agents;
|
|
@@ -1248,9 +1281,9 @@ var DiscoveryService = class {
|
|
|
1248
1281
|
const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
|
|
1249
1282
|
const resultKinds = /* @__PURE__ */ new Set();
|
|
1250
1283
|
for (const agent of agentMap.values()) {
|
|
1251
|
-
for (const
|
|
1252
|
-
if (
|
|
1253
|
-
resultKinds.add(KIND_JOB_RESULT_BASE + (
|
|
1284
|
+
for (const supportedKind of agent.supportedKinds) {
|
|
1285
|
+
if (supportedKind >= KIND_JOB_REQUEST_BASE && supportedKind < KIND_JOB_RESULT_BASE) {
|
|
1286
|
+
resultKinds.add(KIND_JOB_RESULT_BASE + (supportedKind - KIND_JOB_REQUEST_BASE));
|
|
1254
1287
|
}
|
|
1255
1288
|
}
|
|
1256
1289
|
}
|
|
@@ -1270,6 +1303,9 @@ var DiscoveryService = class {
|
|
|
1270
1303
|
),
|
|
1271
1304
|
this.enrichWithMetadata(agents)
|
|
1272
1305
|
]);
|
|
1306
|
+
if (signal.aborted) {
|
|
1307
|
+
return agents;
|
|
1308
|
+
}
|
|
1273
1309
|
const deliveredJobsByProvider = /* @__PURE__ */ new Map();
|
|
1274
1310
|
for (const ev of resultEvents) {
|
|
1275
1311
|
if (!verifyEvent(ev)) {
|
|
@@ -1282,7 +1318,7 @@ var DiscoveryService = class {
|
|
|
1282
1318
|
if (ev.created_at > agent.lastSeen) {
|
|
1283
1319
|
agent.lastSeen = ev.created_at;
|
|
1284
1320
|
}
|
|
1285
|
-
const jobEventId = ev.tags.find((
|
|
1321
|
+
const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
|
|
1286
1322
|
if (jobEventId) {
|
|
1287
1323
|
let delivered = deliveredJobsByProvider.get(ev.pubkey);
|
|
1288
1324
|
if (!delivered) {
|
|
@@ -1296,7 +1332,7 @@ var DiscoveryService = class {
|
|
|
1296
1332
|
if (!verifyEvent(ev)) {
|
|
1297
1333
|
continue;
|
|
1298
1334
|
}
|
|
1299
|
-
const targetPubkey = ev.tags.find((
|
|
1335
|
+
const targetPubkey = ev.tags.find((tag) => tag[0] === "p")?.[1];
|
|
1300
1336
|
if (!targetPubkey) {
|
|
1301
1337
|
continue;
|
|
1302
1338
|
}
|
|
@@ -1307,17 +1343,17 @@ var DiscoveryService = class {
|
|
|
1307
1343
|
if (ev.created_at > agent.lastSeen) {
|
|
1308
1344
|
agent.lastSeen = ev.created_at;
|
|
1309
1345
|
}
|
|
1310
|
-
const rating = ev.tags.find((
|
|
1346
|
+
const rating = ev.tags.find((tag) => tag[0] === "rating")?.[1];
|
|
1311
1347
|
if (rating === "1" || rating === "0") {
|
|
1312
1348
|
agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
|
|
1313
1349
|
if (rating === "1") {
|
|
1314
1350
|
agent.positiveCount = (agent.positiveCount ?? 0) + 1;
|
|
1315
1351
|
}
|
|
1316
1352
|
}
|
|
1317
|
-
const status = ev.tags.find((
|
|
1318
|
-
const txTag = ev.tags.find((
|
|
1353
|
+
const status = ev.tags.find((tag) => tag[0] === "status")?.[1];
|
|
1354
|
+
const txTag = ev.tags.find((tag) => tag[0] === "tx");
|
|
1319
1355
|
const txSignature = txTag?.[1];
|
|
1320
|
-
const jobEventId = ev.tags.find((
|
|
1356
|
+
const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
|
|
1321
1357
|
const hasDeliveredResult = jobEventId !== void 0 && deliveredJobsByProvider.get(targetPubkey)?.has(jobEventId) === true;
|
|
1322
1358
|
if (status === "payment-completed" && typeof txSignature === "string" && txSignature && hasDeliveredResult) {
|
|
1323
1359
|
if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
|
|
@@ -1329,6 +1365,135 @@ var DiscoveryService = class {
|
|
|
1329
1365
|
agents.sort(compareAgentsByRank);
|
|
1330
1366
|
return agents;
|
|
1331
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Stream elisym agents progressively as relays deliver events.
|
|
1370
|
+
*
|
|
1371
|
+
* Two live subscriptions:
|
|
1372
|
+
* - kind:31990 (capability cards) - emits `onAgent(agent)` for every new or
|
|
1373
|
+
* updated `(pubkey, d-tag)`. The emitted Agent is the merged view across
|
|
1374
|
+
* all surviving cards for that author.
|
|
1375
|
+
* - kind:6100 (default-offset job results) tagged `t=elisym` since 30d ago -
|
|
1376
|
+
* emits `onPaidJob(pubkey, ts)` for each delivered result. Custom-kind
|
|
1377
|
+
* results (offset != 100) are not on this stream; they enter the final
|
|
1378
|
+
* ranking via the post-EOSE enrichment pass.
|
|
1379
|
+
*
|
|
1380
|
+
* After capabilities EOSE, an enrichment pass runs in parallel to the live
|
|
1381
|
+
* subscriptions and produces a ranked snapshot via `onComplete`. The snapshot
|
|
1382
|
+
* is a clone, so further live updates do not mutate it.
|
|
1383
|
+
*
|
|
1384
|
+
* `closer.close()` tears down both subscriptions and aborts an in-flight
|
|
1385
|
+
* enrichment. If `opts.signal` is provided, aborting it does the same.
|
|
1386
|
+
*/
|
|
1387
|
+
streamAgents(network, opts) {
|
|
1388
|
+
const eventsByPubkey = /* @__PURE__ */ new Map();
|
|
1389
|
+
const agentByPubkey = /* @__PURE__ */ new Map();
|
|
1390
|
+
const eoseSeen = { caps: false, results: false };
|
|
1391
|
+
let enrichmentStarted = false;
|
|
1392
|
+
const enrichmentAbort = new AbortController();
|
|
1393
|
+
const onExternalAbort = () => enrichmentAbort.abort();
|
|
1394
|
+
if (opts.signal) {
|
|
1395
|
+
if (opts.signal.aborted) {
|
|
1396
|
+
enrichmentAbort.abort();
|
|
1397
|
+
} else {
|
|
1398
|
+
opts.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const checkEose = () => {
|
|
1402
|
+
if (eoseSeen.caps && eoseSeen.results) {
|
|
1403
|
+
opts.onEose?.();
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const startEnrichment = () => {
|
|
1407
|
+
if (enrichmentStarted) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
enrichmentStarted = true;
|
|
1411
|
+
const snapshotAgents = Array.from(agentByPubkey.values()).map((agent) => ({ ...agent }));
|
|
1412
|
+
const snapshotMap = new Map(snapshotAgents.map((agent) => [agent.pubkey, agent]));
|
|
1413
|
+
void this.runEnrichment(snapshotAgents, snapshotMap, enrichmentAbort.signal).then(
|
|
1414
|
+
(sorted) => {
|
|
1415
|
+
if (enrichmentAbort.signal.aborted) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
opts.onComplete?.(sorted);
|
|
1419
|
+
},
|
|
1420
|
+
() => {
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
1423
|
+
};
|
|
1424
|
+
const capSub = this.pool.subscribe(
|
|
1425
|
+
{ kinds: [KIND_APP_HANDLER], "#t": ["elisym"] },
|
|
1426
|
+
(event) => {
|
|
1427
|
+
const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
|
|
1428
|
+
let perDTag = eventsByPubkey.get(event.pubkey);
|
|
1429
|
+
const prev = perDTag?.get(dTag);
|
|
1430
|
+
if (prev && event.created_at <= prev.created_at) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (!verifyEvent(event)) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (!event.content) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
let payload;
|
|
1440
|
+
try {
|
|
1441
|
+
payload = JSON.parse(event.content);
|
|
1442
|
+
} catch {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const isTombstone = payload !== null && typeof payload === "object" && Boolean(payload.deleted);
|
|
1446
|
+
if (!isTombstone && !parseCapabilityEvent(event, network)) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (!perDTag) {
|
|
1450
|
+
perDTag = /* @__PURE__ */ new Map();
|
|
1451
|
+
eventsByPubkey.set(event.pubkey, perDTag);
|
|
1452
|
+
}
|
|
1453
|
+
perDTag.set(dTag, event);
|
|
1454
|
+
const merged = buildAgentsFromEvents(Array.from(perDTag.values()), network).get(
|
|
1455
|
+
event.pubkey
|
|
1456
|
+
);
|
|
1457
|
+
if (!merged) {
|
|
1458
|
+
agentByPubkey.delete(event.pubkey);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
agentByPubkey.set(event.pubkey, merged);
|
|
1462
|
+
opts.onAgent(merged);
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
oneose: () => {
|
|
1466
|
+
eoseSeen.caps = true;
|
|
1467
|
+
startEnrichment();
|
|
1468
|
+
checkEose();
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
);
|
|
1472
|
+
const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
|
|
1473
|
+
const resultsSub = this.pool.subscribe(
|
|
1474
|
+
{ kinds: [KIND_JOB_RESULT], "#t": ["elisym"], since: activitySince },
|
|
1475
|
+
(event) => {
|
|
1476
|
+
if (!verifyEvent(event)) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
opts.onPaidJob?.(event.pubkey, event.created_at);
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
oneose: () => {
|
|
1483
|
+
eoseSeen.results = true;
|
|
1484
|
+
checkEose();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
);
|
|
1488
|
+
return {
|
|
1489
|
+
close: (reason) => {
|
|
1490
|
+
capSub.close(reason);
|
|
1491
|
+
resultsSub.close(reason);
|
|
1492
|
+
enrichmentAbort.abort();
|
|
1493
|
+
opts.signal?.removeEventListener("abort", onExternalAbort);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1332
1497
|
/**
|
|
1333
1498
|
* Publish a capability card (kind:31990) as a provider.
|
|
1334
1499
|
* Solana address is validated for Base58 format only - full decode
|
|
@@ -2544,8 +2709,11 @@ var NostrPool = class {
|
|
|
2544
2709
|
throw new Error(`Failed to publish to all ${this.relays.length} relays`);
|
|
2545
2710
|
}
|
|
2546
2711
|
}
|
|
2547
|
-
subscribe(filter, onEvent) {
|
|
2548
|
-
const rawSub = this.pool.subscribeMany(this.relays, filter, {
|
|
2712
|
+
subscribe(filter, onEvent, opts) {
|
|
2713
|
+
const rawSub = this.pool.subscribeMany(this.relays, filter, {
|
|
2714
|
+
onevent: onEvent,
|
|
2715
|
+
oneose: opts?.oneose
|
|
2716
|
+
});
|
|
2549
2717
|
const tracked = {
|
|
2550
2718
|
close: (reason) => {
|
|
2551
2719
|
this.activeSubscriptions.delete(tracked);
|