Knaph

A Conversation5 of 375 min read

Keeping the History

Leila runs her program, asks "What's a good name for a report about Q3 sales?", and likes the answer. She changes the question to "Make it shorter" and runs it again. The model has no idea what "it" is. Same model, same program, a few seconds apart.


Leila I know why, in theory. The model keeps nothing between requests. But then who keeps the conversation, in a chat app?

Omid The app does. So in our program, who's the app?

Leila We are. So we keep a list of everything said, and send the whole list each time.

Omid Right. Now the design question: after a reply comes back, what exactly do you add to the list?

Leila The answer's text, I'd think. That's what was said.

Omid What about the thinking block, and later the tool calls?

Leila Do they matter? The user never sees them.

Omid The API does. When the model asks for a tool, the next request must contain that exact request, with its id, followed by the result. And thinking blocks are tied to the conversation that produced them. If you keep only the text, you've rewritten what the model said.

Leila So I add the reply's content as it came, blocks and all. Not my summary of it.

Omid Exactly. The list is a record of the conversation, not a transcript for people to read.


The messages list

The messages argument is the conversation so far, as a list. Each message is a dict with a role, "user" or "assistant", and a content, which is a string or a list of content blocks. Your program keeps the list, adds to it on every turn, and sends all of it with every request.

The loop of a chat program: you type a line, it is added as a user message, the whole list is sent, the reply is added as an assistant message, its text is printed, and round again.

Here is a whole chat program:

import anthropic
 
MODEL = "claude-opus-5-5"
client = anthropic.Anthropic()
messages = []
 
while True:
    text = input("you: ")
    if text in ("", "quit"):
        break
    messages.append({"role": "user", "content": text})
    reply = client.messages.create(model=MODEL, max_tokens=2000, messages=messages)
    messages.append({"role": "assistant", "content": reply.content})
    print("claude:", "".join(b.text for b in reply.content if b.type == "text"))
Python

What to notice:

  • Two appends per turn: your line as a user message before the request, and the reply as an assistant message after it.
  • reply.content goes in as it came, a list of blocks, thinking included. The SDK accepts its own block objects back, so there's nothing to convert.
  • The request sends messages, all of it. Nothing else carries the earlier turns. If you sent only the new line, the model would be back to knowing nothing.

Three requests in one conversation. Request 1 holds one message. Request 2 holds that message, the model's reply and a new message. Request 3 holds all of those and two more. The messages list is sent whole every time.

The shape of the list

The list has a shape, and mini keeps to it strictly:

  • It starts with a user message. The model always answers something.
  • Roles take turns: user, assistant, user, assistant. Each user message is answered by one assistant message.
  • It ends with a user message when you send it, since that is what the model is to answer.
  • A tool call is answered in the very next user message, by a result with the same id. (You'll write that part in the module on tools.)

A list that breaks this shape is a bug in the harness, and it doesn't always fail at once: a tool call without its result is refused by the API, but a reply appended twice may just confuse the model. In Check the History you'll write the check mini can run before each request to catch these where they happen.

The system prompt, the instructions that come before everything, isn't in this list at all. It has its own argument, in the next lesson.

The list is the bill, too

Every request sends the whole list, so the input grows with every turn: the tenth request carries the nine turns before it. You can watch it happen: print reply.usage.input_tokens after each turn and it climbs, turn by turn, like a staircase. That's the same growth that makes long chats and agent sessions expensive, now in code you wrote: the token cost of a conversation is the sum of every request's input, not just the last one.

For a chat that's fine for a long while. For an agent, whose turns carry whole files and command outputs, the list grows much faster, and later in the course you'll give mini two ways to keep it in check: caching the part that repeats, and compacting the history when it gets too big.


Leila So "starting a new chat" is just messages = [].

Omid That's all it ever was. And "editing an earlier message" in a chat app is cutting the list at that point and adding a different message.

Leila It's strange how much of what I thought the model did is just a Python list.

Omid That's most of this course, really.


Try it

With a key, run the chat program and tell it your name, then ask what your name is. Then add print(len(messages), reply.usage.input_tokens) after each reply and chat for five turns, watching both numbers grow. Finally, change the request to send messages[-1:] (only the newest message) and ask again: it no longer knows. Without a key, write out by hand the list that would be sent on the third turn of a conversation.

Using it well

  1. Append the whole reply content, not just its text. Thinking and tool-call blocks have to go back exactly as they came, or later requests fail or lose their meaning.
  2. Keep the list's shape strict. User first, roles alternating, ending with the user: a list that always has this shape is one you can check, and bugs show up where they're made.
  3. Watch input_tokens grow. It's the clearest sign of when a conversation has become expensive, and the number to act on later.
  4. Start a fresh list for a new task. An unrelated earlier conversation costs tokens on every request and gives the model things to be confused by.

Words to know

  • messages list - the conversation so far, as a list of messages, which the program keeps and sends in full with every request
  • role - who a message is from: "user" (you, and later the results of tools) or "assistant" (the model)

Big idea

The model keeps nothing, so your program keeps the conversation: a list it adds to on every turn and sends whole with every request. Append the replies exactly as they came, keep the list's shape strict, and remember that every message in it is paid for again on every turn.

Test your understanding

Sign in to test your understanding of this lesson and track your progress.