@desplega.ai/agent-swarm 1.10.0 → 1.10.3
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/.claude/settings.local.json +3 -1
- package/Dockerfile.worker +1 -1
- package/package.json +20 -1
- package/src/be/db.ts +101 -0
- package/src/commands/runner.ts +111 -0
- package/src/http.ts +72 -0
- package/src/tests/session-logs.test.ts +388 -0
- package/src/types.ts +14 -0
- package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +1000 -0
- package/ui/src/components/SessionLogPanel.tsx +433 -0
- package/ui/src/components/TaskDetailPanel.tsx +98 -78
- package/ui/src/hooks/queries.ts +9 -0
- package/ui/src/hooks/useAutoScroll.ts +55 -0
- package/ui/src/lib/api.ts +10 -0
- package/ui/src/types/api.ts +15 -0
package/Dockerfile.worker
CHANGED
|
@@ -118,7 +118,7 @@ WORKDIR /workspace
|
|
|
118
118
|
VOLUME ["/logs"]
|
|
119
119
|
|
|
120
120
|
RUN mkdir -p ./personal ./shared
|
|
121
|
-
VOLUME ["/workspace/personal" "/workspace/shared"]
|
|
121
|
+
VOLUME ["/workspace/personal", "/workspace/shared"]
|
|
122
122
|
|
|
123
123
|
# Expose service port for PM2 processes
|
|
124
124
|
EXPOSE 3000
|
package/package.json
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.3",
|
|
4
4
|
"description": "Agent orchestration layer MCP for Claude Code, Codex, Gemini CLI, and more!",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "desplega.ai <contact@desplega.ai>",
|
|
7
|
+
"maintainers": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Taras",
|
|
10
|
+
"email": "t@desplega.ai"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/desplega-ai/agent-swarm.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/desplega-ai/agent-swarm/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/desplega-ai/agent-swarm#readme",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
5
24
|
"module": "src/http.ts",
|
|
6
25
|
"type": "module",
|
|
7
26
|
"bin": {
|
package/src/be/db.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ChannelType,
|
|
14
14
|
Service,
|
|
15
15
|
ServiceStatus,
|
|
16
|
+
SessionLog,
|
|
16
17
|
} from "../types";
|
|
17
18
|
|
|
18
19
|
let db: Database | null = null;
|
|
@@ -149,6 +150,21 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
149
150
|
|
|
150
151
|
CREATE INDEX IF NOT EXISTS idx_services_agentId ON services(agentId);
|
|
151
152
|
CREATE INDEX IF NOT EXISTS idx_services_status ON services(status);
|
|
153
|
+
|
|
154
|
+
-- Session logs table (raw CLI output from runner)
|
|
155
|
+
CREATE TABLE IF NOT EXISTS session_logs (
|
|
156
|
+
id TEXT PRIMARY KEY,
|
|
157
|
+
taskId TEXT,
|
|
158
|
+
sessionId TEXT NOT NULL,
|
|
159
|
+
iteration INTEGER NOT NULL,
|
|
160
|
+
cli TEXT NOT NULL DEFAULT 'claude',
|
|
161
|
+
content TEXT NOT NULL,
|
|
162
|
+
lineNumber INTEGER NOT NULL,
|
|
163
|
+
createdAt TEXT NOT NULL
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_session_logs_taskId ON session_logs(taskId);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_session_logs_sessionId ON session_logs(sessionId);
|
|
152
168
|
`);
|
|
153
169
|
|
|
154
170
|
// Seed default general channel if it doesn't exist
|
|
@@ -1919,3 +1935,88 @@ export function deleteServicesByAgentId(agentId: string): number {
|
|
|
1919
1935
|
const result = getDb().run("DELETE FROM services WHERE agentId = ?", [agentId]);
|
|
1920
1936
|
return result.changes;
|
|
1921
1937
|
}
|
|
1938
|
+
|
|
1939
|
+
// ============================================================================
|
|
1940
|
+
// Session Log Operations (raw CLI output)
|
|
1941
|
+
// ============================================================================
|
|
1942
|
+
|
|
1943
|
+
type SessionLogRow = {
|
|
1944
|
+
id: string;
|
|
1945
|
+
taskId: string | null;
|
|
1946
|
+
sessionId: string;
|
|
1947
|
+
iteration: number;
|
|
1948
|
+
cli: string;
|
|
1949
|
+
content: string;
|
|
1950
|
+
lineNumber: number;
|
|
1951
|
+
createdAt: string;
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
function rowToSessionLog(row: SessionLogRow): SessionLog {
|
|
1955
|
+
return {
|
|
1956
|
+
id: row.id,
|
|
1957
|
+
taskId: row.taskId ?? undefined,
|
|
1958
|
+
sessionId: row.sessionId,
|
|
1959
|
+
iteration: row.iteration,
|
|
1960
|
+
cli: row.cli,
|
|
1961
|
+
content: row.content,
|
|
1962
|
+
lineNumber: row.lineNumber,
|
|
1963
|
+
createdAt: row.createdAt,
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
export const sessionLogQueries = {
|
|
1968
|
+
insert: () =>
|
|
1969
|
+
getDb().prepare<SessionLogRow, [string, string | null, string, number, string, string, number]>(
|
|
1970
|
+
`INSERT INTO session_logs (id, taskId, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
1971
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
|
|
1972
|
+
),
|
|
1973
|
+
|
|
1974
|
+
insertBatch: () =>
|
|
1975
|
+
getDb().prepare<null, [string, string | null, string, number, string, string, number]>(
|
|
1976
|
+
`INSERT INTO session_logs (id, taskId, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
1977
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
|
|
1978
|
+
),
|
|
1979
|
+
|
|
1980
|
+
getByTaskId: () =>
|
|
1981
|
+
getDb().prepare<SessionLogRow, [string]>(
|
|
1982
|
+
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY iteration ASC, lineNumber ASC",
|
|
1983
|
+
),
|
|
1984
|
+
|
|
1985
|
+
getBySessionId: () =>
|
|
1986
|
+
getDb().prepare<SessionLogRow, [string, number]>(
|
|
1987
|
+
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY lineNumber ASC",
|
|
1988
|
+
),
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
export function createSessionLogs(logs: {
|
|
1992
|
+
taskId?: string;
|
|
1993
|
+
sessionId: string;
|
|
1994
|
+
iteration: number;
|
|
1995
|
+
cli: string;
|
|
1996
|
+
lines: string[];
|
|
1997
|
+
}): void {
|
|
1998
|
+
const stmt = sessionLogQueries.insertBatch();
|
|
1999
|
+
getDb().transaction(() => {
|
|
2000
|
+
for (let i = 0; i < logs.lines.length; i++) {
|
|
2001
|
+
const line = logs.lines[i];
|
|
2002
|
+
if (line === undefined) continue;
|
|
2003
|
+
stmt.run(
|
|
2004
|
+
crypto.randomUUID(),
|
|
2005
|
+
logs.taskId ?? null,
|
|
2006
|
+
logs.sessionId,
|
|
2007
|
+
logs.iteration,
|
|
2008
|
+
logs.cli,
|
|
2009
|
+
line,
|
|
2010
|
+
i,
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
})();
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
export function getSessionLogsByTaskId(taskId: string): SessionLog[] {
|
|
2017
|
+
return sessionLogQueries.getByTaskId().all(taskId).map(rowToSessionLog);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
export function getSessionLogsBySession(sessionId: string, iteration: number): SessionLog[] {
|
|
2021
|
+
return sessionLogQueries.getBySessionId().all(sessionId, iteration).map(rowToSessionLog);
|
|
2022
|
+
}
|
package/src/commands/runner.ts
CHANGED
|
@@ -53,6 +53,71 @@ interface RunClaudeIterationOptions {
|
|
|
53
53
|
systemPrompt?: string;
|
|
54
54
|
additionalArgs?: string[];
|
|
55
55
|
role: string;
|
|
56
|
+
// New fields for log streaming
|
|
57
|
+
apiUrl?: string;
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
agentId?: string;
|
|
60
|
+
sessionId?: string;
|
|
61
|
+
iteration?: number;
|
|
62
|
+
taskId?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Buffer for session logs */
|
|
66
|
+
interface LogBuffer {
|
|
67
|
+
lines: string[];
|
|
68
|
+
lastFlush: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Configuration for log streaming */
|
|
72
|
+
const LOG_BUFFER_SIZE = 50; // Flush after this many lines
|
|
73
|
+
const LOG_FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
|
|
74
|
+
|
|
75
|
+
/** Push buffered logs to the API */
|
|
76
|
+
async function flushLogBuffer(
|
|
77
|
+
buffer: LogBuffer,
|
|
78
|
+
opts: {
|
|
79
|
+
apiUrl: string;
|
|
80
|
+
apiKey: string;
|
|
81
|
+
agentId: string;
|
|
82
|
+
sessionId: string;
|
|
83
|
+
iteration: number;
|
|
84
|
+
taskId?: string;
|
|
85
|
+
cli?: string;
|
|
86
|
+
},
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
if (buffer.lines.length === 0) return;
|
|
89
|
+
|
|
90
|
+
const headers: Record<string, string> = {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"X-Agent-ID": opts.agentId,
|
|
93
|
+
};
|
|
94
|
+
if (opts.apiKey) {
|
|
95
|
+
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(`${opts.apiUrl}/api/session-logs`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers,
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
sessionId: opts.sessionId,
|
|
104
|
+
iteration: opts.iteration,
|
|
105
|
+
taskId: opts.taskId,
|
|
106
|
+
cli: opts.cli || "claude",
|
|
107
|
+
lines: buffer.lines,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
console.warn(`[runner] Failed to push logs: ${response.status}`);
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn(`[runner] Error pushing logs: ${error}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clear buffer after flush
|
|
119
|
+
buffer.lines = [];
|
|
120
|
+
buffer.lastFlush = Date.now();
|
|
56
121
|
}
|
|
57
122
|
|
|
58
123
|
/** Trigger types returned by the poll API */
|
|
@@ -206,6 +271,10 @@ async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<numb
|
|
|
206
271
|
|
|
207
272
|
const stdoutPromise = (async () => {
|
|
208
273
|
if (proc.stdout) {
|
|
274
|
+
// Initialize log buffer for API streaming
|
|
275
|
+
const logBuffer: LogBuffer = { lines: [], lastFlush: Date.now() };
|
|
276
|
+
const shouldStream = opts.apiUrl && opts.sessionId && opts.iteration;
|
|
277
|
+
|
|
209
278
|
for await (const chunk of proc.stdout) {
|
|
210
279
|
stdoutChunks++;
|
|
211
280
|
const text = new TextDecoder().decode(chunk);
|
|
@@ -214,8 +283,43 @@ async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<numb
|
|
|
214
283
|
const lines = text.split("\n");
|
|
215
284
|
for (const line of lines) {
|
|
216
285
|
prettyPrintLine(line, role);
|
|
286
|
+
|
|
287
|
+
// Buffer non-empty lines for API streaming
|
|
288
|
+
if (shouldStream && line.trim()) {
|
|
289
|
+
logBuffer.lines.push(line.trim());
|
|
290
|
+
|
|
291
|
+
// Check if we should flush (buffer full or time elapsed)
|
|
292
|
+
const shouldFlush =
|
|
293
|
+
logBuffer.lines.length >= LOG_BUFFER_SIZE ||
|
|
294
|
+
Date.now() - logBuffer.lastFlush >= LOG_FLUSH_INTERVAL_MS;
|
|
295
|
+
|
|
296
|
+
if (shouldFlush) {
|
|
297
|
+
await flushLogBuffer(logBuffer, {
|
|
298
|
+
apiUrl: opts.apiUrl!,
|
|
299
|
+
apiKey: opts.apiKey || "",
|
|
300
|
+
agentId: opts.agentId || "",
|
|
301
|
+
sessionId: opts.sessionId!,
|
|
302
|
+
iteration: opts.iteration!,
|
|
303
|
+
taskId: opts.taskId,
|
|
304
|
+
cli: "claude",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
217
308
|
}
|
|
218
309
|
}
|
|
310
|
+
|
|
311
|
+
// Final flush for remaining buffered logs
|
|
312
|
+
if (shouldStream && logBuffer.lines.length > 0) {
|
|
313
|
+
await flushLogBuffer(logBuffer, {
|
|
314
|
+
apiUrl: opts.apiUrl!,
|
|
315
|
+
apiKey: opts.apiKey || "",
|
|
316
|
+
agentId: opts.agentId || "",
|
|
317
|
+
sessionId: opts.sessionId!,
|
|
318
|
+
iteration: opts.iteration!,
|
|
319
|
+
taskId: opts.taskId,
|
|
320
|
+
cli: "claude",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
219
323
|
}
|
|
220
324
|
})();
|
|
221
325
|
|
|
@@ -398,6 +502,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
398
502
|
systemPrompt: resolvedSystemPrompt,
|
|
399
503
|
additionalArgs: opts.additionalArgs,
|
|
400
504
|
role,
|
|
505
|
+
// Add streaming options
|
|
506
|
+
apiUrl,
|
|
507
|
+
apiKey,
|
|
508
|
+
agentId,
|
|
509
|
+
sessionId,
|
|
510
|
+
iteration,
|
|
511
|
+
taskId: trigger.taskId,
|
|
401
512
|
});
|
|
402
513
|
|
|
403
514
|
if (exitCode !== 0) {
|
package/src/http.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createServer } from "@/server";
|
|
|
11
11
|
import {
|
|
12
12
|
closeDb,
|
|
13
13
|
createAgent,
|
|
14
|
+
createSessionLogs,
|
|
14
15
|
getAgentById,
|
|
15
16
|
getAgentWithTasks,
|
|
16
17
|
getAllAgents,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
getOfferedTasksForAgent,
|
|
29
30
|
getPendingTaskForAgent,
|
|
30
31
|
getServicesByAgentId,
|
|
32
|
+
getSessionLogsByTaskId,
|
|
31
33
|
getTaskById,
|
|
32
34
|
getUnassignedTasksCount,
|
|
33
35
|
postMessage,
|
|
@@ -354,6 +356,76 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
354
356
|
return;
|
|
355
357
|
}
|
|
356
358
|
|
|
359
|
+
// POST /api/session-logs - Store session logs (batch)
|
|
360
|
+
if (req.method === "POST" && pathSegments[0] === "api" && pathSegments[1] === "session-logs") {
|
|
361
|
+
// Parse request body
|
|
362
|
+
const chunks: Buffer[] = [];
|
|
363
|
+
for await (const chunk of req) {
|
|
364
|
+
chunks.push(chunk);
|
|
365
|
+
}
|
|
366
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
367
|
+
|
|
368
|
+
// Validate required fields
|
|
369
|
+
if (!body.sessionId || typeof body.sessionId !== "string") {
|
|
370
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
371
|
+
res.end(JSON.stringify({ error: "Missing or invalid 'sessionId' field" }));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (typeof body.iteration !== "number" || body.iteration < 1) {
|
|
376
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
377
|
+
res.end(JSON.stringify({ error: "Missing or invalid 'iteration' field" }));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!Array.isArray(body.lines) || body.lines.length === 0) {
|
|
382
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
383
|
+
res.end(JSON.stringify({ error: "Missing or invalid 'lines' array" }));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
createSessionLogs({
|
|
389
|
+
taskId: body.taskId || undefined,
|
|
390
|
+
sessionId: body.sessionId,
|
|
391
|
+
iteration: body.iteration,
|
|
392
|
+
cli: body.cli || "claude",
|
|
393
|
+
lines: body.lines,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
397
|
+
res.end(JSON.stringify({ success: true, count: body.lines.length }));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error("[HTTP] Failed to create session logs:", error);
|
|
400
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
401
|
+
res.end(JSON.stringify({ error: "Failed to store session logs" }));
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// GET /api/tasks/:id/session-logs - Get session logs for a task
|
|
407
|
+
if (
|
|
408
|
+
req.method === "GET" &&
|
|
409
|
+
pathSegments[0] === "api" &&
|
|
410
|
+
pathSegments[1] === "tasks" &&
|
|
411
|
+
pathSegments[2] &&
|
|
412
|
+
pathSegments[3] === "session-logs"
|
|
413
|
+
) {
|
|
414
|
+
const taskId = pathSegments[2];
|
|
415
|
+
const task = getTaskById(taskId);
|
|
416
|
+
|
|
417
|
+
if (!task) {
|
|
418
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
419
|
+
res.end(JSON.stringify({ error: "Task not found" }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const logs = getSessionLogsByTaskId(taskId);
|
|
424
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
425
|
+
res.end(JSON.stringify({ logs }));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
357
429
|
// GET /ecosystem - Generate PM2 ecosystem config for agent's services
|
|
358
430
|
if (req.method === "GET" && req.url === "/ecosystem") {
|
|
359
431
|
if (!myAgentId) {
|