Maintaining Agent State with Sessions

Introduction: From One-Shot to Multi-Turn Conversations

Welcome to the next step of your mastery journey! You have 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 treated each query() call as an independent 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 capturing and reusing session IDs. The TypeScript SDK maintains conversation context through session identifiers that you extract from query responses and pass to subsequent queries using the resume option. You'll learn how to capture session IDs from the message stream, use them to continue conversations, and observe how context preservation enables sophisticated workflows that build on previous work.

Session-Based Conversations: Stateless vs Stateful

The query() function creates a new session for every call by default, 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 stream, and then the session context is lost unless you explicitly capture and reuse its session ID. 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.

By capturing the session ID from a query's response and passing it to subsequent queries using the resume option, you can maintain a persistent conversation. When you resume a session, 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() without resumptionquery() with session resumption
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.
Session managementAutomatic — session is created and used with each call. No session ID handling needed.Manual — requires capturing session ID from init message and passing it via resume option.
Multi-turn chat continuityNot suitable — each call resets context, making follow-up questions impossible.Designed precisely for continuity — agent can reference and build upon earlier conversation turns.
Resource efficiencyEfficient for single tasks — no session overhead between calls.Requires session tracking — more overhead but enables sophisticated workflows that span multiple exchanges.

The choice between stateless and stateful query() usage depends on your use case. Use query() without session resumption when you need quick, independent responses — like generating a single piece of text, answering a standalone question, or performing a one-time task. Use query() with session resumption when you need conversations with context — like building a chatbot, conducting multi-step workflows in which later steps depend on earlier results, or creating interactive applications in which users have back-and-forth exchanges with the agent.

Capturing Session IDs from Message Streams

To maintain conversation context across multiple queries, you need to capture the session ID from the message stream. The SDK includes this information in a special system message with subtype: "init" that appears early in the stream.

TypeScript
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";

// Start a query
const stream = query({
  prompt: "Search the web for information about the Claude Agent SDK.",
  options: {
    model: "haiku",
    maxTurns: 10,
    allowedTools: ["WebSearch"],
  },
});

// Capture the session ID while processing messages
let sessionId: string | undefined;

for await (const message of stream) {
  // Extract session ID from the init message
  if (message.type === "system" && message.subtype === "init") {
    sessionId = message.session_id;
  }
}

// Now sessionId contains the identifier for this conversation
console.log(`Session ID: ${sessionId}`);

The code demonstrates how to capture the session ID while processing the message stream. Before handling assistant messages, we check if the current message is a system message with subtype: "init". When we find this special initialization message, we extract its session_id property and store it in a variable. This session ID is the key to resuming the conversation later — you'll pass it to subsequent query() calls using the resume option.

text
Session ID: 9088d59c-93b3-453f-b3b6-d156a6e4b8cc

Once you've captured the session ID and processed all messages, you can use that ID to continue the conversation with follow-up queries that maintain full context.

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 and captures the session ID for us.

TypeScript
import { type SDKMessage } from "@anthropic-ai/claude-agent-sdk";

/**
 * Helper function to display agent messages with formatting
 * Iterates through the message stream, formats assistant responses and tool calls,
 * and extracts the session ID for resuming conversations
 * 
 * @returns The session ID if found, undefined otherwise
 */
async function displayResponse(stream: AsyncIterable<SDKMessage>): Promise<string | undefined> {
  let sessionId: string | undefined;

  for await (const message of stream) {
    // Extract session ID from the init message for later use
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
    }

    // Display assistant responses as they come in
    if (message.type === "assistant") {
      for (const block of message.message.content) {
        if (block.type === "text") {
          console.log("\n💬 Claude Response:");
          console.log(block.text);
        } else if (block.type === "tool_use") {
          console.log(`\n🔧 [Tool: ${block.name}]`);
        }
      }
    }
  }

  return sessionId;
}

The helper function takes the receive-and-display logic we just wrote and packages it into a reusable function. It accepts the AsyncIterable<SDKMessage> returned by query(), iterates over all messages to display them with formatting, and returns the captured session ID. Now, instead of writing the full for await...of loop and message handling every time we want to see the agent's response, we can simply call await displayResponse(stream) and get back the session ID for later use. This makes our conversation code cleaner and more maintainable. 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 configure our options once and reuse them across queries, then send a first query and capture its session ID.

TypeScript
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
import { resolve } from "path";

// Configure the agent options that will be reused across queries
const options: Options = {
  model: "haiku",
  maxTurns: 10,
  allowedTools: ["WebSearch", "Bash", "Read", "Write", "Edit"],
  cwd: resolve(__dirname),
};

// Start a new query session
const firstStream = query({
  prompt:
    "Search the web for how query() uses session IDs " +
    "in the Claude Agent SDK in TypeScript. Give me a brief explanation.",
  options,
});

// Display the first query and capture the session ID
const sessionId = await displayResponse(firstStream);

The code creates an Options object with configuration that will be reused across multiple queries — model selection, turn limits, allowed tools, and working directory. Then it calls query() with a prompt asking the agent to search for information about session IDs in the TypeScript SDK. The query() function returns an async iterable stream, which we pass to our displayResponse() helper function. The helper processes all messages, displays them with formatting, and returns the captured session ID that we'll need for the next query.

This first query establishes a new session, and the session ID we captured is the key to continuing the conversation. 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 how session IDs work in the TypeScript SDK.

text
💬 Claude Response:
I'll search for information about how query() uses session IDs in the Claude Agent SDK in TypeScript.

🔧 [Tool: WebSearch]

💬 Claude Response:
Based on my search, here's a brief explanation:

## How query() Uses Session IDs in Claude Agent SDK (TypeScript)

In the Claude Agent SDK for TypeScript, the `query()` function uses session IDs to maintain conversation context across multiple interactions:

**Session Creation**: When you call `query()` for the first time, it creates a new session and returns a session ID in the response stream (specifically in the system message with `subtype: "init"`).

**Session Resumption**: To continue a conversation, you capture the session ID from the first query's response and pass it to subsequent `query()` calls using the `resume` option in the options object.

**Context Preservation**: When you resume a session using its ID, the agent has access to the full conversation history from that session, allowing it to understand references to previous exchanges and build on earlier work.

**Example Pattern**:
// First query - creates new session
const stream1 = query({ prompt: "...", options: {...} });
const sessionId = // capture from stream

// Follow-up query - resumes session
const stream2 = query({ 
  prompt: "...", 
  options: { ...options, resume: sessionId } 
});

This session-based approach enables multi-turn conversations while keeping the API simple and functional.

The agent explains its plan, uses the WebSearch tool to find information, and then provides a structured explanation of how session IDs work. 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.

Resuming Sessions with Follow-Up Queries

The real power of session IDs becomes apparent when you send a second query that references information from the first interaction. By passing the captured session ID using the resume option, the agent can understand contextual references like "what you just found."

TypeScript
// Ensure we captured a session ID from the first query
if (!sessionId) {
  throw new Error("Session ID not captured from first query");
}

// Resume the previous session to maintain context
const secondStream = query({
  prompt:
    "Now add a new section at the end of the existing file " +
    "./summaries/claude-agent-sdk.md explaining what you just found.",
  options: {
    ...options, // Reuse the same base options
    resume: sessionId, // Resume previous conversation
  },
});

// Display the second query's response
await displayResponse(secondStream);

The code sends a second query that asks the agent to edit a file and add information about "what 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. The key is the resume: sessionId option, which tells the SDK to continue the previous conversation rather than starting a new one. We spread the original options object (...options) to maintain the same configuration, then add the resume property with the captured session ID.

If you had made two separate query() calls without using resume, the second query would have failed because the agent wouldn't have known what information to include. The session ID is what connects these two queries into a single conversation. 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.

text
💬 Claude Response:
I'll first read the file to see its current content, then add a new section at the end.

🔧 [Tool: Read]

💬 Claude Response:
Now I'll add a new section at the end of the file explaining how query() uses session IDs:

🔧 [Tool: Edit]

💬 Claude Response:
Perfect! I've successfully added a new section to the file `./summaries/claude-agent-sdk.md` at the end. The new section includes:

1. **Session Creation** - explaining how the first query() call creates a session and returns a session ID
2. **Session Resumption** - covering how to capture and reuse session IDs with the resume option
3. **Context Preservation** - describing how resumed sessions maintain full conversation history
4. **Example Pattern** - providing a code example showing the session creation and resumption flow

The new content is clearly formatted with bullet points and code examples for easy reading and aligns with the existing document structure.

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 — session creation, session resumption, context preservation, and the example pattern. This demonstrates that the agent successfully maintained context across both queries and used information from the first interaction to complete the second task.

The session ID acts as a bridge between queries, allowing the agent to access the full conversation history and build on previous work. This pattern of capturing session IDs and using the resume option is the foundation for building sophisticated conversational workflows in TypeScript.

Summary

You have now learned how to move beyond single queries to multi-turn conversations using session IDs in the TypeScript SDK. By capturing session IDs from the system init message in query response streams, storing them for later use, and passing them to subsequent query() calls using the resume option, 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.

The key pattern to remember is: call query() to start a conversation, iterate over the returned stream to capture the session ID from the system init message, then call query() again with resume: sessionId in the options to continue the conversation. This simple but powerful approach gives you full control over conversation flow while maintaining the functional, stream-based API design of the TypeScript SDK.

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