Anatomy of a Turn
The model has no memory of your chat. Whatever it knows about the conversation arrives inside the request you are sending right now — so the messages array is not a log of the past, it is the model's entire present. Three roles fill it, and each one does a different job:
- system — standing rules that apply to every turn: who the assistant is, what format to answer in, what to refuse.
- user — inputs and questions, the turns that drive the conversation forward.
- assistant — what the model said before, re-supplied so it does not contradict itself.
Because nothing persists server-side between calls, the array grows on every exchange:
call 1 call 2 call 3
----- ----- -----
[system, [system, [system,
user: "2+2?" ----> user: "2+2?", ----> user: "2+2?",
assistant: "4"] assistant: "4", assistant: "4",
user: "times 3?"] assistant: "4",
user: "times 3?",
assistant: "12"]
The code that sends call 2:
from openai import OpenAI
client = OpenAI()
history = [
{"role": "system", "content": "Answer with a single number, no prose."},
{"role": "user", "content": "What is 2 + 2?"},
{"role": "assistant", "content": "4"},
{"role": "user", "content": "Now times 3?"},
]
reply = client.chat.completions.create(model="gpt-4o-mini", messages=history)
print(reply.choices[0].message.content)
Input: the four-turn array above.
Output: 12
Two consequences follow from the array being the whole world. First, cost: turn 50 pays for turns 1 through 49 on every request, which is why history size — not answer length — dominates the bill in long chats. Second, control: whatever sits in the array is what the model conditions on, so a stale or contradictory assistant turn produces a stale or contradictory answer. The system entry is the durable part; keeping it pinned while older dialogue expires is the job of Conversation History. The full request/response contract this array plugs into is covered in Your First LLM API Call.
Assistant Turns You Write Yourself
A classifier that answers "Certainly! The ticket appears to be about billing..." when your parser expects billing is not misbehaving — it was never told what an answer looks like. The array gives you a lever for that: an assistant turn you authored yourself. It is not history, it is conditioning.
messages = [
{"role": "system", "content": "Classify support tickets."},
{"role": "user", "content": "Ticket: the app crashes on export"},
{"role": "assistant", "content": "bug"}, # example of the format
{"role": "user", "content": "Ticket: charged twice for invoice 88"},
]
reply = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0,
)
print(reply.choices[0].message.content)
Input: a billing ticket after a bug example turn.
Output: billing
Written assistant turns do two different work. Side by side with a user turn they are a few-shot example — the model pattern-matches the pair and continues the pattern. Placed to demonstrate a schema, they are a format constraint without any parsing machinery. The wider pattern lives in Few-Shot Examples.
The turn order has to survive the endpoint you are talking to:
- Anthropic requires strict alternation, starting with
user. Two user turns in a row return400: roles must alternate. End the array with anassistantturn and generation continues from it — a prefill that pins the first words of the reply. - OpenAI's chat endpoint accepts consecutive same-role turns. Stacked user messages are merged in practice, which keeps a multi-paragraph paste working without ceremony.
When a client appends two messages before sending — a pasted document followed by the actual question — the portable fix is one short assistant placeholder between them:
turns = [{"role": "user", "content": document},
{"role": "assistant", "content": "Understood."},
{"role": "user", "content": question}]
That single acknowledgement restores alternation on strict endpoints and, on lenient ones, reads as the model agreeing to the context — behavior you can verify in Zero-Shot Instructions by removing it and comparing answers.
Deciding What the Model Still Sees
At some point the array stops fitting — a 300k-token context is a ceiling, not a suggestion — and something has to go. The choice is a product decision disguised as an engineering one, because trimming old turns changes model behavior the moment you do it: an instruction you dropped is an instruction the model no longer follows.
Three retention strategies, with what each actually costs:
| Strategy | Keeps | Cost profile | What breaks |
|---|---|---|---|
| Drop oldest turns | System prompt + last N turns | Linear in window size | Early constraints vanish; the model forgets decisions it made |
| Summarize the head | System prompt + rolling summary + recent turns | One extra call per trim | Summaries lose exact values: names, ids, numbers |
| Retrieve turns on demand | System prompt + turns matching the current query | Search infrastructure | Conversation loses continuity; tone drifts between windows |
A minimal drop-oldest policy that stays valid:
def fit(history: list[dict], budget: int = 4000) -> list[dict]:
system = history[0] if history[0]["role"] == "system" else None
turns = history[1:] if system else history
kept = [system] if system else []
total = len(system["content"]) // 4 if system else 0 # ~4 chars/token
while turns and total + len(turns[0]["content"]) // 4 > budget:
turns.pop(0) # drop oldest turn first
if turns and turns[0]["role"] != "user":
turns.pop(0) # a strict endpoint needs user first
return kept + turns
Input: 40 turns of history with a 4,000-token budget.
Output: system prompt + the trailing window, opening on a user turn.
Two lines carry the policy. Dropping whole turns keeps role alternation intact — trimming the array to an assistant-first sequence is how a trimmed conversation starts failing with roles must alternate on strict endpoints. And the system entry never enters the loop: standing rules survive every trim, because a chatbot that forgets its safety instructions after 30 turns is worse than one that forgets what the user said in turn 3.
Nothing here depends on the provider — the array format is the shared substrate, so the same policy ports between the endpoints in OpenAI SDK Essentials and Anthropic SDK Essentials. Once the array is well-formed, its other input knob — how stochastic the next turn sounds — is Sampling Parameters and Determinism.
Quiz
1. Why must a client re-send previous assistant turns on every API call?
2. A strict endpoint rejects your request with 400: roles must alternate, because a pasted document and a question arrived as two user turns. What is the portable fix?
3. Your classifier keeps replying with a full sentence when the parser needs exactly billing, bug, or question. Which change addresses the cause?
4. You trim old turns to fit the context window. After trimming, what must the array look like for a strict endpoint?
Flashcards
Question
What does each role do in a messages array?
Click to reveal answer
Answer
system: standing rules for every turn. user: inputs that drive the dialogue. assistant: prior outputs, re-supplied so the model conditions on them instead of contradicting them.
Question
Why does the whole conversation have to be re-sent every call?
Click to reveal answer
Answer
Endpoints are stateless — the array is the model's only memory. It is also why input cost grows linearly with conversation length.
Question
What is an authored assistant turn used for?
Click to reveal answer
Answer
Conditioning, not history: a few-shot example beside a user turn, or a format demonstration that makes the model continue in the pattern you wrote.
Question
How do you fix two consecutive user turns for a strict endpoint?
Click to reveal answer
Answer
Insert a one-line assistant acknowledgement between them. It restores alternation where required and is harmless where same-role turns are tolerated.
Question
What must a trimmed history preserve?
Click to reveal answer
Answer
The system prompt outside the array, whole user/assistant pairs, and a user turn first — dropping old turns changes behavior, so the retention policy is a product decision.
Revision Notes
Key Takeaways
- 1. The messages array is the model's only memory — statelessness is why you resend history and why cost grows per turn
- 2. system sets standing rules; user drives dialogue; assistant turns you write are conditioning that shapes format and content
- 3. Turn order is a contract: alternation on strict endpoints, placeholders between stacked user messages, prefill to pin a reply's start
- 4. Trimming is a behavior change, not a memory optimization — retention policy decides what the model is still allowed to know
Interview Tips
- • Draw the growing array across three calls; interviewers check that you understand statelessness before asking about cost
- • Have a ready answer for 400 roles must alternate: placeholder assistant turn, why it works on both providers
- • Explain the difference between a system instruction and a few-shot assistant turn — one sets policy, one demonstrates output
- • Be able to defend a retention policy: what you pin, what you drop first, and what behavior changes when early turns disappear
Cheat Sheet
Messages, Roles, and Turn Structure — Cheat Sheet
The rule behind everything
Stateless endpoint: the array you send is the model's entire world this call. Re-send it all, every time.
Roles
| Role | Lives | Job |
|---|---|---|
| system | Top level (Anthropic) or first message (OpenAI) | Standing rules: persona, format, refusals |
| user | messages[] | Inputs and questions |
| assistant | messages[] | Prior outputs — conditioning, not decoration |
Multi-turn growth
call 1: [sys, u1]
call 2: [sys, u1, a1, u2]
call 3: [sys, u1, a1, u2, a2, u3]
Turn-order rules
- Anthropic: strict alternation, first turn user, last turn user — or an assistant prefill to continue generation from your words
- OpenAI: consecutive same-role turns tolerated
- Portable fix for stacked user messages:
[user, assistant("Understood."), user]
Authored assistant turns
{"role": "user", "content": "Ticket: app crashes on export"},
{"role": "assistant", "content": "bug"}, # format demonstration
{"role": "user", "content": "Ticket: charged twice"},
→ output: billing (pattern continued, not requested)
Retention policy checklist
- Pin the system prompt — it never enters the drop loop
- Drop whole turns, never half a pair
- After trimming the head, ensure the first surviving turn is user
- Decide summarization vs dropping deliberately: trimming changes behavior
Continue Learning
Before You Start
- No prerequisites listed.