@tjamescouch/agentchat 0.18.1 → 0.18.3
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/bin/agentchat.js +1 -2
- package/lib/daemon.js +15 -1
- package/lib/server/handlers/identity.js +242 -0
- package/lib/server/handlers/index.js +42 -0
- package/lib/server/handlers/message.js +306 -0
- package/lib/server/handlers/presence.js +44 -0
- package/lib/server/handlers/proposal.js +358 -0
- package/lib/server/handlers/skills.js +141 -0
- package/lib/server.js +52 -1007
- package/package.json +1 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposal Handlers
|
|
3
|
+
* Handles proposal, accept, reject, complete, dispute operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ServerMessageType,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
createMessage,
|
|
10
|
+
createError,
|
|
11
|
+
} from '../../protocol.js';
|
|
12
|
+
import { formatProposal, formatProposalResponse } from '../../proposals.js';
|
|
13
|
+
import {
|
|
14
|
+
EscrowEvent,
|
|
15
|
+
createEscrowCreatedPayload,
|
|
16
|
+
createCompletionPayload,
|
|
17
|
+
createDisputePayload,
|
|
18
|
+
} from '../../escrow-hooks.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handle PROPOSAL command
|
|
22
|
+
*/
|
|
23
|
+
export function handleProposal(server, ws, msg) {
|
|
24
|
+
const agent = server.agents.get(ws);
|
|
25
|
+
if (!agent) {
|
|
26
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Proposals require a persistent identity (signature verification)
|
|
31
|
+
if (!agent.pubkey) {
|
|
32
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Proposals require persistent identity'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const targetId = msg.to.slice(1);
|
|
37
|
+
const targetWs = server.agentById.get(targetId);
|
|
38
|
+
|
|
39
|
+
if (!targetWs) {
|
|
40
|
+
server._send(ws, createError(ErrorCode.AGENT_NOT_FOUND, `Agent ${msg.to} not found`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create proposal in store
|
|
45
|
+
const proposal = server.proposals.create({
|
|
46
|
+
from: `@${agent.id}`,
|
|
47
|
+
to: msg.to,
|
|
48
|
+
task: msg.task,
|
|
49
|
+
amount: msg.amount,
|
|
50
|
+
currency: msg.currency,
|
|
51
|
+
payment_code: msg.payment_code,
|
|
52
|
+
terms: msg.terms,
|
|
53
|
+
expires: msg.expires,
|
|
54
|
+
sig: msg.sig,
|
|
55
|
+
elo_stake: msg.elo_stake || null
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
server._log('proposal', { id: proposal.id, from: agent.id, to: targetId });
|
|
59
|
+
|
|
60
|
+
// Send to target
|
|
61
|
+
const outMsg = createMessage(ServerMessageType.PROPOSAL, {
|
|
62
|
+
...formatProposal(proposal)
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
server._send(targetWs, outMsg);
|
|
66
|
+
// Echo back to sender with the assigned ID
|
|
67
|
+
server._send(ws, outMsg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle ACCEPT command
|
|
72
|
+
*/
|
|
73
|
+
export async function handleAccept(server, ws, msg) {
|
|
74
|
+
const agent = server.agents.get(ws);
|
|
75
|
+
if (!agent) {
|
|
76
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!agent.pubkey) {
|
|
81
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Accepting proposals requires persistent identity'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get proposal first to check stakes
|
|
86
|
+
const existingProposal = server.proposals.get(msg.proposal_id);
|
|
87
|
+
if (!existingProposal) {
|
|
88
|
+
server._send(ws, createError(ErrorCode.PROPOSAL_NOT_FOUND, 'Proposal not found'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const proposerStake = existingProposal.proposer_stake || 0;
|
|
93
|
+
const acceptorStake = msg.elo_stake || 0;
|
|
94
|
+
|
|
95
|
+
// Validate proposer can stake (if they declared a stake)
|
|
96
|
+
if (proposerStake > 0) {
|
|
97
|
+
const canProposerStake = await server.reputationStore.canStake(existingProposal.from, proposerStake);
|
|
98
|
+
if (!canProposerStake.canStake) {
|
|
99
|
+
server._send(ws, createError(ErrorCode.INSUFFICIENT_REPUTATION, `Proposer: ${canProposerStake.reason}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate acceptor can stake (if they declared a stake)
|
|
105
|
+
if (acceptorStake > 0) {
|
|
106
|
+
const canAcceptorStake = await server.reputationStore.canStake(`@${agent.id}`, acceptorStake);
|
|
107
|
+
if (!canAcceptorStake.canStake) {
|
|
108
|
+
server._send(ws, createError(ErrorCode.INSUFFICIENT_REPUTATION, canAcceptorStake.reason));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = server.proposals.accept(
|
|
114
|
+
msg.proposal_id,
|
|
115
|
+
`@${agent.id}`,
|
|
116
|
+
msg.sig,
|
|
117
|
+
msg.payment_code,
|
|
118
|
+
acceptorStake
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (result.error) {
|
|
122
|
+
server._send(ws, createError(ErrorCode.INVALID_PROPOSAL, result.error));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const proposal = result.proposal;
|
|
127
|
+
|
|
128
|
+
// Create escrow if either party has a stake
|
|
129
|
+
if (proposerStake > 0 || acceptorStake > 0) {
|
|
130
|
+
const escrowResult = await server.reputationStore.createEscrow(
|
|
131
|
+
proposal.id,
|
|
132
|
+
{ agent_id: proposal.from, stake: proposerStake },
|
|
133
|
+
{ agent_id: proposal.to, stake: acceptorStake },
|
|
134
|
+
proposal.expires
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (escrowResult.success) {
|
|
138
|
+
proposal.stakes_escrowed = true;
|
|
139
|
+
server._log('escrow_created', {
|
|
140
|
+
proposal_id: proposal.id,
|
|
141
|
+
proposer_stake: proposerStake,
|
|
142
|
+
acceptor_stake: acceptorStake
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Emit escrow:created hook for external integrations
|
|
146
|
+
server.escrowHooks.emit(EscrowEvent.CREATED, createEscrowCreatedPayload(proposal, escrowResult))
|
|
147
|
+
.catch(err => server._log('escrow_hook_error', { event: 'created', error: err.message }));
|
|
148
|
+
} else {
|
|
149
|
+
server._log('escrow_error', { proposal_id: proposal.id, error: escrowResult.error });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
server._log('accept', { id: proposal.id, by: agent.id, proposer_stake: proposerStake, acceptor_stake: acceptorStake });
|
|
154
|
+
|
|
155
|
+
// Notify the proposal creator
|
|
156
|
+
const creatorId = proposal.from.slice(1);
|
|
157
|
+
const creatorWs = server.agentById.get(creatorId);
|
|
158
|
+
|
|
159
|
+
const outMsg = createMessage(ServerMessageType.ACCEPT, {
|
|
160
|
+
...formatProposalResponse(proposal, 'accept')
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (creatorWs) {
|
|
164
|
+
server._send(creatorWs, outMsg);
|
|
165
|
+
}
|
|
166
|
+
// Echo to acceptor
|
|
167
|
+
server._send(ws, outMsg);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle REJECT command
|
|
172
|
+
*/
|
|
173
|
+
export function handleReject(server, ws, msg) {
|
|
174
|
+
const agent = server.agents.get(ws);
|
|
175
|
+
if (!agent) {
|
|
176
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!agent.pubkey) {
|
|
181
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Rejecting proposals requires persistent identity'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result = server.proposals.reject(
|
|
186
|
+
msg.proposal_id,
|
|
187
|
+
`@${agent.id}`,
|
|
188
|
+
msg.sig,
|
|
189
|
+
msg.reason
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (result.error) {
|
|
193
|
+
server._send(ws, createError(ErrorCode.INVALID_PROPOSAL, result.error));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const proposal = result.proposal;
|
|
198
|
+
server._log('reject', { id: proposal.id, by: agent.id });
|
|
199
|
+
|
|
200
|
+
// Notify the proposal creator
|
|
201
|
+
const creatorId = proposal.from.slice(1);
|
|
202
|
+
const creatorWs = server.agentById.get(creatorId);
|
|
203
|
+
|
|
204
|
+
const outMsg = createMessage(ServerMessageType.REJECT, {
|
|
205
|
+
...formatProposalResponse(proposal, 'reject')
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (creatorWs) {
|
|
209
|
+
server._send(creatorWs, outMsg);
|
|
210
|
+
}
|
|
211
|
+
// Echo to rejector
|
|
212
|
+
server._send(ws, outMsg);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle COMPLETE command
|
|
217
|
+
*/
|
|
218
|
+
export async function handleComplete(server, ws, msg) {
|
|
219
|
+
const agent = server.agents.get(ws);
|
|
220
|
+
if (!agent) {
|
|
221
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!agent.pubkey) {
|
|
226
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Completing proposals requires persistent identity'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const result = server.proposals.complete(
|
|
231
|
+
msg.proposal_id,
|
|
232
|
+
`@${agent.id}`,
|
|
233
|
+
msg.sig,
|
|
234
|
+
msg.proof
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (result.error) {
|
|
238
|
+
server._send(ws, createError(ErrorCode.INVALID_PROPOSAL, result.error));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const proposal = result.proposal;
|
|
243
|
+
server._log('complete', { id: proposal.id, by: agent.id });
|
|
244
|
+
|
|
245
|
+
// Update reputation ratings (includes escrow settlement)
|
|
246
|
+
let ratingChanges = null;
|
|
247
|
+
try {
|
|
248
|
+
ratingChanges = await server.reputationStore.processCompletion({
|
|
249
|
+
type: 'COMPLETE',
|
|
250
|
+
proposal_id: proposal.id,
|
|
251
|
+
from: proposal.from,
|
|
252
|
+
to: proposal.to,
|
|
253
|
+
amount: proposal.amount
|
|
254
|
+
});
|
|
255
|
+
server._log('reputation_updated', {
|
|
256
|
+
proposal_id: proposal.id,
|
|
257
|
+
changes: ratingChanges,
|
|
258
|
+
escrow: ratingChanges?._escrow
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Emit settlement:completion hook for external integrations
|
|
262
|
+
if (ratingChanges?._escrow) {
|
|
263
|
+
server.escrowHooks.emit(EscrowEvent.COMPLETION_SETTLED, createCompletionPayload(proposal, ratingChanges))
|
|
264
|
+
.catch(err => server._log('escrow_hook_error', { event: 'completion', error: err.message }));
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
server._log('reputation_error', { error: err.message });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Notify both parties
|
|
271
|
+
const outMsg = createMessage(ServerMessageType.COMPLETE, {
|
|
272
|
+
...formatProposalResponse(proposal, 'complete'),
|
|
273
|
+
rating_changes: ratingChanges
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Notify the other party
|
|
277
|
+
const otherId = proposal.from === `@${agent.id}` ? proposal.to.slice(1) : proposal.from.slice(1);
|
|
278
|
+
const otherWs = server.agentById.get(otherId);
|
|
279
|
+
|
|
280
|
+
if (otherWs) {
|
|
281
|
+
server._send(otherWs, outMsg);
|
|
282
|
+
}
|
|
283
|
+
// Echo to completer
|
|
284
|
+
server._send(ws, outMsg);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Handle DISPUTE command
|
|
289
|
+
*/
|
|
290
|
+
export async function handleDispute(server, ws, msg) {
|
|
291
|
+
const agent = server.agents.get(ws);
|
|
292
|
+
if (!agent) {
|
|
293
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!agent.pubkey) {
|
|
298
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Disputing proposals requires persistent identity'));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const result = server.proposals.dispute(
|
|
303
|
+
msg.proposal_id,
|
|
304
|
+
`@${agent.id}`,
|
|
305
|
+
msg.sig,
|
|
306
|
+
msg.reason
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (result.error) {
|
|
310
|
+
server._send(ws, createError(ErrorCode.INVALID_PROPOSAL, result.error));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const proposal = result.proposal;
|
|
315
|
+
server._log('dispute', { id: proposal.id, by: agent.id, reason: msg.reason });
|
|
316
|
+
|
|
317
|
+
// Update reputation ratings (includes escrow settlement)
|
|
318
|
+
let ratingChanges = null;
|
|
319
|
+
try {
|
|
320
|
+
ratingChanges = await server.reputationStore.processDispute({
|
|
321
|
+
type: 'DISPUTE',
|
|
322
|
+
proposal_id: proposal.id,
|
|
323
|
+
from: proposal.from,
|
|
324
|
+
to: proposal.to,
|
|
325
|
+
amount: proposal.amount,
|
|
326
|
+
disputed_by: `@${agent.id}`
|
|
327
|
+
});
|
|
328
|
+
server._log('reputation_updated', {
|
|
329
|
+
proposal_id: proposal.id,
|
|
330
|
+
changes: ratingChanges,
|
|
331
|
+
escrow: ratingChanges?._escrow
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Emit settlement:dispute hook for external integrations
|
|
335
|
+
if (ratingChanges?._escrow) {
|
|
336
|
+
server.escrowHooks.emit(EscrowEvent.DISPUTE_SETTLED, createDisputePayload(proposal, ratingChanges))
|
|
337
|
+
.catch(err => server._log('escrow_hook_error', { event: 'dispute', error: err.message }));
|
|
338
|
+
}
|
|
339
|
+
} catch (err) {
|
|
340
|
+
server._log('reputation_error', { error: err.message });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Notify both parties
|
|
344
|
+
const outMsg = createMessage(ServerMessageType.DISPUTE, {
|
|
345
|
+
...formatProposalResponse(proposal, 'dispute'),
|
|
346
|
+
rating_changes: ratingChanges
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Notify the other party
|
|
350
|
+
const otherId = proposal.from === `@${agent.id}` ? proposal.to.slice(1) : proposal.from.slice(1);
|
|
351
|
+
const otherWs = server.agentById.get(otherId);
|
|
352
|
+
|
|
353
|
+
if (otherWs) {
|
|
354
|
+
server._send(otherWs, outMsg);
|
|
355
|
+
}
|
|
356
|
+
// Echo to disputer
|
|
357
|
+
server._send(ws, outMsg);
|
|
358
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Handlers
|
|
3
|
+
* Handles skill registration and search
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ServerMessageType,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
createMessage,
|
|
10
|
+
createError,
|
|
11
|
+
} from '../../protocol.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle REGISTER_SKILLS command
|
|
15
|
+
*/
|
|
16
|
+
export function handleRegisterSkills(server, ws, msg) {
|
|
17
|
+
const agent = server.agents.get(ws);
|
|
18
|
+
if (!agent) {
|
|
19
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!agent.pubkey) {
|
|
24
|
+
server._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Skill registration requires persistent identity'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Store skills for this agent
|
|
29
|
+
const registration = {
|
|
30
|
+
agent_id: `@${agent.id}`,
|
|
31
|
+
skills: msg.skills,
|
|
32
|
+
registered_at: Date.now(),
|
|
33
|
+
sig: msg.sig
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
server.skillsRegistry.set(agent.id, registration);
|
|
37
|
+
|
|
38
|
+
server._log('skills_registered', { agent: agent.id, count: msg.skills.length });
|
|
39
|
+
|
|
40
|
+
// Notify the registering agent
|
|
41
|
+
server._send(ws, createMessage(ServerMessageType.SKILLS_REGISTERED, {
|
|
42
|
+
agent_id: `@${agent.id}`,
|
|
43
|
+
skills_count: msg.skills.length,
|
|
44
|
+
registered_at: registration.registered_at
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Optionally broadcast to #discovery channel if it exists
|
|
48
|
+
if (server.channels.has('#discovery')) {
|
|
49
|
+
server._broadcast('#discovery', createMessage(ServerMessageType.MSG, {
|
|
50
|
+
from: '@server',
|
|
51
|
+
to: '#discovery',
|
|
52
|
+
content: `Agent @${agent.id} registered ${msg.skills.length} skill(s): ${msg.skills.map(s => s.capability).join(', ')}`
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle SEARCH_SKILLS command
|
|
59
|
+
*/
|
|
60
|
+
export async function handleSearchSkills(server, ws, msg) {
|
|
61
|
+
const agent = server.agents.get(ws);
|
|
62
|
+
if (!agent) {
|
|
63
|
+
server._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const query = msg.query || {};
|
|
68
|
+
const results = [];
|
|
69
|
+
|
|
70
|
+
// Search through all registered skills
|
|
71
|
+
for (const [agentId, registration] of server.skillsRegistry) {
|
|
72
|
+
for (const skill of registration.skills) {
|
|
73
|
+
let matches = true;
|
|
74
|
+
|
|
75
|
+
// Filter by capability (substring match, case-insensitive)
|
|
76
|
+
if (query.capability) {
|
|
77
|
+
const cap = skill.capability.toLowerCase();
|
|
78
|
+
const search = query.capability.toLowerCase();
|
|
79
|
+
if (!cap.includes(search)) {
|
|
80
|
+
matches = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Filter by max_rate
|
|
85
|
+
if (query.max_rate !== undefined && skill.rate !== undefined) {
|
|
86
|
+
if (skill.rate > query.max_rate) {
|
|
87
|
+
matches = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Filter by currency
|
|
92
|
+
if (query.currency && skill.currency) {
|
|
93
|
+
if (skill.currency.toLowerCase() !== query.currency.toLowerCase()) {
|
|
94
|
+
matches = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (matches) {
|
|
99
|
+
results.push({
|
|
100
|
+
agent_id: registration.agent_id,
|
|
101
|
+
...skill,
|
|
102
|
+
registered_at: registration.registered_at
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Enrich results with reputation data
|
|
109
|
+
const uniqueAgentIds = [...new Set(results.map(r => r.agent_id))];
|
|
110
|
+
const ratingCache = new Map();
|
|
111
|
+
for (const agentId of uniqueAgentIds) {
|
|
112
|
+
const ratingInfo = await server.reputationStore.getRating(agentId);
|
|
113
|
+
ratingCache.set(agentId, ratingInfo);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add rating info to each result
|
|
117
|
+
for (const result of results) {
|
|
118
|
+
const ratingInfo = ratingCache.get(result.agent_id);
|
|
119
|
+
result.rating = ratingInfo.rating;
|
|
120
|
+
result.transactions = ratingInfo.transactions;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Sort by rating (highest first), then by registration time
|
|
124
|
+
results.sort((a, b) => {
|
|
125
|
+
if (b.rating !== a.rating) return b.rating - a.rating;
|
|
126
|
+
return b.registered_at - a.registered_at;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Limit results
|
|
130
|
+
const limit = query.limit || 50;
|
|
131
|
+
const limitedResults = results.slice(0, limit);
|
|
132
|
+
|
|
133
|
+
server._log('skills_search', { agent: agent.id, query, results_count: limitedResults.length });
|
|
134
|
+
|
|
135
|
+
server._send(ws, createMessage(ServerMessageType.SEARCH_RESULTS, {
|
|
136
|
+
query_id: msg.query_id || null,
|
|
137
|
+
query,
|
|
138
|
+
results: limitedResults,
|
|
139
|
+
total: results.length
|
|
140
|
+
}));
|
|
141
|
+
}
|