@desplega.ai/agent-swarm 1.2.0 → 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/.dockerignore +3 -0
- 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 +105 -185
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/assets/agent-swarm.png +0 -0
- package/deploy/docker-push.ts +30 -0
- package/docker-compose.example.yml +137 -0
- package/docker-entrypoint.sh +223 -7
- package/package.json +13 -4
- 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/setup.tsx +5 -5
- package/src/commands/worker.ts +8 -220
- package/src/hooks/hook.ts +108 -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 +47 -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
package/src/slack/app.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { App, LogLevel } from "@slack/bolt";
|
|
2
|
+
import { startTaskWatcher, stopTaskWatcher } from "./watcher";
|
|
3
|
+
|
|
4
|
+
let app: App | null = null;
|
|
5
|
+
let initialized = false;
|
|
6
|
+
|
|
7
|
+
export function getSlackApp(): App | null {
|
|
8
|
+
return app;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function initSlackApp(): Promise<App | null> {
|
|
12
|
+
// Prevent double initialization
|
|
13
|
+
if (initialized) {
|
|
14
|
+
console.log("[Slack] Already initialized, skipping");
|
|
15
|
+
return app;
|
|
16
|
+
}
|
|
17
|
+
initialized = true;
|
|
18
|
+
|
|
19
|
+
// Check if Slack is explicitly disabled
|
|
20
|
+
const slackDisable = process.env.SLACK_DISABLE;
|
|
21
|
+
if (slackDisable === "true" || slackDisable === "1") {
|
|
22
|
+
console.log("[Slack] Disabled via SLACK_DISABLE");
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const botToken = process.env.SLACK_BOT_TOKEN;
|
|
27
|
+
const appToken = process.env.SLACK_APP_TOKEN;
|
|
28
|
+
|
|
29
|
+
if (!botToken || !appToken) {
|
|
30
|
+
console.log("[Slack] Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN, Slack integration disabled");
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
app = new App({
|
|
35
|
+
token: botToken,
|
|
36
|
+
appToken: appToken,
|
|
37
|
+
socketMode: true,
|
|
38
|
+
logLevel: process.env.NODE_ENV === "development" ? LogLevel.DEBUG : LogLevel.INFO,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Register handlers
|
|
42
|
+
const { registerMessageHandler } = await import("./handlers");
|
|
43
|
+
const { registerCommandHandler } = await import("./commands");
|
|
44
|
+
|
|
45
|
+
registerMessageHandler(app);
|
|
46
|
+
registerCommandHandler(app);
|
|
47
|
+
|
|
48
|
+
return app;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function startSlackApp(): Promise<void> {
|
|
52
|
+
if (!app) {
|
|
53
|
+
await initSlackApp();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (app) {
|
|
57
|
+
await app.start();
|
|
58
|
+
console.log("[Slack] Bot connected via Socket Mode");
|
|
59
|
+
|
|
60
|
+
// Start watching for task completions
|
|
61
|
+
startTaskWatcher();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function stopSlackApp(): Promise<void> {
|
|
66
|
+
stopTaskWatcher();
|
|
67
|
+
|
|
68
|
+
if (app) {
|
|
69
|
+
await app.stop();
|
|
70
|
+
app = null;
|
|
71
|
+
console.log("[Slack] Bot disconnected");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { App } from "@slack/bolt";
|
|
2
|
+
import { getAllAgents, getAllTasks } from "../be/db";
|
|
3
|
+
|
|
4
|
+
export function registerCommandHandler(app: App): void {
|
|
5
|
+
app.command("/agent-swarm-status", async ({ ack, respond }) => {
|
|
6
|
+
await ack();
|
|
7
|
+
|
|
8
|
+
const agents = getAllAgents();
|
|
9
|
+
const tasks = getAllTasks({ status: "in_progress" });
|
|
10
|
+
|
|
11
|
+
const statusEmoji: Record<string, string> = {
|
|
12
|
+
idle: ":white_circle:",
|
|
13
|
+
busy: ":large_blue_circle:",
|
|
14
|
+
offline: ":black_circle:",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const agentLines = agents.map((agent) => {
|
|
18
|
+
const emoji = statusEmoji[agent.status] || ":question:";
|
|
19
|
+
const role = agent.isLead ? " (Lead)" : "";
|
|
20
|
+
const activeTask = tasks.find((t) => t.agentId === agent.id);
|
|
21
|
+
const taskInfo = activeTask ? ` - Working on: ${activeTask.task.slice(0, 50)}...` : "";
|
|
22
|
+
return `${emoji} *${agent.name}*${role}: ${agent.status}${taskInfo}`;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const summary = {
|
|
26
|
+
total: agents.length,
|
|
27
|
+
idle: agents.filter((a) => a.status === "idle").length,
|
|
28
|
+
busy: agents.filter((a) => a.status === "busy").length,
|
|
29
|
+
offline: agents.filter((a) => a.status === "offline").length,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await respond({
|
|
33
|
+
response_type: "ephemeral",
|
|
34
|
+
blocks: [
|
|
35
|
+
{
|
|
36
|
+
type: "header",
|
|
37
|
+
text: { type: "plain_text", text: "Agent Swarm Status" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "section",
|
|
41
|
+
text: {
|
|
42
|
+
type: "mrkdwn",
|
|
43
|
+
text: `*Summary:* ${summary.total} agents (${summary.idle} idle, ${summary.busy} busy, ${summary.offline} offline)`,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "divider",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "section",
|
|
51
|
+
text: {
|
|
52
|
+
type: "mrkdwn",
|
|
53
|
+
text: agentLines.join("\n") || "_No agents registered_",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
app.command("/agent-swarm-help", async ({ ack, respond }) => {
|
|
61
|
+
await ack();
|
|
62
|
+
|
|
63
|
+
await respond({
|
|
64
|
+
response_type: "ephemeral",
|
|
65
|
+
blocks: [
|
|
66
|
+
{
|
|
67
|
+
type: "header",
|
|
68
|
+
text: { type: "plain_text", text: "Agent Swarm Help" },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "section",
|
|
72
|
+
text: {
|
|
73
|
+
type: "mrkdwn",
|
|
74
|
+
text: `*How to assign tasks:*
|
|
75
|
+
• Mention an agent by name: \`Hey Alpha, can you review this code?\`
|
|
76
|
+
• Use explicit ID: \`swarm#<uuid> please analyze the logs\`
|
|
77
|
+
• Broadcast to all: \`swarm#all status report please\`
|
|
78
|
+
• Mention the bot: \`@agent-swarm help me\` (routes to lead agent)
|
|
79
|
+
|
|
80
|
+
*Commands:*
|
|
81
|
+
• \`/agent-swarm-status\` - Show all agents and their current status
|
|
82
|
+
• \`/agent-swarm-help\` - Show this help message`,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { App } from "@slack/bolt";
|
|
2
|
+
import type { WebClient } from "@slack/web-api";
|
|
3
|
+
import { createTask, getAgentById, getTasksByAgentId } from "../be/db";
|
|
4
|
+
import { extractTaskFromMessage, routeMessage } from "./router";
|
|
5
|
+
|
|
6
|
+
interface MessageEvent {
|
|
7
|
+
type: string;
|
|
8
|
+
subtype?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
user?: string;
|
|
11
|
+
channel: string;
|
|
12
|
+
ts: string;
|
|
13
|
+
thread_ts?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ThreadMessage {
|
|
17
|
+
user?: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
ts: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Cache for user display names
|
|
23
|
+
const userNameCache = new Map<string, string>();
|
|
24
|
+
|
|
25
|
+
async function getUserDisplayName(client: WebClient, userId: string): Promise<string> {
|
|
26
|
+
if (userNameCache.has(userId)) {
|
|
27
|
+
// biome-ignore lint: This is fine
|
|
28
|
+
return userNameCache.get(userId)!;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await client.users.info({ user: userId });
|
|
32
|
+
const name = result.user?.profile?.display_name || result.user?.real_name || userId;
|
|
33
|
+
userNameCache.set(userId, name);
|
|
34
|
+
return name;
|
|
35
|
+
} catch {
|
|
36
|
+
return userId;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetch thread history and format as context for the task.
|
|
42
|
+
* Returns empty string if not in a thread or no previous messages.
|
|
43
|
+
*/
|
|
44
|
+
async function getThreadContext(
|
|
45
|
+
client: WebClient,
|
|
46
|
+
channel: string,
|
|
47
|
+
threadTs: string | undefined,
|
|
48
|
+
currentTs: string,
|
|
49
|
+
botUserId: string,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
// Not in a thread - no context needed
|
|
52
|
+
if (!threadTs) return "";
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await client.conversations.replies({
|
|
56
|
+
channel,
|
|
57
|
+
ts: threadTs,
|
|
58
|
+
limit: 20, // Last 20 messages max
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const messages = (result.messages || []) as ThreadMessage[];
|
|
62
|
+
// Filter out the current message only (keep bot messages for context)
|
|
63
|
+
const previousMessages = messages.filter((m) => m.ts !== currentTs && m.text);
|
|
64
|
+
|
|
65
|
+
if (previousMessages.length === 0) return "";
|
|
66
|
+
|
|
67
|
+
// Format messages with user names or [Agent] for bot messages
|
|
68
|
+
const formattedMessages: string[] = [];
|
|
69
|
+
for (const m of previousMessages) {
|
|
70
|
+
if (m.user === botUserId) {
|
|
71
|
+
// Bot/agent message - truncate if too long
|
|
72
|
+
const truncatedText = m.text && m.text.length > 500 ? `${m.text.slice(0, 500)}...` : m.text;
|
|
73
|
+
formattedMessages.push(`[Agent]: ${truncatedText}`);
|
|
74
|
+
} else {
|
|
75
|
+
const userName = m.user ? await getUserDisplayName(client, m.user) : "Unknown";
|
|
76
|
+
formattedMessages.push(`${userName}: ${m.text}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `<thread_context>\n${formattedMessages.join("\n")}\n</thread_context>\n\n`;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("[Slack] Failed to fetch thread context:", error);
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const appUrl = process.env.APP_URL || "";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a link to the task in the dashboard, or just the task ID if no APP_URL.
|
|
91
|
+
*/
|
|
92
|
+
function getTaskLink(taskId: string): string {
|
|
93
|
+
const shortId = taskId.slice(0, 8);
|
|
94
|
+
if (appUrl) {
|
|
95
|
+
return `<${appUrl}?tab=tasks&task=${taskId}&expand=true|\`${shortId}\`>`;
|
|
96
|
+
}
|
|
97
|
+
return `\`${shortId}\``;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Message deduplication (prevents duplicate event processing)
|
|
101
|
+
const processedMessages = new Set<string>();
|
|
102
|
+
const MESSAGE_DEDUP_TTL = 60_000; // 1 minute
|
|
103
|
+
|
|
104
|
+
function isMessageProcessed(messageKey: string): boolean {
|
|
105
|
+
if (processedMessages.has(messageKey)) {
|
|
106
|
+
console.log(`[Slack] Duplicate event detected: ${messageKey}`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
processedMessages.add(messageKey);
|
|
110
|
+
setTimeout(() => processedMessages.delete(messageKey), MESSAGE_DEDUP_TTL);
|
|
111
|
+
console.log(`[Slack] Processing new message: ${messageKey}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Rate limiting
|
|
116
|
+
const rateLimitMap = new Map<string, number>();
|
|
117
|
+
const RATE_LIMIT_WINDOW = 60_000; // 1 minute
|
|
118
|
+
const MAX_REQUESTS_PER_WINDOW = 10;
|
|
119
|
+
|
|
120
|
+
function checkRateLimit(userId: string): boolean {
|
|
121
|
+
const userRequests = rateLimitMap.get(userId) || 0;
|
|
122
|
+
|
|
123
|
+
if (userRequests >= MAX_REQUESTS_PER_WINDOW) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
rateLimitMap.set(userId, userRequests + 1);
|
|
128
|
+
|
|
129
|
+
// Decrement after window
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
const current = rateLimitMap.get(userId) || 0;
|
|
132
|
+
if (current > 0) {
|
|
133
|
+
rateLimitMap.set(userId, current - 1);
|
|
134
|
+
}
|
|
135
|
+
}, RATE_LIMIT_WINDOW);
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function registerMessageHandler(app: App): void {
|
|
141
|
+
// Handle all message events
|
|
142
|
+
app.event("message", async ({ event, client, say }) => {
|
|
143
|
+
// Ignore bot messages and message_changed events
|
|
144
|
+
if (
|
|
145
|
+
"subtype" in event &&
|
|
146
|
+
(event.subtype === "bot_message" || event.subtype === "message_changed")
|
|
147
|
+
) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const msg = event as MessageEvent;
|
|
152
|
+
if (!msg.text || !msg.user) return;
|
|
153
|
+
|
|
154
|
+
// Deduplicate events (Slack can send same event twice)
|
|
155
|
+
const messageKey = `${msg.channel}:${msg.ts}`;
|
|
156
|
+
if (isMessageProcessed(messageKey)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Get bot's user ID
|
|
161
|
+
const authResult = await client.auth.test();
|
|
162
|
+
const botUserId = authResult.user_id as string;
|
|
163
|
+
|
|
164
|
+
// Check if bot was mentioned
|
|
165
|
+
const botMentioned = msg.text.includes(`<@${botUserId}>`);
|
|
166
|
+
|
|
167
|
+
// Build thread context for routing (if we're in a thread)
|
|
168
|
+
const routingThreadContext = msg.thread_ts
|
|
169
|
+
? { channelId: msg.channel, threadTs: msg.thread_ts }
|
|
170
|
+
: undefined;
|
|
171
|
+
|
|
172
|
+
// Route message to agents
|
|
173
|
+
const matches = routeMessage(msg.text, botUserId, botMentioned, routingThreadContext);
|
|
174
|
+
|
|
175
|
+
if (matches.length === 0) {
|
|
176
|
+
// No agents matched - ignore message unless bot was directly mentioned
|
|
177
|
+
if (botMentioned) {
|
|
178
|
+
await say({
|
|
179
|
+
text: ":satellite: _No agents are currently available. Use `/agent-swarm-status` to check the swarm._",
|
|
180
|
+
thread_ts: msg.thread_ts || msg.ts,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Rate limit check
|
|
187
|
+
if (!checkRateLimit(msg.user)) {
|
|
188
|
+
await say({
|
|
189
|
+
text: ":satellite: _You're sending too many requests. Please slow down._",
|
|
190
|
+
thread_ts: msg.thread_ts || msg.ts,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Extract task description
|
|
196
|
+
const taskDescription = extractTaskFromMessage(msg.text, botUserId);
|
|
197
|
+
if (!taskDescription) {
|
|
198
|
+
await say({
|
|
199
|
+
text: ":satellite: _Please provide a task description after mentioning an agent._",
|
|
200
|
+
thread_ts: msg.thread_ts || msg.ts,
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create tasks for each matched agent
|
|
206
|
+
const threadTs = msg.thread_ts || msg.ts;
|
|
207
|
+
|
|
208
|
+
// Fetch thread context if in a thread
|
|
209
|
+
const threadContext = await getThreadContext(
|
|
210
|
+
client,
|
|
211
|
+
msg.channel,
|
|
212
|
+
msg.thread_ts,
|
|
213
|
+
msg.ts,
|
|
214
|
+
botUserId,
|
|
215
|
+
);
|
|
216
|
+
const fullTaskDescription = threadContext + taskDescription;
|
|
217
|
+
const results: { assigned: string[]; queued: string[]; failed: string[] } = {
|
|
218
|
+
assigned: [],
|
|
219
|
+
queued: [],
|
|
220
|
+
failed: [],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
for (const match of matches) {
|
|
224
|
+
const agent = getAgentById(match.agent.id);
|
|
225
|
+
|
|
226
|
+
if (!agent) {
|
|
227
|
+
results.failed.push(`\`${match.agent.name}\` (not found)`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const task = createTask(agent.id, fullTaskDescription, {
|
|
233
|
+
source: "slack",
|
|
234
|
+
slackChannelId: msg.channel,
|
|
235
|
+
slackThreadTs: threadTs,
|
|
236
|
+
slackUserId: msg.user,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Check if agent has an in-progress task in this thread (queued follow-up)
|
|
240
|
+
const agentTasks = getTasksByAgentId(agent.id);
|
|
241
|
+
const inProgressInThread = agentTasks.find(
|
|
242
|
+
(t) => t.id !== task.id && t.status === "in_progress" && t.slackThreadTs === threadTs,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (inProgressInThread) {
|
|
246
|
+
results.queued.push(`*${agent.name}* (${getTaskLink(task.id)})`);
|
|
247
|
+
} else {
|
|
248
|
+
results.assigned.push(`*${agent.name}* (${getTaskLink(task.id)})`);
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
results.failed.push(`\`${agent.name}\` (error)`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Send consolidated summary
|
|
256
|
+
const parts: string[] = [];
|
|
257
|
+
if (results.assigned.length > 0) {
|
|
258
|
+
parts.push(`:satellite: _Task assigned to: ${results.assigned.join(", ")}_`);
|
|
259
|
+
}
|
|
260
|
+
if (results.queued.length > 0) {
|
|
261
|
+
parts.push(`:satellite: _Task queued for: ${results.queued.join(", ")}_`);
|
|
262
|
+
}
|
|
263
|
+
if (results.failed.length > 0) {
|
|
264
|
+
parts.push(`:satellite: _Could not assign to: ${results.failed.join(", ")}_`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (parts.length > 0) {
|
|
268
|
+
await say({
|
|
269
|
+
text: parts.join("\n"),
|
|
270
|
+
thread_ts: msg.thread_ts || msg.ts,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Handle app_mention events specifically
|
|
276
|
+
app.event("app_mention", async ({ event }) => {
|
|
277
|
+
// app_mention is already handled by the message event above
|
|
278
|
+
// but we can add specific behavior here if needed
|
|
279
|
+
console.log(`[Slack] App mentioned in channel ${event.channel}`);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { WebClient } from "@slack/web-api";
|
|
2
|
+
import { getAgentById } from "../be/db";
|
|
3
|
+
import type { Agent, AgentTask } from "../types";
|
|
4
|
+
import { getSlackApp } from "./app";
|
|
5
|
+
|
|
6
|
+
const isDev = process.env.ENV === "development";
|
|
7
|
+
const appUrl = process.env.APP_URL || "";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert GitHub-flavored markdown to Slack mrkdwn format.
|
|
11
|
+
*/
|
|
12
|
+
function markdownToSlack(text: string): string {
|
|
13
|
+
return (
|
|
14
|
+
text
|
|
15
|
+
// Headers to bold (# Header -> *Header*)
|
|
16
|
+
.replace(/^#{1,6}\s+(.+)$/gm, "*$1*")
|
|
17
|
+
// Bold **text** -> *text*
|
|
18
|
+
.replace(/\*\*(.+?)\*\*/g, "*$1*")
|
|
19
|
+
// Links [text](url) -> <url|text>
|
|
20
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>")
|
|
21
|
+
// Inline code already works the same
|
|
22
|
+
// Bullet points already work the same
|
|
23
|
+
// Remove excessive blank lines
|
|
24
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the display name for an agent, with (dev) prefix if in development mode.
|
|
30
|
+
*/
|
|
31
|
+
function getAgentDisplayName(agent: Agent): string {
|
|
32
|
+
return isDev ? `(dev) ${agent.name}` : agent.name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a link to the task in the dashboard, or just the task ID if no APP_URL.
|
|
37
|
+
*/
|
|
38
|
+
function getTaskLink(taskId: string): string {
|
|
39
|
+
const shortId = taskId.slice(0, 8);
|
|
40
|
+
if (appUrl) {
|
|
41
|
+
return `<${appUrl}?tab=tasks&task=${taskId}&expand=true|\`${shortId}\`>`;
|
|
42
|
+
}
|
|
43
|
+
return `\`${shortId}\``;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send a task completion message to Slack with the agent's persona.
|
|
48
|
+
*/
|
|
49
|
+
export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
|
|
50
|
+
const app = getSlackApp();
|
|
51
|
+
if (!app || !task.slackChannelId || !task.slackThreadTs) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!task.agentId) {
|
|
56
|
+
console.error(`[Slack] Task ${task.id} has no assigned agent`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const agent = getAgentById(task.agentId);
|
|
61
|
+
if (!agent) {
|
|
62
|
+
console.error(`[Slack] Agent not found for task ${task.id}`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const client = app.client;
|
|
67
|
+
const taskLink = getTaskLink(task.id);
|
|
68
|
+
const footer = `_Check the full logs at ${taskLink}_`;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (task.status === "completed") {
|
|
72
|
+
const output = task.output || "_Task completed._";
|
|
73
|
+
const slackOutput = markdownToSlack(output);
|
|
74
|
+
await sendWithPersona(client, {
|
|
75
|
+
channel: task.slackChannelId,
|
|
76
|
+
thread_ts: task.slackThreadTs,
|
|
77
|
+
text: `${slackOutput}\n\n${footer}`,
|
|
78
|
+
username: getAgentDisplayName(agent),
|
|
79
|
+
icon_emoji: getAgentEmoji(agent),
|
|
80
|
+
});
|
|
81
|
+
} else if (task.status === "failed") {
|
|
82
|
+
const reason = task.failureReason || "Unknown error";
|
|
83
|
+
await sendWithPersona(client, {
|
|
84
|
+
channel: task.slackChannelId,
|
|
85
|
+
thread_ts: task.slackThreadTs,
|
|
86
|
+
text: `:x: *Task failed*\n\`\`\`${reason}\`\`\`\n${footer}`,
|
|
87
|
+
username: getAgentDisplayName(agent),
|
|
88
|
+
icon_emoji: getAgentEmoji(agent),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`[Slack] Failed to send response for task ${task.id}:`, error);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Send a progress update to Slack.
|
|
101
|
+
*/
|
|
102
|
+
export async function sendProgressUpdate(task: AgentTask, progress: string): Promise<boolean> {
|
|
103
|
+
const app = getSlackApp();
|
|
104
|
+
if (!app || !task.slackChannelId || !task.slackThreadTs) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!task.agentId) return false;
|
|
109
|
+
|
|
110
|
+
const agent = getAgentById(task.agentId);
|
|
111
|
+
if (!agent) return false;
|
|
112
|
+
|
|
113
|
+
const taskLink = getTaskLink(task.id);
|
|
114
|
+
const footer = `_Check progress at ${taskLink}_`;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await sendWithPersona(app.client, {
|
|
118
|
+
channel: task.slackChannelId,
|
|
119
|
+
thread_ts: task.slackThreadTs,
|
|
120
|
+
text: `:hourglass_flowing_sand: _${progress}_\n\n${footer}`,
|
|
121
|
+
username: getAgentDisplayName(agent),
|
|
122
|
+
icon_emoji: getAgentEmoji(agent),
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`[Slack] Failed to send progress update:`, error);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function sendWithPersona(
|
|
132
|
+
client: WebClient,
|
|
133
|
+
options: {
|
|
134
|
+
channel: string;
|
|
135
|
+
thread_ts: string;
|
|
136
|
+
text: string;
|
|
137
|
+
username: string;
|
|
138
|
+
icon_emoji: string;
|
|
139
|
+
},
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
await client.chat.postMessage({
|
|
142
|
+
channel: options.channel,
|
|
143
|
+
thread_ts: options.thread_ts,
|
|
144
|
+
text: options.text, // Fallback for notifications
|
|
145
|
+
username: options.username,
|
|
146
|
+
icon_emoji: options.icon_emoji,
|
|
147
|
+
blocks: [
|
|
148
|
+
{
|
|
149
|
+
type: "section",
|
|
150
|
+
text: {
|
|
151
|
+
type: "mrkdwn",
|
|
152
|
+
text: options.text,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getAgentEmoji(agent: Agent): string {
|
|
160
|
+
if (agent.isLead) return ":crown:";
|
|
161
|
+
|
|
162
|
+
// Generate consistent emoji based on agent name hash
|
|
163
|
+
const emojis = [
|
|
164
|
+
":robot_face:",
|
|
165
|
+
":gear:",
|
|
166
|
+
":zap:",
|
|
167
|
+
":rocket:",
|
|
168
|
+
":star:",
|
|
169
|
+
":crystal_ball:",
|
|
170
|
+
":bulb:",
|
|
171
|
+
":wrench:",
|
|
172
|
+
];
|
|
173
|
+
const hash = agent.name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
174
|
+
return emojis[hash % emojis.length] ?? ":robot_face:";
|
|
175
|
+
}
|