UNPKG

@cortexmemory/sdk

Version:

AI agent memory SDK built on Convex - ACID storage, vector search, and conversation management

304 lines (265 loc) 7.93 kB
/** * Cortex Convex Functions - Agents Registry API * * Backend functions for optional agent metadata registration. * Agents work without registration - this is just for discovery and analytics. */ import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Query Operations // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * Get agent registration by ID */ export const get = query({ 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(); return agent; }, }); /** * List agents with optional filters */ export const list = query({ args: { status: v.optional( v.union( v.literal("active"), v.literal("inactive"), v.literal("archived"), ), ), limit: v.optional(v.number()), offset: v.optional(v.number()), }, handler: async (ctx, args) => { let query; if (args.status) { query = ctx.db .query("agents") .withIndex("by_status", (q) => q.eq("status", args.status!)) .order("desc"); } else { query = ctx.db.query("agents").withIndex("by_registered").order("desc"); } if (args.offset) { // Skip first N results const allResults = await query.collect(); const sliced = allResults.slice( args.offset, args.offset + (args.limit || 100), ); return sliced; } if (args.limit) { return await query.take(args.limit); } return await query.take(100); // Default limit }, }); /** * Count agents */ export const count = query({ args: { status: v.optional( v.union( v.literal("active"), v.literal("inactive"), v.literal("archived"), ), ), }, handler: async (ctx, args) => { let agents; if (args.status) { agents = await ctx.db .query("agents") .withIndex("by_status", (q) => q.eq("status", args.status!)) .collect(); } else { agents = await ctx.db.query("agents").collect(); } return agents.length; }, }); /** * Check if agent exists */ export const exists = query({ 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(); return agent !== null; }, }); /** * Compute agent statistics */ export const computeStats = query({ args: { agentId: v.string(), }, handler: async (ctx, args) => { // Count memories where participantId = agentId const memories = await ctx.db .query("memories") .filter((q) => q.eq(q.field("participantId"), args.agentId)) .collect(); // Count conversations where agent is participant const conversations = await ctx.db .query("conversations") .filter((q) => q.or( q.eq(q.field("participants.participantId"), args.agentId), // Check if agentId is in memorySpaceIds array (for agent-agent convos) q.eq(q.field("memorySpaceId"), args.agentId), ), ) .collect(); // Count facts where participantId = agentId const facts = await ctx.db .query("facts") .filter((q) => q.eq(q.field("participantId"), args.agentId)) .collect(); // Find unique memory spaces const memorySpaces = new Set(memories.map((m) => m.memorySpaceId)); // Find last active time const allTimestamps = [ ...memories.map((m) => m.updatedAt), ...conversations.map((c) => c.updatedAt), ...facts.map((f) => f.updatedAt), ]; const lastActive = allTimestamps.length > 0 ? Math.max(...allTimestamps) : undefined; return { totalMemories: memories.length, totalConversations: conversations.length, totalFacts: facts.length, memorySpacesActive: memorySpaces.size, lastActive, }; }, }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Mutation Operations // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * Register an 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 agent already registered const existing = await ctx.db .query("agents") .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId)) .first(); if (existing) { throw new Error("AGENT_ALREADY_REGISTERED"); } const now = Date.now(); 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: now, updatedAt: now, }); const agent = await ctx.db.get(agentId); return agent; }, }); /** * Update agent registration */ 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 Error("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); const updated = await ctx.db.get(agent._id); return updated; }, }); /** * Unregister agent (just removes registration, cascade handled in SDK) */ 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 Error("AGENT_NOT_REGISTERED"); } await ctx.db.delete(agent._id); return { deleted: true, agentId: args.agentId }; }, }); /** * Note: Cascade deletion by participantId is orchestrated in the SDK layer. * * The SDK will: * 1. Query all memory spaces * 2. For each space, find records where participantId = agentId * 3. Delete conversations, memories, facts, graph nodes * 4. Delete agent registration (last) * 5. Verify completeness and rollback on failure * * This approach provides better control and error handling than a single * complex backend mutation. */