Vercel AI SDKAI SDK 7 · TypeScript · Node.js 20+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
17 small wins to finish your path

Support desk with approval and tests

Put the course together: typed triage, an agent with lookups and refunds that need approval over a limit, a runner, and tests that check the rules.

Exampledesk.ts
import { generateText, Output, ToolLoopAgent, isStepCount, type ModelMessage } from "ai";
import type { LanguageModel } from "ai";
import { Ticket } from "./ticket.ts";
import { lookupOrder } from "./tools.ts";
import { refundOrder } from "./refund.ts";

export function createDesk(model: LanguageModel, refundLimit = 50) {
  const agent = new ToolLoopAgent({
    model,
    instructions: "You answer support tickets for an online shop.",
    tools: { lookupOrder, refundOrder },
    toolApproval: { refundOrder: async ({ amount }) => (amount > refundLimit ? "user-approval" : undefined) },
    stopWhen: isStepCount(5),
  });

  async function triage(ticket: string) {
    const { output } = await generateText({ model, output: Output.object({ schema: Ticket }), prompt: ticket });
    return output;
  }

  async function answer(messages: ModelMessage[]) {
    const result = await agent.generate({ messages });
    const approvals = result.content.filter((part) => part.type === "tool-approval-request");
    return { text: result.text, approvals, responseMessages: result.response.messages };
  }

  return { triage, answer };
}
  • createDesk takes the model, so the app, tests and a real provider use the same code.
  • triage is structured output with the Ticket schema from lesson 7.
  • answer runs the agent and reports pending approvals, with the messages needed to continue after a decision, as in lesson 9.
  • refundLimit is a setting, not a prompt instruction, so no wording can change it.
Exampleapp.ts
import { createDesk } from "./desk.ts";
import { shopModel } from "./shop-model.ts";

const desk = createDesk(shopModel);

for (const ticket of ["I was charged twice", "Where is A-1001?", "Refund 30 euros on A-1001", "Refund 90 euros on A-1002"]) {
  const triage = await desk.triage(ticket);
  const reply = await desk.answer([{ role: "user", content: ticket }]);
  const waiting = reply.approvals.map((a) => `${a.toolCall.toolName} ${JSON.stringify(a.toolCall.input)}`);
  console.log(`${ticket}\n  ${triage.category}/${triage.priority} | ${reply.text || "waiting for approval: " + waiting}`);
}
Example
npx tsx app.ts

Every ticket was triaged. The status question used the lookup tool, the 30 euro refund ran, and the 90 euro refund is waiting, with the tool call a person would review.

Four tickets through createDesk
ticketapp.ts sends fourtriageOutput.object, Ticket schemaToolLoopAgentat most 5 stepslookupOrderan order's statusrefundOrderover 50 euros: needs approval
Hover or tap a piece to see what it is and which lesson built it.
Send a ticket

Testing the approval rule

Exampledesk.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createDesk } from "./desk.ts";
import { shopModel } from "./shop-model.ts";

test("small refunds run without approval", async () => {
  const reply = await createDesk(shopModel).answer([{ role: "user", content: "Refund 30 euros on A-1001" }]);
  assert.equal(reply.approvals.length, 0);
  assert.equal(reply.text, "Done: refunded 30 euros on A-1001.");
});

test("large refunds wait for a person", async () => {
  const reply = await createDesk(shopModel).answer([{ role: "user", content: "Refund 90 euros on A-1002" }]);
  assert.equal(reply.approvals.length, 1);
  assert.equal(reply.text, "");
});

test("the refund limit is a setting", async () => {
  const reply = await createDesk(shopModel, 100).answer([{ role: "user", content: "Refund 90 euros on A-1002" }]);
  assert.equal(reply.approvals.length, 0);
});
Example
npx tsx --test desk.test.ts

The tests check the approval rule end to end: a refund under the limit runs, one over it waits and never runs, and the limit comes from code.

Running the desk on a hosted model

Pass openai("gpt-4.1-mini") to createDesk in app.ts and set OPENAI_API_KEY. Serve desk.answer from the chat route in lesson 14, and keep the stand-in in the tests.

What the SDK offers beyond the desk

TopicWhat it is for
Embeddings and rerankingembed, embedMany and rerank for retrieval.
MCP clientsLoading tools from MCP servers.
Generative UIRendering React components from tool results.
Message persistenceSaving and resuming chats with useChat.
Images, speech and transcriptiongenerateImage, generateSpeech and transcribe.
TelemetryOpenTelemetry spans for every call.
Other UI frameworksSvelte, Vue and Angular bindings.

Little by little, you're building something great.