Module 1: LLM API Mastery · Lesson 2 of 7 · 35 min

Messages Are the Only State

The single most important fact in agent engineering: the model is stateless. A 'conversation' is you resending an ever-growing array. Every agent pattern you'll ever build follows from this.

When you chat with Claude or ChatGPT, it feels like the model remembers you. It doesn't. Every single API call is a blank slate. The provider's server receives your request, runs the model over the tokens you sent, returns a completion, and forgets you existed. What creates the illusion of memory is that your code resends the entire conversation history on every call.

the entire illusion of conversation
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

messages = []  # <- this list IS the conversation. You own it.

def chat(user_text: str) -> str:
    messages.append({"role": "user", "content": user_text})
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        system="You are a concise engineering assistant.",
        messages=messages,        # full history, every single time
    )
    reply = response.content[0].text
    messages.append({"role": "assistant", "content": reply})
    return reply

print(chat("My name is Wenming."))
print(chat("What's my name?"))   # works ONLY because we resent turn 1
Comment out the second messages.append and the model instantly 'forgets' — because memory never lived on the server. The system prompt rides along outside the array in Anthropic's API; in OpenAI's it's the first message with role: "system" (or "developer" in newer APIs).

It's just JSON over POST

The SDK is a thin convenience wrapper. Strip it away and every call in this course is one HTTPS POST with an auth header and a JSON body. Seeing it raw once makes everything else less magical — and it's what you'd write from a language with no official SDK.

the same call, no SDK
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "max_tokens": 1024,
    "system": "You are a concise engineering assistant.",
    "messages": [{"role": "user", "content": "My name is Wenming."}]
  }'
The response is JSON too: a list of content blocks, a stop_reason, and a usage object with token counts. Everything the SDK gives you — retries, types, streaming helpers — is built on this one endpoint.

What the server actually does with your POST

When your request lands, the server tokenizes the entire prompt and runs prefill — one massively parallel pass over all input tokens that builds the model's internal working state (the KV cache). Then it decodes output tokens one at a time, each step conditioned on everything before it. When the response finishes, that working state is thrown away. This is why input and output tokens are priced differently — prefill parallelizes across the GPU while decoding is inherently serial — and it's why the next call starts from zero.

Statelessness isn't laziness — it's an architectural choice that buys horizontal scale. Because no server holds your conversation, any machine in the fleet can serve your next request: no session affinity, no replicated conversation store, trivial failover. The cost is pushed onto you (resend everything), then partially refunded by prompt caching (Lesson 5), which keeps a processed prefix warm so identical prefixes skip prefill. A senior answer connects these dots out loud: stateless → resend → quadratic cost → caching as the mitigation.

Predict the output
In the Python example above, comment out the second messages.append (the one that stores the assistant's reply). What does chat("What's my name?") return now, and why?

The roles

RoleWho writes itWhat it's for
systemYou (the developer)Standing instructions: persona, rules, tool guidance. Highest-priority steering.
userThe human (or your code)The task, questions, tool results in OpenAI's flow.
assistantThe modelText replies and tool-call requests. You resend these verbatim.
tool / tool_resultYou, after executingThe output of a tool the model asked you to run. OpenAI: role: "tool"; Anthropic: a tool_result block inside a user message.
Key insight
Because the model is stateless, an 'agent' is just a program that keeps editing a message array in a loop. Adding memory, compacting context, injecting retrieved documents, resuming a crashed session — all of it is list manipulation. Master the array and the rest of this curriculum is variations on a theme.

Tokens: the currency of everything

Models don't see characters — they see tokens, subword chunks (roughly 3–4 English characters, ~¾ of a word each). Every model has a context window: the maximum tokens of input + output it can handle in one call (hundreds of thousands of tokens on frontier models — check your model's docs, these numbers change). You pay per token, input and output priced separately, and output tokens typically cost several times more.

context window (finite budget)systemtoolshistorytool resultsnew turn⚠ approaching limit → compactsystemsummary ✦recent turns← reclaimed budgetold turns are summarized; system prompt and recent turns survive verbatim
A growing conversation eats the context window; compaction reclaims budget by summarizing old turns.
1/6

Here's the trap that surprises everyone: because you resend history every turn, cost grows quadratically with conversation length. Turn 10 doesn't cost one turn's tokens — it re-processes turns 1–9 as input, plus its own. A 10-turn conversation averaging 500 tokens per turn means turn 10's call alone sends ~4,500 input tokens, and the whole session has processed ~25,000 cumulative input tokens.

count before you send
# Exact pre-send count — free, no generation. Pass the SAME system,
# messages, and tools you'll actually send, or the count will be low.
count = client.messages.count_tokens(
    model="claude-sonnet-5",
    system="You are a concise engineering assistant.",
    messages=messages,
    # tools=tools,   # tool schemas are input tokens too — include them
)
if count.input_tokens > 150_000:        # gate before spending on a big call
    messages = compact(messages)        # trim/summarize (Module 4)

# Then log real usage on EVERY response — you can't manage what you can't see.
resp = client.messages.create(
    model="claude-sonnet-5", max_tokens=256, messages=messages,
)
u = resp.usage
log.info("tokens in=%d out=%d cache_read=%d cache_write=%d",
         u.input_tokens, u.output_tokens,
         u.cache_read_input_tokens, u.cache_creation_input_tokens)
Anthropic gives you an exact pre-send count for free via count_tokens — feed it the same system, messages, and tools you're about to send. Then log usage on every call (note the separate cache_read/cache_write fields — you'll tune those in Lesson 5) and aggregate per session/user/day. Cost bugs like resending a huge document every turn hide in unlogged usage.
Predict the output
A 20-turn conversation averages 400 tokens per turn. Roughly how many input tokens does the API call at turn 20 send, and roughly how many cumulative input tokens has the whole session processed?
Spot the bug
To keep token costs down, a teammate adds naive history trimming. It works for a while, then some sessions start failing with a 400: first message must use the "user" role. Others quietly give worse answers. What are the two bugs?
python
MAX_MSGS = 10

def chat(user_text: str) -> str:
    global messages
    messages.append({"role": "user", "content": user_text})
    messages = messages[-MAX_MSGS:]        # keep the context small
    resp = client.messages.create(
        model="claude-sonnet-5", max_tokens=1024, messages=messages,
    )
    reply = resp.content[0].text
    messages.append({"role": "assistant", "content": reply})
    return reply
Interview angle
"Why do you resend the whole conversation every turn, and what does that cost?" is a classic screener. Strong answer: the model is stateless; the messages array is the only context; therefore input cost grows quadratically with turns, and the mitigations are prompt caching, truncation, and summarization. Being able to do the token math above out loud is exactly the bar.
Why this module is the screen
AI Engineer is LinkedIn's #1 fastest-growing U.S. job title for 2026, and postings in the "Agentic AI" skill cluster grew roughly 280% year over year (~90K U.S. postings). The skills those postings name — Python, OpenAI/Anthropic tool calling, structured outputs — all sit on top of this lesson's stateless message loop, which is why interviewers screen it first: everything else in the role is built on it.

Whiteboard drills

Answer each out loud before revealing — in the real loop you'll be talking while writing. The answers below are pitched at the senior bar and end with the follow-up probe an interviewer would fire next.

Check yourself
Drill: Your agent's process crashes mid-conversation and the user reconnects to a different server. Design session resume.
Check yourself
Drill: A 50-turn support conversation averages ~300 tokens per turn, at $3/MTok input and $15/MTok output. Ballpark the session cost out loud — and name the single change that cuts it most.
Check yourself
Drill: Where does the system prompt actually live in the request, and what does that imply for caching and steering?
Key takeaways
  • The model is stateless; conversation = resending the array. Your code owns all state.
  • Roles: system (rules), user (task + tool results), assistant (replies + tool calls).
  • Cost grows quadratically with turns because history is re-sent as input every call.
  • Log usage from every response; count tokens before sending big payloads.
  • When history exceeds the window: truncate oldest turns, or summarize them (Module 4 goes deep).