DEV Community

Cover image for Building Interoperable AI Agents: A2A Protocol + KaibanJS Integration
Dariel Vila for KaibanJS

Posted on

Building Interoperable AI Agents: A2A Protocol + KaibanJS Integration

The Problem with Current AI Agent Systems

If you've worked with AI agents, you know the pain: each framework has its own way of doing things. Want to connect a LangChain agent with a CrewAI agent? Good luck. Need to integrate agents from different vendors? Prepare for a lot of custom integration work.

The A2A (Agent-to-Agent) Protocol solves this by providing a standardized communication layer that lets any agent talk to any other agent, regardless of the underlying framework.

In this post, I'll show you how to integrate the A2A Protocol with KaibanJS to create truly interoperable multi-agent systems.

What is the A2A Protocol?

The A2A Protocol is an open standard that defines how AI agents should communicate. Think of it as HTTP for AI agents - it provides:

  • Standardized message formats - All agents speak the same language
  • Real-time streaming - Live updates and status notifications
  • Multi-modal support - Text, audio, video communication
  • Security - Built-in authentication and validation
  • Vendor agnostic - Works with any framework

What is KaibanJS?

KaibanJS is a multi-agent orchestration framework that makes it easy to create teams of specialized AI agents. Instead of building monolithic agents, you can create:

  • Specialized agents with specific roles and capabilities
  • Task workflows that coordinate multiple agents
  • Real-time monitoring of agent collaboration
  • Tool integration with external APIs

The Architecture

Here's how our integration works:

User Query → A2A Protocol → Express Server → KaibanJS Team → Results
     ↑                                                           ↓
     └─────────────── Real-time Streaming ←──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The key insight is that we use the A2A Protocol as the communication layer, while KaibanJS handles the actual multi-agent orchestration.

Implementation Walkthrough

1. Setting Up the Agent Card

First, we define what our agent can do using an Agent Card:

// server/src/agent-card.ts
export const kaibanjsAgentCard: AgentCard = {
  name: 'Kaibanjs Research Agent',
  description:
    'Research topics using web search and generate comprehensive summaries',
  version: '1.0.0',
  protocolVersion: '1.0.0',
  url: process.env.BASE_URL || 'http://localhost:4000',
  preferredTransport: 'JSONRPC',
  capabilities: {
    streaming: true, // We support real-time updates
    pushNotifications: false,
    stateTransitionHistory: true
  },
  defaultInputModes: ['text/plain'],
  defaultOutputModes: ['text/plain'],
  skills: [
    {
      id: 'research',
      name: 'Research and Summarization',
      description:
        'Research topics using web search and generate comprehensive summaries',
      tags: ['research', 'summarization', 'web-search', 'ai'],
      examples: [
        'What are the latest developments in AI?',
        'Summarize the current state of renewable energy'
      ]
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

2. Creating the KaibanJS Team

Next, we define our multi-agent team:

// server/src/kaibanjs-team.ts
import { Agent, Task, Team } from 'kaibanjs';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';

// Define tools
const searchTool = new TavilySearchResults({
  maxResults: 3,
  apiKey: process.env.TAVILY_API_KEY
});

// Create specialized agents
const searchAgent = new Agent({
  name: 'Scout',
  role: 'Information Gatherer',
  goal: 'Find up-to-date information about the given query.',
  background: 'Research',
  type: 'ReactChampionAgent',
  tools: [searchTool]
});

const contentCreator = new Agent({
  name: 'Writer',
  role: 'Content Creator',
  goal: 'Generate a comprehensive summary of the gathered information.',
  background: 'Journalism',
  type: 'ReactChampionAgent',
  tools: []
});

// Define tasks
const searchTask = new Task({
  description: `Search for detailed information about: {query}. Current date: {currentDate}.`,
  expectedOutput:
    'Detailed information about the topic with key facts and relevant details.',
  agent: searchAgent
});

const summarizeTask = new Task({
  description: `Using the gathered information, create a comprehensive summary.`,
  expectedOutput:
    'A well-structured summary with key points and main findings.',
  agent: contentCreator
});

// Team factory function
export function createKaibanjsTeam(query: string) {
  const currentDate = new Date().toLocaleDateString('en-US', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

  return new Team({
    name: 'Research and Summary Team',
    agents: [searchAgent, contentCreator],
    tasks: [searchTask, summarizeTask],
    inputs: { query, currentDate },
    env: {
      OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
      ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || ''
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

3. The A2A Protocol Bridge

This is where the magic happens - we bridge A2A Protocol messages with KaibanJS workflows:

// server/src/agent-executor.ts
export class KaibanjsAgentExecutor implements AgentExecutor {
  public async execute(
    requestContext: RequestContext,
    eventBus: ExecutionEventBus
  ): Promise<void> {
    const { taskId, contextId, userMessage } = requestContext;

    try {
      // Extract query from A2A message
      const query = userMessage.parts
        .filter((part: Part) => part.kind === 'text')
        .map((part: Part) => (part as any).text)
        .join(' ');

      if (!query.trim()) {
        throw new Error('No query provided in message');
      }

      // Start the task
      eventBus.publish({
        kind: 'status-update',
        taskId,
        contextId,
        status: { state: 'working', timestamp: new Date().toISOString() },
        final: false
      });

      // Create KaibanJS team
      const team = createKaibanjsTeam(query);
      const useTeamStore = team.useStore();

      // Subscribe to workflow logs for real-time streaming
      const unsubscribe = useTeamStore.subscribe(state => {
        const { workflowLogs } = state;

        workflowLogs.forEach(log => {
          if (log) {
            let logMessage = '';

            if (log.logType === 'TaskStatusUpdate') {
              logMessage = `Task Status: ${log.taskStatus}`;
            } else if (log.logType === 'AgentStatusUpdate') {
              logMessage = `Agent Status: ${log.agentStatus}`;
            } else if (log.logType === 'WorkflowStatusUpdate') {
              logMessage = `Workflow Status: ${log.workflowStatus}`;
            }

            if (logMessage) {
              // Stream workflow updates via A2A Protocol
              eventBus.publish({
                kind: 'artifact-update',
                taskId,
                contextId,
                artifact: {
                  artifactId: `log-${Date.now()}`,
                  parts: [{ kind: 'text', text: log.logDescription }],
                  metadata: {
                    timestamp: new Date().toISOString(),
                    logType: log.logType,
                    logMessage
                  }
                }
              });
            }
          }
        });
      });

      // Execute the team workflow
      await team.start();

      // Wait for final logs
      await new Promise(resolve => setTimeout(resolve, 1000));

      // Get final result
      const finalState = useTeamStore.getState();
      const finalResult = finalState.workflowResult;

      // Send final result
      if (finalResult) {
        eventBus.publish({
          kind: 'artifact-update',
          taskId,
          contextId,
          artifact: {
            artifactId: `result-${taskId}`,
            parts: [
              { kind: 'text', text: JSON.stringify(finalResult, null, 2) }
            ],
            metadata: {
              timestamp: new Date().toISOString(),
              type: 'final-result'
            }
          }
        });
      }

      // Complete the task
      eventBus.publish({
        kind: 'status-update',
        taskId,
        contextId,
        status: { state: 'completed', timestamp: new Date().toISOString() },
        final: true
      });

      unsubscribe();
    } catch (error) {
      // Handle errors gracefully
      eventBus.publish({
        kind: 'artifact-update',
        taskId,
        contextId,
        artifact: {
          artifactId: `error-${taskId}`,
          parts: [{ kind: 'text', text: `Error: ${error.message}` }],
          metadata: { timestamp: new Date().toISOString(), type: 'error' }
        }
      });

      eventBus.publish({
        kind: 'status-update',
        taskId,
        contextId,
        status: { state: 'failed', timestamp: new Date().toISOString() },
        final: true
      });
    } finally {
      eventBus.finished();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Client-Side Integration

The React client shows how to consume A2A Protocol streams:

// client/src/stores/a2a-store.ts
export const useA2AStore = create<A2AState>((set, get) => ({
  // ... other state

  sendQuery: async (query: string) => {
    const { client, isConnected } = get();

    if (!client || !isConnected) {
      set({ error: 'Not connected to A2A server' });
      return;
    }

    set({ query, isProcessing: true, error: null, result: null });

    try {
      const streamParams: MessageSendParams = {
        message: {
          messageId: uuidv4(),
          role: 'user',
          parts: [{ kind: 'text', text: query }],
          kind: 'message'
        }
      };

      get().addLog('status', `Sending query: "${query}"`);

      const stream = client.sendMessageStream(streamParams);
      let finalResult = '';

      for await (const event of stream) {
        if (event.kind === 'task') {
          get().addLog('status', `Task created: ${event.id}`);
        } else if (event.kind === 'status-update') {
          get().addLog('status', `Status: ${event.status.state}`);

          if (event.status.state === 'completed') {
            set({ isProcessing: false });
          } else if (event.status.state === 'failed') {
            set({ isProcessing: false, error: 'Task failed' });
          }
        } else if (event.kind === 'artifact-update') {
          // Handle real-time workflow logs
          const textParts = event.artifact.parts.filter(
            part => part.kind === 'text'
          );
          const content = textParts.map(part => (part as any).text).join('\n');

          get().addLog('artifact', content, event.artifact.metadata);

          // Check if this is a final result
          if (event.artifact.artifactId.startsWith('result-')) {
            finalResult = content;
          }
        }
      }

      set({ result: finalResult });
      get().addLog('status', 'Stream completed');
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : 'Query failed';
      set({ error: errorMessage, isProcessing: false });
      get().addLog('error', errorMessage);
    }
  }
}));
Enter fullscreen mode Exit fullscreen mode

Key Integration Points

1. Message Flow

User Query → A2A Message → Agent Executor → KaibanJS Team → Workflow Logs → A2A Artifacts → Client
Enter fullscreen mode Exit fullscreen mode

2. Real-time Streaming

The integration streams KaibanJS workflow logs as A2A Protocol artifacts:

// Stream workflow updates
workflowLogs.forEach(log => {
  eventBus.publish({
    kind: 'artifact-update',
    taskId,
    contextId,
    artifact: {
      artifactId: `log-${Date.now()}`,
      parts: [{ kind: 'text', text: log.logDescription }],
      metadata: {
        logType: log.logType,
        logMessage: `Agent Status: ${log.agentStatus}`
      }
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

3. Error Handling

Robust error handling ensures the A2A Protocol contract is maintained:

catch (error) {
  // Send error as artifact
  eventBus.publish({
    kind: 'artifact-update',
    taskId,
    contextId,
    artifact: {
      artifactId: `error-${taskId}`,
      parts: [{ kind: 'text', text: `Error: ${error.message}` }],
      metadata: { timestamp: new Date().toISOString(), type: 'error' }
    }
  });

  // Mark task as failed
  eventBus.publish({
    kind: 'status-update',
    taskId,
    contextId,
    status: { state: 'failed', timestamp: new Date().toISOString() },
    final: true
  });
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters

1. Universal Interoperability

Your KaibanJS agents can now communicate with any A2A-compliant agent. No more vendor lock-in or custom integration work.

2. Real-time Visibility

Users can see exactly what's happening inside your multi-agent system as it processes their requests.

3. Standardized Communication

All agents speak the same language, making it easier to build complex agent networks.

Getting Started

Prerequisites

  • Node.js 18+
  • API keys for OpenAI and Tavily
  • Basic TypeScript/React knowledge

Quick Setup

# Clone the repository
git clone https://github.com/kaiban-ai/a2a-protocol-kaibanjs-demo

# Install dependencies
npm run install:all

# Configure environment variables
cp server/env.example server/.env
# Add your API keys to server/.env

# Start the development servers
npm run dev
Enter fullscreen mode Exit fullscreen mode

Try It Out

  1. Open http://localhost:5173
  2. Click "Connect to Server"
  3. Enter a research query
  4. Watch the real-time logs
  5. See the final results

Live Demo

Check out the live demo: A2A Protocol Integration Demo

Demo Video

See the A2A Protocol integration with KaibanJS in action - Watch agents collaborate in real-time

What's Next?

This integration opens up exciting possibilities:

  • Cross-platform agent networks - Connect agents from different frameworks
  • Enterprise agent orchestration - Scale agent systems across organizations
  • Multi-modal communication - Add audio and video capabilities
  • Open ecosystem - Contribute to the evolution of agent standards

Resources


Have you worked with multi-agent systems before? What challenges have you faced with agent interoperability? Let me know in the comments!

Top comments (0)