@tjamescouch/agentchat 0.8.0 → 0.9.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 +159 -2
- package/lib/client.js +19 -1
- package/lib/protocol.js +35 -2
- package/lib/server.js +117 -0
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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
|
@@ -67,10 +67,14 @@ export class AgentChatServer {
|
|
|
67
67
|
// Create default channels
|
|
68
68
|
this._createChannel('#general', false);
|
|
69
69
|
this._createChannel('#agents', false);
|
|
70
|
+
this._createChannel('#discovery', false); // For skill announcements
|
|
70
71
|
|
|
71
72
|
// Proposal store for structured negotiations
|
|
72
73
|
this.proposals = new ProposalStore();
|
|
73
74
|
|
|
75
|
+
// Skills registry: agentId -> { skills: [], registered_at, sig }
|
|
76
|
+
this.skillsRegistry = new Map();
|
|
77
|
+
|
|
74
78
|
this.wss = null;
|
|
75
79
|
this.httpServer = null; // For TLS mode
|
|
76
80
|
}
|
|
@@ -315,6 +319,13 @@ export class AgentChatServer {
|
|
|
315
319
|
case ClientMessageType.DISPUTE:
|
|
316
320
|
this._handleDispute(ws, msg);
|
|
317
321
|
break;
|
|
322
|
+
// Skill discovery messages
|
|
323
|
+
case ClientMessageType.REGISTER_SKILLS:
|
|
324
|
+
this._handleRegisterSkills(ws, msg);
|
|
325
|
+
break;
|
|
326
|
+
case ClientMessageType.SEARCH_SKILLS:
|
|
327
|
+
this._handleSearchSkills(ws, msg);
|
|
328
|
+
break;
|
|
318
329
|
}
|
|
319
330
|
}
|
|
320
331
|
|
|
@@ -855,6 +866,112 @@ export class AgentChatServer {
|
|
|
855
866
|
this._send(ws, outMsg);
|
|
856
867
|
}
|
|
857
868
|
|
|
869
|
+
_handleRegisterSkills(ws, msg) {
|
|
870
|
+
const agent = this.agents.get(ws);
|
|
871
|
+
if (!agent) {
|
|
872
|
+
this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (!agent.pubkey) {
|
|
877
|
+
this._send(ws, createError(ErrorCode.SIGNATURE_REQUIRED, 'Skill registration requires persistent identity'));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Store skills for this agent
|
|
882
|
+
const registration = {
|
|
883
|
+
agent_id: `@${agent.id}`,
|
|
884
|
+
skills: msg.skills,
|
|
885
|
+
registered_at: Date.now(),
|
|
886
|
+
sig: msg.sig
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
this.skillsRegistry.set(agent.id, registration);
|
|
890
|
+
|
|
891
|
+
this._log('skills_registered', { agent: agent.id, count: msg.skills.length });
|
|
892
|
+
|
|
893
|
+
// Notify the registering agent
|
|
894
|
+
this._send(ws, createMessage(ServerMessageType.SKILLS_REGISTERED, {
|
|
895
|
+
agent_id: `@${agent.id}`,
|
|
896
|
+
skills_count: msg.skills.length,
|
|
897
|
+
registered_at: registration.registered_at
|
|
898
|
+
}));
|
|
899
|
+
|
|
900
|
+
// Optionally broadcast to #discovery channel if it exists
|
|
901
|
+
if (this.channels.has('#discovery')) {
|
|
902
|
+
this._broadcast('#discovery', createMessage(ServerMessageType.MSG, {
|
|
903
|
+
from: '@server',
|
|
904
|
+
to: '#discovery',
|
|
905
|
+
content: `Agent @${agent.id} registered ${msg.skills.length} skill(s): ${msg.skills.map(s => s.capability).join(', ')}`
|
|
906
|
+
}));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
_handleSearchSkills(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
|
+
const query = msg.query || {};
|
|
918
|
+
const results = [];
|
|
919
|
+
|
|
920
|
+
// Search through all registered skills
|
|
921
|
+
for (const [agentId, registration] of this.skillsRegistry) {
|
|
922
|
+
for (const skill of registration.skills) {
|
|
923
|
+
let matches = true;
|
|
924
|
+
|
|
925
|
+
// Filter by capability (substring match, case-insensitive)
|
|
926
|
+
if (query.capability) {
|
|
927
|
+
const cap = skill.capability.toLowerCase();
|
|
928
|
+
const search = query.capability.toLowerCase();
|
|
929
|
+
if (!cap.includes(search)) {
|
|
930
|
+
matches = false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Filter by max_rate
|
|
935
|
+
if (query.max_rate !== undefined && skill.rate !== undefined) {
|
|
936
|
+
if (skill.rate > query.max_rate) {
|
|
937
|
+
matches = false;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Filter by currency
|
|
942
|
+
if (query.currency && skill.currency) {
|
|
943
|
+
if (skill.currency.toLowerCase() !== query.currency.toLowerCase()) {
|
|
944
|
+
matches = false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (matches) {
|
|
949
|
+
results.push({
|
|
950
|
+
agent_id: registration.agent_id,
|
|
951
|
+
...skill,
|
|
952
|
+
registered_at: registration.registered_at
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Sort by registration time (newest first)
|
|
959
|
+
results.sort((a, b) => b.registered_at - a.registered_at);
|
|
960
|
+
|
|
961
|
+
// Limit results
|
|
962
|
+
const limit = query.limit || 50;
|
|
963
|
+
const limitedResults = results.slice(0, limit);
|
|
964
|
+
|
|
965
|
+
this._log('skills_search', { agent: agent.id, query, results_count: limitedResults.length });
|
|
966
|
+
|
|
967
|
+
this._send(ws, createMessage(ServerMessageType.SEARCH_RESULTS, {
|
|
968
|
+
query_id: msg.query_id || null,
|
|
969
|
+
query,
|
|
970
|
+
results: limitedResults,
|
|
971
|
+
total: results.length
|
|
972
|
+
}));
|
|
973
|
+
}
|
|
974
|
+
|
|
858
975
|
_handleDisconnect(ws) {
|
|
859
976
|
const agent = this.agents.get(ws);
|
|
860
977
|
if (!agent) return;
|