Elena calls your support line. She has called three times this week. On Monday, she reported a damaged item. On Wednesday, she followed up on the return status. Today, she's checking whether the replacement shipped.
With a traditional CX agent, each call starts the same way. The agent pulls her ticket history, reads the case notes, pieces together what happened, confirms which order she's calling about, and then picks up the thread. The reconstruction takes time. It's also imperfect: the agent on Monday's call made an observation about Elena's preferred replacement option that never made it into the ticket, so today's agent doesn't know about it.
With a per-customer agent instance, there's no reconstruction. The agent that handled Monday's call is the same instance that's handling today's. It knows Elena's case without being briefed. It picks up in the middle of a thought it was already holding.
This is the shift. Not from no memory to memory retrieval. From retrieval on every session to an agent that's been thinking about Elena since Monday.
What "per-customer agent" actually means
A per-customer agent is a persistent agent instance tied to one customer, carrying state between conversations rather than reconstructing it from a database each time. The instance lives on between sessions, accumulates understanding over time, and serves as the customer's consistent point of contact across all channels.
This is a different architecture from the dominant pattern today, where a pool of shared agents handles all customers. Each shared agent gets customer context injected at session start from a retrieval system: recent tickets, order history, CRM fields, maybe some vector-searched conversation summaries. The shared agent uses that context for the duration of the session, then the session closes and the context is discarded.
Per-customer instances flip this. Instead of injecting context at session start, the instance builds context continuously over every interaction. The session closes but the instance doesn't. When Elena calls back, the same instance resumes.
| Dimension | Shared agents with retrieval | Per-customer instances |
|---|---|---|
| Context at session start | Retrieved and assembled from storage | Already present in working memory |
| Context assembly time | Every session | One-time at first contact |
| Continuity of reasoning | Summarized or lost at session end | Preserved across sessions |
| State storage | Database records + vector index | Agent instance memory |
| Cold start for new customers | Same as any session | Bootstraps from CRM + first session |
| Cost model | Per-session retrieval costs | Per-instance memory storage costs |
Neither architecture is universally better. Shared agents with retrieval are cheaper at low interaction frequency and simpler to operate. Per-customer instances are more accurate at high interaction frequency and build compound value over time. The question isn't which is right in principle; it's which fits your customer interaction patterns.
Why shared agents lose context between conversations
The core problem with shared agents isn't that retrieval doesn't work. It's that the things that matter most between conversations often don't get stored.
A ticket system captures what happened: "customer reported damaged item, return label issued." It rarely captures what the agent learned: "this customer gets anxious when status is unclear and needs proactive updates" or "she said she'd prefer a replacement in a different color even though the system only shows the original SKU." Those observations live in the agent's context during the session and disappear when the session ends.
Vector search over conversation summaries recovers some of this. But summaries compress out nuance. And retrieval has recall limits: if the system stores 100 conversation summaries, the search might return the 5 most similar, not the 5 most relevant to today's specific question.
// Shared agent: context assembled from scratch every session
async function startSharedAgentSession(customerId: string): Promise<AgentContext> {
// Three separate retrieval calls, each with latency
const [tickets, orders, summaries] = await Promise.all([
ticketSystem.getRecent(customerId, { limit: 10 }),
orderSystem.getHistory(customerId, { limit: 20 }),
vectorStore.search(customerId, { topK: 5, query: "recent issues" }),
]);
// Assemble a context string that fits the model's context window
const contextStr = formatContext({ tickets, orders, summaries });
// This assembly runs at EVERY session start
// And it discards anything that didn't make it into tickets, orders, or summaries
return { systemPrompt: BASE_PROMPT + contextStr, history: [] };
}That assembly runs at every session start. It's also lossy: anything that didn't get captured in a ticket, an order record, or a summarized vector chunk is gone.
Per-customer instances avoid this by making the assembly a one-time process that grows over time, not a repeated reconstruction.
Building the per-customer architecture
A per-customer instance architecture has three main components: an instance manager that creates and routes to instances, the instances themselves with their own memory, and a shared knowledge layer that all instances read from.
The instance manager handles routing: when a request comes in for customer cust_4421, the manager finds the existing instance (or spawns a new one for first-time customers) and forwards the request. The instance handles the conversation turn and updates its own memory with anything worth retaining.
class CustomerAgentManager {
private instances = new Map<string, CustomerAgentInstance>();
async getOrSpawn(customerId: string): Promise<CustomerAgentInstance> {
if (this.instances.has(customerId)) {
// Existing instance -- context is already loaded
return this.instances.get(customerId)!;
}
// Spawn new instance -- bootstrap from available first-party data
const bootstrapContext = await this.loadBootstrapContext(customerId);
const instance = new CustomerAgentInstance({
customerId,
initialMemory: bootstrapContext,
tools: this.buildToolsForCustomer(customerId),
});
this.instances.set(customerId, instance);
return instance;
}
private async loadBootstrapContext(customerId: string): Promise<BootstrapMemory> {
// For new customers, pull structured first-party data
// This runs once, not on every session
const [profile, orders, tickets] = await Promise.all([
crm.getProfile(customerId),
orders.getHistory(customerId, { limit: 50 }),
tickets.getAll(customerId),
]);
return { profile, orders, tickets, episodes: [], insights: [] };
}
}The instance has three memory layers. Episodic memory holds raw conversation history from recent sessions. Semantic memory holds derived facts: preferences, patterns, and observations extracted from episodes. The shared knowledge layer (product catalog, policies, FAQs) is read-only and shared across all instances, not duplicated per customer.
class CustomerAgentInstance {
private episodicMemory: EpisodicMemory;
private semanticMemory: SemanticMemory;
async handleTurn(userMessage: string): Promise<string> {
// Context is already loaded -- no retrieval on this call
const response = await this.model.generate({
system: this.buildSystemPrompt(),
messages: [
...this.episodicMemory.getRecentTurns(20), // recent conversation history
{ role: "user", content: userMessage },
],
});
// After generating, update memory with anything worth retaining
await this.updateMemory(userMessage, response);
return response;
}
private async updateMemory(
userMessage: string,
agentResponse: string
): Promise<void> {
// Save to episodic memory
this.episodicMemory.append({ role: "user", content: userMessage });
this.episodicMemory.append({ role: "assistant", content: agentResponse });
// Extract semantic facts if anything notable happened
const insights = await this.extractInsights(userMessage, agentResponse);
if (insights.length > 0) {
await this.semanticMemory.upsert(insights);
}
}
}The semantic memory layer is what makes per-customer instances compounding. Every time the agent notices something worth knowing ("this customer prefers email confirmations over SMS," "she always needs to know the tracking number before she's satisfied"), it gets written to semantic memory and persists across session boundaries.
Chanl's memory layer provides the storage infrastructure for this pattern: episode storage, semantic extraction, and retrieval APIs that fit the per-instance model without building custom vector infrastructure.

Customer Memory
4 memories recalled
“Discussed upgrading to Business plan. Budget approved at $50k. Follow up next Tuesday.”
Multi-tenancy: isolation without shared state
The most important property of a per-customer architecture is that instances can't see each other's data. Elena's instance should not be able to query another customer's order history, even if a tool call was structured incorrectly.
Application-level filters (WHERE customer_id = ?) can be misconfigured. The right approach is cryptographic scoping at the memory layer: each instance's memory is namespaced to that customer's ID, and the namespace is part of the storage key. A query from instance A can't return data from instance B's namespace, not because of a WHERE clause that might be wrong, but because the addresses don't overlap.
Tool permissions work the same way. When the instance manager spawns a new instance, it creates a tool permission set scoped to that customer's data:
function buildToolsForCustomer(customerId: string): ScopedToolSet {
return {
// These tools are pre-scoped: they can only query this customer's data
get_order_history: () => orders.getByCustomer(customerId),
get_ticket_history: () => tickets.getByCustomer(customerId),
update_ticket: (ticketId: string, update: object) => {
// Verify the ticket belongs to this customer before allowing update
return tickets.updateWithOwnerVerification(ticketId, customerId, update);
},
// Shared read-only tools: no customer scoping needed
lookup_product: (sku: string) => catalog.getProduct(sku),
get_return_policy: () => policies.getReturnPolicy(),
};
}This pattern makes data isolation a property of the instance construction, not an ongoing application responsibility. Misconfigured queries can't leak cross-customer data because the tool layer doesn't expose unscoped queries.
Testing agents that already know someone
Testing a per-customer agent correctly means testing it with customers who have history, not blank-slate sessions. A new customer profile tests bootstrapping; it doesn't test what makes per-customer instances valuable.
For meaningful test coverage, you need synthetic customer profiles with realistic history. Not just "customer has 3 orders" but a profile that models what an agent instance would actually hold after 10 real interactions: specific resolved issues, unresolved follow-ups, noted preferences, channel history.
const syntheticCustomer: SyntheticProfile = {
customerId: "test_elena_4421",
bootstrapData: {
profile: { name: "Elena R.", tier: "gold", preferred_channel: "voice" },
orders: [
{ id: "ord_9981", status: "delivered", date: "2026-06-10", items: ["SKU-44821"] },
{ id: "ord_9983", status: "return_pending", date: "2026-06-20", items: ["SKU-44821"] },
],
tickets: [
{ id: "tkt_7742", subject: "Damaged item on ord_9983", status: "open" },
],
},
semanticMemory: [
{ fact: "Prefers proactive status updates", source: "inferred from session 2026-06-22" },
{ fact: "Wants replacement, not refund, for ord_9983", source: "stated 2026-06-22" },
{ fact: "Anxious about shipping status; needs tracking number to close conversation", source: "observed 2026-06-23" },
],
episodicMemory: [
// Summarized recent sessions
{ date: "2026-06-22", summary: "Reported damaged item, requested replacement, label issued" },
{ date: "2026-06-23", summary: "Followed up on return status, confirmed label received" },
],
};With this profile loaded into a test instance, you can test what per-customer instances are supposed to do: pick up the thread. "Did the replacement ship?" should get a response that references the existing case without requiring Elena to re-explain her situation.
Cross-channel continuity is another test scenario unique to per-customer instances. The same instance should handle a transition from voice to chat mid-case, with full context. Chanl's scenario testing lets you build multi-turn, multi-channel test sequences against a pre-loaded customer profile, rather than writing integration tests that manually assemble instance state.
The personalization dividend
After 20 or 30 interactions, a per-customer agent instance has accumulated something no CRM records: a working model of how this specific person communicates and what makes interactions go well for them.
Not a profile field that someone manually updated. An inferred model built from watching what resolutions stuck, which explanations landed, which approaches triggered frustration, and which shortcuts the customer consistently prefers.
A customer who always asks for tracking numbers before they're satisfied -- the agent learns that and offers the tracking number proactively. A customer who prefers short answers and gets annoyed at lengthy explanations -- the agent adapts its response length. A customer who always escalates when hold times exceed 3 minutes -- the agent knows to set that expectation early.
None of this requires explicit configuration. It emerges from the semantic memory layer accumulating observations across episodes.
async function buildPersonalizedSystemPrompt(
customerId: string,
semanticMemory: SemanticMemory
): Promise<string> {
const insights = await semanticMemory.get(customerId, {
categories: ["communication_preferences", "resolution_preferences", "sensitivity"],
});
// Insights are injected into the system prompt automatically
// They were learned, not configured
const insightText = insights
.map((i) => `- ${i.fact}`)
.join("\n");
return `${BASE_SYSTEM_PROMPT}
What you know about this customer from previous interactions:
${insightText}
Use this context to adapt your communication style and resolution approach.
Do not mention that you're using stored context unless the customer asks.`;
}The compound effect is what makes this architecture compelling at scale. A shared agent with perfect retrieval gives you the same context every session. A per-customer instance gives you that context plus everything the agent has learned since the last time this customer called.
We covered how agent memory architecture works at the component level in building your own AI agent memory system. That piece is a useful complement to this one if you're implementing the memory layer from scratch.
Getting from here to there
Most teams don't start with per-customer instances. They start with shared agents and better retrieval, which is the right choice at low interaction volume per customer.
The trigger for moving toward per-customer instances is usually a failure mode that retrieval can't fix: customers repeatedly re-explaining their situation, agents making inconsistent recommendations across sessions because the nuance didn't make it into the retrieval index, or high-value customers complaining that "I have to start over every time I call."
The transition doesn't have to be all-or-nothing. A common middle path: start with per-customer instances for your highest-value customer segment, measure whether resolution rates and CSAT improve, then expand to broader segments if the data supports it. The infrastructure investment is real. You're managing instance lifecycle, memory pruning, serialization for hibernation, and multi-tenancy isolation. That's not trivial.
What's also true: as customer data accumulates in the per-customer layer, it becomes the best dataset you have for understanding what good resolution looks like. The agent that has handled 200 conversations with 200 different customers, accumulating semantic insights about each one, generates evaluation data that a shared agent with retrieval never sees. That observation loop is covered in agent observability: measuring the gap between what you see and what matters. Chanl's analytics layer surfaces per-customer conversation metrics so you can see which instances are driving the most resolved conversations and where the personalization dividend is actually showing up.
The teams running per-customer agent instances today are mostly in high-frequency, high-value customer relationships: enterprise account management, healthcare patient support, financial services. Those are the places where the personalization dividend pays off fastest because each customer interacts frequently enough for the learned context to accumulate.
For a typical e-commerce contact center, per-customer instances might be overkill for most customers and exactly right for the top 20% who account for 80% of interactions. That segmentation is a reasonable starting point.
See what your agents remember about each customer
Chanl's memory layer stores episodic and semantic context per customer, with the retrieval and lifecycle management built in. Connect it to your agents and start building the context that persists between conversations.
Explore Chanl memoryCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
Learn Agentic AI
Weekly. Patterns for shipping agents that work. MCP, scorecards, regression tests, prompts, model comparisons.
