Skip to chapter content

Chapter 16 3 min read

Multi-Turn Conversations

Why LLMs are stateless and how history fixes that, shown as a before/after comparison

Every example in this book so far has had the memory of a goldfish. Each generate_text() call is completely independent of the last, the model has no idea what you asked five minutes ago, or five seconds ago. Ask a follow-up question expecting it to remember earlier context, and it won’t, because nothing was ever telling it what “earlier” even was.

Why this happens

An AI model doesn’t have a persistent memory of your site, your account, or your last request. Every call starts from nothing. What feels like a “conversation” in a tool like ChatGPT is really the whole conversation so far getting resent with each new message, the illusion of memory is built by the application, not the model. with_history() is how you build that same illusion here.

Handing over what’s already been said

with_history() takes a list of prior messages, each one tagged with who said it:

use WordPress\AiClient\Messages\DTO\UserMessage;
use WordPress\AiClient\Messages\DTO\ModelMessage;
use WordPress\AiClient\Messages\DTO\MessagePart;

$history = array(
    new UserMessage( array( new MessagePart( 'My name is Ada.' ) ) ),
    new ModelMessage( array( new MessagePart( 'Nice to meet you, Ada.' ) ) ),
);

$text = wp_ai_client_prompt( 'What did I say my name was?' )
    ->with_history( ...$history )
    ->generate_text();

MessagePart should look familiar, it’s the same class Chapter 14 used to read a mixed response apart. This time you’re building one instead of reading one, wrapping a plain string in new MessagePart( '...' ). UserMessage and ModelMessage are what carry the “who said this” part, one for your side of the conversation, one for the model’s previous replies. with_history() takes them as separate arguments, not an array, which is why the example spreads the $history array with ...$history when passing it in.

Seeing the difference it makes

Create chapter-16-multi-turn-conversations.php inside includes:

<?php
/**
* Chapter 16: Multi-Turn Conversations
* Usage: add [ai_course_ch16] to any page or post to see the output.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // No direct access.
}
use WordPress\AiClient\Messages\DTO\UserMessage;
use WordPress\AiClient\Messages\DTO\ModelMessage;
use WordPress\AiClient\Messages\DTO\MessagePart;
function ai_course_ch16_multi_turn() {
$follow_up = 'Would this theme work well for a photography portfolio site?';
$without_history = wp_ai_client_prompt( $follow_up )->generate_text();
if ( is_wp_error( $without_history ) ) {
return 'Could not generate the first example: ' . esc_html( $without_history->get_error_message() );
}
$history = array(
new UserMessage( array( new MessagePart( 'My site is running WordPress 7.1 with the Twenty Twenty-Five theme.' ) ) ),
new ModelMessage( array( new MessagePart( 'Got it, WordPress 7.1 with Twenty Twenty-Five. What can I help you with?' ) ) ),
);
$with_history = wp_ai_client_prompt( $follow_up )
->with_history( ...$history )
->generate_text();
if ( is_wp_error( $with_history ) ) {
return 'Could not generate the second example: ' . esc_html( $with_history->get_error_message() );
}
$output = '<p><strong>Without history:</strong><br>' . wp_kses_post( $without_history ) . '</p>';
$output .= '<p><strong>With history:</strong><br>' . wp_kses_post( $with_history ) . '</p>';
return $output;
}
add_shortcode( 'ai_course_ch16', 'ai_course_ch16_multi_turn' );

Notice both calls send the exact same question, “Would this theme work well for a photography portfolio site?” Nothing in that sentence says which theme. Add [ai_course_ch16] to a page and load it. The first answer is vague or asks you to clarify, since as far as that call is concerned, no theme was ever mentioned. The second answer talks specifically about Twenty Twenty-Five, because the fake two-message history handed it exactly that context right before the real question.

This is a demo, not a real chat feature yet

The history here is hardcoded, written once, the same two messages every time the shortcode runs. A real chat feature needs that history to actually grow, each new question and answer getting added to the list and carried into the next call. That means storing it somewhere between requests, and calling this from the frontend with an actual back-and-forth UI, neither of which this chapter covers. Module 7 builds a real multi-turn chatbot widget using exactly this method, once REST endpoints (Module 5) and frontend integration are in place. For now, the goal is just understanding what with_history() does and why it’s needed, the storage and UI problems come later.

Try it yourself

Change the fake history in $history to a different scenario, a customer support exchange, a series of facts about a project, anything with a follow-up question that only makes sense once you know what came before. Confirm the “without history” version genuinely can’t answer it properly, and the “with history” version can.

This book is created with Chapterwright