agentgui 1.0.140 → 1.0.142

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.140",
3
+ "version": "1.0.142",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -40,6 +40,24 @@ const expressApp = express();
40
40
  // Separate Express app for webtalk (STT/TTS) - isolated to contain COEP/COOP headers
41
41
  const webtalkApp = express();
42
42
  const webtalkInstance = webtalk(webtalkApp, { path: '/webtalk' });
43
+
44
+ const webtalkSdkDir = path.dirname(require.resolve('webtalk'));
45
+ const WASM_MIN_BYTES = 1000000;
46
+ const webtalkCriticalFiles = [
47
+ { path: path.join(webtalkSdkDir, 'assets', 'ort-wasm-simd-threaded.jsep.wasm'), minBytes: WASM_MIN_BYTES }
48
+ ];
49
+ for (const file of webtalkCriticalFiles) {
50
+ try {
51
+ if (fs.existsSync(file.path)) {
52
+ const stat = fs.statSync(file.path);
53
+ if (stat.size < file.minBytes) {
54
+ debugLog(`Removing corrupt file ${path.basename(file.path)} (${stat.size} bytes, need ${file.minBytes}+)`);
55
+ fs.unlinkSync(file.path);
56
+ }
57
+ }
58
+ } catch (e) { debugLog(`File check error: ${e.message}`); }
59
+ }
60
+
43
61
  webtalkInstance.init().catch(err => debugLog('Webtalk init: ' + err.message));
44
62
 
45
63
  // File upload endpoint - copies dropped files to conversation workingDirectory
@@ -164,7 +182,7 @@ const server = http.createServer(async (req, res) => {
164
182
  pathOnly.startsWith('/tts/') ||
165
183
  pathOnly.startsWith('/models/');
166
184
  if (isWebtalkRoute) {
167
- const webtalkSdkDir = path.dirname(require.resolve('webtalk/package.json'));
185
+ const webtalkSdkDir = path.dirname(require.resolve('webtalk'));
168
186
  const sdkFiles = { '/demo': 'app.html', '/sdk.js': 'sdk.js', '/stt.js': 'stt.js', '/tts.js': 'tts.js', '/tts-utils.js': 'tts-utils.js' };
169
187
  let stripped = pathOnly.startsWith(webtalkPrefix) ? pathOnly.slice(webtalkPrefix.length) : (pathOnly.startsWith('/webtalk') ? pathOnly.slice('/webtalk'.length) : null);
170
188
  if (stripped !== null && !sdkFiles[stripped] && !stripped.endsWith('.js') && sdkFiles[stripped + '.js']) stripped += '.js';
@@ -193,6 +211,10 @@ const server = http.createServer(async (req, res) => {
193
211
  });
194
212
  }
195
213
  if (req.url.startsWith(BASE_URL)) req.url = req.url.slice(BASE_URL.length) || '/';
214
+ const isModelOrAsset = pathOnly.includes('/models/') || pathOnly.includes('/assets/') || pathOnly.endsWith('.wasm') || pathOnly.endsWith('.onnx');
215
+ if (isModelOrAsset) {
216
+ res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
217
+ }
196
218
  const origSetHeader = res.setHeader.bind(res);
197
219
  res.setHeader = (name, value) => {
198
220
  if (name.toLowerCase() === 'cross-origin-embedder-policy') return;
@@ -569,46 +591,50 @@ const server = http.createServer(async (req, res) => {
569
591
  }
570
592
  });
571
593
 
594
+ const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.wasm': 'application/wasm', '.onnx': 'application/octet-stream' };
595
+
572
596
  function serveFile(filePath, res) {
573
597
  const ext = path.extname(filePath).toLowerCase();
574
- const mimeTypes = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' };
598
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
599
+
600
+ if (ext !== '.html') {
601
+ fs.stat(filePath, (err, stats) => {
602
+ if (err) { res.writeHead(500); res.end('Server error'); return; }
603
+ res.writeHead(200, {
604
+ 'Content-Type': contentType,
605
+ 'Content-Length': stats.size,
606
+ 'Cache-Control': 'public, max-age=3600'
607
+ });
608
+ fs.createReadStream(filePath).pipe(res);
609
+ });
610
+ return;
611
+ }
612
+
575
613
  fs.readFile(filePath, (err, data) => {
576
614
  if (err) { res.writeHead(500); res.end('Server error'); return; }
577
615
  let content = data.toString();
578
- if (ext === '.html') {
579
- const baseTag = `<script>window.__BASE_URL='${BASE_URL}';</script>\n <script type="importmap">{"imports":{"webtalk-sdk":"${BASE_URL}/webtalk/sdk.js"}}</script>`;
580
- content = content.replace('<head>', '<head>\n ' + baseTag);
581
- if (watch) {
582
- content += `\n<script>(function(){const ws=new WebSocket('ws://'+location.host+'${BASE_URL}/hot-reload');ws.onmessage=e=>{if(JSON.parse(e.data).type==='reload')location.reload()};})();</script>`;
583
- }
616
+ const baseTag = `<script>window.__BASE_URL='${BASE_URL}';</script>\n <script type="importmap">{"imports":{"webtalk-sdk":"${BASE_URL}/webtalk/sdk.js"}}</script>`;
617
+ content = content.replace('<head>', '<head>\n ' + baseTag);
618
+ if (watch) {
619
+ content += `\n<script>(function(){const ws=new WebSocket('ws://'+location.host+'${BASE_URL}/hot-reload');ws.onmessage=e=>{if(JSON.parse(e.data).type==='reload')location.reload()};})();</script>`;
584
620
  }
585
- res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' });
621
+ res.writeHead(200, { 'Content-Type': contentType });
586
622
  res.end(content);
587
623
  });
588
624
  }
589
625
 
590
626
  function persistChunkWithRetry(sessionId, conversationId, sequence, blockType, blockData, maxRetries = 3) {
591
- let lastError = null;
592
- const backoffs = [100, 200, 400];
593
-
594
627
  for (let attempt = 0; attempt < maxRetries; attempt++) {
595
628
  try {
596
- const chunk = queries.createChunk(sessionId, conversationId, sequence, blockType, blockData);
597
- return chunk;
629
+ return queries.createChunk(sessionId, conversationId, sequence, blockType, blockData);
598
630
  } catch (err) {
599
- lastError = err;
600
631
  debugLog(`[chunk] Persist attempt ${attempt + 1}/${maxRetries} failed: ${err.message}`);
601
- if (attempt < maxRetries - 1) {
602
- const delayMs = backoffs[attempt] || 400;
603
- const endTime = Date.now() + delayMs;
604
- while (Date.now() < endTime) {
605
- // Synchronous sleep for backoff
606
- }
632
+ if (attempt >= maxRetries - 1) {
633
+ debugLog(`[chunk] Failed to persist after ${maxRetries} retries: ${err.message}`);
634
+ return null;
607
635
  }
608
636
  }
609
637
  }
610
-
611
- debugLog(`[chunk] Failed to persist after ${maxRetries} retries: ${lastError?.message}`);
612
638
  return null;
613
639
  }
614
640
 
@@ -941,23 +967,22 @@ wss.on('connection', (ws, req) => {
941
967
  }
942
968
  });
943
969
 
970
+ const BROADCAST_TYPES = new Set([
971
+ 'message_created', 'conversation_created', 'conversations_updated',
972
+ 'conversation_deleted', 'queue_status', 'streaming_start',
973
+ 'streaming_complete', 'streaming_error'
974
+ ]);
975
+
944
976
  function broadcastSync(event) {
977
+ if (syncClients.size === 0) return;
945
978
  const data = JSON.stringify(event);
979
+ const isBroadcast = BROADCAST_TYPES.has(event.type);
946
980
 
947
981
  for (const ws of syncClients) {
948
982
  if (ws.readyState !== 1) continue;
949
-
950
- let shouldSend = false;
951
-
952
- if (event.sessionId && ws.subscriptions?.has(event.sessionId)) {
953
- shouldSend = true;
954
- } else if (event.conversationId && ws.subscriptions?.has(`conv-${event.conversationId}`)) {
955
- shouldSend = true;
956
- } else if (event.type === 'message_created' || event.type === 'conversation_created' || event.type === 'conversations_updated' || event.type === 'conversation_deleted' || event.type === 'queue_status' || event.type === 'streaming_start' || event.type === 'streaming_complete' || event.type === 'streaming_error') {
957
- shouldSend = true;
958
- }
959
-
960
- if (shouldSend) {
983
+ if (isBroadcast ||
984
+ (event.sessionId && ws.subscriptions?.has(event.sessionId)) ||
985
+ (event.conversationId && ws.subscriptions?.has(`conv-${event.conversationId}`))) {
961
986
  ws.send(data);
962
987
  }
963
988
  }
@@ -348,7 +348,7 @@ class AgentGUIClient {
348
348
 
349
349
  switch (data.type) {
350
350
  case 'streaming_start':
351
- this.handleStreamingStart(data);
351
+ this.handleStreamingStart(data).catch(e => console.error('handleStreamingStart error:', e));
352
352
  break;
353
353
  case 'streaming_progress':
354
354
  this.handleStreamingProgress(data);
@@ -388,7 +388,7 @@ class AgentGUIClient {
388
388
  }
389
389
  }
390
390
 
391
- handleStreamingStart(data) {
391
+ async handleStreamingStart(data) {
392
392
  console.log('Streaming started:', data);
393
393
 
394
394
  // If this streaming event is for a different conversation than what we are viewing,
@@ -420,8 +420,60 @@ class AgentGUIClient {
420
420
  if (outputEl) {
421
421
  let messagesEl = outputEl.querySelector('.conversation-messages');
422
422
  if (!messagesEl) {
423
- outputEl.innerHTML = '<div class="conversation-messages"></div>';
423
+ // Load existing conversation history before starting the stream
424
+ const conv = this.state.currentConversation;
425
+ const wdInfo = conv?.workingDirectory ? ` - ${this.escapeHtml(conv.workingDirectory)}` : '';
426
+ outputEl.innerHTML = `
427
+ <div class="conversation-header">
428
+ <h2>${this.escapeHtml(conv?.title || 'Conversation')}</h2>
429
+ <p class="text-secondary">${conv?.agentType || 'unknown'} - ${new Date(conv?.created_at || Date.now()).toLocaleDateString()}${wdInfo}</p>
430
+ </div>
431
+ <div class="conversation-messages"></div>
432
+ `;
424
433
  messagesEl = outputEl.querySelector('.conversation-messages');
434
+ // Load prior messages into the container
435
+ try {
436
+ const msgResp = await fetch(window.__BASE_URL + `/api/conversations/${data.conversationId}/messages`);
437
+ if (msgResp.ok) {
438
+ const msgData = await msgResp.json();
439
+ const priorChunks = await this.fetchChunks(data.conversationId, 0);
440
+ if (priorChunks.length > 0) {
441
+ const userMsgs = (msgData.messages || []).filter(m => m.role === 'user');
442
+ const sessionOrder = [];
443
+ const sessionGroups = {};
444
+ priorChunks.forEach(c => {
445
+ if (!sessionGroups[c.sessionId]) { sessionGroups[c.sessionId] = []; sessionOrder.push(c.sessionId); }
446
+ sessionGroups[c.sessionId].push(c);
447
+ });
448
+ let ui = 0;
449
+ sessionOrder.forEach(sid => {
450
+ const sList = sessionGroups[sid];
451
+ const sStart = sList[0].created_at;
452
+ while (ui < userMsgs.length && userMsgs[ui].created_at <= sStart) {
453
+ const m = userMsgs[ui++];
454
+ messagesEl.insertAdjacentHTML('beforeend', `<div class="message message-user" data-msg-id="${m.id}"><div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div></div>`);
455
+ }
456
+ const mDiv = document.createElement('div');
457
+ mDiv.className = 'message message-assistant';
458
+ mDiv.id = `message-${sid}`;
459
+ mDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
460
+ const bEl = mDiv.querySelector('.message-blocks');
461
+ sList.forEach(chunk => { if (chunk.block?.type) { const el = this.renderer.renderBlock(chunk.block, chunk); if (el) bEl.appendChild(el); } });
462
+ const ts = document.createElement('div'); ts.className = 'message-timestamp'; ts.textContent = new Date(sList[sList.length - 1].created_at).toLocaleString();
463
+ mDiv.appendChild(ts);
464
+ messagesEl.appendChild(mDiv);
465
+ });
466
+ while (ui < userMsgs.length) {
467
+ const m = userMsgs[ui++];
468
+ messagesEl.insertAdjacentHTML('beforeend', `<div class="message message-user" data-msg-id="${m.id}"><div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div></div>`);
469
+ }
470
+ } else {
471
+ messagesEl.innerHTML = this.renderMessages(msgData.messages || []);
472
+ }
473
+ }
474
+ } catch (e) {
475
+ console.warn('Failed to load prior messages for streaming view:', e);
476
+ }
425
477
  }
426
478
  const streamingDiv = document.createElement('div');
427
479
  streamingDiv.className = 'message message-assistant streaming-message';
@@ -886,40 +938,29 @@ class AgentGUIClient {
886
938
  if (!conversationId) return;
887
939
 
888
940
  const pollState = this.chunkPollState;
889
- if (pollState.isPolling) return; // Already polling
941
+ if (pollState.isPolling) return;
890
942
 
891
943
  pollState.isPolling = true;
892
944
  pollState.lastFetchTimestamp = Date.now();
893
- pollState.backoffDelay = 100;
894
-
895
- console.log('Starting chunk polling for conversation:', conversationId);
945
+ pollState.backoffDelay = 150;
946
+ pollState.sessionCheckCounter = 0;
896
947
 
897
948
  const pollOnce = async () => {
898
949
  if (!pollState.isPolling) return;
899
950
 
900
951
  try {
901
- // Check session status periodically
902
- if (this.state.currentSession?.id) {
952
+ pollState.sessionCheckCounter++;
953
+ if (pollState.sessionCheckCounter % 10 === 0 && this.state.currentSession?.id) {
903
954
  const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
904
955
  if (sessionResponse.ok) {
905
956
  const { session } = await sessionResponse.json();
906
957
  if (session && (session.status === 'complete' || session.status === 'error')) {
907
- // Session has finished, trigger appropriate handler
908
958
  if (session.status === 'complete') {
909
- this.handleStreamingComplete({
910
- sessionId: session.id,
911
- conversationId: conversationId,
912
- timestamp: Date.now()
913
- });
959
+ this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
914
960
  } else {
915
- this.handleStreamingError({
916
- sessionId: session.id,
917
- conversationId: conversationId,
918
- error: session.error || 'Unknown error',
919
- timestamp: Date.now()
920
- });
961
+ this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
921
962
  }
922
- return; // Stop polling
963
+ return;
923
964
  }
924
965
  }
925
966
  }
@@ -927,42 +968,28 @@ class AgentGUIClient {
927
968
  const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
928
969
 
929
970
  if (chunks.length > 0) {
930
- // Reset backoff on success
931
- pollState.backoffDelay = 100;
932
-
933
- // Update last fetch timestamp
971
+ pollState.backoffDelay = 150;
934
972
  const lastChunk = chunks[chunks.length - 1];
935
973
  pollState.lastFetchTimestamp = lastChunk.created_at;
936
-
937
- // Render new chunks
938
974
  chunks.forEach(chunk => {
939
- if (chunk.block && chunk.block.type) {
940
- this.renderChunk(chunk);
941
- }
975
+ if (chunk.block && chunk.block.type) this.renderChunk(chunk);
942
976
  });
977
+ } else {
978
+ pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
943
979
  }
944
980
 
945
- // Schedule next poll
946
981
  if (pollState.isPolling) {
947
- pollState.pollTimer = setTimeout(pollOnce, 100);
982
+ pollState.pollTimer = setTimeout(pollOnce, pollState.backoffDelay);
948
983
  }
949
984
  } catch (error) {
950
- console.warn('Chunk poll error, applying backoff:', error.message);
951
-
952
- // Apply exponential backoff
953
- pollState.backoffDelay = Math.min(
954
- pollState.backoffDelay * 2,
955
- pollState.maxBackoffDelay
956
- );
957
-
958
- // Schedule next poll with backoff
985
+ console.warn('Chunk poll error:', error.message);
986
+ pollState.backoffDelay = Math.min(pollState.backoffDelay * 2, pollState.maxBackoffDelay);
959
987
  if (pollState.isPolling) {
960
988
  pollState.pollTimer = setTimeout(pollOnce, pollState.backoffDelay);
961
989
  }
962
990
  }
963
991
  };
964
992
 
965
- // Start polling loop
966
993
  pollOnce();
967
994
  }
968
995
 
@@ -1181,17 +1208,48 @@ class AgentGUIClient {
1181
1208
  // Render all chunks
1182
1209
  const messagesEl = outputEl.querySelector('.conversation-messages');
1183
1210
  if (chunks.length > 0) {
1184
- // Group chunks by session
1211
+ // Fetch user messages to interleave with session chunks
1212
+ let userMessages = [];
1213
+ try {
1214
+ const msgResp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/messages`);
1215
+ if (msgResp.ok) {
1216
+ const msgData = await msgResp.json();
1217
+ userMessages = (msgData.messages || []).filter(m => m.role === 'user');
1218
+ }
1219
+ } catch (_) {}
1220
+
1221
+ // Group chunks by session, preserving order
1222
+ const sessionOrder = [];
1185
1223
  const sessionChunks = {};
1186
1224
  chunks.forEach(chunk => {
1187
1225
  if (!sessionChunks[chunk.sessionId]) {
1188
1226
  sessionChunks[chunk.sessionId] = [];
1227
+ sessionOrder.push(chunk.sessionId);
1189
1228
  }
1190
1229
  sessionChunks[chunk.sessionId].push(chunk);
1191
1230
  });
1192
1231
 
1193
- // Render each session's chunks
1194
- Object.entries(sessionChunks).forEach(([sessionId, sessionChunkList]) => {
1232
+ // Build a timeline: match user messages to sessions by timestamp
1233
+ let userMsgIdx = 0;
1234
+ sessionOrder.forEach((sessionId) => {
1235
+ const sessionChunkList = sessionChunks[sessionId];
1236
+ const sessionStart = sessionChunkList[0].created_at;
1237
+
1238
+ // Render user messages that came before this session
1239
+ while (userMsgIdx < userMessages.length && userMessages[userMsgIdx].created_at <= sessionStart) {
1240
+ const msg = userMessages[userMsgIdx];
1241
+ const userDiv = document.createElement('div');
1242
+ userDiv.className = 'message message-user';
1243
+ userDiv.setAttribute('data-msg-id', msg.id);
1244
+ userDiv.innerHTML = `
1245
+ <div class="message-role">User</div>
1246
+ ${this.renderMessageContent(msg.content)}
1247
+ <div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
1248
+ `;
1249
+ messagesEl.appendChild(userDiv);
1250
+ userMsgIdx++;
1251
+ }
1252
+
1195
1253
  const isCurrentActiveSession = shouldResumeStreaming && latestSession && latestSession.id === sessionId;
1196
1254
  const messageDiv = document.createElement('div');
1197
1255
  messageDiv.className = `message message-assistant${isCurrentActiveSession ? ' streaming-message' : ''}`;
@@ -1208,7 +1266,6 @@ class AgentGUIClient {
1208
1266
  }
1209
1267
  });
1210
1268
 
1211
- // Add streaming indicator for active session
1212
1269
  if (isCurrentActiveSession) {
1213
1270
  const indicatorDiv = document.createElement('div');
1214
1271
  indicatorDiv.className = 'streaming-indicator';
@@ -1227,6 +1284,21 @@ class AgentGUIClient {
1227
1284
 
1228
1285
  messagesEl.appendChild(messageDiv);
1229
1286
  });
1287
+
1288
+ // Render any remaining user messages after the last session
1289
+ while (userMsgIdx < userMessages.length) {
1290
+ const msg = userMessages[userMsgIdx];
1291
+ const userDiv = document.createElement('div');
1292
+ userDiv.className = 'message message-user';
1293
+ userDiv.setAttribute('data-msg-id', msg.id);
1294
+ userDiv.innerHTML = `
1295
+ <div class="message-role">User</div>
1296
+ ${this.renderMessageContent(msg.content)}
1297
+ <div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
1298
+ `;
1299
+ messagesEl.appendChild(userDiv);
1300
+ userMsgIdx++;
1301
+ }
1230
1302
  } else {
1231
1303
  // Fall back to messages if no chunks
1232
1304
  const messagesResponse = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/messages`);
@@ -127,32 +127,40 @@
127
127
  }
128
128
  }
129
129
 
130
- async function initTTS() {
131
- try {
132
- tts = new TTS({
133
- basePath: BASE + '/webtalk',
134
- apiBasePath: BASE,
135
- onStatus: function() {},
136
- onAudioReady: function(url) {
137
- var audio = new Audio(url);
138
- audio.onended = function() {
139
- isSpeaking = false;
140
- processQueue();
141
- };
142
- audio.onerror = function() {
143
- isSpeaking = false;
144
- processQueue();
145
- };
146
- audio.play().catch(function() {
147
- isSpeaking = false;
148
- processQueue();
149
- });
130
+ async function initTTS(retries) {
131
+ var maxRetries = retries || 3;
132
+ for (var attempt = 0; attempt < maxRetries; attempt++) {
133
+ try {
134
+ tts = new TTS({
135
+ basePath: BASE + '/webtalk',
136
+ apiBasePath: BASE,
137
+ onStatus: function() {},
138
+ onAudioReady: function(url) {
139
+ var audio = new Audio(url);
140
+ audio.onended = function() {
141
+ isSpeaking = false;
142
+ processQueue();
143
+ };
144
+ audio.onerror = function() {
145
+ isSpeaking = false;
146
+ processQueue();
147
+ };
148
+ audio.play().catch(function() {
149
+ isSpeaking = false;
150
+ processQueue();
151
+ });
152
+ }
153
+ });
154
+ await tts.init();
155
+ ttsReady = true;
156
+ return;
157
+ } catch (e) {
158
+ console.warn('TTS init attempt ' + (attempt + 1) + '/' + maxRetries + ' failed:', e.message);
159
+ tts = null;
160
+ if (attempt < maxRetries - 1) {
161
+ await new Promise(function(r) { setTimeout(r, 3000 * (attempt + 1)); });
150
162
  }
151
- });
152
- await tts.init();
153
- ttsReady = true;
154
- } catch (e) {
155
- console.warn('TTS init failed:', e.message);
163
+ }
156
164
  }
157
165
  }
158
166