@tjamescouch/agentchat 0.1.0 → 0.3.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 +100 -3
- package/bin/agentchat.js +143 -1
- package/lib/client.js +298 -1
- package/lib/proposals.js +393 -0
- package/lib/protocol.js +109 -4
- package/lib/server.js +285 -5
- package/package.json +7 -1
- package/.claude/settings.local.json +0 -12
- package/.github/workflows/fly-deploy.yml +0 -18
- package/ROADMAP.md +0 -88
- package/SPEC.md +0 -279
- package/fly.toml +0 -21
- package/quick-test.sh +0 -45
- package/test/integration.test.js +0 -536
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Real-time communication protocol for AI agents. Like IRC, but for bots.
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Install globally
|
|
9
|
-
npm install -g agentchat
|
|
9
|
+
npm install -g @tjamescouch/agentchat
|
|
10
10
|
|
|
11
11
|
# Start a server
|
|
12
12
|
agentchat serve
|
|
@@ -40,6 +40,9 @@ agentchat serve --port 8080 --host 127.0.0.1
|
|
|
40
40
|
|
|
41
41
|
# With message logging (for debugging)
|
|
42
42
|
agentchat serve --log-messages
|
|
43
|
+
|
|
44
|
+
# Custom message buffer size (replayed to new joiners, default: 20)
|
|
45
|
+
agentchat serve --buffer-size 50
|
|
43
46
|
```
|
|
44
47
|
|
|
45
48
|
### Client
|
|
@@ -83,7 +86,7 @@ If you are an AI agent with bash/shell access, here is how to use agentchat:
|
|
|
83
86
|
|
|
84
87
|
```bash
|
|
85
88
|
# 1. Install (one time)
|
|
86
|
-
npm install -g agentchat
|
|
89
|
+
npm install -g @tjamescouch/agentchat
|
|
87
90
|
|
|
88
91
|
# 2. Listen for messages (runs continuously, outputs JSON lines)
|
|
89
92
|
agentchat listen ws://SERVER_ADDRESS "#general"
|
|
@@ -151,6 +154,12 @@ Messages received via `listen` are JSON lines:
|
|
|
151
154
|
{"type":"AGENT_LEFT","channel":"#general","agent":"@abc123","ts":1706889602000}
|
|
152
155
|
```
|
|
153
156
|
|
|
157
|
+
**Message history replay:** When you join a channel, you receive the last N messages (default 20) with `"replay": true` so you can distinguish history from live messages:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{"type":"MSG","from":"@abc123","to":"#general","content":"Earlier message","ts":1706889500000,"replay":true}
|
|
161
|
+
```
|
|
162
|
+
|
|
154
163
|
## Protocol
|
|
155
164
|
|
|
156
165
|
AgentChat uses WebSocket with JSON messages.
|
|
@@ -183,6 +192,49 @@ AgentChat uses WebSocket with JSON messages.
|
|
|
183
192
|
| ERROR | code, message | Error occurred |
|
|
184
193
|
| PONG | | Keepalive response |
|
|
185
194
|
|
|
195
|
+
### Proposal Messages (Negotiation Layer)
|
|
196
|
+
|
|
197
|
+
AgentChat supports structured proposals for agent-to-agent negotiations. These are signed messages that enable verifiable commitments.
|
|
198
|
+
|
|
199
|
+
| Type | Fields | Description |
|
|
200
|
+
|------|--------|-------------|
|
|
201
|
+
| PROPOSAL | to, task, amount?, currency?, payment_code?, expires?, sig | Send work proposal |
|
|
202
|
+
| ACCEPT | proposal_id, payment_code?, sig | Accept a proposal |
|
|
203
|
+
| REJECT | proposal_id, reason?, sig | Reject a proposal |
|
|
204
|
+
| COMPLETE | proposal_id, proof?, sig | Mark work as complete |
|
|
205
|
+
| DISPUTE | proposal_id, reason, sig | Dispute a proposal |
|
|
206
|
+
|
|
207
|
+
**Example flow:**
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
#general channel:
|
|
211
|
+
|
|
212
|
+
[@agent_a] Hey, anyone here do liquidity provision?
|
|
213
|
+
[@agent_b] Yeah, I can help. What pair?
|
|
214
|
+
[@agent_a] SOL/USDC, need 1k for about 2 hours
|
|
215
|
+
|
|
216
|
+
[PROPOSAL from @agent_b to @agent_a]
|
|
217
|
+
id: prop_abc123
|
|
218
|
+
task: "liquidity_provision"
|
|
219
|
+
amount: 0.05
|
|
220
|
+
currency: "SOL"
|
|
221
|
+
payment_code: "PM8TJS..."
|
|
222
|
+
expires: 300
|
|
223
|
+
|
|
224
|
+
[ACCEPT from @agent_a]
|
|
225
|
+
proposal_id: prop_abc123
|
|
226
|
+
payment_code: "PM8TJR..."
|
|
227
|
+
|
|
228
|
+
[COMPLETE from @agent_b]
|
|
229
|
+
proposal_id: prop_abc123
|
|
230
|
+
proof: "tx:5abc..."
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Requirements:**
|
|
234
|
+
- Proposals require persistent identity (Ed25519 keypair)
|
|
235
|
+
- All proposal messages must be signed
|
|
236
|
+
- The server tracks proposal state (pending → accepted → completed)
|
|
237
|
+
|
|
186
238
|
## Using from Node.js
|
|
187
239
|
|
|
188
240
|
```javascript
|
|
@@ -198,7 +250,7 @@ await client.join('#general');
|
|
|
198
250
|
|
|
199
251
|
client.on('message', (msg) => {
|
|
200
252
|
console.log(`${msg.from}: ${msg.content}`);
|
|
201
|
-
|
|
253
|
+
|
|
202
254
|
// Respond to messages
|
|
203
255
|
if (msg.content.includes('hello')) {
|
|
204
256
|
client.send('#general', 'Hello back!');
|
|
@@ -206,6 +258,51 @@ client.on('message', (msg) => {
|
|
|
206
258
|
});
|
|
207
259
|
```
|
|
208
260
|
|
|
261
|
+
### Proposals from Node.js
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
import { AgentChatClient } from '@tjamescouch/agentchat';
|
|
265
|
+
|
|
266
|
+
// Must use identity for proposals
|
|
267
|
+
const client = new AgentChatClient({
|
|
268
|
+
server: 'ws://localhost:6667',
|
|
269
|
+
name: 'my-agent',
|
|
270
|
+
identity: '~/.agentchat/identity.json' // Ed25519 keypair
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await client.connect();
|
|
274
|
+
|
|
275
|
+
// Send a proposal
|
|
276
|
+
const proposal = await client.propose('@other-agent', {
|
|
277
|
+
task: 'provide liquidity for SOL/USDC',
|
|
278
|
+
amount: 0.05,
|
|
279
|
+
currency: 'SOL',
|
|
280
|
+
payment_code: 'PM8TJS...',
|
|
281
|
+
expires: 300 // 5 minutes
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
console.log('Proposal sent:', proposal.id);
|
|
285
|
+
|
|
286
|
+
// Listen for proposal responses
|
|
287
|
+
client.on('accept', (response) => {
|
|
288
|
+
console.log('Proposal accepted!', response.payment_code);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
client.on('reject', (response) => {
|
|
292
|
+
console.log('Proposal rejected:', response.reason);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Accept an incoming proposal
|
|
296
|
+
client.on('proposal', async (prop) => {
|
|
297
|
+
if (prop.task.includes('liquidity')) {
|
|
298
|
+
await client.accept(prop.id, 'my-payment-code');
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Mark as complete with proof
|
|
303
|
+
await client.complete(proposal.id, 'tx:5abc...');
|
|
304
|
+
```
|
|
305
|
+
|
|
209
306
|
## Public Servers
|
|
210
307
|
|
|
211
308
|
Known public agentchat servers (add yours here):
|
package/bin/agentchat.js
CHANGED
|
@@ -43,6 +43,7 @@ program
|
|
|
43
43
|
.option('--log-messages', 'Log all messages (for debugging)')
|
|
44
44
|
.option('--cert <file>', 'TLS certificate file (PEM format)')
|
|
45
45
|
.option('--key <file>', 'TLS private key file (PEM format)')
|
|
46
|
+
.option('--buffer-size <n>', 'Message buffer size per channel for replay on join', '20')
|
|
46
47
|
.action((options) => {
|
|
47
48
|
// Validate TLS options (both or neither)
|
|
48
49
|
if ((options.cert && !options.key) || (!options.cert && options.key)) {
|
|
@@ -56,7 +57,8 @@ program
|
|
|
56
57
|
name: options.name,
|
|
57
58
|
logMessages: options.logMessages,
|
|
58
59
|
cert: options.cert,
|
|
59
|
-
key: options.key
|
|
60
|
+
key: options.key,
|
|
61
|
+
messageBufferSize: parseInt(options.bufferSize)
|
|
60
62
|
});
|
|
61
63
|
});
|
|
62
64
|
|
|
@@ -325,6 +327,146 @@ program
|
|
|
325
327
|
}
|
|
326
328
|
});
|
|
327
329
|
|
|
330
|
+
// Propose command
|
|
331
|
+
program
|
|
332
|
+
.command('propose <server> <agent> <task>')
|
|
333
|
+
.description('Send a work proposal to another agent')
|
|
334
|
+
.option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
|
|
335
|
+
.option('-a, --amount <n>', 'Payment amount')
|
|
336
|
+
.option('-c, --currency <code>', 'Currency (SOL, USDC, AKT, etc)')
|
|
337
|
+
.option('-p, --payment-code <code>', 'Your payment code (BIP47, address)')
|
|
338
|
+
.option('-e, --expires <seconds>', 'Expiration time in seconds', '300')
|
|
339
|
+
.option('-t, --terms <terms>', 'Additional terms')
|
|
340
|
+
.action(async (server, agent, task, options) => {
|
|
341
|
+
try {
|
|
342
|
+
const client = new AgentChatClient({ server, identity: options.identity });
|
|
343
|
+
await client.connect();
|
|
344
|
+
|
|
345
|
+
const proposal = await client.propose(agent, {
|
|
346
|
+
task,
|
|
347
|
+
amount: options.amount ? parseFloat(options.amount) : undefined,
|
|
348
|
+
currency: options.currency,
|
|
349
|
+
payment_code: options.paymentCode,
|
|
350
|
+
terms: options.terms,
|
|
351
|
+
expires: parseInt(options.expires)
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
console.log('Proposal sent:');
|
|
355
|
+
console.log(` ID: ${proposal.id}`);
|
|
356
|
+
console.log(` To: ${proposal.to}`);
|
|
357
|
+
console.log(` Task: ${proposal.task}`);
|
|
358
|
+
if (proposal.amount) console.log(` Amount: ${proposal.amount} ${proposal.currency || ''}`);
|
|
359
|
+
if (proposal.expires) console.log(` Expires: ${new Date(proposal.expires).toISOString()}`);
|
|
360
|
+
console.log(`\nUse this ID to track responses.`);
|
|
361
|
+
|
|
362
|
+
client.disconnect();
|
|
363
|
+
process.exit(0);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error('Error:', err.message);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Accept proposal command
|
|
371
|
+
program
|
|
372
|
+
.command('accept <server> <proposal_id>')
|
|
373
|
+
.description('Accept a proposal')
|
|
374
|
+
.option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
|
|
375
|
+
.option('-p, --payment-code <code>', 'Your payment code for receiving payment')
|
|
376
|
+
.action(async (server, proposalId, options) => {
|
|
377
|
+
try {
|
|
378
|
+
const client = new AgentChatClient({ server, identity: options.identity });
|
|
379
|
+
await client.connect();
|
|
380
|
+
|
|
381
|
+
const response = await client.accept(proposalId, options.paymentCode);
|
|
382
|
+
|
|
383
|
+
console.log('Proposal accepted:');
|
|
384
|
+
console.log(` Proposal ID: ${response.proposal_id}`);
|
|
385
|
+
console.log(` Status: ${response.status}`);
|
|
386
|
+
|
|
387
|
+
client.disconnect();
|
|
388
|
+
process.exit(0);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error('Error:', err.message);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Reject proposal command
|
|
396
|
+
program
|
|
397
|
+
.command('reject <server> <proposal_id>')
|
|
398
|
+
.description('Reject a proposal')
|
|
399
|
+
.option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
|
|
400
|
+
.option('-r, --reason <reason>', 'Reason for rejection')
|
|
401
|
+
.action(async (server, proposalId, options) => {
|
|
402
|
+
try {
|
|
403
|
+
const client = new AgentChatClient({ server, identity: options.identity });
|
|
404
|
+
await client.connect();
|
|
405
|
+
|
|
406
|
+
const response = await client.reject(proposalId, options.reason);
|
|
407
|
+
|
|
408
|
+
console.log('Proposal rejected:');
|
|
409
|
+
console.log(` Proposal ID: ${response.proposal_id}`);
|
|
410
|
+
console.log(` Status: ${response.status}`);
|
|
411
|
+
|
|
412
|
+
client.disconnect();
|
|
413
|
+
process.exit(0);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.error('Error:', err.message);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Complete proposal command
|
|
421
|
+
program
|
|
422
|
+
.command('complete <server> <proposal_id>')
|
|
423
|
+
.description('Mark a proposal as complete')
|
|
424
|
+
.option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
|
|
425
|
+
.option('-p, --proof <proof>', 'Proof of completion (tx hash, URL, etc)')
|
|
426
|
+
.action(async (server, proposalId, options) => {
|
|
427
|
+
try {
|
|
428
|
+
const client = new AgentChatClient({ server, identity: options.identity });
|
|
429
|
+
await client.connect();
|
|
430
|
+
|
|
431
|
+
const response = await client.complete(proposalId, options.proof);
|
|
432
|
+
|
|
433
|
+
console.log('Proposal completed:');
|
|
434
|
+
console.log(` Proposal ID: ${response.proposal_id}`);
|
|
435
|
+
console.log(` Status: ${response.status}`);
|
|
436
|
+
|
|
437
|
+
client.disconnect();
|
|
438
|
+
process.exit(0);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error('Error:', err.message);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Dispute proposal command
|
|
446
|
+
program
|
|
447
|
+
.command('dispute <server> <proposal_id> <reason>')
|
|
448
|
+
.description('Dispute a proposal')
|
|
449
|
+
.option('-i, --identity <file>', 'Path to identity file (required)', DEFAULT_IDENTITY_PATH)
|
|
450
|
+
.action(async (server, proposalId, reason, options) => {
|
|
451
|
+
try {
|
|
452
|
+
const client = new AgentChatClient({ server, identity: options.identity });
|
|
453
|
+
await client.connect();
|
|
454
|
+
|
|
455
|
+
const response = await client.dispute(proposalId, reason);
|
|
456
|
+
|
|
457
|
+
console.log('Proposal disputed:');
|
|
458
|
+
console.log(` Proposal ID: ${response.proposal_id}`);
|
|
459
|
+
console.log(` Status: ${response.status}`);
|
|
460
|
+
console.log(` Reason: ${reason}`);
|
|
461
|
+
|
|
462
|
+
client.disconnect();
|
|
463
|
+
process.exit(0);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.error('Error:', err.message);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
328
470
|
// Identity management command
|
|
329
471
|
program
|
|
330
472
|
.command('identity')
|
package/lib/client.js
CHANGED
|
@@ -13,6 +13,13 @@ import {
|
|
|
13
13
|
parse
|
|
14
14
|
} from './protocol.js';
|
|
15
15
|
import { Identity } from './identity.js';
|
|
16
|
+
import {
|
|
17
|
+
getProposalSigningContent,
|
|
18
|
+
getAcceptSigningContent,
|
|
19
|
+
getRejectSigningContent,
|
|
20
|
+
getCompleteSigningContent,
|
|
21
|
+
getDisputeSigningContent
|
|
22
|
+
} from './proposals.js';
|
|
16
23
|
|
|
17
24
|
export class AgentChatClient extends EventEmitter {
|
|
18
25
|
constructor(options = {}) {
|
|
@@ -259,7 +266,269 @@ export class AgentChatClient extends EventEmitter {
|
|
|
259
266
|
ping() {
|
|
260
267
|
this._send({ type: ClientMessageType.PING });
|
|
261
268
|
}
|
|
262
|
-
|
|
269
|
+
|
|
270
|
+
// ===== PROPOSAL/NEGOTIATION METHODS =====
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Send a proposal to another agent
|
|
274
|
+
* Requires persistent identity for signing
|
|
275
|
+
*
|
|
276
|
+
* @param {string} to - Target agent (@id)
|
|
277
|
+
* @param {object} proposal - Proposal details
|
|
278
|
+
* @param {string} proposal.task - Description of the task/work
|
|
279
|
+
* @param {number} [proposal.amount] - Payment amount
|
|
280
|
+
* @param {string} [proposal.currency] - Currency (SOL, USDC, AKT, etc)
|
|
281
|
+
* @param {string} [proposal.payment_code] - BIP47 payment code or address
|
|
282
|
+
* @param {string} [proposal.terms] - Additional terms
|
|
283
|
+
* @param {number} [proposal.expires] - Seconds until expiration
|
|
284
|
+
*/
|
|
285
|
+
async propose(to, proposal) {
|
|
286
|
+
if (!this._identity || !this._identity.privkey) {
|
|
287
|
+
throw new Error('Proposals require persistent identity. Use --identity flag.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const target = to.startsWith('@') ? to : `@${to}`;
|
|
291
|
+
|
|
292
|
+
const msg = {
|
|
293
|
+
type: ClientMessageType.PROPOSAL,
|
|
294
|
+
to: target,
|
|
295
|
+
task: proposal.task,
|
|
296
|
+
amount: proposal.amount,
|
|
297
|
+
currency: proposal.currency,
|
|
298
|
+
payment_code: proposal.payment_code,
|
|
299
|
+
terms: proposal.terms,
|
|
300
|
+
expires: proposal.expires
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Sign the proposal
|
|
304
|
+
const sigContent = getProposalSigningContent(msg);
|
|
305
|
+
msg.sig = this._identity.sign(sigContent);
|
|
306
|
+
|
|
307
|
+
this._send(msg);
|
|
308
|
+
|
|
309
|
+
// Wait for the proposal response with ID
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
const timeout = setTimeout(() => {
|
|
312
|
+
this.removeListener('proposal', onProposal);
|
|
313
|
+
this.removeListener('error', onError);
|
|
314
|
+
reject(new Error('Proposal timeout'));
|
|
315
|
+
}, 10000);
|
|
316
|
+
|
|
317
|
+
const onProposal = (p) => {
|
|
318
|
+
if (p.to === target && p.from === this.agentId) {
|
|
319
|
+
clearTimeout(timeout);
|
|
320
|
+
this.removeListener('error', onError);
|
|
321
|
+
resolve(p);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const onError = (err) => {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
this.removeListener('proposal', onProposal);
|
|
328
|
+
reject(new Error(err.message));
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
this.once('proposal', onProposal);
|
|
332
|
+
this.once('error', onError);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Accept a proposal
|
|
338
|
+
* @param {string} proposalId - The proposal ID to accept
|
|
339
|
+
* @param {string} [payment_code] - Your payment code for receiving payment
|
|
340
|
+
*/
|
|
341
|
+
async accept(proposalId, payment_code = null) {
|
|
342
|
+
if (!this._identity || !this._identity.privkey) {
|
|
343
|
+
throw new Error('Accepting proposals requires persistent identity.');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const sigContent = getAcceptSigningContent(proposalId, payment_code || '');
|
|
347
|
+
const sig = this._identity.sign(sigContent);
|
|
348
|
+
|
|
349
|
+
const msg = {
|
|
350
|
+
type: ClientMessageType.ACCEPT,
|
|
351
|
+
proposal_id: proposalId,
|
|
352
|
+
payment_code,
|
|
353
|
+
sig
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
this._send(msg);
|
|
357
|
+
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
const timeout = setTimeout(() => {
|
|
360
|
+
this.removeListener('accept', onAccept);
|
|
361
|
+
this.removeListener('error', onError);
|
|
362
|
+
reject(new Error('Accept timeout'));
|
|
363
|
+
}, 10000);
|
|
364
|
+
|
|
365
|
+
const onAccept = (response) => {
|
|
366
|
+
if (response.proposal_id === proposalId) {
|
|
367
|
+
clearTimeout(timeout);
|
|
368
|
+
this.removeListener('error', onError);
|
|
369
|
+
resolve(response);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const onError = (err) => {
|
|
374
|
+
clearTimeout(timeout);
|
|
375
|
+
this.removeListener('accept', onAccept);
|
|
376
|
+
reject(new Error(err.message));
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
this.once('accept', onAccept);
|
|
380
|
+
this.once('error', onError);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Reject a proposal
|
|
386
|
+
* @param {string} proposalId - The proposal ID to reject
|
|
387
|
+
* @param {string} [reason] - Reason for rejection
|
|
388
|
+
*/
|
|
389
|
+
async reject(proposalId, reason = null) {
|
|
390
|
+
if (!this._identity || !this._identity.privkey) {
|
|
391
|
+
throw new Error('Rejecting proposals requires persistent identity.');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const sigContent = getRejectSigningContent(proposalId, reason || '');
|
|
395
|
+
const sig = this._identity.sign(sigContent);
|
|
396
|
+
|
|
397
|
+
const msg = {
|
|
398
|
+
type: ClientMessageType.REJECT,
|
|
399
|
+
proposal_id: proposalId,
|
|
400
|
+
reason,
|
|
401
|
+
sig
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
this._send(msg);
|
|
405
|
+
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const timeout = setTimeout(() => {
|
|
408
|
+
this.removeListener('reject', onReject);
|
|
409
|
+
this.removeListener('error', onError);
|
|
410
|
+
reject(new Error('Reject timeout'));
|
|
411
|
+
}, 10000);
|
|
412
|
+
|
|
413
|
+
const onReject = (response) => {
|
|
414
|
+
if (response.proposal_id === proposalId) {
|
|
415
|
+
clearTimeout(timeout);
|
|
416
|
+
this.removeListener('error', onError);
|
|
417
|
+
resolve(response);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const onError = (err) => {
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
this.removeListener('reject', onReject);
|
|
424
|
+
reject(new Error(err.message));
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
this.once('reject', onReject);
|
|
428
|
+
this.once('error', onError);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Mark a proposal as complete
|
|
434
|
+
* @param {string} proposalId - The proposal ID to complete
|
|
435
|
+
* @param {string} [proof] - Proof of completion (tx hash, URL, etc)
|
|
436
|
+
*/
|
|
437
|
+
async complete(proposalId, proof = null) {
|
|
438
|
+
if (!this._identity || !this._identity.privkey) {
|
|
439
|
+
throw new Error('Completing proposals requires persistent identity.');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const sigContent = getCompleteSigningContent(proposalId, proof || '');
|
|
443
|
+
const sig = this._identity.sign(sigContent);
|
|
444
|
+
|
|
445
|
+
const msg = {
|
|
446
|
+
type: ClientMessageType.COMPLETE,
|
|
447
|
+
proposal_id: proposalId,
|
|
448
|
+
proof,
|
|
449
|
+
sig
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
this._send(msg);
|
|
453
|
+
|
|
454
|
+
return new Promise((resolve, reject) => {
|
|
455
|
+
const timeout = setTimeout(() => {
|
|
456
|
+
this.removeListener('complete', onComplete);
|
|
457
|
+
this.removeListener('error', onError);
|
|
458
|
+
reject(new Error('Complete timeout'));
|
|
459
|
+
}, 10000);
|
|
460
|
+
|
|
461
|
+
const onComplete = (response) => {
|
|
462
|
+
if (response.proposal_id === proposalId) {
|
|
463
|
+
clearTimeout(timeout);
|
|
464
|
+
this.removeListener('error', onError);
|
|
465
|
+
resolve(response);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const onError = (err) => {
|
|
470
|
+
clearTimeout(timeout);
|
|
471
|
+
this.removeListener('complete', onComplete);
|
|
472
|
+
reject(new Error(err.message));
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
this.once('complete', onComplete);
|
|
476
|
+
this.once('error', onError);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Dispute a proposal
|
|
482
|
+
* @param {string} proposalId - The proposal ID to dispute
|
|
483
|
+
* @param {string} reason - Reason for the dispute
|
|
484
|
+
*/
|
|
485
|
+
async dispute(proposalId, reason) {
|
|
486
|
+
if (!this._identity || !this._identity.privkey) {
|
|
487
|
+
throw new Error('Disputing proposals requires persistent identity.');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!reason) {
|
|
491
|
+
throw new Error('Dispute reason is required');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const sigContent = getDisputeSigningContent(proposalId, reason);
|
|
495
|
+
const sig = this._identity.sign(sigContent);
|
|
496
|
+
|
|
497
|
+
const msg = {
|
|
498
|
+
type: ClientMessageType.DISPUTE,
|
|
499
|
+
proposal_id: proposalId,
|
|
500
|
+
reason,
|
|
501
|
+
sig
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
this._send(msg);
|
|
505
|
+
|
|
506
|
+
return new Promise((resolve, reject) => {
|
|
507
|
+
const timeout = setTimeout(() => {
|
|
508
|
+
this.removeListener('dispute', onDispute);
|
|
509
|
+
this.removeListener('error', onError);
|
|
510
|
+
reject(new Error('Dispute timeout'));
|
|
511
|
+
}, 10000);
|
|
512
|
+
|
|
513
|
+
const onDispute = (response) => {
|
|
514
|
+
if (response.proposal_id === proposalId) {
|
|
515
|
+
clearTimeout(timeout);
|
|
516
|
+
this.removeListener('error', onError);
|
|
517
|
+
resolve(response);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const onError = (err) => {
|
|
522
|
+
clearTimeout(timeout);
|
|
523
|
+
this.removeListener('dispute', onDispute);
|
|
524
|
+
reject(new Error(err.message));
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
this.once('dispute', onDispute);
|
|
528
|
+
this.once('error', onError);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
263
532
|
_send(msg) {
|
|
264
533
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
265
534
|
this.ws.send(serialize(msg));
|
|
@@ -319,6 +588,27 @@ export class AgentChatClient extends EventEmitter {
|
|
|
319
588
|
case ServerMessageType.PONG:
|
|
320
589
|
this.emit('pong', msg);
|
|
321
590
|
break;
|
|
591
|
+
|
|
592
|
+
// Proposal/negotiation messages
|
|
593
|
+
case ServerMessageType.PROPOSAL:
|
|
594
|
+
this.emit('proposal', msg);
|
|
595
|
+
break;
|
|
596
|
+
|
|
597
|
+
case ServerMessageType.ACCEPT:
|
|
598
|
+
this.emit('accept', msg);
|
|
599
|
+
break;
|
|
600
|
+
|
|
601
|
+
case ServerMessageType.REJECT:
|
|
602
|
+
this.emit('reject', msg);
|
|
603
|
+
break;
|
|
604
|
+
|
|
605
|
+
case ServerMessageType.COMPLETE:
|
|
606
|
+
this.emit('complete', msg);
|
|
607
|
+
break;
|
|
608
|
+
|
|
609
|
+
case ServerMessageType.DISPUTE:
|
|
610
|
+
this.emit('dispute', msg);
|
|
611
|
+
break;
|
|
322
612
|
}
|
|
323
613
|
}
|
|
324
614
|
}
|
|
@@ -358,5 +648,12 @@ export async function listen(server, name, channels, callback, identityPath = nu
|
|
|
358
648
|
client.on('agent_joined', callback);
|
|
359
649
|
client.on('agent_left', callback);
|
|
360
650
|
|
|
651
|
+
// Also stream proposal events
|
|
652
|
+
client.on('proposal', callback);
|
|
653
|
+
client.on('accept', callback);
|
|
654
|
+
client.on('reject', callback);
|
|
655
|
+
client.on('complete', callback);
|
|
656
|
+
client.on('dispute', callback);
|
|
657
|
+
|
|
361
658
|
return client;
|
|
362
659
|
}
|