DocsGuidesTime Travel

Time Travel

Time travel lets you inspect earlier states and replay alternate execution paths. streamResource() exposes the full checkpoint history and branch navigation through Angular Signals. Use it to debug agent decisions, explore alternate paths, and build undo/redo experiences.

Use cases

Debug agent decisions, explore alternate paths, and build undo/redo experiences for your users. Time travel works with any LangGraph agent that persists checkpoints to a thread.

How checkpointing works

Time travel depends on checkpointing on the agent side. LangGraph automatically saves a checkpoint after every node execution when you compile your graph with a checkpointer.

from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-5-mini")
 
def call_model(state: MessagesState) -> dict:
    response = llm.invoke(state["messages"])
    return {"messages": [response]}
 
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", END)
 
# Compile with a checkpointer to enable time travel
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
 
# Run the graph with a thread ID
config = {"configurable": {"thread_id": "user_123"}}
result = graph.invoke(
    {"messages": [("user", "What is LangGraph?")]},
    config=config,
)
 
# Browse checkpoint history server-side
for state in graph.get_state_history(config):
    print(f"Step: {state.metadata.get('step', '?')}")
    print(f"  Checkpoint: {state.config['configurable']['checkpoint_id']}")
    print(f"  Messages: {len(state.values.get('messages', []))}")
 
# Replay from a specific checkpoint
past_config = {
    "configurable": {
        "thread_id": "user_123",
        "checkpoint_id": "<checkpoint-id-from-history>",
    }
}
past_state = graph.get_state(past_config)

Browsing execution history

The history() signal contains an array of ThreadState checkpoints ordered from oldest to newest. Each checkpoint captures the complete agent state at that point in execution, including messages, intermediate results, and any custom state fields.

const agent = streamResource<AgentState>({
  assistantId: 'agent',
  threadId: signal(threadId),
});
 
// Full execution timeline
const checkpoints = computed(() => agent.history());
const checkpointCount = computed(() => agent.history().length);
 
// Access a specific checkpoint
const latestCheckpoint = computed(() => {
  const history = agent.history();
  return history[history.length - 1];
});

Each ThreadState entry exposes checkpoint, metadata, created_at, and the full values snapshot, giving you complete visibility into every step of execution.

Forking from a checkpoint

Submit with a specific checkpoint to branch execution from an earlier state. This creates a new branch in the thread graph while leaving the original path intact.

forkFromCheckpoint(index: number) {
  const checkpoint = this.agent.history()[index];
  this.agent.submit(
    { messages: [{ role: 'user', content: 'Try a different approach' }] },
    { checkpoint: checkpoint.checkpoint }
  );
}
 
// Fork with a completely different input
retryWithAlternative(index: number, newInput: string) {
  const checkpoint = this.agent.history()[index];
  this.agent.submit(
    { messages: [{ role: 'user', content: newInput }] },
    { checkpoint: checkpoint.checkpoint }
  );
}

Branch navigation

Use branch() and setBranch() to navigate between execution branches. Branches are automatically created when you fork from a checkpoint.

// Current branch identifier
const activeBranch = computed(() => agent.branch());
 
// All available branches (if exposed by your graph)
const allBranches = computed(() => agent.history()
  .map(s => s.metadata?.branch)
  .filter(Boolean)
);
 
// Switch to a different branch
selectBranch(branchId: string) {
  agent.setBranch(branchId);
}

Building a history UI

Expose checkpoint history directly in your component to let users scrub through execution steps or rewind to any earlier state.

import { Component, inject, computed, ChangeDetectionStrategy } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
import { AgentService } from './agent.service';
 
@Component({
  selector: 'app-history-viewer',
  templateUrl: './history-viewer.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HistoryViewerComponent {
  private agentService = inject(AgentService);
  readonly agent = this.agentService.agent;
 
  readonly checkpoints = computed(() => this.agent.history());
  readonly activeIndex = computed(() =>
    this.checkpoints().length - 1
  );
 
  fork(index: number) {
    const checkpoint = this.checkpoints()[index];
    this.agent.submit(
      { messages: [{ role: 'user', content: 'Try a different approach' }] },
      { checkpoint: checkpoint.checkpoint }
    );
  }
 
  formatTime(isoString: string): string {
    return new Date(isoString).toLocaleTimeString();
  }
}

Comparing checkpoints

Diff two checkpoints to understand exactly what changed between execution steps. This is useful for understanding tool call results, message additions, or state mutations.

compareCheckpoints(indexA: number, indexB: number) {
  const history = this.agent.history();
  const stateA = history[indexA]?.values;
  const stateB = history[indexB]?.values;
 
  if (!stateA || !stateB) return null;
 
  // Compare message counts
  const messagesAdded = (stateB.messages?.length ?? 0)
    - (stateA.messages?.length ?? 0);
 
  // Identify changed keys
  const changedKeys = Object.keys({ ...stateA, ...stateB }).filter(
    key => JSON.stringify(stateA[key]) !== JSON.stringify(stateB[key])
  );
 
  return { messagesAdded, changedKeys };
}

Use the comparison result to render a diff view, highlight changed fields in your UI, or log what the agent modified during a specific step.

Replaying with modified input

Combine forking with new input to explore how the agent would have responded differently. This is the core of the undo/redo experience.

@Component({
  selector: 'app-replay',
  templateUrl: './replay.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReplayComponent {
  readonly agent = inject(AgentService).agent;
 
  readonly history = computed(() => this.agent.history());
  readonly canUndo = computed(() => this.history().length > 1);
 
  undo() {
    const history = this.history();
    if (history.length < 2) return;
 
    // Go back one step
    const previousCheckpoint = history[history.length - 2];
    this.agent.submit(undefined, {
      checkpoint: previousCheckpoint.checkpoint,
    });
  }
 
  replayWith(index: number, newMessage: string) {
    const checkpoint = this.history()[index];
    this.agent.submit(
      { messages: [{ role: 'user', content: newMessage }] },
      { checkpoint: checkpoint.checkpoint }
    );
  }
}
Debugging workflow

Time travel is most useful during development. Inspect why an agent chose a particular path by comparing adjacent checkpoints, then fork to test alternatives without restarting the conversation. Combine history() with Angular DevTools to watch checkpoint arrays update in real time as the agent streams.

What's Next