Continue a Conversation

Su Suriya Ravichandran Updated 10 Nov 2026
11 min read ·Lesson 25 of 200

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:

SituationWhat "continue" means
The user sends another messageAdd the next turn to the history
The user returns later, in a new session or requestLoad the stored history and add to it
The last answer was cut offAsk Claude to continue its own answer
A server-side tool paused (pause_turn)Send the paused response back unchanged
The last request was refusedRoll back, then continue from a clean state
The user wants a different answer or to edit their questionRegenerate 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).
  • pending combines 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.
  • JSONField stores the history list, including the content-block dictionaries from to_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_tokens and 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:

OptionHowWhen
Roll back the turnDo not store the refused user message or the response; continue from the previous stateThe rest of the conversation is fine
Start a new conversationClear the history, keeping only what is needed, such as the system promptRepeated 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

  1. load() and save() persist the history between runs. Because the API is stateless, loading the file is all it takes to "remember" the earlier session.
  2. ask() returns both the text to display and the content to store. It returns None for content when the request failed or was refused, so those exchanges are never stored. That is rollback by design.
  3. /retry sends history[:-1], the history without the last answer, which ends with the last user message. That produces a fresh answer, which replaces the old one.
  4. /undo removes the last user and assistant pair with history[:-2].
  5. /reset clears the history and starts over, which is the recommended move after repeated refusals or when the conversation has gone off track.
  6. pending is always a new list, and history is 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_tokens and streaming over frequent continuation.

Exercise

  1. Run resume_chat.py, ask two questions, quit, and run it again. Ask "What did I ask you first?" Does Claude know?
  2. Add a /history command that prints each user question in the stored conversation, numbered.
  3. Add a /continue command that asks Claude to continue its last answer, then replaces the last assistant turn with the combined text.
  4. In the Django view, explain what would go wrong if the filter owner=request.user were removed.
Show answers for steps 2–4

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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Continue a Conversation and get answers drawn from it.

Signed-in readers only.