intelliTeam: Building a Production Multi-Agent System with Human-in-the-Loop
A complete guide to building intelliTeam — a multi-channel AI agent orchestration system featuring a concierge service, mission control, and autonomous worker agents. Built with Claude Agent SDK, TypeScript, and Convex DB.
intelliTeam: Building a Production Multi-Agent System with Human-in-the-Loop
The future of AI isn't a single superintelligent model — it's coordinated teams of specialized agents working together. While Anthropic recently published their approach to multi-agent research systems, and tools like claude-flow are pushing 60+ agent orchestration, there's been surprisingly little focus on human-agent communication as a first-class concern.
Today, we're open-sourcing the architecture behind intelliTeam — a production-ready multi-agent orchestration system that puts human accessibility at its core. Unlike pure automation systems, intelliTeam features:
- Multi-channel human interface (Telegram, WhatsApp, Discord, Signal, Slack)
- Concierge service that triages and routes human queries
- Mission Control for workspace/project state management
- Autonomous worker agents that execute complex tasks
- Full observability into agent activities and decisions
Let's build it from scratch.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ HUMAN LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ 📱 Telegram │ 💬 WhatsApp │ 🎮 Discord │ 📡 Signal │ 💼 Slack │
└───────┬───────┴───────┬───────┴───────┬──────┴──────┬──────┴───────┬───────┘
│ │ │ │ │
└───────────────┴───────┬───────┴─────────────┴──────────────┘
│
┌───────────▼───────────┐
│ 🚪 GATEWAY LAYER │
│ (Channel Adapters) │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ 🛎️ CONCIERGE AGENT │
│ • Query Analysis │
│ • Intent Detection │
│ • Routing Decisions │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ 🎛️ MISSION CONTROL │
│ (Convex Database) │
│ • Workspace State │
│ • Agent Registry │
│ • Task Queue │
│ • Event Log │
└───────────┬───────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐
│ 🔨 CODER │ │ 🔍 REVIEWER │ │ 🧪 TESTER │
│ Agent │ │ Agent │ │ Agent │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌───────────▼───────────┐
│ 🧠 CLAUDE API │
│ (Agent SDK) │
└───────────────────────┘
Core Components
1. The Concierge Agent
The Concierge is the single point of contact for all human interactions. It doesn't execute tasks — it understands, routes, and responds.
// src/agents/concierge.ts
import Anthropic from "@anthropic-ai/sdk";
import { ConvexClient } from "convex/browser";
import { api } from "../convex/_generated/api";
const CONCIERGE_SYSTEM_PROMPT = `You are the Concierge for intelliTeam, a multi-agent AI system.
Your responsibilities:
1. UNDERSTAND: Analyze incoming human queries for intent and context
2. INFORM: Answer questions about workspace state, project progress, agent activities
3. ROUTE: Forward complex tasks to appropriate specialist agents
4. SUMMARIZE: Provide human-readable updates on ongoing work
You have access to Mission Control via tools. Use them to:
- Query workspace state (projects, tasks, progress)
- Check agent availability and current assignments
- Create new tasks and assign to agents
- Retrieve recent activity logs
NEVER execute coding tasks yourself. Route them to specialist agents.
ALWAYS respond in the human's language.
Keep responses concise but complete.`;
export class ConciergeAgent {
private anthropic: Anthropic;
private convex: ConvexClient;
private channelId: string;
constructor(channelId: string) {
this.anthropic = new Anthropic();
this.convex = new ConvexClient(process.env.CONVEX_URL!);
this.channelId = channelId;
}
async handleMessage(userId: string, message: string): Promise<string> {
// Log incoming message
await this.convex.mutation(api.events.log, {
type: "human_message",
channelId: this.channelId,
userId,
content: message,
});
// Run agent loop with tools
const response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
system: CONCIERGE_SYSTEM_PROMPT,
tools: this.getMissionControlTools(),
messages: [{ role: "user", content: message }],
});
// Process tool calls in a loop
let currentResponse = response;
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: message },
];
while (currentResponse.stop_reason === "tool_use") {
const assistantContent = currentResponse.content;
messages.push({ role: "assistant", content: assistantContent });
const toolResults = await this.executeTools(assistantContent);
messages.push({ role: "user", content: toolResults });
currentResponse = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
system: CONCIERGE_SYSTEM_PROMPT,
tools: this.getMissionControlTools(),
messages,
});
}
// Extract text response
const textBlock = currentResponse.content.find(
(block) => block.type === "text"
);
const reply = textBlock?.text || "I couldn't process that request.";
// Log response
await this.convex.mutation(api.events.log, {
type: "concierge_response",
channelId: this.channelId,
userId,
content: reply,
});
return reply;
}
private getMissionControlTools(): Anthropic.Tool[] {
return [
{
name: "get_workspaces",
description: "List all workspaces/projects known to the team",
input_schema: {
type: "object" as const,
properties: {
status: {
type: "string",
enum: ["active", "archived", "all"],
description: "Filter by workspace status",
},
},
},
},
{
name: "get_workspace_state",
description: "Get detailed state of a specific workspace",
input_schema: {
type: "object" as const,
properties: {
workspaceId: { type: "string", description: "Workspace ID" },
},
required: ["workspaceId"],
},
},
{
name: "get_agent_status",
description: "Get status of all agents or a specific agent",
input_schema: {
type: "object" as const,
properties: {
agentId: {
type: "string",
description: "Optional: specific agent ID",
},
},
},
},
{
name: "create_task",
description: "Create a new task and optionally assign to an agent",
input_schema: {
type: "object" as const,
properties: {
workspaceId: { type: "string" },
title: { type: "string" },
description: { type: "string" },
priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
assignTo: { type: "string", description: "Agent type: coder, reviewer, tester" },
},
required: ["workspaceId", "title", "description"],
},
},
{
name: "get_recent_activity",
description: "Get recent activity log for a workspace or agent",
input_schema: {
type: "object" as const,
properties: {
workspaceId: { type: "string" },
agentId: { type: "string" },
limit: { type: "number", description: "Max events to return" },
},
},
},
{
name: "send_to_agent",
description: "Forward a message/task to a specialist agent",
input_schema: {
type: "object" as const,
properties: {
agentType: {
type: "string",
enum: ["coder", "reviewer", "tester", "architect", "devops"],
},
taskId: { type: "string" },
message: { type: "string" },
priority: { type: "string", enum: ["normal", "urgent"] },
},
required: ["agentType", "message"],
},
},
];
}
private async executeTools(
content: Anthropic.ContentBlock[]
): Promise<Anthropic.ToolResultBlockParam[]> {
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of content) {
if (block.type === "tool_use") {
const result = await this.executeTool(block.name, block.input);
results.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
return results;
}
private async executeTool(name: string, input: unknown): Promise<unknown> {
const params = input as Record<string, unknown>;
switch (name) {
case "get_workspaces":
return this.convex.query(api.workspaces.list, {
status: params.status as string,
});
case "get_workspace_state":
return this.convex.query(api.workspaces.getState, {
id: params.workspaceId as string,
});
case "get_agent_status":
return this.convex.query(api.agents.getStatus, {
agentId: params.agentId as string,
});
case "create_task":
return this.convex.mutation(api.tasks.create, {
workspaceId: params.workspaceId as string,
title: params.title as string,
description: params.description as string,
priority: params.priority as string,
assignTo: params.assignTo as string,
});
case "get_recent_activity":
return this.convex.query(api.events.getRecent, {
workspaceId: params.workspaceId as string,
agentId: params.agentId as string,
limit: (params.limit as number) || 20,
});
case "send_to_agent":
return this.convex.mutation(api.agents.sendMessage, {
agentType: params.agentType as string,
taskId: params.taskId as string,
message: params.message as string,
priority: params.priority as string,
});
default:
return { error: `Unknown tool: ${name}` };
}
}
}
2. Mission Control (Convex Database)
Mission Control is the single source of truth for all system state. We use Convex for its real-time subscriptions and seamless TypeScript integration.
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
// Workspaces/Projects
workspaces: defineTable({
name: v.string(),
description: v.string(),
repoUrl: v.optional(v.string()),
status: v.union(v.literal("active"), v.literal("paused"), v.literal("archived")),
createdAt: v.number(),
updatedAt: v.number(),
metadata: v.optional(v.any()),
}).index("by_status", ["status"]),
// Agent Registry
agents: defineTable({
type: v.union(
v.literal("concierge"),
v.literal("coder"),
v.literal("reviewer"),
v.literal("tester"),
v.literal("architect"),
v.literal("devops")
),
instanceId: v.string(),
status: v.union(v.literal("idle"), v.literal("busy"), v.literal("offline")),
currentTaskId: v.optional(v.id("tasks")),
lastHeartbeat: v.number(),
capabilities: v.array(v.string()),
metadata: v.optional(v.any()),
})
.index("by_type", ["type"])
.index("by_status", ["status"]),
// Task Queue
tasks: defineTable({
workspaceId: v.id("workspaces"),
title: v.string(),
description: v.string(),
status: v.union(
v.literal("pending"),
v.literal("assigned"),
v.literal("in_progress"),
v.literal("review"),
v.literal("completed"),
v.literal("failed")
),
priority: v.union(
v.literal("low"),
v.literal("medium"),
v.literal("high"),
v.literal("urgent")
),
assignedAgentId: v.optional(v.id("agents")),
assignedAgentType: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
completedAt: v.optional(v.number()),
result: v.optional(v.any()),
parentTaskId: v.optional(v.id("tasks")),
})
.index("by_workspace", ["workspaceId"])
.index("by_status", ["status"])
.index("by_agent", ["assignedAgentId"]),
// Event Log (Activity Stream)
events: defineTable({
type: v.string(),
workspaceId: v.optional(v.id("workspaces")),
agentId: v.optional(v.id("agents")),
taskId: v.optional(v.id("tasks")),
channelId: v.optional(v.string()),
userId: v.optional(v.string()),
content: v.string(),
metadata: v.optional(v.any()),
timestamp: v.number(),
})
.index("by_workspace", ["workspaceId", "timestamp"])
.index("by_agent", ["agentId", "timestamp"])
.index("by_type", ["type", "timestamp"]),
// Agent Messages (Inter-agent communication)
messages: defineTable({
fromAgentId: v.optional(v.id("agents")),
toAgentType: v.string(),
taskId: v.optional(v.id("tasks")),
content: v.string(),
priority: v.union(v.literal("normal"), v.literal("urgent")),
status: v.union(v.literal("pending"), v.literal("delivered"), v.literal("processed")),
createdAt: v.number(),
processedAt: v.optional(v.number()),
})
.index("by_recipient", ["toAgentType", "status"])
.index("by_task", ["taskId"]),
});
// convex/workspaces.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { status: v.optional(v.string()) },
handler: async (ctx, args) => {
if (args.status && args.status !== "all") {
return ctx.db
.query("workspaces")
.withIndex("by_status", (q) => q.eq("status", args.status as any))
.collect();
}
return ctx.db.query("workspaces").collect();
},
});
export const getState = query({
args: { id: v.string() },
handler: async (ctx, args) => {
const workspace = await ctx.db
.query("workspaces")
.filter((q) => q.eq(q.field("_id"), args.id))
.first();
if (!workspace) return null;
// Get active tasks
const tasks = await ctx.db
.query("tasks")
.withIndex("by_workspace", (q) => q.eq("workspaceId", workspace._id))
.collect();
// Get recent events
const events = await ctx.db
.query("events")
.withIndex("by_workspace", (q) => q.eq("workspaceId", workspace._id))
.order("desc")
.take(10);
// Calculate stats
const stats = {
totalTasks: tasks.length,
pendingTasks: tasks.filter((t) => t.status === "pending").length,
inProgressTasks: tasks.filter((t) => t.status === "in_progress").length,
completedTasks: tasks.filter((t) => t.status === "completed").length,
};
return {
...workspace,
tasks,
recentEvents: events,
stats,
};
},
});
export const create = mutation({
args: {
name: v.string(),
description: v.string(),
repoUrl: v.optional(v.string()),
},
handler: async (ctx, args) => {
const now = Date.now();
return ctx.db.insert("workspaces", {
...args,
status: "active",
createdAt: now,
updatedAt: now,
});
},
});
// convex/agents.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const getStatus = query({
args: { agentId: v.optional(v.string()) },
handler: async (ctx, args) => {
if (args.agentId) {
return ctx.db
.query("agents")
.filter((q) => q.eq(q.field("_id"), args.agentId))
.first();
}
// Return all agents with their current tasks
const agents = await ctx.db.query("agents").collect();
return Promise.all(
agents.map(async (agent) => {
let currentTask = null;
if (agent.currentTaskId) {
currentTask = await ctx.db.get(agent.currentTaskId);
}
return { ...agent, currentTask };
})
);
},
});
export const register = mutation({
args: {
type: v.string(),
instanceId: v.string(),
capabilities: v.array(v.string()),
},
handler: async (ctx, args) => {
// Check if already registered
const existing = await ctx.db
.query("agents")
.filter((q) => q.eq(q.field("instanceId"), args.instanceId))
.first();
if (existing) {
// Update heartbeat
await ctx.db.patch(existing._id, {
status: "idle",
lastHeartbeat: Date.now(),
});
return existing._id;
}
return ctx.db.insert("agents", {
type: args.type as any,
instanceId: args.instanceId,
status: "idle",
lastHeartbeat: Date.now(),
capabilities: args.capabilities,
});
},
});
export const heartbeat = mutation({
args: { agentId: v.id("agents") },
handler: async (ctx, args) => {
await ctx.db.patch(args.agentId, {
lastHeartbeat: Date.now(),
});
},
});
export const sendMessage = mutation({
args: {
agentType: v.string(),
taskId: v.optional(v.string()),
message: v.string(),
priority: v.optional(v.string()),
},
handler: async (ctx, args) => {
return ctx.db.insert("messages", {
toAgentType: args.agentType,
taskId: args.taskId as any,
content: args.message,
priority: (args.priority as any) || "normal",
status: "pending",
createdAt: Date.now(),
});
},
});
export const claimMessages = mutation({
args: {
agentType: v.string(),
agentId: v.id("agents"),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_recipient", (q) =>
q.eq("toAgentType", args.agentType).eq("status", "pending")
)
.take(args.limit || 1);
// Mark as delivered
for (const msg of messages) {
await ctx.db.patch(msg._id, { status: "delivered" });
}
// Update agent status
if (messages.length > 0) {
await ctx.db.patch(args.agentId, { status: "busy" });
}
return messages;
},
});
3. Worker Agents
Worker agents are specialized executors that claim tasks from the queue and process them autonomously.
// src/agents/worker.ts
import Anthropic from "@anthropic-ai/sdk";
import { ConvexClient } from "convex/browser";
import { api } from "../convex/_generated/api";
import { spawn } from "child_process";
interface WorkerConfig {
type: "coder" | "reviewer" | "tester" | "architect" | "devops";
instanceId: string;
workDir: string;
}
const AGENT_PROMPTS: Record<string, string> = {
coder: `You are a Senior Software Engineer agent. Your job is to:
1. Implement features and fix bugs based on task descriptions
2. Write clean, well-documented, tested code
3. Follow project conventions and best practices
4. Report progress and blockers to Mission Control
You have access to file system tools and can execute commands.
Always explain your changes before making them.`,
reviewer: `You are a Code Review Specialist agent. Your job is to:
1. Review code changes for quality, security, and best practices
2. Identify potential bugs, performance issues, and security vulnerabilities
3. Suggest improvements with clear explanations
4. Approve or request changes with actionable feedback
Be thorough but constructive. Focus on significant issues.`,
tester: `You are a QA Engineer agent. Your job is to:
1. Write and run tests for new features
2. Identify edge cases and potential failure modes
3. Verify bug fixes actually resolve the issue
4. Report test coverage and results
Prioritize critical paths and high-risk areas.`,
};
export class WorkerAgent {
private anthropic: Anthropic;
private convex: ConvexClient;
private config: WorkerConfig;
private agentId: string | null = null;
constructor(config: WorkerConfig) {
this.anthropic = new Anthropic();
this.convex = new ConvexClient(process.env.CONVEX_URL!);
this.config = config;
}
async start(): Promise<void> {
// Register with Mission Control
this.agentId = await this.convex.mutation(api.agents.register, {
type: this.config.type,
instanceId: this.config.instanceId,
capabilities: this.getCapabilities(),
});
console.log(`[${this.config.type}] Registered as ${this.agentId}`);
// Start heartbeat
setInterval(() => this.heartbeat(), 30000);
// Start work loop
this.workLoop();
}
private getCapabilities(): string[] {
switch (this.config.type) {
case "coder":
return ["read_file", "write_file", "execute_command", "git"];
case "reviewer":
return ["read_file", "git_diff", "comment"];
case "tester":
return ["read_file", "execute_command", "test_runner"];
default:
return ["read_file"];
}
}
private async heartbeat(): Promise<void> {
if (this.agentId) {
await this.convex.mutation(api.agents.heartbeat, {
agentId: this.agentId as any,
});
}
}
private async workLoop(): Promise<void> {
while (true) {
try {
// Check for messages
const messages = await this.convex.mutation(api.agents.claimMessages, {
agentType: this.config.type,
agentId: this.agentId as any,
limit: 1,
});
if (messages.length > 0) {
await this.processMessage(messages[0]);
} else {
// No work, wait before checking again
await this.sleep(5000);
}
} catch (error) {
console.error(`[${this.config.type}] Error in work loop:`, error);
await this.sleep(10000);
}
}
}
private async processMessage(message: any): Promise<void> {
console.log(`[${this.config.type}] Processing: ${message.content.slice(0, 100)}...`);
await this.convex.mutation(api.events.log, {
type: "agent_task_start",
agentId: this.agentId as any,
taskId: message.taskId,
content: `Started processing: ${message.content.slice(0, 100)}`,
timestamp: Date.now(),
});
// Run agent with tools
const response = await this.runAgent(message.content);
// Log completion
await this.convex.mutation(api.events.log, {
type: "agent_task_complete",
agentId: this.agentId as any,
taskId: message.taskId,
content: response,
timestamp: Date.now(),
});
// Mark message as processed
await this.convex.mutation(api.messages.markProcessed, {
messageId: message._id,
});
// Update agent status
await this.convex.mutation(api.agents.setStatus, {
agentId: this.agentId as any,
status: "idle",
});
}
private async runAgent(task: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
let response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 8192,
system: AGENT_PROMPTS[this.config.type],
tools: this.getTools(),
messages,
});
// Tool use loop
while (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults = await this.executeTools(response.content);
messages.push({ role: "user", content: toolResults });
response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 8192,
system: AGENT_PROMPTS[this.config.type],
tools: this.getTools(),
messages,
});
}
const textBlock = response.content.find((b) => b.type === "text");
return textBlock?.text || "Task completed.";
}
private getTools(): Anthropic.Tool[] {
const baseTools: Anthropic.Tool[] = [
{
name: "read_file",
description: "Read contents of a file",
input_schema: {
type: "object" as const,
properties: {
path: { type: "string", description: "File path relative to workspace" },
},
required: ["path"],
},
},
{
name: "list_directory",
description: "List files in a directory",
input_schema: {
type: "object" as const,
properties: {
path: { type: "string", description: "Directory path" },
},
required: ["path"],
},
},
];
if (this.config.type === "coder") {
baseTools.push(
{
name: "write_file",
description: "Write content to a file",
input_schema: {
type: "object" as const,
properties: {
path: { type: "string" },
content: { type: "string" },
},
required: ["path", "content"],
},
},
{
name: "execute_command",
description: "Execute a shell command",
input_schema: {
type: "object" as const,
properties: {
command: { type: "string" },
cwd: { type: "string", description: "Working directory" },
},
required: ["command"],
},
}
);
}
return baseTools;
}
private async executeTools(
content: Anthropic.ContentBlock[]
): Promise<Anthropic.ToolResultBlockParam[]> {
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of content) {
if (block.type === "tool_use") {
try {
const result = await this.executeTool(block.name, block.input);
results.push({
type: "tool_result",
tool_use_id: block.id,
content: typeof result === "string" ? result : JSON.stringify(result),
});
} catch (error) {
results.push({
type: "tool_result",
tool_use_id: block.id,
content: `Error: ${error}`,
is_error: true,
});
}
}
}
return results;
}
private async executeTool(name: string, input: unknown): Promise<unknown> {
const params = input as Record<string, unknown>;
const path = require("path");
const fs = require("fs").promises;
const resolvePath = (p: string) =>
path.resolve(this.config.workDir, p);
switch (name) {
case "read_file":
return fs.readFile(resolvePath(params.path as string), "utf-8");
case "list_directory":
return fs.readdir(resolvePath(params.path as string));
case "write_file":
await fs.writeFile(
resolvePath(params.path as string),
params.content as string
);
return "File written successfully";
case "execute_command":
return this.executeCommand(
params.command as string,
params.cwd ? resolvePath(params.cwd as string) : this.config.workDir
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
private executeCommand(command: string, cwd: string): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn("sh", ["-c", command], { cwd });
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => (stdout += data));
child.stderr.on("data", (data) => (stderr += data));
child.on("close", (code) => {
if (code === 0) {
resolve(stdout || "Command completed successfully");
} else {
reject(new Error(`Exit code ${code}: ${stderr}`));
}
});
});
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
4. Channel Adapters (Multi-Channel Gateway)
The Gateway layer handles channel-specific protocols and normalizes messages for the Concierge.
// src/gateway/telegram.ts
import { Telegraf } from "telegraf";
import { ConciergeAgent } from "../agents/concierge";
export function createTelegramGateway(token: string): Telegraf {
const bot = new Telegraf(token);
const concierge = new ConciergeAgent("telegram");
bot.on("text", async (ctx) => {
const userId = ctx.from.id.toString();
const message = ctx.message.text;
// Show typing indicator
await ctx.sendChatAction("typing");
try {
const response = await concierge.handleMessage(userId, message);
// Split long messages
const chunks = splitMessage(response, 4000);
for (const chunk of chunks) {
await ctx.reply(chunk, { parse_mode: "Markdown" });
}
} catch (error) {
console.error("Telegram handler error:", error);
await ctx.reply("Sorry, I encountered an error processing your request.");
}
});
return bot;
}
function splitMessage(text: string, maxLength: number): string[] {
if (text.length <= maxLength) return [text];
const chunks: string[] = [];
let remaining = text;
while (remaining.length > 0) {
if (remaining.length <= maxLength) {
chunks.push(remaining);
break;
}
// Find a good split point
let splitAt = remaining.lastIndexOf("\n\n", maxLength);
if (splitAt === -1) splitAt = remaining.lastIndexOf("\n", maxLength);
if (splitAt === -1) splitAt = remaining.lastIndexOf(" ", maxLength);
if (splitAt === -1) splitAt = maxLength;
chunks.push(remaining.slice(0, splitAt));
remaining = remaining.slice(splitAt).trim();
}
return chunks;
}
// src/gateway/discord.ts
import { Client, GatewayIntentBits, Events } from "discord.js";
import { ConciergeAgent } from "../agents/concierge";
export function createDiscordGateway(token: string): Client {
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
});
const concierge = new ConciergeAgent("discord");
client.once(Events.ClientReady, () => {
console.log(`Discord gateway ready as ${client.user?.tag}`);
});
client.on(Events.MessageCreate, async (message) => {
// Ignore bots
if (message.author.bot) return;
// Only respond to mentions or DMs
const isMentioned = message.mentions.has(client.user!);
const isDM = !message.guild;
if (!isMentioned && !isDM) return;
const userId = message.author.id;
const content = message.content
.replace(`<@${client.user!.id}>`, "")
.trim();
if (!content) return;
try {
await message.channel.sendTyping();
const response = await concierge.handleMessage(userId, content);
// Discord has 2000 char limit
const chunks = splitMessage(response, 1900);
for (const chunk of chunks) {
await message.reply(chunk);
}
} catch (error) {
console.error("Discord handler error:", error);
await message.reply("Sorry, I encountered an error.");
}
});
client.login(token);
return client;
}
Docker Compose Deployment
The entire system runs as a Docker Compose stack with isolated containers for each component.
# docker-compose.yml
version: "3.9"
services:
# Gateway - Multi-channel message router
gateway:
build:
context: .
dockerfile: Dockerfile.gateway
environment:
- CONVEX_URL=${CONVEX_URL}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
- SLACK_APP_TOKEN=${SLACK_APP_TOKEN}
restart: unless-stopped
networks:
- intelliteam
# Concierge - Human interface agent
concierge:
build:
context: .
dockerfile: Dockerfile.agent
environment:
- AGENT_TYPE=concierge
- INSTANCE_ID=concierge-${HOSTNAME:-main}
- CONVEX_URL=${CONVEX_URL}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
restart: unless-stopped
networks:
- intelliteam
# Coder Agent
coder:
build:
context: .
dockerfile: Dockerfile.agent
environment:
- AGENT_TYPE=coder
- INSTANCE_ID=coder-${HOSTNAME:-main}
- CONVEX_URL=${CONVEX_URL}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
volumes:
- workspaces:/workspaces
restart: unless-stopped
networks:
- intelliteam
# Reviewer Agent
reviewer:
build:
context: .
dockerfile: Dockerfile.agent
environment:
- AGENT_TYPE=reviewer
- INSTANCE_ID=reviewer-${HOSTNAME:-main}
- CONVEX_URL=${CONVEX_URL}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
volumes:
- workspaces:/workspaces:ro
restart: unless-stopped
networks:
- intelliteam
# Tester Agent
tester:
build:
context: .
dockerfile: Dockerfile.agent
environment:
- AGENT_TYPE=tester
- INSTANCE_ID=tester-${HOSTNAME:-main}
- CONVEX_URL=${CONVEX_URL}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
volumes:
- workspaces:/workspaces
restart: unless-stopped
networks:
- intelliteam
volumes:
workspaces:
networks:
intelliteam:
driver: bridge
# Dockerfile.agent
FROM node:22-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --production
# Copy source
COPY dist/ ./dist/
COPY convex/ ./convex/
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "process.exit(0)"
CMD ["node", "dist/agent.js"]
MCP Integration
For teams using Claude Code or Claude Desktop, intelliTeam exposes an MCP server for direct integration.
// src/mcp/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ConvexClient } from "convex/browser";
import { api } from "../convex/_generated/api";
const server = new Server(
{ name: "intelliteam", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } }
);
const convex = new ConvexClient(process.env.CONVEX_URL!);
// Tools
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "team_status",
description: "Get status of all intelliTeam agents and workspaces",
inputSchema: { type: "object", properties: {} },
},
{
name: "assign_task",
description: "Assign a task to a specific agent type",
inputSchema: {
type: "object",
properties: {
agentType: {
type: "string",
enum: ["coder", "reviewer", "tester"],
},
workspaceId: { type: "string" },
task: { type: "string" },
priority: {
type: "string",
enum: ["low", "medium", "high", "urgent"],
},
},
required: ["agentType", "task"],
},
},
{
name: "get_workspace",
description: "Get detailed state of a workspace",
inputSchema: {
type: "object",
properties: {
workspaceId: { type: "string" },
},
required: ["workspaceId"],
},
},
],
}));
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "team_status":
const agents = await convex.query(api.agents.getStatus, {});
const workspaces = await convex.query(api.workspaces.list, { status: "active" });
return {
content: [
{
type: "text",
text: JSON.stringify({ agents, workspaces }, null, 2),
},
],
};
case "assign_task":
const messageId = await convex.mutation(api.agents.sendMessage, {
agentType: args.agentType,
message: args.task,
priority: args.priority || "medium",
});
return {
content: [
{ type: "text", text: `Task assigned. Message ID: ${messageId}` },
],
};
case "get_workspace":
const state = await convex.query(api.workspaces.getState, {
id: args.workspaceId,
});
return {
content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
};
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
Add to Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"intelliteam": {
"command": "node",
"args": ["/path/to/intelliteam/dist/mcp/server.js"],
"env": {
"CONVEX_URL": "https://your-project.convex.cloud"
}
}
}
}
Usage Examples
Via Telegram
You: What projects is the team working on?
🛎️ Concierge: Currently tracking 3 active workspaces:
1. **intelliteam-core** - The orchestration system itself
- 2 tasks in progress (coder: auth refactor, tester: integration tests)
- 5 tasks completed this week
2. **customer-portal** - React dashboard
- 1 task in review
- Waiting for reviewer agent
3. **api-v2** - New API endpoints
- 3 pending tasks
- No agent assigned yet
Would you like details on any specific project?
You: Have the coder implement dark mode for customer-portal
🛎️ Concierge: ✅ Task created and assigned to Coder agent:
**Task:** Implement dark mode for customer-portal
**Priority:** Medium
**Status:** Assigned → In Progress
The coder agent is now working on this. I'll notify you when it's ready for review.
Via MCP (Claude Code)
> /mcp intelliteam team_status
{
"agents": [
{ "type": "coder", "status": "busy", "currentTask": "Implement dark mode" },
{ "type": "reviewer", "status": "idle" },
{ "type": "tester", "status": "idle" }
],
"workspaces": [
{ "name": "customer-portal", "status": "active", "tasks": 4 }
]
}
> /mcp intelliteam assign_task --agentType=reviewer --task="Review dark mode PR #42"
Task assigned. Message ID: msg_abc123
Scaling Considerations
Horizontal Scaling
# docker-compose.scale.yml
services:
coder:
deploy:
replicas: 3
resources:
limits:
memory: 2G
Each coder instance registers with a unique instanceId and claims tasks independently. Convex handles the coordination.
Priority Queues
// convex/messages.ts - Priority-aware claiming
export const claimMessages = mutation({
handler: async (ctx, args) => {
// Urgent first
let messages = await ctx.db
.query("messages")
.withIndex("by_recipient", (q) =>
q.eq("toAgentType", args.agentType).eq("status", "pending")
)
.filter((q) => q.eq(q.field("priority"), "urgent"))
.take(1);
// Then normal
if (messages.length === 0) {
messages = await ctx.db
.query("messages")
.withIndex("by_recipient", (q) =>
q.eq("toAgentType", args.agentType).eq("status", "pending")
)
.take(1);
}
return messages;
},
});
Observability
The event log in Convex provides full audit trail and enables:
- Real-time dashboards (Convex subscriptions)
- Metrics export to Prometheus/Grafana
- Alert rules on stuck tasks or agent failures
What's Next
intelliTeam is just the beginning. We're working on:
- Self-healing agents — Automatic restart and task reassignment on failures
- Learning from outcomes — Agents that improve based on review feedback
- Cross-workspace context — Agents that understand relationships between projects
- Voice interface — WhatsApp/Telegram voice messages → transcription → Concierge
The future of software development isn't humans OR AI — it's humans WITH teams of specialized AI agents, communicating through the channels we already use every day.
Getting Started
# Clone the repo
git clone https://github.com/intellibrain/intelliteam
cd intelliteam
# Setup Convex
npx convex dev
# Configure environment
cp .env.example .env
# Add your ANTHROPIC_API_KEY, TELEGRAM_BOT_TOKEN, etc.
# Start the stack
docker compose up -d
# Check status
docker compose logs -f
Full source code and documentation: github.com/intellibrain/intelliteam
intelliTeam is developed by intellibrain — building the future of human-AI collaboration.