agentgui 1.0.141 → 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.141",
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
  }
@@ -938,40 +938,29 @@ class AgentGUIClient {
938
938
  if (!conversationId) return;
939
939
 
940
940
  const pollState = this.chunkPollState;
941
- if (pollState.isPolling) return; // Already polling
941
+ if (pollState.isPolling) return;
942
942
 
943
943
  pollState.isPolling = true;
944
944
  pollState.lastFetchTimestamp = Date.now();
945
- pollState.backoffDelay = 100;
946
-
947
- console.log('Starting chunk polling for conversation:', conversationId);
945
+ pollState.backoffDelay = 150;
946
+ pollState.sessionCheckCounter = 0;
948
947
 
949
948
  const pollOnce = async () => {
950
949
  if (!pollState.isPolling) return;
951
950
 
952
951
  try {
953
- // Check session status periodically
954
- if (this.state.currentSession?.id) {
952
+ pollState.sessionCheckCounter++;
953
+ if (pollState.sessionCheckCounter % 10 === 0 && this.state.currentSession?.id) {
955
954
  const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
956
955
  if (sessionResponse.ok) {
957
956
  const { session } = await sessionResponse.json();
958
957
  if (session && (session.status === 'complete' || session.status === 'error')) {
959
- // Session has finished, trigger appropriate handler
960
958
  if (session.status === 'complete') {
961
- this.handleStreamingComplete({
962
- sessionId: session.id,
963
- conversationId: conversationId,
964
- timestamp: Date.now()
965
- });
959
+ this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
966
960
  } else {
967
- this.handleStreamingError({
968
- sessionId: session.id,
969
- conversationId: conversationId,
970
- error: session.error || 'Unknown error',
971
- timestamp: Date.now()
972
- });
961
+ this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
973
962
  }
974
- return; // Stop polling
963
+ return;
975
964
  }
976
965
  }
977
966
  }
@@ -979,42 +968,28 @@ class AgentGUIClient {
979
968
  const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
980
969
 
981
970
  if (chunks.length > 0) {
982
- // Reset backoff on success
983
- pollState.backoffDelay = 100;
984
-
985
- // Update last fetch timestamp
971
+ pollState.backoffDelay = 150;
986
972
  const lastChunk = chunks[chunks.length - 1];
987
973
  pollState.lastFetchTimestamp = lastChunk.created_at;
988
-
989
- // Render new chunks
990
974
  chunks.forEach(chunk => {
991
- if (chunk.block && chunk.block.type) {
992
- this.renderChunk(chunk);
993
- }
975
+ if (chunk.block && chunk.block.type) this.renderChunk(chunk);
994
976
  });
977
+ } else {
978
+ pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
995
979
  }
996
980
 
997
- // Schedule next poll
998
981
  if (pollState.isPolling) {
999
- pollState.pollTimer = setTimeout(pollOnce, 100);
982
+ pollState.pollTimer = setTimeout(pollOnce, pollState.backoffDelay);
1000
983
  }
1001
984
  } catch (error) {
1002
- console.warn('Chunk poll error, applying backoff:', error.message);
1003
-
1004
- // Apply exponential backoff
1005
- pollState.backoffDelay = Math.min(
1006
- pollState.backoffDelay * 2,
1007
- pollState.maxBackoffDelay
1008
- );
1009
-
1010
- // Schedule next poll with backoff
985
+ console.warn('Chunk poll error:', error.message);
986
+ pollState.backoffDelay = Math.min(pollState.backoffDelay * 2, pollState.maxBackoffDelay);
1011
987
  if (pollState.isPolling) {
1012
988
  pollState.pollTimer = setTimeout(pollOnce, pollState.backoffDelay);
1013
989
  }
1014
990
  }
1015
991
  };
1016
992
 
1017
- // Start polling loop
1018
993
  pollOnce();
1019
994
  }
1020
995
 
@@ -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