← Back to Blog
EN2026-02-05

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.

By intellibrain
aiagentsmulti-agentorchestrationclaudetypescriptconvexdocker

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:

  1. Self-healing agents — Automatic restart and task reassignment on failures
  2. Learning from outcomes — Agents that improve based on review feedback
  3. Cross-workspace context — Agents that understand relationships between projects
  4. 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.

intelliBrain

AI-augmented software development. Based in Zürich, working globally.

© 2026 intelliBrain GmbH. All rights reserved.Imprint
BUILT WITH 🧠 + AI