# Typed output

`ai.generate(prompt, as: T)` never hands back a value that hasn't passed
every one of `T`'s rules. You write the type; Jadpo turns it into a
schema, asks for an answer in that shape, and decodes the answer the way
it decodes a request body. The steps, in order:

1. **The schema.** The compiler writes `T`'s JSON Schema when it builds
   your app. It is the same schema the API record and OpenAPI use
   ([OpenAPI](openapi.md)), with each field's `///` description,
   so the model sees what each field means.
2. **Asking.** Where the package's `schema:` mode is `strict_subset`,
   Jadpo sends the schema in the provider's structured-output mode, cut
   to the keywords the provider enforces: types, required fields, enums
   and nesting. Limits it doesn't enforce, such as `max_length`,
   `pattern` or a `rule` across fields, are checked in step 3. With
   `json_only` or `none`, Jadpo adds the schema to the instructions
   under a fixed heading and asks for JSON alone.
3. **Decoding.** The answer goes through the same decoding as
   `json.read(…, as: T)`, every constraint and named rule included. There
   is no second way in. A `T` built on `Text` asks for plain text and
   skips the schema.
4. **One repair.** If decoding fails and `attempts:` allows, Jadpo asks
   once more with the previous answer and the `DecodeProblem` (its path
   and reason, never a value from the prompt) appended as a correction.
   The default is 2 attempts in all and the most is 3; each attempt counts
   against every limit.
5. **The result.** A decoded `T`, or `malformed` carrying the last
   `DecodeProblem`. A refusal or a cut-off ends the call at once.

The built-in driver's mode is `strict_subset`. It sends OpenAPI's schema
for `T`, cut to what the mode enforces. A `T` whose JSON isn't an
object, such as an enum or a list, is asked for as the member `value` of
one. A package's mode may be `json_only` or `none`. The details are under
[Generating, its fake and the local driver](generating-its-fake-and-the-local-driver.md)
and [A package filling `ai`](a-package-filling-ai.md).

`as:` takes most types, but not all:

| `as:` takes | Refused, and why |
|---|---|
| Any type `json.read` takes: objects, lists, enums, `Text` types, the standard types | Secret values (`ApiKey`, `Password` and the rest): `configuration.secret_flow`, a model never makes a secret |
| | References (`X.id`): a model can't know which records exist; pass the candidates in and ask for an enum or a field the code looks up (`semantic.ai_output_type`) |
| | `File`, `Bytes`, `PresentationText`: not something JSON from a model can be |

A model's answer is input from outside, like a request body: typed and
checked, but never proof of anything. [Policy](policy.md) never
grants access because of it, and its text is escaped wherever it is
shown, as any user's text is.

Since a model can't name records, a reply that cites help articles asks
for their slugs, a field of text, picked from the articles the prompt was
given. Here the action first embeds the question, finds the nearest
articles ([Embeddings and search](embeddings-and-search.md)) and passes
them to the prompt:

```jadpo
// domains/support/types/support_answer.jadpo
type ArticleSlug = Text {
    max_length: 120
}

/// What the assistant says back, and which articles it drew on.
type SupportAnswer = Output {
    /// False when the articles given don't answer the question.
    resolved_by_articles: Bool
    /// The reply to show the customer, at most a few short paragraphs.
    reply: Text {
        max_length: 2000
    }
    /// The slugs of the articles the reply used, from those given.
    cited_slugs: List<ArticleSlug> {
        max_length: 5
    }
}
```

```jadpo
// domains/support/actions/answer_customer.jadpo
use support/types { AssistantUnavailable, CustomerQuestion, SupportAnswer }
use support/prompts { AnswerQuestion }

/// Answers a customer's question from the closest help articles.
action answer_customer(asked: CustomerQuestion) fails AssistantUnavailable -> SupportAnswer {
    var question_meaning: ArticleMeaning = attempt ai.embed(asked.text, as: ArticleMeaning) {
        timed_out: AssistantUnavailable
        over_budget: AssistantUnavailable
        too_long: AssistantUnavailable
    }
    var nearest_excerpts: List<Nearest<ArticleExcerpt>> = closest_articles(question_meaning)
    return attempt ai.generate(
        AnswerQuestion {
            question: asked.text
            articles: list.map(nearest_excerpts, found -> found.item)
            reader_locale: asked.locale
        },
        as: SupportAnswer,
        max_output: 600
    ) {
        refused: AssistantUnavailable
        malformed: AssistantUnavailable
        timed_out: AssistantUnavailable
        over_budget: AssistantUnavailable
        too_long: AssistantUnavailable
    }
}
```
