Skip to main content

Agent Registry

Info
Last Updated: 2026-01-08

Overview

The agent registry provides optional metadata registration for agents, enabling discovery, analytics, and team organization. This is complementary to memory spaces, which define isolation boundaries.

Key Concepts:

  • Memory Spaces: Primary isolation boundary (replaces old agentId role)
  • Agent Registry: Optional metadata layer for tracking agent information
  • Complementary: Both serve different purposes and can be used together

Agents vs Memory Spaces

FeatureMemory SpacesAgent Registry
PurposeDefine isolation boundariesTrack agent metadata
StatusActive, required conceptActive, optional feature
Use CaseScope for data (conversations, memories, facts)Analytics, discovery, team organization
RegistrationOptionalOptional
EnablesHive Mode, Collaboration ModeAgent discovery, statistics, cascade deletion
SupportsMulti-participant spacesSingle agent tracking
Query bymemorySpaceIdagentId, capabilities, team
Cascade deleteAll data in spaceAll data by participantId across spaces

Architecture Pattern:

Memory spaces support:

  • Hive Mode: Multiple participants (Cursor, Claude, etc.) share one memory space
  • Collaboration Mode: Memory spaces delegate via context chains
  • Flexible boundaries: Per-user, per-team, per-project isolation

Two Complementary Systems:

  1. Memory Spaces (cortex.memorySpaces.*): Define isolation boundaries and manage participants
  2. Agent Registry (cortex.agents.*): Track agent metadata, analytics, and capabilities
Three Registration Modes
Simple Mode (String IDs)

No registration needed. Just works with any memorySpaceId string.

Memory Space Registry

Isolation boundaries. Enables Hive Mode and cross-space collaboration.

Agent Registry

Optional metadata for agent tracking. Use for analytics, discovery, team organization.

Simple Mode, Memory Space Registry, and Agent Registry serve different purposes

Architecture Principle: Memory spaces define isolation, agent registry provides optional metadata and analytics.


Memory Spaces vs Agent Registry

Memory Space Document (Isolation Boundary)

{
_id: Id<"memorySpaces">,

// Identity
memorySpaceId: string, // Unique identifier
name?: string, // Human-readable name
tenantId?: string, // Multi-tenancy support

// Type
type: "personal" | "team" | "project" | "custom",

// Participants (Hive Mode)
participants: Array<{
id: string, // 'cursor', 'claude', 'my-bot', etc.
type: string, // 'ai-tool', 'human', 'ai-agent', 'system'
joinedAt: number,
}>,

// Metadata
metadata: any,
status: "active" | "archived",

// Timestamps
createdAt: number,
updatedAt: number,
}

Indexes:

  • by_memorySpaceId - Unique lookup
  • by_tenantId - Tenant's memory spaces
  • by_tenant_memorySpaceId - Tenant-scoped lookup
  • by_status - Filter active/archived
  • by_type - Filter by type

Agent Document (Optional Metadata Layer)

{
_id: Id<"agents">,
agentId: string, // Agent identifier (for metadata tracking)
tenantId?: string,

name: string,
description?: string,
metadata?: any, // Team, capabilities, version, etc.
config?: any, // Agent-specific configuration

status: "active" | "inactive" | "archived",

registeredAt: number,
updatedAt: number,
lastActive?: number,
}

Index:

  • by_agentId - Unique lookup

Registration vs Simple Mode

Simple Mode (Default)

// No registration needed
await cortex.memory.store('chatbot-v1', {
content: 'User prefers dark mode',
...
});

await cortex.memory.store('support-agent', {
content: 'Resolved ticket #456',
...
});

// Just works! (Good)
// agentId can be any string
// No setup required

Pros:

  • Zero setup
  • Maximum flexibility
  • Works immediately
  • No extra storage

Cons:

  • No analytics
  • No agent discovery
  • No centralized config
  • Can't enforce policies

Registry Mode (Enhanced)

// Register agents
await cortex.agents.register({
id: "support-agent",
name: "Support Agent",
description: "Handles customer support tickets",
metadata: {
capabilities: ["support", "billing", "technical"],
owner: "support-team",
model: "gpt-5-nano",
version: "2.0",
},
config: {
memoryVersionRetention: 20, // Override default (10)
maxMemories: 100000,
},
});

// Now get analytics
const agent = await cortex.agents.get("support-agent");
if (agent?.stats) {
console.log(`${agent.name}: ${agent.stats.totalMemories} memories`);
if (agent.stats.isApproximate) {
console.log("Note: Counts are approximate (sampled from large dataset)");
}
}

// Discovery
const supportAgents = await cortex.agents.search({
capabilities: ["support"],
});

Pros:

  • Analytics and insights
  • Agent discovery by capability
  • Per-agent configuration
  • Usage tracking
  • Better organization

Cons:

  • Extra setup step
  • Additional storage (minimal)

Registration Operations

Register Agent

export const register = mutation({
args: {
agentId: v.string(),
name: v.string(),
description: v.optional(v.string()),
metadata: v.optional(v.any()),
config: v.optional(v.any()),
},
handler: async (ctx, args) => {
// Check if already registered
const existing = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
.first();

if (existing) {
throw new ConvexError("AGENT_ALREADY_REGISTERED");
}

// Create registration
const agentId = await ctx.db.insert("agents", {
agentId: args.agentId,
name: args.name,
description: args.description,
metadata: args.metadata || {},
config: args.config || {},
status: "active",
registeredAt: Date.now(),
updatedAt: Date.now(),
});

return await ctx.db.get(agentId);
},
});

Update Agent

export const update = mutation({
args: {
agentId: v.string(),
name: v.optional(v.string()),
description: v.optional(v.string()),
metadata: v.optional(v.any()),
config: v.optional(v.any()),
status: v.optional(v.union(
v.literal("active"),
v.literal("inactive"),
v.literal("archived"),
)),
},
handler: async (ctx, args) => {
const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
.first();

if (!agent) {
throw new ConvexError("AGENT_NOT_REGISTERED");
}

// Build update object
const updates: any = {
updatedAt: Date.now(),
};

if (args.name !== undefined) updates.name = args.name;
if (args.description !== undefined) updates.description = args.description;
if (args.metadata !== undefined) updates.metadata = args.metadata;
if (args.config !== undefined) updates.config = args.config;
if (args.status !== undefined) updates.status = args.status;

await ctx.db.patch(agent._id, updates);

return await ctx.db.get(agent._id);
},
});

Unregister Agent

export const unregister = mutation({
args: {
agentId: v.string(),
},
handler: async (ctx, args) => {
const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
.first();

if (!agent) {
throw new ConvexError("AGENT_NOT_REGISTERED");
}

// Delete registration
// Note: Cascade deletion of agent data is handled in the SDK layer
await ctx.db.delete(agent._id);

return { deleted: true, agentId: args.agentId };
},
});

Statistics Tracking

Computed Statistics

Stats are computed on-demand using sampling for performance. For large datasets, counts may be approximate.

export const computeStats = query({
args: { agentId: v.string() },
handler: async (ctx, args) => {
// Use limits to avoid hitting Convex's 16MB read limit
// These are approximate counts for large datasets
const SAMPLE_LIMIT = 1000;

// Count memories where participantId = agentId (with limit)
const memories = await ctx.db
.query("memories")
.withIndex("by_participantId", (q) => q.eq("participantId", args.agentId))
.take(SAMPLE_LIMIT);

// Count conversations where memorySpaceId = agentId (with limit)
const conversations = await ctx.db
.query("conversations")
.withIndex("by_memorySpace", (q) => q.eq("memorySpaceId", args.agentId))
.take(SAMPLE_LIMIT);

// Count facts where participantId = agentId (with limit)
const facts = await ctx.db
.query("facts")
.withIndex("by_participantId", (q) => q.eq("participantId", args.agentId))
.take(SAMPLE_LIMIT);

// Find unique memory spaces from sampled memories
const memorySpaces = new Set(memories.map((m) => m.memorySpaceId));

// Find last active time from sampled data
const allTimestamps = [
...memories.map((m) => m.updatedAt),
...conversations.map((c) => c.updatedAt),
...facts.map((f) => f.updatedAt),
].filter((t): t is number => t !== undefined);

const lastActive =
allTimestamps.length > 0 ? Math.max(...allTimestamps) : undefined;

// Indicate if results are approximate (hit limit)
const isApproximate =
memories.length >= SAMPLE_LIMIT ||
conversations.length >= SAMPLE_LIMIT ||
facts.length >= SAMPLE_LIMIT;

return {
totalMemories: memories.length,
totalConversations: conversations.length,
totalFacts: facts.length,
memorySpacesActive: memorySpaces.size,
lastActive,
isApproximate, // Warning: True when counts are sampled (dataset > 1000 records)
};
},
});

Important Notes:

  • Sampling Limit: Stats use a SAMPLE_LIMIT of 1000 records per query type
  • Approximate Counts: When datasets exceed 1000 records, counts are approximate
  • isApproximate Flag: Check this flag to know if counts are exact or sampled
  • Performance: Sampling prevents hitting Convex's 16MB read limit for large datasets
  • On-Demand: Stats are computed on-demand when calling cortex.agents.get() or cortex.agents.computeStats()

Agent Discovery

Search by Capabilities

Capabilities filtering happens client-side in the SDK. Capabilities are stored in metadata.capabilities.

// SDK usage (client-side filtering)
const supportAgents = await cortex.agents.search({
capabilities: ["support", "billing"],
capabilitiesMatch: "all", // "all" (default) or "any"
});

console.log(`Found ${supportAgents.length} support agents`);

// SDK implementation (simplified)
// 1. Fetches all agents from backend
// 2. Filters client-side by checking metadata.capabilities
// 3. Returns matching agents

How it works:

  1. SDK calls list() backend query to fetch all agents
  2. Client-side filtering checks metadata.capabilities array
  3. capabilitiesMatch: "all" requires all specified capabilities
  4. capabilitiesMatch: "any" requires at least one capability

Note: There is no backend searchByCapabilities query. All filtering happens in the SDK layer for flexibility.

List All Agents

export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("agents").collect();
},
});

Per-Agent Configuration

Configuration Hierarchy

// Global defaults (Cortex config)
const cortex = new Cortex({
convexUrl: process.env.CONVEX_URL,
defaultVersionRetention: 10, // Global default
});

// Agent-specific override
await cortex.agents.configure("audit-agent", {
memoryVersionRetention: -1, // Unlimited for audit agent
});

await cortex.agents.configure("temp-agent", {
memoryVersionRetention: 1, // Only current for temp agent
});

// Apply configuration
export const storeMemory = mutation({
handler: async (ctx, args) => {
// Get agent config
const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
.unique();

const retention = agent?.config.memoryVersionRetention || 10; // Default

// Use retention when updating
// (trim previousVersions to retention limit)
},
});

Mix Registered and Unregistered

// Production agents: Register for analytics
await cortex.agents.register({
id: 'support-agent-prod',
name: 'Production Support Agent',
metadata: {
team: 'support',
capabilities: ['support', 'billing'],
version: '2.0.0',
},
});

// Development agents: Unregistered (simple strings work fine)
await cortex.memory.store('test-agent-123', { ... });
await cortex.memory.store('dev-agent-temp', { ... });

// Both work! (Good)
// Registered agents get analytics and discovery
// Unregistered agents work without any setup

Best Practice:

  • Development: Use simple string IDs (no registration needed)
  • Production: Register for analytics, discovery, and team organization
  • Temporary/Test: Use simple string IDs
  • Critical/Audit: Register for enhanced tracking and config overrides

Registry-Enabled Features

1. Analytics Dashboard (Cloud Mode - Planned)

// Get agent analytics
const agent = await cortex.agents.get("support-agent");

if (agent?.stats) {
console.log({
name: agent.name,
totalMemories: agent.stats.totalMemories,
totalConversations: agent.stats.totalConversations,
totalFacts: agent.stats.totalFacts,
memorySpacesActive: agent.stats.memorySpacesActive,
lastActive: agent.stats.lastActive ? new Date(agent.stats.lastActive) : null,
isApproximate: agent.stats.isApproximate, // Warning: True if counts are sampled
});

if (agent.stats.isApproximate) {
console.log("Warning: Stats are approximate (dataset exceeds 1000 records per type)");
}
}

// Aggregate analytics
const allAgents = await cortex.agents.list();
// Note: Stats are computed on-demand, so each agent needs get() call for stats
const agentsWithStats = await Promise.all(
allAgents.map(agent => cortex.agents.get(agent.id))
);

const totalMemories = agentsWithStats.reduce(
(sum, a) => sum + (a.stats?.totalMemories || 0),
0,
);
const hasApproximate = agentsWithStats.some(
(a) => a.stats?.isApproximate === true
);

if (hasApproximate) {
console.log("Warning: Some agent stats are approximate");
}

2. Agent Discovery

// Find agents by capability
const billingAgents = await cortex.agents.search({
capabilities: ["billing"], // Searches in metadata.capabilities
});

// Find by metadata
const productionAgents = await cortex.agents.search({
metadata: { environment: "production" },
});

// Useful for:
// - Dynamic routing
// - Load balancing
// - Agent selection

3. Per-Agent Policies

// Set agent-specific retention
await cortex.agents.configure("audit-agent", {
memoryVersionRetention: -1, // Unlimited
maxMemories: Infinity,
});

// Apply in governance
export const enforceRetention = mutation({
handler: async (ctx) => {
const agents = await ctx.db.query("agents").collect();

for (const agent of agents) {
const retention = agent.config.memoryVersionRetention || 10;

// Apply to agent's memories
await enforceRetentionForAgent(ctx, agent.agentId, retention);
}
},
});

Implementation Details

Registration is Optional Check

// Before operations, optionally verify registration
export const store = mutation({
args: { memorySpaceId: v.string(), ... },
handler: async (ctx, args) => {
// Optional: Check if agent exists
const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.memorySpaceId))
.first();

if (agent) {
// Registered: Update stats
await ctx.db.patch(agent._id, {
stats: {
...agent.stats,
totalMemories: agent.stats.totalMemories + 1,
lastActive: Date.now(),
},
});
}

// Store memory (works with or without registration)
return await ctx.db.insert("memories", {
memorySpaceId: args.memorySpaceId, // String ID works either way
participantId: args.memorySpaceId, // For agent stats tracking
...
});
},
});

No Foreign Key Constraints

// Convex doesn't enforce foreign keys
// This is valid even if agent not registered:
await ctx.db.insert("memories", {
memorySpaceId: "unregistered-agent", // Works!
content: "Test",
...
});

// vs SQL:
// INSERT INTO memories (agent_id, content) VALUES ('unregistered-agent', 'Test');
// FOREIGN KEY constraint failed

// Cortex benefit: Flexibility!
// - Development: No registration needed
// - Production: Register for features

Migration Path

From Simple to Registry

// Phase 1: Start simple (no registry)
await cortex.memory.store('agent-1', { ... });
await cortex.memory.store('agent-2', { ... });

// Phase 2: Add registry when needed
await cortex.agents.register({
id: 'agent-1', // Same ID as before
name: 'Agent One',
});

await cortex.agents.register({
id: 'agent-2',
name: 'Agent Two',
});

// Phase 3: Backfill stats
for (const agent of ['agent-1', 'agent-2']) {
await cortex.agents.computeStats(agent);
}

// All existing memories still work! (Good)
// Now have analytics on top

Cloud Mode Features (Planned)

Agent Billing

// Track usage for billing (Cloud Mode - Planned)
{
memorySpaceId: "support-agent",
stats: {
totalMemories: 50000,
memoryStorageBytes: 1200000000, // 1.2 GB
totalEmbeddings: 45000,
embeddingTokens: 2500000, // For billing
}
}

// Monthly bill calculation
const usage = agent.stats.totalEmbeddings * tokensPerEmbedding * pricePerToken;

Agent Limits (Enterprise)

export const enforceLimit = mutation({
handler: async (ctx, args) => {
const agent = await getAgent(ctx, args.agentId);

if (!agent) {
return; // Unregistered - no limits
}

// Check limits
if (agent.stats.totalMemories >= agent.config.maxMemories) {
throw new Error("AGENT_MEMORY_LIMIT_EXCEEDED");
}

if (agent.stats.memoryStorageBytes >= agent.config.maxStorageBytes) {
throw new Error("AGENT_STORAGE_LIMIT_EXCEEDED");
}

// Proceed with operation...
},
});

Performance Impact

Minimal Overhead

With registry:

  • Extra query per operation: ~5ms
  • Extra stats update: ~10ms (async, doesn't block)
  • Total overhead: < 15ms

Without registry:

  • No extra queries
  • No stats updates
  • Pure operation time

Recommendation: Registry overhead is negligible for production use.

Caching

// Cache agent registrations
const agentCache = new Map<string, Agent>();

async function getAgentCached(ctx: any, memorySpaceId: string) {
if (agentCache.has(agentId)) {
return agentCache.get(agentId);
}

const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", agentId))
.unique();

if (agent) {
agentCache.set(agentId, agent);

// Cache for 5 minutes
setTimeout(() => agentCache.delete(agentId), 5 * 60 * 1000);
}

return agent;
}

Best Practices

1. Register Production Agents

// Good: Register production agents
await cortex.agents.register({
id: 'support-agent-prod',
name: 'Production Support Agent',
metadata: { environment: 'production' },
});

// Avoid: Don't register temporary agents
await cortex.memory.store('test-agent-' + Date.now(), { ... });
// Just use string ID

2. Use Descriptive Names

// Good
await cortex.agents.register({
id: "support-agent",
name: "Customer Support Agent",
description: "Handles tier-1 support tickets and billing inquiries",
metadata: {
capabilities: ["support", "billing"],
},
});

// Bad
await cortex.agents.register({
id: "agent1",
name: "Agent",
});

3. Track Capabilities

// Enable discovery
await cortex.agents.register({
id: "multilingual-support",
metadata: {
capabilities: [
"support",
"language:en",
"language:es",
"language:fr",
"timezone:americas",
],
},
});

// Find agents
const spanishSupport = await cortex.agents.search({
capabilities: ["support", "language:es"],
});

4. Configure Retention Per Agent

// Critical agent: Unlimited retention
await cortex.agents.configure("audit-agent", {
memoryVersionRetention: -1,
});

// Temporary agent: Minimal retention
await cortex.agents.configure("demo-agent", {
memoryVersionRetention: 1,
});

Next Steps


Questions? Ask in GitHub Discussions.