@desplega.ai/agent-swarm 1.53.0 → 1.53.1

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/README.md CHANGED
@@ -58,6 +58,8 @@ Agent Swarm lets you run a team of AI coding agents that coordinate autonomously
58
58
  - **Onboarding wizard** — Interactive CLI wizard (`agent-swarm onboard`) to set up a new swarm from scratch with presets, credential collection, and docker-compose generation
59
59
  - **Skill system** — Reusable procedural knowledge: create, install, publish, and sync skills from GitHub with scope resolution (agent → swarm → global)
60
60
  - **Human-in-the-Loop** — Workflow nodes that pause for human approval or input, with a dashboard UI for reviewing and responding to requests
61
+ - **MCP server management** — Register, install, and manage MCP servers for agents with scope cascade (agent → swarm → global) and auto-injection into worker containers
62
+ - **Context usage tracking** — Monitor context window utilization and compaction events per task with visual indicators in the dashboard
61
63
 
62
64
  ## Quick Start
63
65
 
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.52.1",
5
+ "version": "1.53.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.53.0",
3
+ "version": "1.53.1",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -21,11 +21,17 @@ Once you have the task details, you should:
21
21
  - Use `memory-get` on any highly relevant results to get full details
22
22
  - This step is NOT optional. Past learnings compound your effectiveness.
23
23
  <!-- /claude-only -->
24
- 2. Figure out if you need to use any of the available commands to help you with your work (see below for available commands)
25
- 2. Use the `/todos` command to add a new todo item indicating you are starting to work on the task (e.g. "Work on task XXX: <short description>"). This will help on restarts, as it will be easier to remember what you were doing.
26
- 3. Call `store-progress` tool to mark the task as "in-progress" with a progress set to something like "Starting work on the task XXX, blah blah". Additionally use `/swarm-chat` command to notify the swarm, human and lead when applicable. Do not be too verbose, nor spammy.
27
- 4. Start working on the task, providing updates as needed by calling `store-progress` tool, use the `progress` field to indicate what you are doing.
28
- 5. Once you either done or in a dead-end, see the "Completion" section below.
24
+ 2. **Check Installed Skills (REQUIRED):** Before researching or implementing, review your "Installed Skills" section in the system prompt:
25
+ - If any skill's description or trigger matches this task, invoke it via the `Skill` tool BEFORE doing manual research
26
+ - Skills contain pre-built, tested procedures that save context window and cost
27
+ - Example: task involves Linear use `linear-interaction` skill, task involves email use `agentmail-sending` skill
28
+ - Only proceed to manual research/web search if NO installed skill covers the task
29
+ - This step is NOT optional. Skipping it wastes context and money.
30
+ 3. Figure out if you need to use any of the available commands to help you with your work (see below for available commands)
31
+ 4. Use the `/todos` command to add a new todo item indicating you are starting to work on the task (e.g. "Work on task XXX: <short description>"). This will help on restarts, as it will be easier to remember what you were doing.
32
+ 5. Call `store-progress` tool to mark the task as "in-progress" with a progress set to something like "Starting work on the task XXX, blah blah". Additionally use `/swarm-chat` command to notify the swarm, human and lead when applicable. Do not be too verbose, nor spammy.
33
+ 6. Start working on the task, providing updates as needed by calling `store-progress` tool, use the `progress` field to indicate what you are doing.
34
+ 7. Once you either done or in a dead-end, see the "Completion" section below.
29
35
 
30
36
  ### Available commands
31
37
 
@@ -13,11 +13,17 @@ Once you get a task assigned, you need to immediately start working on it. To do
13
13
 
14
14
  Once you have the task details, you should:
15
15
 
16
- 1. Figure out if you need to perform any research or planning before starting (see below)
17
- 2. Use the `/skill:todos` to add a new todo item indicating you are starting to work on the task (e.g. "Work on task XXX: <short description>"). This will help on restarts, as it will be easier to remember what you were doing.
18
- 3. Call `store-progress` tool to mark the task as "in-progress" with a progress set to something like "Starting work on the task XXX, blah blah". Additionally use `/skill:swarm-chat` to notify the swarm, human and lead when applicable. Do not be too verbose, nor spammy.
19
- 4. Start working on the task, providing updates as needed by calling `store-progress` tool, use the `progress` field to indicate what you are doing.
20
- 5. Once you either done or in a dead-end, see the "Completion" section below.
16
+ 1. **Check Installed Skills (REQUIRED):** Before researching or implementing, review your "Installed Skills" section in the system prompt:
17
+ - If any skill's description or trigger matches this task, invoke it via the `Skill` tool BEFORE doing manual research
18
+ - Skills contain pre-built, tested procedures that save context window and cost
19
+ - Example: task involves Linear use `linear-interaction` skill, task involves email use `agentmail-sending` skill
20
+ - Only proceed to manual research/web search if NO installed skill covers the task
21
+ - This step is NOT optional. Skipping it wastes context and money.
22
+ 2. Figure out if you need to perform any research or planning before starting (see below)
23
+ 3. Use the `/skill:todos` to add a new todo item indicating you are starting to work on the task (e.g. "Work on task XXX: <short description>"). This will help on restarts, as it will be easier to remember what you were doing.
24
+ 4. Call `store-progress` tool to mark the task as "in-progress" with a progress set to something like "Starting work on the task XXX, blah blah". Additionally use `/skill:swarm-chat` to notify the swarm, human and lead when applicable. Do not be too verbose, nor spammy.
25
+ 5. Start working on the task, providing updates as needed by calling `store-progress` tool, use the `progress` field to indicate what you are doing.
26
+ 6. Once you either done or in a dead-end, see the "Completion" section below.
21
27
 
22
28
  ### Research and Planning
23
29
 
package/src/be/db.ts CHANGED
@@ -7855,6 +7855,13 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
7855
7855
  .run(input.contextPercent, input.taskId);
7856
7856
  }
7857
7857
 
7858
+ // Keep totalContextTokensUsed up to date with the latest known value
7859
+ if (input.contextUsedTokens != null) {
7860
+ getDb()
7861
+ .prepare("UPDATE agent_tasks SET totalContextTokensUsed = ? WHERE id = ?")
7862
+ .run(input.contextUsedTokens, input.taskId);
7863
+ }
7864
+
7858
7865
  if (input.eventType === "compaction") {
7859
7866
  getDb()
7860
7867
  .prepare(
@@ -7863,13 +7870,10 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
7863
7870
  .run(input.taskId);
7864
7871
  }
7865
7872
 
7866
- if (input.eventType === "completion") {
7873
+ if (input.eventType === "completion" && input.contextTotalTokens != null) {
7867
7874
  getDb()
7868
- .prepare(
7869
- `UPDATE agent_tasks SET totalContextTokensUsed = ?, contextWindowSize = ?
7870
- WHERE id = ?`,
7871
- )
7872
- .run(input.contextUsedTokens ?? null, input.contextTotalTokens ?? null, input.taskId);
7875
+ .prepare("UPDATE agent_tasks SET contextWindowSize = ? WHERE id = ?")
7876
+ .run(input.contextTotalTokens, input.taskId);
7873
7877
  }
7874
7878
 
7875
7879
  return {
@@ -271,18 +271,40 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
271
271
 
272
272
  // Check if we already track this issue
273
273
  const existing = getTrackerSyncByExternalId("linear", "task", issueId);
274
+ const sessionId = agentSession ? String(agentSession.id ?? "") : "";
275
+
274
276
  if (existing) {
277
+ const existingTask = getTaskById(existing.swarmId);
278
+
279
+ // If the task is still active, acknowledge the new session but don't create a duplicate
280
+ if (existingTask && !["completed", "failed", "cancelled"].includes(existingTask.status)) {
281
+ console.log(
282
+ `[Linear Sync] Issue ${issueIdentifier} already tracked as active task ${existing.swarmId}, skipping`,
283
+ );
284
+ if (sessionId) {
285
+ taskSessionMap.set(existingTask.id, sessionId);
286
+ acknowledgeAgentSession(
287
+ sessionId,
288
+ `This issue is already being worked on (task ${existing.swarmId}).`,
289
+ ).catch((err) => {
290
+ console.error("[Linear Sync] Failed to acknowledge duplicate AgentSession:", err);
291
+ });
292
+ }
293
+ return;
294
+ }
295
+
296
+ // Task is done/failed/cancelled — create a follow-up task below
275
297
  console.log(
276
- `[Linear Sync] Issue ${issueIdentifier} already tracked as task ${existing.swarmId}, skipping`,
298
+ `[Linear Sync] Issue ${issueIdentifier} was tracked as ${existingTask?.status ?? "unknown"} task ${existing.swarmId}, creating follow-up`,
277
299
  );
278
- return;
279
300
  }
280
301
 
281
302
  const lead = findLeadAgent();
282
303
 
283
304
  const sessionSection = sessionUrl ? `\nSession: ${sessionUrl}` : "";
284
305
  const descriptionSection = issueDescription ? `\nDescription:\n${issueDescription}\n` : "";
285
- const assignedResult = resolveTemplate("linear.issue.assigned", {
306
+ const templateName = existing ? "linear.issue.reassigned" : "linear.issue.assigned";
307
+ const templateResult = resolveTemplate(templateName, {
286
308
  issue_identifier: issueIdentifier,
287
309
  issue_title: issueTitle,
288
310
  issue_url: issueUrl,
@@ -290,16 +312,21 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
290
312
  description_section: descriptionSection,
291
313
  });
292
314
 
293
- if (assignedResult.skipped) {
315
+ if (templateResult.skipped) {
294
316
  return;
295
317
  }
296
318
 
297
- const task = createTaskExtended(assignedResult.text, {
319
+ const task = createTaskExtended(templateResult.text, {
298
320
  agentId: lead?.id ?? "",
299
321
  source: "linear",
300
322
  taskType: "linear-issue",
301
323
  });
302
324
 
325
+ // Delete old tracker_sync before creating new one (UNIQUE constraint)
326
+ if (existing) {
327
+ deleteTrackerSync(existing.id);
328
+ }
329
+
303
330
  createTrackerSync({
304
331
  provider: "linear",
305
332
  entityType: "task",
@@ -313,15 +340,14 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
313
340
  });
314
341
 
315
342
  // Track the AgentSession so outbound sync can post activities to it
316
- const sessionId = agentSession ? String(agentSession.id ?? "") : "";
317
343
  if (sessionId) {
318
344
  taskSessionMap.set(task.id, sessionId);
319
345
 
320
346
  // Acknowledge the AgentSession (pending → active)
321
- acknowledgeAgentSession(
322
- sessionId,
323
- `Task received by Agent Swarm (${task.id}). Processing...`,
324
- ).catch((err) => {
347
+ const ackMsg = existing
348
+ ? `Follow-up task created (${task.id}). Previous task was ${existing.swarmId}. Processing...`
349
+ : `Task received by Agent Swarm (${task.id}). Processing...`;
350
+ acknowledgeAgentSession(sessionId, ackMsg).catch((err) => {
325
351
  console.error("[Linear Sync] Failed to acknowledge AgentSession:", err);
326
352
  });
327
353
 
@@ -336,8 +362,9 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
336
362
  }
337
363
  }
338
364
 
365
+ const action = existing ? "follow-up" : "new";
339
366
  console.log(
340
- `[Linear Sync] Created task ${task.id} for ${issueIdentifier} -> ${lead?.name ?? "unassigned"}`,
367
+ `[Linear Sync] Created ${action} task ${task.id} for ${issueIdentifier} -> ${lead?.name ?? "unassigned"}`,
341
368
  );
342
369
  }
343
370
 
@@ -27,6 +27,23 @@ URL: {{issue_url}}{{session_section}}
27
27
  category: "event",
28
28
  });
29
29
 
30
+ registerTemplate({
31
+ eventType: "linear.issue.reassigned",
32
+ header: "[Linear {{issue_identifier}}] Re-assigned: {{issue_title}}",
33
+ defaultBody: `Source: Linear (Agent Session re-assignment)
34
+ URL: {{issue_url}}{{session_section}}
35
+ {{description_section}}
36
+ This issue was previously tracked but the original task has completed. A new task has been created to handle the re-assignment.`,
37
+ variables: [
38
+ { name: "issue_identifier", description: "Linear issue identifier (e.g. ENG-123)" },
39
+ { name: "issue_title", description: "Issue title" },
40
+ { name: "issue_url", description: "Issue URL on Linear" },
41
+ { name: "session_section", description: "Session URL line or empty string" },
42
+ { name: "description_section", description: "Description section or empty string" },
43
+ ],
44
+ category: "event",
45
+ });
46
+
30
47
  registerTemplate({
31
48
  eventType: "linear.issue.followup",
32
49
  header: "[Linear {{issue_identifier}}] Follow-up: {{issue_title}}",
@@ -0,0 +1,127 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createContextSnapshot,
7
+ createTaskExtended,
8
+ getContextSnapshotsByTaskId,
9
+ getContextSummaryByTaskId,
10
+ initDb,
11
+ } from "../be/db";
12
+
13
+ const TEST_DB_PATH = "./test-context-snapshot.sqlite";
14
+
15
+ describe("Context Snapshots", () => {
16
+ const agentId = "aaaa0000-0000-4000-8000-000000000001";
17
+ const sessionId = "sess-001";
18
+ let taskId: string;
19
+
20
+ beforeAll(async () => {
21
+ for (const suffix of ["", "-wal", "-shm"]) {
22
+ try {
23
+ await unlink(TEST_DB_PATH + suffix);
24
+ } catch {
25
+ // File doesn't exist
26
+ }
27
+ }
28
+
29
+ initDb(TEST_DB_PATH);
30
+ createAgent({ id: agentId, name: "Test Worker", isLead: false, status: "idle" });
31
+ const task = createTaskExtended("Test task for context snapshots", {
32
+ agentId,
33
+ source: "mcp",
34
+ });
35
+ taskId = task.id;
36
+ });
37
+
38
+ afterAll(async () => {
39
+ closeDb();
40
+ for (const suffix of ["", "-wal", "-shm"]) {
41
+ try {
42
+ await unlink(TEST_DB_PATH + suffix);
43
+ } catch {
44
+ // ignore
45
+ }
46
+ }
47
+ });
48
+
49
+ test("completion snapshot without contextUsedTokens preserves last known usage", () => {
50
+ // Simulate progress snapshots during task execution
51
+ createContextSnapshot({
52
+ taskId,
53
+ agentId,
54
+ sessionId,
55
+ eventType: "progress",
56
+ contextUsedTokens: 50000,
57
+ contextTotalTokens: 200000,
58
+ contextPercent: 25,
59
+ });
60
+
61
+ createContextSnapshot({
62
+ taskId,
63
+ agentId,
64
+ sessionId,
65
+ eventType: "progress",
66
+ contextUsedTokens: 80000,
67
+ contextTotalTokens: 200000,
68
+ contextPercent: 40,
69
+ });
70
+
71
+ // Simulate completion snapshot — runner doesn't have contextUsedTokens at session end
72
+ createContextSnapshot({
73
+ taskId,
74
+ agentId,
75
+ sessionId,
76
+ eventType: "completion",
77
+ // No contextUsedTokens or contextPercent — this is the bug scenario
78
+ contextTotalTokens: 200000,
79
+ cumulativeInputTokens: 100000,
80
+ cumulativeOutputTokens: 20000,
81
+ });
82
+
83
+ // The summary should preserve the last known context usage, not null/0
84
+ const summary = getContextSummaryByTaskId(taskId);
85
+ expect(summary.totalContextTokensUsed).toBe(80000);
86
+ expect(summary.contextWindowSize).toBe(200000);
87
+ expect(summary.peakContextPercent).toBe(40);
88
+ });
89
+
90
+ test("completion snapshot with contextUsedTokens uses provided value", () => {
91
+ // Create a second task for an isolated test
92
+ const task2 = createTaskExtended("Test task 2", { agentId, source: "mcp" });
93
+
94
+ createContextSnapshot({
95
+ taskId: task2.id,
96
+ agentId,
97
+ sessionId,
98
+ eventType: "progress",
99
+ contextUsedTokens: 50000,
100
+ contextTotalTokens: 200000,
101
+ contextPercent: 25,
102
+ });
103
+
104
+ // Completion with explicit contextUsedTokens should use that value
105
+ createContextSnapshot({
106
+ taskId: task2.id,
107
+ agentId,
108
+ sessionId,
109
+ eventType: "completion",
110
+ contextUsedTokens: 60000,
111
+ contextTotalTokens: 200000,
112
+ contextPercent: 30,
113
+ });
114
+
115
+ const summary = getContextSummaryByTaskId(task2.id);
116
+ expect(summary.totalContextTokensUsed).toBe(60000);
117
+ expect(summary.contextWindowSize).toBe(200000);
118
+ });
119
+
120
+ test("snapshots are returned in chronological order", () => {
121
+ const snapshots = getContextSnapshotsByTaskId(taskId);
122
+ expect(snapshots.length).toBe(3);
123
+ expect(snapshots[0].eventType).toBe("progress");
124
+ expect(snapshots[1].eventType).toBe("progress");
125
+ expect(snapshots[2].eventType).toBe("completion");
126
+ });
127
+ });
@@ -207,7 +207,7 @@ describe("handleAgentSessionEvent", () => {
207
207
  expect(task!.task).toContain("Fix login bug");
208
208
  });
209
209
 
210
- test("skips duplicate issue (already tracked)", async () => {
210
+ test("skips when already-tracked issue has an active task", async () => {
211
211
  const event = {
212
212
  type: "AgentSession",
213
213
  action: "create",
@@ -221,11 +221,112 @@ describe("handleAgentSessionEvent", () => {
221
221
  },
222
222
  };
223
223
 
224
- // Should not throw or create a second tracker_sync
224
+ // The task from the previous test is still pending (active)
225
+ const syncBefore = getTrackerSyncByExternalId("linear", "task", "issue-agent-session-001");
226
+ expect(syncBefore).not.toBeNull();
227
+ const originalSwarmId = syncBefore!.swarmId;
228
+
225
229
  await handleAgentSessionEvent(event);
226
- // Just verify it didn't throw — the existing sync is still there
227
- const sync = getTrackerSyncByExternalId("linear", "task", "issue-agent-session-001");
230
+
231
+ // Sync should still point to the same task (no follow-up created)
232
+ const syncAfter = getTrackerSyncByExternalId("linear", "task", "issue-agent-session-001");
233
+ expect(syncAfter).not.toBeNull();
234
+ expect(syncAfter!.swarmId).toBe(originalSwarmId);
235
+ });
236
+
237
+ test("creates follow-up task when already-tracked issue has a completed task", async () => {
238
+ // Create a task and tracker_sync, then mark the task as completed
239
+ const originalTask = createTaskExtended("Original linear task", {
240
+ source: "linear",
241
+ taskType: "linear-issue",
242
+ });
243
+ const { getDb } = await import("../be/db");
244
+ getDb().query("UPDATE agent_tasks SET status = 'completed' WHERE id = ?").run(originalTask.id);
245
+
246
+ createTrackerSync({
247
+ provider: "linear",
248
+ entityType: "task",
249
+ providerEntityType: "Issue",
250
+ swarmId: originalTask.id,
251
+ externalId: "issue-followup-completed-001",
252
+ externalIdentifier: "ENG-150",
253
+ externalUrl: "https://linear.app/team/issue/ENG-150",
254
+ lastSyncOrigin: "external",
255
+ syncDirection: "inbound",
256
+ });
257
+
258
+ const event = {
259
+ type: "AgentSession",
260
+ action: "create",
261
+ data: {
262
+ issue: {
263
+ id: "issue-followup-completed-001",
264
+ identifier: "ENG-150",
265
+ title: "Fix login bug again",
266
+ url: "https://linear.app/team/issue/ENG-150",
267
+ description: "Still broken",
268
+ },
269
+ },
270
+ };
271
+
272
+ await handleAgentSessionEvent(event);
273
+
274
+ // tracker_sync should now point to a NEW task
275
+ const sync = getTrackerSyncByExternalId("linear", "task", "issue-followup-completed-001");
228
276
  expect(sync).not.toBeNull();
277
+ expect(sync!.swarmId).not.toBe(originalTask.id);
278
+
279
+ // New task should exist and use the reassigned template
280
+ const followupTask = getTaskById(sync!.swarmId);
281
+ expect(followupTask).not.toBeNull();
282
+ expect(followupTask!.source).toBe("linear");
283
+ expect(followupTask!.taskType).toBe("linear-issue");
284
+ expect(followupTask!.task).toContain("[Linear ENG-150]");
285
+ expect(followupTask!.task).toContain("Re-assigned");
286
+ });
287
+
288
+ test("creates follow-up task when already-tracked issue has a failed task", async () => {
289
+ const originalTask = createTaskExtended("Failed linear task", {
290
+ source: "linear",
291
+ taskType: "linear-issue",
292
+ });
293
+ const { getDb } = await import("../be/db");
294
+ getDb().query("UPDATE agent_tasks SET status = 'failed' WHERE id = ?").run(originalTask.id);
295
+
296
+ createTrackerSync({
297
+ provider: "linear",
298
+ entityType: "task",
299
+ providerEntityType: "Issue",
300
+ swarmId: originalTask.id,
301
+ externalId: "issue-followup-failed-001",
302
+ externalIdentifier: "ENG-151",
303
+ externalUrl: "https://linear.app/team/issue/ENG-151",
304
+ lastSyncOrigin: "external",
305
+ syncDirection: "inbound",
306
+ });
307
+
308
+ const event = {
309
+ type: "AgentSession",
310
+ action: "create",
311
+ data: {
312
+ issue: {
313
+ id: "issue-followup-failed-001",
314
+ identifier: "ENG-151",
315
+ title: "Deploy pipeline fix",
316
+ url: "https://linear.app/team/issue/ENG-151",
317
+ },
318
+ },
319
+ };
320
+
321
+ await handleAgentSessionEvent(event);
322
+
323
+ const sync = getTrackerSyncByExternalId("linear", "task", "issue-followup-failed-001");
324
+ expect(sync).not.toBeNull();
325
+ expect(sync!.swarmId).not.toBe(originalTask.id);
326
+
327
+ const followupTask = getTaskById(sync!.swarmId);
328
+ expect(followupTask).not.toBeNull();
329
+ expect(followupTask!.source).toBe("linear");
229
330
  });
230
331
 
231
332
  test("skips event with no issue data", async () => {