Introduction: From One-Shot to Conversations

Welcome to the next step of your mastery journey! You learned how to enable built-in tools that transform your agent from a text responder into an active assistant capable of searching the web, reading and writing files, and executing commands. However, all your interactions so far have used the query() function, which creates a fresh session for each request. This stateless approach works perfectly for one-off tasks, but it has a significant limitation — the agent has no memory of previous interactions.

In this lesson, you'll learn how to move beyond single queries to multi-turn conversations by using ClaudeSDKClient. This session-based client maintains context across multiple interactions, allowing you to have back-and-forth conversations in which the agent remembers what you discussed earlier. You'll learn how to create and manage a client, control its lifecycle with connection patterns, conduct multi-turn conversations, and observe how context preservation enables sophisticated workflows that build on previous work.

Why ClaudeSDKClient: Stateful vs Stateless

The query() function you've been using creates a new session for every call, which means each interaction is completely independent. When you call query() with a prompt, the SDK starts a fresh agent session, processes your request, returns the response, and then discards all context. This stateless behavior is simple and efficient for one-off tasks, but it prevents the agent from building on previous work or answering follow-up questions.

ClaudeSDKClient takes a different approach by maintaining a persistent session across multiple interactions. When you create a client and send queries through it, the agent remembers everything from previous messages in that session. This stateful behavior enables natural conversations in which you can ask follow-up questions, provide clarifications, or build complex workflows that span multiple exchanges.

Featurequery()ClaudeSDKClient
Session behaviorCreates a new session for each call; no memory of previous interactions.Maintains a persistent session across multiple turns—full context is preserved.
Memory and contextNo memory between calls—each query starts fresh with no knowledge of prior exchanges.Remembers everything from previous messages in the same session, enabling natural follow-ups.
Use caseOne-off tasks: single prompt → single response → done. Best for independent requests.Multi-turn conversations: follow-ups, clarifications, building on previous work. Best for chatbots.
Lifecycle managementAutomatic—session is created, used, and destroyed with each call. No manual control needed.Manual—requires explicit connect() and or async context manager ().
Creating a ClaudeSDKClient

Creating a ClaudeSDKClient uses the same ClaudeAgentOptions configuration you learned in previous lessons. You configure the model, system prompt, tool access, permissions, and working directory exactly as before, then pass those options to the client constructor.

The code creates a ClaudeAgentOptions object with the same configuration pattern you've used before. The only new element is the final line, which creates a ClaudeSDKClient by passing the options to its constructor. At this point, the client exists but hasn't connected to the agent runtime yet — you need to explicitly manage the session lifecycle to start the conversation.

Connecting and Disconnecting Manually

Once you've created a ClaudeSDKClient, you need to establish a connection before you can send queries. The most straightforward approach is to explicitly call connect() to start the session and disconnect() to end it.

The code creates a client, then calls await client.connect() to establish the session. The try/finally block ensures disconnect() is called even if errors occur, preventing resource leaks. This manual approach gives you explicit control over session lifetime, which is useful for long-running services or frameworks that don't work well with context managers.

Automatic Session Management with Context Managers

For most use cases, Python's async context manager provides a cleaner alternative that automatically handles connection and disconnection. This pattern is recommended because it ensures proper cleanup even if errors occur.

The async with statement automatically calls connect() when entering the block and disconnect() when exiting — whether the exit is normal or due to an exception. This eliminates the need for try/finally blocks and ensures you never accidentally leave sessions open. Inside the context manager block, you can send multiple queries, and the agent will maintain context across all of them.

Understanding query() and receive_response()

With the client connected, let's understand how to send queries and handle responses using the client's send-receive pattern. Unlike the standalone query() function that returned an async iterator directly, ClaudeSDKClient separates sending and receiving into two distinct operations.

The code demonstrates the fundamental send-receive pattern of ClaudeSDKClient. First, await client.query() sends a message to the agent. Then, client.receive_response() retrieves the agent's reply by returning an async iterator that yields messages in real-time as the agent works. This streaming behavior means messages arrive progressively — you see tool uses as they happen, text responses as they're generated, and results as they're produced — rather than waiting for the entire response to complete before seeing anything.

The receive_response() method works exactly like the async iterator returned by the standalone query() function you used in previous lessons — it yields the same message types (AssistantMessage, ToolResultBlock, etc.) and follows the same iteration pattern. The key difference is that with , you explicitly call to start receiving messages, whereas the standalone function returned the iterator directly. This separation of sending () and receiving () gives you more control over conversation flow and makes it easier to implement patterns like concurrent message handling, interrupts, or custom streaming logic.

Building a Response Display Helper

Since we'll need to receive and display responses multiple times throughout our conversation, let's create a reusable helper function that encapsulates this pattern.

The helper function takes the receive-and-display logic we just wrote and packages it into a reusable function. Now instead of writing the full async for loop and message handling every time we want to see the agent's response, we can simply call await display_response(client). This makes our conversation code cleaner and more maintainable. You can extend this helper to handle ToolResultBlock objects or other message types as needed. Now let's use this helper to conduct an actual multi-turn conversation.

Conducting a Multi-Turn Conversation

With the helper function in place, we can now conduct a complete multi-turn conversation that demonstrates context preservation. Let's send a first query and observe the response.

The code sends the first query by calling await client.query() with a prompt asking the agent to search for information about the difference between query() and ClaudeSDKClient. After sending the query, it calls await display_response(client) to retrieve and display the agent's response using the helper function we just created. This separation of sending and receiving gives you more control over the conversation flow and makes it easier to implement complex interaction patterns. Let's see what the agent produces in response to this first query.

Observing the First Response

When the agent processes the first query, it searches the web for information and provides a structured explanation of the differences between the two approaches.

The agent explains its plan, uses the WebSearch tool to find information, and then provides a structured explanation of the differences. This response becomes part of the session's context, which means the agent will remember it when processing the next query. Now let's send a second query that builds on this information to demonstrate context preservation.

Sending a Follow-Up Query

The real power of ClaudeSDKClient becomes apparent when you send a second query that references information from the first interaction. Because the client maintains a persistent session, the agent can understand contextual references like "the difference you just found."

The code sends a second query that asks the agent to edit a file and add information about "the difference you just found." Notice how this query uses the phrase "you just found" — this only makes sense if the agent remembers the first query and its results. Because both queries go through the same client session, the agent has access to the full conversation history and can understand what "the difference you just found" refers to. If you had used two separate query() calls instead of a client session, the second query would have failed because the agent wouldn't have known what information to include. Let's examine the agent's response to see how it uses the remembered context.

Observing Context Preservation

When the agent processes the second query, it demonstrates a clear understanding of the previous conversation by using the information it found earlier to complete the file editing task.

The agent reads the existing file, then uses the Edit tool to add a new section based on the information it found in the first query. Notice how the agent's explanation in the final response references the same concepts it discovered during the web search — stateless vs. stateful, tool support, multi-turn conversations, and when to use each approach. This demonstrates that the agent successfully maintained context across both queries and used information from the first interaction to complete the second task. Beyond basic conversations, the client also supports more advanced control mechanisms like interrupts.

Interrupting Agents When Needed

Sometimes you need to stop an agent mid-execution — perhaps it's taking too long, going off track, or you realize you gave it the wrong instructions. The ClaudeSDKClient supports interrupts, which allow you to cancel an ongoing agent run.

To demonstrate interrupts in action, we'll use anyio.create_task_group() to run two operations concurrently — one that displays the agent's response and another that waits a few seconds before interrupting. A task group is an async pattern that lets you run multiple async functions simultaneously and ensures they all complete (or get cancelled together) before moving on.

When you run this code, you'll see the agent begin its work, get interrupted mid-execution, and then respond to a new query:

The example demonstrates a typical interrupt scenario with the following flow:

  1. Initial Query: The client sends a query asking the agent to search for and explain the difference between query() and .
Summary & Conversational Workflows Exercises

You've now learned how to move beyond single queries to multi-turn conversations using ClaudeSDKClient. By creating a session-based client with the same ClaudeAgentOptions configuration you already know, managing its lifecycle with async context managers, and conducting conversations through the send-receive pattern of client.query() and receive_response(), you can build agents that maintain context across multiple interactions. The example demonstrated how context preservation enables natural follow-up queries in which the agent remembers previous work and builds on it to complete complex tasks, opening the door to sophisticated conversational workflows that would be impossible with stateless queries.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal