Chapter 18

Split a Refund Into Preview, Approval, and Commit

A refund is an irreversible action, and the agent doesn't perform it itself. The agent loop may prepare the calculation (preview), the user confirms the exact calculation (approval), and a separate service re-checks the whole business policy and executes an idempotent transaction (commit). Three steps - three levels of trust.

Preview is a tool: it checks ownership and policy and creates a record in the trusted store.

TypeScript
type RefundPreview = {
  id: string;
  actorId: string;
  orderId: string;
  orderVersion: number;
  amountMinor: number;
  currency: string;
  reasonCode: string;
  expiresAt: string;
  status: "pending";
};

async function refundPreview(raw: unknown, context: TrustedToolContext) {
  const input = refundPreviewInput.parse(raw);
  const order = await requireOwnedOrder(input.order_id, context);
  const decision = await refundPolicy.evaluate(order, input.reason_code);

  if (!decision.allowed) return { eligible: false, reason: decision.publicReason };
  const preview = await previews.createFromDecision(order, decision, context.actorId);
  return { eligible: true, preview_id: preview.id };
}

And commit is a separate endpoint that the model doesn't call at all.

TypeScript
export async function POST(request: Request) {
  const actor = await requireSession(request);
  const body = approvalInput.parse(await request.json());

  const result = await db.transaction(async function commit(tx) {
    const preview = await tx.refundPreview.lock(body.preview_id);
    requireSameActor(preview, actor.id);
    requirePendingAndFresh(preview, new Date());

    const order = await tx.order.lock(preview.orderId);
    requireVersion(order, preview.orderVersion);
    await refundPolicy.revalidate(order, preview);

    const refund = await payments.refund({
      orderId: order.id,
      amountMinor: preview.amountMinor,
      currency: preview.currency,
      idempotencyKey: `refund:${preview.id}`
    });

    await tx.refundPreview.markCommitted(preview.id, refund.id);
    return refund;
  });

  return Response.json({ status: "completed", refund_id: result.id });
}

Look at what happens in commit: it locks the preview and the order, verifies the actor, the validity, the order version, re-checks the policy, and executes the payment with an idempotency key. None of these values comes from the model.

"The user agreed" is not a tool argument. The confirmation is tied to a specific preview id, actor, amount, validity, and order version. Changing any of these requires a new preview, not a new "yes."

The refund is split and protected. But not every case should the agent decide itself - sometimes the right outcome is a handoff to an operator, and you must pass proven context.

Knowledge check

Who executes the refund commit?

Links