Pause for a Sign-off
In the last lesson, the dispatcher booked a $180 overhaul without so much as a raised eyebrow. We need book_repair to stop when the stakes are high enough and ask a person first. Cheap jobs can continue immediately; expensive ones should wait for a yes.
Eve adds human approval through one field on the tool. Its durable session machinery handles the queueing, waiting, and resuming from the exact step where the run stopped. You saw that machinery stream as session.waiting back in 1.3. Here, you'll define when the tool should use it.
Outcome
book_repair runs cheap bookings straight through but parks expensive ones on a human approval, then resumes the booking from the exact step once you say yes.
Hands-on exercise
Open the book_repair you wrote in 3.1. You're adding exactly one field, approval, and a threshold constant. Everything else stays.
approval decides, before execute runs, whether this particular call needs a human first. It can be a blanket helper from eve/tools/approval, always(), once(), never(), or, when the decision depends on the input, your own predicate. Booking is the second case: a $20 flat repair shouldn't interrupt anyone, but a $180 overhaul should. So we gate on the quote:
approval: ({ toolInput }) =>
quoteCents(toolInput?.serviceIds ?? []) > APPROVAL_THRESHOLD_CENTS,The predicate sees the same input execute will, so we total the quote the same way execute does and compare it to the threshold. Bookings under the line proceed immediately. Over it, eve pauses the turn and surfaces an approval request before execute runs.
With that field in place, eve turns a true result from approval into a parked turn and a structured approval request. The channel renders the request as buttons in the TUI, Block Kit in Slack, or a control in your web app.
Try It
Restart the dev server. That resets the session, so any bike you saved back in 2.2 is gone (defineState lives and dies with the session). Name the bike in the booking itself so the dispatcher has what it needs. Start with a booking under the line; it behaves exactly like before:
you › I've got a rear flat on my Surly Disc Trucker, 700c. Book a flat repair for Tuesday morning.
⚙ book_repair { serviceIds: ["flat-fix"], slotId: "tue-10", bikeLabel: "Surly Disc Trucker" } ($20, under $150)
dispatcher › Booked! Flat Repair on your Surly Disc Trucker, Tuesday at 10:00am. $20.00.Now push it over the line and watch it stop. Pick a different slot, the Tuesday-morning one is taken now:
you › actually, do the full overhaul on the Surly too, Tuesday afternoon.
⚙ book_repair { serviceIds: ["tune-up-full"], slotId: "tue-14", bikeLabel: "Surly Disc Trucker" }
⏸ approval required: Full Overhaul, $180.00. Approve this booking? [approve] [deny]The turn is parked. Nothing was booked yet, approval runs before execute, so the slot is untouched while it waits. Approve it:
you › approve
↳ { booked: true, when: "Tue 2:00pm", services: ["Full Overhaul"], total: "$180.00" }
dispatcher › Approved and booked, Full Overhaul on the Surly, Tuesday at 2:00pm. $180.The booking resumed from the exact step it paused on, ran execute, and committed. If you'd denied it, execute never runs and the model is told the booking was declined.
Cheap bookings prompting for approval, or expensive ones sailing through? Your predicate is probably totaling the wrong thing, confirm it calls quoteCents(toolInput.serviceIds) and compares against the same threshold in cents. And make sure approval sits on the tool definition object, beside inputSchema, not inside execute.
Done-When
book_repairhas anapprovalpredicate keyed on the booking's total cost.- A booking under the threshold runs with no approval gate (no
⏸ approve/denypause, even if the dispatcher confirms conversationally). - A booking over the threshold parks on an approval request and books nothing until answered.
- Approving resumes and commits; denying skips
execute.
Solution
import { defineTool } from "eve/tools";
import { z } from "zod";
import { getService, quoteCents, bookSlot, formatUsd } from "../lib/shop.js";
const APPROVAL_THRESHOLD_CENTS = 15000; // anything over $150 needs a human yes
export default defineTool({
description:
"Book one or more services into an open slot for a customer's bike. " +
"Returns the confirmation and the total quote.",
inputSchema: z.object({
serviceIds: z.array(z.string()).min(1).describe("Service ids from lookup_service."),
slotId: z.string().describe("An open slot id from check_availability."),
bikeLabel: z.string().optional().describe("Which of the customer's bikes this is for."),
}),
// Cost-based gate: cheap jobs run straight through, big-ticket bookings park
// on an approval request. approval runs before execute and sees the tool
// input, so we re-derive the quote here the same way execute will.
approval: ({ toolInput }) =>
quoteCents(toolInput?.serviceIds ?? []) > APPROVAL_THRESHOLD_CENTS,
async execute({ serviceIds, slotId, bikeLabel }) {
const names = serviceIds.map((id) => getService(id)?.name ?? id);
const total = quoteCents(serviceIds);
const summary = `${names.join(" + ")}${bikeLabel ? ` (${bikeLabel})` : ""}`;
const slot = bookSlot(slotId, summary);
return { booked: true, when: slot.label, services: names, total: formatUsd(total) };
},
});Three lines of change (the threshold and the approval predicate, highlighted) turned a reckless tool into a responsible one. That's the payoff of eve treating approval as a property of the tool rather than a system you assemble: the danger and its guardrail live in the same file, and the durable runtime handles the hard part, holding a paused conversation open until a human answers. The dispatcher is now genuinely safe to put in front of people, which is exactly what Section 4 does.
Was this helpful?