> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Jev (TypeSafe)

> Use Jev — TypeSafe's System One decision model — as Agent.model. Not a chat model. Every helper, option, and answer field.

# Jev (TypeSafe)

## In plain terms

Jev is a **decision** model. You send **state** (the facts) and typed **questions**. You get answers your code can `if` / `switch` on.

It does **not** write emails, stream tokens, or fill free-form tool arguments. Do not pair it with `Agent.deep()`.

Use it three ways:

| You want…                                      | Use                                                                                                                                            |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| The agent *is* the decision                    | `model: jev("jev-latest")` + `agent.run(input, { questions })`                                                                                 |
| A Claude / GPT agent *asks* Jev for a judgment | [JevToolkit](/toolkits/jev)                                                                                                                    |
| Score a chat agent's replies in CI             | [Jev as an eval judge](/eval/jev) — wrap `jev()` in `custom()`                                                                                 |
| Pick the next browser click                    | `new BrowserAgent({ planner: "jev" })` — [Browser Agents](/browser/overview#jev-planner). Do **not** set `model: jev()`.                       |
| Speak, then click                              | `VoiceAgent` + `browser.asTool()` with `planner: "jev"` — [Voice + Jev](/voice/overview#voice--jev-browser). Do **not** set `provider: jev()`. |

***

## The smallest working example

```typescript theme={null}
import { Agent, jev, choice, noul, score } from "@agentium/core";

const agent = new Agent({
  name: "ticket-triage",
  model: jev("jev-latest"),
});

const result = await agent.run("I was charged twice. Fix this ASAP.", {
  questions: {
    category: choice("What is this ticket about?", {
      billing: "Charges, invoices, refunds",
      technical: "Bugs, outages, API errors",
      other: null,
    }),
    urgent: noul("Does this need a response in under 1 hour?"),
    severity: score("How severe is customer impact?", [
      "none",
      "low",
      "medium",
      "high",
      "critical",
    ]),
  },
});

const answers = JSON.parse(result.text);
console.log(answers.category.choice); // "billing"
console.log(answers.urgent.noul);     // 0.91  (0 = no, 1 = yes)
console.log(answers.severity.score);  // 3     (index into the levels array)
```

Build a **new** `questions` object on every `run()` if the decision set changes. Same agent, different questions.

***

## Setup

<Tabs>
  <Tab title="SDK">
    ```bash theme={null}
    npm install @typesafe-ai/sdk
    ```

    `@typesafe-ai/sdk` is an optional peer of `@agentium/core`. Install it before the first `generate()` / `run()`. Without it you get:

    ```
    @typesafe-ai/sdk is required for Jev. Install it: npm install @typesafe-ai/sdk
    ```
  </Tab>

  <Tab title="Environment">
    Get an API key from [typesafe.ai](https://typesafe.ai):

    ```bash theme={null}
    export TYPESAFE_API_KEY="..."
    ```

    Optional override of the API root:

    ```bash theme={null}
    export TYPESAFE_BASE_URL="https://api.typesafe.ai"
    ```
  </Tab>
</Tabs>

***

## The three question helpers

Every question is a name you pick (`category`, `urgent`, …) mapped to one of these three helpers. Names become keys on the answer object.

```typescript theme={null}
import { choice, noul, score } from "@agentium/core";
```

These call `@typesafe-ai/sdk` when it is installed, and otherwise build the same wire-format objects (`{ type, instructions, criteria }`). You can import them even if the SDK is not installed yet.

### `choice(instructions, criteria)` — pick exactly one label

Use when the answer is a **closed list**: team, category, route, label.

```typescript theme={null}
choice("What is this ticket about?", {
  billing: "Charges, invoices, refunds",
  technical: "Bugs, outages, API errors",
  other: null,
})
```

<ParamField path="instructions" type="string" required>
  The question Jev answers. Write it as something a human would decide. Be specific: `"What is this support ticket about?"` is better than `"category"`.
</ParamField>

<ParamField path="criteria" type="Record<string, string | null>" required>
  The allowed labels. **Keys** are what you get back in `answer.choice`. **Values** are extra descriptions, or `null` if the key is already clear.

  * `"billing": "Charges, invoices, refunds"` — label plus hint
  * `"other": null` — label only, no extra text

  Need at least two keys. Jev picks **exactly one**.
</ParamField>

**What you get back**

| Field           | Type                     | Meaning                                                              |
| --------------- | ------------------------ | -------------------------------------------------------------------- |
| `type`          | `"choice"`               | Always `"choice"`.                                                   |
| `choice`        | `string`                 | The winning label (one of your keys).                                |
| `probabilities` | `Record<string, number>` | How likely each label was (they add up to \~1).                      |
| `confidence`    | `number`                 | How sure Jev is about the winner (0–1). Use this to gate automation. |

```typescript theme={null}
{
  type: "choice",
  choice: "billing",
  probabilities: { billing: 0.92, technical: 0.05, other: 0.03 },
  confidence: 0.87
}
```

```typescript theme={null}
const { choice: team, confidence } = answers.category;
if (team === "billing" && confidence > 0.8) {
  await routeToBilling(ticket);
}
```

<Accordion title="When to use choice vs noul vs score">
  * **choice** — one of a named set (`billing` / `technical` / `other`).
  * **noul** — yes or no, as a probability (`0.91` = probably yes).
  * **score** — how much, on an ordered rubric (`none` → `critical`). Index `0` is the lowest level.
</Accordion>

***

### `noul(instructions?, criteria?)` — how true is this?

**Noul** = probability that the statement is true. `0` is no. `1` is yes. `0.5` is a coin flip.

```typescript theme={null}
noul("Does this need a response in under 1 hour?")
```

<ParamField path="instructions" type="string">
  The yes/no statement. Phrase it so **true** means the thing you care about.

  Good: `"Does this need a human response in under 1 hour?"`

  Bad: `"urgency"` — Jev does not know what that word means here.
</ParamField>

<ParamField path="criteria" type="object">
  Optional hints for what yes and no mean.

  ```typescript theme={null}
  noul("Is this message safe to send to a customer?", {
    true: "No abuse, no leaked secrets, no legal risk",
    false: "Spam, insults, PII, or anything legal would block",
  })
  ```
</ParamField>

**What you get back**

| Field  | Type     | Meaning                      |
| ------ | -------- | ---------------------------- |
| `type` | `"noul"` | Always `"noul"`.             |
| `noul` | `number` | Probability of yes, `0`–`1`. |

```typescript theme={null}
{
  type: "noul",
  noul: 0.91
}
```

```typescript theme={null}
if (answers.urgent.noul >= 0.8) pageOncall();
else if (answers.urgent.noul >= 0.5) putInTodayQueue();
else scheduleLater();
```

<Note>
  When questions come from `structuredOutput`, a noul is flattened to a **boolean**: `noul >= 0.5` → `true`. On a normal `run({ questions })` you get the raw `0`–`1` number so you can pick your own threshold.
</Note>

***

### `score(instructions, levels)` — rate on an ordered rubric

Use when the answer is **how much**, not which label. Index `0` is the lowest level.

```typescript theme={null}
score("How severe is customer impact?", [
  "none — no real impact",
  "low — inconvenience",
  "medium — work blocked for one person",
  "high — many customers or money at risk",
  "critical — outage or data loss",
])
```

<ParamField path="instructions" type="string" required>
  What to rate. Same rule as `choice`: write a real question.
</ParamField>

<ParamField path="levels" type="string[]" required>
  Ordered rubric. **First item is 0. Last item is highest.** Need at least two levels.

  Short labels work (`"none"`, `"low"`, `"high"`). Longer text on each level is better — Jev uses that text to decide.

  When questions come from `z.number().min(1).max(5)`, Agentium builds levels `"1"` … `"5"` and later adds `min` back so `result.structured.severity` is `1`–`5`, not `0`–`4`.
</ParamField>

**What you get back**

| Field           | Type                     | Meaning                                             |
| --------------- | ------------------------ | --------------------------------------------------- |
| `type`          | `"score"`                | Always `"score"`.                                   |
| `score`         | `number`                 | **0-based index** into `levels`. `0` = first level. |
| `probabilities` | `Record<string, number>` | How likely each level was.                          |
| `confidence`    | `number`                 | How sure Jev is about the index (0–1).              |

```typescript theme={null}
{
  type: "score",
  score: 3,
  probabilities: { "0": 0.01, "1": 0.04, "2": 0.10, "3": 0.70, "4": 0.15 },
  confidence: 0.72
}
```

```typescript theme={null}
const levels = ["none", "low", "medium", "high", "critical"];
const severity = levels[answers.severity.score]; // "high"
```

<Warning>
  `score` is an index into `levels`, but the live API often returns a **fraction** (an expected value, e.g. `2.46`). Use `Math.round(answers.severity.score)` to pick a level. `structuredOutput` already rounds before it fills `result.structured`.
</Warning>

***

## `jev(modelId?, config?)`

```typescript theme={null}
import { jev } from "@agentium/core";

jev();                              // model id = "jev-latest"
jev("jev-1.13.0");                  // pin a version
jev("jev-latest", { apiKey: "..." });
```

<ParamField path="modelId" type="string" default="jev-latest">
  TypeSafe model id or alias.

  | Id            | Meaning                                                         |
  | ------------- | --------------------------------------------------------------- |
  | `jev-latest`  | Current stable (today: `jev-1.13.0`)                            |
  | `jev-1.13.0`  | Pin this if you have tuned confidence thresholds                |
  | `jev-preview` | TypeSafe preview alias — same as latest unless a preview is out |
</ParamField>

<ParamField path="config" type="JevConfig">
  Optional. See every field below.
</ParamField>

### `JevConfig`

<ParamField path="apiKey" type="string">
  TypeSafe API key. If omitted, uses `TYPESAFE_API_KEY`.

  You can also pass a per-run key: `agent.run(input, { apiKey, questions })`. That only swaps the key for that call.
</ParamField>

<ParamField path="baseURL" type="string">
  API root. Falls back to `TYPESAFE_BASE_URL`, then `https://api.typesafe.ai`.

  Use this for a proxy or a self-hosted TypeSafe endpoint.
</ParamField>

<ParamField path="questions" type="Record<string, Question>">
  **Default** questions if a run does not pass its own. Build them with `choice`, `noul`, and `score`.

  `agent.run(input, { questions })` **wins** and replaces these entirely (no merge).

  ```typescript theme={null}
  // Same questions every time — fine as a default
  const agent = new Agent({
    name: "moderation",
    model: jev("jev-latest", {
      questions: {
        safe: noul("Is this message safe to send?"),
      },
    }),
  });

  await agent.run(message); // uses constructor questions

  await agent.run(message, {
    questions: { spam: noul("Is this spam?") }, // constructor questions ignored
  });
  ```
</ParamField>

`temperature`, `topP`, `stop`, and `reasoning.effort` are **ignored**. Jev is not a sampler.

***

## `agent.run(input, { questions })`

This is the normal way to ask Jev.

<ParamField path="input" type="string | ContentPart[]">
  The **state** — the facts Jev judges. Usually a string (a ticket, a message, a JSON blob).

  * Plain text → sent as that string
  * Text that parses as JSON (`{...}` or `[...]`) → sent as the parsed object
  * Earlier turns in the same session → packed as `{ input, history }` so Jev still sees the thread
</ParamField>

<ParamField path="opts.questions" type="Record<string, Question>">
  Named `choice` / `noul` / `score` questions for **this run only**. Wins over `jev(model, { questions })`. Ignored by chat models (`openai`, `anthropic`, …).

  Keys are yours. Use names you want to read later (`category`, `urgent`).
</ParamField>

`agent.stream(input, { questions })` takes the same options. Jev still makes **one** `systemOne` call, then yields the JSON as a single `text` chunk and `finish`. There is no token stream.

### Different questions per call

```typescript theme={null}
const agent = new Agent({ name: "triage", model: jev("jev-latest") });

await agent.run(billingTicket, {
  questions: {
    refund: noul("Should we issue a refund?"),
    amount: score("How large is the charge dispute?", ["small", "medium", "large"]),
  },
});

await agent.run(outageTicket, {
  questions: {
    page: noul("Should we page on-call right now?"),
    scope: choice("What is down?", {
      api: "Public API",
      web: "Customer website",
      unknown: null,
    }),
  },
});
```

Same agent. Two different decision sets. No need to rebuild `jev()`.

***

## Where questions come from

Jev will not invent a prompt. `generate()` picks the **first** source that has at least one question:

| Priority | Source                            | When to use                                                                                                                    |
| -------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| 1        | `agent.run(input, { questions })` | Different questions per call. **Best.**                                                                                        |
| 2        | `jev(model, { questions })`       | Same questions every run.                                                                                                      |
| 3        | `Agent` `structuredOutput`        | Zod enums → `choice`, booleans → `noul`, `z.number().min().max()` → `score`. Answers are flattened so the schema still parses. |
| 4        | `tools`                           | One `__tool__` choice over tool names plus `none`. If the pick is a tool, the loop gets `tool_calls` with `{}` args.           |

No questions, no mappable schema, no tools → throws:

```
Jev has nothing to ask. Pass questions on agent.run(input, { questions }), jev(model, { questions }), structuredOutput, or closed-set tools.
```

An empty object `{}` does **not** count. Priority falls through to the next source.

***

## What `run()` gives back

`result.text` is a JSON string of TypeSafe answers (unless you used `structuredOutput` — see below).

```typescript theme={null}
const answers = JSON.parse(result.text);
```

| You also get                    | Meaning                                                    |
| ------------------------------- | ---------------------------------------------------------- |
| `result.usage.promptTokens`     | TypeSafe `input_tokens`. This is what you pay for.         |
| `result.usage.completionTokens` | Usually `0`. Output is free.                               |
| `result.raw`                    | Full TypeSafe response (`answers`, `model`, `usage`).      |
| `result.finishReason`           | `"stop"` for answers, `"tool_calls"` if a tool was picked. |
| `result.structured`             | Only when `structuredOutput` is set. Flattened primitives. |

Typical raw answers:

```json theme={null}
{
  "category": {
    "type": "choice",
    "choice": "billing",
    "probabilities": { "billing": 0.92, "technical": 0.05, "other": 0.03 },
    "confidence": 0.87
  },
  "urgent": { "type": "noul", "noul": 0.91 },
  "severity": { "type": "score", "score": 3, "confidence": 0.72 }
}
```

***

## State — what Jev actually sees

The last user message becomes TypeSafe `state`.

| Input                                       | State sent to TypeSafe                                     |
| ------------------------------------------- | ---------------------------------------------------------- |
| `"I was charged twice."`                    | `"I was charged twice."`                                   |
| `'{"ticket":"dup charge","plan":"pro"}'`    | `{ ticket: "dup charge", plan: "pro" }`                    |
| Session with older turns + new `"now this"` | `{ input: "now this", history: [{ role, content }, ...] }` |

JSON parse failures stay as text. System / assistant / earlier user turns land in `history`.

You can send structured state yourself:

```typescript theme={null}
await agent.run(JSON.stringify({
  subject: "Double charge",
  body: "I was billed twice for March.",
  plan: "pro",
  accountAgeDays: 400,
}), { questions });
```

***

## `structuredOutput` as questions

If you do **not** pass `questions`, a Zod object on the agent becomes questions:

| Zod field                        | Becomes                                        | Flattened answer                 |
| -------------------------------- | ---------------------------------------------- | -------------------------------- |
| `z.enum(["a", "b"])`             | `choice` (each enum value, description `null`) | the string (`"a"`)               |
| `z.boolean()`                    | `noul`                                         | `true` if `noul >= 0.5`          |
| `z.number().int().min(1).max(5)` | `score` with levels `"1"`…`"5"`                | `min + score` so you get `1`–`5` |

`.describe("…")` becomes the question text. Without it, the field name is the question.

```typescript theme={null}
import { z } from "zod";

const agent = new Agent({
  name: "triage",
  model: jev("jev-latest"),
  structuredOutput: z.object({
    category: z.enum(["billing", "technical", "other"]).describe("Ticket topic"),
    urgent: z.boolean().describe("Needs a reply in under an hour"),
    severity: z.number().int().min(1).max(5).describe("Customer impact"),
  }),
});

const result = await agent.run("I was charged twice.");
console.log(result.structured);
// { category: "billing", urgent: true, severity: 3 }
```

Number ranges must be **integers**, `max >= min`, and **2–32** levels. `z.number()` without `.min()` / `.max()` throws.

These **cannot** become questions:

* free-form `z.string()`
* open `z.object({ ... })`
* dates
* unbounded numbers
* arrays

```
Jev cannot map "email": use z.enum, z.boolean, or z.number().min().max() — free-form strings and open objects are not questions.
```

<Note>
  `run({ questions })` still wins over `structuredOutput`. If you pass both, Jev asks the run questions and `result.text` is the raw TypeSafe object (not flattened). Skip `questions` if you want `result.structured` to parse.
</Note>

***

## Tools

Jev cannot fill `send_email({ body: "..." })`. It can only pick a **name**.

### Auto `__tool__`

If there are no run / constructor / schema questions, but the agent has tools, Agentium asks one choice:

```
Which tool should run for this request? Pick none if no tool is needed.
```

Labels = tool names + `none`. Descriptions come from each tool's `description`.

If the pick is a registered tool (not `none`), `finishReason` is `"tool_calls"` and the existing loop runs that tool with `{}` arguments.

### A named choice that matches a tool

Constructor / run questions are **not** replaced by auto `__tool__`. But if a `choice` value equals a registered tool name and is not `none`, that tool still runs.

```typescript theme={null}
import { defineTool } from "@agentium/core";
import { z } from "zod";

const escalate = defineTool({
  name: "escalate",
  description: "Page on-call",
  parameters: z.object({}),
  execute: async () => "paged",
});

const agent = new Agent({
  name: "router",
  model: jev("jev-latest"),
  tools: [escalate],
});

await agent.run("The API is down for everyone.", {
  questions: {
    action: choice("What should we do?", {
      escalate: "Page on-call now",
      queue: "Put it in the queue",
    }),
  },
});
// If Jev picks "escalate", the escalate tool runs with {}.
```

Closed-set / no-arg tools only.

***

## Models and price

| Id            | Notes                                                           |
| ------------- | --------------------------------------------------------------- |
| `jev-latest`  | Alias for the current stable release (`jev-1.13.0`)             |
| `jev-1.13.0`  | Pin this if you have tuned confidence thresholds                |
| `jev-preview` | TypeSafe preview alias — same as latest unless a preview is out |

Input is billed at **\$0.042 / million tokens**. Output is free. TypeSafe evaluates every question in a request **in parallel** against the same state — adding questions barely changes latency. See [TypeSafe models](https://docs.typesafe.ai/models).

***

## Errors you will actually see

| Message                                                      | Why                                       | Fix                                                |
| ------------------------------------------------------------ | ----------------------------------------- | -------------------------------------------------- |
| `@typesafe-ai/sdk is required for Jev`                       | Peer not installed                        | `npm install @typesafe-ai/sdk`                     |
| `Jev has nothing to ask`                                     | No questions, schema, or tools            | Pass `{ questions }` on `run()`                    |
| `Jev cannot map "email"`                                     | Schema field is a free string / object    | Use `z.enum`, `z.boolean`, or bounded `z.number()` |
| `Jev cannot map "n": number fields need minimum and maximum` | Unbounded number                          | Add `.min()` and `.max()`                          |
| `number range must be 2–32 integer levels`                   | Range too small, too big, or not integers | Keep 2–32 integer steps                            |
| `Jev structuredOutput must be a Zod object with properties`  | Schema is not an object                   | Wrap fields in `z.object({ ... })`                 |

***

## More examples

### Ticket triage (run a few tickets)

```typescript theme={null}
const questions = {
  category: choice("What is this support ticket about?", {
    billing: "Charges, invoices, refunds, double billing",
    technical: "Bugs, outages, API errors, login failures",
    other: "Anything else",
  }),
  urgent: noul("Does this need a human response in under 1 hour?"),
  severity: score("How severe is the customer impact?", [
    "none — no real impact",
    "low — inconvenience",
    "medium — work blocked for one person",
    "high — many customers or money at risk",
    "critical — outage or data loss",
  ]),
};

for (const ticket of tickets) {
  const result = await agent.run(ticket, { questions });
  const a = JSON.parse(result.text);
  console.log(ticket);
  console.log(" ", a.category.choice, a.urgent.noul, a.severity.score);
}
```

Runnable file: [jev-triage.ts](https://github.com/agentiumOS/agentium-examples/blob/main/models/jev-triage.ts).

```bash theme={null}
TYPESAFE_API_KEY=... npx tsx examples/jev-triage.ts
```

### Branch in your code, not in a prompt

```typescript theme={null}
const a = JSON.parse(result.text);

if (a.category.choice === "billing" && a.urgent.noul > 0.7) {
  await slack.post("#billing-oncall", ticket);
} else if (a.severity.score >= 3) {
  await jira.create({ priority: "high", body: ticket });
} else {
  await queue.push(ticket);
}
```

### Moderation defaults on the constructor

```typescript theme={null}
const agent = new Agent({
  name: "moderation",
  model: jev("jev-latest", {
    questions: {
      safe: noul("Is this message safe to send to a customer?", {
        true: "Polite, no secrets, no legal risk",
        false: "Abuse, spam, PII, or legal risk",
      }),
      kind: choice("If it is unsafe, what kind?", {
        spam: null,
        abuse: null,
        pii: null,
        none: "It is safe",
      }),
    },
  }),
});

const a = JSON.parse((await agent.run(draft)).text);
if (a.safe.noul < 0.8) throw new Error(`blocked: ${a.kind.choice}`);
```

### JSON state for a richer ticket

```typescript theme={null}
await agent.run(JSON.stringify({
  from: "nina@acme.com",
  plan: "enterprise",
  subject: "API 500s since 9am",
  body: "No one can create orders.",
  similarTicketsToday: 40,
}), { questions });
```

***

## What not to do

* `Agent.deep({ model: jev() })` — the deep harness (skills, subagents, filesystem) expects a chat model
* Open-ended chat, reflection-as-prose, or free-form tool args
* Using Jev as a drop-in swap for `anthropic()` without `questions` or `structuredOutput`
* Treating `score` as "points out of N" — it is a **0-based index**
* Expecting `run({ questions })` to merge with constructor questions — it **replaces** them
* Passing `jev()` to `llmJudge` or `AgentJudgeEval` — those need a chat model. Use [`custom()` + Jev](/eval/jev)

***

## See also

* [Jev examples](/examples/jev)
* [Jev as an eval judge](/eval/jev) — score a chat agent with `noul` / `score` / `choice`
* [JevToolkit](/toolkits/jev) — same three primitives as tools on a chat agent
* [RunOpts.questions](/agents/types-reference#runopts)
* [Docs index](/)
* [TypeSafe introduction](https://docs.typesafe.ai/introduction)
* [TypeSafe JS SDK](https://docs.typesafe.ai/sdk/javascript)
* [TypeSafe models](https://docs.typesafe.ai/models)
