agentgui 1.0.393 → 1.0.395

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.
Files changed (3) hide show
  1. package/lib/ws-optimizer.js +30 -110
  2. package/package.json +1 -1
  3. package/.prd +0 -215
@@ -1,6 +1,3 @@
1
- // WebSocket Optimization Module
2
- // Implements batching, rate limiting, compression, deduplication, priority queuing, and monitoring
3
-
4
1
  import zlib from 'zlib';
5
2
 
6
3
  const MESSAGE_PRIORITY = {
@@ -13,7 +10,20 @@ function getPriority(eventType) {
13
10
  if (MESSAGE_PRIORITY.high.includes(eventType)) return 3;
14
11
  if (MESSAGE_PRIORITY.normal.includes(eventType)) return 2;
15
12
  if (MESSAGE_PRIORITY.low.includes(eventType)) return 1;
16
- return 2; // default to normal
13
+ return 2;
14
+ }
15
+
16
+ function getBatchInterval(ws) {
17
+ const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
18
+ const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
19
+ const tier = ws.latencyTier || 'good';
20
+ const trend = ws.latencyTrend;
21
+ if (trend === 'rising' || trend === 'falling') {
22
+ const idx = TIER_ORDER.indexOf(tier);
23
+ if (trend === 'rising' && idx < TIER_ORDER.length - 1) return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
24
+ if (trend === 'falling' && idx > 0) return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
25
+ }
26
+ return BATCH_BY_TIER[tier] || 32;
17
27
  }
18
28
 
19
29
  class ClientQueue {
@@ -31,151 +41,76 @@ class ClientQueue {
31
41
  }
32
42
 
33
43
  add(data, priority) {
34
- // Deduplication: skip if identical to last message
35
44
  if (this.lastMessage === data) return;
36
45
  this.lastMessage = data;
37
-
38
- if (priority === 3) {
39
- this.highPriority.push(data);
40
- } else if (priority === 2) {
41
- this.normalPriority.push(data);
42
- } else {
43
- this.lowPriority.push(data);
44
- }
45
-
46
- // High priority: flush immediately
47
- if (priority === 3) {
48
- this.flushImmediate();
49
- } else if (!this.timer) {
50
- this.scheduleFlush();
51
- }
46
+ if (priority === 3) this.highPriority.push(data);
47
+ else if (priority === 2) this.normalPriority.push(data);
48
+ else this.lowPriority.push(data);
49
+ if (priority === 3) this.flushImmediate();
50
+ else if (!this.timer) this.scheduleFlush();
52
51
  }
53
52
 
54
53
  scheduleFlush() {
55
54
  const interval = this.ws.latencyTier ? getBatchInterval(this.ws) : 100;
56
- this.timer = setTimeout(() => {
57
- this.timer = null;
58
- this.flush();
59
- }, interval);
55
+ this.timer = setTimeout(() => { this.timer = null; this.flush(); }, interval);
60
56
  }
61
57
 
62
58
  flushImmediate() {
63
- if (this.timer) {
64
- clearTimeout(this.timer);
65
- this.timer = null;
66
- }
59
+ if (this.timer) { clearTimeout(this.timer); this.timer = null; }
67
60
  this.flush();
68
61
  }
69
62
 
70
63
  flush() {
71
64
  if (this.ws.readyState !== 1) return;
72
-
73
65
  const now = Date.now();
74
66
  const windowDuration = now - this.windowStart;
75
-
76
- // Reset rate limit window every second
77
67
  if (windowDuration >= 1000) {
78
68
  this.messageCount = 0;
79
69
  this.bytesSent = 0;
80
70
  this.windowStart = now;
81
71
  this.rateLimitWarned = false;
82
72
  }
83
-
84
- // Collect messages from all priorities (high first)
85
- const batch = [
86
- ...this.highPriority.splice(0),
87
- ...this.normalPriority.splice(0, 10),
88
- ...this.lowPriority.splice(0, 5)
89
- ];
90
-
73
+ const batch = [...this.highPriority.splice(0), ...this.normalPriority.splice(0, 10), ...this.lowPriority.splice(0, 5)];
91
74
  if (batch.length === 0) return;
92
-
93
- // Rate limiting: max 100 msg/sec per client
94
75
  const messagesThisSecond = this.messageCount + batch.length;
95
76
  if (messagesThisSecond > 100) {
96
77
  if (!this.rateLimitWarned) {
97
78
  console.warn(`[ws-optimizer] Client ${this.ws.clientId} rate limited: ${messagesThisSecond} msg/sec`);
98
79
  this.rateLimitWarned = true;
99
80
  }
100
- // Keep high priority, drop some normal/low
101
81
  const allowedCount = 100 - this.messageCount;
102
- if (allowedCount <= 0) {
103
- // Reschedule remaining
104
- this.scheduleFlush();
105
- return;
106
- }
82
+ if (allowedCount <= 0) { this.scheduleFlush(); return; }
107
83
  batch.splice(allowedCount);
108
84
  }
109
-
110
- let payload;
111
- if (batch.length === 1) {
112
- payload = batch[0];
113
- } else {
114
- payload = '[' + batch.join(',') + ']';
115
- }
116
-
117
- // Compression for large payloads (>1KB)
85
+ let payload = batch.length === 1 ? batch[0] : '[' + batch.join(',') + ']';
118
86
  if (payload.length > 1024) {
119
87
  try {
120
88
  const compressed = zlib.gzipSync(Buffer.from(payload), { level: 6 });
121
89
  if (compressed.length < payload.length * 0.9) {
122
- // Send compression hint as separate control message
123
90
  this.ws.send(JSON.stringify({ type: '_compressed', encoding: 'gzip' }));
124
91
  this.ws.send(compressed);
125
- payload = null; // Already sent
92
+ payload = null;
126
93
  }
127
- } catch (e) {
128
- // Fall back to uncompressed
129
- }
94
+ } catch (e) {}
130
95
  }
131
-
132
- if (payload) {
133
- this.ws.send(payload);
134
- }
135
-
96
+ if (payload) this.ws.send(payload);
136
97
  this.messageCount += batch.length;
137
98
  this.bytesSent += (payload ? payload.length : 0);
138
-
139
- // Monitor: warn if >1MB/sec sustained for 3+ seconds
140
99
  if (windowDuration >= 3000 && this.bytesSent > 3 * 1024 * 1024) {
141
100
  const mbps = (this.bytesSent / windowDuration * 1000 / 1024 / 1024).toFixed(2);
142
101
  console.warn(`[ws-optimizer] Client ${this.ws.clientId} high bandwidth: ${mbps} MB/sec`);
143
102
  }
144
-
145
- // If there are remaining low-priority messages, schedule next flush
146
103
  if (this.normalPriority.length > 0 || this.lowPriority.length > 0) {
147
104
  if (!this.timer) this.scheduleFlush();
148
105
  }
149
106
  }
150
107
 
151
108
  drain() {
152
- if (this.timer) {
153
- clearTimeout(this.timer);
154
- this.timer = null;
155
- }
109
+ if (this.timer) { clearTimeout(this.timer); this.timer = null; }
156
110
  this.flush();
157
111
  }
158
112
  }
159
113
 
160
- function getBatchInterval(ws) {
161
- const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
162
- const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
163
- const tier = ws.latencyTier || 'good';
164
- const trend = ws.latencyTrend;
165
-
166
- if (trend === 'rising' || trend === 'falling') {
167
- const idx = TIER_ORDER.indexOf(tier);
168
- if (trend === 'rising' && idx < TIER_ORDER.length - 1) {
169
- return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
170
- }
171
- if (trend === 'falling' && idx > 0) {
172
- return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
173
- }
174
- }
175
-
176
- return BATCH_BY_TIER[tier] || 32;
177
- }
178
-
179
114
  class WSOptimizer {
180
115
  constructor() {
181
116
  this.clientQueues = new Map();
@@ -183,16 +118,13 @@ class WSOptimizer {
183
118
 
184
119
  sendToClient(ws, event) {
185
120
  if (ws.readyState !== 1) return;
186
-
187
121
  let queue = this.clientQueues.get(ws);
188
122
  if (!queue) {
189
123
  queue = new ClientQueue(ws);
190
124
  this.clientQueues.set(ws, queue);
191
125
  }
192
-
193
126
  const data = typeof event === 'string' ? event : JSON.stringify(event);
194
127
  const priority = typeof event === 'object' ? getPriority(event.type) : 2;
195
-
196
128
  queue.add(data, priority);
197
129
  }
198
130
 
@@ -205,30 +137,18 @@ class WSOptimizer {
205
137
  }
206
138
 
207
139
  getStats() {
208
- const stats = {
209
- clients: this.clientQueues.size,
210
- totalBytes: 0,
211
- totalMessages: 0,
212
- highBandwidthClients: []
213
- };
214
-
140
+ const stats = { clients: this.clientQueues.size, totalBytes: 0, totalMessages: 0, highBandwidthClients: [] };
215
141
  for (const [ws, queue] of this.clientQueues.entries()) {
216
142
  stats.totalBytes += queue.bytesSent;
217
143
  stats.totalMessages += queue.messageCount;
218
-
219
144
  const windowDuration = Date.now() - queue.windowStart;
220
145
  if (windowDuration > 0) {
221
146
  const mbps = (queue.bytesSent / windowDuration * 1000 / 1024 / 1024);
222
147
  if (mbps > 1) {
223
- stats.highBandwidthClients.push({
224
- clientId: ws.clientId,
225
- mbps: mbps.toFixed(2),
226
- messages: queue.messageCount
227
- });
148
+ stats.highBandwidthClients.push({ clientId: ws.clientId, mbps: mbps.toFixed(2), messages: queue.messageCount });
228
149
  }
229
150
  }
230
151
  }
231
-
232
152
  return stats;
233
153
  }
234
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.393",
3
+ "version": "1.0.395",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/.prd DELETED
@@ -1,215 +0,0 @@
1
- # AgentGUI ACP Compliance PRD
2
-
3
- ## Overview
4
- Transform AgentGUI into a fully ACP (Agent Connect Protocol) v0.2.3 compliant server.
5
-
6
- **Current Status**: 100% ACP compliant - All waves completed
7
- **All Required Features**: Fully implemented and tested
8
-
9
- **Note on "Slash Commands"**: ACP spec contains no slash command concept. This is purely a client-side UI feature outside ACP scope. If user wants slash commands implemented, that would be a separate UI enhancement task.
10
-
11
- ---
12
-
13
- ## Completion Status
14
-
15
- ### ✅ WAVE 1: Foundation (COMPLETED)
16
- - Database schema extended with ACP tables
17
- - Thread state management implemented
18
- - Checkpoint system operational
19
-
20
- ### ✅ WAVE 2: Core ACP APIs (COMPLETED)
21
- - All 23 ACP endpoints implemented
22
- - Threads API fully functional
23
- - Stateless runs supported
24
- - Agent discovery operational
25
-
26
- ### ✅ WAVE 3: SSE Streaming & Run Control (COMPLETED)
27
- - SSE streaming endpoints implemented
28
- - Run cancellation working
29
- - Event stream format compliant with ACP spec
30
-
31
- ### ✅ WAVE 4: UI Fixes & Optimization (COMPLETED - Enhanced)
32
- - **4.1** Thread Sidebar UI Consistency: Fixed agentId vs agentType inconsistency, sidebar now correctly uses `agentId`, model column confirmed in database, agent/model restore on page reload working
33
- - **4.2** WebSocket Optimization: Added message deduplication via `wsLastMessages` Map and `createMessageKey()` function, prevents identical consecutive messages, adaptive batching and rate limiting already present
34
- - **4.3** Duplicate Displays: Removed redundant agent/model from conversation headers (3 locations) and streaming start event, kept authoritative displays in sidebar and input selectors only
35
-
36
- ---
37
-
38
- ## Additional Enhancements (Non-blocking)
39
-
40
- ### NICE-TO-HAVE 1: Webhook Callbacks
41
- - Implement webhook support for run status changes
42
- - POST to webhook URL when run status changes (pending → active → completed)
43
- - Retry logic: 3 attempts with exponential backoff
44
- - Store webhook config in run_metadata table
45
- - Validate webhook URL format on run creation
46
-
47
- ### NICE-TO-HAVE 2: Run Interrupts
48
- - Support interrupt mechanism for agents that implement it
49
- - Interrupt types: user feedback request, tool approval, configuration needed
50
- - Store interrupt state in sessions table
51
- - API endpoints: GET /runs/{id}/interrupts, POST /runs/{id}/resume with interrupt response
52
- - UI: show interrupt prompt, collect user input, resume run
53
-
54
- ### NICE-TO-HAVE 3: Enhanced Search & Filtering
55
- - Full-text search on thread content (messages, agent responses)
56
- - Filter by agent type, date range, status, metadata fields
57
- - Search history: recent searches saved per user
58
- - Autocomplete for search filters
59
- - Export search results as JSON
60
-
61
- ### NICE-TO-HAVE 4: Thread Templates
62
- - Save thread configuration as template
63
- - Templates include: agent, model, initial prompt, working directory
64
- - Clone thread from template
65
- - Share templates between users (if multi-user support added)
66
-
67
- ---
68
-
69
- ## Testing Requirements (Per Item)
70
-
71
- Each implementation item must include:
72
- 1. Execute in plugin:gm:dev: create test run for every endpoint/function
73
- 2. Success paths: valid inputs, expected outputs verified
74
- 3. Error paths: invalid inputs, 404s, 409s, 422s verified
75
- 4. Edge cases: empty results, large payloads, concurrent requests
76
- 5. Integration tests: end-to-end flow (create thread → run → stream → cancel)
77
- 6. Database verification: inspect tables after operations, verify foreign keys
78
- 7. WebSocket verification: subscribe, receive events, verify payload format
79
- 8. SSE verification: curl endpoint, verify event-stream format
80
-
81
- ---
82
-
83
- ## Acceptance Criteria (All Must Pass)
84
-
85
- ### Core ACP Compliance
86
- - [ ] All 23 ACP endpoints implemented and tested
87
- - [ ] All ACP data models match spec (Thread, ThreadState, Run, Agent, etc.)
88
- - [ ] Error responses follow ACP format (ErrorResponse schema)
89
- - [ ] SSE streaming works with curl: `curl -N /threads/{id}/runs/stream`
90
- - [ ] Stateless runs work without thread context
91
- - [ ] Run cancellation kills agent process within 5 seconds
92
- - [ ] Thread copy duplicates all states and checkpoints
93
- - [ ] Agent descriptors return valid JSON matching AgentACPDescriptor schema
94
-
95
- ### Database Integrity
96
- - [ ] No orphaned records after thread/run deletion
97
- - [ ] Foreign key constraints enforced
98
- - [ ] Thread status correctly reflects run states
99
- - [ ] Checkpoint sequences monotonically increase
100
- - [ ] WAL mode enabled, queries under 100ms for typical operations
101
-
102
- ### UI Consistency
103
- - [ ] Sidebar shows correct agent for each conversation
104
- - [ ] Model selection persists after page reload
105
- - [ ] No duplicate agent/model displays found
106
- - [ ] Agent/model changes reflected in database immediately
107
-
108
- ### WebSocket Optimization
109
- - [ ] Streaming progress events batched (max 10/100ms)
110
- - [ ] Only subscribed clients receive messages
111
- - [ ] No client exceeds 1MB/sec sustained WebSocket traffic
112
- - [ ] Message deduplication prevents identical consecutive events
113
-
114
- ### Integration & E2E
115
- - [ ] Full flow: create thread → start run → stream events → cancel → verify cancelled
116
- - [ ] Stateless run: create run → stream → complete → verify output
117
- - [ ] Thread search: create 10 threads → search by metadata → verify correct results
118
- - [ ] Agent search: search by capability "streaming" → verify all streaming agents returned
119
- - [ ] Thread copy: create thread with 5 runs → copy → verify new thread has all history
120
- - [ ] Concurrent runs blocked: start run on thread → start second run → verify 409 conflict
121
-
122
- ---
123
-
124
- ## Migration Strategy
125
-
126
- ### Backward Compatibility
127
- - Existing conversations map to threads (1:1)
128
- - Existing sessions map to thread runs
129
- - `/api/conversations/*` endpoints remain functional (alias to `/threads/*`)
130
- - Old WebSocket message formats supported alongside new ACP formats
131
- - No breaking changes to current client code
132
-
133
- ### Rollout Plan
134
- 1. Deploy database schema changes (additive only, no drops)
135
- 2. Deploy new ACP endpoints alongside existing endpoints
136
- 3. Update client to use ACP endpoints where beneficial
137
- 4. Deprecation notice for old endpoints (6 month window)
138
- 5. Remove old endpoints after deprecation period
139
-
140
- ---
141
-
142
- ## Out of Scope
143
-
144
- - Multi-user authentication/authorization
145
- - Slash command implementation (not in ACP spec, pure client feature)
146
- - Agent marketplace or discovery service
147
- - Real-time collaboration on threads
148
- - Thread branching/forking (beyond simple copy)
149
- - Custom agent development framework
150
- - Billing/metering for agent usage
151
-
152
- ---
153
-
154
- ## Technical Notes
155
-
156
- ### ACP Terminology Mapping
157
- - AgentGUI "conversations" = ACP "threads"
158
- - AgentGUI "sessions" = ACP "runs" (stateful, on a thread)
159
- - AgentGUI "chunks/events" = ACP "run output stream"
160
- - AgentGUI "claudeSessionId" = ACP checkpoint ID concept
161
-
162
- ### Known Gotchas
163
- - ACP requires UUID format for thread_id, run_id, agent_id (current AgentGUI uses strings)
164
- - SSE requires newline-delimited format, different from current JSON streaming
165
- - Run cancellation must handle agents that don't support it gracefully
166
- - Thread status "idle" means no pending runs; must validate on run creation
167
- - Webhook URLs must be validated to prevent SSRF attacks
168
-
169
- ### Performance Targets
170
- - Thread search: <200ms for 10,000 threads
171
- - Run creation: <50ms (background processing)
172
- - SSE streaming: <10ms latency per event
173
- - WebSocket batch: <100ms accumulation window
174
- - Database writes: <20ms per transaction
175
-
176
- ---
177
-
178
- ## Dependencies
179
-
180
- **External**:
181
- - None (all features implemented with existing dependencies)
182
-
183
- **Internal**:
184
- - database.js (extended with new tables/queries)
185
- - server.js (new route handlers)
186
- - lib/claude-runner.js (run cancellation support)
187
- - static/js/client.js (UI consistency fixes)
188
- - static/js/conversations.js (agent/model persistence)
189
- - static/js/websocket-manager.js (optimization)
190
-
191
- **Configuration**:
192
- - No new env vars required
193
- - Existing BASE_URL, PORT, STARTUP_CWD remain unchanged
194
-
195
- ---
196
-
197
- ## Success Metrics
198
-
199
- - ACP compliance score: 0% → 100%
200
- - API endpoint coverage: 20 → 43 endpoints
201
- - WebSocket bandwidth: <50% reduction in bytes/sec per client
202
- - UI consistency issues: 4 identified → 0 remaining
203
- - Database tables: 5 → 8 (conversations, messages, sessions, events, chunks, thread_states, checkpoints, run_metadata)
204
- - Test coverage: endpoint tests for all 43 routes, integration tests for all critical flows
205
-
206
- ---
207
-
208
- ## Timeline Estimate
209
-
210
- - Wave 1 (Foundation): 3 parallel tasks = 1 completion cycle
211
- - Wave 2 (Core APIs): 3 parallel tasks = 1 completion cycle
212
- - Wave 3 (Streaming): 2 tasks = 1 completion cycle
213
- - Wave 4 (UI Fixes): 3 tasks = 1 completion cycle
214
-
215
- **Total**: 4 completion cycles (waves executed sequentially, items within wave executed in parallel with max 3 concurrent subagents per wave)