Engineering

Engineering

Customer-Aware Agent With Gemini 3.5 Flash and Mem0

Customer-Aware Agent With Gemini 3.5 Flash and Mem0

Quick Takeaways

  • A customer-aware support agent uses customer and account history to choose the next support action, not just personalize a reply.

  • The production problem is not "can the agent remember a user?" It is "Can remembered context change the next support action?"

  • Mem0 gives the agent durable customer and account memory. Gemini 3.5 Flash uses that memory to classify the issue, choose tools, generate handoff summaries, and write the support outcome back to memory.

  • The demo in this article uses Mem0 memory add/search calls and the Gemini tool calling. Ticketing and CSM notifications are local operation logs, so the demo does not pretend to send external CRM or email actions.

💡Before you start: Get your free Mem0 API key.

Most AI support agents fail in the most expensive way possible, politely. They answer the ticket, but they make the customer repeat what your company already knows. If you are building an AI agent for customer support, memory should change the workflow, not just the greeting.

A customer-aware support agent is an AI agent that uses durable customer and account memory to decide the next workflow: answer, ask for missing information, escalate, notify a human, or update the support record.

In this walkthrough, we will build a production-mimicking support agent with:

  • Mem0 for durable customer and account memory.

  • Gemini 3.5 Flash for reasoning, structured output, and tool calling.

  • A support policy document for escalation rules.

  • Local operation logs for ticketing and CSM notifications.

  • A memory write-back step so future support turns improve.

By the end, your agent will do this:

The point of the architecture is simple:

Mem0 stores what the agent needs to remember. Gemini decides what to do with it.

What Makes a Support Agent Customer-Aware?

A customer-aware support agent does three things differently from a generic chatbot:

  1. It retrieves durable customer and account memory before deciding what to do.

  2. It applies policy to that memory, such as SLA rules or escalation thresholds.

  3. It turns the decision into an action through tools, then writes the outcome back to memory.

The important shift is from response generation to support orchestration.

Why Customer Memory Is Not Enough

A basic memory demo usually proves that “The agent remembers something about the user.”

For production support, that is too narrow. Support decisions usually depend on multiple scopes of context:

Memory scope

What it stores

Why it matters

Customer memory

Tone preference, previous issues, prior promises, sentiment

Helps the agent avoid asking the customer to repeat themselves

Account memory

Plan, SLA, renewal date, account health, CSM owner

Changes routing, priority, and escalation behavior

Ticket memory

Current active issue, missing fields, and status

Keeps the current workflow coherent

Team/policy context

Refund rules, SLA rules, escalation criteria

Keeps the agent aligned with support operations

In this demo, we intentionally separate customer memory from account memory. That matters because a user is not always the account. A developer on an Enterprise workspace, an admin on a Pro account, and a finance contact handling invoices may all need different contexts from the same company-level memory.

This is where naive support agents usually break.

1. Store Only User-Level Memory

If you store everything under a single user identity, you lose the distinction between the individual customer, the account they belong to, the current ticket, and the support team’s operating policy. That makes retrieval noisy and increases the risk of mixing unrelated contexts. In production, memory should be scoped clearly across customer, account, ticket, and team-level context.

2. Confuse Policy With Memory

Customer memory changes after every support interaction. Policy changes when the business updates its support rules. Do not bury SLA rules, refund policy, or escalation logic inside a user’s memory stream. Put policies in a knowledge base, config file, CMS, or document store designed for official support rules. Retrieve them separately and pass them to the model as policy context.

3. Personalize Tone But Not Workflow

Many memory demos stop at tone personalization, such as remembering that a customer prefers concise replies. That is useful, but it is not enough. In support, the bigger question is whether the agent should ask for more information, answer directly, escalate, create a ticket, notify a human, or update memory.

4. Actions Happened Without Calling Tools

If the agent tells a customer that something was escalated, your application should have a corresponding system action behind it: a ticket created, a handoff generated, a CSM notified, or a memory updated. Gemini tool calling gives you the right control boundary: the model chooses the function, your server executes it, then the model gets the result.

Demo: Building a Customer-Aware Agent with Mem0 and Gemini

💡 Before you start: you need a Mem0 API key and the complete code is available on the GitHub repository.

This demo is designed as a before/after experiment. First, you run the workflow without seeding Mem0. Gemini only sees the current support message and the support policy, so it gives a generic support response.

Before Seeding:

Before seeding output

Then, you click Seed Mem0. The app writes customer and account memories into Mem0. When you run the exact same message again, Gemini receives the retrieved Mem0 context and chooses a more customer-aware workflow.

After Seeding:

After seeding output

We’ll use three customer profiles and send the same support message for each one:

The goal is to show that the message stays the same, but the workflow changes once Mem0 provides customer and account memory.

Customer profile

What Mem0 knows

Expected workflow

First-time Free user

Free plan, no previous billing issues, no contractual SLA

Ask for the invoice ID

Pro user with a repeat issue

Two billing issues on 2026-04-12 and 2026-05-20, plus a promised manual credit

Escalate to billing with prior resolution context

Enterprise renewal account

Enterprise plan, four-hour SLA, renewal on 2026-06-26, assigned CSM Maya Singh, recurring billing complaints

Create an urgent escalation, generate handoff brief, and notify the CSM

This is the core demo: the current message does not mention plan, SLA, renewal date, previous billing issues, or CSM ownership. Those facts come from Mem0.

The same message produces three different workflows because Mem0 retrieved different context for each customer. Try it with your own customer/account facts: start free with Mem0.

System Architecture

The demo has two execution paths. Before seeding, Gemini runs as a baseline support agent with no persistent memory. After seeding, the same message goes through Mem0 retrieval first, so Gemini can choose a customer-aware support workflow.

The support flow looks like this:

Baseline mode, before Mem0 is seeded:

Incoming support message
  
Send message + support policy to Gemini
  
Gemini chooses a generic support action
  
Server executes tools
  
Gemini generates the final customer response
Incoming support message
  
Send message + support policy to Gemini
  
Gemini chooses a generic support action
  
Server executes tools
  
Gemini generates the final customer response
Incoming support message
  
Send message + support policy to Gemini
  
Gemini chooses a generic support action
  
Server executes tools
  
Gemini generates the final customer response

Memory-aware mode, after Mem0 is seeded:

Incoming support message
  
Search Mem0 customer memory
  
Search Mem0 account memory
  
Send message + Mem0 context + support policy to Gemini
  
Gemini chooses tool calls
  
Server executes tools
  
Send tool results back to Gemini
  
Gemini generates customer response + handoff summary
  
Store support outcome back into Mem0
Incoming support message
  
Search Mem0 customer memory
  
Search Mem0 account memory
  
Send message + Mem0 context + support policy to Gemini
  
Gemini chooses tool calls
  
Server executes tools
  
Send tool results back to Gemini
  
Gemini generates customer response + handoff summary
  
Store support outcome back into Mem0
Incoming support message
  
Search Mem0 customer memory
  
Search Mem0 account memory
  
Send message + Mem0 context + support policy to Gemini
  
Gemini chooses tool calls
  
Server executes tools
  
Send tool results back to Gemini
  
Gemini generates customer response + handoff summary
  
Store support outcome back into Mem0

Step 1: Seed Durable Memories in Mem0

The Mem0 Platform API supports adding memories through POST /v3/memories/add/. The request includes conversation-style messages, a scoped entity such as user_id, and optional metadata. Mem0 processes the request asynchronously and returns an event_id for tracking.

In the demo, each server start creates a fresh run namespace:

const DEMO_NAMESPACE = process.env.DEMO_NAMESPACE || "customer-aware-support-demo";
const DEMO_RUN_ID = process.env.DEMO_RUN_ID || createDemoRunId();
const EFFECTIVE_DEMO_NAMESPACE = `${DEMO_NAMESPACE}:run:${DEMO_RUN_ID}`;
const DEMO_NAMESPACE = process.env.DEMO_NAMESPACE || "customer-aware-support-demo";
const DEMO_RUN_ID = process.env.DEMO_RUN_ID || createDemoRunId();
const EFFECTIVE_DEMO_NAMESPACE = `${DEMO_NAMESPACE}:run:${DEMO_RUN_ID}`;
const DEMO_NAMESPACE = process.env.DEMO_NAMESPACE || "customer-aware-support-demo";
const DEMO_RUN_ID = process.env.DEMO_RUN_ID || createDemoRunId();
const EFFECTIVE_DEMO_NAMESPACE = `${DEMO_NAMESPACE}:run:${DEMO_RUN_ID}`;

Customer memories and account memories are stored under different IDs:

function scopedCustomerId(customerId) {
  return `${EFFECTIVE_DEMO_NAMESPACE}:customer:${customerId}`;
}

function scopedAccountId(accountId) {
  return `${EFFECTIVE_DEMO_NAMESPACE}:account:${accountId}`;
}
function scopedCustomerId(customerId) {
  return `${EFFECTIVE_DEMO_NAMESPACE}:customer:${customerId}`;
}

function scopedAccountId(accountId) {
  return `${EFFECTIVE_DEMO_NAMESPACE}:account:${accountId}`;
}
function scopedCustomerId(customerId) {
  return `${EFFECTIVE_DEMO_NAMESPACE}:customer:${customerId}`;
}

function scopedAccountId(accountId) {
  return `${EFFECTIVE_DEMO_NAMESPACE}:account:${accountId}`;
}

The seed call stores exact support facts:

await mem0Add({
  user_id: scopedCustomerId(customer.id),
  messages: [
    {
      role: "user",
      content: "Customer Ben Carter reported a billing issue on 2026-04-12."
    },
    {
      role: "user",
      content: "Customer Ben Carter was promised a manual credit in the billing case from 2026-05-20."
    }
  ],
  metadata: {
    app: DEMO_NAMESPACE,
    run_id: DEMO_RUN_ID,
    scope: "customer",
    scenario: "pro-ben"
  },
  infer: false
});
await mem0Add({
  user_id: scopedCustomerId(customer.id),
  messages: [
    {
      role: "user",
      content: "Customer Ben Carter reported a billing issue on 2026-04-12."
    },
    {
      role: "user",
      content: "Customer Ben Carter was promised a manual credit in the billing case from 2026-05-20."
    }
  ],
  metadata: {
    app: DEMO_NAMESPACE,
    run_id: DEMO_RUN_ID,
    scope: "customer",
    scenario: "pro-ben"
  },
  infer: false
});
await mem0Add({
  user_id: scopedCustomerId(customer.id),
  messages: [
    {
      role: "user",
      content: "Customer Ben Carter reported a billing issue on 2026-04-12."
    },
    {
      role: "user",
      content: "Customer Ben Carter was promised a manual credit in the billing case from 2026-05-20."
    }
  ],
  metadata: {
    app: DEMO_NAMESPACE,
    run_id: DEMO_RUN_ID,
    scope: "customer",
    scenario: "pro-ben"
  },
  infer: false
});

For seeded demo memories, infer: false is intentional. We want the exact facts we wrote to be retrievable, not a paraphrased extraction. For live support transcripts, you can turn inference on so Mem0 extracts durable memories from natural conversations.

Step 2: Retrieve Customer and Account Context

When a user sends a support message, the app only searches Mem0 after the seed step has completed.

const [customerMemory, accountMemory] = mem0SeededForRun
  ? await Promise.all([
      mem0Search(message, customerUserId),
      mem0Search(message, accountUserId)
    ])
  : [emptyMem0Envelope(), emptyMem0Envelope()];
const [customerMemory, accountMemory] = mem0SeededForRun
  ? await Promise.all([
      mem0Search(message, customerUserId),
      mem0Search(message, accountUserId)
    ])
  : [emptyMem0Envelope(), emptyMem0Envelope()];
const [customerMemory, accountMemory] = mem0SeededForRun
  ? await Promise.all([
      mem0Search(message, customerUserId),
      mem0Search(message, accountUserId)
    ])
  : [emptyMem0Envelope(), emptyMem0Envelope()];

Before seeding, customerMemory.results and accountMemory.results are empty. That gives you a true Gemini-only baseline.

After seeding, the app searches Mem0 twice:

mem0Search(message, scopedCustomerId(customer.id));
mem0Search(message, scopedAccountId(customer.account_id));
mem0Search(message, scopedCustomerId(customer.id));
mem0Search(message, scopedAccountId(customer.account_id));
mem0Search(message, scopedCustomerId(customer.id));
mem0Search(message, scopedAccountId(customer.account_id));

The search request uses Mem0's V3 memory search endpoint:

async function mem0Search(query, user_id) {
  const data = await mem0Request("/v3/memories/search/", {
    method: "POST",
    body: {
      query,
      filters: { user_id },
      top_k: 8,
      threshold: 0.0,
      rerank: true
    }
  });

  return normalizeMem0Envelope(data);
}
async function mem0Search(query, user_id) {
  const data = await mem0Request("/v3/memories/search/", {
    method: "POST",
    body: {
      query,
      filters: { user_id },
      top_k: 8,
      threshold: 0.0,
      rerank: true
    }
  });

  return normalizeMem0Envelope(data);
}
async function mem0Search(query, user_id) {
  const data = await mem0Request("/v3/memories/search/", {
    method: "POST",
    body: {
      query,
      filters: { user_id },
      top_k: 8,
      threshold: 0.0,
      rerank: true
    }
  });

  return normalizeMem0Envelope(data);
}

Two details matter here:

  • Entity IDs belong inside filters. That keeps retrieval scoped to the correct customer or account.

  • Retrieval happens before Gemini makes a decision. Gemini should not decide whether an issue is recurring until Mem0 has returned the relevant support history.

Note: Do not prompt the model to remember. Retrieve memory first, then ask the model to reason.

Step 3: Give Gemini the Support Decision Packet

The server sends Gemini a compact decision packet.

const decisionPrompt = {
  current_date: getCurrentDate(),
  customer: publicCustomer,
  user_message: message,
  mem0_enabled: mem0SeededForRun,
  mem0_customer_memory: customerMemory.results,
  mem0_account_memory: accountMemory.results,
  support_policy: supportPolicy,
  instructions: [
    "Use only the current message, Mem0 search results, and support policy provided here.",
    "If mem0_enabled is false, treat this as the baseline run with no persistent customer/account memory.",
    "If the retrieved memory is not enough to justify an escalation, ask for the missing information.",
    "Use tool calls to take the next support action. Do not claim a tool action happened unless you call that tool.",
    "Always include one customer-facing action tool call: ask_for_missing_info, answer_customer, or create_escalation_ticket.",
    "Call notify_customer_success_manager only when the support policy and retrieved Mem0 account memory justify CSM notification.",
    "If mem0_enabled is true, call store_support_outcome_memory with a concise factual support outcome.",
    "If mem0_enabled is false, do not call store_support_outcome_memory.",
    "Do not expose internal labels such as account_risk, renewal risk, or churn risk in the customer-facing message."
  ]
};
const decisionPrompt = {
  current_date: getCurrentDate(),
  customer: publicCustomer,
  user_message: message,
  mem0_enabled: mem0SeededForRun,
  mem0_customer_memory: customerMemory.results,
  mem0_account_memory: accountMemory.results,
  support_policy: supportPolicy,
  instructions: [
    "Use only the current message, Mem0 search results, and support policy provided here.",
    "If mem0_enabled is false, treat this as the baseline run with no persistent customer/account memory.",
    "If the retrieved memory is not enough to justify an escalation, ask for the missing information.",
    "Use tool calls to take the next support action. Do not claim a tool action happened unless you call that tool.",
    "Always include one customer-facing action tool call: ask_for_missing_info, answer_customer, or create_escalation_ticket.",
    "Call notify_customer_success_manager only when the support policy and retrieved Mem0 account memory justify CSM notification.",
    "If mem0_enabled is true, call store_support_outcome_memory with a concise factual support outcome.",
    "If mem0_enabled is false, do not call store_support_outcome_memory.",
    "Do not expose internal labels such as account_risk, renewal risk, or churn risk in the customer-facing message."
  ]
};
const decisionPrompt = {
  current_date: getCurrentDate(),
  customer: publicCustomer,
  user_message: message,
  mem0_enabled: mem0SeededForRun,
  mem0_customer_memory: customerMemory.results,
  mem0_account_memory: accountMemory.results,
  support_policy: supportPolicy,
  instructions: [
    "Use only the current message, Mem0 search results, and support policy provided here.",
    "If mem0_enabled is false, treat this as the baseline run with no persistent customer/account memory.",
    "If the retrieved memory is not enough to justify an escalation, ask for the missing information.",
    "Use tool calls to take the next support action. Do not claim a tool action happened unless you call that tool.",
    "Always include one customer-facing action tool call: ask_for_missing_info, answer_customer, or create_escalation_ticket.",
    "Call notify_customer_success_manager only when the support policy and retrieved Mem0 account memory justify CSM notification.",
    "If mem0_enabled is true, call store_support_outcome_memory with a concise factual support outcome.",
    "If mem0_enabled is false, do not call store_support_outcome_memory.",
    "Do not expose internal labels such as account_risk, renewal risk, or churn risk in the customer-facing message."
  ]
};

The publicCustomer object intentionally contains only safe customer identifiers:

function toPublicCustomer(customer) {
  return {
    id: customer.id,
    name: customer.name,
    account_id: customer.account_id,
    account_name: customer.account_name
  };
}
function toPublicCustomer(customer) {
  return {
    id: customer.id,
    name: customer.name,
    account_id: customer.account_id,
    account_name: customer.account_name
  };
}
function toPublicCustomer(customer) {
  return {
    id: customer.id,
    name: customer.name,
    account_id: customer.account_id,
    account_name: customer.account_name
  };
}

This matters because the baseline run should not accidentally receive hidden labels like "Enterprise renewal account" or "Pro user with repeat issue." If Gemini gets that information in the prompt, then the before/after experiment is contaminated.

Step 4: Define Gemini Tools

In this demo, We’ll be using Gemini 3.5 flash that calls the following tools through function calling:

ask_for_missing_info
answer_customer
create_escalation_ticket
notify_customer_success_manager
store_support_outcome_memory
ask_for_missing_info
answer_customer
create_escalation_ticket
notify_customer_success_manager
store_support_outcome_memory
ask_for_missing_info
answer_customer
create_escalation_ticket
notify_customer_success_manager
store_support_outcome_memory

Here is the escalation tool:

{
  name: "create_escalation_ticket",
  description: "Create a high-priority local support escalation record for recurring, SLA-sensitive, or high-risk customer issues.",
  parameters: {
    type: "object",
    properties: {
      customer_id: { type: "string" },
      account_id: { type: "string" },
      priority: {
        type: "string",
        enum: ["low", "medium", "high", "urgent"]
      },
      reason: { type: "string" },
      summary: { type: "string" },
      handoff_summary: { type: "string" }
    },
    required: [
      "customer_id",
      "account_id",
      "priority",
      "reason",
      "summary",
      "handoff_summary"
    ]
  }
}
{
  name: "create_escalation_ticket",
  description: "Create a high-priority local support escalation record for recurring, SLA-sensitive, or high-risk customer issues.",
  parameters: {
    type: "object",
    properties: {
      customer_id: { type: "string" },
      account_id: { type: "string" },
      priority: {
        type: "string",
        enum: ["low", "medium", "high", "urgent"]
      },
      reason: { type: "string" },
      summary: { type: "string" },
      handoff_summary: { type: "string" }
    },
    required: [
      "customer_id",
      "account_id",
      "priority",
      "reason",
      "summary",
      "handoff_summary"
    ]
  }
}
{
  name: "create_escalation_ticket",
  description: "Create a high-priority local support escalation record for recurring, SLA-sensitive, or high-risk customer issues.",
  parameters: {
    type: "object",
    properties: {
      customer_id: { type: "string" },
      account_id: { type: "string" },
      priority: {
        type: "string",
        enum: ["low", "medium", "high", "urgent"]
      },
      reason: { type: "string" },
      summary: { type: "string" },
      handoff_summary: { type: "string" }
    },
    required: [
      "customer_id",
      "account_id",
      "priority",
      "reason",
      "summary",
      "handoff_summary"
    ]
  }
}

And the CSM notification tool:

{
  name: "notify_customer_success_manager",
  description: "Create a local CSM notification record for enterprise accounts that require human follow-up.",
  parameters: {
    type: "object",
    properties: {
      account_id: { type: "string" },
      customer_id: { type: "string" },
      csm_name: { type: "string" },
      handoff_summary: { type: "string" },
      risk_reason: { type: "string" }
    },
    required: [
      "account_id",
      "customer_id",
      "handoff_summary",
      "risk_reason"
    ]
  }
}
{
  name: "notify_customer_success_manager",
  description: "Create a local CSM notification record for enterprise accounts that require human follow-up.",
  parameters: {
    type: "object",
    properties: {
      account_id: { type: "string" },
      customer_id: { type: "string" },
      csm_name: { type: "string" },
      handoff_summary: { type: "string" },
      risk_reason: { type: "string" }
    },
    required: [
      "account_id",
      "customer_id",
      "handoff_summary",
      "risk_reason"
    ]
  }
}
{
  name: "notify_customer_success_manager",
  description: "Create a local CSM notification record for enterprise accounts that require human follow-up.",
  parameters: {
    type: "object",
    properties: {
      account_id: { type: "string" },
      customer_id: { type: "string" },
      csm_name: { type: "string" },
      handoff_summary: { type: "string" },
      risk_reason: { type: "string" }
    },
    required: [
      "account_id",
      "customer_id",
      "handoff_summary",
      "risk_reason"
    ]
  }
}

Step 5: Execute Tools

The demo creates local operation logs for support actions:

if (call.name === "create_escalation_ticket") {
  return appendOperation("tickets.json", {
    ticket_id: `local-ticket-${Date.now()}`,
    created_at: new Date().toISOString(),
    local_operation_log: true,
    ...args
  });
}
if (call.name === "create_escalation_ticket") {
  return appendOperation("tickets.json", {
    ticket_id: `local-ticket-${Date.now()}`,
    created_at: new Date().toISOString(),
    local_operation_log: true,
    ...args
  });
}
if (call.name === "create_escalation_ticket") {
  return appendOperation("tickets.json", {
    ticket_id: `local-ticket-${Date.now()}`,
    created_at: new Date().toISOString(),
    local_operation_log: true,
    ...args
  });
}

Note: This is production-mimicking.

In this demo we did not wire any Zendesk ticket or Salesforce task. Instead, we use local integration and records from the local log.

Step 6: Send Function Results Back to Gemini

After executing tools, the app sends the function responses back to Gemini. This matters because Gemini should generate the final customer response from actual tool results, not from an assumption.

The function response includes the matching function call ID when Gemini provides one:

const functionResponseParts = toolResults.map((tool) => ({
  functionResponse: {
    name: tool.name,
    ...(tool.id ? { id: tool.id } : {}),
    response: {
      result: tool.result
    }
  }
}));
const functionResponseParts = toolResults.map((tool) => ({
  functionResponse: {
    name: tool.name,
    ...(tool.id ? { id: tool.id } : {}),
    response: {
      result: tool.result
    }
  }
}));
const functionResponseParts = toolResults.map((tool) => ({
  functionResponse: {
    name: tool.name,
    ...(tool.id ? { id: tool.id } : {}),
    response: {
      result: tool.result
    }
  }
}));

Then Gemini generates the final result as JSON:

const finalPrompt = {
  task: "Generate the final support workflow result as JSON after reviewing the function responses.",
  required_schema: {
    customer_response: "string shown to the customer",
    decision_summary: "string explaining the action for the demo operator",
    handoff_brief: "string for a human support teammate, empty string if no handoff is needed",
    mem0_value: "string explaining which retrieved Mem0 memories changed the action",
    tools_executed: "array of executed tool names"
  },
  constraints: [
    "Do not expose internal account-risk labels to the customer.",
    "Do not invent facts beyond the message, Mem0 results, support policy, and executed tool results.",
    "If the tool result says local operation log, describe it as a local demo operation log, not as a real external CRM or email send."
  ]
};
const finalPrompt = {
  task: "Generate the final support workflow result as JSON after reviewing the function responses.",
  required_schema: {
    customer_response: "string shown to the customer",
    decision_summary: "string explaining the action for the demo operator",
    handoff_brief: "string for a human support teammate, empty string if no handoff is needed",
    mem0_value: "string explaining which retrieved Mem0 memories changed the action",
    tools_executed: "array of executed tool names"
  },
  constraints: [
    "Do not expose internal account-risk labels to the customer.",
    "Do not invent facts beyond the message, Mem0 results, support policy, and executed tool results.",
    "If the tool result says local operation log, describe it as a local demo operation log, not as a real external CRM or email send."
  ]
};
const finalPrompt = {
  task: "Generate the final support workflow result as JSON after reviewing the function responses.",
  required_schema: {
    customer_response: "string shown to the customer",
    decision_summary: "string explaining the action for the demo operator",
    handoff_brief: "string for a human support teammate, empty string if no handoff is needed",
    mem0_value: "string explaining which retrieved Mem0 memories changed the action",
    tools_executed: "array of executed tool names"
  },
  constraints: [
    "Do not expose internal account-risk labels to the customer.",
    "Do not invent facts beyond the message, Mem0 results, support policy, and executed tool results.",
    "If the tool result says local operation log, describe it as a local demo operation log, not as a real external CRM or email send."
  ]
};

Step 7: Write Support Outcome Back to Mem0

The app only writes support outcomes back to Mem0 after the seed step has enabled Mem0 for the current run. Baseline runs do not write to memory, because that would contaminate the before/after experiment.

if (call.name === "store_support_outcome_memory") {
  if (!mem0SeededForRun) {
    return {
      skipped: true,
      reason: "Mem0 has not been seeded for this demo run, so baseline output was not written to memory."
    };
  }

  const scopeUserId =
    args.scope === "account"
      ? scopedAccountId(customer.account_id)
      : scopedCustomerId(customer.id);

  const addResult = await mem0Add({
    user_id: scopeUserId,
    messages: [{ role: "assistant", content: args.memory }],
    metadata: {
      app: DEMO_NAMESPACE,
      run_id: DEMO_RUN_ID,
      scope: args.scope,
      issue_type: args.issue_type,
      status: args.status,
      customer_id: customer.id,
      account_id: customer.account_id
    },
    infer: true
  });

  const event = addResult.event_id ? await mem0PollEvent(addResult.event_id) : null;

  return {
    mem0_user_id: scopeUserId,
    event_id: addResult.event_id || null,
    status: event?.status || addResult.status || "submitted"
  };
}
if (call.name === "store_support_outcome_memory") {
  if (!mem0SeededForRun) {
    return {
      skipped: true,
      reason: "Mem0 has not been seeded for this demo run, so baseline output was not written to memory."
    };
  }

  const scopeUserId =
    args.scope === "account"
      ? scopedAccountId(customer.account_id)
      : scopedCustomerId(customer.id);

  const addResult = await mem0Add({
    user_id: scopeUserId,
    messages: [{ role: "assistant", content: args.memory }],
    metadata: {
      app: DEMO_NAMESPACE,
      run_id: DEMO_RUN_ID,
      scope: args.scope,
      issue_type: args.issue_type,
      status: args.status,
      customer_id: customer.id,
      account_id: customer.account_id
    },
    infer: true
  });

  const event = addResult.event_id ? await mem0PollEvent(addResult.event_id) : null;

  return {
    mem0_user_id: scopeUserId,
    event_id: addResult.event_id || null,
    status: event?.status || addResult.status || "submitted"
  };
}
if (call.name === "store_support_outcome_memory") {
  if (!mem0SeededForRun) {
    return {
      skipped: true,
      reason: "Mem0 has not been seeded for this demo run, so baseline output was not written to memory."
    };
  }

  const scopeUserId =
    args.scope === "account"
      ? scopedAccountId(customer.account_id)
      : scopedCustomerId(customer.id);

  const addResult = await mem0Add({
    user_id: scopeUserId,
    messages: [{ role: "assistant", content: args.memory }],
    metadata: {
      app: DEMO_NAMESPACE,
      run_id: DEMO_RUN_ID,
      scope: args.scope,
      issue_type: args.issue_type,
      status: args.status,
      customer_id: customer.id,
      account_id: customer.account_id
    },
    infer: true
  });

  const event = addResult.event_id ? await mem0PollEvent(addResult.event_id) : null;

  return {
    mem0_user_id: scopeUserId,
    event_id: addResult.event_id || null,
    status: event?.status || addResult.status || "submitted"
  };
}

This is what makes the agent compound. A stateless support agent handles a ticket and forgets it.

But, a Mem0-backed support agent handles a ticket and turns the outcome into durable context for the next interaction.

That gives you the following loop:

Retrieve memory
Reason with Gemini
Take action
Store outcome
Retrieve better memory next time
Retrieve memory
Reason with Gemini
Take action
Store outcome
Retrieve better memory next time
Retrieve memory
Reason with Gemini
Take action
Store outcome
Retrieve better memory next time

Running the Demo

The demo app uses a small Node server and browser UI.

cp .env.example .env
cp .env.example .env
cp .env.example .env

Add Mem0 API Key and Gemini API Key credentials as follows:

GEMINI_API_KEY=your_gemini_api_key
MEM0_API_KEY=your_mem0_api_key
GEMINI_MODEL=gemini-3.5-flash
GEMINI_API_KEY=your_gemini_api_key
MEM0_API_KEY=your_mem0_api_key
GEMINI_MODEL=gemini-3.5-flash
GEMINI_API_KEY=your_gemini_api_key
MEM0_API_KEY=your_mem0_api_key
GEMINI_MODEL=gemini-3.5-flash

Start the app:

npm start
npm start
npm start

Open the URL printed by the server:

<http://127.0.0.1:3000>
<http://127.0.0.1:3000>
<http://127.0.0.1:3000>

Then:

  1. Select a customer.

  2. Run the workflow once before seeding. This is the Gemini-only baseline.

  3. Click Seed Mem0.

  4. Run the same message again for the same customer.

  5. Compare the baseline response with the Mem0-aware response.

💡 The complete code is available on the GitHub repository.

Closing Thought

The best support agents are not just better writers, but are the ones who are better operators. They know when to ask for missing information, when to answer directly, when to escalate, when to notify a human, and what context to carry forward.

That requires two layers:

  • Persistent memory: what should survive beyond the current prompt

  • Reasoning and tools: what action should happen now

Mem0 handles the first layer. Gemini handles the second

Frequently Asked Questions

What is a customer-aware support agent?

A customer-aware support agent retrieves durable customer and account memory before deciding what to do. Instead of just personalising tone, it chooses the correct workflow: ask for missing information, answer directly, escalate, notify a human, or update the support record, based on what it already knows about the user.

Why use Mem0 instead of putting everything in the prompt?

Prompts are temporary. Mem0 stores durable memory that survives sessions, tab closes, restarts, and future support conversations. The agent retrieves only relevant customer/account facts before Gemini decides what action to take.

What does Gemini 3.5 Flash do in this architecture?

Gemini 3.5 Flash classifies the issue, reasons over the Mem0 context and policy, chooses function calls, reads tool results, and generates the final customer response and handoff summary.

Is this different from RAG?

Yes. RAG usually retrieves documents such as policies or help-center articles. Mem0 retrieves durable user and account memory, such as prior issues, preferences, promises, renewal context, and support outcomes.

Does the demo create real support tickets?

No. The demo writes ticket and CSM actions to local operation logs. That keeps the demo honest. In production, the same tool functions can call Zendesk, Salesforce Service Cloud, Intercom, Jira Service Management, or your internal support API.

References

Mem0 is an intelligent, open-source memory layer designed for LLMs and AI agents to provide long-term, personalized, and context-aware interactions across sessions.

Get your free API Key hereapp.mem0.ai or

self-host mem0 from our open source GitHub repository.

GET TLDR from:

Summarize

Website/Footer

Summarize

Website/Footer

Summarize

Website/Footer

Summarize

Website/Footer