Skip to main content

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:

The smallest working example

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

Setup

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

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.
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.
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".
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.
What you get back
  • 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 (nonecritical). Index 0 is the lowest level.

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.
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.
object
Optional hints for what yes and no mean.
What you get back
When questions come from structuredOutput, a noul is flattened to a boolean: noul >= 0.5true. On a normal run({ questions }) you get the raw 01 number so you can pick your own threshold.

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

Use when the answer is how much, not which label. Index 0 is the lowest level.
string
required
What to rate. Same rule as choice: write a real question.
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 15, not 04.
What you get back
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.

jev(modelId?, config?)

string
default:"jev-latest"
TypeSafe model id or alias.
JevConfig
Optional. See every field below.

JevConfig

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.
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.
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).
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.
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
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).
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

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: No questions, no mappable schema, no tools → throws:
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).
Typical raw answers:

State — what Jev actually sees

The last user message becomes TypeSafe state. JSON parse failures stay as text. System / assistant / earlier user turns land in history. You can send structured state yourself:

structuredOutput as questions

If you do not pass questions, a Zod object on the agent becomes questions: .describe("…") becomes the question text. Without it, the field name is the question.
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
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.

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:
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.
Closed-set / no-arg tools only.

Models and price

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.

Errors you will actually see


More examples

Ticket triage (run a few tickets)

Runnable file: jev-triage.ts.

Branch in your code, not in a prompt

Moderation defaults on the constructor

JSON state for a richer ticket


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

See also