Skip to content
Conduit

mcp server

kaiban-distributed

Kaiban Distributed - Distributed Agentic AI Platform - Actor Model Kanban Framework Implementation with Enterprise Messaging Layer State (Redis/Kafka) and Individual Deployed AI Nodes (Node.js/TypeScr

andreibesleaga/kaiban-distributedcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 21, 2026

Kaiban Distributed - Multi-Agent AI System

Implementation of Distributed Actor Model for AI swarms using TypeScript/Node.js, Redis/Kafka, OpenTelemetry.

  • One of the first open-source projects to combine Enterprise Messaging (Kafka/Redis), Distributed Actor-Model Isolation, AI Multi-Agent Orchestration, and Kanban Visualization into a single JavaScript-ecosystem runtime, for agents and humans.

Distributed horizontally-scalable Actor-Model Multi-Agent System Runtime, using Kanban style visualization for workflows.

Run multiple AI agents teams with independently deployed Node.js processes, real-time visibility and multi-agent orchestration via Redis/Kafka pub/sub, A2A, and MCP.

For running the examples see EXAMPLES.md. For technical documentation, check the files in docs/.

(For integrating KaibanJS system base, follow documents in docs/KAIBANJS_INTEGRATION.md).

Some short articles can be read on Medium and Substack.

Examples dedicated repository: kaiban-distributed-examples

  • While most frameworks treat agents as scripts, Kaiban Distributed treats them as Stateful Actors. By using async Node.js, each agent operates in its own space, communicating via a pluggable Messaging Abstraction Layer (Kafka or BullMQ/Redis), horizontally scaling AI workforce.

  • The systems allows creation of Teams of Agents for various Tasks, Systems Integrations, and Data handling/analytics, with scalable AI workflows, from local usage to customizable Enterprise Grade Systems.

  • Integrates with existing KaibanJS system and agents, external agentic systems, or any service that can publish via A2A / MCP / Redis / Kafka — connecting them into actor-model team flows or peer-to-peer coordination.

  • Any Agentic AI Patterns (Loops & Harnesses), Team Archetypes, Workflows, and Human-in-The-Loop (BudgetCheckings) interruptions could be implemented programmatically at any point of the workflow/tasks, given the Actor-Model Isolation, Distributed Node Agents, Custom Orchestrators/Coreography, Messaging Layer - in JS/TS, or via other A2A/MCP comms and agents, or by any other programs and connectors into the messaging layer (visually connected kanban boards and any other display methods connecting directly to the published workflows messages on Redis/Kafka, such as CLI I/O, Enterprise Programs, etc.

CI Coverage OpenSSF Scorecard Security Policy TypeScript Node License

Published on npm as kaiban-distributed (canonical) and mirrored on GitHub Packages as @andreibesleaga/kaiban-distributed; the container image is ghcr.io/andreibesleaga/kaiban-distributed.


Summary

# 1. Clone and install
git clone https://github.com/andreibesleaga/kaiban-distributed
cd kaiban-distributed && npm install

# 2. Configure
cp .env.example .env
# Edit .env — add OPENROUTER_API_KEY or OPENAI_API_KEY + AGENT_IDS

# 3. Start the full blog-team demo (Docker Compose, workers, gateway, orchestrator, monitor)
./scripts/blog-team.sh start

# or the Global Research Distributed Team
./scripts/global-research.sh start
# use flags for chaos (20% searcher crash rate) and number of parallel instances
# ./scripts/global-research.sh start --chaos --searchers 6

# → Script prints board URLs when the gateway is ready. Open one in a separate terminal/tab.

# 4. Open the board (choose one, in a separate terminal)
cd board && npm install && npm run dev   # React board → http://localhost:5173
#  — OR —
# Open examples/blog-team/viewer/board.html in your browser (zero setup)

# 5. Stop everything cleanly when done
./scripts/blog-team.sh stop
# ./scripts/global-research.sh stop

To wire your own agent into a distributed worker node:

import { BullMQDriver } from './src/infrastructure/messaging/bullmq-driver';
import { AgentActor } from './src/application/actor/AgentActor';
import { createKaibanTaskHandler } from './src/infrastructure/kaibanjs/kaiban-agent-bridge';
import { createAgentStatePublisher } from './src/shared';

const driver = new BullMQDriver({ connection: { host: 'localhost', port: 6379 } });

// Applies MAX_TOKEN_BUDGET / MAX_TASK_TOKEN_BUDGET from the environment
// (defaults 200000 / 50000; the literal 'unlimited' removes a cap).
const statePublisher = createAgentStatePublisher('redis://localhost:6379', {
  agentId: 'my-agent', name: 'Ada', role: 'Analyst',
});

const handler = statePublisher.wrapHandler(
  createKaibanTaskHandler({
    name: 'Ada', role: 'Analyst',
    goal: 'Analyse datasets and produce structured summaries',
    background: 'Expert in data analysis and statistics',
    llmConfig: { provider: 'openai', model: 'gpt-4o-mini', apiKey: process.env.OPENAI_API_KEY },
  }, driver)
);

const actor = new AgentActor('my-agent', driver, 'kaiban-agents-my-agent', handler);
await actor.start();
statePublisher.publishIdle();  // board shows agent as IDLE within 15s

Running example - blog-team

(Blog team of: researcher, writer, editor - nodes distributed locally over docker services with Redis/Kafka messaging between them and their processes tasks status and results)


Architecture

C4 container view

flowchart TB
  subgraph Clients
    Board["Board UI<br/>(React/Vite or static HTML)"]
    Ext["External orchestrator / agent<br/>(A2A JSON-RPC)"]
  end
  subgraph Gateway["Edge Gateway :3000"]
    GW["GatewayApp<br/>HTTP: /health · /agent-card · /a2a/rpc"]
    SG["SocketGateway<br/>(Socket.io)"]
  end
  Redis[("Redis 7<br/>BullMQ queues + state pub/sub")]
  Kafka[("Kafka<br/>(optional transport)")]
  subgraph Workers["Agent worker nodes (1..N)"]
    A1["AgentActor<br/>+ KaibanJS bridge<br/>+ AgentStatePublisher"]
  end
  Ext -- "message/send (A2A v0.3)" --> GW
  GW -- "publish task" --> Redis
  Redis -- "consume (kaiban-agents-*)" --> A1
  Kafka -. "alt transport" .- A1
  A1 -- "state delta (kaiban-state-events)" --> Redis
  A1 -- "events-completed / failed" --> Redis
  Redis -- "state events" --> SG
  SG -- "ws state + hitl:decision" --> Board

Detailed ASCII view

┌──────────────────────────────────────────────────────────────────────┐
│  Board Viewers (browser)                                             │
│  A. board/  — React + Vite app  (npm run dev → :5173)                │
│     Interactive HITL: Approve / Revise / Reject buttons              │
│     socket.emit('hitl:decision') ───────────────────────────────┐    │
│  B. examples/blog-team/viewer/board.html  (zero-setup)          │    │
│  Socket.io client ──────────────────────────────────────────────┼────┘
└─────────────────────────────────────────────────────────────────│────┘
                                                                  │ ws
┌─────────────────────────────────────────────────────────────────▼────┐
│  Edge Gateway  (port 3000)                                           │
│  GatewayApp:   GET /health · GET /.well-known/agent-card.json        │
│                POST /a2a/rpc  (JSON-RPC 2.0 → routes to queue)       │
│  SocketGateway: subscribes Redis kaiban-state-events → Socket.io     │
│                 listens 'hitl:decision' → publishes kaiban-hitl-     │
│                 decisions (Redis) → orchestrator picks up decision   │
└──────────────────────────────────────────────────────────────────────┘
         │ BullMQ / Kafka task queues        │ Redis Pub/Sub
         │ kaiban-agents-{agentId}           │ kaiban-state-events
┌────────▼──────────┐  ┌────────▼────────┐  ┌────────▼───────────────┐
│ Worker: researcher│  │ Worker: writer  │  │ Worker: editor         │
│  AgentActor       │  │  AgentActor     │  │  AgentActor            │
│  KaibanAgentBridge│  │  KaibanBridge   │  │  KaibanBridge          │
│  → Agent.workOn() │  │  → Agent.work() │  │  → Agent.work()        │
│  AgentState       │  │  AgentState     │  │  AgentState            │
│  Publisher        │  │  Publisher      │  │  Publisher             │
│  (ioredis pub/sub)│  │  (ioredis)      │  │  (ioredis)             │
└───────────────────┘  └─────────────────┘  └────────────────────────┘
         │                      │                        │
         └──────────────────────┴────────────────────────┘
                                │
                    ┌───────────▼────────────┐
                    │  Redis 7 (always)      │
                    │  BullMQ queues +       │
                    │  kaiban-state-events   │
                    └────────────────────────┘

  Optional: Kafka (MESSAGING_DRIVER=kafka)
┌─────────────────────────────────────────────────────────────────────┐
│  Zookeeper + Kafka — high-throughput alternative to BullMQ          │
│  KafkaDriver implements IMessagingDriver (swap via env var)         │
│  State broadcast still uses Redis Pub/Sub (SocketGateway)           │
└─────────────────────────────────────────────────────────────────────┘

High-Level Distributed Topology Example

graph TD
    UI[Kanban Board UI<br>Browser] -- WebSockets --> Gateway[Edge Gateway APP<br>Port 3000]
    Gateway -- Pub/Sub (Socket.io) --> RedisPubSub[(Redis Pub/Sub<br>State Stream)]
    Gateway -- HTTP POST --> A2A[A2A Connector]
    
    A2A --> Queue[(Message Queue<br>BullMQ / Kafka)]
    
    Queue --> |Tasks| AgentA[Node 1: Researcher Actor]
    Queue --> |Tasks| AgentB[Node 2: Writer Actor]
    Queue --> |Tasks| AgentC[Node 3: Editor Actor]
    
    AgentA -- State Updates --> RedisPubSub
    AgentB -- State Updates --> RedisPubSub
    AgentC -- State Updates --> RedisPubSub
    
    AgentA <--> LLM[LLM APIs<br>OpenAI/Anthropic]
    AgentB <--> MCP[MCP Servers<br>Search/Database]

Complete Architectural Schema (Digitalized from Sketch)

flowchart TD
    classDef plain fill:none,stroke:none,color:inherit,font-style:italic;
    classDef solidBox fill:none,stroke:#333,stroke-width:2px;

    %% Row 1
    subgraph TopLevel [" "]
        direction LR
        TL["DISTRIBUTED AGENTIC"]:::plain
        Kanban["VISUALIZE TASKS, STREAMS:<br/> KANBAN STYLE BOARD<br/>(TODO, DOING, DONE, BLOCKED, AWAITING_VALIDATION)"]:::solidBox
        TR["WRAPPER ON KAIBANJS + OTHERS(DIFY, MCP, ETC.)<br/>ACTOR MODEL, ENTERPRISE GRADE MESSAGING + QUEUEING"]:::plain
        TL ~~~ Kanban ~~~ TR
    end
    style TopLevel fill:none,stroke:none;

    %% Row 2
    subgraph MidLevel [" "]
        direction LR
        MsgLayer["MESSAGE LAYER<br/>ASYNC. STREAMING MSG. / REALTIME (KAFKA, REDIS, ETC)"]:::solidBox
        MAL["MAL + DRIVERS / INTERFACES<br/>(MESSAGING AGENT LAYER)"]:::plain
        MsgLayer ~~~ MAL
    end
    style MidLevel fill:none,stroke:none;

    Kanban <--> MsgLayer

    %% Row 3
    subgraph AgentLevel [" "]
        direction LR
        ActorModelText["DISTRIBUTED INFRA: AI NODE AGENTS<br/>EACH AGENT ACTOR MODEL<br/>EDGE, IOT, LOCAL, KUBERNETES, ETC."]:::plain
        N1(("AGENT NODE<br/>optional V. SCALING"))
        N2(("AGENT<br/>NODE"))
        N3(("AGENT<br/>NODE"))
        N4(("AGENT NODE"))
        DistInfraText["TEAM WORKFLOW: RUNNING → FINISHED / STOPPED"]:::plain

        ActorModelText ~~~ N1
        N1 -- "optional H. SCALING" --- N2
        N2 ~~~ N3
        N3 -- "optional H. SCALING" --- N4
        N4 ~~~ DistInfraText
    end
    style AgentLevel fill:none,stroke:none;

    MsgLayer <--> N1
    MsgLayer <--> N2
    MsgLayer <--> N3
    MsgLayer <--> N4

    %% Row 4
    subgraph BottomLevel [" "]
        direction LR
        OtherSystemsText["OTHER SYSTEMS COMPONENTS<br/>INTERACTING VIA A2A, MCP, MESSAGING"]:::plain
        Dify["WORKFLOWS: ◯ ➞ ◻️ ➞ ◇ ➞ ◻️ [RES/OK]<br/>GRAPHIC AI PROGRAMMING"]:::solidBox
        OtherSystemsBox["OTHER SYSTEMS: CONNECTORS TO M.A.L<br/>CUSTOMIZED TO ORG/PROJECT"]:::solidBox

        OtherSystemsText ~~~ Dify ~~~ OtherSystemsBox
    end
    style BottomLevel fill:none,stroke:none;

    N1 -. "I/O - A2A" .-> Dify
    N2 -. "MCP I/O" .-> Dify
    N4 -->|"I/O: A2A, MCP MESSAGING"| OtherSystemsBox

Task State Machine (The Worker Lifecycle)

stateDiagram-v2
    [*] --> TODO : task.create
    note right of TODO
        API response: status='QUEUED'
    end note
    TODO --> DOING : Worker Claims Task

    DOING --> DONE : Inference Success
    DOING --> TODO : Retry (max 3×, linear backoff)
    DOING --> BLOCKED : Max Retries Exceeded (→ kaiban-events-failed)

    DOING --> AWAITING_VALIDATION : HITL Required
    AWAITING_VALIDATION --> DOING : Human Approved

    DONE --> [*]
    BLOCKED --> [*]

Components

ComponentLocationPurpose
AgentActorsrc/application/actor/Actor: subscribes to queue, processes tasks with retry (3×) + DLQ, optional firewall + circuit breaker; outbound message data capped at 64 KB
KaibanAgentBridgesrc/infrastructure/kaibanjs/Wraps KaibanJS agent in a per-task Team; calls team.start(); returns token-tracked KaibanHandlerResult; optional JIT token provider
KaibanTeamBridgesrc/infrastructure/kaibanjs/Wraps KaibanJS Team with distributed state sync
AgentStatePublishersrc/adapters/state/Publishes IDLE/EXECUTING/DONE/ERROR to Redis Pub/Sub; 15s heartbeat; enforces the per-agent and per-task token caps (build it with createAgentStatePublisher)
BullMQDriversrc/infrastructure/messaging/Redis-backed job queue (default); optional TLS; no colon queue names
KafkaDriversrc/infrastructure/messaging/Kafka-backed messaging; optional SSL/mTLS; unique consumer group per worker role
DistributedStateMiddlewaresrc/adapters/state/Intercepts Zustand store setState() and publishes deltas to messaging layer
GatewayAppsrc/adapters/gateway/Express HTTP: /health, /.well-known/agent-card.json, /a2a/rpc
SocketGatewaysrc/adapters/gateway/Socket.io server + Redis pub/sub subscriber; broadcasts state:update to board
A2A stack (buildA2AStack: KaibanAgentExecutor + RedisTaskStore + AgentStatusTracker)src/infrastructure/federation/Official @a2a-js/sdk v0.3 server; message/send validates + publishes to the agent mailbox (ADR-015)
MCPFederationClientsrc/infrastructure/federation/Connects to any MCP tool server via stdio transport
HeuristicFirewallsrc/infrastructure/security/Regex-based prompt injection detection (ASI01); opt-in via SEMANTIC_FIREWALL_ENABLED
EnvTokenProvidersrc/infrastructure/security/JIT token abstraction (ASI03); reads API keys from env vars; opt-in via JIT_TOKENS_ENABLED
SlidingWindowBreakersrc/infrastructure/security/Sliding-window circuit breaker (ASI10); opt-in via CIRCUIT_BREAKER_ENABLED
OrchestratorStatePublisherexamples/blog-team/orchestrator.tsOwns workflow lifecycle (RUNNING→FINISHED/STOPPED/AWAITING)
CompletionRouterexamples/blog-team/orchestrator.tsSingle BullMQ/Kafka subscriber dispatching completion events by taskId

Prerequisites

  • Node.js ≥ 22
  • Docker + Docker Compose (for Redis, Kafka, and multi-node demo)
  • LLM API key — OpenAI (OPENAI_API_KEY), OpenRouter (OPENROUTER_API_KEY), other compatible APIs

Quick Start

1. Install

git clone https://github.com/andreibesleaga/kaiban-distributed
cd kaiban-distributed
npm install

2. Configure

cp .env.example .env

Edit .env — choose your LLM provider:

# Standard OpenAI
OPENAI_API_KEY=sk-...
LLM_MODEL=gpt-4o-mini

# OpenRouter (https://openrouter.ai/keys)
OPENROUTER_API_KEY=sk-or-v1-...
LLM_MODEL=meta-llama/llama-3.1-8b-instruct:free   # free tier

# Required — which agents this node serves
AGENT_IDS=researcher,writer,editor

3. Start infrastructure

docker compose up -d redis

4. Build and run gateway

npm run build
AGENT_IDS=gateway PORT=3000 node dist/src/main/index.js

5. Verify

curl http://localhost:3000/health
# → {"data":{"status":"ok","timestamp":"..."}}

curl http://localhost:3000/.well-known/agent-card.json
# → {"protocolVersion":"0.3.0","name":"kaiban-gateway",...,"capabilities":{"streaming":true,...},"skills":[...]}  (A2A v0.3)

React Board (board/)

The board/ subdirectory is a standalone React + Vite + TypeScript dashboard that visualises distributed agent activity in real time via Socket.io.

FeatureDetail
Tech stackReact 18 · Vite · TypeScript · Tailwind CSS · Zustand · Socket.io client
Gateway URLhttp://localhost:3000 (override with ?gateway=<url> query param or VITE_GATEWAY_URL)
State sourceSocketGateway backend subscribes to Redis kaiban-state-events and fans out state:update events to all connected boards
Agent GridLive status badges per agent — IDLE / THINKING / EXECUTING (pulse) / ERROR
Kanban Board5-column task view: TODO · DOING · REVIEW · DONE · BLOCKED
HITL controlsApprove / Revise / Reject buttons when a task is AWAITING_VALIDATION; emits hitl:decision back through the socket
Economics panelAggregate token count, cost, and duration; reverse-chronological event log (capped at 200 entries)

Start the board (dev):

cd board
cp .env.example .env      # optional: set VITE_GATEWAY_URL
npm install
npm run dev               # → http://localhost:5173

Production build outputs static files to board/dist/ — deploy anywhere (nginx, CDN, etc.).


Individual Node Pattern

Mirrors the kaibanjs-node-demo pattern — each agent runs as an independent process:

// my-agent-node.ts
import 'dotenv/config';
import { BullMQDriver } from './src/infrastructure/messaging/bullmq-driver';
import { AgentActor } from './src/application/actor/AgentActor';
import { createKaibanTaskHandler } from './src/infrastructure/kaibanjs/kaiban-agent-bridge';
import { createAgentStatePublisher } from './src/shared';

const REDIS_URL = process.env['REDIS_URL'] ?? 'redis://localhost:6379';
const redisUrl = new URL(REDIS_URL);

const driver = new BullMQDriver({
  connection: { host: redisUrl.hostname, port: parseInt(redisUrl.port || '6379', 10) },
});

// Applies MAX_TOKEN_BUDGET / MAX_TASK_TOKEN_BUDGET (defaults 200000 / 50000).
const statePublisher = createAgentStatePublisher(REDIS_URL, {
  agentId: 'researcher', name: 'Ava', role: 'News Researcher',
});

const handler = statePublisher.wrapHandler(
  createKaibanTaskHandler({
    name: 'Ava',
    role: 'News Researcher',
    goal: 'Find and summarize the latest news on a given topic',
    background: 'Expert data analyst with deep research experience',
    llmConfig: {
      provider: 'openai',
      model: process.env['LLM_MODEL'] ?? 'gpt-4o-mini',
      apiKey: process.env['OPENAI_API_KEY'],
    },
  }, driver)
);

const actor = new AgentActor('researcher', driver, 'kaiban-agents-researcher', handler);
await actor.start();
statePublisher.publishIdle();  // board shows Ava as IDLE within 15s
console.log('[Researcher] Ava started');

process.on('SIGTERM', async () => {
  await actor.stop();
  await driver.disconnect();
  await statePublisher.disconnect();
});
# Terminal 1 — researcher
OPENAI_API_KEY=sk-... node dist/examples/blog-team/researcher-node.js

# Terminal 2 — writer
OPENAI_API_KEY=sk-... node dist/examples/blog-team/writer-node.js

# Terminal 3 — send a task via A2A v0.3 (message/send; target agent in metadata.agentId)
curl -X POST http://localhost:3000/a2a/rpc \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"kind":"message","role":"user","messageId":"m1","parts":[{"kind":"text","text":"Research the latest AI agent frameworks in 2025"}],"metadata":{"agentId":"researcher","expectedOutput":"A concise summary"}}}}'

Integrating with kaiban-board

kaiban-board is a React component that visualises KaibanJS team execution as a live Kanban board.

How state flows to the board

Worker nodes (each):
  AgentStatePublisher.publishIdle()     → Redis PUBLISH kaiban-state-events { agents: [IDLE] }
  AgentStatePublisher.wrapHandler()     → EXECUTING → DONE/ERROR → Redis PUBLISH
  15-second heartbeat                   → re-publishes current agent status
  (heartbeat NEVER sets teamWorkflowStatus — only the orchestrator does)

Orchestrator:
  workflowStarted()     → { teamWorkflowStatus: 'RUNNING', agents: all IDLE }
  awaitingHITL(...)     → { tasks: [AWAITING_VALIDATION] }
  workflowFinished(...) → { teamWorkflowStatus: 'FINISHED', all tasks: DONE }
  workflowStopped(...)  → { teamWorkflowStatus: 'STOPPED', tasks: BLOCKED }

SocketGateway:
  subscribes Redis kaiban-state-events → emits Socket.io 'state:update' to board

Board state lifecycle

teamWorkflowStatus values (set by the orchestrator only):

teamWorkflowStatusBoard bannerBadge
RUNNINGnone🔵 blue
FINISHED✅ WORKFLOW COMPLETE (green glow)🟢 green
STOPPED⏹ WORKFLOW ENDED (grey)⚫ grey

The ⏸ HUMAN DECISION REQUIRED (orange pulse) banner is shown when any task has status AWAITING_VALIDATION — this is triggered by task state, not by teamWorkflowStatus.

Task card states:

  • TODO — 📋 pending (initial state)
  • DOING — 🔵 blue left border + pulse dot
  • DONE — 🟢 green
  • AWAITING_VALIDATION — 🟠 orange pulsing glow + ⏸ HUMAN DECISION badge + HITL banner
  • BLOCKED — 🔴 red glow + ⛔ ERROR badge + red error banner with message

Option A: Static HTML viewer (zero setup)

Open examples/blog-team/viewer/board.html directly in a browser. Auto-connects to http://localhost:3000. All three agents (Ava, Kai, Morgan) appear as IDLE within 15 seconds.

Event stream shows typed, colour-coded entries:

  • WORKFLOW badge — workflow status transitions
  • AGENT badge — IDLE → EXECUTING → IDLE per agent
  • TASK badge — task status with result preview

Option B: Custom Socket.io client

import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
const agentMap = new Map();
const taskMap  = new Map();

socket.on('state:update', (delta) => {
  // IMPORTANT: merge by ID — each worker publishes only its own slice
  if (delta.agents) {
    for (const a of delta.agents)
      agentMap.set(a.agentId, { ...agentMap.get(a.agentId), ...a });
  }
  if (delta.tasks) {
    for (const t of delta.tasks)
      taskMap.set(t.taskId, { ...taskMap.get(t.taskId), ...t });
  }
});

Option C: React board app (modern UI, interactive HITL)

The board/ directory is a standalone React + Vite + TypeScript app that connects to the same Socket.io gateway and adds interactive Human-in-the-Loop controls:

cd board
cp .env.example .env        # optional: set VITE_GATEWAY_URL
npm install
npm run dev                  # → http://localhost:5173

Or pass the gateway URL at runtime without rebuilding:

http://localhost:5173?gateway=http://my-gateway.example.com:3000

Gateway URL resolution (priority order):

  1. ?gateway=<url> query param (runtime, no rebuild needed)
  2. VITE_GATEWAY_URL build-time env var (.env file)
  3. http://localhost:3000 fallback

Layout (top → bottom):

  • Header — logo, topic, gateway URL chip, workflow status pill, connection badge
  • WorkflowBanner — conditional banner: HITL Approve/Revise/Reject buttons (when any task is AWAITING_VALIDATION), FINISHED, STOPPED, or ERRORED states
  • AgentGrid — responsive 2–4 column grid with live status badges (IDLE / THINKING / EXECUTING + pulse / ERROR)
  • KanbanBoard — 5-column board: TODO · DOING · REVIEW · DONE · BLOCKED
  • EconomicsPanel + EventLog — tokens, cost, duration; reverse-chronological event stream (capped at 200)

HITL decision flow:

Board clicks [Approve]
  → socket.emit('hitl:decision', { taskId, decision: 'PUBLISH' })
  → SocketGateway: publishes to Redis kaiban-hitl-decisions
  → Orchestrator's waitForHITLDecision() races Redis vs terminal input
  → First to respond wins → workflow continues

Both the board and the terminal prompt remain functional simultaneously — first response wins.

Production build:

cd board && npm run build    # → board/dist/ (static files, serve anywhere)

Option D: KaibanTeamBridge (local Team + distributed workers)

import { Agent, Task } from 'kaibanjs';
import { BullMQDriver } from './src/infrastructure/messaging/bullmq-driver';
import { KaibanTeamBridge } from './src/infrastructure/kaibanjs/kaiban-team-bridge';

const ava = new Agent({ name: 'Ava', role: 'Researcher', goal: '...', background: '...' });
const kai = new Agent({ name: 'Kai', role: 'Writer',     goal: '...', background: '...' });

const driver = new BullMQDriver({ connection: { host: 'localhost', port: 6379 } });

const bridge = new KaibanTeamBridge({
  name: 'Blog Team',
  agents: [ava, kai],
  tasks: [
    new Task({ description: 'Research {topic}', expectedOutput: 'Summary', agent: ava }),
    new Task({ description: 'Write blog',       expectedOutput: 'Blog post', agent: kai }),
  ],
}, driver);

const result = await bridge.start({ topic: 'AI agents 2025' });
// State propagates: Redis Pub/Sub → SocketGateway → Socket.io → board

A2A Protocol (Agent-to-Agent)

The Edge Gateway exposes a wire-conformant A2A v0.3 JSON-RPC surface (served by the official @a2a-js/sdk v0.3.x, ADR-015) for interoperability with other AI systems. It serves a v0.3 AgentCard at the spec path (/.well-known/agent-card.json) and implements the real v0.3 methods message/send, message/stream (SSE), tasks/get, tasks/cancel. A real @a2a-js/sdk client interoperates with it (proven by tests/e2e/a2a-protocol.test.ts).

History (superseded): v1.x exposed a custom, non-standard method set (tasks.create / tasks.get / agent.status) and a flat { capabilities: string[], endpoints } card. Those were removed in the v2.0 A2A migration (the custom A2AConnector is gone). See MIGRATION.md + docs/decisions/ADR-015.

Agent Card (v0.3)

curl http://localhost:3000/.well-known/agent-card.json
{
  "protocolVersion": "0.3.0",
  "name": "kaiban-gateway",
  "description": "Kaiban distributed A2A gateway — bridges A2A tasks onto the actor messaging layer.",
  "url": "http://localhost:3000/a2a/rpc",
  "version": "2.0.0",
  "preferredTransport": "JSONRPC",
  "additionalInterfaces": [
    { "transport": "JSONRPC", "url": "http://localhost:3000/a2a/rpc" },
    { "transport": "HTTP+JSON", "url": "http://localhost:3000/a2a/rest" },
    { "transport": "GRPC", "url": "http://localhost:3000/a2a/grpc" }
  ],
  "capabilities": { "streaming": true, "pushNotifications": false, "stateTransitionHistory": false },
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    { "id": "researcher", "name": "Delegate to researcher", "tags": ["task", "agent", "researcher"] }
  ]
}

capabilities is an object in v0.3 (it was a string[] in the old card); an agent's discrete abilities live in skills[] (one skill per agent id).

RPC Methods (A2A v0.3)

MethodPurposeNotes
message/sendDispatch a task; await terminal resultTarget agent in message.metadata.agentId; optional instruction/expectedOutput/context/inputs in metadata (or plain text parts). Input caps (A2A_INPUT_CAPS, 64 KB); oversized/wrong-typed → -32602.
message/streamSame, streamed over SSEsubmitted → working → completed/failed/canceled
tasks/getFetch a persisted task by idReal data from RedisTaskStore
tasks/cancelCancel an in-flight taskStops the gateway wait only; emits terminal canceled. The worker is not told and keeps running to its own timeout

Live agent status is GET /a2a/agents/:agentId/status (not a JSON-RPC method).

# External A2A v0.3 caller: dispatch a task via message/send
curl -X POST http://localhost:3000/a2a/rpc \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0", "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "kind": "message", "role": "user", "messageId": "m1",
        "parts": [{ "kind": "text", "text": "Research quantum computing breakthroughs in 2025" }],
        "metadata": {
          "agentId": "researcher",
          "expectedOutput": "A 300-word technical summary",
          "inputs": { "topic": "quantum computing" }
        }
      }
    }
  }'

In-process dispatch (not A2A). When you orchestrate your own agent nodes in the same process, dispatch over the actor mailbox directly instead of the HTTP A2A surface:

import { createDriver, dispatchToAgent, CompletionRouter } from 'kaiban-distributed/shared';
const driver = createDriver('researcher');
const taskId = await dispatchToAgent(driver, 'researcher', {
  instruction: 'Research quantum computing breakthroughs in 2025',
  expectedOutput: 'A 300-word technical summary',
  inputs: { topic: 'quantum computing' },
});
// await the result by taskId via CompletionRouter.wait(...)

MCP Integration

Attach any Model Context Protocol tool server to your agents:

import { MCPFederationClient } from './src/infrastructure/federation/mcp-client';

const mcp = new MCPFederationClient('npx', ['-y', '@modelcontextprotocol/server-brave-search']);
await mcp.connect();
const tools = await mcp.listTools();
const result = await mcp.callTool('brave_web_search', { query: 'AI agents 2025' });
await mcp.disconnect();

MCP servers for Redis and Kafka enable AI agents to intercept and query live data streams:

ServerPurpose
mcp-redisQuery kaiban-state-events pub/sub, streams (XREAD), vector search
Confluent MCPFlink SQL queries over live Kafka topics (Confluent Cloud)
tuannvm/kafka-mcp-serverConsume Kafka messages at specific offsets (self-hosted)
// claude_desktop_config.json
{
  "redis": {
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-redis", "--url", "redis://localhost:6379"]
  }
}

Messaging Drivers

BullMQ (Default — Redis)

Best for: development, small-to-medium scale, reliable delivery, job history.

MESSAGING_DRIVER=bullmq
REDIS_URL=redis://localhost:6379

Important: BullMQ v5 rejects queue names containing colons. All internal channels use dashes: kaiban-agents-researcher, kaiban-events-completed, kaiban-events-failed, kaiban-state-events

Kafka (High-Throughput)

Best for: large scale, event streaming, message replay, multi-datacenter.

MESSAGING_DRIVER=kafka
KAFKA_BROKERS=localhost:9092
KAFKA_CLIENT_ID=kaiban-worker
KAFKA_GROUP_ID=kaiban-group

Kafka consumer group isolation — unique group suffix per component:

ComponentConsumer Group
researcher workerkaiban-group-researcher
writer workerkaiban-group-writer
editor workerkaiban-group-editor
orchestrator (completed events)kaiban-group-orchestrator-completed
orchestrator (failed/DLQ events)kaiban-group-orchestrator-failed

Task queues use Kafka topics. State broadcast (kaiban-state-events) always uses Redis Pub/Sub — SocketGateway reads directly from Redis regardless of MESSAGING_DRIVER.

Driver factory (for custom node code)

// src/shared/driver-factory.ts (shared helpers; also re-exported from src/shared)
import { createDriver, getDriverType } from './src/shared';
const driver = createDriver('researcher');   // BullMQ or Kafka based on MESSAGING_DRIVER env

The same shared barrel (kaiban-distributed/shared in the published package) exports startAgentNode and createAgentStatePublisher, the factory that builds an AgentStatePublisher with MAX_TOKEN_BUDGET / MAX_TASK_TOKEN_BUDGET applied; the root kaiban-distributed entry exports createAgentStatePublisher too.

Switching at runtime

Set MESSAGING_DRIVER=kafka (or bullmq) — the IMessagingDriver interface is the abstraction. Worker code is identical for both drivers.


API Reference

HTTP Endpoints

MethodPathDescription
GET/health{ data: { status: 'ok', timestamp } }
GET/.well-known/agent-card.jsonA2A agent capabilities
POST/a2a/rpcA2A v0.3 JSON-RPC: message/send, message/stream, tasks/get, tasks/cancel
GET/a2a/agents/:agentId/statusReal last-known agent status (AgentStatusTracker)

All responses: { data, meta, errors } envelope.

Socket.io Events

EventDirectionPayload
state:updateserver → clientStateDelta (PII-sanitized)
hitl:decisionclient → server{ taskId: string, decision: 'PUBLISH' | 'REVISE' | 'REJECT' }

Internal Channel Names

ChannelDriverPurpose
kaiban-agents-{agentId}BullMQ / KafkaTask inbox per agent
kaiban-events-completedBullMQ / KafkaSuccessful task results
kaiban-events-failedBullMQ / KafkaDLQ after 3 retry failures, or after one non-retryable failure (budget breach)
kaiban-state-eventsRedis Pub/SubAgent/workflow state → board
kaiban-hitl-decisionsRedis Pub/SubBoard HITL decisions → orchestrator

Configuration Reference

VariableDefaultRequiredDescription
AGENT_IDS—YesComma-separated agent IDs this node serves
REDIS_URLredis://localhost:6379NoRedis connection URL
MESSAGING_DRIVERbullmqNobullmq or kafka
KAFKA_BROKERSlocalhost:9092Kafka onlyComma-separated broker addresses
KAFKA_CLIENT_IDkaiban-workerNoKafka client identifier
KAFKA_GROUP_IDkaiban-groupNoKafka consumer group base ID
PORT3000NoHTTP + WebSocket port
SERVICE_NAMEkaiban-workerNoName in telemetry and agent card
OPENAI_API_KEY—For agentsStandard OpenAI API key
OPENROUTER_API_KEY—For agentsOpenRouter key (auto-configures base URL)
OPENAI_BASE_URL—OptionalCustom OpenAI-compatible endpoint
LLM_MODELgpt-4o-miniNoModel (for OpenRouter: meta-llama/llama-3.1-8b-instruct:free)
OTEL_EXPORTER_OTLP_ENDPOINT—NoOpenTelemetry OTLP endpoint (else console)
MAX_TOKEN_BUDGET200000NoToken cap per agent (one AgentStatePublisher, in its worker process; resets on restart)
MAX_TASK_TOKEN_BUDGET50000NoToken cap per task, all attempts seen by the same agent process (attempts whose KaibanJS workflow ends BLOCKED included; an ERRORED workflow makes team.start() reject and is not counted)
MAX_WORKFLOW_COST_USD0.50NoCost cap per workflow (orchestrator)
MAX_WORKFLOW_TOKENSunlimitedNoToken cap per workflow (orchestrator)

Only the literal unlimited removes a spend cap; 0, a negative number or text gives the default and logs one warning.

Board app (board/.env)

VariableDefaultDescription
VITE_GATEWAY_URLhttp://localhost:3000Gateway WebSocket URL (build-time; overridable via ?gateway= query param at runtime)

Security (all opt-in, disabled by default)

VariableDefaultDescription
REDIS_TLS_CA / REDIS_TLS_CERT / REDIS_TLS_KEY—Paths to Redis mTLS certificates
KAFKA_SSL_CA / KAFKA_SSL_CERT / KAFKA_SSL_KEY—Paths to Kafka mTLS certificates
TLS_REJECT_UNAUTHORIZEDtrueSet false for self-signed certs in staging
SEMANTIC_FIREWALL_ENABLEDfalseEnable heuristic prompt injection firewall
SEMANTIC_FIREWALL_LLM_URL—Optional local LLM endpoint for deep analysis
JIT_TOKENS_ENABLEDfalseEnable JIT token provider for LLM API keys
CIRCUIT_BREAKER_ENABLEDfalseEnable sliding-window circuit breaker
CIRCUIT_BREAKER_THRESHOLD10Failures before breaker trips
CIRCUIT_BREAKER_WINDOW_MS60000Sliding window duration (ms)

Security & Compliance

Security audits have been performed against the OWASP Top 10 for Agentic AI (2026) and OWASP Top 10 for LLM Applications (2025).

For a complete reference of every security feature, configuration option, and deployment checklist see SECURITY_FEATURES.md.

Security Features

FeatureComponentOWASPActivation
Board Viewer JWTboard-auth.ts + SocketGatewayCRIT-01BOARD_JWT_SECRET
A2A Bearer Token Autha2a-auth.ts + GatewayAppCRIT-02A2A_JWT_SECRET
Redis Channel Signingchannel-signing.tsHIGH-01CHANNEL_SIGNING_SECRET
CORS AllowlistSocketGatewayHIGH-03SOCKET_CORS_ORIGINS (required in production)
Token Expiry EnforcementSocketGatewayLOW-01automatic when BOARD_JWT_SECRET set
Semantic FirewallHeuristicFirewallASI01SEMANTIC_FIREWALL_ENABLED=true
mTLSKafkaDriver / BullMQDriverASI07REDIS_TLS_* / KAFKA_SSL_*
JIT Token ProviderEnvTokenProviderASI03JIT_TOKENS_ENABLED=true
Circuit BreakerSlidingWindowBreakerASI10CIRCUIT_BREAKER_ENABLED=true
W3C Traceparent ValidationBullMQDriverMED-06always-on
HTTP HardeningGatewayApp (Helmet, rate limit, CSP, HSTS)MED-04/05always-on
WebSocket HardeningSocketGateway (buffer limit, ping, HITL validation)—always-on
A2A Task-Input ValidationvalidateTaskInput (a2a-input-validation.ts)HIGH-04always-on

Authentication and signing features are env-var gated: when the relevant secret is not set, the system behaves exactly as before (backwards-compatible). When set, full enforcement is active.

Compliance-supporting controls

kaiban-distributed is a library/runtime, not a certified product. The controls below can support a deploying organization's GDPR, SOC 2, or ISO/IEC 27001 program — they do not, by themselves, constitute compliance or certification, which remain the operator's responsibility.

CapabilityImplementationSupports
PII minimisation in logsAgent IDs SHA-256 hashed (8-char prefix) via sanitizeId()GDPR
PII stripping in state deltassanitizeDelta() strips email, name, phone, ip, password, token, secret, ssn, dob (applied on both the middleware and worker-publisher paths)GDPR
Data minimisationresult capped at 20,000 chars (20 KB) in state events; outbound data capped at 64 KB in AgentActorGDPR
Least-privilege containerDockerfile non-root USER kaibanSOC 2 (CC6)
Secret hygieneSecrets via env vars only; .env gitignored; .env.example placeholdersSOC 2
Encryption in transitmTLS for Redis/Kafka; HTTPS to LLM APIs; scripts/generate-dev-certs.sh for stagingISO/IEC 27001 (A.8.24)
Observability / audit trailOpenTelemetry tracing; W3C traceparent across BullMQ/Kafka hops; run-logger decision trailSOC 2 / ISO 27001
Supply chain0 high/critical advisories (CI-enforced); CycloneDX SBOM; SLSA provenance + Sigstore-signed releases; overrides pin patched @langchain/*, axios, protobufjs, etc. — see SECURITY.md—

Development

Commands

npm run build          # tsc → dist/src/ and dist/examples/
npm run dev            # node dist/src/main/index.js (build first)
npm run test           # 1194 unit tests (no external deps)
npm run test:coverage  # 100% coverage — all metrics
npm run test:e2e       # BullMQ E2E (Docker Redis auto-started)
npm run test:e2e:kafka # Kafka E2E (Docker Kafka + Zookeeper required)
# cd board && npm run test:visual   # Playwright visual baselines (board + example viewers)
npm run lint           # ESLint + complexity ≤10 — 0 errors target
npm run typecheck      # tsc --noEmit — strict mode
npm run format         # prettier --write
npm run lint:arch      # madge --circular src/ — no circular imports

Testing

SuiteCommandCountInfrastructure
Unitnpm test1194 tests, 111 filesNone (all mocked)
BullMQ E2Enpm run test:e2e69 tests, 11 filesDocker Redis (auto-managed by globalSetup)
Kafka E2Enpm run test:e2e:kafka3 tests, 2 filesDocker Kafka + Zookeeper
Boardcd board && npm testReact component + Zustand store + a11y testshappy-dom
Visual (Playwright)cd board && npm run test:visualboard + example-viewer screenshot baselinesboard dev server + gateway + chromium

Coverage

MetricResult
Statements100%
Branches100%
Functions100%
Lines100%

Project Structure

kaiban-distributed/
├── src/
│   ├── domain/
│   │   ├── entities/          # DistributedTask, DistributedAgentState (with type guards)
│   │   ├── errors/            # DomainError, TaskNotFoundError, MessagingError, ...
│   │   ├── result.ts          # Result<T,E> — ok(), err(), isOk(), isErr()
│   │   └── security/          # Domain interfaces for security components
│   │       ├── semantic-firewall.ts  # ISemanticFirewall — evaluates payloads for injection
│   │       ├── token-provider.ts     # ITokenProvider — JIT token abstraction
│   │       └── circuit-breaker.ts    # ICircuitBreaker — success/failure tracking
│   ├── application/
│   │   └── actor/
│   │       └── AgentActor.ts  # Core: retry×3 + linear backoff, DLQ, firewall, circuit breaker
│   ├── adapters/
│   │   ├── gateway/
│   │   │   ├── GatewayApp.ts       # Express: /health, agent-card, /a2a/rpc, 404
│   │   │   └── SocketGateway.ts    # Socket.io + Redis pub/sub → board
│   │   └── state/
│   │       ├── distributedMiddleware.ts    # Intercepts Zustand setState → messaging
│   │       └── agent-state-publisher.ts   # Direct Redis pub/sub; 15s heartbeat; lifecycle
│   ├── infrastructure/
│   │   ├── messaging/
│   │   │   ├── interfaces.ts       # IMessagingDriver (publish, subscribe, unsubscribe, disconnect)
│   │   │   ├── channels.ts         # Canonical channel names (STATE, COMPLETED, DLQ)
│   │   │   ├── bullmq-driver.ts    # BullMQ Worker + Queue; optional TLS; no colons in queue names
│   │   │   └── kafka-driver.ts     # KafkaJS producer + consumer; optional SSL/mTLS
│   │   ├── federation/
│   │   │   ├── a2a-executor.ts     # KaibanAgentExecutor: A2A v0.3 message/send → agent mailbox (ADR-015)
│   │   │   ├── a2a-agent-card.ts   # buildAgentCard: v0.3 AgentCard (capabilities object + skills[])
│   │   │   ├── a2a-task-store.ts   # RedisTaskStore: persisted tasks for tasks/get, tasks/cancel
│   │   │   └── mcp-client.ts       # MCPFederationClient via stdio transport
│   │   ├── kaibanjs/
│   │   │   ├── kaiban-agent-bridge.ts  # createKaibanTaskHandler; JIT tokens; error detection
│   │   │   └── kaiban-team-bridge.ts   # KaibanTeamBridge with DistributedStateMiddleware
│   │   ├── security/              # Security infrastructure implementations
│   │   │   ├── heuristic-firewall.ts    # Regex prompt injection detection (10+ patterns)
│   │   │   ├── env-token-provider.ts    # Env-var backed JIT token provider
│   │   │   └── sliding-window-breaker.ts # Configurable sliding-window circuit breaker
│   │   └── telemetry/
│   │       ├── telemetry.ts        # initTelemetry(); recordAnomalyEvent(); OTLP or console
│   │       └── TraceContext.ts     # injectTraceContext / extractTraceContext (W3C)
│   └── main/
│       ├── index.ts    # Composition root: wires all layers + security deps, starts HTTP + actors
│       └── config.ts   # loadConfig(); TLS config; security feature flags
├── tests/
│   ├── unit/           # 1194 unit tests — mirrors src/ structure, 100% coverage
│   └── e2e/
│       ├── distributed-execution.test.ts      # BullMQ: execution, fault tolerance, state sync
│       ├── fan-out-fan-in.test.ts             # Parallel fan-out/fan-in workflow (7 scenarios)
│       ├── horizontal-scaling-bullmq.test.ts  # Competing consumers, exact-once delivery
│       ├── horizontal-scaling-kafka.test.ts   # Kafka consumer groups scaling
│       ├── a2a-protocol.test.ts               # HTTP gateway + A2A
│       ├── kafka-driver.test.ts               # Kafka pub/sub round-trip
│       └── setup/
│           ├── globalSetup.ts             # Docker Redis auto-start; resilient to existing Redis
│           └── kafkaSetup.ts              # Docker Kafka + Zookeeper + Redis auto-start
├── examples/
│   └── global-research/                   # Multi-agent distributed researchers pipeline example
│   └── blog-team/                         # Three-agent editorial pipeline
│       ├── team-config.ts                 # Agent configs (Ava, Kai, Morgan) + LLM factory
│       ├── driver-factory.ts              # createDriver(suffix) — BullMQ or Kafka from env
│       ├── researcher-node.ts             # Ava worker entry point
│       ├── writer-node.ts                 # Kai worker entry point
│       ├── editor-node.ts                 # Morgan worker entry point
│       ├── orchestrator.ts                # Event-driven pipeline + HITL terminal
│       ├── build-security-deps.ts         # Shared security setup (firewall, breaker, tokens)
│       ├── docker-compose.yml             # BullMQ: redis + gateway + 3 workers
│       ├── docker-compose.kafka.yml       # Kafka: zookeeper + kafka + redis + gateway + 3 workers
│       └── viewer/
│           ├── board.html                 # Live Kanban board — open in browser, no build
│           ├── board.js                   # Socket.io client + state rendering logic
│           └── board.css                  # Board styling
├── scripts/
│   ├── blog-team.sh                       # Start/stop orchestration wrapper (all modes)
│   ├── monitor.sh                         # Real-time terminal dashboard (all streams)
│   └── generate-dev-certs.sh              # Self-signed CA + server/client certs for mTLS
├── board/                                 # React + Vite + TypeScript board app (standalone)
│   ├── src/
│   │   ├── main.tsx / App.tsx             # Entry point + root layout
│   │   ├── types/board.ts                 # StateDelta, AgentDelta, TaskDelta, BoardState
│   │   ├── store/boardStore.ts            # Zustand: applyDelta, setConnectionStatus, addLog
│   │   ├── socket/socketClient.ts         # Socket.io singleton + sendHitlDecision()
│   │   └── components/                    # layout/ · workflow/ · agents/ · kanban/ · economics/ · log/
│   ├── package.json                       # React 18, Vite 8, Tailwind, socket.io-client, zustand
│   └── .env.example                       # VITE_GATEWAY_URL=http://localhost:3000
├── docker-compose.yml                     # Full root stack (Redis + Kafka + single worker)
├── Dockerfile                             # Multi-stage: builder (npm install + tsc) → runner (non-root)
└── .env.example                           # All env vars documented with examples

Distributed Actor Model documentation: see docs/architecture/ACTOR_MODEL.md for a full explanation of how actor isolation, mailboxes, message-passing, fault containment, and horizontal scaling are implemented.

Architecture Decisions

DecisionRationale
BullMQ as defaultLower ops overhead for dev; Kafka requires Zookeeper
No colons in BullMQ queue namesBullMQ v5 rejects colons; all internal names use dashes
IMessagingDriver abstractionSwap BullMQ ↔ Kafka via MESSAGING_DRIVER; worker code unchanged
Workers never set teamWorkflowStatusOnly orchestrator owns workflow lifecycle; prevents heartbeats overriding FINISHED
AgentStatePublisher uses ioredis directlySocketGateway reads Redis pub/sub; BullMQ queues are separate concerns
15-second heartbeat in AgentStatePublisherRedis pub/sub is fire-and-forget; late-connecting boards see state within 15s
Two KafkaDriver instances in CompletionRouterKafkaJS consumer.run() cannot subscribe to new topics after start
Team per task in agent bridgeTeam.start() initializes LLM automatically from env; WorkflowResult.stats provides token counts without internal hacks
KaibanJS ERRORED status throwsteam.start() rejects on ERRORED (no usage, not counted) and resolves { status: 'BLOCKED' } when an LLM/provider error blocks the task; on BLOCKED the bridge throws FailedAttemptError with the attempt's token usage, which the state publisher counts against the token caps; either way AgentActor retries (3×), then DLQs; once the counted usage reaches a cap, a non-retryable BudgetExceededError is dead-lettered without a further retry (and records no circuit-breaker failure)
forceFinalAnswer: true on editorFree 8B models reach max iterations without structured output
SHA-256 hash prefix for agent IDs8-char prefix preserves debuggability while preventing PII leakage
64 KB cap on published message dataAgentActor truncates result before publishing to prevent oversized frames from overloading messaging layer
globalSetup catches Redis port conflictE2E tests are resilient when Redis is already running from another compose stack
healthcheck: disable: true on workersWorkers are not HTTP servers; Dockerfile HEALTHCHECK checks port 3000 which is gateway-only
React board uses custom Socket.io client, not kaiban-board npm packagekaiban-board@0.4.3 requires a KaibanJS Team instance; the distributed board consumes state:update events directly
Board HITL races readline vs RediswaitForHITLDecision() in orchestrator resolves from whichever arrives first — terminal or board click; both remain usable simultaneously
?gateway= query param for runtime URL overrideAllows the same static build to connect to any gateway without rebuilding

Managing the Example & Real-Time Monitor

Unified Start/Stop Script

The easiest way to run the full blog-team example is using the orchestration script. It handles Docker Compose, the API Gateway, worker nodes, the orchestrator, and the terminal monitor. When the gateway is ready it prints board URLs — open one in a separate terminal or browser tab.

# BullMQ/Redis — local orchestrator (default)
./scripts/blog-team.sh start
./scripts/blog-team.sh stop

# Kafka — local orchestrator
./scripts/blog-team.sh start --kafka
./scripts/blog-team.sh stop --kafka

# BullMQ/Redis — fully containerised (orchestrator runs in Docker, HITL via attached terminal)
./scripts/blog-team.sh start --docker
./scripts/blog-team.sh stop --docker

# Kafka — fully containerised (flags are order-independent)
./scripts/blog-team.sh start --kafka --docker
./scripts/blog-team.sh stop --kafka --docker

--docker mode runs every component — including the orchestrator — as a Docker container. The orchestrator service (docker compose run --rm orchestrator) attaches your terminal for interactive HITL decisions [1/2/3/4]. Inside Docker it connects to gateway/Redis/Kafka via service-name hostnames. Without --docker, the orchestrator runs locally via npx ts-node (requires Node.js + project deps installed).

Opening a Board UI

The script prints board URLs when the gateway is ready. Open one or more in a separate terminal or browser tab — they are all synchronized from the backend stream at all times:

A) React Board — interactive HITL Approve / Revise / Reject, modern Kanban UI (requires Node.js):

# In a separate terminal, from the kaiban-distributed root:
cd board && npm install && npm run dev
# → http://localhost:5173

# Point to a non-default gateway at runtime (no rebuild needed):
# http://localhost:5173?gateway=http://my-gateway:3000

B) Static HTML viewer — zero setup, open directly in any browser:

examples/blog-team/viewer/board.html
# Auto-connects to http://localhost:3000

Multiple boards (React + HTML viewer + additional tabs) can all be open simultaneously and will all reflect the same live state. Each board receives a full state snapshot on connect and every incremental delta in real-time. HITL decisions can be sent from any connected board — the orchestrator accepts the first response.

Standalone Real-Time Monitor

If you started components manually, you can run the terminal monitor on its own. It automatically detects the MESSAGING_DRIVER (BullMQ/Redis or Kafka).

./scripts/monitor.sh

# With options:
REDIS_URL=redis://localhost:6379 \
COMPOSE_FILE=examples/blog-team/docker-compose.yml \
MESSAGING_DRIVER=bullmq \
LOG_TAIL=200 QUEUE_POLL_SEC=3 \
  ./scripts/monitor.sh
StreamDescription
[workflow]Status transitions: RUNNING → FINISHED / STOPPED
[agents]IDLE · EXECUTING (green) · THINKING (blue) · ERROR (red)
[tasks]DOING · DONE (green) · BLOCKED (red) · AWAITING_VALIDATION (yellow)
[logs] researcher/writer/editor/gatewayPer-container process logs
[queue]BullMQ queue depths polled every 5s
[!ERR]All errors across all containers (red highlight)

Common Issues

ErrorCauseFix
401 User not foundInvalid OpenRouter API keyGet valid key at https://openrouter.ai/keys
404 MODEL_NOT_FOUND / data policyFree model requires privacy opt-inEnable https://openrouter.ai/settings/privacy or use paid model
No endpoints found matching your data policyFree tier data-sharing requiredEnable https://openrouter.ai/settings/privacy
LLM instance is not initializedKaibanJS llmInstance not bootstrappedNo longer occurs — Team initialises the LLM automatically from the env map
Queue name cannot contain :Colon in BullMQ queue nameFixed — all internal queues use dashes
Agent failed: Max retries exceededLLM API errorCheck API key and model name
Task incomplete: max iterationsSmall model can't produce structured outputFixed — forceFinalAnswer: true on editor; increase maxIterations
network not found on docker compose upStale network from previous compose stackdocker compose down --remove-orphans && docker network prune --force
Worker shows unhealthyDockerfile HEALTHCHECK pings port 3000; workers aren't HTTP serversFixed — healthcheck: disable: true in worker services
Kafka: orchestrator timeout on writing/revisionSecond subscribe() after consumer.run() silently droppedFixed — TWO KafkaDriver instances with distinct consumer groups
Timeout waiting for researchTask failed (DLQ) but orchestrator not notifiedFixed — CompletionRouter subscribes to both completed AND failed
BullMQ E2E: port 6379 already in useAnother compose stack has RedisFixed — globalSetup catches and skips; or stop other stack first
No OTEL endpoint warning on startupOTEL_EXPORTER_OTLP_ENDPOINT not set — using verbose ConsoleSpanExporterExpected in dev; set OTEL_EXPORTER_OTLP_ENDPOINT for production

Project Context

Built with help from GABBE Agentic Engineering Kit, following the SDD/TDD lifecycle:

PhaseDeliverableStatus
S01PRD.md — requirements✅
S02PLAN.md — C4 architecture diagrams✅
S03SPEC.md — domain models, API schemas✅
S04Task decomposition✅
S05Core implementation (6 modules)✅
S06Unit test suite — 100% coverage✅
S07KaibanJS integration, blog-team pipeline, Kafka, README✅
S08Security remediation — mTLS, semantic firewall, JIT tokens, circuit breakers✅

License

Dual-licensed (see LICENSING.md, ADR-011):

  • The published npm library (kaiban-distributed — the dist/src artifact) is Apache-2.0, usable in commercial / closed-source projects.
  • The full application (board, examples, tests, Docker images) is GPL-3.0.

Apache-2.0 (library) / GPL-3.0 (application) ©2026 Andrei Besleaga

↑↓ navigate openesc close