@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.
@@ -22,7 +22,9 @@
22
22
  "Bash(bun run build:*)",
23
23
  "Bash(pnpm run build:*)",
24
24
  "WebFetch(domain:geminicli.com)",
25
- "Bash(bun run docs:mcp:*)"
25
+ "Bash(bun run docs:mcp:*)",
26
+ "Bash(bun test:*)",
27
+ "Bash(curl:*)"
26
28
  ]
27
29
  },
28
30
  "enableAllProjectMcpServers": true,
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.0",
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
+ }
@@ -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) {