How to Orchestrate Multi-Call Conversations with an LLM and Twilio Conversation Memory with PHP

September 11, 2026
Written by

Have you ever been on the phone with an AI voice agent and gotten frustrated with its lack of memory? Maybe your agent hung up on you, or it got disconnected, forcing you to start a conversation all over again. This kind of interruption can waste time for you and your users, and cause a lot of frustration.

Twilio Conversation Memory is your solution. Conversation Memory allows context to be persisted between calls. This means that if you call the Twilio agent back, it won't lose the context of what you were talking about when you hung up, and can pick up right where you left off. This can save you a lot of frustration and help you get things done better and faster when you're talking to an agent.

In this tutorial, you will make a PHP service using Open Swoole that retains caller context, preferences and action history across multiple separate inbound calls.

Prerequisites

To complete this tutorial you will need:

Building the app

Step 1 - Set up the PHP project

To get started, create a new PHP project:

mkdir -p TwilioMultiCallMemory/public TwilioMultiCallMemory/src/Service TwilioMultiCallMemory/src/WebSocket
cd TwilioMultiCallMemory

Step 2 - Install required dependencies

Install the Twilio, OpenAI, PHP Dotenv, Monolog, and Guzzle packages.

composer require guzzlehttp/guzzle guzzlehttp/psr7 monolog/monolog openai-php/client twilio/sdk vlucas/phpdotenv

The Twilio package will allow your application to interface with Twilio's services. The PHP Dotenv package allows you to import your environment variables into your solution using a .env file. You will add those variables in the next step. The OpenAI package will be used to connect your solution to OpenAI. Monolog will be used for simplified logging.

Then, create a custom PSR-4 namespace named "App" by adding the following configuration to composer.json:

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
}

Step 3 - Create a Twilio Memory Store

For this tutorial, you will need a Conversation Memory Store. Go into your Twilio console and look for Memory Stores. You can use the console search, or look for Orchestration > Conversation Memory > Memory Stores.

Memory Stores use machine learning, and you may have to agree to a warning before proceeding. Keep in mind that Conversation Memory is not intended for use with sensitive information. Conversation products are only available on the new Twilio Console, so make sure your account has been migrated. For more information about Conversation Memory, you may want to read the documentation, including the Getting Started Guide.

Once you have found the correct tab, click on Create New Store.

Memory Stores in 1console
Memory Stores in 1console

Now follow the steps to set up your memory store.

The console gives you a setup checklist to get you started. Click on Connect Conversation Orchestrator, and give it a friendly name. You write a short description, then can move on to Messaging and Chat Traffic. For the remainder of the items in this checklist, you can select the default values for now.

You don't have any customer profiles yet, so you can skip the rest of the checklist. However, you will need your memory store ID, which is at the top left of the memory store screen. There should be a convenient button to copy-paste that ID. Keep that ID for the next step.

Step 4 - Configure environment variables

Now that you have a memory store created, you will need to be able to access that from your application. For this, you will need to get your Memory Store ID and paste that into your secrets file. Create a .env file in the root directory of your project. Add the following values, replacing the placeholders.

OPENAI_API_KEY=sk-...
TWILIO_API_KEY=SK...
TWILIO_API_SECRET=...
TWILIO_MEMORY_STORE_ID=mem_store_...

Get your API key from your Twilio console, created under Settings > Account Settings > API Keys & Auth Tokens. Creating a Main API key is the simplest method for this tutorial. You get your memory store key from the previous step and paste it in here. Your OpenAI API Key is generated from OpenAI's dashboard.

Save the file, and move on to the next step, creating your services.

Step 5 - Set up the OpenAI service

This demonstration uses the fiction of an auto repair shop as the agent that you are calling. However, Conversation Memory would be useful in lots of different scenarios, such as tech support, travel, and more. Feel free to adjust the audio prompts as you see fit for your own personal projects.

Create a new file in src/Service named OpenAiService.php class to handle interaction with gpt-4o-mini.

Paste the following into your new class:

<?php

declare(strict_types=1);

namespace App\Service;

use App\WebSocket\ConversationRelayHandler;
use OpenAI;
use OpenAI\Contracts\ClientContract;
use Psr\Log\LoggerInterface;
use Swoole\WebSocket\Server as WsServer;
use Throwable;

use function trim;

final class OpenAiService
{
    private const string MODEL   = 'gpt-4o-mini';
    private const int MAX_TOKENS = 400;

    private const BASE_SYSTEM_PROMPT = <<<'TXT'
        You are the phone assistant for Owlbert's Auto Repair. You are friendly, concise, and speak naturally as if on the phone.
        Do not use lists, bullet points, or emojis — respond in plain sentences.
        If the caller mentions their name, vehicle, or a problem with their car, remember it and refer back to it naturally as the conversation continues.
        If you do not know something, say so honestly rather than guessing.
        TXT;

    private readonly ClientContract $client;

    public function __construct(private readonly LoggerInterface $log)
    {
        $this->client = OpenAI::client($_ENV['OPENAI_API_KEY']);
    }

    public function streamResponse(
        WsServer $server,
        int $fd,
        ?string $callSid,
        ?string $memoryContext,
        array $messages,
    ): string {
        $systemPrompt = ($memoryContext === null || trim($memoryContext) === '')
            ? self::BASE_SYSTEM_PROMPT
            : self::BASE_SYSTEM_PROMPT . "\n\nPrior context on this customer (from previous calls):\n" . $memoryContext;

        $openAiMessages = [['role' => 'system', 'content' => $systemPrompt]];
        foreach ($messages as $m) {
            $openAiMessages[] = ['role' => $m['role'], 'content' => $m['content']];
        }

        $fullResponse = '';
        try {
            $stream = $this->client->chat()->createStreamed([
                'model'      => self::MODEL,
                'max_tokens' => self::MAX_TOKENS,
                'messages'   => $openAiMessages,
            ]);

            foreach ($stream as $response) {
                $token = $response->choices[0]->delta->content ?? null;
                if ($token !== null && $token !== '') {
                    $fullResponse .= $token;
                    ConversationRelayHandler::sendJson($server, $fd, [
                        'type'  => 'text',
                        'token' => $token,
                        'last'  => false,
                    ]);
                }
            }
        } catch (Throwable $ex) {
            $this->log->warning('[{callSid}] OpenAI stream failed: {err}', [
                'callSid' => $callSid,
                'err'     => $ex->getMessage(),
            ]);
        } finally {
            ConversationRelayHandler::sendJson($server, $fd, [
                'type'  => 'text',
                'token' => '',
                'last'  => true,
            ]);
        }

        $this->log->info(
            '[{callSid}] Assistant: {response}',
            [
                'callSid' => $callSid,
                'response' => $fullResponse
            ]
        );
        return $fullResponse;
    }
}

This code handles your initial connection to OpenAI. Its function is to parse the information from a caller and stream it to the OpenAI API. Notice the system prompt here, which explains the functionality of the agent. It contains some useful instructions for the agent, such as to avoid bullet points and emojis when speaking on the phone.

Next, create the webhook for Twilio's connection.

Step 6 - Build the Twilio webhook and Conversation Memory pipeline

Create another new file, this time in src/WebSocket called ConversationRelayHandler.php. Paste in this code:

<?php

declare(strict_types=1);

namespace App\WebSocket;

use App\Service\ConversationMemoryService;
use App\Service\OpenAiService;
use Psr\Log\LoggerInterface;
use Swoole\WebSocket\Server as WsServer;
use Throwable;

use function array_pop;
use function end;
use function is_string;
use function json_decode;
use function json_encode;
use function trim;

final class ConversationRelayHandler
{
    private ?string $callSid = null;
    private string $callerPhone = '';
    private ?string $memoryContext = null;

    /** @var list<array{role:string, content:string}> */
    private array $messages = [];

    public function __construct(
        private readonly WsServer $server,
        private readonly int $fd,
        private readonly OpenAiService $openAi,
        private readonly ConversationMemoryService $memory,
        private readonly LoggerInterface $log,
    ) {
    }

    public function onMessage(string $data): void
    {
        try {
            /** @var array<string, mixed> $msg */
            $msg = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
        } catch (Throwable $ex) {
            $this->log->warning('[{callSid}] Bad JSON frame: {err}', [
                'callSid' => $this->callSid,
                'err'     => $ex->getMessage(),
            ]);
            return;
        }

        $type = is_string($msg['type'] ?? null) ? $msg['type'] : null;

        switch ($type) {
            case 'setup':
                $this->handleSetup($msg);
                break;
            case 'prompt':
                $this->handlePrompt($msg);
                break;
            case 'interrupt':
                $this->handleInterrupt($msg);
                break;
            case 'error':
                $desc = is_string($msg['description'] ?? null) ? $msg['description'] : '';
                $this->log->warning('[{callSid}] Conversation Relay error: {desc}', [
                    'callSid' => $this->callSid,
                    'desc'    => $desc,
                ]);
                break;
        }
    }

    public function onClose(): void
    {
        $this->log->info('[{callSid}] Call ended', ['callSid' => $this->callSid]);
    }

    /** @param array<string, mixed> $msg */
    private function handleSetup(array $msg): void
    {
        $this->callSid     = is_string($msg['callSid'] ?? null) ? $msg['callSid'] : null;
        $this->callerPhone = is_string($msg['from']    ?? null) ? $msg['from'] : '';

        $this->log->info('[{callSid}] Call connected from {phone}', [
            'callSid' => $this->callSid,
            'phone'   => $this->callerPhone,
        ]);

        try {
            $this->memoryContext = $this->memory->getContext($this->callerPhone);
            if (trim($this->memoryContext) !== '') {
                $this->log->info('[{callSid}] Memory context loaded ({chars} chars)', [
                    'callSid' => $this->callSid,
                    'chars'   => strlen($this->memoryContext),
                ]);
            }
        } catch (Throwable $ex) {
            $this->log->warning('[{callSid}] Memory recall failed — continuing without context: {err}', [
                'callSid' => $this->callSid,
                'err'     => $ex->getMessage(),
            ]);
            $this->memoryContext = null;
        }
    }

    /** @param array<string, mixed> $msg */
    private function handlePrompt(array $msg): void
    {
        if (empty($msg['last'])) {
            return;
        }

        $userText = is_string($msg['voicePrompt'] ?? null) ? $msg['voicePrompt'] : '';
        $this->log->info('[{callSid}] Caller: {text}', [
            'callSid' => $this->callSid,
            'text'    => $userText,
        ]);

        $this->messages[] = ['role' => 'user', 'content' => $userText];
        $responseText = $this->openAi->streamResponse(
            $this->server,
            $this->fd,
            $this->callSid,
            $this->memoryContext,
            $this->messages,
        );
        $this->messages[] = ['role' => 'assistant', 'content' => $responseText];
    }

    /** @param array<string, mixed> $msg */
    private function handleInterrupt(array $msg): void
    {
        $spoken = is_string($msg['utteranceUntilInterrupt'] ?? null)
            ? $msg['utteranceUntilInterrupt']
            : '';

        $this->log->info("[{callSid}] Interrupted after: '{spoken}'", [
            'callSid' => $this->callSid,
            'spoken'  => $spoken,
        ]);

        $last = end($this->messages);
        if ($last !== false && $last['role'] === 'assistant') {
            array_pop($this->messages);
        }
    }

    /** @param array<string, mixed> $payload */
    public static function sendJson(WsServer $server, int $fd, array $payload): void
    {
        $server->push(
            $fd,
            json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
        );
    }
}

ConversationRelayHandler is the bridge between Twilio's WebSocket and the rest of the app. When Twilio opens the socket after a <ConversationRelay> TwiML directive, the class loops reading frames one at a time, assembling multi-part messages into a memory context. A prompt frame carries the caller's transcribed speech. It appends the text to an in-memory conversation history and hands the whole history plus the memory context off to OpenAiService::streamResponse, which streams tokens back out through the same socket.

The code also contains interruption handling for your agent. If the agent is interrupted during a conversation, it pops the last message off of the history so the agent realizes the full message was not sent and was incomplete. This will allow the customer to continue talking and handle the interruption in a more human way, without your user missing context.

Step 7 - Process multi-turn historical context

Now you will create one more class to handle the context and memory processing. You'll call this file ConversationMemoryService.php and create it in src/Service. Paste the following into the file:

<?php

declare(strict_types=1);

namespace App\Service;

use Nullform\HttpStatus;
use Psr\Log\LoggerInterface;
use Swoole\Coroutine\Http\Client;
use Throwable;

use function base64_encode;
use function implode;
use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
use function trim;

final class ConversationMemoryService
{
    private const MEMORY_HOST = 'memory.twilio.com';
    private const MEMORY_PORT = 443;

    private readonly string $storeId;
    private readonly string $basicAuth;

    public function __construct(private readonly LoggerInterface $log)
    {
        $apiKey    = $_ENV['TWILIO_API_KEY'];
        $apiSecret = $_ENV['TWILIO_API_SECRET'];
        $storeId   = $_ENV['TWILIO_MEMORY_STORE_ID'];

        $this->storeId   = $storeId;
        $this->basicAuth = base64_encode("{$apiKey}:{$apiSecret}");
    }

    public function getContext(string $phone): string
    {
        if (trim($phone) === '') {
            return '';
        }

        $profileId = $this->lookupProfileId($phone);
        if ($profileId === null) {
            $this->log->info(
                'No profile found for {phone} — treating as new caller.',
                [
                    'phone' => $phone,
                ]
            );
            return '';
        }

        $this->log->info(
            'Profile lookup for {phone} -> {profileId}',
            [
                'phone' => $phone,
                'profileId' => $profileId
            ]
        );
        return $this->recall($profileId);
    }

    private function lookupProfileId(string $phone): ?string
    {
        $body = json_encode(
            [
                'idType' => 'phone',
                'value' => $phone
            ],
            JSON_THROW_ON_ERROR
        );
        $path = "/v1/Stores/{$this->storeId}/Profiles/Lookup";
        [$status, $respBody] = $this->post($path, $body);

        if ($status === HttpStatus::NOT_FOUND) {
            return null;
        }

        if ($status < HttpStatus::OK || $status >= HttpStatus::MULTIPLE_CHOICES) {
            $this->log->warning(
                'Profile lookup failed ({status}): {body}',
                [
                    'status' => $status,
                    'body' => $respBody
                ]
            );
            return null;
        }

        try {
            /** @var array<string, mixed> $data */
            $data = json_decode($respBody, true, flags: JSON_THROW_ON_ERROR);
        } catch (Throwable) {
            $this->log->warning('Could not parse profile lookup response: {body}', ['body' => $respBody]);
            return null;
        }

        $profiles = $data['profiles'] ?? [];
        if ($profiles !== []) {
            $first = $profiles[0] ?? [];
            if (is_array($first)) {
                if (isset($first['profileId']) && is_string($first['profileId'])) {
                    return $first['profileId'];
                }
                if (isset($first['id']) && is_string($first['id'])) {
                    return $first['id'];
                }
            }
        }

        if (isset($data['profileId']) && is_string($data['profileId'])) {
            return $data['profileId'];
        }

        return null;
    }

    private function recall(string $profileId): string
    {
        $body = json_encode([
            'observationsLimit'    => 20,
            'summariesLimit'       => 5,
            'communicationsLimit'  => 0,
        ], JSON_THROW_ON_ERROR);

        $path = "/v1/Stores/{$this->storeId}/Profiles/{$profileId}/Recall";
        [$status, $respBody] = $this->post($path, $body);

        if ($status < HttpStatus::OK || $status >= HttpStatus::MULTIPLE_CHOICES) {
            $this->log->warning(
                'Recall failed ({status}): {body}',
                [
                    'status' => $status,
                    'body' => $respBody
                ]
            );
            return '';
        }

        $this->log->info('Recall raw response: {body}', ['body' => $respBody]);
        return $this->formatRecall($respBody);
    }

    private function post(string $path, string $body): array
    {
        $client = new Client(self::MEMORY_HOST, self::MEMORY_PORT, true);
        $client->setHeaders([
            'Host'          => self::MEMORY_HOST,
            'Authorization' => "Basic {$this->basicAuth}",
            'Content-Type'  => 'application/json',
            'Accept'        => 'application/json',
        ]);
        $client->post($path, $body);
        $status = $client->statusCode;
        $respBody = $client->body ?? '';
        $client->close();

        return [$status < 0 ? 0 : $status, $respBody];
    }

    private function formatRecall(string $json): string
    {
        try {
            /** @var array<string, mixed> $data */
            $data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
        } catch (Throwable) {
            return '';
        }

        $lines = [];

        $observations = $data['observations'] ?? [];
        if ($observations !== []) {
            foreach ($observations as $observation) {
                $text = $this->extractText($observation, ['text', 'content', 'observation', 'value']);
                if (trim($text) !== '') {
                    $lines[] = "- {$text}";
                }
            }
        }

        $summaries = $data['summaries'] ?? [];
        if ($summaries !== []) {
            foreach ($summaries as $summary) {
                $text = $this->extractText($summary, ['text', 'summary', 'content', 'value']);
                if (trim($text) !== '') {
                    $lines[] = "Summary: {$text}";
                }
            }
        }

        return $lines === [] ? '' : implode("\n", $lines);
    }

    /** @param list<string> $candidates */
    private function extractText(mixed $el, array $candidates): string
    {
        if (is_string($el)) {
            return $el;
        }
        if (!is_array($el)) {
            return '';
        }
        foreach ($candidates as $name) {
            if (isset($el[$name]) && is_string($el[$name])) {
                return $el[$name];
            }
        }
        return '';
    }
}

This part of the code is what will handle your multi-turn conversation.

ConversationMemoryService is a thin wrapper around Twilio's Conversation Memory REST API that turns a caller's phone number into a prompt-ready string of prior-call context. It reads from environment variables and configures your HTTP client.

The public entry point getContext() runs a two-step lookup: first lookupProfileIdId() normalizes the number, matches it against a canonical profile, and returns a profile ID (or null if no previous caller with that number was found). If a profile was found, recall() posts, asking for up to 20 observations and 5 summaries. The response is logged raw for inspection and then passed to formatRecall(), which parses the response.

The application tries several plausible field names (text, content, observation, summary, value) via ExtractText and stitches whatever it finds into a bulleted string. That string is what eventually gets prepended to the OpenAI system prompt as "Prior context on this customer," making the caller's history part of the model's instructions before they've even spoken.

Step 8 - Finalize your application

To complete your project you will need to create two further files. Firstly, create a file named WebSocketServer.php in src/WebSocket, and paste the code below into the file:

<?php

declare(strict_types=1);

namespace App\WebSocket;

use App\Service\ConversationMemoryService;
use App\Service\OpenAiService;
use Nullform\HttpStatus;
use Psr\Log\LoggerInterface;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use Twilio\TwiML\VoiceResponse;

use function strcasecmp;

final class WebSocketServer
{
    public const string DEFAULT_DOMAIN         = '0.0.0.0';
    public const int DEFAULT_PORT              = 9501;
    public const string DEFAULT_PATH           = '/';
    public const string DEFAULT_REQUEST_METHOD = 'GET';
    public const int DEFAULT_NUM_WORKERS       = 2;

    /** @var array<int, ConversationRelayHandler> */
    private array $handlers = [];

    private readonly Server $server;

    public function __construct(
        private readonly OpenAiService $openAi,
        private readonly ConversationMemoryService $memory,
        private readonly LoggerInterface $log,
        private readonly string $host = self::DEFAULT_DOMAIN,
        private readonly int $port = self::DEFAULT_PORT,
    ) {
        $this->server = new Server($this->host, $this->port);

        $this->server->set([
            'worker_num' => (int) ($_ENV['SWOOLE_WORKERS'] ?? self::DEFAULT_NUM_WORKERS),
            'hook_flags' => SWOOLE_HOOK_ALL,
        ]);

        $this->server->on('request', $this->onHttpRequest(...));
        $this->server->on('open', $this->onOpen(...));
        $this->server->on('message', $this->onMessage(...));
        $this->server->on('close', $this->onClose(...));

        $this->log->info(
            'Listening on {host}:{port}',
            [
                'host' => $this->host,
                'port' => $this->port
            ]
        );
        $this->server->start();
    }

    private function onHttpRequest(Request $req, Response $res): void
    {
        $method = $req->server['request_method'] ?? self::DEFAULT_REQUEST_METHOD;
        $uri    = $req->server['request_uri'] ?? self::DEFAULT_PATH;

        if (strcasecmp($method, 'POST') === 0 && $uri === '/voice') {
            $host = $_ENV['DOMAIN'] ?? $req->header['host'] ?? self::DEFAULT_DOMAIN;

            $response = new VoiceResponse();
            $connect = $response->connect();
            $conversationrelay = $connect->conversationRelay([
                'url' => "wss://{$host}/ws",
                'welcomeGreeting' => "Thank's for calling Owlbert's Auto Repair. How can I help you today?",
            ]);

            $res->header('Content-Type', 'application/xml');
            $res->end($response->asXML());
            return;
        }

        $res->status(HttpStatus::NOT_FOUND);
        $res->end();
    }

    private function onOpen(Server $server, Request $req): void
    {
        $uri = $req->server['request_uri'] ?? self::DEFAULT_PATH;
        if ($uri !== '/ws') {
            $server->disconnect($req->fd, 1008, 'Unknown path');
            return;
        }

        $this->handlers[$req->fd] = new ConversationRelayHandler(
            $server,
            $req->fd,
            $this->openAi,
            $this->memory,
            $this->log,
        );
    }

    private function onMessage(Server $server, Frame $frame): void
    {
        unset($server);
        $handler = $this->handlers[$frame->fd] ?? null;
        $handler?->onMessage($frame->data);
    }

    private function onClose(Server $server, int $fd): void
    {
        unset($server);
        $handler = $this->handlers[$fd] ?? null;
        if ($handler !== null) {
            $handler->onClose();
            unset($this->handlers[$fd]);
        }
    }
}

This sets up the websockets to call your ConversationRelayHandler, and includes the initial greeting for your user. Feel free to change the greeting according to your needs.

Now, create a file named index.php in the project's top-level directory, then paste the code below into the file.

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use App\Service\ConversationMemoryService;
use App\Service\OpenAiService;
use App\WebSocket\WebSocketServer;
use Dotenv\Dotenv;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;

// Load environment variables from .env (walking up the tree, matching DotNetEnv's Env.TraversePath()).
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
$dotenv->required([
    'DOMAIN',
    'OPENAI_API_KEY',
    'TWILIO_API_KEY',
    'TWILIO_API_SECRET',
    'TWILIO_MEMORY_STORE_ID',
]);

$logger = new Logger('conversation-relay');
$logger->pushProcessor(new PsrLogMessageProcessor(removeUsedContextFields: true));
$logger->pushHandler(new StreamHandler('php://stderr', Level::Info));

$openAi = new OpenAiService($logger);
$memory = new ConversationMemoryService($logger);

$host = $_ENV['LISTEN_HOST'] ?? WebSocketServer::DEFAULT_DOMAIN;
$port = (int) ($_ENV['LISTEN_PORT'] ?? WebSocketServer::DEFAULT_PORT);

new WebSocketServer($openAi, $memory, $logger, $host, $port);

This initialises the WebSocketServer, starting it listening on port 9501 on the local machine (or the port that you set in .env, if you changed the default value.

Testing, troubleshooting, or product demonstration

It is now time to test your voice application. Save your files and run the project with:

php index.php

Once your webhook is running, you will need to expose it to the internet by using ngrok or another tunneling service.

ngrok http localhost:9501

Replace 9501 with whatever port your application is running on if you have a different port shown.

Now ngrok will provide you with a url for utilizing in your Twilio console. Go into your Twilio console and find the Twilio phone number that you prepared. Under the option A Call Comes In, choose Webhook, and fill in your ngrok URL followed by /voice, as shown in the graphic below:

configuration for webhooks in the Twilio console
configuration for webhooks in the Twilio console

To put your AI to the test, you'll have to make two phone calls and check the conversation memory.

  1. Make call #1: Talk to the agent about a car repair issue, including some details such as make and model.
  2. Hang up and make call #2 from the same phone number.
  3. Verify the agent greets you and remembers the details of your first call without any prompting.

Conclusion

Today you have learned how Twilio Conversation Memory simplifies maintaining state across separate voice calls in PHP. This should provide value to any phone AI agent, storing information that keeps conversations feeling more convenient and human.

Do you want to do more with Twilio Conversations? Explore the possibilities by checking out the conversations documentation, where you can find blueprints for Conversational Agents, AI-to-Human handoff, and more.

Matthew Setter is a PHP, Go, and Rust Editor in the Twilio Voices team. He’s also the author of Mezzio Essentials and Deploy with Docker Compose. You can find him at msetter@twilio.com. He's also on LinkedIn and GitHub.