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.
- package/.prd +0 -26
- package/package.json +1 -1
- package/server.js +50 -53
- 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
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
|
-
});
|