@tjamescouch/agentchat 0.18.0 → 0.18.2

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,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
+ }