agentgui 1.0.382 → 1.0.383
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/acp-queries.js +22 -2
- package/package.json +1 -1
- package/server.js +603 -40
package/acp-queries.js
CHANGED
|
@@ -144,8 +144,28 @@ export function createACPQueries(db, prep) {
|
|
|
144
144
|
const ths = rows.map(r => ({ thread_id: r.id, created_at: iso(r.created_at), updated_at: iso(r.updated_at), metadata: jp(r.metadata), status: r.status || 'idle' }));
|
|
145
145
|
return { threads: ths, total: tot, limit, offset, hasMore: offset + limit < tot };
|
|
146
146
|
},
|
|
147
|
-
searchAgents(flt = {}) {
|
|
148
|
-
|
|
147
|
+
searchAgents(agents, flt = {}) {
|
|
148
|
+
const { name, version, capabilities, limit = 50, offset = 0 } = flt;
|
|
149
|
+
let results = agents;
|
|
150
|
+
if (name) {
|
|
151
|
+
const n = name.toLowerCase();
|
|
152
|
+
results = results.filter(a => a.name.toLowerCase().includes(n) || a.id.toLowerCase().includes(n));
|
|
153
|
+
}
|
|
154
|
+
if (capabilities) {
|
|
155
|
+
results = results.filter(a => {
|
|
156
|
+
const desc = this.getAgentDescriptor ? this.getAgentDescriptor(a.id) : null;
|
|
157
|
+
if (!desc) return false;
|
|
158
|
+
const caps = desc.specs?.capabilities || {};
|
|
159
|
+
if (capabilities.streaming !== undefined && !caps.streaming) return false;
|
|
160
|
+
if (capabilities.threads !== undefined && caps.threads !== capabilities.threads) return false;
|
|
161
|
+
if (capabilities.interrupts !== undefined && caps.interrupts !== capabilities.interrupts) return false;
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const total = results.length;
|
|
166
|
+
const paginated = results.slice(offset, offset + limit);
|
|
167
|
+
const agents_list = paginated.map(a => ({ agent_id: a.id, name: a.name, version: version || '1.0.0', path: a.path }));
|
|
168
|
+
return { agents: agents_list, total, limit, offset, hasMore: offset + limit < total };
|
|
149
169
|
},
|
|
150
170
|
searchRuns(flt = {}) {
|
|
151
171
|
const { agent_id, thread_id, status, limit = 50, offset = 0 } = flt;
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1444,9 +1444,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1444
1444
|
return;
|
|
1445
1445
|
}
|
|
1446
1446
|
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1447
|
+
const oldRunByIdMatch = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1448
|
+
if (oldRunByIdMatch) {
|
|
1449
|
+
const runId = oldRunByIdMatch[1];
|
|
1450
1450
|
const session = queries.getSession(runId);
|
|
1451
1451
|
|
|
1452
1452
|
if (!session) {
|
|
@@ -1511,9 +1511,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1511
1511
|
}
|
|
1512
1512
|
}
|
|
1513
1513
|
|
|
1514
|
-
const
|
|
1514
|
+
const oldRunCancelMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
1515
1515
|
if (runCancelMatch && req.method === 'POST') {
|
|
1516
|
-
const runId =
|
|
1516
|
+
const runId = oldRunCancelMatch[1];
|
|
1517
1517
|
const session = queries.getSession(runId);
|
|
1518
1518
|
|
|
1519
1519
|
if (!session) {
|
|
@@ -1767,34 +1767,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1767
1767
|
return;
|
|
1768
1768
|
}
|
|
1769
1769
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
let parsed = {};
|
|
1775
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1776
|
-
|
|
1777
|
-
const { query } = parsed;
|
|
1778
|
-
let results = discoveredAgents;
|
|
1779
|
-
|
|
1780
|
-
if (query) {
|
|
1781
|
-
const q = query.toLowerCase();
|
|
1782
|
-
results = discoveredAgents.filter(a =>
|
|
1783
|
-
a.name.toLowerCase().includes(q) ||
|
|
1784
|
-
a.id.toLowerCase().includes(q) ||
|
|
1785
|
-
(a.description && a.description.toLowerCase().includes(q))
|
|
1786
|
-
);
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
const agents = results.map(a => ({
|
|
1790
|
-
id: a.id,
|
|
1791
|
-
name: a.name,
|
|
1792
|
-
description: a.description || '',
|
|
1793
|
-
icon: a.icon || null,
|
|
1794
|
-
status: 'available'
|
|
1795
|
-
}));
|
|
1796
|
-
|
|
1797
|
-
sendJSON(req, res, 200, agents);
|
|
1770
|
+
if (pathOnly === '/api/agents/search' && req.method === 'POST') {
|
|
1771
|
+
const body = await parseBody(req);
|
|
1772
|
+
const result = acpQueries.searchAgents(discoveredAgents, body);
|
|
1773
|
+
sendJSON(req, res, 200, result);
|
|
1798
1774
|
return;
|
|
1799
1775
|
}
|
|
1800
1776
|
|
|
@@ -1915,6 +1891,214 @@ const server = http.createServer(async (req, res) => {
|
|
|
1915
1891
|
return;
|
|
1916
1892
|
}
|
|
1917
1893
|
|
|
1894
|
+
if (pathOnly === '/api/runs' && req.method === 'POST') {
|
|
1895
|
+
const body = await parseBody(req);
|
|
1896
|
+
const { agent_id, input, config, webhook_url } = body;
|
|
1897
|
+
if (!agent_id) {
|
|
1898
|
+
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1902
|
+
if (!agent) {
|
|
1903
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const run = acpQueries.createRun(agent_id, null, input, config, webhook_url);
|
|
1907
|
+
sendJSON(req, res, 201, run);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (pathOnly === '/api/runs/search' && req.method === 'POST') {
|
|
1912
|
+
const body = await parseBody(req);
|
|
1913
|
+
const result = acpQueries.searchRuns(body);
|
|
1914
|
+
sendJSON(req, res, 200, result);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
|
|
1919
|
+
const body = await parseBody(req);
|
|
1920
|
+
const { agent_id, input, config } = body;
|
|
1921
|
+
if (!agent_id) {
|
|
1922
|
+
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1926
|
+
if (!agent) {
|
|
1927
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const run = acpQueries.createRun(agent_id, null, input, config);
|
|
1931
|
+
res.writeHead(200, {
|
|
1932
|
+
'Content-Type': 'text/event-stream',
|
|
1933
|
+
'Cache-Control': 'no-cache',
|
|
1934
|
+
'Connection': 'keep-alive'
|
|
1935
|
+
});
|
|
1936
|
+
res.write('data: ' + JSON.stringify({ type: 'run_created', run_id: run.run_id }) + '\n\n');
|
|
1937
|
+
const eventHandler = (eventData) => {
|
|
1938
|
+
if (eventData.sessionId === run.run_id || eventData.conversationId === run.thread_id) {
|
|
1939
|
+
res.write('data: ' + JSON.stringify(eventData) + '\n\n');
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
const cleanup = () => {
|
|
1943
|
+
res.end();
|
|
1944
|
+
};
|
|
1945
|
+
req.on('close', cleanup);
|
|
1946
|
+
const statelessThreadId = acpQueries.getRun(run.run_id)?.thread_id;
|
|
1947
|
+
if (statelessThreadId) {
|
|
1948
|
+
const conv = queries.getConversation(statelessThreadId);
|
|
1949
|
+
if (conv && input?.content) {
|
|
1950
|
+
runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null).catch(() => {});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
if (pathOnly === '/api/runs/wait' && req.method === 'POST') {
|
|
1957
|
+
const body = await parseBody(req);
|
|
1958
|
+
const { agent_id, input, config } = body;
|
|
1959
|
+
if (!agent_id) {
|
|
1960
|
+
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1964
|
+
if (!agent) {
|
|
1965
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const run = acpQueries.createRun(agent_id, null, input, config);
|
|
1969
|
+
const statelessThreadId = acpQueries.getRun(run.run_id)?.thread_id;
|
|
1970
|
+
if (statelessThreadId && input?.content) {
|
|
1971
|
+
try {
|
|
1972
|
+
await runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null);
|
|
1973
|
+
const finalRun = acpQueries.getRun(run.run_id);
|
|
1974
|
+
sendJSON(req, res, 200, finalRun);
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
acpQueries.updateRunStatus(run.run_id, 'error');
|
|
1977
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
sendJSON(req, res, 200, run);
|
|
1981
|
+
}
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const oldRunByIdMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1986
|
+
if (oldRunByIdMatch1) {
|
|
1987
|
+
const runId = oldRunByIdMatch1[1];
|
|
1988
|
+
|
|
1989
|
+
if (req.method === 'GET') {
|
|
1990
|
+
const run = acpQueries.getRun(runId);
|
|
1991
|
+
if (!run) {
|
|
1992
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
sendJSON(req, res, 200, run);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (req.method === 'POST') {
|
|
2000
|
+
const body = await parseBody(req);
|
|
2001
|
+
const run = acpQueries.getRun(runId);
|
|
2002
|
+
if (!run) {
|
|
2003
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
if (run.status !== 'pending') {
|
|
2007
|
+
sendJSON(req, res, 409, { error: 'Run is not resumable' });
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (body.input?.content && run.thread_id) {
|
|
2011
|
+
runClaudeWithStreaming(run.agent_id, run.thread_id, body.input.content, null).catch(() => {});
|
|
2012
|
+
}
|
|
2013
|
+
sendJSON(req, res, 200, run);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
if (req.method === 'DELETE') {
|
|
2018
|
+
try {
|
|
2019
|
+
acpQueries.deleteRun(runId);
|
|
2020
|
+
res.writeHead(204);
|
|
2021
|
+
res.end();
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2024
|
+
}
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const runWaitMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/wait$/);
|
|
2030
|
+
if (runWaitMatch && req.method === 'GET') {
|
|
2031
|
+
const runId = runWaitMatch[1];
|
|
2032
|
+
const run = acpQueries.getRun(runId);
|
|
2033
|
+
if (!run) {
|
|
2034
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
const startTime = Date.now();
|
|
2038
|
+
const pollInterval = setInterval(() => {
|
|
2039
|
+
const currentRun = acpQueries.getRun(runId);
|
|
2040
|
+
if (!currentRun || ['success', 'error', 'cancelled'].includes(currentRun.status) || (Date.now() - startTime) > 30000) {
|
|
2041
|
+
clearInterval(pollInterval);
|
|
2042
|
+
sendJSON(req, res, 200, currentRun || run);
|
|
2043
|
+
}
|
|
2044
|
+
}, 500);
|
|
2045
|
+
req.on('close', () => clearInterval(pollInterval));
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
const runStreamMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/);
|
|
2050
|
+
if (runStreamMatch && req.method === 'GET') {
|
|
2051
|
+
const runId = runStreamMatch[1];
|
|
2052
|
+
const run = acpQueries.getRun(runId);
|
|
2053
|
+
if (!run) {
|
|
2054
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
res.writeHead(200, {
|
|
2058
|
+
'Content-Type': 'text/event-stream',
|
|
2059
|
+
'Cache-Control': 'no-cache',
|
|
2060
|
+
'Connection': 'keep-alive'
|
|
2061
|
+
});
|
|
2062
|
+
res.write('data: ' + JSON.stringify({ type: 'joined', run_id: runId }) + '\n\n');
|
|
2063
|
+
const eventHandler = (eventData) => {
|
|
2064
|
+
if (eventData.sessionId === runId || eventData.conversationId === run.thread_id) {
|
|
2065
|
+
res.write('data: ' + JSON.stringify(eventData) + '\n\n');
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
const cleanup = () => {
|
|
2069
|
+
res.end();
|
|
2070
|
+
};
|
|
2071
|
+
req.on('close', cleanup);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const oldRunCancelMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
2076
|
+
if (runCancelMatch && req.method === 'POST') {
|
|
2077
|
+
const runId = oldRunCancelMatch1[1];
|
|
2078
|
+
try {
|
|
2079
|
+
const run = acpQueries.cancelRun(runId);
|
|
2080
|
+
const execution = activeExecutions.get(run.thread_id);
|
|
2081
|
+
if (execution?.process) {
|
|
2082
|
+
execution.process.kill('SIGTERM');
|
|
2083
|
+
setTimeout(() => {
|
|
2084
|
+
if (execution.process && !execution.process.killed) {
|
|
2085
|
+
execution.process.kill('SIGKILL');
|
|
2086
|
+
}
|
|
2087
|
+
}, 5000);
|
|
2088
|
+
}
|
|
2089
|
+
sendJSON(req, res, 200, run);
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
if (err.message === 'Run not found') {
|
|
2092
|
+
sendJSON(req, res, 404, { error: err.message });
|
|
2093
|
+
} else if (err.message.includes('already completed')) {
|
|
2094
|
+
sendJSON(req, res, 409, { error: err.message });
|
|
2095
|
+
} else {
|
|
2096
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
1918
2102
|
if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
|
|
1919
2103
|
try {
|
|
1920
2104
|
const result = await startGeminiOAuth(req);
|
|
@@ -2174,9 +2358,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
2174
2358
|
return;
|
|
2175
2359
|
}
|
|
2176
2360
|
|
|
2177
|
-
const
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2361
|
+
const oldThreadByIdMatch = pathOnly.match(/^\/api\/threads\/([^/]+)$/);
|
|
2362
|
+
if (oldThreadByIdMatch) {
|
|
2363
|
+
const threadId = oldThreadByIdMatch[1];
|
|
2180
2364
|
const conv = queries.getConversation(threadId);
|
|
2181
2365
|
|
|
2182
2366
|
if (!conv) {
|
|
@@ -2236,9 +2420,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
2236
2420
|
}
|
|
2237
2421
|
}
|
|
2238
2422
|
|
|
2239
|
-
const
|
|
2423
|
+
const oldThreadHistoryMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/history$/);
|
|
2240
2424
|
if (threadHistoryMatch && req.method === 'GET') {
|
|
2241
|
-
const threadId =
|
|
2425
|
+
const threadId = oldThreadHistoryMatch[1];
|
|
2242
2426
|
const conv = queries.getConversation(threadId);
|
|
2243
2427
|
|
|
2244
2428
|
if (!conv) {
|
|
@@ -2261,7 +2445,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2261
2445
|
return;
|
|
2262
2446
|
}
|
|
2263
2447
|
|
|
2264
|
-
const
|
|
2448
|
+
const oldThreadCopyMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/copy$/);
|
|
2265
2449
|
if (threadCopyMatch && req.method === 'POST') {
|
|
2266
2450
|
const threadId = threadCopyMatch[1];
|
|
2267
2451
|
const original = queries.getConversation(threadId);
|
|
@@ -2357,6 +2541,143 @@ const server = http.createServer(async (req, res) => {
|
|
|
2357
2541
|
}
|
|
2358
2542
|
}
|
|
2359
2543
|
|
|
2544
|
+
const threadRunsStreamMatch = pathOnly.match(/^\/api\/threads\/([^\/]+)\/runs\/stream$/);
|
|
2545
|
+
if (threadRunsStreamMatch && req.method === 'POST') {
|
|
2546
|
+
const threadId = threadRunsStreamMatch[1];
|
|
2547
|
+
const conv = queries.getConversation(threadId);
|
|
2548
|
+
|
|
2549
|
+
if (!conv) {
|
|
2550
|
+
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
const activeEntry = activeExecutions.get(threadId);
|
|
2555
|
+
if (activeEntry) {
|
|
2556
|
+
sendJSON(req, res, 409, { error: 'Thread already has an active run' });
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
let body = '';
|
|
2561
|
+
for await (const chunk of req) { body += chunk; }
|
|
2562
|
+
let parsed = {};
|
|
2563
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2564
|
+
|
|
2565
|
+
const { input, agentId } = parsed;
|
|
2566
|
+
if (!input) {
|
|
2567
|
+
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
const resolvedAgentId = agentId || conv.agentId || 'claude-code';
|
|
2572
|
+
const resolvedModel = parsed.model || conv.model || null;
|
|
2573
|
+
|
|
2574
|
+
const session = queries.createSession(threadId, resolvedAgentId, 'pending');
|
|
2575
|
+
const message = queries.createMessage(threadId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
2576
|
+
|
|
2577
|
+
res.writeHead(200, {
|
|
2578
|
+
'Content-Type': 'text/event-stream',
|
|
2579
|
+
'Cache-Control': 'no-cache',
|
|
2580
|
+
'Connection': 'keep-alive'
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
const eventListeners = [];
|
|
2584
|
+
const broadcastListener = (event) => {
|
|
2585
|
+
if (event.sessionId === session.id) {
|
|
2586
|
+
if (event.type === 'streaming_progress') {
|
|
2587
|
+
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
|
2588
|
+
} else if (event.type === 'streaming_complete') {
|
|
2589
|
+
res.write(`event: done\ndata: ${JSON.stringify({ status: 'completed' })}\n\n`);
|
|
2590
|
+
res.end();
|
|
2591
|
+
} else if (event.type === 'streaming_error') {
|
|
2592
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: event.error })}\n\n`);
|
|
2593
|
+
res.end();
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
eventListeners.push(broadcastListener);
|
|
2598
|
+
|
|
2599
|
+
req.on('close', () => {
|
|
2600
|
+
eventListeners.length = 0;
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
processMessageWithStreaming(threadId, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const threadRunsWaitMatch = pathOnly.match(/^\/api\/threads\/([^\/]+)\/runs\/wait$/);
|
|
2608
|
+
if (threadRunsWaitMatch && req.method === 'POST') {
|
|
2609
|
+
const threadId = threadRunsWaitMatch[1];
|
|
2610
|
+
const conv = queries.getConversation(threadId);
|
|
2611
|
+
|
|
2612
|
+
if (!conv) {
|
|
2613
|
+
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
const activeEntry = activeExecutions.get(threadId);
|
|
2618
|
+
if (activeEntry) {
|
|
2619
|
+
sendJSON(req, res, 409, { error: 'Thread already has an active run' });
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
let body = '';
|
|
2624
|
+
for await (const chunk of req) { body += chunk; }
|
|
2625
|
+
let parsed = {};
|
|
2626
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2627
|
+
|
|
2628
|
+
const { input, agentId } = parsed;
|
|
2629
|
+
if (!input) {
|
|
2630
|
+
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
const resolvedAgentId = agentId || conv.agentId || 'claude-code';
|
|
2635
|
+
const resolvedModel = parsed.model || conv.model || null;
|
|
2636
|
+
|
|
2637
|
+
const session = queries.createSession(threadId, resolvedAgentId, 'pending');
|
|
2638
|
+
const message = queries.createMessage(threadId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
2639
|
+
|
|
2640
|
+
const waitPromise = new Promise((resolve, reject) => {
|
|
2641
|
+
const checkInterval = setInterval(() => {
|
|
2642
|
+
const updatedSession = queries.getSession(session.id);
|
|
2643
|
+
if (!updatedSession) {
|
|
2644
|
+
clearInterval(checkInterval);
|
|
2645
|
+
reject(new Error('Session not found'));
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
if (['success', 'error', 'interrupted', 'cancelled'].includes(updatedSession.status)) {
|
|
2649
|
+
clearInterval(checkInterval);
|
|
2650
|
+
resolve(updatedSession);
|
|
2651
|
+
}
|
|
2652
|
+
}, 500);
|
|
2653
|
+
|
|
2654
|
+
setTimeout(() => {
|
|
2655
|
+
clearInterval(checkInterval);
|
|
2656
|
+
const updatedSession = queries.getSession(session.id);
|
|
2657
|
+
resolve(updatedSession || session);
|
|
2658
|
+
}, 300000);
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
processMessageWithStreaming(threadId, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
2662
|
+
|
|
2663
|
+
try {
|
|
2664
|
+
const completedSession = await waitPromise;
|
|
2665
|
+
sendJSON(req, res, 200, {
|
|
2666
|
+
id: completedSession.id,
|
|
2667
|
+
threadId: threadId,
|
|
2668
|
+
status: completedSession.status,
|
|
2669
|
+
started_at: completedSession.started_at,
|
|
2670
|
+
completed_at: completedSession.completed_at,
|
|
2671
|
+
agentId: resolvedAgentId,
|
|
2672
|
+
output: completedSession.response || null
|
|
2673
|
+
});
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
2676
|
+
}
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
|
|
2360
2681
|
const threadRunByIdMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)$/);
|
|
2361
2682
|
if (threadRunByIdMatch) {
|
|
2362
2683
|
const threadId = threadRunByIdMatch[1];
|
|
@@ -2427,6 +2748,106 @@ const server = http.createServer(async (req, res) => {
|
|
|
2427
2748
|
}
|
|
2428
2749
|
}
|
|
2429
2750
|
|
|
2751
|
+
const threadRunWaitMatch = pathOnly.match(/^\/api\/threads\/([^\/]+)\/runs\/([^\/]+)\/wait$/);
|
|
2752
|
+
if (threadRunWaitMatch && req.method === 'GET') {
|
|
2753
|
+
const threadId = threadRunWaitMatch[1];
|
|
2754
|
+
const runId = threadRunWaitMatch[2];
|
|
2755
|
+
const session = queries.getSession(runId);
|
|
2756
|
+
|
|
2757
|
+
if (!session || session.conversationId !== threadId) {
|
|
2758
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
const waitPromise = new Promise((resolve) => {
|
|
2763
|
+
const checkInterval = setInterval(() => {
|
|
2764
|
+
const updatedSession = queries.getSession(runId);
|
|
2765
|
+
if (!updatedSession) {
|
|
2766
|
+
clearInterval(checkInterval);
|
|
2767
|
+
resolve(session);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (['success', 'error', 'interrupted', 'cancelled'].includes(updatedSession.status)) {
|
|
2771
|
+
clearInterval(checkInterval);
|
|
2772
|
+
resolve(updatedSession);
|
|
2773
|
+
}
|
|
2774
|
+
}, 500);
|
|
2775
|
+
|
|
2776
|
+
setTimeout(() => {
|
|
2777
|
+
clearInterval(checkInterval);
|
|
2778
|
+
const updatedSession = queries.getSession(runId) || session;
|
|
2779
|
+
resolve(updatedSession);
|
|
2780
|
+
}, 30000);
|
|
2781
|
+
});
|
|
2782
|
+
|
|
2783
|
+
try {
|
|
2784
|
+
const completedSession = await waitPromise;
|
|
2785
|
+
sendJSON(req, res, 200, {
|
|
2786
|
+
id: completedSession.id,
|
|
2787
|
+
threadId: threadId,
|
|
2788
|
+
status: completedSession.status,
|
|
2789
|
+
started_at: completedSession.started_at,
|
|
2790
|
+
completed_at: completedSession.completed_at,
|
|
2791
|
+
agentId: completedSession.agentId,
|
|
2792
|
+
output: completedSession.response || null
|
|
2793
|
+
});
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
2796
|
+
}
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
const threadRunStreamMatch = pathOnly.match(/^\/api\/threads\/([^\/]+)\/runs\/([^\/]+)\/stream$/);
|
|
2801
|
+
if (threadRunStreamMatch && req.method === 'GET') {
|
|
2802
|
+
const threadId = threadRunStreamMatch[1];
|
|
2803
|
+
const runId = threadRunStreamMatch[2];
|
|
2804
|
+
const session = queries.getSession(runId);
|
|
2805
|
+
|
|
2806
|
+
if (!session || session.conversationId !== threadId) {
|
|
2807
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
res.writeHead(200, {
|
|
2812
|
+
'Content-Type': 'text/event-stream',
|
|
2813
|
+
'Cache-Control': 'no-cache',
|
|
2814
|
+
'Connection': 'keep-alive'
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
const chunks = queries.getSessionChunks(runId, 0);
|
|
2818
|
+
for (const chunk of chunks) {
|
|
2819
|
+
res.write(`event: message\ndata: ${JSON.stringify(chunk)}\n\n`);
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
const eventListeners = [];
|
|
2823
|
+
const broadcastListener = (event) => {
|
|
2824
|
+
if (event.sessionId === runId) {
|
|
2825
|
+
if (event.type === 'streaming_progress') {
|
|
2826
|
+
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
|
2827
|
+
} else if (event.type === 'streaming_complete') {
|
|
2828
|
+
res.write(`event: done\ndata: ${JSON.stringify({ status: 'completed' })}\n\n`);
|
|
2829
|
+
res.end();
|
|
2830
|
+
} else if (event.type === 'streaming_error') {
|
|
2831
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: event.error })}\n\n`);
|
|
2832
|
+
res.end();
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
eventListeners.push(broadcastListener);
|
|
2837
|
+
|
|
2838
|
+
req.on('close', () => {
|
|
2839
|
+
eventListeners.length = 0;
|
|
2840
|
+
});
|
|
2841
|
+
|
|
2842
|
+
if (['success', 'error', 'interrupted', 'cancelled'].includes(session.status)) {
|
|
2843
|
+
res.write(`event: done\ndata: ${JSON.stringify({ status: session.status })}\n\n`);
|
|
2844
|
+
res.end();
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
|
|
2430
2851
|
const threadRunCancelMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/cancel$/);
|
|
2431
2852
|
if (threadRunCancelMatch && req.method === 'POST') {
|
|
2432
2853
|
const threadId = threadRunCancelMatch[1];
|
|
@@ -2734,7 +3155,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2734
3155
|
if (pathOnly === '/api/git/push' && req.method === 'POST') {
|
|
2735
3156
|
try {
|
|
2736
3157
|
const isWindows = os.platform() === 'win32';
|
|
2737
|
-
const gitCommand = isWindows
|
|
3158
|
+
const gitCommand = isWindows
|
|
2738
3159
|
? 'git add -A & git commit -m "Auto-commit" & git push'
|
|
2739
3160
|
: 'git add -A && git commit -m "Auto-commit" && git push';
|
|
2740
3161
|
execSync(gitCommand, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
@@ -2745,6 +3166,148 @@ const server = http.createServer(async (req, res) => {
|
|
|
2745
3166
|
return;
|
|
2746
3167
|
}
|
|
2747
3168
|
|
|
3169
|
+
// ============================================================
|
|
3170
|
+
// THREAD API ENDPOINTS (ACP v0.2.3)
|
|
3171
|
+
// ============================================================
|
|
3172
|
+
|
|
3173
|
+
// POST /threads - Create empty thread
|
|
3174
|
+
if (pathOnly === '/api/threads' && req.method === 'POST') {
|
|
3175
|
+
try {
|
|
3176
|
+
const body = await parseBody(req);
|
|
3177
|
+
const metadata = body.metadata || {};
|
|
3178
|
+
const thread = queries.createThread(metadata);
|
|
3179
|
+
sendJSON(req, res, 201, thread);
|
|
3180
|
+
} catch (err) {
|
|
3181
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
3182
|
+
}
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
// POST /threads/search - Search threads
|
|
3187
|
+
if (pathOnly === '/api/threads/search' && req.method === 'POST') {
|
|
3188
|
+
try {
|
|
3189
|
+
const body = await parseBody(req);
|
|
3190
|
+
const result = queries.searchThreads(body);
|
|
3191
|
+
sendJSON(req, res, 200, result);
|
|
3192
|
+
} catch (err) {
|
|
3193
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
3194
|
+
}
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// GET /threads/{thread_id} - Get thread by ID
|
|
3199
|
+
const acpThreadMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})$/);
|
|
3200
|
+
if (acpThreadMatch && req.method === 'GET') {
|
|
3201
|
+
const threadId = threadByIdMatch[1];
|
|
3202
|
+
try {
|
|
3203
|
+
const thread = queries.getThread(threadId);
|
|
3204
|
+
if (!thread) {
|
|
3205
|
+
sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
|
|
3206
|
+
} else {
|
|
3207
|
+
sendJSON(req, res, 200, thread);
|
|
3208
|
+
}
|
|
3209
|
+
} catch (err) {
|
|
3210
|
+
sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
|
|
3211
|
+
}
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// PATCH /threads/{thread_id} - Update thread metadata
|
|
3216
|
+
if (acpThreadMatch && req.method === 'PATCH') {
|
|
3217
|
+
const threadId = threadByIdMatch[1];
|
|
3218
|
+
try {
|
|
3219
|
+
const body = await parseBody(req);
|
|
3220
|
+
const thread = queries.patchThread(threadId, body);
|
|
3221
|
+
sendJSON(req, res, 200, thread);
|
|
3222
|
+
} catch (err) {
|
|
3223
|
+
if (err.message.includes('not found')) {
|
|
3224
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
3225
|
+
} else {
|
|
3226
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
// DELETE /threads/{thread_id} - Delete thread (fail if pending runs exist)
|
|
3233
|
+
if (acpThreadMatch && req.method === 'DELETE') {
|
|
3234
|
+
const threadId = threadByIdMatch[1];
|
|
3235
|
+
try {
|
|
3236
|
+
queries.deleteThread(threadId);
|
|
3237
|
+
res.writeHead(204);
|
|
3238
|
+
res.end();
|
|
3239
|
+
} catch (err) {
|
|
3240
|
+
if (err.message.includes('not found')) {
|
|
3241
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
3242
|
+
} else if (err.message.includes('pending runs')) {
|
|
3243
|
+
sendJSON(req, res, 409, { error: err.message, type: 'conflict' });
|
|
3244
|
+
} else {
|
|
3245
|
+
sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// GET /threads/{thread_id}/history - Get thread state history with pagination
|
|
3252
|
+
const threadHistoryMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/history$/);
|
|
3253
|
+
if (threadHistoryMatch && req.method === 'GET') {
|
|
3254
|
+
const threadId = threadHistoryMatch[1];
|
|
3255
|
+
try {
|
|
3256
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3257
|
+
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
3258
|
+
const before = url.searchParams.get('before') || null;
|
|
3259
|
+
|
|
3260
|
+
// Convert 'before' cursor to offset if needed
|
|
3261
|
+
let offset = 0;
|
|
3262
|
+
if (before) {
|
|
3263
|
+
// Simple cursor-based pagination: before is an offset value
|
|
3264
|
+
offset = parseInt(before, 10);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
const result = queries.getThreadHistory(threadId, limit, offset);
|
|
3268
|
+
|
|
3269
|
+
// Generate next_cursor if there are more results
|
|
3270
|
+
const response = {
|
|
3271
|
+
states: result.states,
|
|
3272
|
+
next_cursor: result.hasMore ? String(offset + limit) : null
|
|
3273
|
+
};
|
|
3274
|
+
|
|
3275
|
+
sendJSON(req, res, 200, response);
|
|
3276
|
+
} catch (err) {
|
|
3277
|
+
if (err.message.includes('not found')) {
|
|
3278
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
3279
|
+
} else {
|
|
3280
|
+
sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// POST /threads/{thread_id}/copy - Copy thread with all states/checkpoints
|
|
3287
|
+
const threadCopyMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/copy$/);
|
|
3288
|
+
if (threadCopyMatch && req.method === 'POST') {
|
|
3289
|
+
const sourceThreadId = threadCopyMatch[1];
|
|
3290
|
+
try {
|
|
3291
|
+
const body = await parseBody(req);
|
|
3292
|
+
const newThread = queries.copyThread(sourceThreadId);
|
|
3293
|
+
|
|
3294
|
+
// Update metadata if provided
|
|
3295
|
+
if (body.metadata) {
|
|
3296
|
+
const updated = queries.patchThread(newThread.thread_id, { metadata: body.metadata });
|
|
3297
|
+
sendJSON(req, res, 201, updated);
|
|
3298
|
+
} else {
|
|
3299
|
+
sendJSON(req, res, 201, newThread);
|
|
3300
|
+
}
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
if (err.message.includes('not found')) {
|
|
3303
|
+
sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
|
|
3304
|
+
} else {
|
|
3305
|
+
sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
2748
3311
|
if (routePath.startsWith('/api/image/')) {
|
|
2749
3312
|
const imagePath = routePath.slice('/api/image/'.length);
|
|
2750
3313
|
const decodedPath = decodeURIComponent(imagePath);
|