Agent Registry
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
| Feature | Memory Spaces | Agent Registry |
|---|---|---|
| Purpose | Define isolation boundaries | Track agent metadata |
| Status | Active, required concept | Active, optional feature |
| Use Case | Scope for data (conversations, memories, facts) | Analytics, discovery, team organization |
| Registration | Optional | Optional |
| Enables | Hive Mode, Collaboration Mode | Agent discovery, statistics, cascade deletion |
| Supports | Multi-participant spaces | Single agent tracking |
| Query by | memorySpaceId | agentId, capabilities, team |
| Cascade delete | All data in space | All 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:
- Memory Spaces (
cortex.memorySpaces.*): Define isolation boundaries and manage participants - Agent Registry (
cortex.agents.*): Track agent metadata, analytics, and capabilities
No registration needed. Just works with any memorySpaceId string.
Isolation boundaries. Enables Hive Mode and cross-space collaboration.
Optional metadata for agent tracking. Use for analytics, discovery, team organization.
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 lookupby_tenantId- Tenant's memory spacesby_tenant_memorySpaceId- Tenant-scoped lookupby_status- Filter active/archivedby_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_LIMITof 1000 records per query type - Approximate Counts: When datasets exceed 1000 records, counts are approximate
isApproximateFlag: 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()orcortex.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:
- SDK calls
list()backend query to fetch all agents - Client-side filtering checks
metadata.capabilitiesarray capabilitiesMatch: "all"requires all specified capabilitiescapabilitiesMatch: "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)
},
});
Hybrid Approach (Recommended)
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
- Performance - Optimization techniques
- Security & Privacy - Data protection
- Agent Management API - API usage
Questions? Ask in GitHub Discussions.