@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 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
  }