Skip to main content

Agent Registry

Last Updated: 2025-10-28

Optional agent registry architecture for enhanced features and analytics.

Overview

The agent registry is completely optional - Cortex works with simple string IDs. Registration enables enhanced features like:

  • Analytics and usage tracking
  • Agent discovery and capabilities
  • Per-agent configuration
  • Usage-based billing (Cloud Mode)
  • Agent-specific retention policies
┌────────────────────────────────────────────────┐
│ Simple Mode (String IDs) │
├────────────────────────────────────────────────┤
│ await cortex.memory.store('my-agent', { ... }) │
│ No registration needed ✅ │
│ Just works with any string │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ Registry Mode (Enhanced) │
├────────────────────────────────────────────────┤
│ await cortex.agents.register({ │
│ id: 'my-agent', │
│ name: 'Support Agent', │
│ capabilities: ['support', 'billing'], │
│ }); │
│ │
│ Enables: Analytics, discovery, config ✅ │
└────────────────────────────────────────────────┘

Key Principle: Start simple (string IDs), add registry when you need enhanced features.


Registry Schema

Agent Document

{
_id: Id<"agents">,
memorySpaceId: string, // Unique identifier (can be anything)

// Metadata
name: string,
description?: string,
capabilities?: string[], // What this agent can do

// Custom metadata
metadata: {
owner?: string,
team?: string,
version?: string,
model?: string, // LLM model used
[key: string]: any,
},

// Configuration
config: {
memoryVersionRetention?: number, // Override default
embeddingDimensions?: number,
maxMemories?: number,
[key: string]: any,
},

// Usage statistics
stats: {
totalMemories: number,
totalConversations: number,
totalContexts: number,
lastActive?: number,
memoryStorageBytes?: number,
},

// Timestamps
registeredAt: number,
updatedAt: 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! ✅
// 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",
capabilities: ["support", "billing", "technical"],
metadata: {
owner: "support-team",
model: "gpt-4",
version: "2.0",
},
config: {
memoryVersionRetention: 20, // Override default (10)
maxMemories: 100000,
},
});

// Now get analytics
const agent = await cortex.agents.get("support-agent");
console.log(`${agent.name}: ${agent.stats.totalMemories} memories`);

// 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: {
id: v.string(),
name: v.string(),
description: v.optional(v.string()),
capabilities: v.optional(v.array(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.id))
.unique();

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

// Create registration
const agentId = await ctx.db.insert("agents", {
memorySpaceId: args.id,
name: args.name,
description: args.description,
capabilities: args.capabilities || [],
metadata: args.metadata || {},
config: args.config || {},
stats: {
totalMemories: 0,
totalConversations: 0,
totalContexts: 0,
},
registeredAt: Date.now(),
updatedAt: Date.now(),
});

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

Update Agent

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

if (!agent) {
throw new Error("AGENT_NOT_FOUND");
}

await ctx.db.patch(agent._id, {
...args.updates,
updatedAt: Date.now(),
});

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

Unregister Agent

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

if (!agent) {
throw new Error("AGENT_NOT_FOUND");
}

if (args.deleteData) {
// Delete all agent data
const memories = await ctx.db
.query("memories")
.withIndex("by_agent", (q) => q.eq("agentId", args.agentId))
.collect();

for (const memory of memories) {
await ctx.db.delete(memory._id);
}

// Delete conversations, contexts, etc.
}

// Delete registration
await ctx.db.delete(agent._id);

return { deleted: true, dataDeleted: args.deleteData };
},
});

Statistics Tracking

Automatic Stats Updates

// Trigger on memory creation
export const storeMemory = mutation({
handler: async (ctx, args) => {
// Insert memory
const memoryId = await ctx.db.insert("memories", args);

// Update agent stats (if registered)
const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
.unique();

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

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

Computed Statistics

export const computeStats = mutation({
args: { memorySpaceId: v.string() },
handler: async (ctx, args) => {
// Count memories
const memories = await ctx.db
.query("memories")
.withIndex("by_agent", (q) => q.eq("agentId", args.agentId))
.collect();

// Count conversations
const conversations = await ctx.db
.query("conversations")
.withIndex("by_agentId", (q) =>
q.eq("participants.agentId", args.agentId),
)
.collect();

// Count contexts
const contexts = await ctx.db
.query("contexts")
.withIndex("by_agent", (q) => q.eq("agentId", args.agentId))
.collect();

// Calculate storage
const storageBytes = memories.reduce((sum, m) => {
return (
sum +
(m.content?.length || 0) +
(m.embedding?.length || 0) * 8 +
JSON.stringify(m.metadata).length
);
}, 0);

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

if (agent) {
await ctx.db.patch(agent._id, {
stats: {
totalMemories: memories.length,
totalConversations: conversations.length,
totalContexts: contexts.length,
memoryStorageBytes: storageBytes,
lastActive: Date.now(),
},
});
}

return {
memories: memories.length,
conversations: conversations.length,
contexts: contexts.length,
storage: storageBytes,
};
},
});

Agent Discovery

Search by Capabilities

export const searchByCapabilities = query({
args: { capabilities: v.array(v.string()) },
handler: async (ctx, args) => {
const allAgents = await ctx.db.query("agents").collect();

// Find agents with ALL specified capabilities
return allAgents.filter((agent) =>
args.capabilities.every((cap) => agent.capabilities.includes(cap)),
);
},
});

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

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

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: Registered
await cortex.agents.register({
id: 'support-agent-prod',
name: 'Production Support Agent',
capabilities: ['support'],
});

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

// Both work! ✅
// Registered agents get analytics
// Unregistered agents work without setup

Best Practice:

  • Development: Use simple strings
  • Production: Register for analytics
  • Temporary: Use simple strings
  • Critical: Register for config overrides

Registry-Enabled Features

1. Analytics Dashboard (Cloud Mode)

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

console.log({
name: agent.name,
totalMemories: agent.stats.totalMemories,
totalConversations: agent.stats.totalConversations,
storageUsed: formatBytes(agent.stats.memoryStorageBytes),
lastActive: new Date(agent.stats.lastActive),
});

// Aggregate analytics
const allAgents = await cortex.agents.list();
const totalMemories = allAgents.reduce(
(sum, a) => sum + a.stats.totalMemories,
0,
);
const totalStorage = allAgents.reduce(
(sum, a) => sum + (a.stats.memoryStorageBytes || 0),
0,
);

2. Agent Discovery

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

// 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.agentId))
.unique();

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.agentId, // String ID works either way
...
});
},
});

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! ✅
// Now have analytics on top

Cloud Mode Features

Agent Billing

// Track usage for billing (Cloud Mode)
{
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

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

// ⚠️ 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",
capabilities: ["support", "billing"],
});

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

3. Track Capabilities

// Enable discovery
await cortex.agents.register({
id: "multilingual-support",
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 or Discord.