Skip to main content
P4 Intermediate

Session Management

Multi-turn agents with resume, forkSession, and continue.

SDK APIs session_id resume forkSession continue

Exercise 1: Capture and Resume

Start a session, capture the session_id, then resume it with a follow-up question that references context from the first turn.

Starter Code exercise-1.ts
import { query } from "@anthropic-ai/claude-agent-sdk";

// Helper to run a query and extract session_id + content
async function runQuery(prompt: string, options: any) {
  const response = query({ prompt, options });
  let sessionId = "";
  let content = "";

  for await (const message of response) {
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
    }
    if (message.type === "result") {
      content = message.content ?? "";
    }
  }
  return { sessionId, content };
}

// Turn 1: Start a new session
const turn1 = await runQuery(
  "Read package.json and tell me the project name and version.",
  { allowedTools: ["Read"] }
);

console.log(`[Session] ID: ${turn1.sessionId}`);
console.log(`Turn 1: ${turn1.content}\n`);

// Turn 2: Resume the same session with a follow-up
const turn2 = await runQuery(
  "Now list all the dependencies you found. Which ones are outdated?",
  {
    resume: turn1.sessionId,  // continues the conversation
    allowedTools: ["Read", "Bash"]
  }
);

console.log(`Turn 2: ${turn2.content}\n`);

// Verify continuity - the agent should reference package.json
// without needing to read it again
console.log(`Same session? ${turn1.sessionId === turn2.sessionId}`);

Your task: Run this code and verify that Turn 2 references information from Turn 1 without re-reading the file. Then add a Turn 3 that asks the agent to suggest version upgrades.

Exercise 2: Fork and Compare

Use forkSession to branch a conversation and give two agents divergent instructions from the same starting point.

Exercise Code exercise-2.ts
import { query } from "@anthropic-ai/claude-agent-sdk";

// Helper to run a query and extract session_id + content
async function runQuery(prompt: string, options: any) {
  const response = query({ prompt, options });
  let sessionId = "";
  let content = "";

  for await (const message of response) {
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
    }
    if (message.type === "result") {
      content = message.content ?? "";
    }
  }
  return { sessionId, content };
}

// Shared context: analyze a codebase
const base = await runQuery(
  "Read src/index.ts and summarize what the application does.",
  { allowedTools: ["Read", "Glob"] }
);

console.log(`[Session] Base: ${base.sessionId}`);
console.log(`Base analysis: ${base.content.slice(0, 200)}...\n`);

// Fork A: Optimize for performance (forkSession: true creates a branch)
const branchA = await runQuery(
  "Based on your analysis, refactor the code for maximum performance. Focus on async patterns and caching.",
  {
    resume: base.sessionId,
    forkSession: true,  // Creates a new branch, original unchanged
    allowedTools: ["Read", "Write"]
  }
);

// Fork B: Optimize for readability (another branch from the same base)
const branchB = await runQuery(
  "Based on your analysis, refactor the code for maximum readability. Add JSDoc comments and extract helper functions.",
  {
    resume: base.sessionId,
    forkSession: true,  // Creates another branch
    allowedTools: ["Read", "Write"]
  }
);

// Compare the two approaches
console.log("=== Branch A (Performance) ===");
console.log(branchA.content.slice(0, 300));
console.log("\n=== Branch B (Readability) ===");
console.log(branchB.content.slice(0, 300));

// Note: Both branches share the base context but diverge independently
console.log(`\n[Sessions] Base: ${base.sessionId}`);
console.log(`[Sessions] Branch A: ${branchA.sessionId}`);
console.log(`[Sessions] Branch B: ${branchB.sessionId}`);

Your task: Run both forks and compare the outputs. Notice how each branch retains the base context but diverges in direction. Try creating a third fork that optimizes for security.

Exercise 3: Session Lifecycle Tracker

Build a system that monitors the full lifecycle of sessions across resume, forkSession, and continue operations.

Challenge exercise-3.ts
// Build a SessionTracker class that:
//
// 1. Wraps query() to automatically track every session interaction
// 2. Records per-turn metrics:
//    - Turn number, timestamp, prompt length
//    - Tools used (names and count)
//    - Token usage (input, output, total)
//    - Duration (ms)
//
// 3. Tracks session relationships:
//    - Parent session (if resumed or forked)
//    - Child sessions (forks created from this session)
//    - Session type: "new" | "resumed" | "forked" | "continued"
//
// 4. Outputs a tree view of the session graph:
//
//    Session abc123 (new)
//    ├── Turn 1: "Read package.json..." → 3 tools, 1.2k tokens, 850ms
//    ├── Turn 2: "List dependencies..." → 1 tool, 800 tokens, 420ms
//    ├── Fork → Session def456
//    │   ├── Turn 3: "Optimize perf..." → 4 tools, 2.1k tokens, 1.3s
//    │   └── Turn 4: "Add caching..." → 2 tools, 950 tokens, 600ms
//    └── Fork → Session ghi789
//        └── Turn 3: "Improve readability..." → 3 tools, 1.8k tokens, 900ms
//
// Hints:
// - Use a Map<string, SessionRecord> to store session data
// - Wrap query() with a trackedQuery() that intercepts all events
// - Use process.hrtime.bigint() for accurate timing
// - Track forkSession() calls by wrapping the function
← Back to Exercises