Context Chain Design
Architecture of hierarchical context chains for multi-memory-space workflow coordination with cross-space delegation support.
Overview
Context chains enable hierarchical task delegation with cross-memory-space access control. Memory spaces can share workflow state without breaking isolation boundaries. Each context in a chain has visibility into its parent, children, and root context - even across memory space boundaries.
Purpose: "Process customer refund" Memory Space: supervisor-space Data: amount: 500, userId: "user-123"
Purpose: "Approve refund" Memory Space: finance-space (Different space!) Can access: Root data (via grantedAccess) Participant: finance-bot
Purpose: "Send apology email" Memory Space: customer-relations-space Can access: Root data (via grantedAccess) Participant: relations-bot
Purpose: "Update CRM" Memory Space: crm-space Can access: Root data (via grantedAccess) Participant: crm-bot
Key Insight: Context chains enable cross-space delegation while maintaining memory space isolation - they're a controlled access mechanism, not a backdoor.
Data Structure
Context Document
{
_id: "ctx_abc123",
// Identity & hierarchy
contextId: "ctx_abc123",
parentId: "ctx_parent", // Can be in different memory space
rootId: "ctx_root", // Self if root
depth: 2, // 0=root
// Ownership & Isolation
memorySpaceId: "finance-space", // Memory space this context lives in
tenantId: "tenant-acme", // Multi-tenancy
userId: "user-123", // User association (GDPR-enabled)
// Purpose
purpose: "Approve $500 refund",
description: "Review and approve customer refund request",
// Children tracking
childIds: ["ctx_child1", "ctx_child2"],
participants: ["finance-space", "legal-space"], // Memory spaces involved
// Cross-space access control (NEW)
grantedAccess: [
{
memorySpaceId: "legal-space", // Legal space can access
scope: "read-only", // Read-only permission
grantedAt: 1729900000000,
},
{
memorySpaceId: "audit-space", // Audit space can access
scope: "context-only", // Only context data, not memories
grantedAt: 1729900100000,
},
],
// Optional: Link to originating conversation
conversationRef: {
conversationId: "conv-456",
messageIds: ["msg-089"],
},
// Workflow data (flexible)
data: {
amount: 500,
reason: "defective product",
ticketId: "TICKET-456",
approvalRequired: true,
// Any custom fields...
},
// Metadata
metadata: {
importance: 85,
tags: ["refund", "approval"],
},
// Status tracking
status: "active", // active | completed | cancelled | blocked
// Timestamps
createdAt: 1729900000000,
updatedAt: 1729900500000,
completedAt: null,
// Versioning
version: 2,
previousVersions: [
{
version: 1,
status: "active",
data: { ... },
timestamp: 1729900000000,
updatedBy: "finance-bot",
},
],
}
Hierarchy Management
Creating Root Context
// Create root context
const rootContext = await cortex.contexts.create({
purpose: "Process customer refund",
memorySpaceId: "supervisor-space",
userId: "user-123",
data: {
amount: 500,
ticketId: "TICKET-456",
},
});
// Create child context (automatically inherits rootId and depth)
const childContext = await cortex.contexts.create({
purpose: "Approve refund",
memorySpaceId: "finance-space",
parentId: rootContext._id, // Links to parent
data: {
approvalRequired: true,
},
});
Traversing the Chain
// Get complete chain from any context
export const getChain = query({
args: { contextId: v.id("contexts") },
handler: async (ctx, args) => {
const current = await ctx.db.get(args.contextId);
if (!current) {
throw new Error("CONTEXT_NOT_FOUND");
}
// Get root
const root = await ctx.db.get(current.rootId);
// Get parent
const parent = current.parentId ? await ctx.db.get(current.parentId) : null;
// Get children
const children = await Promise.all(
current.childIds.map((id) => ctx.db.get(id)),
);
// Get siblings
const siblings = parent
? await Promise.all(
parent.childIds
.filter((id) => id !== current._id)
.map((id) => ctx.db.get(id)),
)
: [];
// Get all ancestors (walk up)
const ancestors = [];
let node = parent;
while (node) {
ancestors.unshift(node);
node = node.parentId ? await ctx.db.get(node.parentId) : null;
}
// Get all descendants (recursive)
const descendants = await getAllDescendants(ctx, current._id);
return {
current,
root,
parent,
children,
siblings,
ancestors,
descendants,
depth: current.depth,
totalNodes: 1 + ancestors.length + descendants.length,
};
},
});
async function getAllDescendants(
ctx: any,
contextId: string,
): Promise<Context[]> {
const context = await ctx.db.get(contextId);
const children = await Promise.all(
context.childIds.map((id) => ctx.db.get(id)),
);
// Recursive
const grandchildren = await Promise.all(
children.map((child) => getAllDescendants(ctx, child._id)),
);
return [...children, ...grandchildren.flat()];
}
Status Management
Status Transitions
update method accepts any status transition. This validation is planned for a future release.// Planned: Valid transitions (not currently enforced)
const STATUS_TRANSITIONS = {
active: ["completed", "cancelled", "blocked"],
blocked: ["active", "cancelled"],
completed: [], // Terminal state
cancelled: [], // Terminal state
};
// Current implementation: Use SDK update method
await cortex.contexts.update(contextId, {
status: "completed",
data: { result: "success" },
});
Auto-Complete Parent
// Planned: When all children complete, complete parent
// This function does not exist in the current implementation
export const checkAndCompleteParent = mutation({
args: { contextId: v.id("contexts") },
handler: async (ctx, args) => {
// Implementation planned for future release
},
});
Context Propagation
Data Inheritance
getChain() to access parent data. This feature is planned for a future release.// Planned: Children can access parent data automatically
// This function does not exist in the current implementation
export const getWithInheritedData = query({
args: { contextId: v.id("contexts") },
handler: async (ctx, args) => {
// Implementation planned for future release
},
});
// Current implementation: Manually traverse chain
const chain = await cortex.contexts.getChain(childContextId);
// Manually merge data from chain
const inheritedData = {};
for (const context of [chain.root, ...chain.ancestors, chain.current]) {
Object.assign(inheritedData, context.data || {});
}
console.log(inheritedData);
// {
// amount: 500, // From root
// ticketId: "TICKET-456", // From root
// approvedBy: "finance-agent", // From parent
// confirmationNumber: "REF-789", // From current
// }
Participant Propagation
addParticipant() to add participants to individual contexts. Chain-wide propagation is planned for a future release.// Current implementation: Add participant to single context
await cortex.contexts.addParticipant(contextId, "legal-space");
// Planned: Add participant to context and all ancestors
// This function does not exist in the current implementation
export const addParticipantToChain = mutation({
args: {
contextId: v.id("contexts"),
memorySpaceId: v.string(),
},
handler: async (ctx, args) => {
// Implementation planned for future release
},
});
Conversation Linking
Contexts Reference Conversations
createFromConversation function. Use the standard create() method with conversationRef parameter.// Current implementation: Create context with conversation reference
const context = await cortex.contexts.create({
purpose: "Process customer refund",
memorySpaceId: "supervisor-space",
userId: "user-123",
conversationRef: {
conversationId: "conv-456",
messageIds: ["msg-089"],
},
});
Retrieve Original Conversation
get() with includeConversation: true option to retrieve context with conversation data.// Current implementation: Get context with conversation
const result = await cortex.contexts.get(contextId, {
includeChain: true,
includeConversation: true,
});
// Result includes conversation and triggerMessages if conversationRef exists
if (result.conversation) {
console.log(result.conversation);
console.log(result.triggerMessages);
}
Memory Integration
Linking Memories to Contexts
storeWithContext function. Use the standard memory storage API and include contextId in metadata.// Current implementation: Store memory with context reference
const context = await cortex.contexts.get(contextId);
const memory = await cortex.memories.store({
memorySpaceId: "finance-space",
content: "Refund approved for $500",
metadata: {
contextId: context._id, // ← Link to context
workflowPurpose: context.purpose,
},
});
Finding Memories by Context
getMemoriesForWorkflow function. Query memories by filtering on metadata.contextId manually.// Current implementation: Find memories for a workflow
// Get all contexts in workflow
const chain = await cortex.contexts.getChain(rootContextId);
const allContextIds = [
chain.root._id,
...chain.descendants.map((c) => c._id),
];
// Query memories from all memory spaces involved
const allMemories = [];
for (const context of [chain.root, ...chain.descendants]) {
const memories = await cortex.memories.list({
memorySpaceId: context.memorySpaceId,
});
// Filter memories linked to this workflow
const workflowMemories = memories.filter(
(m) => m.metadata?.contextId && allContextIds.includes(m.metadata.contextId)
);
allMemories.push(...workflowMemories);
}
// Sort by creation time
allMemories.sort((a, b) => a.createdAt - b.createdAt);
Workflow Patterns
Sequential Workflow
// Step 1 -> Step 2 -> Step 3
async function createSequentialWorkflow(purpose: string, steps: any[]) {
// Create root
const root = await ctx.db.insert("contexts", {
purpose,
rootId: null,
depth: 0,
childIds: [],
status: "active",
...
});
await ctx.db.patch(root, { rootId: root });
// Create steps as siblings (all same parent)
const stepIds = [];
for (const step of steps) {
const stepContext = await cortex.contexts.create({
purpose: step.purpose,
memorySpaceId: step.memorySpaceId,
parentId: root._id,
data: { stepNumber: stepIds.length + 1 },
});
stepIds.push(stepContext._id);
}
// Update root with all children
await ctx.db.patch(root, { childIds: stepIds });
return root;
}
Approval Chain
// Request -> Manager -> Finance (nested)
async function createApprovalChain(request: any) {
// Level 1: Request
const requestCtx = await cortex.contexts.create({
purpose: "Expense approval request",
memorySpaceId: "employee-space",
data: { amount: request.amount },
});
// Level 2: Manager review
const managerCtx = await cortex.contexts.create({
purpose: "Manager review",
memorySpaceId: "manager-space",
parentId: requestCtx._id, // ← Nested
data: { approved: null }, // To be filled
});
// Level 3: Finance approval
const financeCtx = await cortex.contexts.create({
purpose: "Finance approval",
memorySpaceId: "finance-space",
parentId: managerCtx._id, // ← Nested deeper
data: { allocated: null },
});
return {
depth: 3,
chain: [requestCtx, managerCtx, financeCtx],
};
}
Parallel Workflow
// Fork: One parent, multiple parallel children
async function createParallelWorkflow(purpose: string, tasks: any[]) {
const root = await cortex.contexts.create({
purpose,
memorySpaceId: "supervisor-space",
});
// Create all children in parallel
const children = await Promise.all(
tasks.map((task) =>
cortex.contexts.create({
purpose: task.purpose,
memorySpaceId: task.memorySpaceId,
parentId: root._id,
data: task.data,
}),
),
);
return { root, children };
}
Query Patterns
Finding Contexts by Status
// Current implementation: Get all active workflows for a memory space
const activeWorkflows = await cortex.contexts.list({
memorySpaceId: "finance-space",
status: "active",
});
// Get blocked workflows (needs attention)
const blockedWorkflows = await cortex.contexts.list({
status: "blocked",
});
Finding by Depth
// Get all root contexts
export const getRoots = query({
handler: async (ctx) => {
return await ctx.db
.query("contexts")
.withIndex("by_depth", (q) => q.eq("depth", 0))
.collect();
},
});
// Get all leaf contexts (no children)
export const getLeaves = query({
handler: async (ctx) => {
const all = await ctx.db.query("contexts").collect();
return all.filter((c) => c.childIds.length === 0);
},
});
Finding by Conversation
// Current implementation: Get all workflows from a conversation
const contexts = await cortex.contexts.getByConversation("conv-456");
Orphan Detection
Finding Orphaned Contexts
export const findOrphaned = query({
handler: async (ctx) => {
const allContexts = await ctx.db.query("contexts").collect();
const orphaned = [];
for (const context of allContexts) {
if (context.parentId) {
// Check if parent exists
const parent = await ctx.db.get(context.parentId);
if (!parent) {
orphaned.push(context);
}
}
}
return orphaned;
},
});
Cleanup Orphans
export const cleanupOrphans = mutation({
handler: async (ctx) => {
const orphaned = await ctx.runQuery("contexts:findOrphaned");
for (const context of orphaned) {
// Option 1: Delete
await ctx.db.delete(context._id);
// Option 2: Promote to root
// await ctx.db.patch(context._id, {
// parentId: null,
// rootId: context._id,
// depth: 0,
// });
}
return { cleaned: orphaned.length };
},
});
GDPR Cascade
Contexts Support userId
// Create context for user
const context = await cortex.contexts.create({
purpose: "Handle user request",
memorySpaceId: "support-space",
userId: "user-123", // ← GDPR-enabled
});
// GDPR cascade (planned for Cloud Mode)
await cortex.users.delete("user-123", { cascade: true });
// Contexts with userId are deleted
// Children without explicit userId are preserved (workflow metadata)
Selective Deletion
export const deleteUserContexts = mutation({
args: { userId: v.string() },
handler: async (ctx, args) => {
// Find all contexts for user
const userContexts = await ctx.db
.query("contexts")
.withIndex("by_userId", (q) => q.eq("userId", args.userId))
.collect();
for (const context of userContexts) {
// Delete context and descendants
await deleteWithDescendants(ctx, context._id);
}
return { deleted: userContexts.length };
},
});
async function deleteWithDescendants(ctx: any, contextId: string) {
const context = await ctx.db.get(contextId);
// Delete all children first (recursive)
for (const childId of context.childIds) {
await deleteWithDescendants(ctx, childId);
}
// Delete this context
await ctx.db.delete(contextId);
}
Performance Considerations
Index Strategy
Required indexes:
by_memorySpace- Find memory space's contextsby_userId- GDPR cascadeby_status- Filter by statusby_parentId- Get childrenby_rootId- Get entire workflow
Compound indexes:
by_memorySpace_status- Memory space's active workflows (common query)
Depth Limits
// Planned: Enforce maximum depth
const MAX_DEPTH = 10;
// Current implementation: No depth limit enforced
// You can manually check depth before creating child contexts
const parent = await cortex.contexts.get(parentId);
if (parent.depth >= MAX_DEPTH) {
throw new Error("Maximum context depth exceeded");
}
Lazy Loading Children
// Current implementation: Use getChain() for full chain, or getChildren() for children
// Get single context (lightweight)
const context = await cortex.contexts.get(contextId);
// Get full chain (includes children, ancestors, descendants)
const chain = await cortex.contexts.getChain(contextId);
// Get just children (with optional recursion)
const children = await cortex.contexts.getChildren(contextId, {
recursive: false, // Set to true for all descendants
});
Next Steps
- Agent Registry - Optional registry architecture
- Performance - Optimization techniques
- Security & Privacy - Data protection
- Context Operations API - API usage
Questions? Ask in GitHub Discussions.