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 ←──────────────────────┘
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'
]
}
]
};
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 || ''
}
});
}
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();
}
}
}
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);
}
}
}));
Key Integration Points
1. Message Flow
User Query → A2A Message → Agent Executor → KaibanJS Team → Workflow Logs → A2A Artifacts → Client
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}`
}
}
});
});
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
});
}
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
Try It Out
- Open http://localhost:5173
- Click "Connect to Server"
- Enter a research query
- Watch the real-time logs
- 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
- Repository: A2A Protocol KaibanJS Demo
- Live Demo: A2A Protocol Integration Demo
- Demo Video: Watch the Integration in Action
- KaibanJS Docs: kaibanjs.com
- A2A Protocol: A2A Project
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)