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)
},
});
Hybrid Approach (Recommended)
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
- Performance - Optimization techniques
- Security & Privacy - Data protection
- Agent Management API - API usage
Questions? Ask in GitHub Discussions or Discord.