agentgui 1.0.654 → 1.0.659
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/codec.js +53 -0
- package/lib/ws-optimizer.js +2 -3
- package/lib/ws-protocol.js +4 -9
- package/package.json +1 -1
- package/server.js +39 -217
- package/static/index.html +0 -1
- package/static/js/client.js +0 -1
- package/static/js/codec.js +47 -0
- package/static/js/websocket-manager.js +8 -13
- package/static/js/ws-client.js +11 -7
- package/lib/sse-stream.js +0 -125
- package/lib/ws-events.js +0 -20
- package/static/js/event-consolidator.js +0 -100
package/lib/codec.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary codec for WebSocket messages.
|
|
3
|
+
* Wraps msgpackr for framing + GPT tokenizer BPE compression for large text fields.
|
|
4
|
+
*
|
|
5
|
+
* Wire format: msgpackr-packed object where string fields > THRESHOLD chars
|
|
6
|
+
* are replaced with { __tok: true, d: Uint32Array } — tokenized and decompressed
|
|
7
|
+
* transparently on both sides.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { pack, unpack } from 'msgpackr';
|
|
11
|
+
import { encode as tokEncode, decode as tokDecode } from 'gpt-tokenizer';
|
|
12
|
+
|
|
13
|
+
const THRESHOLD = 200; // bytes before compression is worthwhile
|
|
14
|
+
const COMPRESSIBLE = new Set(['content', 'text', 'output', 'response', 'prompt', 'input', 'data']);
|
|
15
|
+
|
|
16
|
+
function compressText(str) {
|
|
17
|
+
return { __tok: true, d: tokEncode(str) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function decompressText(val) {
|
|
21
|
+
return tokDecode(val.d);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function encodeObj(obj) {
|
|
25
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
26
|
+
if (Array.isArray(obj)) return obj.map(encodeObj);
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const k of Object.keys(obj)) {
|
|
29
|
+
const v = obj[k];
|
|
30
|
+
if (COMPRESSIBLE.has(k) && typeof v === 'string' && v.length > THRESHOLD) {
|
|
31
|
+
out[k] = compressText(v);
|
|
32
|
+
} else if (v && typeof v === 'object' && !ArrayBuffer.isView(v)) {
|
|
33
|
+
out[k] = encodeObj(v);
|
|
34
|
+
} else {
|
|
35
|
+
out[k] = v;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decodeObj(obj) {
|
|
42
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
43
|
+
if (Array.isArray(obj)) return obj.map(decodeObj);
|
|
44
|
+
if (obj.__tok && obj.d) return decompressText(obj);
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const k of Object.keys(obj)) {
|
|
47
|
+
out[k] = decodeObj(obj[k]);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function encode(obj) { return pack(encodeObj(obj)); }
|
|
53
|
+
export function decode(buf) { return decodeObj(unpack(buf instanceof Uint8Array ? buf : new Uint8Array(buf))); }
|
package/lib/ws-optimizer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encode } from './codec.js';
|
|
2
2
|
|
|
3
3
|
const MESSAGE_PRIORITY = {
|
|
4
4
|
high: ['streaming_error', 'streaming_complete', 'rate_limit_hit', 'streaming_cancelled', 'run_cancelled', 'tool_install_complete', 'tool_update_complete', 'tool_install_failed', 'tool_update_failed'],
|
|
@@ -84,9 +84,8 @@ class ClientQueue {
|
|
|
84
84
|
if (allowedCount <= 0) { this.scheduleFlush(); return; }
|
|
85
85
|
batch.splice(allowedCount);
|
|
86
86
|
}
|
|
87
|
-
// Pack as msgpackr binary — perMessageDeflate on the WS server handles gzip
|
|
88
87
|
const envelope = batch.length === 1 ? batch[0] : batch;
|
|
89
|
-
const binary =
|
|
88
|
+
const binary = encode(envelope);
|
|
90
89
|
this.ws.send(binary);
|
|
91
90
|
this.messageCount += batch.length;
|
|
92
91
|
this.bytesSent += binary.length;
|
package/lib/ws-protocol.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encode, decode } from './codec.js';
|
|
2
2
|
|
|
3
3
|
function sendBinary(ws, obj) {
|
|
4
|
-
if (ws.readyState === 1) ws.send(
|
|
4
|
+
if (ws.readyState === 1) ws.send(encode(obj));
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
class WsRouter {
|
|
@@ -33,7 +33,7 @@ class WsRouter {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
broadcast(clients, type, data) {
|
|
36
|
-
const msg =
|
|
36
|
+
const msg = encode({ t: type, d: data || {} });
|
|
37
37
|
for (const ws of clients) {
|
|
38
38
|
if (ws.readyState === 1) ws.send(msg);
|
|
39
39
|
}
|
|
@@ -42,12 +42,7 @@ class WsRouter {
|
|
|
42
42
|
async onMessage(ws, rawData) {
|
|
43
43
|
let parsed;
|
|
44
44
|
try {
|
|
45
|
-
|
|
46
|
-
if (Buffer.isBuffer(rawData) || rawData instanceof Uint8Array) {
|
|
47
|
-
parsed = unpack(rawData);
|
|
48
|
-
} else {
|
|
49
|
-
parsed = JSON.parse(rawData.toString());
|
|
50
|
-
}
|
|
45
|
+
parsed = decode(rawData);
|
|
51
46
|
} catch {
|
|
52
47
|
sendBinary(ws, { r: null, e: { c: 400, m: 'Invalid message' } });
|
|
53
48
|
return;
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -16,9 +16,10 @@ import fsbrowse from 'fsbrowse';
|
|
|
16
16
|
import { queries } from './database.js';
|
|
17
17
|
import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
18
18
|
import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
|
|
19
|
-
import { SSEStreamManager } from './lib/sse-stream.js';
|
|
20
19
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
21
20
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
21
|
+
import { encode as wsEncode } from './lib/codec.js';
|
|
22
|
+
const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
|
|
22
23
|
import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
|
|
23
24
|
import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
|
|
24
25
|
import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
|
|
@@ -278,10 +279,8 @@ function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
|
|
281
|
-
const VOICE_INSTRUCTIONS = `Plain text. Spoken aloud. Conversational. No markdown, formatting, bullets, or lists. Short sentences. Technical facts only.`;
|
|
282
|
-
|
|
283
282
|
function buildSystemPrompt(agentId, model, subAgent) {
|
|
284
|
-
const parts = [
|
|
283
|
+
const parts = [];
|
|
285
284
|
if (agentId && agentId !== 'claude-code') {
|
|
286
285
|
const displayAgentId = agentId.split('-·-')[0];
|
|
287
286
|
parts.push(`Use ${displayAgentId} subagent for all tasks.`);
|
|
@@ -1537,57 +1536,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1537
1536
|
return;
|
|
1538
1537
|
}
|
|
1539
1538
|
|
|
1540
|
-
// POST /runs/stream -
|
|
1539
|
+
// POST /runs/stream - SSE removed, use WebSocket
|
|
1541
1540
|
if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
|
|
1542
|
-
|
|
1543
|
-
const { agent_id, input, config } = body;
|
|
1544
|
-
if (!agent_id) {
|
|
1545
|
-
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1549
|
-
if (!agent) {
|
|
1550
|
-
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
const run = queries.createRun(agent_id, null, input, config);
|
|
1554
|
-
const sseManager = new SSEStreamManager(res, run.run_id);
|
|
1555
|
-
sseManager.start();
|
|
1556
|
-
sseManager.sendProgress({ type: 'run_created', run_id: run.run_id });
|
|
1557
|
-
|
|
1558
|
-
const eventHandler = (eventData) => {
|
|
1559
|
-
if (eventData.sessionId === run.run_id || eventData.conversationId === run.thread_id) {
|
|
1560
|
-
if (eventData.type === 'streaming_progress' && eventData.block) {
|
|
1561
|
-
sseManager.sendProgress(eventData.block);
|
|
1562
|
-
} else if (eventData.type === 'streaming_error') {
|
|
1563
|
-
sseManager.sendError(eventData.error || 'Execution error');
|
|
1564
|
-
} else if (eventData.type === 'streaming_complete') {
|
|
1565
|
-
sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
|
|
1566
|
-
sseManager.cleanup();
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
};
|
|
1570
|
-
|
|
1571
|
-
sseStreamHandlers.set(run.run_id, eventHandler);
|
|
1572
|
-
req.on('close', () => {
|
|
1573
|
-
sseStreamHandlers.delete(run.run_id);
|
|
1574
|
-
sseManager.cleanup();
|
|
1575
|
-
});
|
|
1576
|
-
|
|
1577
|
-
const statelessThreadId = queries.getRun(run.run_id)?.thread_id;
|
|
1578
|
-
if (statelessThreadId) {
|
|
1579
|
-
const conv = queries.getConversation(statelessThreadId);
|
|
1580
|
-
if (conv && input?.content) {
|
|
1581
|
-
const statelessSession = queries.createSession(statelessThreadId);
|
|
1582
|
-
queries.updateRunStatus(run.run_id, 'active');
|
|
1583
|
-
activeExecutions.set(statelessThreadId, { pid: null, startTime: Date.now(), sessionId: statelessSession.id, lastActivity: Date.now() });
|
|
1584
|
-
activeProcessesByRunId.set(run.run_id, { threadId: statelessThreadId, sessionId: statelessSession.id });
|
|
1585
|
-
queries.setIsStreaming(statelessThreadId, true);
|
|
1586
|
-
processMessageWithStreaming(statelessThreadId, null, statelessSession.id, input.content, agent_id, config?.model || null)
|
|
1587
|
-
.then(() => { queries.updateRunStatus(run.run_id, 'success'); activeProcessesByRunId.delete(run.run_id); })
|
|
1588
|
-
.catch((err) => { queries.updateRunStatus(run.run_id, 'error'); activeProcessesByRunId.delete(run.run_id); sseManager.sendError(err.message); sseManager.cleanup(); });
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1541
|
+
res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
|
|
1591
1542
|
return;
|
|
1592
1543
|
}
|
|
1593
1544
|
|
|
@@ -2346,35 +2297,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2346
2297
|
|
|
2347
2298
|
const runStreamMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/);
|
|
2348
2299
|
if (runStreamMatch && req.method === 'GET') {
|
|
2349
|
-
|
|
2350
|
-
const run = queries.getRun(runId);
|
|
2351
|
-
if (!run) {
|
|
2352
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2353
|
-
return;
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
const sseManager = new SSEStreamManager(res, runId);
|
|
2357
|
-
sseManager.start();
|
|
2358
|
-
sseManager.sendProgress({ type: 'joined', run_id: runId });
|
|
2359
|
-
|
|
2360
|
-
const eventHandler = (eventData) => {
|
|
2361
|
-
if (eventData.sessionId === runId || eventData.conversationId === run.thread_id) {
|
|
2362
|
-
if (eventData.type === 'streaming_progress' && eventData.block) {
|
|
2363
|
-
sseManager.sendProgress(eventData.block);
|
|
2364
|
-
} else if (eventData.type === 'streaming_error') {
|
|
2365
|
-
sseManager.sendError(eventData.error || 'Execution error');
|
|
2366
|
-
} else if (eventData.type === 'streaming_complete') {
|
|
2367
|
-
sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
|
|
2368
|
-
sseManager.cleanup();
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
};
|
|
2372
|
-
|
|
2373
|
-
sseStreamHandlers.set(runId, eventHandler);
|
|
2374
|
-
req.on('close', () => {
|
|
2375
|
-
sseStreamHandlers.delete(runId);
|
|
2376
|
-
sseManager.cleanup();
|
|
2377
|
-
});
|
|
2300
|
+
res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
|
|
2378
2301
|
return;
|
|
2379
2302
|
}
|
|
2380
2303
|
|
|
@@ -3220,112 +3143,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
3220
3143
|
return;
|
|
3221
3144
|
}
|
|
3222
3145
|
|
|
3223
|
-
// POST /threads/{thread_id}/runs/stream -
|
|
3146
|
+
// POST /threads/{thread_id}/runs/stream - SSE removed, use WebSocket
|
|
3224
3147
|
const threadRunsStreamMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/stream$/);
|
|
3225
3148
|
if (threadRunsStreamMatch && req.method === 'POST') {
|
|
3226
|
-
|
|
3227
|
-
try {
|
|
3228
|
-
const body = await parseBody(req);
|
|
3229
|
-
const { agent_id, input, config } = body;
|
|
3230
|
-
|
|
3231
|
-
const thread = queries.getThread(threadId);
|
|
3232
|
-
if (!thread) {
|
|
3233
|
-
sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
|
|
3234
|
-
return;
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
if (thread.status !== 'idle') {
|
|
3238
|
-
sendJSON(req, res, 409, { error: 'Thread has pending runs', type: 'conflict' });
|
|
3239
|
-
return;
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
3243
|
-
if (!agent) {
|
|
3244
|
-
sendJSON(req, res, 404, { error: 'Agent not found', type: 'not_found' });
|
|
3245
|
-
return;
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
const run = queries.createRun(agent_id, threadId, input, config);
|
|
3249
|
-
const sseManager = new SSEStreamManager(res, run.run_id);
|
|
3250
|
-
sseManager.start();
|
|
3251
|
-
sseManager.sendProgress({ type: 'run_created', run_id: run.run_id, thread_id: threadId });
|
|
3252
|
-
|
|
3253
|
-
const eventHandler = (eventData) => {
|
|
3254
|
-
if (eventData.sessionId === run.run_id || eventData.conversationId === threadId) {
|
|
3255
|
-
if (eventData.type === 'streaming_progress' && eventData.block) {
|
|
3256
|
-
sseManager.sendProgress(eventData.block);
|
|
3257
|
-
} else if (eventData.type === 'streaming_error') {
|
|
3258
|
-
sseManager.sendError(eventData.error || 'Execution error');
|
|
3259
|
-
} else if (eventData.type === 'streaming_complete') {
|
|
3260
|
-
sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
|
|
3261
|
-
sseManager.cleanup();
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
};
|
|
3265
|
-
|
|
3266
|
-
sseStreamHandlers.set(run.run_id, eventHandler);
|
|
3267
|
-
req.on('close', () => {
|
|
3268
|
-
sseStreamHandlers.delete(run.run_id);
|
|
3269
|
-
sseManager.cleanup();
|
|
3270
|
-
});
|
|
3271
|
-
|
|
3272
|
-
const conv = queries.getConversation(threadId);
|
|
3273
|
-
if (conv && input?.content) {
|
|
3274
|
-
const session = queries.createSession(threadId);
|
|
3275
|
-
queries.updateRunStatus(run.run_id, 'active');
|
|
3276
|
-
activeExecutions.set(threadId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
|
|
3277
|
-
activeProcessesByRunId.set(run.run_id, { threadId, sessionId: session.id });
|
|
3278
|
-
queries.setIsStreaming(threadId, true);
|
|
3279
|
-
processMessageWithStreaming(threadId, null, session.id, input.content, agent_id, config?.model || null)
|
|
3280
|
-
.then(() => { queries.updateRunStatus(run.run_id, 'success'); activeProcessesByRunId.delete(run.run_id); })
|
|
3281
|
-
.catch((err) => { queries.updateRunStatus(run.run_id, 'error'); activeProcessesByRunId.delete(run.run_id); sseManager.sendError(err.message); sseManager.cleanup(); });
|
|
3282
|
-
}
|
|
3283
|
-
} catch (err) {
|
|
3284
|
-
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
3285
|
-
}
|
|
3149
|
+
res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
|
|
3286
3150
|
return;
|
|
3287
3151
|
}
|
|
3288
3152
|
|
|
3289
|
-
// GET /threads/{thread_id}/runs/{run_id}/stream -
|
|
3153
|
+
// GET /threads/{thread_id}/runs/{run_id}/stream - SSE removed, use WebSocket
|
|
3290
3154
|
const threadRunStreamMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/([a-f0-9-]{36})\/stream$/);
|
|
3291
3155
|
if (threadRunStreamMatch && req.method === 'GET') {
|
|
3292
|
-
|
|
3293
|
-
const runId = threadRunStreamMatch[2];
|
|
3294
|
-
|
|
3295
|
-
const thread = queries.getThread(threadId);
|
|
3296
|
-
if (!thread) {
|
|
3297
|
-
sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
|
|
3298
|
-
return;
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
const run = queries.getRun(runId);
|
|
3302
|
-
if (!run || run.thread_id !== threadId) {
|
|
3303
|
-
sendJSON(req, res, 404, { error: 'Run not found on thread', type: 'not_found' });
|
|
3304
|
-
return;
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
const sseManager = new SSEStreamManager(res, runId);
|
|
3308
|
-
sseManager.start();
|
|
3309
|
-
sseManager.sendProgress({ type: 'joined', run_id: runId, thread_id: threadId });
|
|
3310
|
-
|
|
3311
|
-
const eventHandler = (eventData) => {
|
|
3312
|
-
if (eventData.sessionId === runId || eventData.conversationId === threadId) {
|
|
3313
|
-
if (eventData.type === 'streaming_progress' && eventData.block) {
|
|
3314
|
-
sseManager.sendProgress(eventData.block);
|
|
3315
|
-
} else if (eventData.type === 'streaming_error') {
|
|
3316
|
-
sseManager.sendError(eventData.error || 'Execution error');
|
|
3317
|
-
} else if (eventData.type === 'streaming_complete') {
|
|
3318
|
-
sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
|
|
3319
|
-
sseManager.cleanup();
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
};
|
|
3323
|
-
|
|
3324
|
-
sseStreamHandlers.set(runId, eventHandler);
|
|
3325
|
-
req.on('close', () => {
|
|
3326
|
-
sseStreamHandlers.delete(runId);
|
|
3327
|
-
sseManager.cleanup();
|
|
3328
|
-
});
|
|
3156
|
+
res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
|
|
3329
3157
|
return;
|
|
3330
3158
|
}
|
|
3331
3159
|
|
|
@@ -4102,7 +3930,6 @@ wss.on('error', (err) => {
|
|
|
4102
3930
|
const hotReloadClients = [];
|
|
4103
3931
|
const syncClients = new Set();
|
|
4104
3932
|
const subscriptionIndex = new Map();
|
|
4105
|
-
const sseStreamHandlers = new Map();
|
|
4106
3933
|
const pm2Subscribers = new Set();
|
|
4107
3934
|
|
|
4108
3935
|
wss.on('connection', (ws, req) => {
|
|
@@ -4117,7 +3944,7 @@ wss.on('connection', (ws, req) => {
|
|
|
4117
3944
|
ws.subscriptions = new Set();
|
|
4118
3945
|
ws.clientId = `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
4119
3946
|
|
|
4120
|
-
ws
|
|
3947
|
+
sendWs(ws, ({
|
|
4121
3948
|
type: 'sync_connected',
|
|
4122
3949
|
clientId: ws.clientId,
|
|
4123
3950
|
timestamp: Date.now()
|
|
@@ -4187,11 +4014,6 @@ function broadcastSync(event) {
|
|
|
4187
4014
|
}
|
|
4188
4015
|
}
|
|
4189
4016
|
|
|
4190
|
-
if (sseStreamHandlers.size > 0) {
|
|
4191
|
-
for (const [runId, handler] of sseStreamHandlers.entries()) {
|
|
4192
|
-
try { handler(event); } catch (e) {}
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
4017
|
} catch (err) {
|
|
4196
4018
|
console.error('[BROADCAST] Error (contained):', err.message);
|
|
4197
4019
|
}
|
|
@@ -4242,7 +4064,7 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4242
4064
|
}
|
|
4243
4065
|
const subTarget = data.sessionId || data.conversationId;
|
|
4244
4066
|
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
4245
|
-
ws
|
|
4067
|
+
sendWs(ws, ({
|
|
4246
4068
|
type: 'subscription_confirmed',
|
|
4247
4069
|
sessionId: data.sessionId,
|
|
4248
4070
|
conversationId: data.conversationId,
|
|
@@ -4253,7 +4075,7 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4253
4075
|
if (data.conversationId && activeExecutions.has(data.conversationId)) {
|
|
4254
4076
|
const execution = activeExecutions.get(data.conversationId);
|
|
4255
4077
|
const conv = queries.getConversation(data.conversationId);
|
|
4256
|
-
ws
|
|
4078
|
+
sendWs(ws, ({
|
|
4257
4079
|
type: 'streaming_start',
|
|
4258
4080
|
sessionId: execution.sessionId,
|
|
4259
4081
|
conversationId: data.conversationId,
|
|
@@ -4271,7 +4093,7 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4271
4093
|
|
|
4272
4094
|
const latestSession = queries.getLatestSession(data.conversationId);
|
|
4273
4095
|
if (latestSession) {
|
|
4274
|
-
ws
|
|
4096
|
+
sendWs(ws, ({
|
|
4275
4097
|
type: 'streaming_resumed',
|
|
4276
4098
|
sessionId: latestSession.id,
|
|
4277
4099
|
conversationId: data.conversationId,
|
|
@@ -4282,7 +4104,7 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4282
4104
|
}));
|
|
4283
4105
|
|
|
4284
4106
|
checkpointManager.injectCheckpointEvents(latestSession.id, checkpoint, (evt) => {
|
|
4285
|
-
ws
|
|
4107
|
+
sendWs(ws, ({
|
|
4286
4108
|
...evt,
|
|
4287
4109
|
sessionId: latestSession.id,
|
|
4288
4110
|
conversationId: data.conversationId
|
|
@@ -4305,7 +4127,7 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4305
4127
|
}
|
|
4306
4128
|
debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
|
|
4307
4129
|
} else if (data.type === 'get_subscriptions') {
|
|
4308
|
-
ws
|
|
4130
|
+
sendWs(ws, ({
|
|
4309
4131
|
type: 'subscriptions',
|
|
4310
4132
|
subscriptions: Array.from(ws.subscriptions),
|
|
4311
4133
|
timestamp: Date.now()
|
|
@@ -4317,7 +4139,7 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4317
4139
|
ws.latencyAvg = data.avg || 0;
|
|
4318
4140
|
ws.latencyTrend = data.trend || 'stable';
|
|
4319
4141
|
} else if (data.type === 'ping') {
|
|
4320
|
-
ws
|
|
4142
|
+
sendWs(ws, ({
|
|
4321
4143
|
type: 'pong',
|
|
4322
4144
|
requestId: data.requestId,
|
|
4323
4145
|
timestamp: Date.now()
|
|
@@ -4340,18 +4162,18 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4340
4162
|
ws.terminalProc = proc;
|
|
4341
4163
|
ws.terminalPty = true;
|
|
4342
4164
|
proc.on('data', (chunk) => {
|
|
4343
|
-
if (ws.readyState === 1) ws
|
|
4165
|
+
if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: Buffer.from(chunk).toString('base64'), encoding: 'base64' }));
|
|
4344
4166
|
});
|
|
4345
4167
|
proc.on('exit', (code) => {
|
|
4346
|
-
if (ws.readyState === 1) ws
|
|
4168
|
+
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
|
|
4347
4169
|
ws.terminalProc = null;
|
|
4348
4170
|
});
|
|
4349
4171
|
proc.on('error', (err) => {
|
|
4350
4172
|
console.error('[TERMINAL] PTY error (contained):', err.message);
|
|
4351
|
-
if (ws.readyState === 1) ws
|
|
4173
|
+
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
|
|
4352
4174
|
ws.terminalProc = null;
|
|
4353
4175
|
});
|
|
4354
|
-
ws
|
|
4176
|
+
sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
|
|
4355
4177
|
} catch (e) {
|
|
4356
4178
|
console.error('[TERMINAL] Failed to spawn PTY, falling back to pipes:', e.message);
|
|
4357
4179
|
const { spawn } = require('child_process');
|
|
@@ -4361,24 +4183,24 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4361
4183
|
ws.terminalProc = proc;
|
|
4362
4184
|
ws.terminalPty = false;
|
|
4363
4185
|
proc.stdout.on('data', (chunk) => {
|
|
4364
|
-
if (ws.readyState === 1) ws
|
|
4186
|
+
if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
4365
4187
|
});
|
|
4366
4188
|
proc.stderr.on('data', (chunk) => {
|
|
4367
|
-
if (ws.readyState === 1) ws
|
|
4189
|
+
if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
4368
4190
|
});
|
|
4369
4191
|
proc.on('exit', (code) => {
|
|
4370
|
-
if (ws.readyState === 1) ws
|
|
4192
|
+
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
|
|
4371
4193
|
ws.terminalProc = null;
|
|
4372
4194
|
});
|
|
4373
4195
|
proc.on('error', (err) => {
|
|
4374
4196
|
console.error('[TERMINAL] Spawn error (contained):', err.message);
|
|
4375
|
-
if (ws.readyState === 1) ws
|
|
4197
|
+
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
|
|
4376
4198
|
ws.terminalProc = null;
|
|
4377
4199
|
});
|
|
4378
4200
|
proc.stdin.on('error', () => {});
|
|
4379
4201
|
proc.stdout.on('error', () => {});
|
|
4380
4202
|
proc.stderr.on('error', () => {});
|
|
4381
|
-
ws
|
|
4203
|
+
sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
|
|
4382
4204
|
}
|
|
4383
4205
|
} else if (data.type === 'terminal_input') {
|
|
4384
4206
|
if (ws.terminalProc) {
|
|
@@ -4407,56 +4229,56 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4407
4229
|
}
|
|
4408
4230
|
} else if (data.type === 'pm2_list') {
|
|
4409
4231
|
if (!pm2Manager.connected) {
|
|
4410
|
-
if (ws.readyState === 1) ws
|
|
4232
|
+
if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
|
|
4411
4233
|
} else {
|
|
4412
4234
|
pm2Manager.listProcesses().then(processes => {
|
|
4413
4235
|
if (ws.readyState === 1) {
|
|
4414
4236
|
const hasActive = processes.some(p => ['online','launching','stopping','waiting restart'].includes(p.status));
|
|
4415
|
-
ws
|
|
4237
|
+
sendWs(ws, { type: 'pm2_list_response', processes, hasActive });
|
|
4416
4238
|
}
|
|
4417
4239
|
}).catch(() => {
|
|
4418
|
-
if (ws.readyState === 1) ws
|
|
4240
|
+
if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'list failed', timestamp: Date.now() }));
|
|
4419
4241
|
});
|
|
4420
4242
|
}
|
|
4421
4243
|
} else if (data.type === 'pm2_start_monitoring') {
|
|
4422
4244
|
pm2Subscribers.add(ws);
|
|
4423
4245
|
ws.pm2Subscribed = true;
|
|
4424
4246
|
if (!pm2Manager.connected) {
|
|
4425
|
-
if (ws.readyState === 1) ws
|
|
4247
|
+
if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
|
|
4426
4248
|
} else {
|
|
4427
|
-
ws
|
|
4249
|
+
sendWs(ws, { type: 'pm2_monitoring_started' });
|
|
4428
4250
|
}
|
|
4429
4251
|
} else if (data.type === 'pm2_stop_monitoring') {
|
|
4430
4252
|
pm2Subscribers.delete(ws);
|
|
4431
4253
|
ws.pm2Subscribed = false;
|
|
4432
|
-
ws
|
|
4254
|
+
sendWs(ws, { type: 'pm2_monitoring_stopped' });
|
|
4433
4255
|
} else if (data.type === 'pm2_start') {
|
|
4434
4256
|
pm2Manager.startProcess(data.name).then(result => {
|
|
4435
|
-
ws
|
|
4257
|
+
sendWs(ws, { type: 'pm2_start_response', name: data.name, ...result });
|
|
4436
4258
|
});
|
|
4437
4259
|
} else if (data.type === 'pm2_stop') {
|
|
4438
4260
|
pm2Manager.stopProcess(data.name).then(result => {
|
|
4439
|
-
ws
|
|
4261
|
+
sendWs(ws, { type: 'pm2_stop_response', name: data.name, ...result });
|
|
4440
4262
|
});
|
|
4441
4263
|
} else if (data.type === 'pm2_restart') {
|
|
4442
4264
|
pm2Manager.restartProcess(data.name).then(result => {
|
|
4443
|
-
ws
|
|
4265
|
+
sendWs(ws, { type: 'pm2_restart_response', name: data.name, ...result });
|
|
4444
4266
|
});
|
|
4445
4267
|
} else if (data.type === 'pm2_delete') {
|
|
4446
4268
|
pm2Manager.deleteProcess(data.name).then(result => {
|
|
4447
|
-
ws
|
|
4269
|
+
sendWs(ws, { type: 'pm2_delete_response', name: data.name, ...result });
|
|
4448
4270
|
});
|
|
4449
4271
|
} else if (data.type === 'pm2_logs') {
|
|
4450
4272
|
pm2Manager.getLogs(data.name, { lines: data.lines || 100 }).then(result => {
|
|
4451
|
-
ws
|
|
4273
|
+
sendWs(ws, { type: 'pm2_logs_response', name: data.name, ...result });
|
|
4452
4274
|
});
|
|
4453
4275
|
} else if (data.type === 'pm2_flush_logs') {
|
|
4454
4276
|
pm2Manager.flushLogs(data.name).then(result => {
|
|
4455
|
-
ws
|
|
4277
|
+
sendWs(ws, { type: 'pm2_flush_logs_response', name: data.name, ...result });
|
|
4456
4278
|
});
|
|
4457
4279
|
} else if (data.type === 'pm2_ping') {
|
|
4458
4280
|
pm2Manager.ping().then(result => {
|
|
4459
|
-
ws
|
|
4281
|
+
sendWs(ws, { type: 'pm2_ping_response', ...result });
|
|
4460
4282
|
});
|
|
4461
4283
|
}
|
|
4462
4284
|
|
package/static/index.html
CHANGED
|
@@ -3250,7 +3250,6 @@
|
|
|
3250
3250
|
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
3251
3251
|
<script defer src="/gm/js/image-loader.js"></script>
|
|
3252
3252
|
<script src="/gm/lib/msgpackr.min.js"></script>
|
|
3253
|
-
<script defer src="/gm/js/event-consolidator.js"></script>
|
|
3254
3253
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
3255
3254
|
<script defer src="/gm/js/ws-client.js"></script>
|
|
3256
3255
|
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
package/static/js/client.js
CHANGED
|
@@ -80,7 +80,6 @@ class AgentGUIClient {
|
|
|
80
80
|
this._scrollAnimating = false;
|
|
81
81
|
this._scrollLerpFactor = config.scrollAnimationSpeed || 0.15;
|
|
82
82
|
|
|
83
|
-
this._consolidator = typeof EventConsolidator !== 'undefined' ? new EventConsolidator() : null;
|
|
84
83
|
|
|
85
84
|
this._serverProcessingEstimate = 2000;
|
|
86
85
|
this._lastSendTime = 0;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side codec — mirrors lib/codec.js exactly.
|
|
3
|
+
* msgpackr (global from msgpackr.min.js) + GPT tokenizer BPE compression.
|
|
4
|
+
*/
|
|
5
|
+
import { encode as tokEncode, decode as tokDecode } from 'https://esm.sh/gpt-tokenizer';
|
|
6
|
+
|
|
7
|
+
const THRESHOLD = 200;
|
|
8
|
+
const COMPRESSIBLE = new Set(['content', 'text', 'output', 'response', 'prompt', 'input', 'data']);
|
|
9
|
+
|
|
10
|
+
function compressText(str) {
|
|
11
|
+
return { __tok: true, d: tokEncode(str) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function decompressText(val) {
|
|
15
|
+
return tokDecode(val.d);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function encodeObj(obj) {
|
|
19
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
20
|
+
if (Array.isArray(obj)) return obj.map(encodeObj);
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const k of Object.keys(obj)) {
|
|
23
|
+
const v = obj[k];
|
|
24
|
+
if (COMPRESSIBLE.has(k) && typeof v === 'string' && v.length > THRESHOLD) {
|
|
25
|
+
out[k] = compressText(v);
|
|
26
|
+
} else if (v && typeof v === 'object' && !(v instanceof ArrayBuffer) && !ArrayBuffer.isView(v)) {
|
|
27
|
+
out[k] = encodeObj(v);
|
|
28
|
+
} else {
|
|
29
|
+
out[k] = v;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decodeObj(obj) {
|
|
36
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
37
|
+
if (Array.isArray(obj)) return obj.map(decodeObj);
|
|
38
|
+
if (obj.__tok && obj.d) return decompressText(obj);
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const k of Object.keys(obj)) {
|
|
41
|
+
out[k] = decodeObj(obj[k]);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function encode(obj) { return msgpackr.pack(encodeObj(obj)); }
|
|
47
|
+
export function decode(buf) { return decodeObj(msgpackr.unpack(new Uint8Array(buf instanceof ArrayBuffer ? buf : buf))); }
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// codec is loaded as ES module and exposed globally by ws-client.js
|
|
2
|
+
// or inline: import('./codec.js').then(m => window._codec = m)
|
|
3
|
+
|
|
1
4
|
class WebSocketManager {
|
|
2
5
|
constructor(config = {}) {
|
|
3
6
|
this.config = {
|
|
@@ -133,16 +136,8 @@ class WebSocketManager {
|
|
|
133
136
|
async onMessage(event) {
|
|
134
137
|
try {
|
|
135
138
|
let parsed;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const buf = event.data instanceof Blob
|
|
139
|
-
? await event.data.arrayBuffer()
|
|
140
|
-
: event.data;
|
|
141
|
-
parsed = msgpackr.unpack(new Uint8Array(buf));
|
|
142
|
-
} else {
|
|
143
|
-
// Fallback: plain JSON (ping/pong control frames, legacy)
|
|
144
|
-
parsed = JSON.parse(event.data);
|
|
145
|
-
}
|
|
139
|
+
const buf = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
|
|
140
|
+
parsed = window._codec ? window._codec.decode(buf) : msgpackr.unpack(new Uint8Array(buf));
|
|
146
141
|
const messages = Array.isArray(parsed) ? parsed : [parsed];
|
|
147
142
|
this.stats.totalMessagesReceived += messages.length;
|
|
148
143
|
|
|
@@ -443,7 +438,7 @@ class WebSocketManager {
|
|
|
443
438
|
}
|
|
444
439
|
|
|
445
440
|
try {
|
|
446
|
-
this.ws.send(msgpackr.pack(data));
|
|
441
|
+
this.ws.send(window._codec ? window._codec.encode(data) : msgpackr.pack(data));
|
|
447
442
|
this.stats.totalMessagesSent++;
|
|
448
443
|
return true;
|
|
449
444
|
} catch (error) {
|
|
@@ -467,7 +462,7 @@ class WebSocketManager {
|
|
|
467
462
|
this.messageBuffer = [];
|
|
468
463
|
for (const message of messages) {
|
|
469
464
|
try {
|
|
470
|
-
this.ws.send(msgpackr.pack(message));
|
|
465
|
+
this.ws.send(window._codec ? window._codec.encode(message) : msgpackr.pack(message));
|
|
471
466
|
this.stats.totalMessagesSent++;
|
|
472
467
|
} catch (error) {
|
|
473
468
|
this.bufferMessage(message);
|
|
@@ -491,7 +486,7 @@ class WebSocketManager {
|
|
|
491
486
|
if (type === 'session') msg.sessionId = id;
|
|
492
487
|
else msg.conversationId = id;
|
|
493
488
|
try {
|
|
494
|
-
this.ws.send(msgpackr.pack(msg));
|
|
489
|
+
this.ws.send(window._codec ? window._codec.encode(msg) : msgpackr.pack(msg));
|
|
495
490
|
this.stats.totalMessagesSent++;
|
|
496
491
|
} catch (_) {}
|
|
497
492
|
}
|
package/static/js/ws-client.js
CHANGED
|
@@ -78,10 +78,14 @@ class WsClient {
|
|
|
78
78
|
|
|
79
79
|
window.WsClient = WsClient;
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
// Bootstrap: create wsManager and wsClient synchronously so other modules can use them immediately.
|
|
82
|
+
// Codec is loaded async and upgrades encoding once ready; websocket-manager falls back to msgpackr until then.
|
|
83
|
+
window.wsManager = new WebSocketManager();
|
|
84
|
+
window.wsClient = new WsClient(window.wsManager);
|
|
85
|
+
window.wsManager.connect().catch(function() {});
|
|
86
|
+
|
|
87
|
+
import('./codec.js').then(codec => {
|
|
88
|
+
window._codec = codec;
|
|
89
|
+
}).catch(e => {
|
|
90
|
+
console.error('[ws-client] Failed to load codec, using msgpackr fallback:', e);
|
|
91
|
+
});
|
package/lib/sse-stream.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
|
-
|
|
3
|
-
export function formatSSEEvent(eventType, data) {
|
|
4
|
-
const lines = [];
|
|
5
|
-
if (eventType) {
|
|
6
|
-
lines.push(`event: ${eventType}`);
|
|
7
|
-
}
|
|
8
|
-
if (data) {
|
|
9
|
-
const jsonData = typeof data === 'string' ? data : JSON.stringify(data);
|
|
10
|
-
lines.push(`data: ${jsonData}`);
|
|
11
|
-
}
|
|
12
|
-
lines.push('');
|
|
13
|
-
return lines.join('\n') + '\n';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function convertToACPRunOutputStream(sessionId, block, runStatus = 'active') {
|
|
17
|
-
const eventId = crypto.randomUUID();
|
|
18
|
-
return {
|
|
19
|
-
id: eventId,
|
|
20
|
-
event: 'agent_event',
|
|
21
|
-
data: {
|
|
22
|
-
type: 'custom',
|
|
23
|
-
run_id: sessionId,
|
|
24
|
-
status: runStatus,
|
|
25
|
-
update: block
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function createErrorEvent(runId, errorMessage, errorCode = 'execution_error') {
|
|
31
|
-
const eventId = crypto.randomUUID();
|
|
32
|
-
return {
|
|
33
|
-
id: eventId,
|
|
34
|
-
event: 'agent_event',
|
|
35
|
-
data: {
|
|
36
|
-
type: 'error',
|
|
37
|
-
run_id: runId,
|
|
38
|
-
error: errorMessage,
|
|
39
|
-
code: errorCode,
|
|
40
|
-
status: 'error'
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function createCompletionEvent(runId, values = {}, metadata = {}) {
|
|
46
|
-
const eventId = crypto.randomUUID();
|
|
47
|
-
return {
|
|
48
|
-
id: eventId,
|
|
49
|
-
event: 'agent_event',
|
|
50
|
-
data: {
|
|
51
|
-
type: 'result',
|
|
52
|
-
run_id: runId,
|
|
53
|
-
status: 'completed',
|
|
54
|
-
values,
|
|
55
|
-
metadata
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function createKeepAlive() {
|
|
61
|
-
return ': ping\n\n';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class SSEStreamManager {
|
|
65
|
-
constructor(res, runId) {
|
|
66
|
-
this.res = res;
|
|
67
|
-
this.runId = runId;
|
|
68
|
-
this.keepAliveInterval = null;
|
|
69
|
-
this.closed = false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
start() {
|
|
73
|
-
this.res.writeHead(200, {
|
|
74
|
-
'Content-Type': 'text/event-stream',
|
|
75
|
-
'Cache-Control': 'no-cache',
|
|
76
|
-
'Connection': 'keep-alive',
|
|
77
|
-
'X-Accel-Buffering': 'no'
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
this.keepAliveInterval = setInterval(() => {
|
|
81
|
-
if (!this.closed) {
|
|
82
|
-
this.writeRaw(createKeepAlive());
|
|
83
|
-
}
|
|
84
|
-
}, 15000);
|
|
85
|
-
|
|
86
|
-
this.res.on('close', () => {
|
|
87
|
-
this.cleanup();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
writeRaw(text) {
|
|
92
|
-
if (!this.closed) {
|
|
93
|
-
this.res.write(text);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
sendProgress(block, runStatus = 'active') {
|
|
98
|
-
const acpEvent = convertToACPRunOutputStream(this.runId, block, runStatus);
|
|
99
|
-
const sse = formatSSEEvent('message', acpEvent.data);
|
|
100
|
-
this.writeRaw(sse);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
sendError(errorMessage, errorCode = 'execution_error') {
|
|
104
|
-
const errorEvent = createErrorEvent(this.runId, errorMessage, errorCode);
|
|
105
|
-
const sse = formatSSEEvent('error', errorEvent.data);
|
|
106
|
-
this.writeRaw(sse);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
sendComplete(values = {}, metadata = {}) {
|
|
110
|
-
const completionEvent = createCompletionEvent(this.runId, values, metadata);
|
|
111
|
-
const sse = formatSSEEvent('done', completionEvent.data);
|
|
112
|
-
this.writeRaw(sse);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
cleanup() {
|
|
116
|
-
if (this.keepAliveInterval) {
|
|
117
|
-
clearInterval(this.keepAliveInterval);
|
|
118
|
-
this.keepAliveInterval = null;
|
|
119
|
-
}
|
|
120
|
-
this.closed = true;
|
|
121
|
-
if (!this.res.writableEnded) {
|
|
122
|
-
this.res.end();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
package/lib/ws-events.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
const S2L = {
|
|
2
|
-
's.start': 'streaming_start', 's.prog': 'streaming_progress',
|
|
3
|
-
's.done': 'streaming_complete', 's.err': 'streaming_error',
|
|
4
|
-
's.cancel': 'streaming_cancelled', 'conv.new': 'conversation_created',
|
|
5
|
-
'conv.upd': 'conversation_updated', 'convs.upd': 'conversations_updated',
|
|
6
|
-
'conv.del': 'conversation_deleted', 'msg.new': 'message_created',
|
|
7
|
-
'q.stat': 'queue_status', 'q.upd': 'queue_updated',
|
|
8
|
-
'rl.hit': 'rate_limit_hit', 'rl.clr': 'rate_limit_clear',
|
|
9
|
-
'scr.start': 'script_started', 'scr.stop': 'script_stopped',
|
|
10
|
-
'scr.out': 'script_output', 'mdl.prog': 'model_download_progress',
|
|
11
|
-
'stt.prog': 'stt_progress', 'tts.prog': 'tts_setup_progress',
|
|
12
|
-
'voice.ls': 'voice_list', 'sub.ok': 'subscription_confirmed',
|
|
13
|
-
'term.out': 'terminal_output', 'term.exit': 'terminal_exit',
|
|
14
|
-
'term.start': 'terminal_started'
|
|
15
|
-
};
|
|
16
|
-
const L2S = Object.fromEntries(Object.entries(S2L).map(([k, v]) => [v, k]));
|
|
17
|
-
const toLong = (s) => S2L[s] || s;
|
|
18
|
-
const toShort = (l) => L2S[l] || l;
|
|
19
|
-
if (typeof window !== 'undefined') window.wsEvents = { S2L, L2S, toLong, toShort };
|
|
20
|
-
export { S2L, L2S, toLong, toShort };
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
class EventConsolidator {
|
|
2
|
-
consolidate(chunks) {
|
|
3
|
-
const stats = { original: chunks.length, deduplicated: 0, textMerged: 0, toolsCollapsed: 0, systemSuperseded: 0 };
|
|
4
|
-
if (chunks.length <= 1) return { consolidated: chunks, stats };
|
|
5
|
-
|
|
6
|
-
const sorted = chunks.slice().sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
|
|
7
|
-
|
|
8
|
-
const seen = new Set();
|
|
9
|
-
const deduped = [];
|
|
10
|
-
for (const c of sorted) {
|
|
11
|
-
const key = c.sessionId + ':' + c.sequence;
|
|
12
|
-
if (c.sequence !== undefined && seen.has(key)) { stats.deduplicated++; continue; }
|
|
13
|
-
if (c.sequence !== undefined) seen.add(key);
|
|
14
|
-
deduped.push(c);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const bySession = {};
|
|
18
|
-
for (const c of deduped) {
|
|
19
|
-
const sid = c.sessionId || '_';
|
|
20
|
-
if (!bySession[sid]) bySession[sid] = [];
|
|
21
|
-
bySession[sid].push(c);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const result = [];
|
|
25
|
-
for (const sid of Object.keys(bySession)) {
|
|
26
|
-
const sessionChunks = bySession[sid];
|
|
27
|
-
const merged = this._mergeTextBlocks(sessionChunks, stats);
|
|
28
|
-
this._collapseToolPairs(merged, stats);
|
|
29
|
-
const superseded = this._supersedeSystemBlocks(merged, stats);
|
|
30
|
-
result.push(...superseded);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
result.sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
|
|
34
|
-
return { consolidated: result, stats };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
_mergeTextBlocks(chunks, stats) {
|
|
38
|
-
const result = [];
|
|
39
|
-
let pending = null;
|
|
40
|
-
const MAX_MERGE = 50 * 1024;
|
|
41
|
-
|
|
42
|
-
for (const c of chunks) {
|
|
43
|
-
if (c.block?.type === 'text') {
|
|
44
|
-
if (pending) {
|
|
45
|
-
const pendingText = pending.block.text || '';
|
|
46
|
-
const newText = c.block.text || '';
|
|
47
|
-
const combined = pendingText + newText;
|
|
48
|
-
if (combined.length <= MAX_MERGE) {
|
|
49
|
-
pending = {
|
|
50
|
-
...pending,
|
|
51
|
-
block: { ...pending.block, text: combined },
|
|
52
|
-
created_at: c.created_at,
|
|
53
|
-
_mergedSequences: [...(pending._mergedSequences || [pending.sequence]), c.sequence]
|
|
54
|
-
};
|
|
55
|
-
stats.textMerged++;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (pending) result.push(pending);
|
|
60
|
-
pending = { ...c, _mergedSequences: [c.sequence] };
|
|
61
|
-
} else {
|
|
62
|
-
if (pending) { result.push(pending); pending = null; }
|
|
63
|
-
result.push(c);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (pending) result.push(pending);
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
_collapseToolPairs(chunks, stats) {
|
|
71
|
-
const toolUseMap = {};
|
|
72
|
-
for (const c of chunks) {
|
|
73
|
-
if (c.block?.type === 'tool_use' && c.block.id) toolUseMap[c.block.id] = c;
|
|
74
|
-
}
|
|
75
|
-
for (const c of chunks) {
|
|
76
|
-
if (c.block?.type === 'tool_result' && c.block.tool_use_id) {
|
|
77
|
-
const match = toolUseMap[c.block.tool_use_id];
|
|
78
|
-
if (match) {
|
|
79
|
-
match.block._hasResult = true;
|
|
80
|
-
c.block._collapsed = true;
|
|
81
|
-
stats.toolsCollapsed++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
_supersedeSystemBlocks(chunks, stats) {
|
|
88
|
-
const systemIndices = [];
|
|
89
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
90
|
-
if (chunks[i].block?.type === 'system') systemIndices.push(i);
|
|
91
|
-
}
|
|
92
|
-
if (systemIndices.length <= 1) return chunks;
|
|
93
|
-
const keep = new Set();
|
|
94
|
-
keep.add(systemIndices[systemIndices.length - 1]);
|
|
95
|
-
stats.systemSuperseded += systemIndices.length - 1;
|
|
96
|
-
return chunks.filter((_, i) => !systemIndices.includes(i) || keep.has(i));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = EventConsolidator;
|