Session Rewind
Travel back in time by restoring sessions to a previous state before a specific invocation
Session rewind allows you to "undo" a conversation by restoring the session to its state before a specific invocation. This is useful for debugging, A/B testing different agent responses, recovering from errors, or implementing user-facing "undo" functionality.
What Gets Rewound?
When you rewind a session, the framework restores:
- Session State: All state keys (including
user:,app:, and unprefixed scopes) are restored to their values before the target invocation - Artifacts: All artifacts are reverted to their versions before the target invocation
- Event History: Events from the target invocation onwards are marked with a rewind marker, so they're excluded from future LLM context
History is preserved
Events are never actually deleted—they're kept in the history but filtered out when building LLM context. This maintains audit trails while effectively "undoing" the conversation.
Basic Usage
The Runner class provides the rewind method:
import { Runner } from '@iqai/adk';
const runner = new Runner({
agent,
sessionService,
artifactService
});
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.
How It Works
The rewind process involves three steps:
1. State Delta Computation
The runner scans the event history to compute what state changes happened during and after the target invocation:
const stateDelta = await runner._computeStateDeltaForRewind(
session,
rewindBeforeInvocationId
);This produces a delta containing:
- Keys that were modified (with their "before" values)
- Keys that were created (marked as
nullto delete them)
2. Artifact Version Restoration
Similarly, the runner determines which artifact versions to restore:
const artifactDelta = await runner._computeArtifactDeltaForRewind(
session,
rewindBeforeInvocationId
);This finds the most recent version of each modified artifact that existed before the target invocation.
3. Persistence
Finally, the runner:
- Creates a rewind event marking the cutoff point
- Applies the state delta (reverting or deleting keys)
- Applies the artifact delta (restoring previous versions)
const rewindEvent = new Event({
invocationId: newInvocationId,
author: 'system',
content: { parts: [{ text: 'Session rewound' }] },
actions: new EventActions({
rewindBeforeInvocationId,
stateDelta,
artifactDelta
})
});
await sessionService.appendEvent(session, rewindEvent);Example: Implementing Undo
Here's how you might add an "undo" feature to a chat interface:
import { Runner, InMemorySessionService, InMemoryArtifactService } from '@iqai/adk';
class ChatApp {
private runner: Runner;
private lastInvocationId?: string;
constructor(agent) {
this.runner = new Runner({
agent,
sessionService: new InMemorySessionService(),
artifactService: new InMemoryArtifactService()
});
}
async sendMessage(userId: string, sessionId: string, message: string) {
const config = {
userId,
sessionId,
appName: 'chat-app',
userMessage: message
};
const events = [];
for await (const event of this.runner.runAsync(config)) {
events.push(event);
if (event.invocationId) {
this.lastInvocationId = event.invocationId;
}
}
return events;
}
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
});
this.lastInvocationId = undefined;
console.log('Last message undone');
}
}
const app = new ChatApp(myAgent);
await app.sendMessage('user-1', 'session-1', 'What is 2+2?');
await app.sendMessage('user-1', 'session-1', 'Actually, tell me about quantum physics');
await app.undo('user-1', 'session-1');Example: Error Recovery
Automatically rewind when an agent encounters an error:
async function runWithErrorRecovery(runner, config) {
let lastSuccessfulInvocation;
try {
for await (const event of runner.runAsync(config)) {
yield event;
if (!event.partial && event.invocationId) {
lastSuccessfulInvocation = event.invocationId;
}
}
} 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;
}
}Implementation Details
Event Filtering
After a rewind, events from the target invocation onwards are automatically excluded from LLM context. The framework handles this in llm-flows/contents.ts:
export function getContentsForLlmRequest(session: Session): Content[] {
const rewindMarker = findLastRewindMarker(session.events);
return session.events
.filter(e => shouldIncludeInLlmContext(e, rewindMarker))
.map(e => e.content);
}State Key Deletion
When rewinding creates a stateDelta with null values, the session service interprets these as deletions:
if (value === null || value === undefined) {
delete session.state[key];
} else {
session.state[key] = value;
}Artifact References
Artifacts support references to other artifacts using special URIs:
import { getArtifactUri, parseArtifactUri } from '@iqai/adk';
const uri = getArtifactUri('session', 'session-id', 'report.pdf', 2);
const { scope, scopeId, filename, version } = parseArtifactUri(uri);When loading artifacts, the service recursively resolves references and returns the actual data.
Testing Rewind
The framework includes comprehensive tests in packages/adk/src/tests/runners-rewind.test.ts:
cd packages/adk
pnpm test runners-rewindTests cover:
- State restoration across scopes (
user:,app:, unprefixed) - Artifact version rollback
- Event history filtering
- Multiple sequential rewinds
Limitations and Considerations
External side effects
Rewind only affects session state and artifacts managed by ADK. External side effects (API calls, database writes, sent emails) cannot be automatically undone.
- No external rollback: If your tool made external API calls or wrote to external databases, those changes persist
- Memory persistence: Rewind doesn't affect memory services (if you're using vector stores or RAG systems, those entries remain)
- Invocation ID must exist: The
rewindBeforeInvocationIdmust reference a real invocation in the session history - Performance: For sessions with many events/artifacts, computing deltas can be expensive
When to Use Rewind
Good use cases:
- Debugging: Try different responses without starting over
- User control: Let users undo mistakes or unwanted agent actions
- A/B testing: Compare agent behavior by rewinding and trying alternatives
- Error recovery: Automatically revert to a known good state after failures
Not recommended for:
- Real-time streaming undo: Rewind is designed for completed invocations
- Fine-grained version control: Use git or dedicated versioning systems for code artifacts
- Audit compliance: While events are preserved, rewind complicates audit trails
Related Topics
How is this guide?