Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

AI types

On this page

The AI types are the values an app passes to and from a model: chat messages and their roles, vectors and embeddings, and token counts. AI covers the work you do with them: the ai capability and its operations ai.generate, ai.stream, ai.embed and ai.embed_all, with their failures (The ai capability); prompts as checked files; vectors stored in pgvector and the nearest-neighbour query; budgets; personal data sent to a model; and the fake that tests answer from.

The models themselves come from provider packages (External services). Jadpo renders the prompt from a checked file and decodes the answer into a type, as it renders an email and hands it to a sender. Training a model is left out: a package or an outside service trains it, and your app calls the result. This section describes the types themselves.

Messages

  • ChatRole is a standard enum of a message's four roles, system, user, assistant and tool. It is written and shown as its variant's name in every locale, as every provider's API and a transcript write a role.
  • ChatMessage is its role and its content, any text: the shape OpenAI's, Anthropic's and Gemini's APIs share, {"role": "user", "content": "..."}. A message's name, a tool call's id, and content parts (images, tool calls and results as blocks) aren't part of it, and a member for any of them is refused; AI leaves tool calling and content parts out. It is stored as a text column each, and shown after its role: user: Which soups are vegan?.
  • A message is personal data, health, whatever field holds it (Personal data). What a person writes to a model, or is written back, is free text that can hold anything, special category details included. So it takes the sensitive category that fits such text: it is never cached, and every read is recorded.

Vectors

An embedding is a list of numbers a model makes from a text, so that texts with similar meanings have nearby vectors. A Vector holds one.

Its items are single-precision floats, as pgvector's vector column stores them and as models make embeddings, so storing one loses nothing. Each item is held as the decimal that is the nearest float to the number it was built or read from, rounded half to even as IEEE 754 rounds, in its shortest form.

  • Building one from 0.123456789 keeps 0.12345679, as scale: keeps a decimal to its places, and a vector never breaks format.
  • A JSON number is rounded once, from its text as written to the nearest float. So a float a client wrote out as a double (-0.006929283495992422) is that float again (-0.0069292835), and a vector written and read back is the vector it was.
  • A decimal holds 38 places, so an item below 1e-30 is held as 0. That is far below a float's precision beside an embedding's other items. An item of 1e38 or more is refused, as max (or min below 0) at its place in the array.
  • JSON has no NaN or infinity, so neither can be read.

A vector is a list. To code it is a List<Decimal>, so list.sum and the other list operations take it. It has 1 to 16,000 items, the most pgvector allows in a vector column. It is built from a list, Vector([0.5, -0.25]). It is sent as a JSON array of numbers, never as decimals in text as other Decimals are, because every provider sends one that way. It is shown in the locale's digits in brackets, joined by ; , as a line's points are: [0.5; -0.25; 1]. == compares its items in order. It isn't ordered.

dimensions: fixes a vector type's length: type ArticleVector = Vector { dimensions: 1536 }, a whole number from 1 to 16,000. It goes only on a type built on Vector; elsewhere it is type.constraint_base.

  • A vector of another length breaks dimensions, a rule named for its setting, as from and to are. It is checked first, so where it decides the length it settles min_length and max_length too.
  • It is proved for a list written out with that many items, or a value of a type with the same dimensions:. JSON, json.read and OpenAPI (minItems and maxItems) check it, and the API record writes it.
  • A plain Vector has any length, so an Embedding can hold any model's. min_length and max_length narrow a vector type as they narrow a list.
  • A type built on a vector type repeats its dimensions: or leaves it out. One outside its lengths, or a different one, is type.constraint_contradiction, as a different scale: is.

A vector type also holds its items to what its distance: and its index need: not all zeros when compared by cosine, breaking distance, and within ±65,504 when indexed at half precision, breaking min and max (Storing a vector and The vector index).

No text form. A vector, an embedding, a usage and a message have no text form: text.from and text.parse on one are type.mismatch, as on a PostalAddress. A vector's JSON array is its one form, and it is also pgvector's text.

Embeddings and token counts

  • Embedding is its model, a ModelId, and its vector, a plain Vector. It is sent as named fields, its model as ModelId's text, and shown as its model, then its vector.
  • TokenCount is a whole number from 0 to JavaScript's largest safe integer, as UnitQuantity's count is, so a period's totals fit too. It is sent as a JSON number, stored in a bigint, and shown in the locale's digits with no unit, since "1 tokens" needs a plural form CLDR doesn't name. It is ordered.
  • TokenUsage is a call's input, output and cached tokens, the last none where a provider doesn't say. cached counts the input tokens the provider's cache served, within input, as OpenAI and Gemini count them, so it is at or below input: its rule cached_within_input. A provider package that counts them apart, such as Anthropic's, adds them in.
  • A usage's total is input + output, worked out where it is needed and never kept, so it can't disagree. A total member is refused, as any unknown field is. A usage is sent as named fields, cached as null when it is none, stored as a bigint column each, and shown by labels, the same in every locale, as projected coordinates are: input 1,200, output 350, cached 800.

Vectors, embeddings and counts aren't personal data on their own. Mark an embedding of a person's text with that text's category.

Stored and matched

Stored: a vector is one pgvector vector(n) column (Storage::Vector), and an embedding is three columns, its model's two and its vector (Storing a vector). A count is one bigint column. A usage and a message are stored as a column per part.

Shapes: none of these has a distinctive shape for type.standard_exists. A { role, content } or an { input, output } is too common to flag.

Here a help article's embedding is three numbers, where a real model's are 1,536 or more. A list of the wrong length is refused by name:

example
type ModelReply = Text {}type EmbeddingUnreadable = Failure {    kind: FailureKind.invalid_value}/// A help article's embedding, as the support site's model makes it.type ArticleVector = Vector {    dimensions: 3}/// A help article's embedding from the model's reply: JSON that isn't one/// at all, or a vector of another length.function article_vector(model_reply: ModelReply) fails EmbeddingUnreadable -> ArticleVector {    return attempt json.read(model_reply, as: ArticleVector) {        malformed: EmbeddingUnreadable        mismatch: EmbeddingUnreadable    }}
example
test "an embedding has its model's length" {    var article_meaning: ArticleVector = attempt article_vector("[0.5,-0.25,1]")    assert article_meaning == ArticleVector([0.5, -0.25, 1.0])    assert article_vector("[0.5,-0.25]") fails EmbeddingUnreadable    assert article_vector("[0.5,-0.25,1e39]") fails EmbeddingUnreadable}

What is built: AI says which parts of AI work are built.

Markdown for agents: /docs/ai-types.md.Built from Jadpo main at a707efb.