Continue a Conversation
What Continuing Means
Lesson 24 showed how to send history so Claude remembers earlier turns. Continuing a conversation builds on that. It is the set of techniques for picking up an existing conversation and moving it forward correctly in different situations:
| Situation | What "continue" means |
|---|---|
| The user sends another message | Add the next turn to the history |
| The user returns later, in a new session or request | Load the stored history and add to it |
| The last answer was cut off | Ask Claude to continue its own answer |
A server-side tool paused (pause_turn) | Send the paused response back unchanged |
| The last request was refused | Roll back, then continue from a clean state |
| The user wants a different answer or to edit their question | Regenerate or replace the last turn |
All of these come down to one principle: the next request is the stored history, adjusted as needed, plus a valid final user turn.
Continuing with the Next Message
The everyday case: append the new user message to the existing history and send it:
history.append({"role": "user", "content": "Can you show an example?"})
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=history,
)
history.append({"role": "assistant", "content": reply.to_dict()["content"]})
Nothing more is required. Claude sees all previous turns and answers in context. For example, "an example" refers to whatever was discussed last.
Resuming a Saved Conversation
When a user returns later, the conversation must be loaded from storage before continuing. Because the API is stateless, a resumed conversation works exactly like one that never stopped, as long as the stored history is complete and valid.
import json
from pathlib import Path
history = json.loads(Path("priya_chat.json").read_text(encoding="utf-8"))
history.append({"role": "user", "content": "Where did we leave off?"})
reply = client.messages.create(model="claude-opus-5-5", max_tokens=2048, messages=history)
Resuming in a Web Application (Django Example)
In a web application, each HTTP request is independent, so the history lives in a database. A Django model with a JSONField stores it naturally:
# models.py
from django.db import models
class Conversation(models.Model):
owner = models.ForeignKey("auth.User", on_delete=models.CASCADE)
history = models.JSONField(default=list)
updated_at = models.DateTimeField(auto_now=True)
# views.py
import anthropic
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from .claude import client # One shared Anthropic client (Lesson 10)
from .models import Conversation
@login_required
@require_POST
def continue_chat(request, conversation_id):
conversation = get_object_or_404(Conversation, id=conversation_id, owner=request.user)
user_text = request.POST.get("message", "").strip()
if not user_text:
return JsonResponse({"error": "Message is empty."}, status=400)
pending = conversation.history + [{"role": "user", "content": user_text}]
try:
reply = client.messages.create(model="claude-opus-5-5", max_tokens=2048, messages=pending)
except anthropic.APIError:
return JsonResponse({"error": "The assistant is unavailable. Please try again."}, status=502)
if reply.stop_reason == "refusal":
return JsonResponse({"reply": "I can't help with that request."})
conversation.history = pending + [{"role": "assistant", "content": reply.to_dict()["content"]}]
conversation.save()
text = "".join(block.text for block in reply.content if block.type == "text")
return JsonResponse({"reply": text})
How it works:
get_object_or_404(..., owner=request.user)loads the conversation only if it belongs to the logged-in user. This prevents one user from continuing, or reading, another user's conversation by guessing an ID.- The browser sends only text, never roles or history. The server builds the message list (Lesson 23).
pendingcombines the stored history with the new message. The database is updated only after a successful, non-refused reply, so failures never leave half-finished turns.JSONFieldstores the history list, including the content-block dictionaries fromto_dict(), without manual serialization.
Continuing a Truncated Answer
If an answer stops with stop_reason == "max_tokens", you can ask Claude to continue it. Within a conversation, add the partial answer as an assistant turn, then a user message asking it to continue. The request must end with a user message (Lesson 23):
partial = "".join(block.text for block in reply.content if block.type == "text")
continuation_messages = history + [
{"role": "assistant", "content": partial},
{"role": "user", "content": "Please continue from where you left off."},
]
more = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
messages=continuation_messages,
)
full_answer = partial + "".join(block.text for block in more.content if block.type == "text")
Here history ends with the user message that produced the truncated answer.
What to store afterward: for a clean history, store one assistant turn containing the combined full_answer, rather than the partial answer, the "please continue" request, and the second part. The conversation then reads as one complete reply.
Note: If answers are often truncated, raise
max_tokensand use streaming (Lessons 51–60). Continuation is a recovery technique, not a substitute for adequate limits.
Continuing After pause_turn
When server-side tools such as web search pause (Lesson 20), continue by sending the paused response back unchanged, with the same tools:
if reply.stop_reason == "pause_turn":
history.append({"role": "assistant", "content": reply.content})
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
messages=history,
tools=tools,
)
This is the one case where the request ends with an assistant message: the API continues the paused turn rather than starting a new one.
Continuing After a Refusal
When a request is refused (stop_reason == "refusal"), continuing to build on that exchange can affect later turns. The guidance in Anthropic's documentation on refusals is to reset the conversation context when a refusal occurs.
Two practical options:
| Option | How | When |
|---|---|---|
| Roll back the turn | Do not store the refused user message or the response; continue from the previous state | The rest of the conversation is fine |
| Start a new conversation | Clear the history, keeping only what is needed, such as the system prompt | Repeated refusals, or a conversation that has gone off track |
The Conversation class in Lesson 24 already implements rollback, by committing a turn only after a usable reply.
Regenerating and Editing Turns
Because your application owns the history, it can change it before continuing:
Regenerate the Last Answer
Remove the last assistant turn and send the same history again:
def regenerate(history: list[dict]) -> list[dict]:
if history and history[-1]["role"] == "assistant":
return history[:-1] # Ends with the last user message again
return history
Sending the result produces a new answer to the same question.
Edit the Last User Message
Remove the last exchange, then add the corrected question:
def edit_last_question(history: list[dict], new_text: str) -> list[dict]:
trimmed = history[:]
if trimmed and trimmed[-1]["role"] == "assistant":
trimmed = trimmed[:-1]
if trimmed and trimmed[-1]["role"] == "user":
trimmed = trimmed[:-1]
return trimmed + [{"role": "user", "content": new_text}]
Branch a Conversation
Copy the history to explore two directions independently:
import copy
branch_a = copy.deepcopy(history) + [{"role": "user", "content": "Explain it with Flask instead."}]
branch_b = copy.deepcopy(history) + [{"role": "user", "content": "Explain it with FastAPI instead."}]
copy.deepcopy creates fully independent copies, including the nested content dictionaries, so changes to one branch can never affect the other.
Practical Example: A Resumable Command-Line Chat
This script continues a saved conversation across runs and supports commands to regenerate, undo, and reset.
"""resume_chat.py: a command-line chat that continues where you left off.
Commands: /retry regenerate the last answer
/undo remove the last exchange
/reset start a new conversation
/quit save and exit
"""
import json
from pathlib import Path
import anthropic
from anthropic import Anthropic
HISTORY_FILE = Path("chat_history.json")
MODEL = "claude-opus-5-5"
SYSTEM = "You are a helpful Python tutor. Keep answers under 120 words."
client = Anthropic()
def load() -> list[dict]:
if HISTORY_FILE.exists():
return json.loads(HISTORY_FILE.read_text(encoding="utf-8"))
return []
def save(history: list[dict]) -> None:
HISTORY_FILE.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
def ask(messages: list[dict]) -> tuple[str, list[dict] | None]:
"""Return (text to show, assistant content to store or None if not storable)."""
try:
reply = client.messages.create(model=MODEL, max_tokens=2048, system=SYSTEM, messages=messages)
except anthropic.APIError as error:
return f"[Request failed: {error}]", None
if reply.stop_reason == "refusal":
return "[Claude declined this request. Please rephrase.]", None
text = "".join(block.text for block in reply.content if block.type == "text")
return text, reply.to_dict()["content"]
def main() -> None:
history = load()
turns = sum(1 for message in history if message["role"] == "user")
print(f"Resumed conversation with {turns} earlier question(s)." if history else "New conversation.")
while True:
user_text = input("\nYou: ").strip()
if not user_text:
continue
if user_text == "/quit":
break
if user_text == "/reset":
history = []
print("Conversation cleared.")
continue
if user_text == "/undo":
history = history[:-2] if len(history) >= 2 else []
print("Last exchange removed.")
continue
if user_text == "/retry":
if len(history) < 2:
print("Nothing to retry.")
continue
pending = history[:-1] # Drop the last answer
else:
pending = history + [{"role": "user", "content": user_text}]
text, content = ask(pending)
print(f"Claude: {text}")
if content is not None:
history = pending + [{"role": "assistant", "content": content}]
save(history)
print(f"Saved {len(history)} messages to {HISTORY_FILE}.")
if __name__ == "__main__":
main()
Example session (two runs):
$ python resume_chat.py
New conversation.
You: I'm learning about Python decorators.
Claude: Great topic! A decorator wraps a function to add behavior...
You: /quit
Saved 2 messages to chat_history.json.
$ python resume_chat.py
Resumed conversation with 1 earlier question(s).
You: Can you show a simple example?
Claude: Sure! Here's a decorator that prints when a function runs: ...
How It Works
load()andsave()persist the history between runs. Because the API is stateless, loading the file is all it takes to "remember" the earlier session.ask()returns both the text to display and the content to store. It returnsNonefor content when the request failed or was refused, so those exchanges are never stored. That is rollback by design./retrysendshistory[:-1], the history without the last answer, which ends with the last user message. That produces a fresh answer, which replaces the old one./undoremoves the last user and assistant pair withhistory[:-2]./resetclears the history and starts over, which is the recommended move after repeated refusals or when the conversation has gone off track.pendingis always a new list, andhistoryis updated only after a storable reply. The saved file therefore always contains a valid, alternating conversation.
Common Mistakes
Continuing with a Corrupted History
What happens: a stored history that ends with an unanswered user message, or has two user turns in a row from a past failure, causes confusing replies or 400 errors.
Fix: commit turns only after successful replies. Validate loaded histories (Lesson 23) before continuing.
Ending a Continuation Request with an Assistant Message
What happens: sending history + [{"role": "assistant", "content": partial}] without a following user message returns a 400 error, because prefill is not supported.
Fix: add a user message such as "Please continue from where you left off." The exception is pause_turn, where the paused response itself is sent back.
Storing the Continuation Scaffolding
What happens: the stored history contains a partial answer, a "please continue" message, and the rest, which clutters the conversation and wastes tokens.
Fix: combine the parts into one assistant turn before storing.
Loading Another User's Conversation
What happens: a web endpoint loads conversations by ID alone, so users can access each other's chats.
Fix: always filter by owner, as in the Django example.
Sharing History Lists Between Branches
What happens: two "branches" made with branch = history are the same list, so changes to one appear in the other.
Fix: use copy.deepcopy(history) for independent branches.
Best Practices
- Treat the stored history as the source of truth for continuing conversations.
- Commit only complete, usable turns: skip failures and refusals, and merge continuations.
- Validate histories loaded from storage before continuing them.
- Scope stored conversations to their owners in multi-user applications.
- Offer regenerate, undo, and reset in chat interfaces. They are simple history operations.
- Reset context after refusals, as Anthropic's guidance recommends.
- Prefer adequate
max_tokensand streaming over frequent continuation.
Exercise
- Run
resume_chat.py, ask two questions, quit, and run it again. Ask "What did I ask you first?" Does Claude know? - Add a
/historycommand that prints each user question in the stored conversation, numbered. - Add a
/continuecommand that asks Claude to continue its last answer, then replaces the last assistant turn with the combined text. - In the Django view, explain what would go wrong if the filter
owner=request.userwere removed.
Step 2:
if user_text == "/history":
questions = [m["content"] for m in history if m["role"] == "user"]
for number, question in enumerate(questions, start=1):
print(f"{number}. {question}")
continue
Step 3 (core logic):
if user_text == "/continue":
if len(history) < 2 or history[-1]["role"] != "assistant":
print("Nothing to continue.")
continue
last_text = "".join(b["text"] for b in history[-1]["content"] if b.get("type") == "text")
pending = history + [{"role": "user", "content": "Please continue from where you left off."}]
more_text, content = ask(pending)
if content is not None:
combined = last_text + more_text
history = history[:-1] + [{"role": "assistant", "content": combined}]
print(f"Claude: {more_text}")
continue
The stored content is a list of block dictionaries, so b["text"] reads text blocks by key. The combined text then replaces the last assistant turn, which keeps the history clean.
Step 4: without the owner filter, any logged-in user could send requests with someone else's conversation_id. They could then continue that conversation, reading its context through Claude's replies and adding messages to it. This is a classic insecure direct object reference (IDOR) vulnerability.