Command Palette

Search for a command to run...

Hectal
PHASE 8Advanced ~29 min· topic 9 of 9

Topic 8.9

Build: Mitra Takes Actions

In one line

Mitra gets two tools. It looks up an order and shows it as a card, and it cancels an order only after the customer reads exactly what will happen and presses Yes. Failures, refusals and double clicks are handled, and the server checks everything again.

Think of it like this

The market stall from Phase 0 now has a till and a back room. A helper can fetch an item from the back room when you ask (a lookup), but takes anything off the books only after the owner has read the slip and signed it. Mitra learns the same two kinds of work: fetching is free, and changing needs a signature.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

lookupOrder
Mitra's read-only tool: finds an order by id.
cancelOrder
Mitra's changing tool: cancels an order that is still processing, only after approval.
OrderCard
The component that draws a tool part in each of its states.
Approval
The component that shows the request text and the Yes and No buttons.
not-applicable
A toolApproval answer meaning "no approval needed here". Mitra uses it when the cancel could not happen anyway.
Waiting for your answer
Mitra's status while an approval card is open.

Step by step

01Start from Phase 7's Mitra

You have Mitra with a route, useChat, a message list and a composer (#messages, #prompt, #send, #stop, #error, #status), and cards from structured output. This topic adds two tools to the route and two component branches to Message. Everything else stays.

02Step 1: the tools

Add lookupOrder (read-only) and cancelOrder (changes data) to the tools object, and set stopWhen: stepCountIs(4). Both return { error } objects for expected problems and never throw for them.

route.ts (the tools)whole filets
tools: {
  lookupOrder: tool({
    description: "Look up one order by its id",
    inputSchema: z.object({ orderId: z.string() }),
    execute: async ({ orderId }) => {
      const o = ORDERS[orderId];
      return o ? { orderId, item: o.item, status: o.status } : { error: "I could not find " + orderId + "." };
    },
  }),
  cancelOrder: tool({
    description: "Cancel an order that is still being prepared",
    inputSchema: z.object({ orderId: z.string() }),
    execute: async ({ orderId }) => {
      const o = ORDERS[orderId];
      if (!o) return { error: "I could not find " + orderId + "." };
      if (o.status === "cancelled") return { error: orderId + " is already cancelled." };
      if (o.status !== "processing") return { error: orderId + " is " + o.status + ", so it can no longer be cancelled." };
      o.status = "cancelled";
      return { orderId, item: o.item, status: "cancelled" };
    },
  }),
},

03Step 2: ask the person

Add toolApproval for cancelOrder. The function returns "not-applicable" when the cancel cannot happen, so the model gets the tool's own explanation and the person is not asked about nothing. Otherwise it returns a user-approval request with text built from the order.

route.ts (approval)whole filets
toolApproval: {
  cancelOrder: ({ orderId }) => {
    const o = ORDERS[orderId];
    if (!o || o.status !== "processing") return "not-applicable";
    return { type: "user-approval", reason: "Cancel " + orderId + ": " + o.item + ", Rs " + o.price + ". This cannot be undone." };
  },
},

04Step 3: draw the parts and answer the card

In Message, send approval-requested parts to Approval and every other tool part to OrderCard. Pass useChat's addToolApprovalResponse down as respond. Add sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses.

Approval keeps an answered flag so both buttons lock on the first press. Use part.toolCallId as the key.

05Step 4: lock the composer while a card is open

Compute waitingForYou from the parts. While it is true the status reads "Waiting for your answer" and the send guard in submit still refuses a second question because the chat is not idle. Try asking something else while a card is open in the example: the answer to the card comes first.

06Step 5: run it

The scripted visitor in the last example asks about ORD-1042, asks to cancel ORD-1043 (the card appears; it presses Yes), then tries to cancel the shipped ORD-1042 and the already-cancelled ORD-1044, and finally says "Hello". Each step waits for the page, not for a timer. The last line shows the order table at the end: only ORD-1043 changed.

Step 5: run itdiagram
Rendering diagram…

Try it yourself

  1. 1

    Press No

    Run the example, wait for it to finish, then type Cancel ORD-1043 again. It is already cancelled, so there is no card. To see the No path, change ORD-1043's status back to processing in the table, press Run, and click No, keep it by hand when the card appears.

    The status goes back to Ready, the card shows Cancellation not approved., and Mitra says it did not cancel.

  2. 2

    Ask for something unknown

    Type Cancel ORD-9999.

    There is no card (the order does not exist, so the approval function answers not-applicable) and Mitra says "I could not find ORD-9999."

Code & diagrams

Mitra with tools and approval

The model is a scripted stand-in and the order table lives in the page for the demo. In a real app the table is your database and the route is app/api/chat/route.ts. The visitor waits for the page each time, so this prints the same on any machine.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

you: Where is ORD-1042? | Mitra: ORD-1042 · Masala chai 250 g · shipped / ORD-1042 is shipped.
approval card: Cancel ORD-1043: Ginger biscuits, Rs 180. This cannot be undone. | status: Waiting for your answer
(server) cancelled ORD-1043
you: Cancel ORD-1043 | Mitra: ORD-1043 · Ginger biscuits · cancelled / ORD-1043 is cancelled.
you: Cancel ORD-1042 | Mitra: ORD-1042 is shipped, so it can no longer be cancelled.
you: Cancel ORD-1044 | Mitra: ORD-1044 is already cancelled.
you: Hello | Mitra: I can look up an order or cancel one that has not shipped. Which order number?
order table now: ORD-1042 shipped, ORD-1043 cancelled, ORD-1044 cancelled

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Delete toolApproval

In the example delete the whole toolApproval: { … }, block from the route and press Run.

terminal
$ Console
── what you'll see ──
you: Where is ORD-1042? | Mitra: ORD-1042 · Masala chai 250 g · shipped / ORD-1042 is shipped.
(server) cancelled ORD-1043
you: Cancel ORD-1043 | Mitra: ORD-1043 · Ginger biscuits · cancelled / ORD-1043 is cancelled.

Myth vs fact

Myth

Mitra decides when to cancel.

Fact

Mitra only asks. The server decides whether a request is allowed, and the customer decides whether it happens.

Myth

Approval is only needed when the model is unsure.

Fact

Approval depends on what the action can do, not on how sure the model sounds. A confident wrong cancel is just as permanent.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Keep the order table and its rules in one server module that both tools and toolApproval use. If they each hold their own idea of "cancellable", the card and the action will disagree.

  • ▸

    The approval function runs before execute, and the world can change in between. execute repeats the status check, so a cancel approved a minute late is still refused if the order shipped meanwhile.

  • ▸

    Log each approval request, each answer and each execution with the user and order id. You need it the first time someone says they never agreed.

  • ▸

    Keep Phase 3's Stop button: it also stops a pending request, and a stopped chat must not leave an approval card that can still be pressed.

Remember this

  1. 1

    The problem this solves: Until Phase 7, Mitra could talk and show cards but could not touch an order. Customers do not want a chat about cancelling, they want the order cancelled, safely. This topic assembles Topics 8.1 to 8.8 into one small program: tools on the server, parts drawn in the browser, an approval card and checks on every side.

  2. 2

    What Mitra can do now that it could not before. Look up an order by id and show a card (lookupOrder). Cancel an order that is still processing (cancelOrder), but only after a person approves a card built from the server's own data. Refuse politely when the order does not exist, has shipped, or is already cancelled. Not run anything if the person says No. Lock the input while it waits for the answer, and keep the buttons from being used twice.

  3. 3

    What is still missing, and where it comes from. Mitra still uses a scripted stand-in model, not a real one (a real provider is a one-line change on the route, Topic 8.2). It does not yet know who is signed in, so the owner check from Topic 8.8 is not in this version (sign-in is outside this course, and per-user limits are Topic 13.2). Hostile text is not yet filtered (Phase 9). Cost and budgets across a whole conversation are Topics 13.1 and 13.2. Tests and evals for tool use are Topics 13.6 and 13.7. Accessibility of streaming UI and international text are Topics 10.6 and 10.7.

  4. 4

    The server part. One route with two tools. lookupOrder waits a little and returns the order or an { error }. cancelOrder checks the order and changes its status. toolApproval for cancelOrder returns "not-applicable" when the cancel could not happen anyway (so the person is not asked about something impossible), and otherwise a user-approval request whose reason is built from the order data: item, price, and "This cannot be undone.".

  5. 5

    The browser part. useChat with sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses. Message draws each part: text as a paragraph, tool parts through OrderCard, approval requests through Approval. OrderCard draws every state (Topic 8.3) and hides { error } results, since the assistant explains them in words. Approval has a local answered flag that disables both buttons on the first press.

  6. 6

    Mitra's states now include a new one, "Waiting for your answer", shown while an approval is pending. In that state the composer is locked (a new question while a card is open would be confusing), and #status says why. #stop appears while a request runs, as in Phase 3.

  7. 7

    Every scenario this version handles. *Normal:* a lookup, a card, a sentence. *Success with approval:* the cancel is asked, approved, run once. *Order missing:* "I could not find…" with no card. *Wrong state:* shipped orders cannot be cancelled, and no approval is asked. *Already cancelled:* a clear sentence. *Denied:* nothing runs, the model is told. *Double click:* the second press does nothing. *Unknown request:* Mitra asks which order. The last example runs five of these in one session.

Explain it without notes

01

Describe what happens from the sentence "Cancel ORD-1043" to the order being cancelled, naming each piece (tool, approval, card, resend).

02

Why does Mitra's approval function return not-applicable for a shipped order?

03

How does Mitra avoid cancelling twice when the person double clicks Yes?

04

What does Mitra still lack at the end of this phase, and which phase adds each missing piece?

Practice

01

Add a third tool returnPolicy that returns { days: 7 } and let Mitra answer "What is the return window?". Which parts of the browser code change?

02

Make the composer show "Answer the card above first" as its placeholder while an approval is pending.

Trade-offs

  • ↔

    Asking before every cancel is slower than a button that cancels at once. It is the right price for an action nobody can take back, and a wrong choice for a harmless one.

  • ↔

    Hiding { error } results in cards keeps the screen clean and makes the model's sentence carry the message. If the model words it badly, the user gets the wrong words, so test those phrases.

Done when you can

  • I can add a read tool and a changing tool to the Mitra route.

  • I can draw each tool state and an approval card from server-built text.

  • I can lock the composer while a card is open.

  • I can show the checks that hold even when the person approves.

  • I can say what Mitra still needs and which phase adds it.