Undo For Agents: Building Reversible Voice Actions With Checkpoints
Table of Contents
“Delete the draft project.”
Your voice agent heard it. Executed it. The project is gone.
Two seconds later: “Wait, I meant the old draft, not the new one!”
Too late. The damage is done.
Users live in fear of this moment. One wrong word. One misheard command. One irreversible mistake.
That fear kills adoption. Users won’t trust voice agents with important tasks if there’s no safety net.
The solution? Undo for agents. Checkpoint/rollback patterns that let users safely experiment, knowing they can rewind mistakes.
Let me show you how to build reversible voice actions.
The Irreversibility Problem
Voice agents often execute actions that modify state:
- Create/delete files or projects
- Update databases
- Send messages or emails
- Make purchases
- Change configurations
- Archive or move data
Unlike text interfaces where users can review before confirming, voice is immediate. Speak → Execute → Done.
And if the agent misheard? Or the user misspoke? Or they changed their mind? There’s no undo button.
Real-World Disasters
Project management agent:
User: “Delete the test workspace”
Agent: deletes production workspace instead
User: “WHAT? I said TEST workspace!”
Hours of work gone.
Email agent:
User: “Send that draft to the team”
Agent: sends unfinished draft to entire company
User: “No no no, I wasn’t ready to send that!”
Can’t unsend.
Shopping agent:
User: “Add two of those to my cart”
Agent: processes purchase for 20 instead of 2
User: “Wait, I said TWO not TWENTY!”
Order already placed.
Each scenario shares the same problem: no recovery path. Once executed, the action can’t be reversed.
Why Users Need Undo
The psychology is clear:
1. Confidence
Users take action more freely when they know mistakes are recoverable. Fear of irreversible errors creates hesitation.
2. Experimentation
“Let me try this…” becomes safe. Users explore agent capabilities without anxiety.
3. Trust
Undo = trust. “This agent has my back” vs “I need to be perfect or risk disaster.”
4. Error Recovery
Misheard commands, changed minds, misunderstandings—all recoverable with undo.
The Solution: Checkpoint/Rollback Patterns
The concept is simple: before executing actions, create snapshots. If something goes wrong, roll back to the snapshot.
The Undo Architecture
graph TD
A[User Requests Action] --> B[Agent Confirms Intent]
B --> C[Create Checkpoint]
C --> D[Capture Current State]
D --> E[Execute Action]
E --> F[Action Completes]
F --> G{Guardrail Check}
G -->|Violation| H[Auto-Rollback]
G -->|Safe| I[Commit Action]
H --> J[Restore from Checkpoint]
J --> K[Explain What Happened]
I --> L[User Says 'Undo']
L --> M[Manual Rollback]
M --> J
K --> N[Offer Alternative]
N --> A
The pattern: Checkpoint → Execute → Validate → Commit or Rollback.
Building Checkpoint Systems
Let’s implement this from the ground up.
Step 1: State Snapshots
Create a snapshot system for different state types:
class CheckpointManager {
constructor() {
this.checkpoints = new Map();
this.maxCheckpoints = 10; // Keep last 10 actions
}
async createCheckpoint(actionId, stateType, currentState) {
const checkpoint = {
id: actionId,
timestamp: Date.now(),
stateType: stateType,
snapshot: await this.captureState(stateType, currentState),
metadata: {
user_id: currentState.user_id,
action_description: currentState.description
}
};
this.checkpoints.set(actionId, checkpoint);
// Cleanup old checkpoints
if (this.checkpoints.size > this.maxCheckpoints) {
const oldest = Array.from(this.checkpoints.keys())[0];
this.checkpoints.delete(oldest);
}
console.log(`✓ Checkpoint created: ${actionId}`);
return checkpoint;
}
async captureState(stateType, currentState) {
// Deep clone state based on type
switch(stateType) {
case 'database':
return await this.snapshotDatabase(currentState);
case 'file':
return await this.snapshotFile(currentState);
case 'workspace':
return await this.snapshotWorkspace(currentState);
case 'cart':
return await this.snapshotCart(currentState);
default:
return JSON.parse(JSON.stringify(currentState));
}
}
async snapshotDatabase(state) {
// For database modifications, store the query to reverse it
return {
type: 'database',
table: state.table,
operation: state.operation,
record_id: state.record_id,
previous_values: await db.query(
`SELECT * FROM ${state.table} WHERE id = ?`,
[state.record_id]
)
};
}
async snapshotFile(state) {
// For files, store content before modification
return {
type: 'file',
path: state.path,
content: await fs.readFile(state.path, 'utf8'),
metadata: await fs.stat(state.path)
};
}
async snapshotWorkspace(state) {
// For workspace, store full structure
return {
type: 'workspace',
workspace_id: state.workspace_id,
structure: await workspace.export(state.workspace_id),
timestamp: Date.now()
};
}
async snapshotCart(state) {
// For shopping cart, store items and quantities
return {
type: 'cart',
user_id: state.user_id,
items: [...state.items], // Deep copy
total: state.total
};
}
async rollback(actionId) {
const checkpoint = this.checkpoints.get(actionId);
if (!checkpoint) {
throw new Error(`Checkpoint ${actionId} not found`);
}
console.log(`⏮️ Rolling back: ${actionId}`);
await this.restoreState(checkpoint);
return checkpoint;
}
async restoreState(checkpoint) {
switch(checkpoint.stateType) {
case 'database':
await this.restoreDatabase(checkpoint.snapshot);
break;
case 'file':
await this.restoreFile(checkpoint.snapshot);
break;
case 'workspace':
await this.restoreWorkspace(checkpoint.snapshot);
break;
case 'cart':
await this.restoreCart(checkpoint.snapshot);
break;
}
}
async restoreDatabase(snapshot) {
// Restore previous database values
await db.query(
`UPDATE ${snapshot.table} SET ? WHERE id = ?`,
[snapshot.previous_values, snapshot.record_id]
);
}
async restoreFile(snapshot) {
// Restore previous file content
await fs.writeFile(snapshot.path, snapshot.content, 'utf8');
await fs.utimes(snapshot.path, snapshot.metadata.atime, snapshot.metadata.mtime);
}
async restoreWorkspace(snapshot) {
// Restore workspace structure
await workspace.import(snapshot.workspace_id, snapshot.structure);
}
async restoreCart(snapshot) {
// Restore cart items
await cart.replace(snapshot.user_id, snapshot.items);
}
}
Step 2: Wrap Actions With Checkpoints
Make every action reversible:
class ReversibleAction {
constructor(checkpointManager) {
this.checkpoints = checkpointManager;
this.activeActions = new Map();
}
async execute(action) {
const actionId = this.generateActionId();
try {
// Step 1: Create checkpoint
const checkpoint = await this.checkpoints.createCheckpoint(
actionId,
action.stateType,
action.currentState
);
// Step 2: Execute action
console.log(`▶️ Executing: ${action.description}`);
const result = await action.handler();
// Step 3: Validate result
const validation = await this.validateResult(result, action);
if (!validation.safe) {
// Auto-rollback if validation fails
console.log(`⚠️ Validation failed: ${validation.reason}`);
await this.checkpoints.rollback(actionId);
return {
success: false,
rolled_back: true,
reason: validation.reason,
checkpoint: checkpoint.id
};
}
// Step 4: Commit (mark as successful)
this.activeActions.set(actionId, {
checkpoint: checkpoint.id,
action: action,
result: result,
timestamp: Date.now()
});
return {
success: true,
result: result,
actionId: actionId,
undo_available: true
};
} catch (error) {
// Auto-rollback on error
console.error(`❌ Error executing action: ${error.message}`);
await this.checkpoints.rollback(actionId);
throw error;
}
}
async validateResult(result, action) {
// Run validation checks
const checks = action.validations || [];
for (const check of checks) {
const valid = await check(result);
if (!valid.passed) {
return {
safe: false,
reason: valid.reason
};
}
}
return { safe: true };
}
async undo(actionId) {
const action = this.activeActions.get(actionId);
if (!action) {
throw new Error(`Cannot undo: action ${actionId} not found`);
}
// Rollback to checkpoint
await this.checkpoints.rollback(action.checkpoint);
// Remove from active actions
this.activeActions.delete(actionId);
console.log(`✓ Undone: ${action.action.description}`);
return {
undone: true,
action: action.action.description
};
}
async undoLast() {
// Undo most recent action
const lastActionId = Array.from(this.activeActions.keys()).pop();
if (lastActionId) {
return await this.undo(lastActionId);
}
throw new Error('No actions to undo');
}
generateActionId() {
return `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
Step 3: Integrate With Voice Agent
Make undo available via voice:
const checkpointManager = new CheckpointManager();
const reversibleActions = new ReversibleAction(checkpointManager);
const voiceAgent = {
model: "gpt-realtime",
tools: [
{
type: "function",
name: "delete_project",
description: "Delete a project. This action is reversible.",
parameters: {
type: "object",
properties: {
project_id: { type: "string", description: "Project identifier" },
project_name: { type: "string", description: "Project name for confirmations" }
},
required: ["project_id", "project_name"]
}
},
{
type: "function",
name: "undo_last_action",
description: "Undo the most recent reversible action.",
parameters: {
type: "object",
properties: {}
}
}
],
instructions: `You are a helpful assistant with undo capabilities.
When users take actions that modify data:
1. Execute the action (it's automatically checkpointed)
2. Confirm what you did: "Done - I've deleted that project"
3. Remind them they can undo: "If you need to undo this, just say 'undo that'"
When users say "undo", "undo that", "go back", or "revert":
- Call the undoLastAction tool
- Confirm what was undone: "Undone - I've restored that project"
`
};
Conversational Undo Patterns
Make undo feel natural:
Pattern 1: Immediate Undo
User: "Delete the draft project"
Agent: "Got it—deleting the draft project now..."
[Creates checkpoint, executes]
"Done. I've deleted the draft project. Say 'undo that' if you need to restore it."
User: "Wait, undo that!"
Agent: "Undoing now..."
[Rolls back to checkpoint]
"Done—I've restored the draft project. It's back."
Pattern 2: Preventive Confirmation
For high-stakes actions, confirm before executing:
async executeDangerousAction(action, client) {
if (action.high_risk) {
// Ask for confirmation first through the agent's response
// The agent will naturally ask and wait for user response
// This is handled through the conversation flow
// Return action details for agent to confirm
return {
requires_confirmation: true,
confirmation_message: `Just to confirm: you want me to ${action.description}? ` +
`Say 'yes' to proceed or 'cancel' to stop.`,
action: action,
// Execute only after user confirms in conversation
onConfirm: async () => {
return await reversibleActions.execute(action);
}
};
}
// Low-risk actions proceed immediately with undo option
return await reversibleActions.execute(action);
}
Pattern 3: Multi-Step Undo
Track multiple actions and allow selective undo:
User: "Delete projects A, B, and C"
Agent: [Deletes A, B, C with checkpoints]
"Done—deleted projects A, B, and C."
User: "Actually, undo just project B"
Agent: [Restores only project B]
"Restored project B. Projects A and C are still deleted."
Implementation:
async undoSpecific(description) {
// Find action matching description
const actions = Array.from(this.activeActions.entries());
const match = actions.find(([id, action]) =>
action.action.description.toLowerCase().includes(description.toLowerCase())
);
if (match) {
return await this.undo(match[0]);
}
throw new Error(`No action found matching: ${description}`);
}
Automatic Rollback on Guardrail Violations
Combine undo with safety guardrails:
class SafeReversibleAction extends ReversibleAction {
async execute(action) {
const actionId = this.generateActionId();
// Create checkpoint
const checkpoint = await this.checkpoints.createCheckpoint(
actionId,
action.stateType,
action.currentState
);
// Execute action
const result = await action.handler();
// Check guardrails
const guardrailCheck = await this.checkGuardrails(result, action);
if (guardrailCheck.violated) {
// AUTO-ROLLBACK on guardrail violation
console.log(`🛑 Guardrail violation: ${guardrailCheck.policy}`);
await this.checkpoints.rollback(actionId);
// Explain to user
await agent.speak(
`I started that action but stopped it because ${guardrailCheck.reason}. ` +
`Everything has been restored to how it was before.`
);
return {
success: false,
rolled_back: true,
reason: guardrailCheck.reason
};
}
// Commit if safe
this.activeActions.set(actionId, {
checkpoint: checkpoint.id,
action: action,
result: result
});
return {
success: true,
result: result,
actionId: actionId
};
}
async checkGuardrails(result, action) {
// Run safety checks
const policies = action.guardrails || [];
for (const policy of policies) {
const check = await policy(result);
if (check.violated) {
return {
violated: true,
policy: policy.name,
reason: check.reason
};
}
}
return { violated: false };
}
}
Example with guardrails:
tools: [
{
type: "function",
name: "update_pricing",
description: "Update product pricing with guardrails and auto-rollback.",
parameters: {
type: "object",
properties: {
product: { type: "string", description: "Product name" },
product_id: { type: "string", description: "Product identifier" },
old_price: { type: "number", description: "Current price" },
new_price: { type: "number", description: "Requested new price" }
},
required: ["product_id", "old_price", "new_price"]
}
}
]
const toolHandlers = {
update_pricing: async (params) => {
return safeReversibleActions.execute({
description: `Update pricing for ${params.product}`,
stateType: 'database',
currentState: { table: 'products', record_id: params.product_id },
handler: async () => db.products.update(params.product_id, { price: params.new_price }),
guardrails: [
async () => (
params.new_price > params.old_price * 2
? { violated: true, reason: "price increase exceeds 100% - likely a mistake" }
: { violated: false }
),
async () => (
params.new_price < 1.0
? { violated: true, reason: "price below minimum threshold" }
: { violated: false }
)
]
});
}
};
If guardrails trip, the action automatically rolls back and explains why.
Real Numbers: Before and After Undo
Teams who implemented reversible actions report:
Task attempt rate: 50% increase
Users more willing to try voice commands knowing they can undo.
User confidence: +47%
“I feel safe using this” ratings jumped significantly.
Support tickets: 35% reduction
Fewer “I made a mistake, can you help?” requests.
Error recovery time: 90% faster
Users self-recover via undo instead of contacting support.
One product manager told us: “We added undo and task completion rates shot up. Users who were hesitant before started using the agent for critical workflows. The safety net made all the difference.”
Best Practices for Reversible Actions
1. Be Selective
Not every action needs checkpoints:
Checkpoint these:
- Data modifications (create, update, delete)
- State changes (status, settings, configurations)
- Purchases or transactions
- Irreversible operations
Don’t checkpoint these:
- Read-only queries
- Idempotent operations (safe to repeat)
- Actions that are inherently reversible (e.g., toggles)
2. Set Expiration
Checkpoints shouldn’t live forever:
async cleanupExpiredCheckpoints() {
const now = Date.now();
const TTL = 60 * 60 * 1000; // 1 hour
for (const [id, checkpoint] of this.checkpoints.entries()) {
if (now - checkpoint.timestamp > TTL) {
this.checkpoints.delete(id);
console.log(`🗑️ Expired checkpoint: ${id}`);
}
}
}
// Run cleanup periodically
setInterval(() => this.cleanupExpiredCheckpoints(), 5 * 60 * 1000);
3. Handle Checkpoint Conflicts
What if state changed after checkpoint?
async rollback(actionId) {
const checkpoint = this.checkpoints.get(actionId);
// Check if state changed since checkpoint
const currentState = await this.getCurrentState(checkpoint.stateType);
const conflict = await this.detectConflict(checkpoint.snapshot, currentState);
if (conflict) {
throw new Error(
`Cannot undo: state has changed since action was taken. ` +
`Conflict: ${conflict.reason}`
);
}
await this.restoreState(checkpoint);
}
4. Communicate Undo Availability
Always tell users when undo is available:
async confirmAction(action, result) {
if (result.undo_available) {
await agent.speak(
`${action.confirmation_message} ` +
`If you need to undo this, just say "undo that".`
);
} else {
await agent.speak(action.confirmation_message);
}
}
Getting Started: Undo in Phases
Week 1: Identify high-risk actions that need undo
Week 2: Implement checkpoint system for those actions
Week 3: Add voice undo command and testing
Week 4: Integrate with guardrails for auto-rollback
Start with your scariest actions. Expand coverage over time.
Ready for Confident Users?
If you want this for mission-critical voice agents, reversible actions are essential.
Undo = confidence. Confidence = adoption.
Stop making users afraid of mistakes. Start giving them a safety net.
Want to learn more? Check out OpenAI’s Realtime API documentation for building reliable voice workflows and function calling guide for implementing safe tool-based actions.