@tjamescouch/agentchat 0.8.0 → 0.10.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/bin/agentchat.js CHANGED
@@ -679,14 +679,22 @@ program
679
679
  console.log(` Instance: ${instanceName}`);
680
680
  console.log(` Server: ${server}`);
681
681
  console.log(` Identity: ${options.identity}`);
682
- console.log(` Channels: ${options.channels.join(', ')}`);
682
+
683
+ // Normalize channels: handle both comma-separated and space-separated formats
684
+ const normalizedChannels = options.channels
685
+ .flatMap(c => c.split(','))
686
+ .map(c => c.trim())
687
+ .filter(c => c.length > 0)
688
+ .map(c => c.startsWith('#') ? c : '#' + c);
689
+
690
+ console.log(` Channels: ${normalizedChannels.join(', ')}`);
683
691
  console.log('');
684
692
 
685
693
  const daemon = new AgentChatDaemon({
686
694
  server,
687
695
  name: instanceName,
688
696
  identity: options.identity,
689
- channels: options.channels,
697
+ channels: normalizedChannels,
690
698
  maxReconnectTime: parseInt(options.maxReconnectTime) * 60 * 1000 // Convert minutes to ms
691
699
  });
692
700
 
@@ -944,6 +952,155 @@ program
944
952
  }
945
953
  });
946
954
 
955
+ // Skills command - skill discovery and announcement
956
+ program
957
+ .command('skills <action> [server]')
958
+ .description('Manage skill discovery: announce, search, list')
959
+ .option('-c, --capability <capability>', 'Skill capability for announce/search')
960
+ .option('-r, --rate <rate>', 'Rate/price for the skill', parseFloat)
961
+ .option('--currency <currency>', 'Currency for rate (e.g., SOL, TEST)', 'TEST')
962
+ .option('--description <desc>', 'Description of skill')
963
+ .option('-f, --file <file>', 'YAML file with skill definitions')
964
+ .option('-i, --identity <file>', 'Path to identity file', DEFAULT_IDENTITY_PATH)
965
+ .option('--max-rate <rate>', 'Maximum rate for search', parseFloat)
966
+ .option('-l, --limit <n>', 'Limit search results', parseInt)
967
+ .option('--json', 'Output as JSON')
968
+ .action(async (action, server, options) => {
969
+ try {
970
+ if (action === 'announce') {
971
+ if (!server) {
972
+ console.error('Server URL required: agentchat skills announce <server>');
973
+ process.exit(1);
974
+ }
975
+
976
+ let skills = [];
977
+
978
+ // Load from file if provided
979
+ if (options.file) {
980
+ const yaml = await import('js-yaml');
981
+ const content = await fsp.readFile(options.file, 'utf-8');
982
+ const data = yaml.default.load(content);
983
+ skills = data.skills || [data];
984
+ } else if (options.capability) {
985
+ // Single skill from CLI args
986
+ skills = [{
987
+ capability: options.capability,
988
+ rate: options.rate,
989
+ currency: options.currency,
990
+ description: options.description
991
+ }];
992
+ } else {
993
+ console.error('Either --file or --capability required');
994
+ process.exit(1);
995
+ }
996
+
997
+ // Load identity and sign
998
+ const identity = await Identity.load(options.identity);
999
+ const skillsContent = JSON.stringify(skills);
1000
+ const sig = identity.sign(skillsContent);
1001
+
1002
+ // Connect and announce
1003
+ const client = new AgentChatClient({ server, identity: options.identity });
1004
+ await client.connect();
1005
+
1006
+ await client.sendRaw({
1007
+ type: 'REGISTER_SKILLS',
1008
+ skills,
1009
+ sig: sig.toString('base64')
1010
+ });
1011
+
1012
+ // Wait for response
1013
+ const response = await new Promise((resolve, reject) => {
1014
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1015
+ client.on('message', (msg) => {
1016
+ if (msg.type === 'SKILLS_REGISTERED' || msg.type === 'ERROR') {
1017
+ clearTimeout(timeout);
1018
+ resolve(msg);
1019
+ }
1020
+ });
1021
+ });
1022
+
1023
+ client.disconnect();
1024
+
1025
+ if (response.type === 'ERROR') {
1026
+ console.error('Error:', response.message);
1027
+ process.exit(1);
1028
+ }
1029
+
1030
+ console.log(`Registered ${response.skills_count} skill(s) for ${response.agent_id}`);
1031
+
1032
+ } else if (action === 'search') {
1033
+ if (!server) {
1034
+ console.error('Server URL required: agentchat skills search <server>');
1035
+ process.exit(1);
1036
+ }
1037
+
1038
+ const query = {};
1039
+ if (options.capability) query.capability = options.capability;
1040
+ if (options.maxRate !== undefined) query.max_rate = options.maxRate;
1041
+ if (options.currency) query.currency = options.currency;
1042
+ if (options.limit) query.limit = options.limit;
1043
+
1044
+ // Connect and search
1045
+ const client = new AgentChatClient({ server });
1046
+ await client.connect();
1047
+
1048
+ const queryId = `q_${Date.now()}`;
1049
+ await client.sendRaw({
1050
+ type: 'SEARCH_SKILLS',
1051
+ query,
1052
+ query_id: queryId
1053
+ });
1054
+
1055
+ // Wait for response
1056
+ const response = await new Promise((resolve, reject) => {
1057
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1058
+ client.on('message', (msg) => {
1059
+ if (msg.type === 'SEARCH_RESULTS' || msg.type === 'ERROR') {
1060
+ clearTimeout(timeout);
1061
+ resolve(msg);
1062
+ }
1063
+ });
1064
+ });
1065
+
1066
+ client.disconnect();
1067
+
1068
+ if (response.type === 'ERROR') {
1069
+ console.error('Error:', response.message);
1070
+ process.exit(1);
1071
+ }
1072
+
1073
+ if (options.json) {
1074
+ console.log(JSON.stringify(response.results, null, 2));
1075
+ } else {
1076
+ console.log(`Found ${response.results.length} skill(s) (${response.total} total):\n`);
1077
+ for (const skill of response.results) {
1078
+ const rate = skill.rate !== undefined ? `${skill.rate} ${skill.currency || ''}` : 'negotiable';
1079
+ console.log(` ${skill.agent_id}`);
1080
+ console.log(` Capability: ${skill.capability}`);
1081
+ console.log(` Rate: ${rate}`);
1082
+ if (skill.description) console.log(` Description: ${skill.description}`);
1083
+ console.log('');
1084
+ }
1085
+ }
1086
+
1087
+ } else if (action === 'list') {
1088
+ // List own registered skills (if server supports it)
1089
+ console.error('List action not yet implemented');
1090
+ process.exit(1);
1091
+
1092
+ } else {
1093
+ console.error(`Unknown action: ${action}`);
1094
+ console.error('Valid actions: announce, search, list');
1095
+ process.exit(1);
1096
+ }
1097
+
1098
+ } catch (err) {
1099
+ console.error('Error:', err.message);
1100
+ process.exit(1);
1101
+ }
1102
+ });
1103
+
947
1104
  // Deploy command
948
1105
  program
949
1106
  .command('deploy')
package/lib/client.js CHANGED
@@ -534,7 +534,14 @@ export class AgentChatClient extends EventEmitter {
534
534
  this.ws.send(serialize(msg));
535
535
  }
536
536
  }
537
-
537
+
538
+ /**
539
+ * Send a raw message (for protocol extensions)
540
+ */
541
+ sendRaw(msg) {
542
+ this._send(msg);
543
+ }
544
+
538
545
  _handleMessage(data) {
539
546
  let msg;
540
547
  try {
@@ -609,6 +616,17 @@ export class AgentChatClient extends EventEmitter {
609
616
  case ServerMessageType.DISPUTE:
610
617
  this.emit('dispute', msg);
611
618
  break;
619
+
620
+ // Skills discovery messages
621
+ case ServerMessageType.SKILLS_REGISTERED:
622
+ this.emit('skills_registered', msg);
623
+ this.emit('message', msg);
624
+ break;
625
+
626
+ case ServerMessageType.SEARCH_RESULTS:
627
+ this.emit('search_results', msg);
628
+ this.emit('message', msg);
629
+ break;
612
630
  }
613
631
  }
614
632
  }
package/lib/protocol.js CHANGED
@@ -21,7 +21,10 @@ export const ClientMessageType = {
21
21
  ACCEPT: 'ACCEPT',
22
22
  REJECT: 'REJECT',
23
23
  COMPLETE: 'COMPLETE',
24
- DISPUTE: 'DISPUTE'
24
+ DISPUTE: 'DISPUTE',
25
+ // Skill discovery message types
26
+ REGISTER_SKILLS: 'REGISTER_SKILLS',
27
+ SEARCH_SKILLS: 'SEARCH_SKILLS'
25
28
  };
26
29
 
27
30
  // Server -> Client message types
@@ -41,7 +44,10 @@ export const ServerMessageType = {
41
44
  ACCEPT: 'ACCEPT',
42
45
  REJECT: 'REJECT',
43
46
  COMPLETE: 'COMPLETE',
44
- DISPUTE: 'DISPUTE'
47
+ DISPUTE: 'DISPUTE',
48
+ // Skill discovery message types
49
+ SKILLS_REGISTERED: 'SKILLS_REGISTERED',
50
+ SEARCH_RESULTS: 'SEARCH_RESULTS'
45
51
  };
46
52
 
47
53
  // Error codes
@@ -284,6 +290,33 @@ export function validateClientMessage(raw) {
284
290
  }
285
291
  break;
286
292
 
293
+ case ClientMessageType.REGISTER_SKILLS:
294
+ // Register skills requires: skills array and signature
295
+ if (!msg.skills || !Array.isArray(msg.skills)) {
296
+ return { valid: false, error: 'Missing or invalid skills array' };
297
+ }
298
+ if (msg.skills.length === 0) {
299
+ return { valid: false, error: 'Skills array cannot be empty' };
300
+ }
301
+ // Validate each skill has at least a capability
302
+ for (const skill of msg.skills) {
303
+ if (!skill.capability || typeof skill.capability !== 'string') {
304
+ return { valid: false, error: 'Each skill must have a capability string' };
305
+ }
306
+ }
307
+ if (!msg.sig) {
308
+ return { valid: false, error: 'Skill registration must be signed' };
309
+ }
310
+ break;
311
+
312
+ case ClientMessageType.SEARCH_SKILLS:
313
+ // Search skills requires: query object
314
+ if (!msg.query || typeof msg.query !== 'object') {
315
+ return { valid: false, error: 'Missing or invalid query object' };
316
+ }
317
+ // query_id is optional but useful for tracking responses
318
+ break;
319
+
287
320
  default:
288
321
  return { valid: false, error: `Unknown message type: ${msg.type}` };
289
322
  }
package/lib/server.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  isProposalMessage
24
24
  } from './protocol.js';
25
25
  import { ProposalStore, formatProposal, formatProposalResponse } from './proposals.js';
26
+ import { ReputationStore } from './reputation.js';
26
27
 
27
28
  export class AgentChatServer {
28
29
  constructor(options = {}) {
@@ -67,10 +68,17 @@ export class AgentChatServer {
67
68
  // Create default channels
68
69
  this._createChannel('#general', false);
69
70
  this._createChannel('#agents', false);
71
+ this._createChannel('#discovery', false); // For skill announcements
70
72
 
71
73
  // Proposal store for structured negotiations
72
74
  this.proposals = new ProposalStore();
73
75
 
76
+ // Skills registry: agentId -> { skills: [], registered_at, sig }
77
+ this.skillsRegistry = new Map();
78
+
79
+ // Reputation store for ELO ratings
80
+ this.reputationStore = new ReputationStore();
81
+
74
82
  this.wss = null;
75
83
  this.httpServer = null; // For TLS mode
76
84
  }
@@ -315,6 +323,13 @@ export class AgentChatServer {
315
323
  case ClientMessageType.DISPUTE:
316
324
  this._handleDispute(ws, msg);
317
325
  break;
326
+ // Skill discovery messages
327
+ case ClientMessageType.REGISTER_SKILLS:
328
+ this._handleRegisterSkills(ws, msg);
329
+ break;
330
+ case ClientMessageType.SEARCH_SKILLS:
331
+ this._handleSearchSkills(ws, msg);
332
+ break;
318
333
  }
319
334
  }
320
335
 
@@ -769,7 +784,7 @@ export class AgentChatServer {
769
784
  this._send(ws, outMsg);
770
785
  }
771
786
 
772
- _handleComplete(ws, msg) {
787
+ async _handleComplete(ws, msg) {
773
788
  const agent = this.agents.get(ws);
774
789
  if (!agent) {
775
790
  this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
@@ -796,9 +811,27 @@ export class AgentChatServer {
796
811
  const proposal = result.proposal;
797
812
  this._log('complete', { id: proposal.id, by: agent.id });
798
813
 
814
+ // Update reputation ratings
815
+ let ratingChanges = null;
816
+ try {
817
+ ratingChanges = await this.reputationStore.processCompletion({
818
+ type: 'COMPLETE',
819
+ from: proposal.from,
820
+ to: proposal.to,
821
+ amount: proposal.amount
822
+ });
823
+ this._log('reputation_updated', {
824
+ proposal_id: proposal.id,
825
+ changes: ratingChanges
826
+ });
827
+ } catch (err) {
828
+ this._log('reputation_error', { error: err.message });
829
+ }
830
+
799
831
  // Notify both parties
800
832
  const outMsg = createMessage(ServerMessageType.COMPLETE, {
801
- ...formatProposalResponse(proposal, 'complete')
833
+ ...formatProposalResponse(proposal, 'complete'),
834
+ rating_changes: ratingChanges
802
835
  });
803
836
 
804
837
  // Notify the other party
@@ -812,7 +845,7 @@ export class AgentChatServer {
812
845
  this._send(ws, outMsg);
813
846
  }
814
847
 
815
- _handleDispute(ws, msg) {
848
+ async _handleDispute(ws, msg) {
816
849
  const agent = this.agents.get(ws);
817
850
  if (!agent) {
818
851
  this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
@@ -839,9 +872,28 @@ export class AgentChatServer {
839
872
  const proposal = result.proposal;
840
873
  this._log('dispute', { id: proposal.id, by: agent.id, reason: msg.reason });
841
874
 
875
+ // Update reputation ratings
876
+ let ratingChanges = null;
877
+ try {
878
+ ratingChanges = await this.reputationStore.processDispute({
879
+ type: 'DISPUTE',
880
+ from: proposal.from,
881
+ to: proposal.to,
882
+ amount: proposal.amount,
883
+ disputed_by: `@${agent.id}`
884
+ });
885
+ this._log('reputation_updated', {
886
+ proposal_id: proposal.id,
887
+ changes: ratingChanges
888
+ });
889
+ } catch (err) {
890
+ this._log('reputation_error', { error: err.message });
891
+ }
892
+
842
893
  // Notify both parties
843
894
  const outMsg = createMessage(ServerMessageType.DISPUTE, {
844
- ...formatProposalResponse(proposal, 'dispute')
895
+ ...formatProposalResponse(proposal, 'dispute'),
896
+ rating_changes: ratingChanges
845
897
  });
846
898
 
847
899
  // Notify the other party
@@ -855,6 +907,130 @@ export class AgentChatServer {
855
907
  this._send(ws, outMsg);
856
908
  }
857
909
 
910
+ _handleRegisterSkills(ws, msg) {
911
+ const agent = this.agents.get(ws);
912
+ if (!agent) {
913
+ this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
914
+ return;
915
+ }
916
+
917
+ if (!agent.pubkey) {
918
+ this._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Skill registration requires persistent identity'));
919
+ return;
920
+ }
921
+
922
+ // Store skills for this agent
923
+ const registration = {
924
+ agent_id: `@${agent.id}`,
925
+ skills: msg.skills,
926
+ registered_at: Date.now(),
927
+ sig: msg.sig
928
+ };
929
+
930
+ this.skillsRegistry.set(agent.id, registration);
931
+
932
+ this._log('skills_registered', { agent: agent.id, count: msg.skills.length });
933
+
934
+ // Notify the registering agent
935
+ this._send(ws, createMessage(ServerMessageType.SKILLS_REGISTERED, {
936
+ agent_id: `@${agent.id}`,
937
+ skills_count: msg.skills.length,
938
+ registered_at: registration.registered_at
939
+ }));
940
+
941
+ // Optionally broadcast to #discovery channel if it exists
942
+ if (this.channels.has('#discovery')) {
943
+ this._broadcast('#discovery', createMessage(ServerMessageType.MSG, {
944
+ from: '@server',
945
+ to: '#discovery',
946
+ content: `Agent @${agent.id} registered ${msg.skills.length} skill(s): ${msg.skills.map(s => s.capability).join(', ')}`
947
+ }));
948
+ }
949
+ }
950
+
951
+ async _handleSearchSkills(ws, msg) {
952
+ const agent = this.agents.get(ws);
953
+ if (!agent) {
954
+ this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
955
+ return;
956
+ }
957
+
958
+ const query = msg.query || {};
959
+ const results = [];
960
+
961
+ // Search through all registered skills
962
+ for (const [agentId, registration] of this.skillsRegistry) {
963
+ for (const skill of registration.skills) {
964
+ let matches = true;
965
+
966
+ // Filter by capability (substring match, case-insensitive)
967
+ if (query.capability) {
968
+ const cap = skill.capability.toLowerCase();
969
+ const search = query.capability.toLowerCase();
970
+ if (!cap.includes(search)) {
971
+ matches = false;
972
+ }
973
+ }
974
+
975
+ // Filter by max_rate
976
+ if (query.max_rate !== undefined && skill.rate !== undefined) {
977
+ if (skill.rate > query.max_rate) {
978
+ matches = false;
979
+ }
980
+ }
981
+
982
+ // Filter by currency
983
+ if (query.currency && skill.currency) {
984
+ if (skill.currency.toLowerCase() !== query.currency.toLowerCase()) {
985
+ matches = false;
986
+ }
987
+ }
988
+
989
+ if (matches) {
990
+ results.push({
991
+ agent_id: registration.agent_id,
992
+ ...skill,
993
+ registered_at: registration.registered_at
994
+ });
995
+ }
996
+ }
997
+ }
998
+
999
+ // Enrich results with reputation data
1000
+ const uniqueAgentIds = [...new Set(results.map(r => r.agent_id))];
1001
+ const ratingCache = new Map();
1002
+ for (const agentId of uniqueAgentIds) {
1003
+ const ratingInfo = await this.reputationStore.getRating(agentId);
1004
+ ratingCache.set(agentId, ratingInfo);
1005
+ }
1006
+
1007
+ // Add rating info to each result
1008
+ for (const result of results) {
1009
+ const ratingInfo = ratingCache.get(result.agent_id);
1010
+ result.rating = ratingInfo.rating;
1011
+ result.transactions = ratingInfo.transactions;
1012
+ }
1013
+
1014
+ // Sort by rating (highest first), then by registration time
1015
+ results.sort((a, b) => {
1016
+ if (b.rating !== a.rating) return b.rating - a.rating;
1017
+ return b.registered_at - a.registered_at;
1018
+ });
1019
+
1020
+ // Limit results
1021
+ const limit = query.limit || 50;
1022
+ const limitedResults = results.slice(0, limit);
1023
+
1024
+ this._log('skills_search', { agent: agent.id, query, results_count: limitedResults.length });
1025
+
1026
+ this._send(ws, createMessage(ServerMessageType.SEARCH_RESULTS, {
1027
+ query_id: msg.query_id || null,
1028
+ query,
1029
+ results: limitedResults,
1030
+ total: results.length
1031
+ }));
1032
+ }
1033
+
858
1034
  _handleDisconnect(ws) {
859
1035
  const agent = this.agents.get(ws);
860
1036
  if (!agent) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/agentchat",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Real-time IRC-like communication protocol for AI agents",
5
5
  "main": "lib/client.js",
6
6
  "files": [