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
rewindBeforeInvocationIdmust 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