Skip to main content

A2A Communication API

Complete API reference for agent-to-agent communication helpers.

Overview

The A2A Communication API (cortex.a2a.*) provides convenience helpers for inter-agent communication. It's not a separate storage system - it's syntactic sugar over the standard memory system with source.type='a2a'.

Infrastructure Requirement

Some A2A operations require real-time pub/sub infrastructure.

OperationPub/Sub RequiredWorks WithoutWhy
send()NoYesFire-and-forget (async)
request()YesNoNeeds real-time response notification
broadcast()Yes (optimal)DegradedCan store without pub/sub, but no delivery confirmation
subscribe()YesNoReal-time inbox notifications (Planned)
getConversation()NoYesDatabase query only

Pub/Sub Options:

  • Direct Mode: Bring your own Redis, RabbitMQ, NATS, or similar
  • Cloud Mode: Pub/sub infrastructure included and optimized
Key Insight

A2A = Agent Memory + Pub/Sub (optional) + Convenience

Validation Constraints:

FieldConstraintDefaultDetails
Agent IDs (from, to)Pattern: /^[a-zA-Z0-9_-]+$/Only alphanumeric, hyphens, underscores
Agent IDs (from, to)Max length: 100 characters
messageMax size: 100KBMeasured in UTF-8 bytes
importanceInteger 0-10060 for send/broadcast, 70 for request
timeoutInteger 1000-300000ms30000ms (30 seconds)
retriesInteger 0-101
to[] (broadcast)Max 100 recipientsNo duplicates, sender not allowed
limitInteger 1-1000100
offsetInteger >= 00
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Approach 1: A2A Helper (RECOMMENDED - 7 lines)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
await cortex.a2a.send({
from: "finance-agent",
to: "hr-agent",
message: "What is the Q4 budget?",
importance: 85,
});
// Done! Handles ACID + both Vector memories automatically

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Approach 2: Layer 3 remember() (Better than manual - 20 lines)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Get/create A2A conversation
const conversationId = await getOrCreateA2AConversation(
"finance-agent",
"hr-agent",
);

// Store in sender's memory (ACID + Vector)
await cortex.memory.remember({
memorySpaceId: "finance-agent-space",
conversationId,
userMessage: "What is the Q4 budget?",
agentResponse: "[Sent to hr-agent]",
userId: "finance-agent", // Sender as "user"
userName: "Finance Agent",
});

// Store in receiver's memory (ACID + Vector)
await cortex.memory.remember({
memorySpaceId: "hr-agent-space",
conversationId,
userMessage: "[From finance-agent]",
agentResponse: "What is the Q4 budget?",
userId: "finance-agent", // Sender as source
userName: "Finance Agent",
});
// Works, but awkward (remember() is for user-agent, not agent-agent)
// Still need to manage conversationId manually
// Have to call remember() twice (once per agent)

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Approach 3: Manual Layer 1 + Layer 2 (Most code - 50+ lines)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1. Get or create A2A conversation in ACID (Layer 1a)
let conversationId;
const existing = await cortex.conversations.search({
type: "agent-agent",
participants: { $all: ["finance-agent", "hr-agent"] },
});

if (existing.length > 0) {
conversationId = existing[0].conversationId;
} else {
const conv = await cortex.conversations.create({
type: "agent-agent",
participants: { agent1: "finance-agent", agent2: "hr-agent" },
});
conversationId = conv.conversationId;
}

// 2. Add message to ACID conversation
const msg = await cortex.conversations.addMessage(conversationId, {
type: "a2a",
from: "finance-agent",
to: "hr-agent",
text: "What is the Q4 budget?",
timestamp: new Date(),
});

// 3. Store in sender's Vector memory (Layer 2)
await cortex.vector.store("finance-agent", {
content: "Sent to hr-agent: What is the Q4 budget?",
contentType: "raw",
embedding: await embed("What is the Q4 budget?"), // Optional
source: {
type: "a2a",
fromAgent: "finance-agent",
toAgent: "hr-agent",
timestamp: new Date(),
},
conversationRef: {
conversationId,
messageIds: [msg.id], // Links to ACID
},
metadata: {
importance: 85,
tags: ["a2a", "sent", "hr-agent"],
direction: "outbound",
},
});

// 4. Store in receiver's Vector memory (Layer 2)
await cortex.vector.store("hr-agent", {
content: "Received from finance-agent: What is the Q4 budget?",
contentType: "raw",
embedding: await embed("What is the Q4 budget?"),
source: {
type: "a2a",
fromAgent: "finance-agent",
toAgent: "hr-agent",
timestamp: new Date(),
},
conversationRef: {
conversationId,
messageIds: [msg.id], // Same ACID message
},
metadata: {
importance: 85,
tags: ["a2a", "received", "finance-agent"],
direction: "inbound",
},
});

// That's 50+ lines vs 7 lines with cortex.a2a.send()!

Code Reduction:

ApproachLines of CodeHandles ACIDBidirectional / Complexity
cortex.a2a.send()7AutomaticBoth agents / Low complexity
cortex.memory.remember()~20AutomaticOne agent (call 2x) / Medium complexity
Manual (Layer 1+2)50+ManualOne agent (call 2x) / High complexity
Why cortex.a2a.send() is best for A2A
  • Handles ACID conversation management
  • Stores in BOTH agents automatically
  • Consistent metadata and tagging
  • One call instead of four
  • 85% less code

Architecture Integration:

A2A Architecture
Layer 1a: ACID Conversations

A2A conversations (type='agent-agent') — Immutable threads

Layer 2: Vector Memories

Sender memory (direction='outbound') + Receiver memory (direction='inbound') — Both reference ACID via conversationRef

Pub/Sub Infrastructure

Direct Mode: Your Redis/RabbitMQ/NATS — Cloud Mode (planned): Cortex-managed infrastructure

A2A Helpers

Convenience wrappers (handle ACID + bidirectional Vector storage + pub/sub)

Why use A2A helpers
  • Reduce code - 7 lines instead of 40+
  • Bidirectional - Automatically stores in both agents
  • ACID tracking - Manages A2A conversations automatically
  • Real-time - Pub/sub notifications (BYO or Cloud-provided)
  • Consistent - Standard tagging and linking
  • Fewer bugs - Less code to maintain

Configuration

Direct Mode: Developer Manages Execution + Pub/Sub

What Cortex provides:

  • Storage APIs (database operations)
  • Metadata conventions (messageType, inReplyTo, etc.)
  • Patterns for request-response

What YOU must provide:

  • Agent execution infrastructure (how agents run)
  • Pub/sub infrastructure (your own Redis, RabbitMQ, etc.)
  • Agent subscription handlers (connecting agents to pub/sub)

Example Setup (Direct Mode):

import Redis from "ioredis"; // Standard ioredis package

// 1. Your Redis instance
const redis = new Redis(process.env.REDIS_URL);

// 2. Cortex SDK (storage only)
const cortex = new Cortex({
convexUrl: process.env.CONVEX_URL,
});

// 3. YOUR agent execution + subscription logic
async function runAgent(memorySpaceId: string) {
// Subscribe to memory space's inbox channel
redis.subscribe(`memoryspace:${memorySpaceId}:inbox`, (err) => {
if (err) throw err;
});

// Handle incoming notifications
redis.on("message", async (channel, message) => {
const notification = JSON.parse(message);

if (notification.type === "a2a-request") {
// 1. Fetch from Cortex storage
const request = await cortex.memory.get(
memorySpaceId,
notification.memoryId,
);

// 2. Process with your agent logic
const answer = await yourAgentLogic(request.content);

// 3. Store response in Cortex
await cortex.a2a.send({
from: memorySpaceId,
to: notification.from,
message: answer,
metadata: {
messageType: "response",
inReplyTo: notification.messageId,
},
});

// 4. Publish response notification (your pub/sub)
await redis.publish(
`agent:${notification.from}:responses:${notification.messageId}`,
JSON.stringify({ response: answer }),
);
}
});
}

// 4. Manual request() implementation (you build this)
async function manualRequest(
from: string,
to: string,
message: string,
timeout = 30000,
) {
// Send via Cortex (stores in database)
const sent = await cortex.a2a.send({
from,
to,
message,
metadata: {
messageType: "request",
requiresResponse: true,
},
});

// Publish notification (your pub/sub)
await redis.publish(
`agent:${to}:inbox`,
JSON.stringify({
type: "a2a-request",
messageId: sent.messageId,
memoryId: sent.receiverMemoryId,
from,
}),
);

// Wait for response (your pub/sub)
return new Promise((resolve, reject) => {
const channel = `agent:${from}:responses:${sent.messageId}`;

redis.subscribe(channel);
const handler = (ch, msg) => {
if (ch === channel) {
redis.unsubscribe(channel);
redis.off("message", handler);
resolve(JSON.parse(msg));
}
};
redis.on("message", handler);

setTimeout(() => {
redis.unsubscribe(channel);
redis.off("message", handler);
reject(new Error("Timeout"));
}, timeout);
});
}

// Start your agents
runAgent("hr-agent");
runAgent("finance-agent");

// Use your manual request
const response = await manualRequest(
"finance-agent",
"hr-agent",
"What is the budget?",
);

Key Points:

  • Cortex provides storage APIs (database operations)
  • YOU provide execution (agent runners) + pub/sub (Redis/RabbitMQ)
  • YOU wire them together (subscriptions, notifications)
  • Cortex just provides the patterns and storage conventions

Cloud Mode: Cortex Manages Everything

Cloud Feature

Cloud Mode messaging layer provides managed pub/sub infrastructure (planned).

In Cloud Mode, Cortex will provide:

const cortex = new Cortex({
mode: "cloud",
apiKey: process.env.CORTEX_CLOUD_KEY,
});

// request() works automatically
const response = await cortex.a2a.request({
from: "finance-agent",
to: "hr-agent",
message: "What is the budget?",
});
// Cloud handles:
// - Pub/sub infrastructure (managed Redis)
// - Agent execution (webhooks or serverless functions)
// - Subscription wiring (automatic)

Core Helpers

send()

Send a message from one agent to another. Stores in ACID conversation + both agents' Vector memories.

No Pub/Sub Required

This is a fire-and-forget operation. Works in all modes without pub/sub.

Signature:

cortex.a2a.send(
params: A2ASendParams
): Promise<A2AMessage>

Parameters:

interface A2ASendParams {
// Required
from: string; // Sender agent ID
to: string; // Receiver agent ID
message: string; // The message content

// Optional linking
userId?: string; // If about a specific user (GDPR-enabled)
contextId?: string; // If part of a workflow

// Optional control
importance?: number; // 0-100 (default: 60)
trackConversation?: boolean; // Store in ACID (default: true)

// Cloud Mode
autoEmbed?: boolean; // Auto-generate embeddings (Cloud Mode)

// Metadata
metadata?: {
tags?: string[];
priority?: "low" | "normal" | "high" | "urgent";
[key: string]: any;
};
}

Returns:

interface A2AMessage {
messageId: string; // Unique message ID
sentAt: number; // Unix timestamp (milliseconds)

// ACID tracking
conversationId?: string; // ACID conversation (if trackConversation=true)
acidMessageId?: string; // Message ID in ACID

// Vector storage
senderMemoryId: string; // Memory ID in sender's storage
receiverMemoryId: string; // Memory ID in receiver's storage
}

Side Effects:

  • Creates/updates A2A conversation in Layer 1a (ACID) - unless trackConversation: false
  • Stores memory in sender's Vector index (Layer 2) with conversationRef
  • Stores memory in receiver's Vector index (Layer 2) with conversationRef
  • Both memories reference same ACID message

Example 1: Basic send

const result = await cortex.a2a.send({
from: "sales-agent",
to: "support-agent",
message: "Customer is asking about enterprise pricing",
importance: 70,
});

console.log(`Message ${result.messageId} sent`);
console.log(`ACID conversation: ${result.conversationId}`);
console.log(`Sender memory: ${result.senderMemoryId}`);
console.log(`Receiver memory: ${result.receiverMemoryId}`);

Example 2: With user context

// A2A about a specific user (GDPR-enabled)
await cortex.a2a.send({
from: "support-agent",
to: "billing-agent",
message: "User-123 requesting invoice for October",
userId: "user-123", // ← Links to user (enables GDPR cascade)
importance: 75,
metadata: {
tags: ["billing", "invoice", "request"],
requestType: "invoice",
},
});

// Later: GDPR deletion includes this A2A message
await cortex.users.delete("user-123", { cascade: true });
// A2A message deleted from both agents!

Example 3: With workflow context

// A2A as part of larger workflow
const context = await cortex.contexts.create({
purpose: "Process refund for order #789",
memorySpaceId: "supervisor-agent-space",
userId: "user-123",
});

await cortex.a2a.send({
from: "supervisor-agent",
to: "finance-agent",
message: "Please approve $500 refund",
userId: "user-123",
contextId: context.id, // ← Links to workflow
importance: 85,
});

// Finance agent can access full context
const ctx = await cortex.contexts.get(context.id);
console.log("Workflow purpose:", ctx.purpose);

Example 4: Fire-and-forget (no ACID tracking)

// Low-value notification (skip ACID to save storage)
await cortex.a2a.send({
from: "agent-1",
to: "agent-2",
message: "FYI: Daily report generated",
trackConversation: false, // ← Skip ACID storage
importance: 30,
});

// Still stores in Vector memories (both agents)
// But no conversationRef (not tracked in ACID)

Errors:

  • A2AValidationError('INVALID_AGENT_ID') - from or to is empty, malformed, or exceeds 100 chars
  • A2AValidationError('EMPTY_MESSAGE') - Message is empty or whitespace-only
  • A2AValidationError('MESSAGE_TOO_LARGE') - Message exceeds 100KB (UTF-8 bytes)
  • A2AValidationError('SAME_AGENT_COMMUNICATION') - from and to are the same agent
  • A2AValidationError('INVALID_IMPORTANCE') - Importance not integer 0-100

See Also:


request()

Send a request and wait for response (synchronous request-response pattern).

Requires Pub/Sub Infrastructure

This operation requires real-time messaging.

  • Direct Mode: Configure your own Redis/RabbitMQ/NATS adapter (see Configuration)
  • Cloud Mode (planned): Pub/sub infrastructure will be included automatically

How it works:

  1. Agent A calls request() → Stores in database + publishes to Agent B's inbox channel
  2. Agent B receives pub/sub notification → Fetches from database → Processes → Responds
  3. Agent B sends response → Stores in database + publishes to Agent A's response channel
  4. Agent A receives pub/sub notification → Returns response to caller

Storage: Convex (persistent)
Notifications: Pub/sub (ephemeral, real-time)

Signature:

cortex.a2a.request(
params: A2ARequestParams
): Promise<A2AResponse>

Parameters:

interface A2ARequestParams {
// Required
from: string;
to: string;
message: string;

// Timeout control
timeout?: number; // ms (default: 30000)
retries?: number; // Retry attempts (default: 1)

// Optional linking
userId?: string;
contextId?: string;

// Optional
importance?: number; // 0-100
}

Returns:

interface A2AResponse {
response: string; // Response message
messageId: string; // Original request message ID
responseMessageId: string; // Response message ID
respondedAt: number; // Unix timestamp (milliseconds)
responseTime: number; // ms
}

Side Effects:

  • Sends request via send() (creates ACID + Vector memories)
  • Publishes notification to receiver's inbox channel (pub/sub)
  • Subscribes to response channel (pub/sub)
  • Marks request as responded when answer received
  • Throws timeout error if no response within timeout period

Example 1: Basic request

try {
const response = await cortex.a2a.request({
from: "finance-agent",
to: "hr-agent",
message: "What is the Q4 headcount budget?",
timeout: 30000, // 30 seconds
importance: 85,
});

console.log(`Response: ${response.response}`);
console.log(`Response time: ${response.responseTime}ms`);
} catch (error) {
if (error instanceof A2ATimeoutError) {
console.log("HR agent did not respond within 30 seconds");
// Fallback logic
}
}

Example 2: With retries

// Auto-retry on timeout
const response = await cortex.a2a.request({
from: "agent-1",
to: "agent-2",
message: "Complex calculation needed",
timeout: 60000, // 60 second timeout
retries: 3, // Will retry up to 3 times
importance: 85,
});

// Total possible wait: 60s × 3 = 180s max

Example 3: Receiver handles requests (you implement this)

// Direct Mode: YOU implement agent execution + pub/sub handling
// See Configuration section for complete example

// In Cloud Mode: This would be automatic via webhooks

Alternative: Batch processing without pub/sub

// If you need to manually check for requests (batch processing)
async function handleIncomingRequests(memorySpaceId: string, agentId: string) {
// List all A2A memories and filter for pending requests
const allA2A = await cortex.vector.list({
memorySpaceId,
sourceType: "a2a",
limit: 100,
});

// Filter for pending requests (client-side filtering)
const requests = allA2A.filter((m) => {
const meta = m.metadata as {
messageType?: string;
requiresResponse?: boolean;
responded?: boolean;
};
return (
meta.messageType === "request" &&
meta.requiresResponse === true &&
meta.responded !== true
);
});

for (const request of requests) {
const answer = await processRequest(request.content);
const meta = request.metadata as { fromAgent?: string; messageId?: string };

await cortex.a2a.send({
from: agentId,
to: meta.fromAgent!,
message: answer,
metadata: {
messageType: "response",
inReplyTo: meta.messageId,
},
});

// Mark as responded
await cortex.vector.update(memorySpaceId, request.memoryId, {
metadata: {
...request.metadata,
responded: true,
respondedAt: Date.now(),
},
});
}
}

// Run on schedule
cron.schedule("*/5 * * * *", () =>
handleIncomingRequests("hr-agent", "hr-agent"),
);

Errors:

  • A2AValidationError('INVALID_AGENT_ID') - from or to is empty, malformed, or exceeds 100 chars
  • A2AValidationError('EMPTY_MESSAGE') - Message is empty or whitespace-only
  • A2AValidationError('MESSAGE_TOO_LARGE') - Message exceeds 100KB (UTF-8 bytes)
  • A2AValidationError('SAME_AGENT_COMMUNICATION') - from and to are the same agent
  • A2AValidationError('INVALID_TIMEOUT') - Timeout not between 1000ms and 300000ms
  • A2AValidationError('INVALID_RETRIES') - Retries not integer 0-10
  • A2AValidationError('INVALID_IMPORTANCE') - Importance not integer 0-100
  • A2ATimeoutError - No response within timeout (includes pub/sub not configured)

See Also:


broadcast()

Send one message to multiple agents efficiently.

Requires Pub/Sub Infrastructure

Optimized delivery requires real-time messaging.

  • Direct Mode: Configure your own pub/sub adapter
  • Cloud Mode (planned): Pub/sub infrastructure will be included

Signature:

cortex.a2a.broadcast(
params: A2ABroadcastParams
): Promise<A2ABroadcastResult>

Parameters:

interface A2ABroadcastParams {
// Required
from: string;
to: string[]; // Multiple recipients
message: string;

// Optional linking
userId?: string;
contextId?: string;

// Optional
importance?: number; // 0-100
trackConversation?: boolean; // Default: true

// Metadata
metadata?: {
tags?: string[];
[key: string]: any;
};
}

Returns:

interface A2ABroadcastResult {
messageId: string; // Broadcast ID (same for all)
sentAt: number; // Unix timestamp (milliseconds)
recipients: string[];

// Storage results
senderMemoryIds: string[]; // One per recipient
receiverMemoryIds: string[]; // One per recipient
memoriesCreated: number; // Total (sender + receiver for each)

// ACID tracking (if trackConversation=true)
conversationIds?: string[]; // One per recipient
}

Side Effects:

  • Creates N memories in sender's storage (one per recipient)
  • Creates N memories in receivers' storage (one per sender)
  • Total: 2N memories created
  • Each pair gets separate A2A conversation in ACID (if tracking enabled)

Example 1: Team announcement

const result = await cortex.a2a.broadcast({
from: "manager-agent",
to: ["dev-agent-1", "dev-agent-2", "qa-agent", "designer-agent"],
message: "Sprint review meeting Friday at 2 PM",
importance: 70,
metadata: {
tags: ["meeting", "sprint-review", "team"],
meetingId: "meeting-456",
},
});

console.log(`Broadcast to ${result.recipients.length} agents`);
console.log(`Created ${result.memoriesCreated} total memories`);
// 4 recipients = 8 memories (4 sender + 4 receiver)

Example 2: Policy update broadcast

// Notify all agents about policy change
const allAgents = await cortex.agents.list();

await cortex.a2a.broadcast({
from: "admin-agent",
to: allAgents.map((a) => a.id),
message: "New policy: All refunds over $1000 require manager approval",
importance: 90,
metadata: {
tags: ["policy", "announcement", "important"],
policyId: "POL-789",
effectiveDate: new Date("2025-11-01"),
},
});

// Each agent can query their announcements
const allA2A = await cortex.vector.list({
memorySpaceId: "agent-1",
sourceType: "a2a",
limit: 100,
});

// Filter for broadcast announcements
const announcements = allA2A.filter((m) => {
const meta = m.metadata as { broadcast?: boolean };
return meta.broadcast === true && m.tags.includes("announcement");
});

Errors:

  • A2AValidationError('INVALID_AGENT_ID') - from or any to[] entry is empty, malformed, or exceeds 100 chars
  • A2AValidationError('EMPTY_MESSAGE') - Message is empty or whitespace-only
  • A2AValidationError('MESSAGE_TOO_LARGE') - Message exceeds 100KB (UTF-8 bytes)
  • A2AValidationError('EMPTY_RECIPIENTS') - to[] array is empty
  • A2AValidationError('TOO_MANY_RECIPIENTS') - More than 100 recipients
  • A2AValidationError('DUPLICATE_RECIPIENTS') - Same agent ID appears multiple times
  • A2AValidationError('INVALID_RECIPIENT') - Sender is in recipients list
  • A2AValidationError('INVALID_IMPORTANCE') - Importance not integer 0-100

See Also:


getConversation()

Get chronological conversation between two agents with rich filtering.

Signature:

cortex.a2a.getConversation(
agent1: string,
agent2: string,
filters?: A2AConversationFilters
): Promise<A2AConversation>

Parameters:

interface A2AConversationFilters {
// Time range
since?: Date;
until?: Date;

// Filtering
minImportance?: number; // 0-100
tags?: string[];
userId?: string; // A2A about specific user

// Pagination
limit?: number; // Default: 100, max: 1000
offset?: number;

// Format (currently only "chronological" is supported)
format?: "chronological"; // Default and only option
}

Returns:

interface A2AConversation {
participants: [string, string];
conversationId?: string; // ACID conversation ID (if exists)
messageCount: number;

messages: A2AConversationMessage[];

period: {
start: number; // Unix timestamp (milliseconds)
end: number; // Unix timestamp (milliseconds)
};

tags?: string[]; // Filtered tags
canRetrieveFullHistory: boolean; // True if ACID conversation exists
}

interface A2AConversationMessage {
from: string;
to: string;
message: string;
importance: number;
timestamp: number; // Unix timestamp (milliseconds)
messageId: string;
memoryId: string; // Vector memory ID
acidMessageId?: string; // ACID message ID (if tracked)
tags?: string[];
direction?: string; // 'outbound' or 'inbound'
broadcast?: boolean; // True if part of a broadcast
broadcastId?: string; // Broadcast ID (if broadcast)
}

Side Effects:

  • None (read-only)

Example 1: Get complete conversation

// Get all communication between two agents
const convo = await cortex.a2a.getConversation("finance-agent", "hr-agent");

console.log(`${convo.messageCount} messages exchanged`);
convo.messages.forEach((msg) => {
console.log(`[${msg.timestamp}] ${msg.from}${msg.to}`);
console.log(` ${msg.message}`);
});

// Can retrieve full ACID history if needed
if (convo.canRetrieveFullHistory) {
const fullHistory = await cortex.conversations.get(convo.conversationId);
console.log(
"Complete conversation:",
fullHistory.messages.length,
"messages",
);
}

Example 2: Filtered conversation

// Get only important budget-related messages from last 30 days
const filtered = await cortex.a2a.getConversation("finance-agent", "hr-agent", {
since: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
minImportance: 70,
tags: ["budget"],
limit: 50,
});

console.log(`Found ${filtered.messageCount} important budget discussions`);

Example 3: User-specific A2A

// Get all A2A about a specific user
const userRelated = await cortex.a2a.getConversation(
"support-agent",
"billing-agent",
{
userId: "user-123", // Only A2A about this user
since: new Date("2025-10-01"),
},
);

console.log(`${userRelated.messageCount} messages about user-123`);

Errors:

  • A2AValidationError('INVALID_AGENT_ID') - agent1 or agent2 is empty, malformed, or exceeds 100 chars
  • A2AValidationError('INVALID_DATE_RANGE') - 'since' is after 'until'
  • A2AValidationError('INVALID_IMPORTANCE') - minImportance not between 0-100
  • A2AValidationError('INVALID_LIMIT') - Limit not integer 1-1000
  • A2AValidationError('INVALID_OFFSET') - Offset is negative

See Also:


Querying A2A Messages (Using Memory API)

Since A2A uses the memory system, you can use any memory operation on A2A messages:

Finding A2A Messages

// All A2A for an agent (semantic search on "a2a" query)
const allA2A = await cortex.memory.search("agent-1", "a2a communication", {
sourceType: "a2a",
limit: 100,
});

// Use list() for non-semantic filtering by source type
const allA2AList = await cortex.vector.list({
memorySpaceId: "agent-1",
sourceType: "a2a",
limit: 100,
});

// Filter by tags (sent vs received are tagged)
const sent = await cortex.vector.list({
memorySpaceId: "agent-1",
sourceType: "a2a",
// Post-filter for 'sent' tag
});

Semantic Search on A2A

// Semantic search across A2A messages
const results = await cortex.memory.search(
"agent-1",
"did we discuss the budget increase?",
{
embedding: await embed("budget increase discussion"),
sourceType: "a2a",
minImportance: 50,
limit: 10,
},
);

results.forEach((m) => {
const meta = m.metadata as { toAgent?: string; fromAgent?: string };
console.log(`${m.content}`);
console.log(` With: ${meta.toAgent || meta.fromAgent}`);
});

Finding Unanswered Requests

// Get A2A requests and filter for pending
const a2aMemories = await cortex.vector.list({
memorySpaceId: "hr-agent",
sourceType: "a2a",
limit: 100,
});

// Filter for pending requests (client-side filtering)
const pending = a2aMemories.filter((m) => {
const meta = m.metadata as {
messageType?: string;
requiresResponse?: boolean;
responded?: boolean;
};
return (
meta.messageType === "request" &&
meta.requiresResponse === true &&
meta.responded !== true
);
});

console.log(`${pending.length} pending requests`);

Counting A2A Messages

// Total A2A messages
const total = await cortex.vector.count({
memorySpaceId: "agent-1",
sourceType: "a2a",
});

console.log(`Total A2A messages: ${total}`);

Integration with Other Systems

A2A + Contexts (Workflow Coordination)

// Create workflow context
const context = await cortex.contexts.create({
purpose: "Process refund request",
memorySpaceId: "supervisor-agent-space",
userId: "user-123",
});

// Delegate via A2A linked to context
await cortex.a2a.send({
from: "supervisor-agent",
to: "specialist-agent",
message: "Please handle refund for ticket #456",
userId: "user-123",
contextId: context.id, // ← Links to workflow
importance: 85,
});

// Specialist can access context
const ctx = await cortex.contexts.get(context.id);

// Find all A2A for this workflow
const allA2A = await cortex.vector.list({
memorySpaceId: "supervisor-agent",
sourceType: "a2a",
limit: 100,
});

// Filter for this context
const workflowComms = allA2A.filter((m) => {
const meta = m.metadata as { contextId?: string };
return meta.contextId === context.id;
});

A2A + Conversations (User-Triggered)

// User makes request
const userMsg = await cortex.conversations.addMessage("conv-456", {
role: "user",
text: "I need help with my refund",
userId: "user-123",
});

// Create context linked to conversation
const context = await cortex.contexts.create({
purpose: "Handle user refund request",
memorySpaceId: "support-agent-space",
userId: "user-123",
conversationRef: {
conversationId: "conv-456",
messageIds: [userMsg.id],
},
});

// A2A delegation with full traceability
await cortex.a2a.send({
from: "support-agent",
to: "finance-agent",
message: "User needs refund approval for $500",
userId: "user-123",
contextId: context.id,
});

// Finance agent can trace back to original user request
const ctx = await cortex.contexts.get(context.id, {
includeConversation: true,
});
console.log("Original user request:", ctx.triggerMessages[0].text);

A2A + Users (GDPR Cascade)

// A2A about a user
await cortex.a2a.send({
from: "support-agent",
to: "billing-agent",
message: "User requesting account deletion",
userId: "user-123", // ← GDPR link
importance: 95,
});

// User requests deletion
const result = await cortex.users.delete("user-123", { cascade: true });

console.log(`A2A messages deleted: ${result.vectorMemoriesDeleted}`);
// Includes the A2A message above!

Advanced Patterns

Pattern 1: Complete Traceability

// Full traceability: User → Context → A2A → Memories

// 1. User conversation (Layer 1a - ACID)
const userMsg = await cortex.conversations.addMessage("conv-456", {
role: "user",
text: "I need a refund",
userId: "user-123",
});

// 2. Workflow context (linked to conversation)
const context = await cortex.contexts.create({
purpose: "Process refund",
memorySpaceId: "supervisor-agent-space",
userId: "user-123",
conversationRef: {
conversationId: "conv-456",
messageIds: [userMsg.id],
},
});

// 3. A2A delegation (linked to context AND conversation)
const a2aResult = await cortex.a2a.send({
from: "supervisor-agent",
to: "specialist-agent",
message: "Handle refund for user",
userId: "user-123",
contextId: context.id,
});

// Now specialist agent can access:
// - A2A message (in their memory)
const a2aMemory = await cortex.memory.get(
"specialist-agent",
a2aResult.receiverMemoryId,
);

// - Context chain
const ctx = await cortex.contexts.get(a2aMemory.metadata.contextId);

// - Original user conversation (via context)
const userConvo = await cortex.conversations.get(
ctx.conversationRef.conversationId,
);

// Complete audit trail!

Pattern 2: A2A Communication Analytics

// Analyze communication patterns
async function analyzeA2ACommunication(
memorySpaceId: string,
period: number = 30,
) {
const sinceTimestamp = Date.now() - period * 24 * 60 * 60 * 1000;

// Get all A2A memories
const allMemories = await cortex.vector.list({
memorySpaceId,
sourceType: "a2a",
limit: 1000,
});

// Filter for period
const allA2A = allMemories.filter((m) => m.createdAt >= sinceTimestamp);

// Type metadata
type A2AMeta = {
direction?: string;
toAgent?: string;
fromAgent?: string;
};

// Sent vs received
const sent = allA2A.filter(
(m) => (m.metadata as A2AMeta).direction === "outbound",
);
const received = allA2A.filter(
(m) => (m.metadata as A2AMeta).direction === "inbound",
);

// By partner agent
const partners: Record<string, number> = {};
allA2A.forEach((m) => {
const meta = m.metadata as A2AMeta;
const partner = meta.toAgent || meta.fromAgent;
if (partner) {
partners[partner] = (partners[partner] || 0) + 1;
}
});

// By importance (importance is a top-level field on MemoryEntry)
const critical = allA2A.filter((m) => m.importance >= 90).length;
const important = allA2A.filter((m) => m.importance >= 70).length;

return {
total: allA2A.length,
sent: sent.length,
received: received.length,
partners,
avgImportance:
allA2A.reduce((sum, m) => sum + m.importance, 0) / allA2A.length || 0,
critical,
important,
};
}

const stats = await analyzeA2ACommunication("finance-agent", 30);
console.log("Last 30 days:", stats);

Pattern 3: Bulk Operations on A2A

// List A2A memories for bulk processing
const a2aMemories = await cortex.vector.list({
memorySpaceId: "agent-1",
sourceType: "a2a",
limit: 1000,
});

// Filter old, low-importance messages for cleanup
const oldLowImportance = a2aMemories.filter((m) => {
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000;
return m.createdAt < ninetyDaysAgo && m.importance <= 50;
});

// Update each memory individually (mark as archived)
for (const memory of oldLowImportance) {
await cortex.vector.update("agent-1", memory.memoryId, {
metadata: { ...memory.metadata, archived: true },
});
}

// Delete trivial old A2A one by one
const trivialOld = a2aMemories.filter((m) => {
const sixMonthsAgo = Date.now() - 180 * 24 * 60 * 60 * 1000;
return m.createdAt < sixMonthsAgo && m.importance <= 30 && m.accessCount <= 1;
});

for (const memory of trivialOld) {
await cortex.vector.delete("agent-1", memory.memoryId);
}

// Export A2A for audit (manual serialization)
const auditMemories = a2aMemories.filter((m) => {
const start = new Date("2025-10-01").getTime();
const end = new Date("2025-10-31").getTime();
return m.createdAt >= start && m.createdAt <= end;
});

// Write to file or process as needed
const auditData = JSON.stringify(auditMemories, null, 2);
// fs.writeFileSync('audits/october-a2a.json', auditData);

A2A Message Structure

When you use A2A helpers, messages are stored as standard Vector memories with specific structure:

// Sender's memory
interface A2ASenderMemory extends MemoryEntry {
memorySpaceId: string; // Sender's memory space
content: string; // 'Sent to {recipient}: {message}'

source: {
type: "a2a";
fromAgent: string; // Sender
toAgent: string; // Recipient
timestamp: Date;
};

conversationRef?: {
// Links to ACID A2A conversation
conversationId: string; // e.g., 'a2a-conv-789'
messageIds: string[];
};

metadata: {
importance: number; // 0-100
tags: string[]; // Includes 'a2a', 'sent', recipient
direction: "outbound";
messageId: string; // A2A message ID

// Optional
contextId?: string; // Workflow link
userId?: string; // User link (GDPR)
broadcast?: boolean; // If broadcast
broadcastId?: string;
messageType?: "request" | "response";
inReplyTo?: string; // For responses
requiresResponse?: boolean;
responded?: boolean;
};
}

// Receiver's memory (similar structure with direction='inbound')

This means all memory operations work on A2A:

  • cortex.memory.search() - Semantic or text search
  • cortex.memory.get() - Retrieve by ID
  • cortex.memory.update() - Update (creates version)
  • cortex.memory.delete() - Delete
  • cortex.memory.count() - Count
  • All universal filters apply!

When to Use Each Approach

Use A2A Helpers When:

  • Simple send - One agent to another
  • Request-response - Need synchronous answer
  • Broadcasting - One to many
  • Viewing conversation - Chronological thread between two agents
  • Less code - Want convenience

Use Memory API Directly When:

  • Complex queries - Semantic search, multiple filters
  • Bulk operations - updateMany, deleteMany on A2A messages
  • Custom logic - Need full control
  • Advanced filtering - Combining many criteria
  • Performance - Optimizing specific queries

Mix Both:

// Send with helper (convenience)
await cortex.a2a.send({
from: "agent-1",
to: "agent-2",
message: "Important update",
importance: 85,
});

// Query with memory API (power) - semantic search
const results = await cortex.memory.search("agent-2", "important update", {
embedding: await embed("important update"),
sourceType: "a2a",
minImportance: 80,
limit: 20,
});

// Filter for last 7 days (client-side)
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const important = results.filter((m) => m.createdAt >= sevenDaysAgo);

Best Practices

1. Set Appropriate Importance

// A2A importance guidelines (0-100 scale)
const A2A_IMPORTANCE = {
CRITICAL_DECISION: 95, // Major decisions, approvals
URGENT_REQUEST: 85, // Time-sensitive requests
IMPORTANT_INFO: 75, // Key information sharing
STANDARD_COLLAB: 60, // Regular collaboration (default)
STATUS_UPDATE: 50, // Routine updates
FYI: 40, // Nice-to-know information
NOTIFICATION: 30, // Low-priority notifications
DEBUG: 10, // Debug/diagnostic messages
};

await cortex.a2a.send({
from: "agent-1",
to: "agent-2",
message: "Board approved budget increase",
importance: A2A_IMPORTANCE.CRITICAL_DECISION,
});

2. Use Tags for Organization

await cortex.a2a.send({
from: "agent-1",
to: "agent-2",
message: "Approved budget increase",
importance: 90,
metadata: {
tags: [
"a2a", // Automatic
"approval", // Action type
"budget", // Topic
"finance", // Department
"urgent", // Priority
],
approvalType: "budget",
approvedAmount: 50000,
},
});

// Query by tags using semantic search
const approvals = await cortex.memory.search("agent-1", "budget approval", {
sourceType: "a2a",
tags: ["approval", "budget"],
limit: 20,
});
// Always link A2A to context when part of workflow
const context = await cortex.contexts.create({
purpose: "Process refund",
memorySpaceId: "supervisor-agent-space",
});

await cortex.a2a.send({
from: "supervisor-agent",
to: "specialist-agent",
message: "Handle this refund",
contextId: context.id, // ← Links to workflow
});

4. Track Conversation for Audit

// Default: Track in ACID (audit trail)
await cortex.a2a.send({
from: "agent-1",
to: "agent-2",
message: "Important decision",
importance: 90,
// trackConversation: true (default)
});

// Only skip for ephemeral, low-value messages
await cortex.a2a.send({
from: "agent-1",
to: "agent-2",
message: "Heartbeat ping",
importance: 10,
trackConversation: false, // Skip ACID (save storage)
});

5. Handle Request Timeouts

// Use try-catch for request() pattern
async function askWithFallback(from: string, to: string, message: string) {
try {
const response = await cortex.a2a.request({
from,
to,
message,
timeout: 30000,
retries: 2,
});

return response.response;
} catch (error) {
if (error instanceof A2ATimeoutError) {
// Fallback to async
await cortex.a2a.send({
from,
to,
message: `${message} (respond when ready)`,
metadata: { async: true },
});

return null; // Will check later
}
throw error;
}
}

Comparison: Helper vs Direct

OperationA2A HelperMemory API DirectLines of Code
Send messagea2a.send()ACID + 2x Vector stores7 vs 40+
Request-responsea2a.request()Send + polling loop5 vs 35+
Broadcasta2a.broadcast()Loop + Nx2 stores8 vs 50+
Get conversationa2a.getConversation()Bidirectional search + merge6 vs 40+
Total saved26 lines165+ lines85% reduction
Helpers are pure convenience

Everything A2A helpers do can be done with the memory API directly. But they eliminate boilerplate and ensure consistency.


Cloud Mode Features (Planned)

Cloud Mode Under Development

The following features are planned for Cortex Cloud Mode. The SDK provides full A2A functionality today via Direct Mode.

A2A Analytics Dashboard

  • Communication frequency between agent pairs
  • Average response times
  • Bottleneck identification (slow responders)
  • Collaboration graphs and network visualization
  • Topic clustering from A2A messages

Smart Routing

  • AI suggests which agent to ask based on expertise
  • Learns from communication patterns
  • Automatic load balancing

Automated Summarization

  • Daily/weekly A2A summaries per agent
  • Topic extraction and clustering
  • Action item detection

Graph-Lite Capabilities

A2A communication creates an agent collaboration graph:

Graph Structure:

  • Nodes: Agents
  • Edges: Messages (with properties: direction, importance, timestamp)

A2A as Directed Graph:

finance-agent
├──[SENT_TO {importance: 85}]──> hr-agent
├──[SENT_TO {importance: 90}]──> legal-agent
├──[RECEIVED_FROM]<──────────── ceo-agent
└──[SENT_TO]────────────────────> ops-agent

Graph Queries via Memory API:

type A2AMeta = {
direction?: string;
toAgent?: string;
fromAgent?: string;
};

// 1-hop: Direct collaborators
const allA2A = await cortex.vector.list({
memorySpaceId: "finance-agent",
sourceType: "a2a",
limit: 1000,
});

const sent = allA2A.filter(
(m) => (m.metadata as A2AMeta).direction === "outbound",
);
const directCollaborators = new Set(
sent.map((m) => (m.metadata as A2AMeta).toAgent).filter(Boolean),
);

// 2-hop: Collaborator's collaborators
const secondDegree = new Set<string>();
for (const agent of directCollaborators) {
const theirA2A = await cortex.vector.list({
memorySpaceId: agent as string,
sourceType: "a2a",
limit: 1000,
});

const theirSent = theirA2A.filter(
(m) => (m.metadata as A2AMeta).direction === "outbound",
);

theirSent.forEach((m) => {
const toAgent = (m.metadata as A2AMeta).toAgent;
if (toAgent && toAgent !== "finance-agent") {
secondDegree.add(toAgent);
}
});
}

// Build weighted graph (edge weights = message count + importance)
const edges = new Map<string, { count: number; avgImportance: number }>();
sent.forEach((m) => {
const partner = (m.metadata as A2AMeta).toAgent;
if (!partner) return;

const current = edges.get(partner) || { count: 0, avgImportance: 0 };
edges.set(partner, {
count: current.count + 1,
avgImportance:
(current.avgImportance * current.count + m.importance) /
(current.count + 1),
});
});

Performance:

  • 1-hop (direct collaborators): 20-50ms
  • 2-hop (network expansion): 100-200ms
  • 3-hop: 200-400ms (consider graph DB for frequent deep queries)

Network Analysis:

For advanced graph analytics (community detection, centrality, influence metrics), consider integrating a graph database:

Learn more: Graph Database Integration


Error Reference

A2AValidationError Class

The SDK exports a custom error class for validation failures:

import { A2AValidationError } from "@cortex/sdk/a2a";

// Error properties
interface A2AValidationError extends Error {
name: "A2AValidationError";
code: string; // Error code (e.g., 'INVALID_AGENT_ID')
field?: string; // Field that failed validation (e.g., 'from', 'to')
}

// Usage
try {
await cortex.a2a.send({
from: "", // Invalid - empty
to: "agent-2",
message: "Hello",
});
} catch (error) {
if (error instanceof A2AValidationError) {
console.log(`Validation failed: ${error.code}`);
console.log(`Field: ${error.field}`); // 'from'
}
}

A2ATimeoutError Class

Thrown when request() times out waiting for a response:

import { A2ATimeoutError } from "@cortex/sdk";

interface A2ATimeoutError extends Error {
name: "A2ATimeoutError";
messageId: string; // ID of the request message
timeout: number; // Timeout value in milliseconds
}

try {
await cortex.a2a.request({
from: "agent-1",
to: "agent-2",
message: "Hello",
});
} catch (error) {
if (error instanceof A2ATimeoutError) {
console.log(
`Request ${error.messageId} timed out after ${error.timeout}ms`,
);
}
}

All Error Codes

Error CodeDescriptionCause
INVALID_AGENT_IDAgent ID is invalidEmpty, malformed, or exceeds 100 chars
EMPTY_MESSAGEMessage is emptyMessage is empty or whitespace-only
MESSAGE_TOO_LARGEMessage exceeds limitMessage exceeds 100KB (UTF-8 bytes)
EMPTY_RECIPIENTSNo recipientsto[] array is empty
TOO_MANY_RECIPIENTSToo many recipientsMore than 100 recipients in broadcast
DUPLICATE_RECIPIENTSDuplicate recipientsSame agent ID appears multiple times in to[]
INVALID_RECIPIENTInvalid recipientSender included in recipients list
SAME_AGENT_COMMUNICATIONSelf-communicationfrom and to are the same agent
INVALID_IMPORTANCEInvalid importanceImportance not integer 0-100
INVALID_TIMEOUTInvalid timeoutTimeout not between 1000ms and 300000ms
INVALID_RETRIESInvalid retriesRetries not integer 0-10
INVALID_DATE_RANGEInvalid date range'since' is after 'until'
INVALID_LIMITInvalid limitLimit not integer 1-1000
INVALID_OFFSETInvalid offsetOffset is negative
PUBSUB_NOT_CONFIGUREDPub/sub requiredrequest() needs pub/sub adapter
A2ATimeoutErrorRequest timeoutNo response within timeout
A2AValidationErrorValidation failedClient-side validation error (see code/field)
CONVEX_ERRORDatabase errorConvex operation failed

See Also:


Next Steps


Questions? Ask in GitHub Discussions.