Vercel Logo

Pause for a Sign-off

In the last lesson, the dispatcher booked a $180 overhaul without so much as a raised eyebrow. The fix is not to make book_repair smarter, it's to make it stop when the stakes are high enough, and ask a person first. Cheap jobs still sail through. Big ones wait for a yes.

In a lot of frameworks, this is where you'd go build a whole approval system: a queue, a state machine, a way to remember where you were when the human finally clicks "approve" an hour later. In eve, human-in-the-loop isn't a system you build. It's one field on the tool. The pausing, the waiting, the resuming-exactly-where-it-stopped, that's the same durable session machinery you watched stream as session.waiting back in 1.3. You just tell the tool when to 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. Under the line, no approval, the booking just happens. Over it, eve pauses the turn and surfaces an approval request before execute ever runs.

Helper or predicate?

Reach for always()/once()/never() when the answer is the same every time (a delete_everything tool is always()). Reach for a predicate when it depends on the arguments, like our cost gate. Same approval field, two shapes.

One field, and you're done. You don't write the pause, the prompt, or the resume. eve turns a true from approval into a parked turn and a structured approval request, and the channel renders it, buttons in the TUI, Block Kit in Slack, 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). No problem, just name the bike in the booking itself and the dispatcher works from that. Book under the line first, 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.
A spoken “shall I confirm?” is not the approval gate

Your dispatcher may still ask "shall I go ahead and book?" before the cheap job. That's the persona being courteous, not eve stopping the turn, and the two are different things. The tell is what you don't see: no ⏸ approval required … [approve] [deny] control. Under the line, approval returns false, so eve never pauses and execute runs the instant the model calls the tool, whatever the model says around it. The hard gate, the structured pause that blocks execute until a human approves, is the thing you're about to trigger with the overhaul.

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.

This is the durable pause from 1.3, doing real work

Remember session.waiting in the event stream? This is it earning its keep. The parked turn isn't holding a process open, it's durably suspended. The approval can come a minute later or, on a deployed agent, an hour later from a manager's phone. When it arrives, the turn picks up precisely where it stopped.

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_repair has an approval predicate keyed on the booking's total cost.
  • A booking under the threshold runs with no approval gate (no ⏸ approve/deny pause, 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

agent/tools/book_repair.ts
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?

supported.