@tjamescouch/agentchat 0.1.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/.claude/settings.local.json +12 -0
- package/.github/workflows/fly-deploy.yml +18 -0
- package/Dockerfile +12 -0
- package/README.md +296 -0
- package/ROADMAP.md +88 -0
- package/SPEC.md +279 -0
- package/bin/agentchat.js +702 -0
- package/fly.toml +21 -0
- package/lib/client.js +362 -0
- package/lib/deploy/akash.js +811 -0
- package/lib/deploy/config.js +128 -0
- package/lib/deploy/index.js +149 -0
- package/lib/identity.js +166 -0
- package/lib/protocol.js +236 -0
- package/lib/server.js +526 -0
- package/package.json +44 -0
- package/quick-test.sh +45 -0
- package/test/integration.test.js +536 -0
package/lib/server.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentChat Server
|
|
3
|
+
* WebSocket relay for agent-to-agent communication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import {
|
|
10
|
+
ClientMessageType,
|
|
11
|
+
ServerMessageType,
|
|
12
|
+
ErrorCode,
|
|
13
|
+
createMessage,
|
|
14
|
+
createError,
|
|
15
|
+
validateClientMessage,
|
|
16
|
+
generateAgentId,
|
|
17
|
+
serialize,
|
|
18
|
+
isChannel,
|
|
19
|
+
isAgent,
|
|
20
|
+
isValidChannel,
|
|
21
|
+
pubkeyToAgentId
|
|
22
|
+
} from './protocol.js';
|
|
23
|
+
|
|
24
|
+
export class AgentChatServer {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.port = options.port || 6667;
|
|
27
|
+
this.host = options.host || '0.0.0.0';
|
|
28
|
+
this.serverName = options.name || 'agentchat';
|
|
29
|
+
this.logMessages = options.logMessages || false;
|
|
30
|
+
|
|
31
|
+
// TLS options
|
|
32
|
+
this.tlsCert = options.cert || null;
|
|
33
|
+
this.tlsKey = options.key || null;
|
|
34
|
+
|
|
35
|
+
// Rate limiting: 1 message per second per agent
|
|
36
|
+
this.rateLimitMs = options.rateLimitMs || 1000;
|
|
37
|
+
|
|
38
|
+
// State
|
|
39
|
+
this.agents = new Map(); // ws -> agent info
|
|
40
|
+
this.agentById = new Map(); // id -> ws
|
|
41
|
+
this.channels = new Map(); // channel name -> channel info
|
|
42
|
+
this.lastMessageTime = new Map(); // ws -> timestamp of last message
|
|
43
|
+
this.pubkeyToId = new Map(); // pubkey -> stable agent ID (for persistent identity)
|
|
44
|
+
|
|
45
|
+
// Create default channels
|
|
46
|
+
this._createChannel('#general', false);
|
|
47
|
+
this._createChannel('#agents', false);
|
|
48
|
+
|
|
49
|
+
this.wss = null;
|
|
50
|
+
this.httpServer = null; // For TLS mode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_createChannel(name, inviteOnly = false) {
|
|
54
|
+
if (!this.channels.has(name)) {
|
|
55
|
+
this.channels.set(name, {
|
|
56
|
+
name,
|
|
57
|
+
inviteOnly,
|
|
58
|
+
invited: new Set(),
|
|
59
|
+
agents: new Set()
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return this.channels.get(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_log(event, data = {}) {
|
|
66
|
+
const entry = {
|
|
67
|
+
ts: new Date().toISOString(),
|
|
68
|
+
event,
|
|
69
|
+
...data
|
|
70
|
+
};
|
|
71
|
+
console.error(JSON.stringify(entry));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_send(ws, msg) {
|
|
75
|
+
if (ws.readyState === 1) { // OPEN
|
|
76
|
+
ws.send(serialize(msg));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_broadcast(channel, msg, excludeWs = null) {
|
|
81
|
+
const ch = this.channels.get(channel);
|
|
82
|
+
if (!ch) return;
|
|
83
|
+
|
|
84
|
+
for (const ws of ch.agents) {
|
|
85
|
+
if (ws !== excludeWs) {
|
|
86
|
+
this._send(ws, msg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_getAgentId(ws) {
|
|
92
|
+
const agent = this.agents.get(ws);
|
|
93
|
+
return agent ? `@${agent.id}` : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
start() {
|
|
97
|
+
const tls = !!(this.tlsCert && this.tlsKey);
|
|
98
|
+
|
|
99
|
+
if (tls) {
|
|
100
|
+
// TLS mode: create HTTPS server and attach WebSocket
|
|
101
|
+
const httpsOptions = {
|
|
102
|
+
cert: fs.readFileSync(this.tlsCert),
|
|
103
|
+
key: fs.readFileSync(this.tlsKey)
|
|
104
|
+
};
|
|
105
|
+
this.httpServer = https.createServer(httpsOptions);
|
|
106
|
+
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
107
|
+
this.httpServer.listen(this.port, this.host);
|
|
108
|
+
} else {
|
|
109
|
+
// Plain WebSocket mode
|
|
110
|
+
this.wss = new WebSocketServer({
|
|
111
|
+
port: this.port,
|
|
112
|
+
host: this.host
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._log('server_start', { port: this.port, host: this.host, tls });
|
|
117
|
+
|
|
118
|
+
this.wss.on('connection', (ws, req) => {
|
|
119
|
+
const ip = req.socket.remoteAddress;
|
|
120
|
+
this._log('connection', { ip });
|
|
121
|
+
|
|
122
|
+
ws.on('message', (data) => {
|
|
123
|
+
this._handleMessage(ws, data.toString());
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
ws.on('close', () => {
|
|
127
|
+
this._handleDisconnect(ws);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
ws.on('error', (err) => {
|
|
131
|
+
this._log('ws_error', { error: err.message });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.wss.on('error', (err) => {
|
|
136
|
+
this._log('server_error', { error: err.message });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
stop() {
|
|
143
|
+
if (this.wss) {
|
|
144
|
+
this.wss.close();
|
|
145
|
+
}
|
|
146
|
+
if (this.httpServer) {
|
|
147
|
+
this.httpServer.close();
|
|
148
|
+
}
|
|
149
|
+
this._log('server_stop');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_handleMessage(ws, data) {
|
|
153
|
+
const { valid, msg, error } = validateClientMessage(data);
|
|
154
|
+
|
|
155
|
+
if (!valid) {
|
|
156
|
+
this._send(ws, createError(ErrorCode.INVALID_MSG, error));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.logMessages) {
|
|
161
|
+
this._log('message', { type: msg.type, from: this._getAgentId(ws) });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
switch (msg.type) {
|
|
165
|
+
case ClientMessageType.IDENTIFY:
|
|
166
|
+
this._handleIdentify(ws, msg);
|
|
167
|
+
break;
|
|
168
|
+
case ClientMessageType.JOIN:
|
|
169
|
+
this._handleJoin(ws, msg);
|
|
170
|
+
break;
|
|
171
|
+
case ClientMessageType.LEAVE:
|
|
172
|
+
this._handleLeave(ws, msg);
|
|
173
|
+
break;
|
|
174
|
+
case ClientMessageType.MSG:
|
|
175
|
+
this._handleMsg(ws, msg);
|
|
176
|
+
break;
|
|
177
|
+
case ClientMessageType.LIST_CHANNELS:
|
|
178
|
+
this._handleListChannels(ws);
|
|
179
|
+
break;
|
|
180
|
+
case ClientMessageType.LIST_AGENTS:
|
|
181
|
+
this._handleListAgents(ws, msg);
|
|
182
|
+
break;
|
|
183
|
+
case ClientMessageType.CREATE_CHANNEL:
|
|
184
|
+
this._handleCreateChannel(ws, msg);
|
|
185
|
+
break;
|
|
186
|
+
case ClientMessageType.INVITE:
|
|
187
|
+
this._handleInvite(ws, msg);
|
|
188
|
+
break;
|
|
189
|
+
case ClientMessageType.PING:
|
|
190
|
+
this._send(ws, createMessage(ServerMessageType.PONG));
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_handleIdentify(ws, msg) {
|
|
196
|
+
// Check if already identified
|
|
197
|
+
if (this.agents.has(ws)) {
|
|
198
|
+
this._send(ws, createError(ErrorCode.INVALID_MSG, 'Already identified'));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let id;
|
|
203
|
+
|
|
204
|
+
// Use pubkey-derived stable ID if pubkey provided
|
|
205
|
+
if (msg.pubkey) {
|
|
206
|
+
// Check if this pubkey has connected before
|
|
207
|
+
const existingId = this.pubkeyToId.get(msg.pubkey);
|
|
208
|
+
if (existingId) {
|
|
209
|
+
// Returning agent - use their stable ID
|
|
210
|
+
id = existingId;
|
|
211
|
+
} else {
|
|
212
|
+
// New agent with pubkey - generate stable ID from pubkey
|
|
213
|
+
id = pubkeyToAgentId(msg.pubkey);
|
|
214
|
+
this.pubkeyToId.set(msg.pubkey, id);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if this ID is currently in use by another connection
|
|
218
|
+
if (this.agentById.has(id)) {
|
|
219
|
+
this._send(ws, createError(ErrorCode.INVALID_MSG, 'Agent with this identity already connected'));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// Ephemeral agent - generate random ID
|
|
224
|
+
id = generateAgentId();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const agent = {
|
|
228
|
+
id,
|
|
229
|
+
name: msg.name,
|
|
230
|
+
pubkey: msg.pubkey || null,
|
|
231
|
+
channels: new Set(),
|
|
232
|
+
connectedAt: Date.now()
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
this.agents.set(ws, agent);
|
|
236
|
+
this.agentById.set(id, ws);
|
|
237
|
+
|
|
238
|
+
this._log('identify', { id, name: msg.name, hasPubkey: !!msg.pubkey });
|
|
239
|
+
|
|
240
|
+
this._send(ws, createMessage(ServerMessageType.WELCOME, {
|
|
241
|
+
agent_id: `@${id}`,
|
|
242
|
+
server: this.serverName
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_handleJoin(ws, msg) {
|
|
247
|
+
const agent = this.agents.get(ws);
|
|
248
|
+
if (!agent) {
|
|
249
|
+
this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const channel = this.channels.get(msg.channel);
|
|
254
|
+
if (!channel) {
|
|
255
|
+
this._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check invite-only
|
|
260
|
+
if (channel.inviteOnly && !channel.invited.has(agent.id)) {
|
|
261
|
+
this._send(ws, createError(ErrorCode.NOT_INVITED, `Channel ${msg.channel} is invite-only`));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Add to channel
|
|
266
|
+
channel.agents.add(ws);
|
|
267
|
+
agent.channels.add(msg.channel);
|
|
268
|
+
|
|
269
|
+
this._log('join', { agent: agent.id, channel: msg.channel });
|
|
270
|
+
|
|
271
|
+
// Notify others
|
|
272
|
+
this._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_JOINED, {
|
|
273
|
+
channel: msg.channel,
|
|
274
|
+
agent: `@${agent.id}`
|
|
275
|
+
}), ws);
|
|
276
|
+
|
|
277
|
+
// Send confirmation with agent list
|
|
278
|
+
const agentList = [];
|
|
279
|
+
for (const memberWs of channel.agents) {
|
|
280
|
+
const member = this.agents.get(memberWs);
|
|
281
|
+
if (member) agentList.push(`@${member.id}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this._send(ws, createMessage(ServerMessageType.JOINED, {
|
|
285
|
+
channel: msg.channel,
|
|
286
|
+
agents: agentList
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_handleLeave(ws, msg) {
|
|
291
|
+
const agent = this.agents.get(ws);
|
|
292
|
+
if (!agent) {
|
|
293
|
+
this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const channel = this.channels.get(msg.channel);
|
|
298
|
+
if (!channel) return;
|
|
299
|
+
|
|
300
|
+
channel.agents.delete(ws);
|
|
301
|
+
agent.channels.delete(msg.channel);
|
|
302
|
+
|
|
303
|
+
this._log('leave', { agent: agent.id, channel: msg.channel });
|
|
304
|
+
|
|
305
|
+
// Notify others
|
|
306
|
+
this._broadcast(msg.channel, createMessage(ServerMessageType.AGENT_LEFT, {
|
|
307
|
+
channel: msg.channel,
|
|
308
|
+
agent: `@${agent.id}`
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
this._send(ws, createMessage(ServerMessageType.LEFT, {
|
|
312
|
+
channel: msg.channel
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_handleMsg(ws, msg) {
|
|
317
|
+
const agent = this.agents.get(ws);
|
|
318
|
+
if (!agent) {
|
|
319
|
+
this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Rate limiting: 1 message per second per agent
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
const lastTime = this.lastMessageTime.get(ws) || 0;
|
|
326
|
+
if (now - lastTime < this.rateLimitMs) {
|
|
327
|
+
this._send(ws, createError(ErrorCode.RATE_LIMITED, 'Rate limit exceeded (max 1 message per second)'));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.lastMessageTime.set(ws, now);
|
|
331
|
+
|
|
332
|
+
const outMsg = createMessage(ServerMessageType.MSG, {
|
|
333
|
+
from: `@${agent.id}`,
|
|
334
|
+
to: msg.to,
|
|
335
|
+
content: msg.content,
|
|
336
|
+
...(msg.sig && { sig: msg.sig }) // Pass through signature if present
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (isChannel(msg.to)) {
|
|
340
|
+
// Channel message
|
|
341
|
+
const channel = this.channels.get(msg.to);
|
|
342
|
+
if (!channel) {
|
|
343
|
+
this._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.to} not found`));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!agent.channels.has(msg.to)) {
|
|
348
|
+
this._send(ws, createError(ErrorCode.NOT_INVITED, `Not a member of ${msg.to}`));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Broadcast to channel including sender
|
|
353
|
+
this._broadcast(msg.to, outMsg);
|
|
354
|
+
|
|
355
|
+
} else if (isAgent(msg.to)) {
|
|
356
|
+
// Direct message
|
|
357
|
+
const targetId = msg.to.slice(1); // remove @
|
|
358
|
+
const targetWs = this.agentById.get(targetId);
|
|
359
|
+
|
|
360
|
+
if (!targetWs) {
|
|
361
|
+
this._send(ws, createError(ErrorCode.AGENT_NOT_FOUND, `Agent ${msg.to} not found`));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Send to target
|
|
366
|
+
this._send(targetWs, outMsg);
|
|
367
|
+
// Echo back to sender
|
|
368
|
+
this._send(ws, outMsg);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_handleListChannels(ws) {
|
|
373
|
+
const list = [];
|
|
374
|
+
for (const [name, channel] of this.channels) {
|
|
375
|
+
if (!channel.inviteOnly) {
|
|
376
|
+
list.push({
|
|
377
|
+
name,
|
|
378
|
+
agents: channel.agents.size
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._send(ws, createMessage(ServerMessageType.CHANNELS, { list }));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_handleListAgents(ws, msg) {
|
|
387
|
+
const channel = this.channels.get(msg.channel);
|
|
388
|
+
if (!channel) {
|
|
389
|
+
this._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const list = [];
|
|
394
|
+
for (const memberWs of channel.agents) {
|
|
395
|
+
const member = this.agents.get(memberWs);
|
|
396
|
+
if (member) list.push(`@${member.id}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this._send(ws, createMessage(ServerMessageType.AGENTS, {
|
|
400
|
+
channel: msg.channel,
|
|
401
|
+
list
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_handleCreateChannel(ws, msg) {
|
|
406
|
+
const agent = this.agents.get(ws);
|
|
407
|
+
if (!agent) {
|
|
408
|
+
this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (this.channels.has(msg.channel)) {
|
|
413
|
+
this._send(ws, createError(ErrorCode.CHANNEL_EXISTS, `Channel ${msg.channel} already exists`));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const channel = this._createChannel(msg.channel, msg.invite_only || false);
|
|
418
|
+
|
|
419
|
+
// Creator is automatically invited and joined
|
|
420
|
+
if (channel.inviteOnly) {
|
|
421
|
+
channel.invited.add(agent.id);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this._log('create_channel', { agent: agent.id, channel: msg.channel, inviteOnly: channel.inviteOnly });
|
|
425
|
+
|
|
426
|
+
// Auto-join creator
|
|
427
|
+
channel.agents.add(ws);
|
|
428
|
+
agent.channels.add(msg.channel);
|
|
429
|
+
|
|
430
|
+
this._send(ws, createMessage(ServerMessageType.JOINED, {
|
|
431
|
+
channel: msg.channel,
|
|
432
|
+
agents: [`@${agent.id}`]
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_handleInvite(ws, msg) {
|
|
437
|
+
const agent = this.agents.get(ws);
|
|
438
|
+
if (!agent) {
|
|
439
|
+
this._send(ws, createError(ErrorCode.AUTH_REQUIRED, 'Must IDENTIFY first'));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const channel = this.channels.get(msg.channel);
|
|
444
|
+
if (!channel) {
|
|
445
|
+
this._send(ws, createError(ErrorCode.CHANNEL_NOT_FOUND, `Channel ${msg.channel} not found`));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Must be a member to invite
|
|
450
|
+
if (!agent.channels.has(msg.channel)) {
|
|
451
|
+
this._send(ws, createError(ErrorCode.NOT_INVITED, `Not a member of ${msg.channel}`));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const targetId = msg.agent.slice(1); // remove @
|
|
456
|
+
channel.invited.add(targetId);
|
|
457
|
+
|
|
458
|
+
this._log('invite', { agent: agent.id, target: targetId, channel: msg.channel });
|
|
459
|
+
|
|
460
|
+
// Notify target if connected
|
|
461
|
+
const targetWs = this.agentById.get(targetId);
|
|
462
|
+
if (targetWs) {
|
|
463
|
+
this._send(targetWs, createMessage(ServerMessageType.MSG, {
|
|
464
|
+
from: `@${agent.id}`,
|
|
465
|
+
to: msg.agent,
|
|
466
|
+
content: `You have been invited to ${msg.channel}`
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_handleDisconnect(ws) {
|
|
472
|
+
const agent = this.agents.get(ws);
|
|
473
|
+
if (!agent) return;
|
|
474
|
+
|
|
475
|
+
this._log('disconnect', { agent: agent.id });
|
|
476
|
+
|
|
477
|
+
// Leave all channels
|
|
478
|
+
for (const channelName of agent.channels) {
|
|
479
|
+
const channel = this.channels.get(channelName);
|
|
480
|
+
if (channel) {
|
|
481
|
+
channel.agents.delete(ws);
|
|
482
|
+
this._broadcast(channelName, createMessage(ServerMessageType.AGENT_LEFT, {
|
|
483
|
+
channel: channelName,
|
|
484
|
+
agent: `@${agent.id}`
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Remove from state
|
|
490
|
+
this.agentById.delete(agent.id);
|
|
491
|
+
this.agents.delete(ws);
|
|
492
|
+
this.lastMessageTime.delete(ws);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Allow running directly
|
|
497
|
+
export function startServer(options = {}) {
|
|
498
|
+
// Support environment variable overrides (for Docker)
|
|
499
|
+
const config = {
|
|
500
|
+
port: parseInt(options.port || process.env.PORT || 6667),
|
|
501
|
+
host: options.host || process.env.HOST || '0.0.0.0',
|
|
502
|
+
name: options.name || process.env.SERVER_NAME || 'agentchat',
|
|
503
|
+
logMessages: options.logMessages || process.env.LOG_MESSAGES === 'true',
|
|
504
|
+
cert: options.cert || process.env.TLS_CERT || null,
|
|
505
|
+
key: options.key || process.env.TLS_KEY || null,
|
|
506
|
+
rateLimitMs: options.rateLimitMs || parseInt(process.env.RATE_LIMIT_MS || 1000)
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const server = new AgentChatServer(config);
|
|
510
|
+
server.start();
|
|
511
|
+
|
|
512
|
+
const protocol = (config.cert && config.key) ? 'wss' : 'ws';
|
|
513
|
+
console.log(`AgentChat server running on ${protocol}://${server.host}:${server.port}`);
|
|
514
|
+
console.log('Default channels: #general, #agents');
|
|
515
|
+
if (config.cert && config.key) {
|
|
516
|
+
console.log('TLS enabled');
|
|
517
|
+
}
|
|
518
|
+
console.log('Press Ctrl+C to stop');
|
|
519
|
+
|
|
520
|
+
process.on('SIGINT', () => {
|
|
521
|
+
server.stop();
|
|
522
|
+
process.exit(0);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return server;
|
|
526
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tjamescouch/agentchat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Real-time IRC-like communication protocol for AI agents",
|
|
5
|
+
"main": "lib/client.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentchat": "./bin/agentchat.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/agentchat.js serve",
|
|
11
|
+
"test": "node --test test/*.test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"agents",
|
|
16
|
+
"chat",
|
|
17
|
+
"irc",
|
|
18
|
+
"llm",
|
|
19
|
+
"communication",
|
|
20
|
+
"protocol"
|
|
21
|
+
],
|
|
22
|
+
"author": "James Couch",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://github.com/tjamescouch/agentchat#readme",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@akashnetwork/akashjs": "^0.11.1",
|
|
27
|
+
"@cosmjs/proto-signing": "^0.32.4",
|
|
28
|
+
"@cosmjs/stargate": "^0.32.4",
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"js-yaml": "^4.1.1",
|
|
31
|
+
"ws": "^8.16.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/tjamescouch/agentchat.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/tjamescouch/agentchat/issues"
|
|
42
|
+
},
|
|
43
|
+
"type": "module"
|
|
44
|
+
}
|
package/quick-test.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Quick test script for agentchat
|
|
4
|
+
# Run this after npm install to verify everything works
|
|
5
|
+
|
|
6
|
+
echo "=== AgentChat Quick Test ==="
|
|
7
|
+
echo ""
|
|
8
|
+
|
|
9
|
+
# Start server in background
|
|
10
|
+
echo "Starting server..."
|
|
11
|
+
node bin/agentchat.js serve &
|
|
12
|
+
SERVER_PID=$!
|
|
13
|
+
sleep 1
|
|
14
|
+
|
|
15
|
+
# Check if server started
|
|
16
|
+
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
|
17
|
+
echo "ERROR: Server failed to start"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "Server running (PID: $SERVER_PID)"
|
|
22
|
+
echo ""
|
|
23
|
+
|
|
24
|
+
# List channels
|
|
25
|
+
echo "Listing channels..."
|
|
26
|
+
node bin/agentchat.js channels ws://localhost:6667
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# Send a test message
|
|
30
|
+
echo "Sending test message..."
|
|
31
|
+
node bin/agentchat.js send ws://localhost:6667 "#general" "Test message from quick-test.sh"
|
|
32
|
+
echo ""
|
|
33
|
+
|
|
34
|
+
# Clean up
|
|
35
|
+
echo "Stopping server..."
|
|
36
|
+
kill $SERVER_PID 2>/dev/null
|
|
37
|
+
wait $SERVER_PID 2>/dev/null
|
|
38
|
+
|
|
39
|
+
echo ""
|
|
40
|
+
echo "=== Test Complete ==="
|
|
41
|
+
echo ""
|
|
42
|
+
echo "To run manually:"
|
|
43
|
+
echo " Terminal 1: node bin/agentchat.js serve"
|
|
44
|
+
echo " Terminal 2: node bin/agentchat.js listen ws://localhost:6667 '#general'"
|
|
45
|
+
echo " Terminal 3: node bin/agentchat.js send ws://localhost:6667 '#general' 'Hello!'"
|