Session Management
Multi-turn agents with resume, forkSession, and 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.
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.
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.
// 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