@desplega.ai/agent-swarm 1.0.2 → 1.2.0
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 +2 -1
- package/.dockerignore +58 -0
- package/.env.docker.example +12 -0
- package/Dockerfile.worker +112 -0
- package/README.md +117 -0
- package/cc-plugin/.claude-plugin/plugin.json +13 -0
- package/cc-plugin/README.md +49 -0
- package/cc-plugin/commands/setup-leader.md +73 -0
- package/cc-plugin/commands/start-worker.md +64 -0
- package/cc-plugin/hooks/hooks.json +71 -0
- package/deploy/DEPLOY.md +3 -0
- package/deploy/agent-swarm.service +3 -2
- package/deploy/install.ts +56 -6
- package/deploy/prod-db.ts +42 -0
- package/deploy/uninstall.ts +4 -2
- package/deploy/update.ts +21 -0
- package/docker-compose.worker.yml +35 -0
- package/docker-entrypoint.sh +62 -0
- package/package.json +9 -2
- package/src/be/db.ts +68 -20
- package/src/cli.tsx +96 -8
- package/src/commands/hook.ts +2 -2
- package/src/commands/setup.tsx +579 -550
- package/src/commands/worker.ts +225 -0
- package/src/hooks/hook.ts +180 -175
- package/src/server.ts +1 -2
- package/src/tools/get-task-details.ts +7 -3
- package/src/tools/join-swarm.ts +23 -11
- package/src/tools/poll-task.ts +34 -2
- package/src/tools/send-task.ts +40 -2
- package/src/tools/store-progress.ts +29 -0
package/src/tools/join-swarm.ts
CHANGED
|
@@ -11,6 +11,10 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
11
11
|
title: "Join the agent swarm",
|
|
12
12
|
description: "Tool for an agent to join the swarm of agents.",
|
|
13
13
|
inputSchema: z.object({
|
|
14
|
+
requestedId: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Requested ID for the agent (overridden by X-Agent-ID header)."),
|
|
14
18
|
lead: z.boolean().default(false).describe("Whether this agent should be the lead."),
|
|
15
19
|
name: z.string().min(1).describe("The name of the agent joining the swarm."),
|
|
16
20
|
}),
|
|
@@ -20,37 +24,45 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
20
24
|
agent: AgentSchema.optional(),
|
|
21
25
|
}),
|
|
22
26
|
},
|
|
23
|
-
async ({ lead, name }, requestInfo, _meta) => {
|
|
27
|
+
async ({ lead, name, requestedId }, requestInfo, _meta) => {
|
|
24
28
|
// Check if agent ID is set
|
|
25
|
-
if (!requestInfo.agentId) {
|
|
29
|
+
if (!requestInfo.agentId && !requestedId) {
|
|
26
30
|
return {
|
|
27
31
|
content: [
|
|
28
32
|
{
|
|
29
33
|
type: "text",
|
|
30
|
-
text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
34
|
+
text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header, or provide a requestedId.',
|
|
31
35
|
},
|
|
32
36
|
],
|
|
33
37
|
structuredContent: {
|
|
34
|
-
yourAgentId: requestInfo.agentId,
|
|
38
|
+
yourAgentId: requestInfo.agentId ?? requestedId,
|
|
35
39
|
success: false,
|
|
36
|
-
message:
|
|
40
|
+
message:
|
|
41
|
+
'Agent ID not found. The MCP client should define the "X-Agent-ID" header, or provide a requestedId.',
|
|
37
42
|
},
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
const agentId = requestInfo.agentId;
|
|
46
|
+
const agentId = requestInfo.agentId ?? requestedId ?? "";
|
|
42
47
|
|
|
43
48
|
try {
|
|
44
49
|
const agentTx = getDb().transaction(() => {
|
|
45
50
|
const agents = getAllAgents();
|
|
46
51
|
|
|
52
|
+
const existingIdAgent = agents.find((agent) => agent.id === agentId);
|
|
53
|
+
|
|
54
|
+
if (existingIdAgent) {
|
|
55
|
+
throw new Error(`Agent with ID "${agentId}" already exists.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
const existingAgent = agents.find((agent) => agent.name === name);
|
|
48
|
-
const existingLead = agents.find((agent) => agent.isLead);
|
|
49
59
|
|
|
50
60
|
if (existingAgent) {
|
|
51
61
|
throw new Error(`Agent with name "${name}" already exists.`);
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
const existingLead = agents.find((agent) => agent.isLead);
|
|
65
|
+
|
|
54
66
|
// If lead is true, demote e
|
|
55
67
|
if (lead && existingLead) {
|
|
56
68
|
throw new Error(
|
|
@@ -72,13 +84,13 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
72
84
|
content: [
|
|
73
85
|
{
|
|
74
86
|
type: "text",
|
|
75
|
-
text: `Successfully joined swarm as agent "${agent.name}" (ID: ${agent.id}).`,
|
|
87
|
+
text: `Successfully joined swarm as ${agent.isLead ? "Lead" : "Worker"} agent "${agent.name}" (ID: ${agent.id}).`,
|
|
76
88
|
},
|
|
77
89
|
],
|
|
78
90
|
structuredContent: {
|
|
79
|
-
yourAgentId:
|
|
91
|
+
yourAgentId: agent.id,
|
|
80
92
|
success: true,
|
|
81
|
-
message: `Successfully joined swarm as agent "${agent.name}" (ID: ${agent.id}).`,
|
|
93
|
+
message: `Successfully joined swarm as ${agent.isLead ? "Lead" : "Worker"} agent "${agent.name}" (ID: ${agent.id}).`,
|
|
82
94
|
agent,
|
|
83
95
|
},
|
|
84
96
|
};
|
|
@@ -91,7 +103,7 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
91
103
|
},
|
|
92
104
|
],
|
|
93
105
|
structuredContent: {
|
|
94
|
-
yourAgentId: requestInfo.agentId,
|
|
106
|
+
yourAgentId: requestInfo.agentId ?? requestedId,
|
|
95
107
|
success: false,
|
|
96
108
|
message: `Failed to join swarm: ${(error as Error).message}`,
|
|
97
109
|
},
|
package/src/tools/poll-task.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { addMinutes } from "date-fns";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
-
import { getDb, getPendingTaskForAgent, startTask } from "@/be/db";
|
|
4
|
+
import { getAgentById, getDb, getPendingTaskForAgent, startTask, updateAgentStatus } from "@/be/db";
|
|
5
5
|
import { createToolRegistrar } from "@/tools/utils";
|
|
6
6
|
import { AgentTaskSchema } from "@/types";
|
|
7
7
|
|
|
@@ -46,13 +46,45 @@ export const registerPollTaskTool = (server: McpServer) => {
|
|
|
46
46
|
const now = new Date();
|
|
47
47
|
const maxTime = addMinutes(now, MAX_POLL_DURATION_MS / 60000);
|
|
48
48
|
|
|
49
|
+
const agent = getAgentById(agentId);
|
|
50
|
+
if (!agent) {
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Agent with ID "${agentId}" not found in the swarm.`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
structuredContent: {
|
|
59
|
+
yourAgentId: requestInfo.agentId,
|
|
60
|
+
success: false,
|
|
61
|
+
message: `Agent with ID "${agentId}" not found in the swarm.`,
|
|
62
|
+
waitedForSeconds: 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
// Poll for pending tasks
|
|
50
68
|
while (new Date() < maxTime) {
|
|
51
69
|
// Fetch and update in a single transaction to avoid race conditions
|
|
52
70
|
const startedTask = getDb().transaction(() => {
|
|
71
|
+
const agentNow = getAgentById(agentId)!;
|
|
72
|
+
|
|
73
|
+
if (agentNow.status !== "busy") {
|
|
74
|
+
updateAgentStatus(agentId, "idle");
|
|
75
|
+
}
|
|
76
|
+
|
|
53
77
|
const pendingTask = getPendingTaskForAgent(agentId);
|
|
54
78
|
if (!pendingTask) return null;
|
|
55
|
-
|
|
79
|
+
|
|
80
|
+
const maybeTask = startTask(pendingTask.id);
|
|
81
|
+
|
|
82
|
+
if (maybeTask) {
|
|
83
|
+
// Update automatically in case the agent forgets xd
|
|
84
|
+
updateAgentStatus(agentId, "busy");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return maybeTask;
|
|
56
88
|
})();
|
|
57
89
|
|
|
58
90
|
if (startedTask) {
|
package/src/tools/send-task.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { createTask, getAgentById, getDb
|
|
3
|
+
import { createTask, getAgentById, getDb } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
5
|
import { AgentTaskSchema } from "@/types";
|
|
6
6
|
|
|
@@ -21,6 +21,38 @@ export const registerSendTaskTool = (server: McpServer) => {
|
|
|
21
21
|
}),
|
|
22
22
|
},
|
|
23
23
|
async ({ agentId, task }, requestInfo, _meta) => {
|
|
24
|
+
if (!requestInfo.agentId) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
structuredContent: {
|
|
33
|
+
yourAgentId: requestInfo.agentId,
|
|
34
|
+
success: false,
|
|
35
|
+
message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (agentId === requestInfo.agentId) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: "Cannot send a task to yourself, are you drunk?",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
structuredContent: {
|
|
49
|
+
yourAgentId: requestInfo.agentId,
|
|
50
|
+
success: false,
|
|
51
|
+
message: "Cannot send a task to yourself, are you drunk?",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
24
56
|
const txn = getDb().transaction(() => {
|
|
25
57
|
const agent = getAgentById(agentId);
|
|
26
58
|
|
|
@@ -31,6 +63,13 @@ export const registerSendTaskTool = (server: McpServer) => {
|
|
|
31
63
|
};
|
|
32
64
|
}
|
|
33
65
|
|
|
66
|
+
if (agent.isLead) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
message: `Cannot assign tasks to the lead agent "${agent.name}", wtf?`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
if (agent.status !== "idle") {
|
|
35
74
|
return {
|
|
36
75
|
success: false,
|
|
@@ -39,7 +78,6 @@ export const registerSendTaskTool = (server: McpServer) => {
|
|
|
39
78
|
}
|
|
40
79
|
|
|
41
80
|
const newTask = createTask(agentId, task);
|
|
42
|
-
updateAgentStatus(agentId, "busy");
|
|
43
81
|
|
|
44
82
|
return {
|
|
45
83
|
success: true,
|
|
@@ -3,6 +3,7 @@ import * as z from "zod";
|
|
|
3
3
|
import {
|
|
4
4
|
completeTask,
|
|
5
5
|
failTask,
|
|
6
|
+
getAgentById,
|
|
6
7
|
getDb,
|
|
7
8
|
getTaskById,
|
|
8
9
|
updateAgentStatus,
|
|
@@ -38,7 +39,32 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
38
39
|
}),
|
|
39
40
|
},
|
|
40
41
|
async ({ taskId, progress, status, output, failureReason }, requestInfo, _meta) => {
|
|
42
|
+
if (!requestInfo.agentId) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
structuredContent: {
|
|
51
|
+
yourAgentId: requestInfo.agentId,
|
|
52
|
+
success: false,
|
|
53
|
+
message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
const txn = getDb().transaction(() => {
|
|
59
|
+
const agent = getAgentById(requestInfo.agentId ?? "");
|
|
60
|
+
|
|
61
|
+
if (!agent) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
message: `Agent with ID "${requestInfo.agentId}" not found in the swarm, register before storing task progress.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
const existingTask = getTaskById(taskId);
|
|
43
69
|
|
|
44
70
|
if (!existingTask) {
|
|
@@ -69,6 +95,9 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
69
95
|
updatedTask = result;
|
|
70
96
|
updateAgentStatus(existingTask.agentId, "idle");
|
|
71
97
|
}
|
|
98
|
+
} else {
|
|
99
|
+
// Keep it busy if just updating progress
|
|
100
|
+
updateAgentStatus(existingTask.agentId, "busy");
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
return {
|