Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Branching

Explore conversation paths with graph-based branching

The Context package uses a DAG (directed acyclic graph) to store messages. Branching creates new paths in this graph, preserving all history.

The Graph Model

Messages are immutable nodes linked by parentId:

    User: "Hi"  →  Assistant: "Hello!"  →  User: "Tell me a joke"  →  Assistant: "Why did..."
       (msg-1)         (msg-2)                  (msg-3)                    (msg-4)

Branches are just pointers to head messages:
    main → msg-4

After rewind('msg-2'):
    main → msg-4
    main-v2 → msg-2  (new branch, you're here now)

Key insight: Nothing is deleted. Branching creates new pointers; original messages stay intact.

Why Branch?

  • Retry responses: Bad answer? Branch from the question, try again.
  • A/B test: Compare responses with different prompts.
  • User choice: Let users pick from multiple AI responses.
  • What-if analysis: Explore alternative conversation paths.

Branch Operations

rewind(messageId)

Create a new branch from a specific message and switch to it.

import {
  ContextEngine,
  SqliteContextStore,
  XmlRenderer,
  role,
  user,
  assistant,
} from '@deepagents/context';
import { generateText } from 'ai';
import { groq } from '@ai-sdk/groq';

const store = new SqliteContextStore('./chat.db');
const context = new ContextEngine({
  store,
  chatId: 'chat-001',
  userId: 'user-001',
}).set(role('You are helpful.'));

// Initial conversation
context.set(user('Explain monads', { id: 'q1' }));
const { systemPrompt, messages } = await context.resolve({
  renderer: new XmlRenderer(),
});

const response = await generateText({
  model: groq('gpt-oss-20b'),
  system: systemPrompt,
  messages,
});

context.set(assistant(response.text));
await context.save();
// Branch: main → q1 → a1

// Response too complex - rewind to question
const newBranch = await context.rewind('q1');
console.log(newBranch.name); // 'main-v2'
console.log(context.branch); // 'main-v2'

// Add simpler prompt and retry
context.set(hint('Explain simply, no jargon.'));
const { systemPrompt: sp2, messages: m2 } = await context.resolve({
  renderer: new XmlRenderer(),
});

const simpler = await generateText({
  model: groq('gpt-oss-20b'),
  system: sp2,
  messages: m2,
});

context.set(assistant(simpler.text));
await context.save();
// Branch: main-v2 → q1 → a2 (simpler)
// Branch: main → q1 → a1 (original, preserved)

switchBranch(name)

Switch to an existing branch.

// Currently on main-v2 with simpler explanation
await context.switchBranch('main');
// Now on main with original explanation

const { messages } = await context.resolve({ renderer: new XmlRenderer() });
// messages contains the original complex explanation

Switching clears pending (unsaved) messages.

btw()

Create a parallel branch without switching ("by the way").

// User asked a question, waiting for response
context.set(user('What is the weather?'));
await context.save();

// User wants to ask another question without waiting
const parallelBranch = await context.btw();
// parallelBranch = { name: 'main-v2', ... }
// Still on 'main', pending messages preserved

// Later, switch and ask the other question
await context.switchBranch(parallelBranch.name);
context.set(user('What time is it?'));
await context.save();

Use btw() when you want to fork without abandoning current work.

Listing Branches

Use the store directly to list branches:

const branches = await store.listBranches(context.chatId);
// [
//   { name: 'main', headMessageId: 'msg-4', messageCount: 4, isActive: false, ... },
//   { name: 'main-v2', headMessageId: 'msg-2', messageCount: 2, isActive: true, ... },
// ]

Branch Naming

Branches auto-name based on the parent branch:

main → (rewind) → main-v2 → (rewind) → main-v2-v2
     → (rewind) → main-v3

A/B Testing Pattern

Compare responses with different system prompts:

async function abTest(
  context: ContextEngine,
  store: ContextStore,
  question: string,
): Promise<{ a: string; b: string }> {
  // Setup
  context.set(user(question, { id: 'question' }));
  await context.save();

  // Variant A
  context.set(hint('Be formal and detailed.'));
  const { systemPrompt: spA, messages: mA } = await context.resolve({
    renderer: new XmlRenderer(),
  });

  const responseA = await generateText({
    model: groq('gpt-oss-20b'),
    system: spA,
    messages: mA,
  });

  context.set(assistant(responseA.text));
  await context.save();
  // main → question → answerA

  // Variant B - rewind to question
  await context.rewind('question');
  // main-v2 → question

  context.set(hint('Be casual and brief.'));
  const { systemPrompt: spB, messages: mB } = await context.resolve({
    renderer: new XmlRenderer(),
  });

  const responseB = await generateText({
    model: groq('gpt-oss-20b'),
    system: spB,
    messages: mB,
  });

  context.set(assistant(responseB.text));
  await context.save();
  // main-v2 → question → answerB

  return { a: responseA.text, b: responseB.text };
}

User Choice Pattern

Generate multiple responses, let user pick:

interface ResponseOption {
  branchName: string;
  text: string;
  style: string;
}

async function generateOptions(
  context: ContextEngine,
  question: string,
): Promise<ResponseOption[]> {
  context.set(user(question, { id: 'q' }));
  await context.save();

  const styles = [
    { hint: 'Be concise.', label: 'Brief' },
    { hint: 'Be detailed with examples.', label: 'Detailed' },
    { hint: 'Use analogies to explain.', label: 'Analogies' },
  ];

  const options: ResponseOption[] = [];

  for (let i = 0; i < styles.length; i++) {
    if (i > 0) {
      await context.rewind('q');
    }

    context.set(hint(styles[i].hint));
    const { systemPrompt, messages } = await context.resolve({
      renderer: new XmlRenderer(),
    });

    const response = await generateText({
      model: groq('gpt-oss-20b'),
      system: systemPrompt,
      messages,
    });

    context.set(assistant(response.text));
    await context.save();

    options.push({
      branchName: context.branch,
      text: response.text,
      style: styles[i].label,
    });
  }

  return options;
}

async function selectOption(
  context: ContextEngine,
  option: ResponseOption,
): Promise<void> {
  await context.switchBranch(option.branchName);
}

// Usage
const options = await generateOptions(context, 'Explain quantum computing');
// Show options to user...

// User picks "Analogies"
await selectOption(context, options[2]);
// Conversation continues on that branch

Graph Visualization

Get the full graph for debugging:

const graph = await store.getGraph(context.chatId);
// {
//   nodes: [
//     { id: 'msg-1', parentId: null, name: 'user', data: 'Hi', ... },
//     { id: 'msg-2', parentId: 'msg-1', name: 'assistant', data: 'Hello!', ... },
//   ],
//   branches: [
//     { name: 'main', headMessageId: 'msg-2', isActive: true, ... },
//   ],
//   checkpoints: [
//     { name: 'before-choice', messageId: 'msg-1', ... },
//   ],
// }

Use inspect() for a complete snapshot including token estimates:

const snapshot = await context.inspect({
  modelId: 'openai:gpt-4o',
  renderer: new XmlRenderer(),
});

When to Use What

NeedMethod
Retry bad responserewind() to question
Compare approachesrewind() + generate multiple
Fork without switchingbtw()
Return to previous branchswitchBranch()
Save position for latercheckpoint()

Best Practices

  1. Use custom message IDs for rewind targets:

    user('Question', { id: 'turn-1-question' })
  2. List branches to understand the graph:

    const branches = await store.listBranches(chatId);
  3. Remember: nothing is deleted. All branches and messages persist.

  4. Pending messages clear on switch. Save before switching branches.

Next Steps