Recipes← all recipesView on GitHub

Commit a transaction from a call

AI Agentsvoice transaction confirmation

Use an allow-list to validate the caller's confirmation before one transaction tool commits the request.

tool-callingcontexts

The claim

Three tools, one write. set_order records the items in global_data and marks the order unconfirmed. confirm_order takes an answer argument: the words the model relays from the caller after the readback. The handler marks the order confirmed only when it finds that answer in an allow-list of whole phrases. commit_order writes to the order book once, and only when confirmed is true in the global_data the platform posted with the call.

Why it holds

The model proposes each step and relays the words. The handlers decide whether the step counts, from state they wrote themselves. No sequence of model calls writes an order without an answer the code accepted as a yes.

How it works

def confirm_order(self, args, raw_data):
    order = raw_data.get("global_data", {}).get("order")
    if not order:
        return FunctionResult("INCOMPLETE: there is no order to confirm yet.")
    readback = f"{', '.join(order['items'])} for {order['total']:.2f}"
    if normalise(args.get("answer")) not in YES:
        return FunctionResult(f"NOT_A_YES: the caller did not clearly agree to {readback}.")
    r = FunctionResult(f"Confirmed: {readback}. You may commit it now.")
    r.add_action("set_global_data", {"confirmed": True})
    return r

normalise lower-cases the answer, expands “that’s” and “it’s”, and drops punctuation and four politeness words. The handler then looks the result up in a set of whole answers. Membership, never substring: “yesterday” contains “yes” and is not a yes, and neither is “I can’t say yes”. commit_order checks three things in order: the book already has this call_id, there is an order, and confirmed is true.

Every refusal is a response with no action, so the payload carries the reason and changes nothing. set_order emits "confirmed": false alongside the order every time:

{"response": "Order noted: helmet, puncture-kit, total 101.50. Read it back ...",
 "action": [{"set_global_data": {"order": {"items": ["helmet", "puncture-kit"],
                                           "total": 101.5},
                                 "confirmed": false}}]}

A caller who confirms and then changes their mind therefore confirms again. The order book is keyed by call_id, which makes the commit idempotent per call: a repeated commit_order returns the id already on file.

The items parameter carries an enum of the catalogue. The handler drops anything outside it anyway, and refuses a list with nothing left.

Limitations

The handler judges the words it is given. The model relays them; nothing here proves they are the caller’s, and a stricter recipe would take the answer from DTMF or a prompt the platform collected.

The yes-set is small on purpose, and “sure thing” is asked again. Extend YES with whole phrases, never with substrings.

The order book is a dictionary in the process. Your version writes to a system that can itself refuse a duplicate keyed by call_id, because a restart between two commits would forget the first.

What to change first

Delete the NOT_CONFIRMED check in commit_order and run the verifier. The commit-before-confirmation assertion fails and the book has an order nobody confirmed, which is the failure this recipe exists to prevent.