@highway1/core 0.1.43 → 0.1.45

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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Capability Matcher
3
+ *
4
+ * Matches semantic queries against agent capabilities
5
+ */
6
+
7
+ import type { Capability } from './agent-card-types.js';
8
+ import type { SemanticQuery } from './search-index.js';
9
+
10
+ /**
11
+ * Capability Matcher
12
+ */
13
+ export class CapabilityMatcher {
14
+ /**
15
+ * Match a query against a capability
16
+ * Returns a score between 0 and 1
17
+ */
18
+ match(query: SemanticQuery, capability: Capability): number {
19
+ let score = 0;
20
+ let weights = 0;
21
+
22
+ // Exact capability ID match
23
+ if (query.capability && capability.id === query.capability) {
24
+ return 1.0;
25
+ }
26
+
27
+ // Fuzzy capability name match
28
+ if (query.capability) {
29
+ const nameScore = this.fuzzyMatch(query.capability, capability.name);
30
+ score += nameScore * 0.4;
31
+ weights += 0.4;
32
+ }
33
+
34
+ // Text keyword match
35
+ if (query.text) {
36
+ const keywords = this.extractKeywords(query.text);
37
+ const keywordScore = this.matchKeywords(keywords, capability);
38
+ score += keywordScore * 0.4;
39
+ weights += 0.4;
40
+ }
41
+
42
+ // Type hierarchy match
43
+ if (query.capability && capability['@type']) {
44
+ const typeScore = this.matchesType(query.capability, capability['@type']) ? 0.6 : 0;
45
+ score += typeScore * 0.2;
46
+ weights += 0.2;
47
+ }
48
+
49
+ return weights > 0 ? score / weights : 0;
50
+ }
51
+
52
+ /**
53
+ * Extract keywords from natural language text
54
+ */
55
+ extractKeywords(text: string): string[] {
56
+ // Remove common stop words
57
+ const stopWords = new Set([
58
+ 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been',
59
+ 'to', 'from', 'in', 'on', 'at', 'by', 'for', 'with', 'about',
60
+ 'can', 'could', 'should', 'would', 'will', 'do', 'does', 'did',
61
+ 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her',
62
+ ]);
63
+
64
+ return text
65
+ .toLowerCase()
66
+ .split(/\W+/)
67
+ .filter(word => word.length > 2 && !stopWords.has(word));
68
+ }
69
+
70
+ /**
71
+ * Match keywords against capability
72
+ */
73
+ matchKeywords(keywords: string[], capability: Capability): number {
74
+ if (keywords.length === 0) return 0;
75
+
76
+ const capText = `${capability.name} ${capability.description}`.toLowerCase();
77
+ const matches = keywords.filter(keyword => capText.includes(keyword));
78
+
79
+ return matches.length / keywords.length;
80
+ }
81
+
82
+ /**
83
+ * Check if query matches capability type hierarchy
84
+ */
85
+ matchesType(query: string, type: string): boolean {
86
+ const queryLower = query.toLowerCase();
87
+ const typeLower = type.toLowerCase();
88
+
89
+ // Direct match
90
+ if (typeLower.includes(queryLower)) return true;
91
+
92
+ // Common type mappings
93
+ const typeMap: Record<string, string[]> = {
94
+ translate: ['translation', 'translationservice'],
95
+ review: ['codereview', 'reviewservice'],
96
+ analyze: ['analysis', 'dataanalysis'],
97
+ generate: ['generation', 'textgeneration', 'imagegeneration'],
98
+ search: ['searchservice', 'query'],
99
+ compute: ['computation', 'computationservice'],
100
+ store: ['storage', 'storageservice'],
101
+ message: ['messaging', 'messagingservice'],
102
+ auth: ['authentication', 'authenticationservice'],
103
+ };
104
+
105
+ for (const [key, values] of Object.entries(typeMap)) {
106
+ if (queryLower.includes(key) && values.some(v => typeLower.includes(v))) {
107
+ return true;
108
+ }
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ /**
115
+ * Fuzzy string matching using Levenshtein distance
116
+ */
117
+ private fuzzyMatch(query: string, target: string): number {
118
+ const queryLower = query.toLowerCase();
119
+ const targetLower = target.toLowerCase();
120
+
121
+ // Exact match
122
+ if (queryLower === targetLower) return 1.0;
123
+
124
+ // Substring match
125
+ if (targetLower.includes(queryLower)) return 0.8;
126
+ if (queryLower.includes(targetLower)) return 0.7;
127
+
128
+ // Levenshtein distance
129
+ const distance = this.levenshteinDistance(queryLower, targetLower);
130
+ const maxLen = Math.max(queryLower.length, targetLower.length);
131
+ const similarity = 1 - distance / maxLen;
132
+
133
+ // Only return positive scores for reasonable similarity
134
+ return similarity > 0.5 ? similarity * 0.6 : 0;
135
+ }
136
+
137
+ /**
138
+ * Calculate Levenshtein distance between two strings
139
+ */
140
+ private levenshteinDistance(a: string, b: string): number {
141
+ const matrix: number[][] = [];
142
+
143
+ for (let i = 0; i <= b.length; i++) {
144
+ matrix[i] = [i];
145
+ }
146
+
147
+ for (let j = 0; j <= a.length; j++) {
148
+ matrix[0][j] = j;
149
+ }
150
+
151
+ for (let i = 1; i <= b.length; i++) {
152
+ for (let j = 1; j <= a.length; j++) {
153
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
154
+ matrix[i][j] = matrix[i - 1][j - 1];
155
+ } else {
156
+ matrix[i][j] = Math.min(
157
+ matrix[i - 1][j - 1] + 1, // substitution
158
+ matrix[i][j - 1] + 1, // insertion
159
+ matrix[i - 1][j] + 1 // deletion
160
+ );
161
+ }
162
+ }
163
+ }
164
+
165
+ return matrix[b.length][a.length];
166
+ }
167
+ }
@@ -0,0 +1,310 @@
1
+ import type { Libp2p } from 'libp2p';
2
+ import type { AgentCard } from './agent-card.js';
3
+ import { createLogger } from '../utils/logger.js';
4
+ import { DiscoveryError } from '../utils/errors.js';
5
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
6
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
7
+ import { encodeForDHT, decodeFromCBOR } from './agent-card-encoder.js';
8
+ import { createSemanticSearch } from './semantic-search.js';
9
+ import type { SemanticQuery } from './search-index.js';
10
+
11
+ const logger = createLogger('dht');
12
+
13
+ export interface ResolvedDID {
14
+ peerId: string;
15
+ multiaddrs: string[];
16
+ }
17
+
18
+ interface CachedPeerInfo {
19
+ peerId: string;
20
+ multiaddrs: string[];
21
+ timestamp: number;
22
+ }
23
+
24
+ const peerCache = new Map<string, CachedPeerInfo>();
25
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
26
+
27
+ export interface DHTOperations {
28
+ publishAgentCard: (card: AgentCard) => Promise<void>;
29
+ queryAgentCard: (did: string) => Promise<AgentCard | null>;
30
+ queryByCapability: (capability: string) => Promise<AgentCard[]>;
31
+ searchSemantic: (query: SemanticQuery) => Promise<AgentCard[]>;
32
+ resolveDID: (did: string) => Promise<ResolvedDID | null>;
33
+ queryRelayPeers: () => Promise<string[]>;
34
+ }
35
+
36
+ /** Extract a DHT value from a get() event, handling both VALUE and PEER_RESPONSE */
37
+ function extractValue(event: any): Uint8Array | null {
38
+ if (event.name === 'VALUE' && event.value) return event.value;
39
+ if (event.name === 'PEER_RESPONSE' && event.value) return event.value;
40
+ return null;
41
+ }
42
+
43
+ /** Normalize capability name for use as a DHT key segment */
44
+ function capKey(cap: string): string {
45
+ return cap.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
46
+ }
47
+
48
+ /** Read a DID list stored at a DHT key (newline-separated) */
49
+ async function readDIDList(dht: any, key: Uint8Array): Promise<string[]> {
50
+ try {
51
+ for await (const event of dht.get(key, { signal: AbortSignal.timeout(30000) })) {
52
+ const raw = extractValue(event);
53
+ if (raw) {
54
+ const text = uint8ArrayToString(raw);
55
+ return text.split('\n').filter(Boolean);
56
+ }
57
+ }
58
+ } catch {
59
+ // key not found or timeout
60
+ }
61
+ return [];
62
+ }
63
+
64
+ /** Write a DID list to a DHT key (newline-separated), deduplicating */
65
+ async function writeDIDList(dht: any, key: Uint8Array, dids: string[]): Promise<void> {
66
+ const value = uint8ArrayFromString([...new Set(dids)].join('\n'));
67
+ // Retry up to 3 times with 30s timeout each
68
+ for (let attempt = 1; attempt <= 3; attempt++) {
69
+ try {
70
+ for await (const _ of dht.put(key, value, { signal: AbortSignal.timeout(30000) })) { /* consume */ }
71
+ return; // Success
72
+ } catch (e: any) {
73
+ if (e?.name === 'AbortError' && attempt < 3) {
74
+ logger.debug(`DHT put timeout, retrying (${attempt}/3)...`);
75
+ continue;
76
+ }
77
+ if (e?.name !== 'AbortError') throw e;
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Create DHT operations for a libp2p node
84
+ */
85
+ export function createDHTOperations(libp2p: Libp2p): DHTOperations {
86
+ const operations: DHTOperations = {} as DHTOperations;
87
+ const searchEngine = createSemanticSearch(operations);
88
+
89
+ return Object.assign(operations, {
90
+ publishAgentCard: async (card: AgentCard) => {
91
+ try {
92
+ const dht = (libp2p as any).services?.dht;
93
+ if (!dht) throw new DiscoveryError('DHT service not available');
94
+
95
+ // 1. Store Agent Card under /clawiverse/agent/<did>
96
+ const agentKey = uint8ArrayFromString(`/clawiverse/agent/${card.did}`);
97
+ // Retry up to 3 times with 30s timeout each
98
+ for (let attempt = 1; attempt <= 3; attempt++) {
99
+ try {
100
+ for await (const _ of dht.put(agentKey, encodeForDHT(card), { signal: AbortSignal.timeout(30000) })) { /* consume */ }
101
+ break; // Success
102
+ } catch (e: any) {
103
+ if (e?.name === 'AbortError' && attempt < 3) {
104
+ logger.debug(`DHT put agent card timeout, retrying (${attempt}/3)...`);
105
+ continue;
106
+ }
107
+ logger.warn('DHT put agent card failed (non-fatal)', { error: (e as Error).message });
108
+ break;
109
+ }
110
+ }
111
+
112
+ // 2. For each capability, append this DID to /clawiverse/cap/<capability>
113
+ // Skip capability indexing in slow networks - rely on local index and semantic search instead
114
+ const caps: string[] = (card.capabilities ?? []).flatMap((c: any) => {
115
+ if (typeof c === 'string') return [c];
116
+ return [c.name, c.id].filter((v): v is string => typeof v === 'string' && v.length > 0);
117
+ });
118
+
119
+ // Always index under the special "all" key so name/description search works
120
+ caps.push('__all__');
121
+
122
+ // Keep indexing lightweight, but preserve relay discovery path.
123
+ const importantCaps = ['__all__'];
124
+ if (caps.some((cap) => capKey(cap) === 'relay')) {
125
+ importantCaps.push('relay');
126
+ }
127
+ await Promise.all(importantCaps.map(async (cap) => {
128
+ const capKeyStr = `/clawiverse/cap/${capKey(cap)}`;
129
+ const capDHTKey = uint8ArrayFromString(capKeyStr);
130
+ try {
131
+ const existing = await readDIDList(dht, capDHTKey);
132
+ if (!existing.includes(card.did)) {
133
+ await writeDIDList(dht, capDHTKey, [...existing, card.did]);
134
+ logger.debug('Indexed capability in DHT', { cap: capKey(cap), did: card.did });
135
+ }
136
+ } catch (e: any) {
137
+ logger.warn('Failed to index capability (non-fatal)', { cap: capKey(cap), error: (e as Error).message });
138
+ }
139
+ }));
140
+
141
+ // 3. Index locally for fast in-process search
142
+ searchEngine.indexAgentCard(card);
143
+
144
+ logger.info('Published Agent Card to DHT', { did: card.did });
145
+ } catch (error) {
146
+ // DHT publish failure is non-fatal - local index is still available
147
+ logger.warn('Failed to publish Agent Card to DHT (non-fatal)', { error: (error as Error).message });
148
+ // Still index locally even if DHT fails
149
+ searchEngine.indexAgentCard(card);
150
+ }
151
+ },
152
+
153
+ queryAgentCard: async (did: string) => {
154
+ try {
155
+ const dht = (libp2p as any).services?.dht;
156
+ if (!dht) throw new DiscoveryError('DHT service not available');
157
+
158
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
159
+ for await (const event of dht.get(key)) {
160
+ const raw = extractValue(event);
161
+ if (raw) {
162
+ const card = decodeFromCBOR(raw);
163
+ searchEngine.indexAgentCard(card);
164
+ logger.debug('Found Agent Card in DHT', { did });
165
+ return card;
166
+ }
167
+ }
168
+
169
+ logger.debug('Agent Card not found in DHT', { did });
170
+ return null;
171
+ } catch (error) {
172
+ logger.warn('Failed to query Agent Card', { did, error });
173
+ return null;
174
+ }
175
+ },
176
+
177
+ queryByCapability: async (capability: string) => {
178
+ try {
179
+ const dht = (libp2p as any).services?.dht;
180
+
181
+ // 1. Check local index first
182
+ const local = searchEngine.getAllIndexedCards().filter(card =>
183
+ card.capabilities.some((cap: any) => {
184
+ const name = typeof cap === 'string' ? cap : cap.name;
185
+ return name?.toLowerCase().includes(capability.toLowerCase());
186
+ })
187
+ );
188
+ if (local.length > 0) return local;
189
+
190
+ // 2. Fall back to DHT capability index
191
+ if (!dht) return [];
192
+
193
+ const capDHTKey = uint8ArrayFromString(`/clawiverse/cap/${capKey(capability)}`);
194
+ const dids = await readDIDList(dht, capDHTKey);
195
+ logger.debug('DHT capability index', { capability, dids });
196
+
197
+ // Fetch each Agent Card in parallel
198
+ const cards = await Promise.all(
199
+ dids.map(async (did) => {
200
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
201
+ try {
202
+ for await (const event of dht.get(key)) {
203
+ const raw = extractValue(event);
204
+ if (raw) {
205
+ const card = decodeFromCBOR(raw);
206
+ searchEngine.indexAgentCard(card);
207
+ return card;
208
+ }
209
+ }
210
+ } catch { /* skip unreachable */ }
211
+ return null;
212
+ })
213
+ );
214
+
215
+ return cards.filter((c): c is AgentCard => c !== null);
216
+ } catch (error) {
217
+ throw new DiscoveryError('Failed to query by capability', error);
218
+ }
219
+ },
220
+
221
+ searchSemantic: async (query: SemanticQuery) => {
222
+ try {
223
+ // Always pull from DHT __all__ index to discover remote nodes
224
+ const dht = (libp2p as any).services?.dht;
225
+ if (dht) {
226
+ const allKey = uint8ArrayFromString('/clawiverse/cap/__all__');
227
+ const dids = await readDIDList(dht, allKey);
228
+ logger.debug('DHT __all__ index', { count: dids.length });
229
+
230
+ await Promise.all(
231
+ dids.map(async (did) => {
232
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
233
+ try {
234
+ for await (const event of dht.get(key)) {
235
+ const raw = extractValue(event);
236
+ if (raw) {
237
+ searchEngine.indexAgentCard(decodeFromCBOR(raw));
238
+ break;
239
+ }
240
+ }
241
+ } catch { /* skip */ }
242
+ })
243
+ );
244
+ }
245
+
246
+ return searchEngine.search(query);
247
+ } catch (error) {
248
+ throw new DiscoveryError('Failed to perform semantic search', error);
249
+ }
250
+ },
251
+
252
+ queryRelayPeers: async (): Promise<string[]> => {
253
+ const dht = (libp2p as any).services?.dht;
254
+ if (!dht) return [];
255
+ const capDHTKey = uint8ArrayFromString('/clawiverse/cap/relay');
256
+ const dids = await readDIDList(dht, capDHTKey);
257
+ const addrs: string[] = [];
258
+ await Promise.all(dids.map(async (did) => {
259
+ const card = await operations.queryAgentCard(did);
260
+ if (card?.endpoints) {
261
+ addrs.push(...card.endpoints.filter((e: string) => !e.includes('/p2p-circuit/')));
262
+ }
263
+ }));
264
+ return addrs;
265
+ },
266
+
267
+ resolveDID: async (did: string) => {
268
+ try {
269
+ // Check cache first
270
+ const cached = peerCache.get(did);
271
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
272
+ logger.debug('Using cached peer info', { did });
273
+ return { peerId: cached.peerId, multiaddrs: cached.multiaddrs };
274
+ }
275
+
276
+ const dht = (libp2p as any).services?.dht;
277
+ if (!dht) throw new DiscoveryError('DHT service not available');
278
+
279
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
280
+ for await (const event of dht.get(key)) {
281
+ const raw = extractValue(event);
282
+ if (raw) {
283
+ const card = decodeFromCBOR(raw);
284
+ if (card.peerId) {
285
+ logger.debug('Resolved DID to peer', { did, peerId: card.peerId });
286
+ const result = { peerId: card.peerId, multiaddrs: card.endpoints || [] };
287
+
288
+ // Cache the result
289
+ peerCache.set(did, {
290
+ peerId: result.peerId,
291
+ multiaddrs: result.multiaddrs,
292
+ timestamp: Date.now()
293
+ });
294
+
295
+ return result;
296
+ }
297
+ logger.warn('Agent Card found but has no peerId', { did });
298
+ return null;
299
+ }
300
+ }
301
+
302
+ logger.debug('DID not found in DHT', { did });
303
+ return null;
304
+ } catch (error) {
305
+ logger.warn('Failed to resolve DID', { did, error });
306
+ return null;
307
+ }
308
+ },
309
+ });
310
+ }
@@ -0,0 +1,3 @@
1
+ export * from './agent-card.js';
2
+ export * from './dht.js';
3
+ export * from './bootstrap.js';