@tjamescouch/agentchat 0.7.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/lib/server.js CHANGED
@@ -48,13 +48,33 @@ export class AgentChatServer {
48
48
  this.lastMessageTime = new Map(); // ws -> timestamp of last message
49
49
  this.pubkeyToId = new Map(); // pubkey -> stable agent ID (for persistent identity)
50
50
 
51
+ // Idle prompt settings
52
+ this.idleTimeoutMs = options.idleTimeoutMs || 5 * 60 * 1000; // 5 minutes default
53
+ this.idleCheckInterval = null;
54
+ this.channelLastActivity = new Map(); // channel name -> timestamp
55
+
56
+ // Conversation starters for idle prompts
57
+ this.conversationStarters = [
58
+ "It's quiet here. What's everyone working on?",
59
+ "Any agents want to test the proposal system? Try: PROPOSE @agent \"task\" --amount 0",
60
+ "Topic: What capabilities would make agent coordination more useful?",
61
+ "Looking for collaborators? Post your skills and what you're building.",
62
+ "Challenge: Describe your most interesting current project in one sentence.",
63
+ "Question: What's the hardest part about agent-to-agent coordination?",
64
+ "Idle hands... anyone want to pair on a spec or code review?",
65
+ ];
66
+
51
67
  // Create default channels
52
68
  this._createChannel('#general', false);
53
69
  this._createChannel('#agents', false);
70
+ this._createChannel('#discovery', false); // For skill announcements
54
71
 
55
72
  // Proposal store for structured negotiations
56
73
  this.proposals = new ProposalStore();
57
74
 
75
+ // Skills registry: agentId -> { skills: [], registered_at, sig }
76
+ this.skillsRegistry = new Map();
77
+
58
78
  this.wss = null;
59
79
  this.httpServer = null; // For TLS mode
60
80
  }
@@ -173,11 +193,64 @@ export class AgentChatServer {
173
193
  this.wss.on('error', (err) => {
174
194
  this._log('server_error', { error: err.message });
175
195
  });
176
-
196
+
197
+ // Start idle channel checker
198
+ this.idleCheckInterval = setInterval(() => {
199
+ this._checkIdleChannels();
200
+ }, 60 * 1000); // Check every minute
201
+
177
202
  return this;
178
203
  }
179
-
204
+
205
+ /**
206
+ * Check for idle channels and post conversation starters
207
+ */
208
+ _checkIdleChannels() {
209
+ const now = Date.now();
210
+
211
+ for (const [channelName, channel] of this.channels) {
212
+ // Skip if no agents in channel
213
+ if (channel.agents.size < 2) continue;
214
+
215
+ const lastActivity = this.channelLastActivity.get(channelName) || 0;
216
+ const idleTime = now - lastActivity;
217
+
218
+ if (idleTime >= this.idleTimeoutMs) {
219
+ // Pick a random conversation starter
220
+ const starter = this.conversationStarters[
221
+ Math.floor(Math.random() * this.conversationStarters.length)
222
+ ];
223
+
224
+ // Get list of agents to mention
225
+ const agentMentions = [];
226
+ for (const ws of channel.agents) {
227
+ const agent = this.agents.get(ws);
228
+ if (agent) agentMentions.push(`@${agent.id}`);
229
+ }
230
+
231
+ const prompt = `${agentMentions.join(', ')} - ${starter}`;
232
+
233
+ // Broadcast the prompt
234
+ const msg = createMessage(ServerMessageType.MSG, {
235
+ from: '@server',
236
+ to: channelName,
237
+ content: prompt
238
+ });
239
+ this._broadcast(channelName, msg);
240
+ this._bufferMessage(channelName, msg);
241
+
242
+ // Update activity time so we don't spam
243
+ this.channelLastActivity.set(channelName, now);
244
+
245
+ this._log('idle_prompt', { channel: channelName, agents: agentMentions.length });
246
+ }
247
+ }
248
+ }
249
+
180
250
  stop() {
251
+ if (this.idleCheckInterval) {
252
+ clearInterval(this.idleCheckInterval);
253
+ }
181
254
  if (this.wss) {
182
255
  this.wss.close();
183
256
  }
@@ -246,6 +319,13 @@ export class AgentChatServer {
246
319
  case ClientMessageType.DISPUTE:
247
320
  this._handleDispute(ws, msg);
248
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;
249
329
  }
250
330
  }
251
331
 
@@ -349,6 +429,38 @@ export class AgentChatServer {
349
429
 
350
430
  // Replay recent messages to the joining agent
351
431
  this._replayMessages(ws, msg.channel);
432
+
433
+ // Send welcome prompt to the new joiner
434
+ this._send(ws, createMessage(ServerMessageType.MSG, {
435
+ from: '@server',
436
+ to: msg.channel,
437
+ content: `Welcome to ${msg.channel}, @${agent.id}! Say hello to introduce yourself and start collaborating with other agents.`
438
+ }));
439
+
440
+ // Prompt existing agents to engage with the new joiner (if there are others)
441
+ const otherAgents = [];
442
+ for (const memberWs of channel.agents) {
443
+ if (memberWs !== ws) {
444
+ const member = this.agents.get(memberWs);
445
+ if (member) otherAgents.push({ ws: memberWs, id: member.id });
446
+ }
447
+ }
448
+
449
+ if (otherAgents.length > 0) {
450
+ // Send a prompt to existing agents to welcome the newcomer
451
+ const welcomePrompt = createMessage(ServerMessageType.MSG, {
452
+ from: '@server',
453
+ to: msg.channel,
454
+ content: `Hey ${otherAgents.map(a => `@${a.id}`).join(', ')} - new agent @${agent.id} just joined! Say hi and share what you're working on.`
455
+ });
456
+
457
+ for (const other of otherAgents) {
458
+ this._send(other.ws, welcomePrompt);
459
+ }
460
+ }
461
+
462
+ // Update channel activity
463
+ this.channelLastActivity.set(msg.channel, Date.now());
352
464
  }
353
465
 
354
466
  _handleLeave(ws, msg) {
@@ -419,6 +531,9 @@ export class AgentChatServer {
419
531
  // Buffer the message for replay to future joiners
420
532
  this._bufferMessage(msg.to, outMsg);
421
533
 
534
+ // Update channel activity timestamp (for idle detection)
535
+ this.channelLastActivity.set(msg.to, Date.now());
536
+
422
537
  } else if (isAgent(msg.to)) {
423
538
  // Direct message
424
539
  const targetId = msg.to.slice(1); // remove @
@@ -751,6 +866,112 @@ export class AgentChatServer {
751
866
  this._send(ws, outMsg);
752
867
  }
753
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
+
754
975
  _handleDisconnect(ws) {
755
976
  const agent = this.agents.get(ws);
756
977
  if (!agent) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/agentchat",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Real-time IRC-like communication protocol for AI agents",
5
5
  "main": "lib/client.js",
6
6
  "files": [
@@ -14,7 +14,9 @@
14
14
  },
15
15
  "scripts": {
16
16
  "start": "node bin/agentchat.js serve",
17
- "test": "node --test test/*.test.js"
17
+ "test": "node --test test/identity.test.js test/deploy.test.js test/daemon.test.js test/receipts.test.js test/reputation.test.js",
18
+ "test:integration": "node --test test/*.integration.test.js",
19
+ "test:all": "node --test test/*.test.js"
18
20
  },
19
21
  "keywords": [
20
22
  "ai",