Orchestration Observer API
Last Updated: 2026-01-26 | Version: v0.35.2+
Real-time monitoring of the memory orchestration pipeline for building responsive UIs.
Native Reasoning Panel Integration - Memory orchestration now renders inline as collapsible reasoning panels using the AI SDK 6 protocol. Your users see memory recall and storage as native "thinking" indicators, just like extended thinking from reasoning models. See Native Reasoning Panel Integration for details.
Overview
Memory orchestration in Cortex operates in two distinct phases:
- Recall Phase: Retrieves context from memory layers before the LLM generates a response
- Remember Phase: Stores new memories after the LLM generates a response
The OrchestrationObserver API provides real-time callbacks for each phase, enabling you to build progress indicators, debugging tools, and responsive user experiences.
memorySpace → user → agent → context
AI generates response using retrieved context
conversation → vector → facts → graph
Quick Start
import { Cortex, OrchestrationObserver } from "@cortexmemory/sdk";
const cortex = new Cortex({ convexUrl: "..." });
// Create a phase-aware observer
const observer: OrchestrationObserver = {
// Recall phase (context retrieval)
onRecallStart: (id) => {
console.log(`Recall started: ${id}`);
},
onRecallComplete: (summary) => {
console.log(`Retrieved ${summary.context.memoriesCount} memories, ${summary.context.factsCount} facts`);
},
// Remember phase (memory storage)
onRememberStart: (id) => {
console.log(`Remember started: ${id}`);
},
onRememberComplete: (summary) => {
console.log(`Stored ${summary.createdIds.memoryIds?.length} memories`);
},
// Layer updates (both phases)
onLayerUpdate: (event) => {
console.log(`[${event.phase}] ${event.layer}: ${event.status} (${event.latencyMs}ms)`);
},
};
// Pass observer to remember()
const result = await cortex.memory.remember({
memorySpaceId: "space-1",
conversationId: "conv-1",
userMessage: "I prefer dark mode",
agentResponse: "I'll remember that preference!",
userId: "user-1",
agentId: "agent-1",
observer, // <-- Real-time events
});
Types
MemoryLayer
The eight layers in the orchestration pipeline, organized by phase:
type MemoryLayer =
// Recall Phase (context retrieval)
| "memorySpace" // Validates memory space exists
| "user" // Loads/validates user profile
| "agent" // Loads/validates agent
| "context" // NEW: Assembles context from memories, facts, graph
// Remember Phase (memory storage)
| "conversation" // Stores conversation messages
| "vector" // Creates vector embeddings
| "facts" // Extracts and stores facts (with belief revision)
| "graph"; // Syncs to graph database (if configured)
The context layer is new - it represents the context assembly step during recall, where memories, facts, and graph entities are merged and formatted for LLM injection.
Phase Layer Groupings:
// Layers involved in recall (context retrieval)
const RECALL_LAYERS = ["memorySpace", "user", "agent", "context"];
// Layers involved in remember (memory storage)
const REMEMBER_LAYERS = ["conversation", "vector", "facts", "graph"];
LayerStatus
Status of each layer during orchestration:
type LayerStatus =
| "pending" // Layer queued but not started
| "in_progress" // Layer actively processing
| "complete" // Layer finished successfully
| "error" // Layer failed
| "skipped"; // Layer not applicable (e.g., no graph configured)
LayerEvent
Emitted for each layer status change:
interface LayerEvent {
/** Which layer this event is for */
layer: MemoryLayer;
/** Current status of the layer */
status: LayerStatus;
/** Unix timestamp when this status was set */
timestamp: number;
/** Milliseconds elapsed since phase started */
latencyMs?: number;
/** NEW: Phase this event belongs to */
phase: "recall" | "remember";
/** Data about created/retrieved entities (on complete) */
data?: {
id?: string;
preview?: string;
metadata?: Record<string, unknown>;
};
/** Error details (on error status) */
error?: {
message: string;
code?: string;
};
/** For facts layer: what action was taken */
revisionAction?: "ADD" | "UPDATE" | "SUPERSEDE" | "NONE";
/** For facts layer: IDs of facts that were superseded */
supersededFacts?: string[];
}
RecallSummary
NEW in v0.35.1 - Summary of the recall (context retrieval) phase:
interface RecallSummary {
/** Unique ID for this orchestration run */
orchestrationId: string;
/** Phase identifier */
phase: "recall";
/** Total time for recall phase in milliseconds */
totalLatencyMs: number;
/** Status of each layer queried during recall */
layers: Partial<Record<MemoryLayer, LayerEvent>>;
/** Retrieved context ready for LLM injection */
context: {
/** Formatted context string for LLM prompt */
formatted?: string;
/** Number of memories retrieved */
memoriesCount: number;
/** Number of facts retrieved */
factsCount: number;
/** Number of graph entities retrieved */
graphEntitiesCount: number;
};
}
OrchestrationSummary
Summary of the remember (memory storage) phase:
interface OrchestrationSummary {
/** Unique ID for this orchestration run */
orchestrationId: string;
/** Phase identifier */
phase: "remember";
/** Total time for remember phase in milliseconds */
totalLatencyMs: number;
/** Final status of each layer */
layers: Record<MemoryLayer, LayerEvent>;
/** IDs of all created records */
createdIds: {
conversationId?: string;
memoryIds?: string[];
factIds?: string[];
};
}
OrchestrationObserver
The observer interface with phase-specific callbacks - all callbacks are optional:
interface OrchestrationObserver {
/** Called when recall phase starts (context retrieval) */
onRecallStart?: (orchestrationId: string) => void | Promise<void>;
/** Called when recall phase completes with retrieved context */
onRecallComplete?: (summary: RecallSummary) => void | Promise<void>;
/** Called when remember phase starts (memory storage) */
onRememberStart?: (orchestrationId: string) => void | Promise<void>;
/** Called when remember phase completes with created IDs */
onRememberComplete?: (summary: OrchestrationSummary) => void | Promise<void>;
/** Called when any layer's status changes (includes phase info) */
onLayerUpdate?: (event: LayerEvent) => void | Promise<void>;
}
Event Flow
A typical chat turn with recall() and remember() emits events in this order:
Recall Phase (Context Retrieval)
onRecallStart("orch-abc123")
onLayerUpdate({ layer: "memorySpace", status: "in_progress", phase: "recall" })
onLayerUpdate({ layer: "memorySpace", status: "complete", phase: "recall", latencyMs: 5 })
onLayerUpdate({ layer: "user", status: "in_progress", phase: "recall" })
onLayerUpdate({ layer: "user", status: "complete", phase: "recall", latencyMs: 12 })
onLayerUpdate({ layer: "agent", status: "in_progress", phase: "recall" })
onLayerUpdate({ layer: "agent", status: "complete", phase: "recall", latencyMs: 18 })
onLayerUpdate({ layer: "context", status: "in_progress", phase: "recall" })
onLayerUpdate({
layer: "context",
status: "complete",
phase: "recall",
latencyMs: 85,
data: {
preview: "Retrieved 5 memories, 3 facts",
metadata: { vectorMatches: 5, factMatches: 3, graphEntities: 2 }
}
})
onRecallComplete({
orchestrationId: "orch-abc123",
phase: "recall",
totalLatencyMs: 120,
layers: { /* recall layer events */ },
context: {
memoriesCount: 5,
factsCount: 3,
graphEntitiesCount: 2,
formatted: "## Relevant Context\n..."
}
})
LLM Generation
At this point, the LLM generates a response using the retrieved context.
Remember Phase (Memory Storage)
onRememberStart("orch-abc123")
onLayerUpdate({ layer: "conversation", status: "in_progress", phase: "remember" })
onLayerUpdate({ layer: "conversation", status: "complete", phase: "remember", latencyMs: 45 })
onLayerUpdate({ layer: "vector", status: "in_progress", phase: "remember" })
onLayerUpdate({ layer: "vector", status: "complete", phase: "remember", latencyMs: 120 })
onLayerUpdate({ layer: "facts", status: "in_progress", phase: "remember" })
onLayerUpdate({
layer: "facts",
status: "complete",
phase: "remember",
latencyMs: 200,
revisionAction: "SUPERSEDE",
supersededFacts: ["fact-old-123"]
})
onLayerUpdate({ layer: "graph", status: "skipped", phase: "remember" }) // No graph configured
onRememberComplete({
orchestrationId: "orch-abc123",
phase: "remember",
totalLatencyMs: 366,
layers: { /* all final layer events */ },
createdIds: {
conversationId: "conv-1",
memoryIds: ["mem-1", "mem-2"],
factIds: ["fact-new-456"]
}
})
The same orchestrationId is shared across both phases for a single chat turn, allowing you to correlate recall and remember operations.
Usage with rememberStream()
The observer works identically with streaming. For streaming scenarios, recall happens before streaming starts, and remember happens after the stream completes:
const observer: OrchestrationObserver = {
onRecallStart: (id) => showRecallSpinner(),
onRecallComplete: (summary) => {
hideRecallSpinner();
console.log(`Context ready: ${summary.context.memoriesCount} memories`);
},
onRememberStart: (id) => showSaveIndicator(),
onRememberComplete: (summary) => {
hideSaveIndicator();
console.log(`Saved ${summary.createdIds.memoryIds?.length} memories`);
},
onLayerUpdate: (event) => {
updateLayerStatus(event.phase, event.layer, event.status);
},
};
const result = await cortex.memory.rememberStream(
{
memorySpaceId: "space-1",
conversationId: "conv-1",
userMessage: "Tell me about TypeScript",
userId: "user-1",
agentId: "agent-1",
observer, // Phase-aware observer
},
{
llmStream: myLLMStream,
onChunk: (chunk) => console.log("Token:", chunk),
},
);
Building a Progress UI
React Example
import { useState, useCallback } from "react";
import type {
LayerEvent,
OrchestrationObserver,
RecallSummary,
OrchestrationSummary,
} from "@cortexmemory/sdk";
function useOrchestrationProgress() {
const [recallLayers, setRecallLayers] = useState<Record<string, LayerEvent>>({});
const [rememberLayers, setRememberLayers] = useState<Record<string, LayerEvent>>({});
const [isRecalling, setIsRecalling] = useState(false);
const [isRemembering, setIsRemembering] = useState(false);
const [recallSummary, setRecallSummary] = useState<RecallSummary | null>(null);
const observer: OrchestrationObserver = {
onRecallStart: useCallback(() => {
setIsRecalling(true);
setRecallLayers({});
}, []),
onRecallComplete: useCallback((summary: RecallSummary) => {
setIsRecalling(false);
setRecallSummary(summary);
}, []),
onRememberStart: useCallback(() => {
setIsRemembering(true);
setRememberLayers({});
}, []),
onRememberComplete: useCallback(() => {
setIsRemembering(false);
}, []),
onLayerUpdate: useCallback((event: LayerEvent) => {
if (event.phase === "recall") {
setRecallLayers((prev) => ({ ...prev, [event.layer]: event }));
} else {
setRememberLayers((prev) => ({ ...prev, [event.layer]: event }));
}
}, []),
};
return {
recallLayers,
rememberLayers,
isRecalling,
isRemembering,
recallSummary,
observer,
};
}
// Usage in component
function MemoryProgress() {
const {
recallLayers,
rememberLayers,
isRecalling,
isRemembering,
recallSummary,
observer,
} = useOrchestrationProgress();
const handleSend = async () => {
await cortex.memory.remember({
// ... params
observer,
});
};
return (
<div>
{/* Recall Phase */}
{isRecalling && <div className="spinner">Retrieving context...</div>}
{Object.entries(recallLayers).map(([layer, event]) => (
<div key={`recall-${layer}`} className="layer-status">
[Recall] {layer}: {event.status}
{event.latencyMs && ` (${event.latencyMs}ms)`}
</div>
))}
{recallSummary && (
<div className="context-summary">
Found {recallSummary.context.memoriesCount} memories,
{recallSummary.context.factsCount} facts
</div>
)}
{/* Remember Phase */}
{isRemembering && <div className="spinner">Saving memories...</div>}
{Object.entries(rememberLayers).map(([layer, event]) => (
<div key={`remember-${layer}`} className="layer-status">
[Remember] {layer}: {event.status}
{event.latencyMs && ` (${event.latencyMs}ms)`}
</div>
))}
{!isRecalling && !isRemembering && <div>Complete!</div>}
</div>
);
}
Vercel AI SDK Integration
For Vercel AI SDK apps, bridge events to the client via data streams:
// API Route
import { createDataStreamResponse, streamText } from "ai";
import type { OrchestrationObserver } from "@cortexmemory/sdk";
export async function POST(req: Request) {
const { messages } = await req.json();
return createDataStreamResponse({
execute: async (dataStream) => {
const observer: OrchestrationObserver = {
// Recall phase events
onRecallStart: (orchestrationId) => {
dataStream.writeData({ type: "data-recall-start", orchestrationId });
},
onRecallComplete: (summary) => {
dataStream.writeData({
type: "data-recall-complete",
totalLatencyMs: summary.totalLatencyMs,
context: summary.context,
});
},
// Remember phase events
onRememberStart: (orchestrationId) => {
dataStream.writeData({ type: "data-remember-start", orchestrationId });
},
onRememberComplete: (summary) => {
dataStream.writeData({
type: "data-remember-complete",
totalLatencyMs: summary.totalLatencyMs,
createdIds: summary.createdIds,
});
},
// Layer updates (both phases)
onLayerUpdate: (event) => {
dataStream.writeData({
type: "data-layer-update",
phase: event.phase,
layer: event.layer,
status: event.status,
latencyMs: event.latencyMs,
});
},
};
// Your LLM streaming logic here
const result = streamText({ model, messages });
result.mergeIntoDataStream(dataStream);
},
});
}
// Client Component - using useLayerTracking hook
import { useChat } from "@ai-sdk/react";
import { useLayerTracking } from "@cortexmemory/vercel-ai-provider/react";
function Chat() {
const {
recallLayers,
rememberLayers,
isRecalling,
isRemembering,
handleDataPart,
} = useLayerTracking();
const { messages } = useChat({
onData: handleDataPart, // Automatically parses phase-aware events
});
return (
<div>
{/* Phase-aware progress visualization */}
{isRecalling && <RecallProgress layers={recallLayers} />}
{isRemembering && <RememberProgress layers={rememberLayers} />}
{messages.map((m) => (
<Message key={m.id} message={m} />
))}
</div>
);
}
The useLayerTracking hook from @cortexmemory/vercel-ai-provider/react provides built-in state management for phase-aware layer tracking. See useLayerTracking Hook for full documentation.
Native Reasoning Panel Integration
This feature uses the AI SDK 6 protocol to render memory orchestration as native reasoning panels.
Starting with v0.35.2, memory orchestration can render inline as collapsible reasoning panels in your chat UI. This provides a native "thinking" experience for users—they see memory operations as extended thinking indicators, exactly like reasoning models.
How It Works
The LayerObserver emits AI SDK 6 reasoning protocol events alongside the traditional transient data events:
Recall or Remember phase starts
reasoning-start → reasoning-delta → reasoning-end
data-recall-start → data-layer-update → data-recall-complete
Visual Treatment
Memory operations appear as collapsible reasoning panels with distinct visual treatments:
| Phase | Icon | Label | Timing |
|---|---|---|---|
| Memory Recall | DatabaseIcon | "Recalling Memory" | Before LLM response generation |
| Memory Storage | SaveIcon | "Storing Memory" | After LLM response completes |
The reasoning panel shows layer-by-layer progress:
▼ Recalling Memory
Memory Space: complete
User Profile: complete
Agent Context: complete
Context Assembly: complete (5 matches)
Implementation Example
Here's how to emit reasoning parts from your API route:
// API Route with reasoning panel integration
import { createUIMessageStream, streamText } from "ai";
import { createCortexMemoryAsync } from "@cortexmemory/vercel-ai-provider";
import type { LayerObserver } from "@cortexmemory/vercel-ai-provider";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = createUIMessageStream({
execute: async ({ writer: dataStream }) => {
// Track orchestration IDs for reasoning part emission
let currentRecallOrchestrationId: string | undefined;
let currentRememberOrchestrationId: string | undefined;
// Helper: Map layer names to display names
const getLayerDisplayName = (layer: string): string => {
const displayNames: Record<string, string> = {
memorySpace: "Memory Space",
user: "User Profile",
agent: "Agent Context",
context: "Context Assembly",
conversation: "Conversation",
vector: "Vector Search",
facts: "Facts Engine",
graph: "Knowledge Graph",
};
return displayNames[layer] || layer;
};
// Helper: Format layer status with metadata
const formatLayerStatus = (event: {
status: string;
data?: { metadata?: Record<string, unknown> };
}): string => {
const { status, data } = event;
if (status === "complete" && data?.metadata) {
const meta = data.metadata;
if (typeof meta.vectorMatches === "number") {
return `complete (${meta.vectorMatches} matches)`;
}
if (typeof meta.factMatches === "number") {
return `complete (${meta.factMatches} facts)`;
}
}
return status;
};
const layerObserver: LayerObserver = {
onRecallStart: (orchestrationId) => {
currentRecallOrchestrationId = orchestrationId;
// Emit reasoning-start for recall phase (AI SDK 6 Protocol)
dataStream.write({
type: "reasoning-start",
id: `memory-recall-${orchestrationId}`,
});
// Keep transient event for backward compatibility
dataStream.write({
type: "data-recall-start",
data: { orchestrationId },
transient: true,
});
},
onRecallComplete: (summary) => {
const orchestrationId =
summary?.orchestrationId || currentRecallOrchestrationId;
// Emit reasoning-end for recall phase
dataStream.write({
type: "reasoning-end",
id: `memory-recall-${orchestrationId}`,
});
// Keep transient event for backward compatibility
dataStream.write({
type: "data-recall-complete",
data: summary,
transient: true,
});
},
onRememberStart: (orchestrationId) => {
currentRememberOrchestrationId = orchestrationId;
// Emit reasoning-start for storage phase
dataStream.write({
type: "reasoning-start",
id: `memory-storage-${orchestrationId}`,
});
// Keep transient event for backward compatibility
dataStream.write({
type: "data-remember-start",
data: { orchestrationId },
transient: true,
});
},
onRememberComplete: (summary) => {
const orchestrationId =
summary?.orchestrationId || currentRememberOrchestrationId;
// Emit reasoning-end for storage phase
dataStream.write({
type: "reasoning-end",
id: `memory-storage-${orchestrationId}`,
});
// Keep transient event for backward compatibility
dataStream.write({
type: "data-remember-complete",
data: summary,
transient: true,
});
},
onLayerUpdate: (event) => {
// Determine reasoning ID based on phase
const phase = event.phase;
const orchestrationId =
phase === "recall"
? currentRecallOrchestrationId
: currentRememberOrchestrationId;
const reasoningId =
phase === "recall"
? `memory-recall-${orchestrationId}`
: `memory-storage-${orchestrationId}`;
// Emit reasoning-delta with layer status
const layerDisplayName = getLayerDisplayName(event.layer);
const statusText = formatLayerStatus(event);
dataStream.write({
type: "reasoning-delta",
id: reasoningId,
delta: `${layerDisplayName}: ${statusText}\n`,
});
// Keep transient event for backward compatibility
dataStream.write({
type: "data-layer-update",
data: event,
transient: true,
});
},
};
// Create Cortex memory with observer
const cortexMemory = await createCortexMemoryAsync({
convexUrl: process.env.CORTEX_CONVEX_URL!,
memorySpaceId: "space-1",
userId: "user-1",
conversationId: "conv-1",
observer: layerObserver,
});
// Stream with memory-wrapped model
const result = streamText({
model: cortexMemory(yourLanguageModel),
messages,
});
// Merge with sendReasoning to include reasoning parts
dataStream.merge(result.toUIMessageStream({ sendReasoning: true }));
},
});
return new Response(stream);
}
Client-Side Rendering
The reasoning parts are automatically rendered by AI SDK 6 compatible chat UIs. If you're using @ai-sdk/react, reasoning parts appear as collapsible panels:
import { useChat } from "@ai-sdk/react";
function Chat() {
const { messages } = useChat();
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{/* Reasoning parts (including memory operations) render automatically */}
{message.parts.map((part, index) => {
if (part.type === "reasoning") {
return (
<details key={index} className="reasoning-panel">
<summary>
{part.id?.startsWith("memory-recall")
? "🗄️ Recalling Memory"
: part.id?.startsWith("memory-storage")
? "💾 Storing Memory"
: "💭 Thinking"}
</summary>
<pre>{part.reasoning}</pre>
</details>
);
}
if (part.type === "text") {
return <p key={index}>{part.text}</p>;
}
return null;
})}
</div>
))}
</div>
);
}
Backward Compatibility
Existing implementations using useLayerTracking continue to work unchanged. The transient events (data-recall-start, data-layer-update, etc.) are still emitted alongside reasoning parts.
The dual-emission architecture ensures backward compatibility:
| Event Type | Destination | Purpose |
|---|---|---|
reasoning-* | Message parts | Native UI rendering |
data-* (transient) | useLayerTracking / custom handlers | Programmatic access |
Migration Guide
If you've built a custom UI with useLayerTracking, you have two options:
Option 1: Keep using useLayerTracking (No changes required)
Your existing code continues to work. Transient events are still emitted:
// This still works exactly as before
const { recallLayers, rememberLayers, isRecalling, isRemembering } =
useLayerTracking();
Option 2: Switch to native reasoning parts (Recommended for new projects)
For a cleaner implementation, render reasoning parts directly from message parts:
// New approach: render from message.parts
{message.parts
.filter((p) => p.type === "reasoning")
.map((part) => (
<ReasoningPanel key={part.id} part={part} />
))}
Benefits of native reasoning parts:
- Persistence: Reasoning parts are stored with the message, surviving page refreshes
- Consistency: Same rendering as model reasoning (extended thinking)
- Simplicity: No separate state management needed
Belief Revision Events
When the facts layer processes with belief revision enabled, you get additional information:
const observer: OrchestrationObserver = {
onLayerUpdate: (event) => {
if (event.layer === "facts" && event.status === "complete") {
switch (event.revisionAction) {
case "ADD":
console.log("New fact created");
break;
case "UPDATE":
console.log("Existing fact updated");
break;
case "SUPERSEDE":
console.log(`Superseded: ${event.supersededFacts?.join(", ")}`);
break;
case "NONE":
console.log("Duplicate fact skipped");
break;
}
}
},
};
Error Handling
Errors in layer processing are captured in the event:
const observer: OrchestrationObserver = {
onLayerUpdate: (event) => {
if (event.status === "error") {
console.error(`${event.layer} failed: ${event.error?.message}`);
// Show error UI, retry logic, etc.
}
},
};
Observer callback errors are caught internally and logged - they won't crash your application:
// Safe - errors are caught
const observer: OrchestrationObserver = {
onLayerUpdate: (event) => {
throw new Error("Bug in my callback");
// This won't crash - it's caught and logged
},
};
Best Practices
- Keep callbacks lightweight - Heavy processing should be deferred
- Use async callbacks sparingly - They're awaited, which adds latency
- Handle errors gracefully - Check
event.status === "error" - Don't mutate events - Treat them as read-only
- Use for observability, not control flow - The pipeline runs regardless
Integration Agnostic
The OrchestrationObserver API is designed to work with any integration:
- Vercel AI SDK - Via data streams (shown above)
- LangChain - Via custom callbacks
- Direct SDK usage - Via the observer parameter
- Server-side logging - Write to your observability stack
- CLI tools - Print progress to terminal
The SDK emits events; how you display or process them is up to you.