@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/README.md +35 -108
- package/bin/agentchat.js +161 -2
- package/lib/chat.py +175 -0
- package/lib/client.js +19 -1
- package/lib/daemon.js +31 -5
- package/lib/deploy/akash.js +1 -1
- package/lib/deploy/docker.js +132 -0
- package/lib/deploy/index.js +2 -127
- package/lib/identity.js +1 -1
- package/lib/proposals.js +8 -0
- package/lib/protocol.js +35 -2
- package/lib/receipts.js +1 -1
- package/lib/reputation.js +1 -1
- package/lib/server.js +223 -2
- package/package.json +4 -2
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.
|
|
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
|
|
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",
|