TypeScriptADK-TS

Session Rewind

Restore sessions to previous states for debugging, testing, and undo functionality

Session rewind lets you restore a session to an earlier state by undoing one or more invocations. This is useful for debugging agent behavior, implementing undo features, testing different conversation paths, or recovering from errors.

Non-Destructive

Events are never deleted—they're preserved in the history but marked to be excluded from future LLM context. This maintains complete audit trails while effectively "undoing" the conversation.

What Gets Restored

When you rewind a session, the framework restores:

  • Session State: All state keys restored to their values before the target invocation
  • Artifacts: Artifact versions reverted to what existed before the target invocation
  • Event History: Events from the target invocation onwards are marked with a rewind marker and excluded from LLM context

Basic Usage

Rewind a session using the Runner.rewind method:

import { Runner } from "@iqai/adk";

const runner = new Runner({
  appName: "my-app",
  agent: myAgent,
  sessionService: sessionService,
  artifactService: artifactService,
});

// Rewind to before a specific invocation
await runner.rewind({
  userId: "user_123",
  sessionId: "session_456",
  rewindBeforeInvocationId: "invocation_789",
});

This restores the session to the state it had before invocation_789 started. All invocations from that point onwards are effectively undone.

Common Use Cases

Implementing Undo

Add an "undo last message" feature to your chat application:

import { Runner } from "@iqai/adk";

class ChatApp {
  private runner: Runner;
  private lastInvocationId?: string;

  async sendMessage(userId: string, sessionId: string, message: string) {
    for await (const event of this.runner.runAsync({
      userId,
      sessionId,
      newMessage: { parts: [{ text: message }] },
    })) {
      // Track the last invocation ID
      if (event.invocationId) {
        this.lastInvocationId = event.invocationId;
      }
      // Display event to user
      displayEvent(event);
    }
  }

  async undo(userId: string, sessionId: string) {
    if (!this.lastInvocationId) {
      throw new Error("No invocation to undo");
    }

    await this.runner.rewind({
      userId,
      sessionId,
      rewindBeforeInvocationId: this.lastInvocationId,
    });

    console.log("Last message undone");
    this.lastInvocationId = undefined;
  }
}

// Usage
const app = new ChatApp(runner);
await app.sendMessage("user-1", "session-1", "What is 2+2?");
await app.sendMessage("user-1", "session-1", "Tell me about quantum physics");

// Undo the last message
await app.undo("user-1", "session-1");

Error Recovery

Automatically rewind when an agent encounters an error:

async function runWithErrorRecovery(runner: Runner, config) {
  let lastSuccessfulInvocation: string | undefined;

  try {
    for await (const event of runner.runAsync(config)) {
      // Track successful events
      if (!event.partial && !event.errorCode && event.invocationId) {
        lastSuccessfulInvocation = event.invocationId;
      }

      if (event.errorCode) {
        throw new Error(`Agent error: ${event.errorMessage}`);
      }

      yield event;
    }
  } catch (error) {
    console.error("Agent failed:", error);

    if (lastSuccessfulInvocation) {
      console.log("Rewinding to last successful state...");
      await runner.rewind({
        userId: config.userId,
        sessionId: config.sessionId,
        rewindBeforeInvocationId: lastSuccessfulInvocation,
      });
    }

    throw error;
  }
}

A/B Testing Agent Responses

Test different agent behaviors by rewinding and trying alternatives:

async function testAgentVariants(runner: Runner, config) {
  const results = [];

  // Try agent configuration A
  const agentA = new LlmAgent({ name: "variant-a", ...configA });
  runner.agent = agentA;

  const eventsA = [];
  for await (const event of runner.runAsync(config)) {
    eventsA.push(event);
  }
  results.push({ variant: "A", events: eventsA });

  // Get the invocation ID to rewind
  const invocationId = eventsA[0]?.invocationId;

  if (invocationId) {
    // Rewind to before this test
    await runner.rewind({
      userId: config.userId,
      sessionId: config.sessionId,
      rewindBeforeInvocationId: invocationId,
    });

    // Try agent configuration B
    const agentB = new LlmAgent({ name: "variant-b", ...configB });
    runner.agent = agentB;

    const eventsB = [];
    for await (const event of runner.runAsync(config)) {
      eventsB.push(event);
    }
    results.push({ variant: "B", events: eventsB });
  }

  return results;
}

How Rewind Works

The rewind process involves three steps:

1. Compute State Delta

The Runner scans event history to determine what state changes occurred from the target invocation onwards:

// Example of state changes to revert
{
  theme: 'light',        // Restore to previous value
  newKey: null,          // Delete (didn't exist before)
  unchangedKey: undefined // No change needed
}

2. Compute Artifact Delta

Similarly, the Runner finds the previous versions of modified artifacts:

// Example of artifact versions to restore
{
  'report.pdf': 3,  // Restore to version 3
  'image.png': 1    // Restore to version 1
}

3. Apply and Persist

The Runner creates a rewind event and applies the deltas:

// Create rewind event
const rewindEvent = new Event({
  invocationId: newInvocationId,
  author: "system",
  content: { parts: [{ text: "Session rewound" }] },
  actions: new EventActions({
    rewindBeforeInvocationId,
    stateDelta,
    artifactDelta,
  }),
});

// Append rewind event to session
await sessionService.appendEvent(session, rewindEvent);

Event Filtering

After a rewind, the framework automatically excludes marked events from LLM context:

// Events before rewind marker: included in LLM context
// Rewind marker event: excluded
// Events after rewind marker: excluded

function getContentsForLlmRequest(session: Session): Content[] {
  const rewindMarker = findLastRewindMarker(session.events);

  return session.events
    .filter(e => shouldIncludeInLlmContext(e, rewindMarker))
    .map(e => e.content);
}

Important Considerations

External Side Effects

Rewind only affects ADK-managed session state and artifacts. External side effects (API calls, database writes, emails sent) cannot be automatically undone.

Limitations:

  • No external rollback: API calls, database operations, and other external changes persist
  • Memory service unaffected: Vector stores and RAG systems retain their entries
  • Invocation must exist: The specified rewindBeforeInvocationId must reference a real invocation
  • Performance: Computing deltas for sessions with many events/artifacts can be slow

Best practices:

  • Use rewind for development, debugging, and testing
  • Implement idempotent tools when possible
  • Track external side effects separately if they need reversal
  • Consider performance implications for large sessions

When to Use Rewind

Good use cases:

  • Debugging agent behavior by trying different paths
  • Implementing user-facing undo functionality
  • A/B testing different agent configurations
  • Recovering from errors automatically
  • Testing conversation flows during development

Not recommended for:

  • Real-time streaming undo (rewind is for completed invocations)
  • Fine-grained version control of code artifacts
  • Compliance-critical audit trails (complicates forensics)
  • Undoing external API calls or database changes

Next Steps