agentgui 1.0.387 → 1.0.389

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 (4) hide show
  1. package/.prd +0 -26
  2. package/package.json +1 -1
  3. package/server.js +50 -53
  4. package/test-cancel.mjs +0 -185
package/.prd CHANGED
@@ -12,32 +12,6 @@ Transform AgentGUI into a fully ACP (Agent Connect Protocol) v0.2.3 compliant se
12
12
 
13
13
  ## Dependency Graph & Execution Waves
14
14
 
15
- ### WAVE 3: Streaming & Run Control (2 items - after Wave 2)
16
-
17
- **3.1** SSE (Server-Sent Events) Streaming
18
- - BLOCKS: 2.1, 2.2, 2.3
19
- - BLOCKED_BY: 4.1
20
- - Implement SSE endpoint format (Content-Type: text/event-stream)
21
- - Stream run outputs as ACP `RunOutputStream` format
22
- - Support both `ValueRunResultUpdate` and `CustomRunResultUpdate` modes
23
- - Event types: data, error, done
24
- - Keep-alive pings every 15 seconds
25
- - Handle client disconnect gracefully
26
- - Convert existing chunk/event stream to SSE format
27
- - Parallel SSE + WebSocket support (both work simultaneously)
28
-
29
- **3.2** Run Cancellation & Control
30
- - BLOCKS: 1.1, 1.2
31
- - BLOCKED_BY: 4.1
32
- - Implement run status state machine: pending → active → completed/error/cancelled
33
- - Cancel endpoint kills agent process (SIGTERM then SIGKILL)
34
- - Update run status to 'cancelled' in database
35
- - Broadcast cancellation via WebSocket
36
- - Clean up active execution tracking
37
- - Return 409 if run already completed/cancelled
38
- - Wait endpoint implements long-polling (30s timeout, return current status)
39
- - Handle graceful degradation if agent doesn't support cancellation
40
-
41
15
  ### WAVE 4: UI Fixes & Optimization (3 items - after Wave 3)
42
16
 
43
17
  **4.1** Thread Sidebar UI Consistency
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.387",
3
+ "version": "1.0.389",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1447,6 +1447,56 @@ const server = http.createServer(async (req, res) => {
1447
1447
  return;
1448
1448
  }
1449
1449
 
1450
+ // POST /runs/stream - Create stateless run and stream output (MUST be before generic /runs/:id route)
1451
+ if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
1452
+ const body = await parseBody(req);
1453
+ const { agent_id, input, config } = body;
1454
+ if (!agent_id) {
1455
+ sendJSON(req, res, 422, { error: 'agent_id is required' });
1456
+ return;
1457
+ }
1458
+ const agent = discoveredAgents.find(a => a.id === agent_id);
1459
+ if (!agent) {
1460
+ sendJSON(req, res, 404, { error: 'Agent not found' });
1461
+ return;
1462
+ }
1463
+ const run = queries.createRun(agent_id, null, input, config);
1464
+ const sseManager = new SSEStreamManager(res, run.run_id);
1465
+ sseManager.start();
1466
+ sseManager.sendProgress({ type: 'run_created', run_id: run.run_id });
1467
+
1468
+ const eventHandler = (eventData) => {
1469
+ if (eventData.sessionId === run.run_id || eventData.conversationId === run.thread_id) {
1470
+ if (eventData.type === 'streaming_progress' && eventData.block) {
1471
+ sseManager.sendProgress(eventData.block);
1472
+ } else if (eventData.type === 'streaming_error') {
1473
+ sseManager.sendError(eventData.error || 'Execution error');
1474
+ } else if (eventData.type === 'streaming_complete') {
1475
+ sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
1476
+ sseManager.cleanup();
1477
+ }
1478
+ }
1479
+ };
1480
+
1481
+ sseStreamHandlers.set(run.run_id, eventHandler);
1482
+ req.on('close', () => {
1483
+ sseStreamHandlers.delete(run.run_id);
1484
+ sseManager.cleanup();
1485
+ });
1486
+
1487
+ const statelessThreadId = queries.getRun(run.run_id)?.thread_id;
1488
+ if (statelessThreadId) {
1489
+ const conv = queries.getConversation(statelessThreadId);
1490
+ if (conv && input?.content) {
1491
+ runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null).catch((err) => {
1492
+ sseManager.sendError(err.message);
1493
+ sseManager.cleanup();
1494
+ });
1495
+ }
1496
+ }
1497
+ return;
1498
+ }
1499
+
1450
1500
  const oldRunByIdMatch = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
1451
1501
  if (oldRunByIdMatch) {
1452
1502
  const runId = oldRunByIdMatch[1];
@@ -1918,59 +1968,6 @@ const server = http.createServer(async (req, res) => {
1918
1968
  return;
1919
1969
  }
1920
1970
 
1921
- if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
1922
- const body = await parseBody(req);
1923
- const { agent_id, input, config } = body;
1924
- if (!agent_id) {
1925
- sendJSON(req, res, 422, { error: 'agent_id is required' });
1926
- return;
1927
- }
1928
- const agent = discoveredAgents.find(a => a.id === agent_id);
1929
- if (!agent) {
1930
- sendJSON(req, res, 404, { error: 'Agent not found' });
1931
- return;
1932
- }
1933
- const run = queries.createRun(agent_id, null, input, config);
1934
- const sseManager = new SSEStreamManager(res, run.run_id);
1935
- sseManager.start();
1936
- sseManager.sendProgress({ type: 'run_created', run_id: run.run_id });
1937
-
1938
- const eventHandler = (eventData) => {
1939
- if (eventData.sessionId === run.run_id || eventData.conversationId === run.thread_id) {
1940
- if (eventData.type === 'streaming_progress' && eventData.block) {
1941
- sseManager.sendProgress(eventData.block);
1942
- } else if (eventData.type === 'streaming_error') {
1943
- sseManager.sendError(eventData.error || 'Execution error');
1944
- } else if (eventData.type === 'streaming_complete') {
1945
- sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
1946
- sseManager.cleanup();
1947
- }
1948
- }
1949
- };
1950
-
1951
- sseStreamHandlers.set(run.run_id, eventHandler);
1952
- req.on('close', () => {
1953
- sseStreamHandlers.delete(run.run_id);
1954
- sseManager.cleanup();
1955
- });
1956
-
1957
- const statelessThreadId = queries.getRun(run.run_id)?.thread_id;
1958
- if (statelessThreadId) {
1959
- const conv = queries.getConversation(statelessThreadId);
1960
- if (conv && input?.content) {
1961
- const session = queries.createSession(statelessThreadId);
1962
- acpQueries.updateRunStatus(run.run_id, 'active');
1963
- activeExecutions.set(statelessThreadId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
1964
- activeProcessesByRunId.set(run.run_id, { threadId: statelessThreadId, sessionId: session.id });
1965
- queries.setIsStreaming(statelessThreadId, true);
1966
- processMessageWithStreaming(statelessThreadId, null, session.id, input.content, agent_id, config?.model || null)
1967
- .then(() => { acpQueries.updateRunStatus(run.run_id, 'success'); activeProcessesByRunId.delete(run.run_id); })
1968
- .catch((err) => { acpQueries.updateRunStatus(run.run_id, 'error'); activeProcessesByRunId.delete(run.run_id); sseManager.sendError(err.message); sseManager.cleanup(); });
1969
- }
1970
- }
1971
- return;
1972
- }
1973
-
1974
1971
  if (pathOnly === '/api/runs/wait' && req.method === 'POST') {
1975
1972
  const body = await parseBody(req);
1976
1973
  const { agent_id, input, config } = body;
package/test-cancel.mjs DELETED
@@ -1,185 +0,0 @@
1
- // Integration test for run cancellation and control
2
- import http from 'http';
3
- import { randomUUID } from 'crypto';
4
- import Database from 'better-sqlite3';
5
- import path from 'path';
6
- import os from 'os';
7
- import { createACPQueries } from './acp-queries.js';
8
-
9
- const dbPath = path.join(os.homedir(), '.gmgui', 'data.db');
10
- const db = new Database(dbPath);
11
- const prep = (sql) => db.prepare(sql);
12
- const acpQueries = createACPQueries(db, prep);
13
-
14
- const BASE_URL = 'http://localhost:3000/gm';
15
- const testResults = {
16
- passed: [],
17
- failed: []
18
- };
19
-
20
- function testPass(name) {
21
- testResults.passed.push(name);
22
- console.log(`✓ ${name}`);
23
- }
24
-
25
- function testFail(name, error) {
26
- testResults.failed.push({ name, error });
27
- console.log(`✗ ${name}: ${error}`);
28
- }
29
-
30
- async function makeRequest(method, path, body = null) {
31
- return new Promise((resolve, reject) => {
32
- const fullPath = `/gm${path}`;
33
- const options = {
34
- method,
35
- hostname: 'localhost',
36
- port: 3000,
37
- path: fullPath,
38
- headers: {
39
- 'Content-Type': 'application/json'
40
- }
41
- };
42
-
43
- const req = http.request(options, (res) => {
44
- let data = '';
45
- res.on('data', chunk => data += chunk);
46
- res.on('end', () => {
47
- try {
48
- const parsed = data ? JSON.parse(data) : null;
49
- resolve({ status: res.statusCode, data: parsed, headers: res.headers });
50
- } catch {
51
- resolve({ status: res.statusCode, data: data, headers: res.headers });
52
- }
53
- });
54
- });
55
-
56
- req.on('error', reject);
57
- if (body) req.write(JSON.stringify(body));
58
- req.end();
59
- });
60
- }
61
-
62
- async function runTests() {
63
- console.log('=== RUNNING INTEGRATION TESTS ===\n');
64
-
65
- try {
66
- // Test 1: Create a thread
67
- console.log('[Test 1] Creating thread...');
68
- const threadResp = await makeRequest('POST', '/api/threads', {});
69
- if ((threadResp.status === 200 || threadResp.status === 201) && threadResp.data.thread_id) {
70
- testPass('Thread creation');
71
- } else {
72
- testFail('Thread creation', `Status ${threadResp.status}`);
73
- return;
74
- }
75
-
76
- const threadId = threadResp.data.thread_id;
77
-
78
- // Test 2: Create a run (stateless, without thread)
79
- console.log('[Test 2] Creating stateless run...');
80
- const runResp = await makeRequest('POST', '/api/runs', {
81
- agent_id: 'claude-code',
82
- input: 'test input'
83
- });
84
- if (runResp.status === 200 && runResp.data.run_id) {
85
- testPass('Stateless run creation');
86
- } else {
87
- testFail('Stateless run creation', `Status ${runResp.status}`);
88
- return;
89
- }
90
-
91
- const runId = runResp.data.run_id;
92
-
93
- // Test 3: Verify run status is pending
94
- console.log('[Test 3] Verifying run status...');
95
- const run = acpQueries.getRun(runId);
96
- if (run && run.status === 'pending') {
97
- testPass('Run status is pending');
98
- } else {
99
- testFail('Run status is pending', `Status is ${run?.status}`);
100
- }
101
-
102
- // Test 4: Cancel the run using /api/runs/{run_id}/cancel
103
- console.log('[Test 4] Cancelling run via /api/runs/{run_id}/cancel...');
104
- const cancelResp = await makeRequest('POST', `/api/runs/${runId}/cancel`);
105
- if (cancelResp.status === 200 && cancelResp.data.status === 'cancelled') {
106
- testPass('Run cancellation via /api/runs');
107
- } else {
108
- testFail('Run cancellation via /api/runs', `Status ${cancelResp.status}, run status ${cancelResp.data?.status}`);
109
- }
110
-
111
- // Test 5: Verify run status is cancelled in database
112
- console.log('[Test 5] Verifying cancelled status in DB...');
113
- const cancelledRun = acpQueries.getRun(runId);
114
- if (cancelledRun && cancelledRun.status === 'cancelled') {
115
- testPass('Cancelled status persisted in database');
116
- } else {
117
- testFail('Cancelled status persisted in database', `Status is ${cancelledRun?.status}`);
118
- }
119
-
120
- // Test 6: Try to cancel again - should get 409 conflict
121
- console.log('[Test 6] Testing 409 conflict on re-cancel...');
122
- const recancel = await makeRequest('POST', `/api/runs/${runId}/cancel`);
123
- if (recancel.status === 409) {
124
- testPass('409 conflict on already-cancelled run');
125
- } else {
126
- testFail('409 conflict on already-cancelled run', `Got status ${recancel.status}`);
127
- }
128
-
129
- // Test 7: Test wait endpoint with already-completed run
130
- console.log('[Test 7] Testing wait endpoint with completed run...');
131
- const waitStart = Date.now();
132
- const waitResp = await makeRequest('GET', `/api/runs/${runId}/wait`);
133
- const waitDuration = Date.now() - waitStart;
134
- if (waitResp.status === 200 && waitDuration < 5000) {
135
- testPass('Wait endpoint returns immediately for completed run');
136
- } else {
137
- testFail('Wait endpoint returns immediately for completed run', `Took ${waitDuration}ms`);
138
- }
139
-
140
- // Test 8: Test cancellation of non-existent run
141
- console.log('[Test 8] Testing 404 on non-existent run...');
142
- const fakeRunId = randomUUID();
143
- const notFound = await makeRequest('POST', `/api/runs/${fakeRunId}/cancel`);
144
- if (notFound.status === 404) {
145
- testPass('404 on non-existent run');
146
- } else {
147
- testFail('404 on non-existent run', `Got status ${notFound.status}`);
148
- }
149
-
150
- // Cleanup
151
- console.log('\n[Cleanup] Deleting test thread...');
152
- try {
153
- acpQueries.deleteThread(threadId);
154
- console.log('Cleanup complete');
155
- } catch (e) {
156
- console.log('Cleanup warning:', e.message);
157
- }
158
-
159
- } catch (error) {
160
- console.error('Test suite error:', error);
161
- testFail('Test suite execution', error.message);
162
- }
163
-
164
- db.close();
165
-
166
- // Summary
167
- console.log('\n=== TEST SUMMARY ===');
168
- console.log(`Passed: ${testResults.passed.length}`);
169
- console.log(`Failed: ${testResults.failed.length}`);
170
- if (testResults.failed.length > 0) {
171
- console.log('\nFailed tests:');
172
- testResults.failed.forEach(f => console.log(` - ${f.name}: ${f.error}`));
173
- }
174
-
175
- return testResults.passed.length > 0 && testResults.failed.length === 0;
176
- }
177
-
178
- // Run the tests
179
- runTests().then(success => {
180
- console.log(`\n${success ? '✓ ALL TESTS PASSED' : '✗ SOME TESTS FAILED'}`);
181
- process.exit(success ? 0 : 1);
182
- }).catch(err => {
183
- console.error('Fatal test error:', err);
184
- process.exit(1);
185
- });