Create your own
Lesson illustration

Bounded Structured Extraction with Allowlisted Functions in TypeScript

Good to see you again. In the previous lesson, you built a bounded reranking workflow: Jev evaluated a fixed candidate set, while deterministic Python code owned thresholds, ordering, and fallback behavior.

This lesson applies the same architectural boundary to tool-like requests in a TypeScript application. A user may write, “Where is order ORD-104928?” or “Download invoice INV-883104.” Jev may determine which supported intent the request expresses, but it must not invent a function name, execute code, or supply unchecked data to a backend.

By the end, you will have a TypeScript design that:

  • maps natural-language requests to a fixed allowlist of operations with a Jev Choice,
  • extracts identifiers only through deterministic code,
  • validates constructed arguments with Zod,
  • returns a typed command plan rather than executing model output directly,
  • routes unknown, ambiguous, or incomplete requests into a safe clarification path.

Structured extraction is not unrestricted tool calling

A common LLM integration gives a generative model a list of functions and asks it to emit a function name plus JSON arguments. That can be useful, but the output is still generated token by token. Even if it resembles JSON, application code must parse, validate, authorize, and constrain it before execution.

Jev supports a narrower approach: use its fixed-choice judgment to select from operations you have already decided are permissible. Then let ordinary TypeScript construct and validate the command.

A comparison of a chat LLM generating a JSON-like response token by token and a System One model filling fixed decision fields in parallel. For bounded extraction, the application controls the fixed operation set and validates the resulting arguments before any backend action.

The boundary is:

ComponentResponsibility
JevClassify the request into one of your allowed operations
TypeScriptExtract known identifier formats, construct arguments, validate schemas, authorize, and dispatch
Backend serviceEnforce tenant ownership, permissions, business rules, and audit logging

A schema is important, but it is not authorization. A value such as ORD-104928 may be structurally valid while belonging to another customer. The order service must still verify that the authenticated user can access it.

For this lesson, use three read-only capabilities:

Allowlisted operationArgumentsExample request
get_order_statusorderId“What is the status of order ORD-104928?”
download_invoiceinvoiceId“Download invoice INV-883104.”
show_return_policyregion from trusted account state“What is your return policy?”
unsupportedNone“Change my delivery address.”

The unsupported option is intentional. If the request does not fit your current product contract, the correct outcome is not a guessed operation.


Use Choice as the bounded operation selector

A Jev Choice is appropriate because operation names are a fixed, unordered set. Its response contains the winning option, probabilities for every option, and confidence.

Choice - TypeSafe AI

Read TypeSafe AI’s Choice documentation to ground the command classifier in the intended primitive. It also explains why criteria descriptions matter as much as option names.

Start in the “Choice” section and read the primitive overview. Then, in “Request structure,” read the request design guidance, focusing on the relationship between a question ID, instructions, and criteria. Finally, in “Structured instructions and criteria,” read the guidance on detailed criteria. Notice that a criterion can state both what belongs to an option and what explicitly belongs elsewhere.

Define the operation contract in one module. Keeping all options, descriptions, and thresholds together makes the behavioral part of the integration easy to review.

// src/command-contract.ts

export const COMMAND_CRITERIA = {
  get_order_status: {
    what: "The user asks to view the current status, progress, or delivery state of one order.",
    not_for: "Downloading an invoice, changing an order, cancelling an order, or asking about general policy.",
    examples: [
      "Where is order ORD-104928?",
      "Is ORD-104928 shipped yet?",
    ],
  },

  download_invoice: {
    what: "The user asks to view, retrieve, email, or download an invoice for one purchase.",
    not_for: "Checking an order's delivery status or asking about a charge without requesting an invoice.",
    examples: [
      "Download invoice INV-883104.",
      "I need the invoice for INV-883104.",
    ],
  },

  show_return_policy: {
    what: "The user asks about the company's general return eligibility, deadlines, or procedure.",
    not_for: "The status of a specific return, cancelling an order, or requesting an exception.",
    examples: [
      "What is your return policy?",
      "How many days do I have to return an item?",
    ],
  },

  unsupported: {
    what: "The request does not fit any supported operation, lacks enough information to identify one operation, or requests a change or action not supported here.",
    examples: [
      "Change my delivery address.",
      "Cancel my order.",
      "Talk to a person.",
    ],
  },
} as const;

export type CommandName = keyof typeof COMMAND_CRITERIA;

export const COMMAND_QUESTIONS = {
  command: {
    type: "choice",
    instructions:
      "Which supported operation best matches the user's request? " +
      "Choose unsupported when the request does not clearly match one supported operation. " +
      "Do not infer an operation that is not explicitly offered.",
    criteria: COMMAND_CRITERIA,
  },
} as const;

Two design details matter here:

  1. Operation names are implementation identifiers. They should be stable, readable, and owned by your application. A user never gets to supply one.
  2. Criteria make boundaries explicit. get_order_status says it is not for download requests; download_invoice says it is not for delivery tracking. These negative boundaries reduce accidental overlap.

TypeSafe’s guidance recommends an other or none_of_the_above option when the allowlist does not cover every input. Here, unsupported serves that role.

A deep dive into Jev, TypeSafe's System One model - Flavio Copes

Read Flavio Copes’ discussion of Choice and question-writing practice for a concise explanation of why bounded options and explicit exits are essential.

In the “Choice: pick one option” part, read the Choice design guidance. Then find “Writing questions Jev answers well” and read the question-writing advice. Focus especially on the rule to keep numbers, dates, and counting in deterministic code.


Build a typed command plan, not a dynamic function call

The model response should become an internal plan. It should not become a property lookup such as handlers[answer.choice](...), and it should never become eval() or dynamically imported code.

First, define Zod schemas for the argument objects accepted by each backend capability.

// src/command-schemas.ts

import { z } from "zod";

export const AccountRegionSchema = z.enum(["US", "EU"]);

export const GetOrderStatusArgsSchema = z
  .object({
    orderId: z.string().regex(/^ORD-\d{6}$/, "Expected an order ID such as ORD-104928."),
  })
  .strict();

export const DownloadInvoiceArgsSchema = z
  .object({
    invoiceId: z.string().regex(/^INV-\d{6}$/, "Expected an invoice ID such as INV-883104."),
  })
  .strict();

export const ShowReturnPolicyArgsSchema = z
  .object({
    region: AccountRegionSchema,
  })
  .strict();

export type GetOrderStatusArgs = z.infer<typeof GetOrderStatusArgsSchema>;
export type DownloadInvoiceArgs = z.infer<typeof DownloadInvoiceArgsSchema>;
export type ShowReturnPolicyArgs = z.infer<typeof ShowReturnPolicyArgsSchema>;

The .strict() call is deliberate. It rejects unexpected properties rather than silently accepting them. That prevents a future refactor from accidentally treating extra request data as trusted command input.

Now represent only valid plans as a discriminated union:

// src/command-plan.ts

import type {
  DownloadInvoiceArgs,
  GetOrderStatusArgs,
  ShowReturnPolicyArgs,
} from "./command-schemas.js";

export type CommandPlan =
  | {
      kind: "get_order_status";
      args: GetOrderStatusArgs;
    }
  | {
      kind: "download_invoice";
      args: DownloadInvoiceArgs;
    }
  | {
      kind: "show_return_policy";
      args: ShowReturnPolicyArgs;
    };

export type ClarificationPlan = {
  kind: "clarify";
  message: string;
  reason:
    | "unsupported_request"
    | "low_confidence"
    | "missing_or_invalid_identifier";
};

export type PlannedOutcome = CommandPlan | ClarificationPlan;

This union is a useful frontend and backend boundary. A UI can render a clarification message without knowing anything about Jev. A server dispatcher can execute only the three recognized CommandPlan variants.


Extract identifiers deterministically and validate them

Do not ask a model to invent arbitrary IDs. Your application already knows their syntax.

For this example, orders follow ORD- plus six digits, and invoices follow INV- plus six digits. A small deterministic extractor finds a candidate token, normalizes its case, and lets Zod decide whether it is valid.

// src/extract-identifiers.ts

function requireIdentifier(
  request: string,
  expression: RegExp,
  label: string,
): string {
  const match = request.match(expression);

  if (!match) {
    throw new Error(`Missing ${label}.`);
  }

  return match[0].toUpperCase();
}

export function extractOrderId(request: string): string {
  return requireIdentifier(
    request,
    /\bORD-\d{6}\b/i,
    "order ID such as ORD-104928",
  );
}

export function extractInvoiceId(request: string): string {
  return requireIdentifier(
    request,
    /\bINV-\d{6}\b/i,
    "invoice ID such as INV-883104",
  );
}

This is deliberately narrow. It will not “helpfully” interpret a random number as an order ID. If product requirements later permit multiple formats, add them explicitly, update the Zod schema, and test the new behavior.

A natural-language request now has two independent parts:

PartSourceValidation
Intended supported operationJev ChoiceAllowlisted CommandName plus confidence policy
Order or invoice identifierDeterministic parserRegex plus Zod schema
Customer’s policy regionAuthenticated account stateZod enum

A request such as “Where is ORD-104928?” can produce this internal plan:

{
  kind: "get_order_status",
  args: {
    orderId: "ORD-104928",
  },
}

The user cannot turn that into delete_all_orders, because no such plan exists in the TypeScript union, no criterion names it, and no dispatcher case implements it.


Convert the Jev answer into a safe plan

The API call should be a thin boundary around an otherwise pure planning function. The following response types reflect the documented Choice response shape: selected option, probability distribution, and confidence.

// src/plan-command.ts

import {
  DownloadInvoiceArgsSchema,
  GetOrderStatusArgsSchema,
  ShowReturnPolicyArgsSchema,
  type AccountRegionSchema,
} from "./command-schemas.js";

import type { z } from "zod";

import { extractInvoiceId, extractOrderId } from "./extract-identifiers.js";
import type { CommandName } from "./command-contract.js";
import type { PlannedOutcome } from "./command-plan.js";

type AccountRegion = z.infer<typeof AccountRegionSchema>;

export type CommandChoiceAnswer = {
  choice: CommandName;
  confidence: number;
  probabilities: Record<CommandName, number>;
};

export function planFromChoice(
  request: string,
  accountRegion: AccountRegion,
  answer: CommandChoiceAnswer,
): PlannedOutcome {
  if (answer.choice === "unsupported") {
    return {
      kind: "clarify",
      reason: "unsupported_request",
      message:
        "I can help check an order status, download an invoice, or explain the return policy.",
    };
  }

  // Initial policy only. Calibrate it on labeled requests in Week 4.
  if (answer.confidence < 0.7) {
    return {
      kind: "clarify",
      reason: "low_confidence",
      message:
        "I am not sure which supported request you mean. Please ask about an order status, an invoice, or the return policy.",
    };
  }

  try {
    switch (answer.choice) {
      case "get_order_status":
        return {
          kind: "get_order_status",
          args: GetOrderStatusArgsSchema.parse({
            orderId: extractOrderId(request),
          }),
        };

      case "download_invoice":
        return {
          kind: "download_invoice",
          args: DownloadInvoiceArgsSchema.parse({
            invoiceId: extractInvoiceId(request),
          }),
        };

      case "show_return_policy":
        return {
          kind: "show_return_policy",
          args: ShowReturnPolicyArgsSchema.parse({
            region: accountRegion,
          }),
        };
    }
  } catch {
    return {
      kind: "clarify",
      reason: "missing_or_invalid_identifier",
      message:
        "Please include the identifier, such as ORD-104928 for an order or INV-883104 for an invoice.",
    };
  }
}

Notice what this code does not do:

  • It does not use a model-produced string as a function name.
  • It does not let the model produce unrestricted JSON.
  • It does not treat a high-confidence answer as permission to bypass validation.
  • It does not take region from the user’s message when authenticated account state already supplies it.
  • It does not quietly substitute one operation for another when an identifier is missing.

The confidence threshold of 0.7 is a provisional product policy. In a production system, evaluate it on labeled request data. A low confidence response should usually lead to clarification, while an unsupported result should never execute a supported operation merely because its confidence is high.


Call Jev, then keep execution deterministic

The exact client initialization is the one you configured in Week 1 with your TypeSafe API key. Keep the request state minimal: Jev needs the request to classify, not account IDs, permissions, billing data, or backend records.

// src/handle-request.ts

import { COMMAND_QUESTIONS } from "./command-contract.js";
import { planFromChoice, type CommandChoiceAnswer } from "./plan-command.js";
import type { PlannedOutcome } from "./command-plan.js";

type SystemOneClient = {
  systemOne(input: {
    state: { request: string };
    questions: typeof COMMAND_QUESTIONS;
  }): Promise<{
    answers: {
      command: CommandChoiceAnswer;
    };
  }>;
};

type AccountRegion = "US" | "EU";

export async function planUserRequest(
  client: SystemOneClient,
  request: string,
  accountRegion: AccountRegion,
): Promise<PlannedOutcome> {
  if (!request.trim()) {
    return {
      kind: "clarify",
      reason: "unsupported_request",
      message: "Please describe what you need help with.",
    };
  }

  const response = await client.systemOne({
    state: { request },
    questions: COMMAND_QUESTIONS,
  });

  return planFromChoice(
    request,
    accountRegion,
    response.answers.command,
  );
}

The next boundary is dispatch. It is intentionally an ordinary switch statement, with no generic registry based on untrusted strings.

// src/dispatch-command.ts

import type { CommandPlan, PlannedOutcome } from "./command-plan.js";

async function getOrderStatus(orderId: string, userId: string) {
  // The backend query must enforce that userId may access this orderId.
  return { orderId, status: "shipped" };
}

async function createInvoiceDownloadUrl(invoiceId: string, userId: string) {
  // The backend must verify invoice ownership before returning a URL.
  return { invoiceId, url: "https://example.invalid/signed-download-url" };
}

async function getReturnPolicy(region: "US" | "EU") {
  return { region, policyVersion: "2026-01" };
}

export async function dispatchPlan(
  plan: PlannedOutcome,
  userId: string,
) {
  if (plan.kind === "clarify") {
    return {
      type: "clarification",
      message: plan.message,
      reason: plan.reason,
    };
  }

  return dispatchCommand(plan, userId);
}

async function dispatchCommand(
  command: CommandPlan,
  userId: string,
) {
  switch (command.kind) {
    case "get_order_status":
      return getOrderStatus(command.args.orderId, userId);

    case "download_invoice":
      return createInvoiceDownloadUrl(command.args.invoiceId, userId);

    case "show_return_policy":
      return getReturnPolicy(command.args.region);
  }
}

Even though all three examples are read-only, preserve this separation for write operations too. A future cancel_order capability should have its own schema, authorization rules, explicit confirmation UI, idempotency handling, and audit record. Do not add it as “just another Choice option” without those surrounding controls.


Test the deterministic boundary without calling Jev

The most valuable tests here do not need live API calls. planFromChoice is pure: provide a recorded Choice answer and inspect the resulting plan.

// src/plan-command.test.ts

import { describe, expect, it } from "vitest";
import { planFromChoice } from "./plan-command.js";

describe("planFromChoice", function () {
  it("creates a validated order-status plan", function () {
    const result = planFromChoice(
      "Where is order ord-104928?",
      "EU",
      {
        choice: "get_order_status",
        confidence: 0.91,
        probabilities: {
          get_order_status: 0.91,
          download_invoice: 0.03,
          show_return_policy: 0.01,
          unsupported: 0.05,
        },
      },
    );

    expect(result).toEqual({
      kind: "get_order_status",
      args: { orderId: "ORD-104928" },
    });
  });

  it("asks for clarification when an invoice ID is absent", function () {
    const result = planFromChoice(
      "Please download my invoice",
      "US",
      {
        choice: "download_invoice",
        confidence: 0.95,
        probabilities: {
          get_order_status: 0.01,
          download_invoice: 0.95,
          show_return_policy: 0.01,
          unsupported: 0.03,
        },
      },
    );

    expect(result).toMatchObject({
      kind: "clarify",
      reason: "missing_or_invalid_identifier",
    });
  });
});

Add tests for at least these cases as you implement:

Input conditionExpected outcome
Clear order-status request with valid ORD- IDValid get_order_status plan
Clear invoice request with valid INV- IDValid download_invoice plan
Correct request but no IDClarification, never a guessed ID
Low-confidence model answerClarification
unsupported answerClarification
Validly formatted ID belonging to a different accountBackend authorization denial

The final row cannot be validated in this module alone. It belongs in the backend service test suite, because authorization requires real ownership data.


Key takeaways

Bounded structured extraction is a controlled composition of model judgment and deterministic code:

  • Use Jev Choice to select from a fixed, reviewed set of operations.
  • Include an explicit unsupported option rather than forcing a best-fit operation.
  • Keep free-form IDs, date calculations, authorization, and execution out of the model decision.
  • Construct arguments in TypeScript and validate them with strict Zod schemas.
  • Return a typed plan first; dispatch only recognized plan variants.
  • Treat confidence gates as policies to evaluate and calibrate, not as guarantees of correctness.

Next, you will begin Week 4 by building a labeled evaluation dataset from representative states, expected decisions, and review outcomes. That dataset will let you test command routing and confidence policies against real examples instead of intuition alone.

Can't find a good explanation? Sign up and we'll make it for you

Sign up