@desplega.ai/agent-swarm 1.2.1 → 1.9.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 +20 -1
- package/.env.docker.example +22 -1
- package/.env.example +17 -0
- package/.github/workflows/docker-publish.yml +92 -0
- package/CONTRIBUTING.md +270 -0
- package/DEPLOYMENT.md +391 -0
- package/Dockerfile.worker +29 -1
- package/FAQ.md +19 -0
- package/LICENSE +21 -0
- package/MCP.md +249 -0
- package/README.md +103 -207
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/docker-compose.example.yml +137 -0
- package/docker-entrypoint.sh +223 -7
- package/package.json +8 -3
- package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/codebase-analyzer.md +143 -0
- package/plugin/agents/codebase-locator.md +122 -0
- package/plugin/agents/codebase-pattern-finder.md +227 -0
- package/plugin/agents/web-search-researcher.md +109 -0
- package/plugin/commands/create-plan.md +415 -0
- package/plugin/commands/implement-plan.md +89 -0
- package/plugin/commands/research.md +200 -0
- package/plugin/commands/start-leader.md +101 -0
- package/plugin/commands/start-worker.md +56 -0
- package/plugin/commands/swarm-chat.md +78 -0
- package/plugin/commands/todos.md +66 -0
- package/plugin/commands/work-on-task.md +44 -0
- package/plugin/skills/.gitkeep +0 -0
- package/scripts/generate-mcp-docs.ts +415 -0
- package/slack-manifest.json +69 -0
- package/src/be/db.ts +1431 -25
- package/src/cli.tsx +135 -11
- package/src/commands/lead.ts +13 -0
- package/src/commands/runner.ts +255 -0
- package/src/commands/worker.ts +8 -220
- package/src/hooks/hook.ts +102 -14
- package/src/http.ts +361 -5
- package/src/prompts/base-prompt.ts +131 -0
- package/src/server.ts +56 -0
- package/src/slack/app.ts +73 -0
- package/src/slack/commands.ts +88 -0
- package/src/slack/handlers.ts +281 -0
- package/src/slack/index.ts +3 -0
- package/src/slack/responses.ts +175 -0
- package/src/slack/router.ts +170 -0
- package/src/slack/types.ts +20 -0
- package/src/slack/watcher.ts +119 -0
- package/src/tools/create-channel.ts +80 -0
- package/src/tools/get-tasks.ts +54 -21
- package/src/tools/join-swarm.ts +28 -4
- package/src/tools/list-channels.ts +37 -0
- package/src/tools/list-services.ts +110 -0
- package/src/tools/poll-task.ts +46 -3
- package/src/tools/post-message.ts +87 -0
- package/src/tools/read-messages.ts +192 -0
- package/src/tools/register-service.ts +118 -0
- package/src/tools/send-task.ts +80 -7
- package/src/tools/store-progress.ts +9 -3
- package/src/tools/task-action.ts +211 -0
- package/src/tools/unregister-service.ts +110 -0
- package/src/tools/update-profile.ts +105 -0
- package/src/tools/update-service-status.ts +118 -0
- package/src/types.ts +110 -3
- package/src/utils/pretty-print.ts +224 -0
- package/thoughts/shared/plans/.gitkeep +0 -0
- package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
- package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
- package/tsconfig.json +3 -1
- package/ui/bun.lock +692 -0
- package/ui/index.html +22 -0
- package/ui/package.json +32 -0
- package/ui/pnpm-lock.yaml +3034 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/logo.png +0 -0
- package/ui/src/App.tsx +43 -0
- package/ui/src/components/ActivityFeed.tsx +415 -0
- package/ui/src/components/AgentDetailPanel.tsx +534 -0
- package/ui/src/components/AgentsPanel.tsx +549 -0
- package/ui/src/components/ChatPanel.tsx +1820 -0
- package/ui/src/components/ConfigModal.tsx +232 -0
- package/ui/src/components/Dashboard.tsx +534 -0
- package/ui/src/components/Header.tsx +168 -0
- package/ui/src/components/ServicesPanel.tsx +612 -0
- package/ui/src/components/StatsBar.tsx +288 -0
- package/ui/src/components/StatusBadge.tsx +124 -0
- package/ui/src/components/TaskDetailPanel.tsx +807 -0
- package/ui/src/components/TasksPanel.tsx +575 -0
- package/ui/src/hooks/queries.ts +170 -0
- package/ui/src/index.css +235 -0
- package/ui/src/lib/api.ts +161 -0
- package/ui/src/lib/config.ts +35 -0
- package/ui/src/lib/theme.ts +214 -0
- package/ui/src/lib/utils.ts +48 -0
- package/ui/src/main.tsx +32 -0
- package/ui/src/types/api.ts +164 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tailwind.config.js +35 -0
- package/ui/tsconfig.json +31 -0
- package/ui/vite.config.ts +22 -0
- package/cc-plugin/README.md +0 -49
- package/cc-plugin/commands/setup-leader.md +0 -73
- package/cc-plugin/commands/start-worker.md +0 -64
- package/docker-compose.worker.yml +0 -35
- package/example-req-meta.json +0 -24
- /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { getAgentById, getAgentWorkingOnThread, getAllAgents } from "../be/db";
|
|
2
|
+
import type { AgentMatch } from "./types";
|
|
3
|
+
|
|
4
|
+
interface ThreadContext {
|
|
5
|
+
channelId: string;
|
|
6
|
+
threadTs: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Common 3-letter words to exclude from matching
|
|
10
|
+
const COMMON_WORDS = new Set([
|
|
11
|
+
"the",
|
|
12
|
+
"and",
|
|
13
|
+
"for",
|
|
14
|
+
"are",
|
|
15
|
+
"but",
|
|
16
|
+
"not",
|
|
17
|
+
"you",
|
|
18
|
+
"all",
|
|
19
|
+
"can",
|
|
20
|
+
"had",
|
|
21
|
+
"her",
|
|
22
|
+
"was",
|
|
23
|
+
"one",
|
|
24
|
+
"our",
|
|
25
|
+
"out",
|
|
26
|
+
"has",
|
|
27
|
+
"his",
|
|
28
|
+
"how",
|
|
29
|
+
"its",
|
|
30
|
+
"let",
|
|
31
|
+
"may",
|
|
32
|
+
"new",
|
|
33
|
+
"now",
|
|
34
|
+
"old",
|
|
35
|
+
"see",
|
|
36
|
+
"way",
|
|
37
|
+
"who",
|
|
38
|
+
"boy",
|
|
39
|
+
"did",
|
|
40
|
+
"get",
|
|
41
|
+
"say",
|
|
42
|
+
"she",
|
|
43
|
+
"too",
|
|
44
|
+
"use",
|
|
45
|
+
"hey",
|
|
46
|
+
"hi",
|
|
47
|
+
"hello",
|
|
48
|
+
"please",
|
|
49
|
+
"help",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a word is suitable for agent name matching.
|
|
54
|
+
* Allows 3+ char words if they're not common words.
|
|
55
|
+
* Always allows uppercase words (CEO, CTO, etc).
|
|
56
|
+
*/
|
|
57
|
+
function isMatchableWord(word: string): boolean {
|
|
58
|
+
if (word.length < 3) return false;
|
|
59
|
+
// Always allow fully uppercase words (acronyms like CEO, CTO)
|
|
60
|
+
if (word === word.toUpperCase() && word.length >= 3) return true;
|
|
61
|
+
// Allow 3+ char words that aren't common
|
|
62
|
+
if (word.length >= 3 && !COMMON_WORDS.has(word.toLowerCase())) return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Routes a Slack message to the appropriate agent(s) based on mentions.
|
|
68
|
+
*
|
|
69
|
+
* Routing rules:
|
|
70
|
+
* - `swarm#<uuid>` → exact agent by ID
|
|
71
|
+
* - `swarm#all` → all non-lead agents
|
|
72
|
+
* - Partial name match (3+ chars, not common words) → agent by name
|
|
73
|
+
* - Multiple partial matches → route to lead (let lead decide)
|
|
74
|
+
* - Thread follow-up (no match but agent working on thread) → route to that agent
|
|
75
|
+
* - Bot @mention only → lead agent
|
|
76
|
+
*/
|
|
77
|
+
export function routeMessage(
|
|
78
|
+
text: string,
|
|
79
|
+
_botUserId: string,
|
|
80
|
+
botMentioned: boolean,
|
|
81
|
+
threadContext?: ThreadContext,
|
|
82
|
+
): AgentMatch[] {
|
|
83
|
+
const matches: AgentMatch[] = [];
|
|
84
|
+
const agents = getAllAgents().filter((a) => a.status !== "offline");
|
|
85
|
+
|
|
86
|
+
// Check for explicit swarm#<id> syntax
|
|
87
|
+
const idMatches = text.matchAll(/swarm#([a-f0-9-]{36})/gi);
|
|
88
|
+
for (const match of idMatches) {
|
|
89
|
+
const agentId = match[1];
|
|
90
|
+
if (!agentId) continue;
|
|
91
|
+
const agent = getAgentById(agentId);
|
|
92
|
+
if (agent && agent.status !== "offline") {
|
|
93
|
+
matches.push({ agent, matchedText: match[0] });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for swarm#all broadcast
|
|
98
|
+
if (/swarm#all/i.test(text)) {
|
|
99
|
+
const nonLeadAgents = agents.filter((a) => !a.isLead);
|
|
100
|
+
for (const agent of nonLeadAgents) {
|
|
101
|
+
if (!matches.some((m) => m.agent.id === agent.id)) {
|
|
102
|
+
matches.push({ agent, matchedText: "swarm#all" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for partial name matches (3+ chars, not common words)
|
|
108
|
+
if (matches.length === 0) {
|
|
109
|
+
for (const agent of agents) {
|
|
110
|
+
const nameWords = agent.name.split(/\s+/).filter(isMatchableWord);
|
|
111
|
+
for (const word of nameWords) {
|
|
112
|
+
const regex = new RegExp(`\\b${escapeRegex(word)}\\b`, "i");
|
|
113
|
+
if (regex.test(text)) {
|
|
114
|
+
if (!matches.some((m) => m.agent.id === agent.id)) {
|
|
115
|
+
matches.push({ agent, matchedText: word });
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If multiple agents matched a partial name, route to lead instead (let lead decide)
|
|
123
|
+
if (matches.length > 1) {
|
|
124
|
+
const lead = agents.find((a) => a.isLead);
|
|
125
|
+
if (lead) {
|
|
126
|
+
const matchedWords = matches.map((m) => m.matchedText).join(", ");
|
|
127
|
+
const matchedAgents = matches.map((m) => m.agent.name).join(", ");
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
agent: lead,
|
|
131
|
+
matchedText: `ambiguous match "${matchedWords}" (could be: ${matchedAgents})`,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Thread follow-up: If no matches and we're in a thread, check if an agent is working on it
|
|
139
|
+
if (matches.length === 0 && threadContext) {
|
|
140
|
+
const workingAgent = getAgentWorkingOnThread(threadContext.channelId, threadContext.threadTs);
|
|
141
|
+
if (workingAgent && workingAgent.status !== "offline") {
|
|
142
|
+
matches.push({ agent: workingAgent, matchedText: "thread follow-up" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If only bot was mentioned and no agents matched, route to lead
|
|
147
|
+
if (matches.length === 0 && botMentioned) {
|
|
148
|
+
const lead = agents.find((a) => a.isLead);
|
|
149
|
+
if (lead) {
|
|
150
|
+
matches.push({ agent: lead, matchedText: "@bot" });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return matches;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function escapeRegex(str: string): string {
|
|
158
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extracts the task description from a message, removing bot mentions and agent references.
|
|
163
|
+
*/
|
|
164
|
+
export function extractTaskFromMessage(text: string, botUserId: string): string {
|
|
165
|
+
return text
|
|
166
|
+
.replace(new RegExp(`<@${botUserId}>`, "g"), "") // Remove bot mentions
|
|
167
|
+
.replace(/swarm#[a-f0-9-]{36}/gi, "") // Remove swarm#<id>
|
|
168
|
+
.replace(/swarm#all/gi, "") // Remove swarm#all
|
|
169
|
+
.trim();
|
|
170
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Agent } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface SlackMessageContext {
|
|
4
|
+
channelId: string;
|
|
5
|
+
threadTs?: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
text: string;
|
|
8
|
+
botUserId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AgentMatch {
|
|
12
|
+
agent: Agent;
|
|
13
|
+
matchedText: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SlackConfig {
|
|
17
|
+
botToken: string;
|
|
18
|
+
appToken: string;
|
|
19
|
+
signingSecret?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getCompletedSlackTasks, getInProgressSlackTasks } from "../be/db";
|
|
2
|
+
import { getSlackApp } from "./app";
|
|
3
|
+
import { sendProgressUpdate, sendTaskResponse } from "./responses";
|
|
4
|
+
|
|
5
|
+
let watcherInterval: ReturnType<typeof setInterval> | null = null;
|
|
6
|
+
let isProcessing = false;
|
|
7
|
+
|
|
8
|
+
// Track notified completion tasks (taskId -> timestamp)
|
|
9
|
+
const notifiedCompletions = new Map<string, number>();
|
|
10
|
+
|
|
11
|
+
// Track sent progress messages (taskId -> last progress text)
|
|
12
|
+
const sentProgress = new Map<string, string>();
|
|
13
|
+
|
|
14
|
+
// Track in-flight sends to prevent race conditions
|
|
15
|
+
const pendingSends = new Set<string>();
|
|
16
|
+
|
|
17
|
+
// Track last send time per task to throttle (taskId -> timestamp)
|
|
18
|
+
const lastSendTime = new Map<string, number>();
|
|
19
|
+
const MIN_SEND_INTERVAL = 1000; // Don't send for same task within 1 second
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start watching for Slack task updates and sending responses.
|
|
23
|
+
*/
|
|
24
|
+
export function startTaskWatcher(intervalMs = 3000): void {
|
|
25
|
+
if (watcherInterval) {
|
|
26
|
+
console.log("[Slack] Task watcher already running");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Initialize with existing completed tasks to avoid re-notifying on restart
|
|
31
|
+
const existingCompleted = getCompletedSlackTasks();
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
for (const task of existingCompleted) {
|
|
34
|
+
notifiedCompletions.set(task.id, now);
|
|
35
|
+
}
|
|
36
|
+
console.log(`[Slack] Initialized with ${existingCompleted.length} existing completed tasks`);
|
|
37
|
+
|
|
38
|
+
watcherInterval = setInterval(async () => {
|
|
39
|
+
// Prevent overlapping processing cycles
|
|
40
|
+
if (isProcessing || !getSlackApp()) return;
|
|
41
|
+
isProcessing = true;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Check for progress updates on in-progress tasks
|
|
45
|
+
const inProgressTasks = getInProgressSlackTasks();
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
for (const task of inProgressTasks) {
|
|
48
|
+
const progressKey = `progress:${task.id}`;
|
|
49
|
+
|
|
50
|
+
// Skip if already sending or sent recently (throttle)
|
|
51
|
+
if (pendingSends.has(progressKey)) continue;
|
|
52
|
+
const lastSent = lastSendTime.get(progressKey);
|
|
53
|
+
if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
|
|
54
|
+
|
|
55
|
+
const lastSentProgress = sentProgress.get(task.id);
|
|
56
|
+
// Only send if progress exists and is different from last sent
|
|
57
|
+
if (task.progress && task.progress !== lastSentProgress) {
|
|
58
|
+
// Mark as pending and sent BEFORE sending
|
|
59
|
+
pendingSends.add(progressKey);
|
|
60
|
+
sentProgress.set(task.id, task.progress);
|
|
61
|
+
lastSendTime.set(progressKey, now);
|
|
62
|
+
try {
|
|
63
|
+
await sendProgressUpdate(task, task.progress);
|
|
64
|
+
console.log(`[Slack] Sent progress update for task ${task.id.slice(0, 8)}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// If send fails, clear markers so we can retry
|
|
67
|
+
sentProgress.delete(task.id);
|
|
68
|
+
lastSendTime.delete(progressKey);
|
|
69
|
+
console.error(`[Slack] Failed to send progress:`, error);
|
|
70
|
+
} finally {
|
|
71
|
+
pendingSends.delete(progressKey);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for completed tasks
|
|
77
|
+
const completedTasks = getCompletedSlackTasks();
|
|
78
|
+
for (const task of completedTasks) {
|
|
79
|
+
const completionKey = `completion:${task.id}`;
|
|
80
|
+
|
|
81
|
+
// Skip if already notified or currently sending or sent recently
|
|
82
|
+
if (notifiedCompletions.has(task.id) || pendingSends.has(completionKey)) continue;
|
|
83
|
+
const lastSent = lastSendTime.get(completionKey);
|
|
84
|
+
if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
|
|
85
|
+
|
|
86
|
+
// Mark as pending and notified BEFORE sending
|
|
87
|
+
pendingSends.add(completionKey);
|
|
88
|
+
notifiedCompletions.set(task.id, now);
|
|
89
|
+
lastSendTime.set(completionKey, now);
|
|
90
|
+
try {
|
|
91
|
+
await sendTaskResponse(task);
|
|
92
|
+
// Clean up progress tracking
|
|
93
|
+
sentProgress.delete(task.id);
|
|
94
|
+
console.log(`[Slack] Sent ${task.status} response for task ${task.id.slice(0, 8)}`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// If send fails, remove from notified so we can retry
|
|
97
|
+
notifiedCompletions.delete(task.id);
|
|
98
|
+
lastSendTime.delete(completionKey);
|
|
99
|
+
console.error(`[Slack] Failed to send completion:`, error);
|
|
100
|
+
} finally {
|
|
101
|
+
pendingSends.delete(completionKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
isProcessing = false;
|
|
106
|
+
}
|
|
107
|
+
}, intervalMs);
|
|
108
|
+
|
|
109
|
+
console.log(`[Slack] Task watcher started (interval: ${intervalMs}ms)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function stopTaskWatcher(): void {
|
|
113
|
+
if (watcherInterval) {
|
|
114
|
+
clearInterval(watcherInterval);
|
|
115
|
+
watcherInterval = null;
|
|
116
|
+
isProcessing = false;
|
|
117
|
+
console.log("[Slack] Task watcher stopped");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createChannel, getChannelByName } from "@/be/db";
|
|
4
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
+
import { ChannelSchema, ChannelTypeSchema } from "@/types";
|
|
6
|
+
|
|
7
|
+
export const registerCreateChannelTool = (server: McpServer) => {
|
|
8
|
+
createToolRegistrar(server)(
|
|
9
|
+
"create-channel",
|
|
10
|
+
{
|
|
11
|
+
title: "Create Channel",
|
|
12
|
+
description: "Creates a new channel for cross-agent communication.",
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
name: z.string().min(1).max(100).describe("Channel name (must be unique)."),
|
|
15
|
+
description: z.string().max(500).optional().describe("Channel description."),
|
|
16
|
+
type: ChannelTypeSchema.optional().describe("Channel type: 'public' (default) or 'dm'."),
|
|
17
|
+
participants: z.array(z.uuid()).optional().describe("Agent IDs for DM channels."),
|
|
18
|
+
}),
|
|
19
|
+
outputSchema: z.object({
|
|
20
|
+
success: z.boolean(),
|
|
21
|
+
message: z.string(),
|
|
22
|
+
channel: ChannelSchema.optional(),
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
async ({ name, description, type, participants }, requestInfo, _meta) => {
|
|
26
|
+
if (!requestInfo.agentId) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
|
|
29
|
+
structuredContent: {
|
|
30
|
+
success: false,
|
|
31
|
+
message: 'Agent ID not found. Set the "X-Agent-ID" header.',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if channel already exists
|
|
37
|
+
const existing = getChannelByName(name);
|
|
38
|
+
if (existing) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Channel "${name}" already exists.` }],
|
|
41
|
+
structuredContent: {
|
|
42
|
+
yourAgentId: requestInfo.agentId,
|
|
43
|
+
success: false,
|
|
44
|
+
message: `Channel "${name}" already exists.`,
|
|
45
|
+
channel: existing,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const channel = createChannel(name, {
|
|
52
|
+
description,
|
|
53
|
+
type: type ?? "public",
|
|
54
|
+
createdBy: requestInfo.agentId,
|
|
55
|
+
participants,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: `Created channel "${name}".` }],
|
|
60
|
+
structuredContent: {
|
|
61
|
+
yourAgentId: requestInfo.agentId,
|
|
62
|
+
success: true,
|
|
63
|
+
message: `Created channel "${name}".`,
|
|
64
|
+
channel,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: `Failed to create channel: ${message}` }],
|
|
71
|
+
structuredContent: {
|
|
72
|
+
yourAgentId: requestInfo.agentId,
|
|
73
|
+
success: false,
|
|
74
|
+
message: `Failed to create channel: ${message}`,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
};
|
package/src/tools/get-tasks.ts
CHANGED
|
@@ -6,8 +6,14 @@ import { AgentTaskStatusSchema } from "@/types";
|
|
|
6
6
|
|
|
7
7
|
const TaskSummarySchema = z.object({
|
|
8
8
|
id: z.string(),
|
|
9
|
+
agentId: z.string().nullable(),
|
|
9
10
|
task: z.string(),
|
|
10
11
|
status: AgentTaskStatusSchema,
|
|
12
|
+
taskType: z.string().optional(),
|
|
13
|
+
tags: z.array(z.string()),
|
|
14
|
+
priority: z.number(),
|
|
15
|
+
dependsOn: z.array(z.string()),
|
|
16
|
+
offeredTo: z.string().optional(),
|
|
11
17
|
createdAt: z.string(),
|
|
12
18
|
lastUpdatedAt: z.string(),
|
|
13
19
|
finishedAt: z.string().optional(),
|
|
@@ -20,56 +26,83 @@ export const registerGetTasksTool = (server: McpServer) => {
|
|
|
20
26
|
{
|
|
21
27
|
title: "Get tasks",
|
|
22
28
|
description:
|
|
23
|
-
"Returns a list of tasks in the swarm
|
|
29
|
+
"Returns a list of tasks in the swarm with various filters. Sorted by priority (desc) then lastUpdatedAt (desc).",
|
|
24
30
|
inputSchema: z.object({
|
|
25
31
|
status: AgentTaskStatusSchema.optional().describe(
|
|
26
|
-
"Filter by task status
|
|
32
|
+
"Filter by task status (unassigned, offered, pending, in_progress, completed, failed).",
|
|
27
33
|
),
|
|
28
|
-
mineOnly: z
|
|
34
|
+
mineOnly: z.boolean().optional().describe("Only return tasks assigned to you."),
|
|
35
|
+
unassigned: z.boolean().optional().describe("Only return unassigned tasks in the pool."),
|
|
36
|
+
offeredToMe: z
|
|
29
37
|
.boolean()
|
|
30
38
|
.optional()
|
|
31
|
-
.describe(
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
.describe("Only return tasks offered to you (awaiting accept/reject)."),
|
|
40
|
+
readyOnly: z.boolean().optional().describe("Only return tasks whose dependencies are met."),
|
|
41
|
+
taskType: z.string().optional().describe("Filter by task type (e.g., 'bug', 'feature')."),
|
|
42
|
+
tags: z.array(z.string()).optional().describe("Filter by any matching tag."),
|
|
43
|
+
search: z.string().optional().describe("Search in task description."),
|
|
34
44
|
}),
|
|
35
45
|
outputSchema: z.object({
|
|
36
46
|
tasks: z.array(TaskSummarySchema),
|
|
37
47
|
}),
|
|
38
48
|
},
|
|
39
|
-
async (
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
async (
|
|
50
|
+
{ status, mineOnly, unassigned, offeredToMe, readyOnly, taskType, tags, search },
|
|
51
|
+
requestInfo,
|
|
52
|
+
_meta,
|
|
53
|
+
) => {
|
|
54
|
+
const agentId = requestInfo.agentId;
|
|
42
55
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
// Build filters
|
|
57
|
+
const tasks = getAllTasks({
|
|
58
|
+
status,
|
|
59
|
+
agentId: mineOnly ? (agentId ?? undefined) : undefined,
|
|
60
|
+
unassigned,
|
|
61
|
+
offeredTo: offeredToMe ? (agentId ?? undefined) : undefined,
|
|
62
|
+
readyOnly,
|
|
63
|
+
taskType,
|
|
64
|
+
tags,
|
|
65
|
+
search,
|
|
66
|
+
});
|
|
52
67
|
|
|
53
68
|
const taskSummaries = tasks.map((t) => ({
|
|
54
69
|
id: t.id,
|
|
70
|
+
agentId: t.agentId,
|
|
55
71
|
task: t.task,
|
|
56
72
|
status: t.status,
|
|
73
|
+
taskType: t.taskType,
|
|
74
|
+
tags: t.tags,
|
|
75
|
+
priority: t.priority,
|
|
76
|
+
dependsOn: t.dependsOn,
|
|
77
|
+
offeredTo: t.offeredTo,
|
|
57
78
|
createdAt: t.createdAt,
|
|
58
79
|
lastUpdatedAt: t.lastUpdatedAt,
|
|
59
80
|
finishedAt: t.finishedAt,
|
|
60
81
|
progress: t.progress,
|
|
61
82
|
}));
|
|
62
83
|
|
|
63
|
-
|
|
84
|
+
// Build filter description for message
|
|
85
|
+
const filters: string[] = [];
|
|
86
|
+
if (status) filters.push(`status='${status}'`);
|
|
87
|
+
if (mineOnly) filters.push("mine only");
|
|
88
|
+
if (unassigned) filters.push("unassigned");
|
|
89
|
+
if (offeredToMe) filters.push("offered to me");
|
|
90
|
+
if (readyOnly) filters.push("ready only");
|
|
91
|
+
if (taskType) filters.push(`type='${taskType}'`);
|
|
92
|
+
if (tags?.length) filters.push(`tags=[${tags.join(", ")}]`);
|
|
93
|
+
if (search) filters.push(`search='${search}'`);
|
|
94
|
+
|
|
95
|
+
const filterMsg = filters.length > 0 ? ` (${filters.join(", ")})` : "";
|
|
96
|
+
|
|
64
97
|
return {
|
|
65
98
|
content: [
|
|
66
99
|
{
|
|
67
100
|
type: "text",
|
|
68
|
-
text: `Found ${taskSummaries.length} task(s)
|
|
101
|
+
text: `Found ${taskSummaries.length} task(s)${filterMsg}.`,
|
|
69
102
|
},
|
|
70
103
|
],
|
|
71
104
|
structuredContent: {
|
|
72
|
-
yourAgentId:
|
|
105
|
+
yourAgentId: agentId,
|
|
73
106
|
tasks: taskSummaries,
|
|
74
107
|
},
|
|
75
108
|
};
|
package/src/tools/join-swarm.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 { createAgent, getAllAgents, getDb } from "@/be/db";
|
|
3
|
+
import { createAgent, getAllAgents, getDb, updateAgentProfile } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
5
|
import { AgentSchema } from "@/types";
|
|
6
6
|
|
|
@@ -9,7 +9,8 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
9
9
|
"join-swarm",
|
|
10
10
|
{
|
|
11
11
|
title: "Join the agent swarm",
|
|
12
|
-
description:
|
|
12
|
+
description:
|
|
13
|
+
"Tool for an agent to join the swarm of agents with optional profile information.",
|
|
13
14
|
inputSchema: z.object({
|
|
14
15
|
requestedId: z
|
|
15
16
|
.string()
|
|
@@ -17,6 +18,16 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
17
18
|
.describe("Requested ID for the agent (overridden by X-Agent-ID header)."),
|
|
18
19
|
lead: z.boolean().default(false).describe("Whether this agent should be the lead."),
|
|
19
20
|
name: z.string().min(1).describe("The name of the agent joining the swarm."),
|
|
21
|
+
description: z.string().optional().describe("Agent description."),
|
|
22
|
+
role: z
|
|
23
|
+
.string()
|
|
24
|
+
.max(100)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Agent role (free-form, e.g., 'frontend dev', 'code reviewer')."),
|
|
27
|
+
capabilities: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("List of capabilities (e.g., ['typescript', 'react', 'testing'])."),
|
|
20
31
|
}),
|
|
21
32
|
outputSchema: z.object({
|
|
22
33
|
success: z.boolean(),
|
|
@@ -24,7 +35,7 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
24
35
|
agent: AgentSchema.optional(),
|
|
25
36
|
}),
|
|
26
37
|
},
|
|
27
|
-
async ({ lead, name, requestedId }, requestInfo, _meta) => {
|
|
38
|
+
async ({ lead, name, requestedId, description, role, capabilities }, requestInfo, _meta) => {
|
|
28
39
|
// Check if agent ID is set
|
|
29
40
|
if (!requestInfo.agentId && !requestedId) {
|
|
30
41
|
return {
|
|
@@ -70,12 +81,25 @@ export const registerJoinSwarmTool = (server: McpServer) => {
|
|
|
70
81
|
);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
const agent = createAgent({
|
|
74
85
|
id: agentId,
|
|
75
86
|
name,
|
|
76
87
|
isLead: lead,
|
|
77
88
|
status: "idle",
|
|
89
|
+
capabilities: [],
|
|
78
90
|
});
|
|
91
|
+
|
|
92
|
+
// Update profile if any profile fields were provided
|
|
93
|
+
if (description !== undefined || role !== undefined || capabilities !== undefined) {
|
|
94
|
+
const updatedAgent = updateAgentProfile(agent.id, {
|
|
95
|
+
description,
|
|
96
|
+
role,
|
|
97
|
+
capabilities,
|
|
98
|
+
});
|
|
99
|
+
return updatedAgent ?? agent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return agent;
|
|
79
103
|
});
|
|
80
104
|
|
|
81
105
|
const agent = agentTx();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { getAllChannels } from "@/be/db";
|
|
4
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
+
import { ChannelSchema } from "@/types";
|
|
6
|
+
|
|
7
|
+
export const registerListChannelsTool = (server: McpServer) => {
|
|
8
|
+
createToolRegistrar(server)(
|
|
9
|
+
"list-channels",
|
|
10
|
+
{
|
|
11
|
+
title: "List Channels",
|
|
12
|
+
description: "Lists all available channels for cross-agent communication.",
|
|
13
|
+
inputSchema: z.object({}),
|
|
14
|
+
outputSchema: z.object({
|
|
15
|
+
success: z.boolean(),
|
|
16
|
+
channels: z.array(ChannelSchema),
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
async (_input, requestInfo, _meta) => {
|
|
20
|
+
const channels = getAllChannels();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: `Found ${channels.length} channel(s): ${channels.map((c) => c.name).join(", ") || "(none)"}`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
structuredContent: {
|
|
30
|
+
yourAgentId: requestInfo.agentId,
|
|
31
|
+
success: true,
|
|
32
|
+
channels,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
};
|