Leadership

Splitting a Destructive Tool into Prepare and Commit

Antonio Evans10 min read

A split-screen scene in a clean technical style.
Key points

Key points

  1. 1A destructive call that plans and executes in one step hides the decision inside execution, so an approval covers intent rather than consequences.
  2. 2Splitting it into prepare and commit lets the approver see the exact resources, diff, plan hash and expiry before anything changes.
  3. 3Commit must reject any plan whose hash no longer matches the live state, apply idempotently, and have a before-image saved for rollback.
  4. 4The pattern is a policy choice as much as an API design: oversight needs knowledge, observation, control and intervention, not just a pause button.

Why one destructive call hides the decision

Consider the shape of a typical destructive tool: delete_workspace(id) or bulk_update_records(filter, patch). The agent supplies arguments, the server applies them, and a row disappears. The approval prompt, if there is one, reads like "the agent wants to run delete_workspace." A reviewer either trusts the agent or blocks the task entirely.

The problem is that the agent has already decided what to delete by the time anyone is asked. The reviewer is consenting to a category of action, not to a list of affected resources. Research on agent oversight makes this concrete: effective oversight requires knowledge, observation, control and intervention, and a pause button only matters if the user can recognize the need to pause and has a workable view of what the agent is doing Data & Society's oversight agenda. A prompt that says "run this tool" without showing the affected rows gives the reviewer none of those four.

This is not a theoretical risk. A Cursor agent deleted an entire production database, including volume-level backups, in about nine seconds while trying to fix a credential mismatch, because it held deletion permissions no legitimate task required the blast radius analysis. No approval step would have helped if the approval was "the agent wants to clean up." It might have helped if it had shown 412 affected tables and asked for a hash.

The deeper issue is divergence between the goal the user gave, the plan the agent formed and the execution it carried out; even small divergences change how work gets done, and some produce plausible-looking outputs via wrong methods the sociotechnical research agenda. If plan formation and execution happen in one call, the divergence is invisible. Split them, and the plan becomes an artifact a human can read.

In practice, most oversight arrangements provide moments of formal intervention while leaving the conditions for meaningful intervention underdeveloped, inviting a user to supervise without giving them a usable view Data & Society. A one-shot destructive tool is the canonical example.

The two-phase commit analogy, and where it stops

The pattern borrows its shape from distributed transactions. In two-phase commit, a prepare phase asks each participant whether it can promise to carry out an update, and only if every participant agrees does the commit phase carry it out; the coordinator also durably logs decisions so it can recover after a crash Martin Fowler's Two-Phase Commit. Participants write tentative updates to a log, vote yes or no, and only finalize when the coordinator sends commit; a single no vote aborts the transaction the 2PC walkthrough.

The analogy is useful but incomplete, and knowing where it breaks is what makes the API design work.

PropertyClassic 2PCPrepare/commit for a destructive tool
Locks held between phasesYes, participants hold locks until commit or abortUsually no; instead the commit re-validates state via plan hash
Prepare costTentative writes plus log recordsA read-only dry run plus a stored plan
Failure between phasesCoordinator recovers from its logToken expires; the plan is discarded or re-planned
Atomicity guaranteeAbsolute across participantsOptimistic: commit succeeds only if nothing changed
Who decidesCoordinator, mechanicallyA human (or policy) reviewing the visible plan

Classic 2PC holds locks, which is why it is expensive and why many systems avoid it the 2PC problem explained. You do not want an agent holding a write lock on a production table while a human finishes lunch. So the destructive-tool version is optimistic: prepare records the state it observed, and commit refuses to apply if the state moved.

What prepare returns

Prepare is a read-only call with the same inputs the destructive call would have taken. It does the planning work, changes nothing, and returns a structured plan. Five fields matter:

  1. Affected resources. An explicit list of IDs, paths or keys, not a count. If the list exceeds a threshold (say 500 rows), return the count plus a paginated sample and a way to download the full manifest.
  2. A diff. For updates, the field-level before and after values. For deletes, a summary of what disappears and what references it. For infrastructure changes, a rendered plan like the ones Terraform emits.
  3. A plan hash. A digest over the normalized inputs plus the observed state of every affected resource. This is the integrity anchor for commit.
  4. A before-image handle. Where the rollback data lives: a snapshot ID, an object-store key, a soft-delete marker. Prepare should say whether rollback will be possible and for how long.
  5. A commit token with an expiry. Short-lived, single-use, scoped to this plan and this principal. Five to fifteen minutes is usually enough for a person to read a diff and not enough for a leaked token to be dangerous later.

A response might look like this:

{
  "plan_id": "plan_8f21c",
  "summary": "Soft-delete 3 projects and revoke 14 API keys",
  "affected": {
    "projects": ["proj_a1", "proj_b7", "proj_c9"],
    "api_keys": ["key_11", "key_12", "... 12 more"]
  },
  "diff": [
    {"resource": "proj_a1", "field": "state", "from": "active", "to": "soft_deleted"},
    {"resource": "key_11", "field": "status", "from": "active", "to": "revoked"}
  ],
  "plan_hash": "sha256:4b1e...9d3f",
  "before_image": {"kind": "snapshot", "ref": "snap_2026_11_03_7c", "retain_days": 30},
  "commit_token": "ct_9x2...",
  "expires_at": "2026-11-03T14:12:00Z",
  "reversible_for_hours": 72
}

Two design notes. First, the summary line is for humans and the structured fields are for machines; generate the summary from the structured data rather than asking a model to write it freehand, so the two cannot disagree. Second, prepare should be cheap enough that the agent can call it freely while iterating. A dry run that costs nothing encourages exploration, which is why HubSpot requires a successful dry run and a returned digest before it will update or delete a workflow at all HubSpot's Agent CLI dry-run post.

What commit enforces

Commit takes the commit token and applies exactly the plan that was prepared. Four invariants:

  • Hash match. Recompute the plan hash from current state. If it differs from the stored hash, refuse with a conflict and force a new prepare. This is the check that makes approval meaningful: the human approved a specific set of resources in a specific state.
  • Idempotency. The same token applied twice must produce the same result and the same response, not a second deletion. Store the outcome keyed by token and return the memoized result on replay.
  • Before-image first. Write the rollback artifact and confirm it durably exists before the first mutation. If the snapshot fails, abort. This mirrors 2PC's requirement that participants ensure durability of their decisions before promising to commit Martin Fowler's Two-Phase Commit.
  • Expiry and single use. An expired or already-consumed token returns an error the agent can handle by re-planning. Never silently re-prepare inside commit.
commit(token):
  plan = load(token)
  reject if plan.consumed or now > plan.expires_at
  reject if recompute_hash(plan.inputs, observe(plan.affected)) != plan.plan_hash
  ensure before_image(plan) is durable
  mark token consumed (durably, before mutation)
  apply(plan.diff)
  return recorded result for this token

The order of the last three lines matters. If you consume the token after applying, a crash mid-apply leaves a retry that either double-applies or deadlocks. If you consume it before, a crash leaves a plan marked consumed that may be partially applied, which is why the before-image and an operation log should let you finish or roll back deterministically. This is exactly the recovery problem 2PC coordinators solve with a log, and the log content matters: it records transaction state, the participants, and the decision so in-doubt transactions can be resolved the 2PC walkthrough.

Worked example: 412 rows, three reviewers

Suppose a data team's agent is asked to purge records that fail a quality check. A naive tool would accept the filter and delete 412 rows.

With prepare and commit, here is how the numbers land:

  • The agent calls prepare with the filter. The server evaluates it and finds 412 matching rows across 3 tenants.
  • Prepare returns a manifest of 412 IDs, a diff summary, a hash over the 412 row versions, a snapshot handle, and a token expiring in 10 minutes.
  • Policy routes the plan to reviewers by tenant: tenant A has 290 rows and needs a data owner, tenants B and C have a combined 122 and need one reviewer. Reviewers see the manifest and the count.
  • One reviewer approves tenant A. Another approves B and C. Each approval is bound to the plan hash, so approving one reviewer's copy cannot be replayed into a second commit.
  • Between prepare and commit, an ingestion job updates 6 of the 412 rows. Commit recomputes the hash, finds a mismatch, and refuses. The agent re-prepares; now the manifest shows 412 rows with 6 updated versions, and the reviewers get a plan with a note that 6 rows changed since the first plan.

Without the hash, all 412 rows would have been deleted with the reviewers believing they approved the versions they saw. Without the split, reviewers would have been asked "may I purge low-quality records?" which approves nothing specific. The blast radius work on chained tool calls makes the same point from a security angle: a control that evaluates one call at a time approves a plan and misses the compounding expansion, because a tool result from one step becomes the input that widens the next Aurascape's blast radius analysis.

One more number worth planning for: set a manifest size threshold and treat anything above it as a separate approval class. A reviewer can read 50 rows. Nobody reads 50,000, and a reviewer who cannot read the plan is back to approving intent.

Putting the gate where the plan is visible

The gate belongs at prepare, not at commit. Commit is a mechanical check. The human decision happens when the plan is on screen.

A risk-tier table helps route plans before anyone is paged:

TierExample operationGate
AutomaticRead-only queries, logging, draftsNone; log only
Async reviewWrites to non-critical internal data, reversible within a dayAuto-apply, notify with plan and rollback link
Real-time approveProduction writes, external sends, cross-system changesPrepare pauses for human approval of the plan
Hard blockDestructive production operations, credential changesPrepare returns a plan but commit is disabled by policy

The real-time tier is where the split pays off; dry-run mode, where the agent explains what it would do without doing it, is the key pattern because the human reviews the plan, not the aftermath the risk classification matrix. Guardian-style oversight systems work the same way, operating in a loop of observing context, interpreting intended actions against policy, deciding to allow, block or escalate, and recording decision logs the guardian agent control loop. Prepare is the observation payload that loop needs; commit is the intervention point.

The authorization gap that this closes deserves a precise statement. OAuth scope answers "can this agent call this tool," while an approval gate answers "should this invocation execute, with these arguments, right now"; the two are different questions and scope cannot model dynamic risk context, where the same scope covers a sandbox field update and a 10,000-row production bulk update Scalekit on human-in-the-loop tool calling. Prepare returns the arguments and the affected set, which is exactly what scope cannot express.

Two operational details are easy to skip and expensive to skip. First, an agent that pauses for approval may resume with stale credentials; the credential valid at pause is not guaranteed to be valid at resume, which is where production approval implementations break Scalekit. Second, the acceptance criteria for a plan should be explicit enough to check mechanically: output format, conditions, failure cases and scope limits, defined before the agent runs rather than after acceptance criteria for agent tasks. A plan manifest with a hash and a resource list is a set of acceptance criteria in executable form.

If you are building agents that call destructive tools across many models and providers, the tool layer is where this pattern lives, and running it through a single gateway keeps the prepare and commit contract identical no matter which model plans the action. Quanta Inference exposes text, image, video, audio and embedding models behind one OpenAI-compatible API key, which makes it practical to keep the destructive-tool contract in your own code rather than per-provider adapters: Quanta Inference.

Failure modes and how to test for them

Every invariant above has a corresponding failure, and each one is testable before you ship.

The state-changed-since-prepare case is the most common. Test it by preparing a plan, mutating one affected row out of band, then calling commit with the token. Expect a 409-style conflict and a new prepare requirement, not a partial apply.

The double-commit case catches missing idempotency. Call commit twice with the same token in parallel. Expect one apply and two identical responses. If the second call returns "already done" but with different resource IDs, your token is not bound tightly enough.

The token-leak case checks expiry and scoping. Try a token from a different principal, then one from a different plan, then an expired one. All three should fail closed.

The crash-mid-apply case checks the log and before-image. Kill the process between the first and last mutation, restart, and verify the operation either completes or rolls back to the snapshot, deterministically and without human guesswork.

The reviewer-drift case is social rather than technical. Show a reviewer a plan, have someone else approve it, and confirm the approval is bound to the hash rather than to the plan ID. Otherwise a well-intentioned reviewer can approve a plan they never read.

Agents fail about half the time on representative programmable tasks, with planning errors and execution issues among the causes the failure analysis paper. A prepare step that surfaces the plan does double duty: it gives humans something to inspect, and it gives the agent a chance to see its own plan before acting, which is the same reason planning in read-only mode before execution is a recommended agent workflow Addy Osmani on specs for AI agents.

FAQ

FAQ

Isn't prepare just a dry run with extra steps?

It is a dry run with three additions that matter: a plan hash that commit enforces, a before-image handle that makes rollback concrete, and a short-lived token that binds the approval to this exact plan and principal. A dry run that only prints what would happen lets the agent run a slightly different action afterward.

What happens if the plan is stale when commit arrives?

Commit recomputes the hash from current state and refuses on mismatch. The agent then calls prepare again, and the new plan goes back through whatever gate the risk tier requires. Refusing is cheap; applying a stale plan to resources that have changed is how a purge deletes the wrong 6 rows.

Should the agent or the server generate the human-readable summary?

The server, derived from the structured diff, so the prose and the manifest cannot disagree. If a model writes the summary, a prompt-injected document could make the summary lie while the manifest stays honest, and reviewers read summaries. Guardians mitigate prompt injection by enforcing policy at the point of action, which only works if the action description is trustworthy guardian agents explained.

Does every destructive tool need this, or only production ones?

Apply the split wherever the action is irreversible and the affected set is not obvious from the arguments. Reads and drafts do not need it. Soft deletes with a long retention window can run at the async-review tier with a plan logged rather than pre-approved.

How long should the commit token live?

Long enough to read the diff, short enough that a leaked token is useless: five to fifteen minutes, refreshed by a new prepare rather than by extending the old token. Anything longer and the approval stops meaning "right now, with these arguments," which is the whole point.

Sources

Sources

  1. A Sociotechnical Research Agenda for the Oversight of AI Agents
  2. Agent Blast Radius: Bounding Worst-Case Impact Before ...
  3. Two-Phase Commit
  4. Two-Phase Commit (2PC) Protocol Explained for Beginners
  5. The Two-Phase Commit Problem: Why Distributed ...
  6. Using dry run to change workflows safely with Agent CLI
  7. AI Agent Blast Radius: How Chained Tool Calls Expand It
  8. Guardian Agents: How AI Oversight Systems Work - Sweet Security
  9. Human-in-the-Loop Tool Calling: Approval Gates for AI ...
  10. How to Write Acceptance Criteria for AI Agent Tasks
  11. A Closer Look at Why They Fail When Completing Tasks
  12. How to write a good spec for AI agents
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles