Skip to main content

Orchestration Observer API

Info

Last Updated: 2026-01-26 | Version: v0.35.2+

Real-time monitoring of the memory orchestration pipeline for building responsive UIs.

New in v0.35.2

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:

  1. Recall Phase: Retrieves context from memory layers before the LLM generates a response
  2. 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.

Two-Phase Memory Orchestration
Recall Phase

memorySpace → user → agent → context

LLM Generation

AI generates response using retrieved context

Remember Phase

conversation → vector → facts → graph

Recall retrieves context, Remember stores memories

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)
New in v0.35.1

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"]
}
})
Tip

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>
);
}
Tip

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​

New in v0.35.2

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:

Dual-Emission Architecture
Memory Operation

Recall or Remember phase starts

Reasoning Parts

reasoning-start → reasoning-delta → reasoning-end

Transient Events

data-recall-start → data-layer-update → data-recall-complete

Both reasoning parts and transient events are emitted for maximum flexibility

Visual Treatment​

Memory operations appear as collapsible reasoning panels with distinct visual treatments:

PhaseIconLabelTiming
Memory RecallDatabaseIcon"Recalling Memory"Before LLM response generation
Memory StorageSaveIcon"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​

No Breaking Changes

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 TypeDestinationPurpose
reasoning-*Message partsNative UI rendering
data-* (transient)useLayerTracking / custom handlersProgrammatic 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​

  1. Keep callbacks lightweight - Heavy processing should be deferred
  2. Use async callbacks sparingly - They're awaited, which adds latency
  3. Handle errors gracefully - Check event.status === "error"
  4. Don't mutate events - Treat them as read-only
  5. 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.