agentgui 1.0.214 → 1.0.216
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/package.json +1 -1
- package/server.js +11 -2
- package/static/index.html +17 -0
- package/static/js/client.js +267 -17
- package/static/js/event-consolidator.js +98 -0
- package/static/js/kalman-filter.js +67 -0
- package/static/js/websocket-manager.js +82 -5
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -99,7 +99,7 @@ const express = require('express');
|
|
|
99
99
|
const Busboy = require('busboy');
|
|
100
100
|
const fsbrowse = require('fsbrowse');
|
|
101
101
|
|
|
102
|
-
const SYSTEM_PROMPT = `Your output will be spoken aloud by a text-to-speech system. Write ONLY plain conversational sentences that sound natural when read aloud. Never use markdown, bold, italics, headers, bullet points, numbered lists, tables, or any formatting. Never use colons to introduce lists or options. Never use labels like "Option A" or "1." followed by a title. Instead of listing options, describe them conversationally in flowing sentences. For example, instead of "**Option 1**: Do X" say "One approach would be to do X." Keep sentences short and simple. Use transition words like "also", "another option", "or alternatively" to connect ideas. Write as if you are speaking to someone in a casual conversation.`;
|
|
102
|
+
const SYSTEM_PROMPT = `Your output will be spoken aloud by a text-to-speech system. Write ONLY plain conversational sentences that sound natural when read aloud. Never use markdown, bold, italics, headers, bullet points, numbered lists, tables, or any formatting. Never use colons to introduce lists or options. Never use labels like "Option A" or "1." followed by a title. Instead of listing options, describe them conversationally in flowing sentences. For example, instead of "**Option 1**: Do X" say "One approach would be to do X." Keep sentences short and simple. Use transition words like "also", "another option", "or alternatively" to connect ideas. When mentioning file names, spell out the dot between the name and extension as the word "dot" so it is spoken clearly. For example, say "server dot js" instead of "server.js", "index dot html" instead of "index.html", and "package dot json" instead of "package.json". Write as if you are speaking to someone in a casual conversation.`;
|
|
103
103
|
|
|
104
104
|
const activeExecutions = new Map();
|
|
105
105
|
const activeScripts = new Map();
|
|
@@ -2168,6 +2168,7 @@ wss.on('connection', (ws, req) => {
|
|
|
2168
2168
|
} else if (data.type === 'latency_report') {
|
|
2169
2169
|
ws.latencyTier = data.quality || 'good';
|
|
2170
2170
|
ws.latencyAvg = data.avg || 0;
|
|
2171
|
+
ws.latencyTrend = data.trend || 'stable';
|
|
2171
2172
|
} else if (data.type === 'ping') {
|
|
2172
2173
|
ws.send(JSON.stringify({
|
|
2173
2174
|
type: 'pong',
|
|
@@ -2207,8 +2208,16 @@ const BROADCAST_TYPES = new Set([
|
|
|
2207
2208
|
const wsBatchQueues = new Map();
|
|
2208
2209
|
const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
|
|
2209
2210
|
|
|
2211
|
+
const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
|
|
2210
2212
|
function getBatchInterval(ws) {
|
|
2211
|
-
|
|
2213
|
+
const tier = ws.latencyTier || 'good';
|
|
2214
|
+
const trend = ws.latencyTrend;
|
|
2215
|
+
if (trend === 'rising' || trend === 'falling') {
|
|
2216
|
+
const idx = TIER_ORDER.indexOf(tier);
|
|
2217
|
+
if (trend === 'rising' && idx < TIER_ORDER.length - 1) return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
|
|
2218
|
+
if (trend === 'falling' && idx > 0) return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
|
|
2219
|
+
}
|
|
2220
|
+
return BATCH_BY_TIER[tier] || 32;
|
|
2212
2221
|
}
|
|
2213
2222
|
|
|
2214
2223
|
function flushWsBatch(ws) {
|
package/static/index.html
CHANGED
|
@@ -2292,6 +2292,21 @@
|
|
|
2292
2292
|
.streaming-blocks > details.block-tool-use {
|
|
2293
2293
|
transition: max-height 0.3s ease;
|
|
2294
2294
|
}
|
|
2295
|
+
@keyframes chunk-placeholder-pulse {
|
|
2296
|
+
0%, 100% { opacity: 0.3; }
|
|
2297
|
+
50% { opacity: 0.6; }
|
|
2298
|
+
}
|
|
2299
|
+
.chunk-placeholder {
|
|
2300
|
+
height: 2rem;
|
|
2301
|
+
border-radius: 0.375rem;
|
|
2302
|
+
background: var(--color-bg-secondary);
|
|
2303
|
+
animation: chunk-placeholder-pulse 1s ease-in-out infinite;
|
|
2304
|
+
margin: 0.25rem 0;
|
|
2305
|
+
}
|
|
2306
|
+
.connection-dot.degrading {
|
|
2307
|
+
animation: pulse 1s ease-in-out infinite;
|
|
2308
|
+
background-color: var(--color-warning) !important;
|
|
2309
|
+
}
|
|
2295
2310
|
</style>
|
|
2296
2311
|
</head>
|
|
2297
2312
|
<body>
|
|
@@ -2483,6 +2498,8 @@
|
|
|
2483
2498
|
</script>
|
|
2484
2499
|
<script defer src="/gm/js/event-processor.js"></script>
|
|
2485
2500
|
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
2501
|
+
<script defer src="/gm/js/kalman-filter.js"></script>
|
|
2502
|
+
<script defer src="/gm/js/event-consolidator.js"></script>
|
|
2486
2503
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
2487
2504
|
<script defer src="/gm/js/event-filter.js"></script>
|
|
2488
2505
|
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
package/static/js/client.js
CHANGED
|
@@ -62,6 +62,22 @@ class AgentGUIClient {
|
|
|
62
62
|
this._inflightRequests = new Map();
|
|
63
63
|
this._previousConvAbort = null;
|
|
64
64
|
|
|
65
|
+
this._scrollKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 50, measurementNoise: 100 }) : null;
|
|
66
|
+
this._scrollTarget = 0;
|
|
67
|
+
this._scrollAnimating = false;
|
|
68
|
+
this._scrollLerpFactor = config.scrollAnimationSpeed || 0.15;
|
|
69
|
+
|
|
70
|
+
this._chunkTimingKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 10, measurementNoise: 200 }) : null;
|
|
71
|
+
this._lastChunkArrival = 0;
|
|
72
|
+
this._chunkTimingUpdateCount = 0;
|
|
73
|
+
this._chunkMissedPredictions = 0;
|
|
74
|
+
|
|
75
|
+
this._consolidator = typeof EventConsolidator !== 'undefined' ? new EventConsolidator() : null;
|
|
76
|
+
|
|
77
|
+
this._serverProcessingEstimate = 2000;
|
|
78
|
+
this._lastSendTime = 0;
|
|
79
|
+
this._countdownTimer = null;
|
|
80
|
+
|
|
65
81
|
// Router state
|
|
66
82
|
this.routerState = {
|
|
67
83
|
currentConversationId: null,
|
|
@@ -103,6 +119,7 @@ class AgentGUIClient {
|
|
|
103
119
|
|
|
104
120
|
this.state.isInitialized = true;
|
|
105
121
|
this.emit('initialized');
|
|
122
|
+
this._setupDebugHooks();
|
|
106
123
|
|
|
107
124
|
console.log('AgentGUI client initialized');
|
|
108
125
|
return this;
|
|
@@ -147,6 +164,16 @@ class AgentGUIClient {
|
|
|
147
164
|
this.wsManager.on('latency_update', (data) => {
|
|
148
165
|
this._updateConnectionIndicator(data.quality);
|
|
149
166
|
});
|
|
167
|
+
|
|
168
|
+
this.wsManager.on('connection_degrading', () => {
|
|
169
|
+
const dot = document.querySelector('.connection-dot');
|
|
170
|
+
if (dot) dot.classList.add('degrading');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.wsManager.on('connection_recovering', () => {
|
|
174
|
+
const dot = document.querySelector('.connection-dot');
|
|
175
|
+
if (dot) dot.classList.remove('degrading');
|
|
176
|
+
});
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
/**
|
|
@@ -409,6 +436,13 @@ class AgentGUIClient {
|
|
|
409
436
|
|
|
410
437
|
async handleStreamingStart(data) {
|
|
411
438
|
console.log('Streaming started:', data);
|
|
439
|
+
this._clearThinkingCountdown();
|
|
440
|
+
if (this._lastSendTime > 0) {
|
|
441
|
+
const actual = Date.now() - this._lastSendTime;
|
|
442
|
+
const predicted = this.wsManager?.latency?.predicted || 0;
|
|
443
|
+
const serverTime = Math.max(500, actual - predicted);
|
|
444
|
+
this._serverProcessingEstimate = 0.7 * this._serverProcessingEstimate + 0.3 * serverTime;
|
|
445
|
+
}
|
|
412
446
|
|
|
413
447
|
// If this streaming event is for a different conversation than what we are viewing,
|
|
414
448
|
// just track the state but do not modify the DOM or start polling
|
|
@@ -585,21 +619,54 @@ class AgentGUIClient {
|
|
|
585
619
|
}
|
|
586
620
|
|
|
587
621
|
scrollToBottom() {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
622
|
+
const scrollContainer = document.getElementById('output-scroll');
|
|
623
|
+
if (!scrollContainer) return;
|
|
624
|
+
const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
|
|
625
|
+
|
|
626
|
+
if (distFromBottom > 150) {
|
|
627
|
+
this._unseenCount = (this._unseenCount || 0) + 1;
|
|
628
|
+
this._showNewContentPill();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
633
|
+
const isStreaming = this.state.streamingConversations.size > 0;
|
|
634
|
+
|
|
635
|
+
if (!isStreaming || !this._scrollKalman || Math.abs(maxScroll - scrollContainer.scrollTop) > 2000) {
|
|
636
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
637
|
+
this._removeNewContentPill();
|
|
638
|
+
this._scrollAnimating = false;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
this._scrollKalman.update(maxScroll);
|
|
643
|
+
this._scrollTarget = this._scrollKalman.predict();
|
|
644
|
+
|
|
645
|
+
const conf = this._chunkArrivalConfidence();
|
|
646
|
+
if (conf > 0.5) {
|
|
647
|
+
const estHeight = this._estimatedBlockHeight('text') * 0.5 * conf;
|
|
648
|
+
this._scrollTarget += estHeight;
|
|
649
|
+
const trueMax = scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
650
|
+
if (this._scrollTarget > trueMax + 100) this._scrollTarget = trueMax + 100;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (!this._scrollAnimating) {
|
|
654
|
+
this._scrollAnimating = true;
|
|
655
|
+
const animate = () => {
|
|
656
|
+
if (!this._scrollAnimating) return;
|
|
657
|
+
const sc = document.getElementById('output-scroll');
|
|
658
|
+
if (!sc) { this._scrollAnimating = false; return; }
|
|
659
|
+
const diff = this._scrollTarget - sc.scrollTop;
|
|
660
|
+
if (Math.abs(diff) < 1) {
|
|
661
|
+
sc.scrollTop = this._scrollTarget;
|
|
662
|
+
if (this.state.streamingConversations.size === 0) { this._scrollAnimating = false; return; }
|
|
663
|
+
}
|
|
664
|
+
sc.scrollTop += diff * this._scrollLerpFactor;
|
|
597
665
|
this._removeNewContentPill();
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
});
|
|
666
|
+
requestAnimationFrame(animate);
|
|
667
|
+
};
|
|
668
|
+
requestAnimationFrame(animate);
|
|
669
|
+
}
|
|
603
670
|
}
|
|
604
671
|
|
|
605
672
|
_showNewContentPill() {
|
|
@@ -627,6 +694,7 @@ class AgentGUIClient {
|
|
|
627
694
|
|
|
628
695
|
handleStreamingError(data) {
|
|
629
696
|
console.error('Streaming error:', data);
|
|
697
|
+
this._clearThinkingCountdown();
|
|
630
698
|
|
|
631
699
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
632
700
|
|
|
@@ -658,6 +726,7 @@ class AgentGUIClient {
|
|
|
658
726
|
|
|
659
727
|
handleStreamingComplete(data) {
|
|
660
728
|
console.log('Streaming completed:', data);
|
|
729
|
+
this._clearThinkingCountdown();
|
|
661
730
|
|
|
662
731
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
663
732
|
if (conversationId) this.invalidateCache(conversationId);
|
|
@@ -1157,7 +1226,130 @@ class AgentGUIClient {
|
|
|
1157
1226
|
|
|
1158
1227
|
_getAdaptivePollInterval() {
|
|
1159
1228
|
const quality = this.wsManager?.latency?.quality || 'unknown';
|
|
1160
|
-
|
|
1229
|
+
const base = this._pollIntervalByTier[quality] || 200;
|
|
1230
|
+
const trend = this.wsManager?.latency?.trend;
|
|
1231
|
+
if (!trend || trend === 'stable') return base;
|
|
1232
|
+
const tiers = ['excellent', 'good', 'fair', 'poor', 'bad'];
|
|
1233
|
+
const idx = tiers.indexOf(quality);
|
|
1234
|
+
if (trend === 'rising' && idx < tiers.length - 1) return this._pollIntervalByTier[tiers[idx + 1]];
|
|
1235
|
+
if (trend === 'falling' && idx > 0) return this._pollIntervalByTier[tiers[idx - 1]];
|
|
1236
|
+
return base;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
_chunkArrivalConfidence() {
|
|
1240
|
+
if (this._chunkTimingUpdateCount < 2) return 0;
|
|
1241
|
+
const base = Math.min(1, this._chunkTimingUpdateCount / 8);
|
|
1242
|
+
const penalty = Math.min(1, this._chunkMissedPredictions * 0.33);
|
|
1243
|
+
return Math.max(0, base - penalty);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
_predictedNextChunkArrival() {
|
|
1247
|
+
if (!this._chunkTimingKalman || this._chunkTimingUpdateCount < 2) return 0;
|
|
1248
|
+
return this._lastChunkArrival + Math.min(this._chunkTimingKalman.predict(), 5000);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
_schedulePreAllocation(sessionId) {
|
|
1252
|
+
if (this._placeholderTimer) clearTimeout(this._placeholderTimer);
|
|
1253
|
+
if (this._chunkArrivalConfidence() < 0.5) return;
|
|
1254
|
+
const scrollContainer = document.getElementById('output-scroll');
|
|
1255
|
+
if (!scrollContainer) return;
|
|
1256
|
+
const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
|
|
1257
|
+
if (distFromBottom > 150) return;
|
|
1258
|
+
const nextArrival = this._predictedNextChunkArrival();
|
|
1259
|
+
if (!nextArrival) return;
|
|
1260
|
+
const delay = Math.max(0, nextArrival - performance.now() - 100);
|
|
1261
|
+
this._placeholderTimer = setTimeout(() => {
|
|
1262
|
+
this._placeholderTimer = null;
|
|
1263
|
+
this._insertPlaceholder(sessionId);
|
|
1264
|
+
}, delay);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
_insertPlaceholder(sessionId) {
|
|
1268
|
+
this._removePlaceholder();
|
|
1269
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
1270
|
+
if (!streamingEl) return;
|
|
1271
|
+
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
1272
|
+
if (!blocksEl) return;
|
|
1273
|
+
const ph = document.createElement('div');
|
|
1274
|
+
ph.className = 'chunk-placeholder';
|
|
1275
|
+
ph.id = 'chunk-placeholder-active';
|
|
1276
|
+
blocksEl.appendChild(ph);
|
|
1277
|
+
this._placeholderAutoRemove = setTimeout(() => this._removePlaceholder(), 500);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
_removePlaceholder() {
|
|
1281
|
+
if (this._placeholderAutoRemove) { clearTimeout(this._placeholderAutoRemove); this._placeholderAutoRemove = null; }
|
|
1282
|
+
const ph = document.getElementById('chunk-placeholder-active');
|
|
1283
|
+
if (ph && ph.parentNode) ph.remove();
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
_trackBlockHeight(block, element) {
|
|
1287
|
+
if (!element || !block?.type) return;
|
|
1288
|
+
const h = element.offsetHeight;
|
|
1289
|
+
if (h <= 0) return;
|
|
1290
|
+
if (!this._blockHeightAvg) this._blockHeightAvg = {};
|
|
1291
|
+
const t = block.type;
|
|
1292
|
+
if (!this._blockHeightAvg[t]) this._blockHeightAvg[t] = { sum: 0, count: 0 };
|
|
1293
|
+
this._blockHeightAvg[t].sum += h;
|
|
1294
|
+
this._blockHeightAvg[t].count++;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
_estimatedBlockHeight(type) {
|
|
1298
|
+
const defaults = { text: 40, tool_use: 60, tool_result: 40 };
|
|
1299
|
+
if (this._blockHeightAvg?.[type]?.count >= 3) {
|
|
1300
|
+
return this._blockHeightAvg[type].sum / this._blockHeightAvg[type].count;
|
|
1301
|
+
}
|
|
1302
|
+
return defaults[type] || 40;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
_startThinkingCountdown() {
|
|
1306
|
+
this._clearThinkingCountdown();
|
|
1307
|
+
if (!this._lastSendTime) return;
|
|
1308
|
+
const predicted = this.wsManager?.latency?.predicted || 0;
|
|
1309
|
+
const estimatedWait = predicted + this._serverProcessingEstimate;
|
|
1310
|
+
if (estimatedWait < 1000) return;
|
|
1311
|
+
let remaining = Math.ceil(estimatedWait / 1000);
|
|
1312
|
+
const update = () => {
|
|
1313
|
+
const indicator = document.querySelector('.streaming-indicator');
|
|
1314
|
+
if (!indicator) return;
|
|
1315
|
+
if (remaining > 0) {
|
|
1316
|
+
indicator.textContent = `Thinking... (~${remaining}s)`;
|
|
1317
|
+
remaining--;
|
|
1318
|
+
this._countdownTimer = setTimeout(update, 1000);
|
|
1319
|
+
} else {
|
|
1320
|
+
indicator.textContent = 'Thinking... (taking longer than expected)';
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
this._countdownTimer = setTimeout(update, 100);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
_clearThinkingCountdown() {
|
|
1327
|
+
if (this._countdownTimer) { clearTimeout(this._countdownTimer); this._countdownTimer = null; }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
_setupDebugHooks() {
|
|
1331
|
+
if (typeof window === 'undefined') return;
|
|
1332
|
+
const kalmanHistory = { latency: [], scroll: [], chunkTiming: [] };
|
|
1333
|
+
const self = this;
|
|
1334
|
+
window.__kalman = {
|
|
1335
|
+
latency: this.wsManager?._latencyKalman || null,
|
|
1336
|
+
scroll: this._scrollKalman || null,
|
|
1337
|
+
chunkTiming: this._chunkTimingKalman || null,
|
|
1338
|
+
history: kalmanHistory,
|
|
1339
|
+
getState: () => ({
|
|
1340
|
+
latency: self.wsManager?._latencyKalman?.getState() || null,
|
|
1341
|
+
scroll: self._scrollKalman?.getState() || null,
|
|
1342
|
+
chunkTiming: self._chunkTimingKalman?.getState() || null,
|
|
1343
|
+
serverProcessingEstimate: self._serverProcessingEstimate,
|
|
1344
|
+
chunkConfidence: self._chunkArrivalConfidence(),
|
|
1345
|
+
latencyTrend: self.wsManager?.latency?.trend || null
|
|
1346
|
+
})
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
this.wsManager.on('latency_prediction', (data) => {
|
|
1350
|
+
kalmanHistory.latency.push({ time: Date.now(), ...data });
|
|
1351
|
+
if (kalmanHistory.latency.length > 100) kalmanHistory.latency.shift();
|
|
1352
|
+
});
|
|
1161
1353
|
}
|
|
1162
1354
|
|
|
1163
1355
|
_showSkeletonLoading(conversationId) {
|
|
@@ -1230,6 +1422,8 @@ class AgentGUIClient {
|
|
|
1230
1422
|
this.wsManager.subscribeToSession(result.session.id);
|
|
1231
1423
|
}
|
|
1232
1424
|
|
|
1425
|
+
this._lastSendTime = Date.now();
|
|
1426
|
+
this._startThinkingCountdown();
|
|
1233
1427
|
this.emit('execution:started', result);
|
|
1234
1428
|
} catch (error) {
|
|
1235
1429
|
console.error('Stream execution error:', error);
|
|
@@ -1334,14 +1528,38 @@ class AgentGUIClient {
|
|
|
1334
1528
|
pollState.emptyPollCount = 0;
|
|
1335
1529
|
const lastChunk = chunks[chunks.length - 1];
|
|
1336
1530
|
pollState.lastFetchTimestamp = lastChunk.created_at;
|
|
1531
|
+
|
|
1532
|
+
const now = performance.now();
|
|
1533
|
+
if (this._lastChunkArrival > 0 && this._chunkTimingKalman) {
|
|
1534
|
+
const delta = now - this._lastChunkArrival;
|
|
1535
|
+
this._chunkTimingKalman.update(delta);
|
|
1536
|
+
this._chunkTimingUpdateCount++;
|
|
1537
|
+
this._chunkMissedPredictions = 0;
|
|
1538
|
+
}
|
|
1539
|
+
this._lastChunkArrival = now;
|
|
1540
|
+
|
|
1337
1541
|
this.renderChunkBatch(chunks.filter(c => c.block && c.block.type));
|
|
1542
|
+
if (this.state.currentSession?.id) this._schedulePreAllocation(this.state.currentSession.id);
|
|
1338
1543
|
} else {
|
|
1339
1544
|
pollState.emptyPollCount++;
|
|
1545
|
+
if (this._chunkTimingUpdateCount > 0) this._chunkMissedPredictions++;
|
|
1340
1546
|
pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
|
|
1341
1547
|
}
|
|
1342
1548
|
|
|
1343
1549
|
if (pollState.isPolling) {
|
|
1344
|
-
|
|
1550
|
+
let nextDelay = pollState.backoffDelay;
|
|
1551
|
+
if (this._chunkArrivalConfidence() >= 0.3 && this._chunkTimingKalman) {
|
|
1552
|
+
const predicted = this._chunkTimingKalman.predict();
|
|
1553
|
+
const elapsed = performance.now() - this._lastChunkArrival;
|
|
1554
|
+
const untilNext = predicted - elapsed - 20;
|
|
1555
|
+
nextDelay = Math.max(50, Math.min(2000, untilNext));
|
|
1556
|
+
if (this._chunkMissedPredictions >= 3) {
|
|
1557
|
+
this._chunkTimingKalman.setProcessNoise(20);
|
|
1558
|
+
} else {
|
|
1559
|
+
this._chunkTimingKalman.setProcessNoise(10);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
pollState.pollTimer = setTimeout(pollOnce, nextDelay);
|
|
1345
1563
|
}
|
|
1346
1564
|
} catch (error) {
|
|
1347
1565
|
console.warn('Chunk poll error:', error.message);
|
|
@@ -1372,6 +1590,13 @@ class AgentGUIClient {
|
|
|
1372
1590
|
}
|
|
1373
1591
|
|
|
1374
1592
|
pollState.isPolling = false;
|
|
1593
|
+
this._scrollAnimating = false;
|
|
1594
|
+
if (this._scrollKalman) this._scrollKalman.reset();
|
|
1595
|
+
if (this._chunkTimingKalman) this._chunkTimingKalman.reset();
|
|
1596
|
+
this._chunkTimingUpdateCount = 0;
|
|
1597
|
+
this._chunkMissedPredictions = 0;
|
|
1598
|
+
this._lastChunkArrival = 0;
|
|
1599
|
+
if (this._placeholderTimer) { clearTimeout(this._placeholderTimer); this._placeholderTimer = null; }
|
|
1375
1600
|
}
|
|
1376
1601
|
|
|
1377
1602
|
/**
|
|
@@ -1397,13 +1622,36 @@ class AgentGUIClient {
|
|
|
1397
1622
|
|
|
1398
1623
|
renderChunkBatch(chunks) {
|
|
1399
1624
|
if (!chunks.length) return;
|
|
1400
|
-
const
|
|
1625
|
+
const deduped = [];
|
|
1401
1626
|
for (const chunk of chunks) {
|
|
1402
1627
|
const sid = chunk.sessionId;
|
|
1403
1628
|
if (!this._renderedSeqs.has(sid)) this._renderedSeqs.set(sid, new Set());
|
|
1404
1629
|
const seqSet = this._renderedSeqs.get(sid);
|
|
1405
1630
|
if (chunk.sequence !== undefined && seqSet.has(chunk.sequence)) continue;
|
|
1406
1631
|
if (chunk.sequence !== undefined) seqSet.add(chunk.sequence);
|
|
1632
|
+
deduped.push(chunk);
|
|
1633
|
+
}
|
|
1634
|
+
if (!deduped.length) return;
|
|
1635
|
+
|
|
1636
|
+
let toRender = deduped;
|
|
1637
|
+
if (this._consolidator) {
|
|
1638
|
+
const { consolidated, stats } = this._consolidator.consolidate(deduped);
|
|
1639
|
+
toRender = consolidated;
|
|
1640
|
+
for (const c of consolidated) {
|
|
1641
|
+
if (c._mergedSequences) {
|
|
1642
|
+
const seqSet = this._renderedSeqs.get(c.sessionId);
|
|
1643
|
+
if (seqSet) c._mergedSequences.forEach(s => seqSet.add(s));
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (stats.textMerged || stats.toolsCollapsed || stats.systemSuperseded) {
|
|
1647
|
+
console.log('Consolidation:', stats);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
this._removePlaceholder();
|
|
1652
|
+
const groups = {};
|
|
1653
|
+
for (const chunk of toRender) {
|
|
1654
|
+
const sid = chunk.sessionId;
|
|
1407
1655
|
if (!groups[sid]) groups[sid] = [];
|
|
1408
1656
|
groups[sid].push(chunk);
|
|
1409
1657
|
}
|
|
@@ -1542,6 +1790,8 @@ class AgentGUIClient {
|
|
|
1542
1790
|
tooltip.innerHTML = [
|
|
1543
1791
|
`<div>State: ${state}</div>`,
|
|
1544
1792
|
`<div>Latency: ${Math.round(latency.avg || 0)}ms</div>`,
|
|
1793
|
+
`<div>Predicted: ${Math.round(latency.predicted || 0)}ms (Kalman)</div>`,
|
|
1794
|
+
`<div>Trend: ${latency.trend || 'unknown'}</div>`,
|
|
1545
1795
|
`<div>Jitter: ${Math.round(latency.jitter || 0)}ms</div>`,
|
|
1546
1796
|
`<div>Quality: ${latency.quality || 'unknown'}</div>`,
|
|
1547
1797
|
`<div>Reconnects: ${stats.totalReconnects || 0}</div>`,
|
|
@@ -0,0 +1,98 @@
|
|
|
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 combined = (pending.block.text || '') + '\n' + (c.block.text || '');
|
|
46
|
+
if (combined.length <= MAX_MERGE) {
|
|
47
|
+
pending = {
|
|
48
|
+
...pending,
|
|
49
|
+
block: { ...pending.block, text: combined },
|
|
50
|
+
created_at: c.created_at,
|
|
51
|
+
_mergedSequences: [...(pending._mergedSequences || [pending.sequence]), c.sequence]
|
|
52
|
+
};
|
|
53
|
+
stats.textMerged++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (pending) result.push(pending);
|
|
58
|
+
pending = { ...c, _mergedSequences: [c.sequence] };
|
|
59
|
+
} else {
|
|
60
|
+
if (pending) { result.push(pending); pending = null; }
|
|
61
|
+
result.push(c);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (pending) result.push(pending);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_collapseToolPairs(chunks, stats) {
|
|
69
|
+
const toolUseMap = {};
|
|
70
|
+
for (const c of chunks) {
|
|
71
|
+
if (c.block?.type === 'tool_use' && c.block.id) toolUseMap[c.block.id] = c;
|
|
72
|
+
}
|
|
73
|
+
for (const c of chunks) {
|
|
74
|
+
if (c.block?.type === 'tool_result' && c.block.tool_use_id) {
|
|
75
|
+
const match = toolUseMap[c.block.tool_use_id];
|
|
76
|
+
if (match) {
|
|
77
|
+
match.block._hasResult = true;
|
|
78
|
+
c.block._collapsed = true;
|
|
79
|
+
stats.toolsCollapsed++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_supersedeSystemBlocks(chunks, stats) {
|
|
86
|
+
const systemIndices = [];
|
|
87
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
88
|
+
if (chunks[i].block?.type === 'system') systemIndices.push(i);
|
|
89
|
+
}
|
|
90
|
+
if (systemIndices.length <= 1) return chunks;
|
|
91
|
+
const keep = new Set();
|
|
92
|
+
keep.add(systemIndices[systemIndices.length - 1]);
|
|
93
|
+
stats.systemSuperseded += systemIndices.length - 1;
|
|
94
|
+
return chunks.filter((_, i) => !systemIndices.includes(i) || keep.has(i));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = EventConsolidator;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class KalmanFilter {
|
|
2
|
+
constructor(config = {}) {
|
|
3
|
+
this._initEst = config.initialEstimate || 0;
|
|
4
|
+
this._initErr = config.initialError || 1000;
|
|
5
|
+
this._q = Math.max(config.processNoise || 1, 0.001);
|
|
6
|
+
this._r = config.measurementNoise || 10;
|
|
7
|
+
this._est = this._initEst;
|
|
8
|
+
this._err = this._initErr;
|
|
9
|
+
this._gain = 0;
|
|
10
|
+
this._initialized = false;
|
|
11
|
+
this._lastValid = this._initEst;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
update(measurement) {
|
|
15
|
+
if (!Number.isFinite(measurement)) {
|
|
16
|
+
return { estimate: this._est, error: this._err, gain: this._gain };
|
|
17
|
+
}
|
|
18
|
+
if (measurement < 0) measurement = 0;
|
|
19
|
+
if (!this._initialized) {
|
|
20
|
+
this._est = measurement;
|
|
21
|
+
this._err = this._r;
|
|
22
|
+
this._initialized = true;
|
|
23
|
+
this._lastValid = measurement;
|
|
24
|
+
this._gain = 1;
|
|
25
|
+
return { estimate: this._est, error: this._err, gain: this._gain };
|
|
26
|
+
}
|
|
27
|
+
let r = this._r;
|
|
28
|
+
if (this._est > 0 && Math.abs(measurement - this._est) > this._est * 10) {
|
|
29
|
+
r = r * 100;
|
|
30
|
+
}
|
|
31
|
+
const predErr = this._err + this._q;
|
|
32
|
+
this._gain = predErr / (predErr + r);
|
|
33
|
+
this._est = this._est + this._gain * (measurement - this._est);
|
|
34
|
+
this._err = (1 - this._gain) * predErr;
|
|
35
|
+
if (this._err < 1e-10) this._err = 1e-10;
|
|
36
|
+
this._lastValid = this._est;
|
|
37
|
+
return { estimate: this._est, error: this._err, gain: this._gain };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
predict() {
|
|
41
|
+
return this._est;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setProcessNoise(q) { this._q = Math.max(q, 0.001); }
|
|
45
|
+
setMeasurementNoise(r) { this._r = r; }
|
|
46
|
+
|
|
47
|
+
getState() {
|
|
48
|
+
return {
|
|
49
|
+
estimate: this._est,
|
|
50
|
+
error: this._err,
|
|
51
|
+
gain: this._gain,
|
|
52
|
+
processNoise: this._q,
|
|
53
|
+
measurementNoise: this._r,
|
|
54
|
+
initialized: this._initialized
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
reset() {
|
|
59
|
+
this._est = this._initEst;
|
|
60
|
+
this._err = this._initErr;
|
|
61
|
+
this._gain = 0;
|
|
62
|
+
this._initialized = false;
|
|
63
|
+
this._lastValid = this._initEst;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = KalmanFilter;
|
|
@@ -31,10 +31,18 @@ class WebSocketManager {
|
|
|
31
31
|
avg: 0,
|
|
32
32
|
jitter: 0,
|
|
33
33
|
quality: 'unknown',
|
|
34
|
+
predicted: 0,
|
|
35
|
+
predictedNext: 0,
|
|
36
|
+
trend: 'stable',
|
|
34
37
|
missedPongs: 0,
|
|
35
38
|
pingCounter: 0
|
|
36
39
|
};
|
|
37
40
|
|
|
41
|
+
this._latencyKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 1, measurementNoise: 10 }) : null;
|
|
42
|
+
this._trendHistory = [];
|
|
43
|
+
this._trendCount = 0;
|
|
44
|
+
this._reconnectedAt = 0;
|
|
45
|
+
|
|
38
46
|
this.stats = {
|
|
39
47
|
totalConnections: 0,
|
|
40
48
|
totalReconnects: 0,
|
|
@@ -106,6 +114,7 @@ class WebSocketManager {
|
|
|
106
114
|
this.isConnected = true;
|
|
107
115
|
this.isConnecting = false;
|
|
108
116
|
this.connectionEstablishedAt = Date.now();
|
|
117
|
+
this._reconnectedAt = this.stats.totalConnections > 0 ? Date.now() : 0;
|
|
109
118
|
this.stats.totalConnections++;
|
|
110
119
|
this.stats.lastConnectedTime = Date.now();
|
|
111
120
|
this.latency.missedPongs = 0;
|
|
@@ -162,21 +171,47 @@ class WebSocketManager {
|
|
|
162
171
|
if (samples.length > this.config.latencyWindowSize) samples.shift();
|
|
163
172
|
|
|
164
173
|
this.latency.current = rtt;
|
|
165
|
-
|
|
174
|
+
|
|
175
|
+
if (this._latencyKalman && samples.length > 3) {
|
|
176
|
+
if (this._reconnectedAt && Date.now() - this._reconnectedAt < 5000) {
|
|
177
|
+
this._latencyKalman.setMeasurementNoise(50);
|
|
178
|
+
} else {
|
|
179
|
+
this._latencyKalman.setMeasurementNoise(10);
|
|
180
|
+
}
|
|
181
|
+
const result = this._latencyKalman.update(rtt);
|
|
182
|
+
this.latency.predicted = result.estimate;
|
|
183
|
+
this.latency.predictedNext = this._latencyKalman.predict();
|
|
184
|
+
this.latency.avg = result.estimate;
|
|
185
|
+
} else {
|
|
186
|
+
this.latency.avg = samples.reduce((a, b) => a + b, 0) / samples.length;
|
|
187
|
+
this.latency.predicted = this.latency.avg;
|
|
188
|
+
this.latency.predictedNext = this.latency.avg;
|
|
189
|
+
}
|
|
166
190
|
|
|
167
191
|
if (samples.length > 1) {
|
|
168
|
-
const mean =
|
|
192
|
+
const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
|
|
169
193
|
const variance = samples.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / samples.length;
|
|
170
194
|
this.latency.jitter = Math.sqrt(variance);
|
|
171
195
|
}
|
|
172
196
|
|
|
173
|
-
|
|
197
|
+
this._trendHistory.push(this.latency.predicted);
|
|
198
|
+
if (this._trendHistory.length > 3) this._trendHistory.shift();
|
|
199
|
+
if (this._trendHistory.length >= 3) {
|
|
200
|
+
const [a, b, c] = this._trendHistory;
|
|
201
|
+
const rising = b > a * 1.05 && c > b * 1.05;
|
|
202
|
+
const falling = b < a * 0.95 && c < b * 0.95;
|
|
203
|
+
this.latency.trend = rising ? 'rising' : falling ? 'falling' : 'stable';
|
|
204
|
+
}
|
|
205
|
+
|
|
174
206
|
this.latency.quality = this._qualityTier(this.latency.avg);
|
|
175
207
|
this.stats.avgLatency = this.latency.avg;
|
|
176
208
|
|
|
177
209
|
this.emit('latency_update', {
|
|
178
210
|
latency: rtt,
|
|
179
211
|
avg: this.latency.avg,
|
|
212
|
+
predicted: this.latency.predicted,
|
|
213
|
+
predictedNext: this.latency.predictedNext,
|
|
214
|
+
trend: this.latency.trend,
|
|
180
215
|
jitter: this.latency.jitter,
|
|
181
216
|
quality: this.latency.quality
|
|
182
217
|
});
|
|
@@ -184,6 +219,37 @@ class WebSocketManager {
|
|
|
184
219
|
if (rtt > this.latency.avg * 3 && samples.length >= 3) {
|
|
185
220
|
this.emit('latency_spike', { latency: rtt, avg: this.latency.avg });
|
|
186
221
|
}
|
|
222
|
+
|
|
223
|
+
this.emit('latency_prediction', {
|
|
224
|
+
predicted: this.latency.predicted,
|
|
225
|
+
predictedNext: this.latency.predictedNext,
|
|
226
|
+
trend: this.latency.trend,
|
|
227
|
+
gain: this._latencyKalman ? this._latencyKalman.getState().gain : 0
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this._checkDegradation();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_checkDegradation() {
|
|
234
|
+
if (this.latency.trend === 'rising') {
|
|
235
|
+
this._trendCount = (this._trendCount || 0) + 1;
|
|
236
|
+
} else {
|
|
237
|
+
if (this._trendCount >= 5 && (this.latency.trend === 'stable' || this.latency.trend === 'falling')) {
|
|
238
|
+
this.emit('connection_recovering', { currentTier: this.latency.quality });
|
|
239
|
+
}
|
|
240
|
+
this._trendCount = 0;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (this._trendCount < 5) return;
|
|
244
|
+
const currentTier = this.latency.quality;
|
|
245
|
+
const predictedTier = this._qualityTier(this.latency.predictedNext);
|
|
246
|
+
if (predictedTier === currentTier) return;
|
|
247
|
+
const thresholds = { excellent: 50, good: 150, fair: 300, poor: 500 };
|
|
248
|
+
const threshold = thresholds[currentTier];
|
|
249
|
+
if (!threshold) return;
|
|
250
|
+
const rate = this._trendHistory.length >= 2 ? this._trendHistory[this._trendHistory.length - 1] - this._trendHistory[0] : 0;
|
|
251
|
+
const timeToChange = rate > 0 ? Math.round((threshold - this.latency.predicted) / rate * 1000) : Infinity;
|
|
252
|
+
this.emit('connection_degrading', { currentTier, predictedTier, predictedLatency: this.latency.predictedNext, timeToChange });
|
|
187
253
|
}
|
|
188
254
|
|
|
189
255
|
_qualityTier(avg) {
|
|
@@ -283,13 +349,24 @@ class WebSocketManager {
|
|
|
283
349
|
type: 'latency_report',
|
|
284
350
|
avg: Math.round(this.latency.avg),
|
|
285
351
|
jitter: Math.round(this.latency.jitter),
|
|
286
|
-
quality: this.latency.quality
|
|
352
|
+
quality: this.latency.quality,
|
|
353
|
+
trend: this.latency.trend,
|
|
354
|
+
predictedNext: Math.round(this.latency.predictedNext)
|
|
287
355
|
});
|
|
288
356
|
}
|
|
289
357
|
}
|
|
290
358
|
|
|
291
359
|
_handleVisibilityChange() {
|
|
292
|
-
if (typeof document !== 'undefined' && document.hidden)
|
|
360
|
+
if (typeof document !== 'undefined' && document.hidden) {
|
|
361
|
+
this._hiddenAt = Date.now();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (this._hiddenAt && this._latencyKalman && Date.now() - this._hiddenAt > 30000) {
|
|
365
|
+
this._latencyKalman.reset();
|
|
366
|
+
this._trendHistory = [];
|
|
367
|
+
this.latency.trend = 'stable';
|
|
368
|
+
}
|
|
369
|
+
this._hiddenAt = 0;
|
|
293
370
|
if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
|
|
294
371
|
if (this.reconnectTimer) {
|
|
295
372
|
clearTimeout(this.reconnectTimer);
|