# Models

Call a built-in model with a state and typed questions, and get typed answers back.

Sends a state and typed questions to a provider's model and returns its typed answers. A model is a built-in action of kind `model`: Twisp ships it, every tenant has it, and `actions(where: { native: true, kind: "model" })` lists every one. Its name is reserved in every tenant. A write of kind `model` is rejected: no tenant creates, updates, or deletes one. A model has no triggers, `tools`, `skills`, `session` block, or policies: it performs no database operation. It runs only as a call, from `invokeAction`, an MCP server, an agent's `tools`, or a Starlark program's `tools.<name>(...)`, and needs `INVOKE` on the model like any tool.

| Model          | Provider | API          | `actionId`                             |
| -------------- | -------- | ------------ | -------------------------------------- |
| `typesafe_jev` | TypeSafe | `decision/1` | `83d17bdc-ece0-59e6-9af3-6dbb76ea351d` |

## The model action

```json
{
  "name": "typesafe_jev",
  "kind": "model",
  "enabled": true,
  "description": "Model typesafe_jev, release jev-1.13.0.",
  "config": { "provider": "typesafe", "model": "jev-1.13.0", "api": "decision/1" },
  "tool": {
    "name": "typesafe_jev",
    "description": "Ask Jev typed questions about a state. Returns calibrated answers: a choice with probabilities, a score on a scale, or the probability of yes.",
    "inputSchema": { "...": "decision/1 input" },
    "outputSchema": { "...": "decision/1 output" }
  },
  "policies": []
}
```

| Field               | Meaning                                                                                            |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `name`, `tool.name` | The model's name, `<provider>_<family>`. A Starlark program calls it as `tools.typesafe_jev(...)`. |
| `enabled`           | False for a [retired](https://www.twisp.com/docs/reference/actions/models.md#versions-and-retirement) version.                                           |
| `config.provider`   | The model's provider.                                                                              |
| `config.model`      | The provider's model release this version calls, such as `jev-1.13.0`.                             |
| `config.api`        | The API this version implements, such as `decision/1`.                                             |
| `config.retired`    | On a retired version only: the date it was retired.                                                |
| `tool.inputSchema`  | The API's input schema at this version.                                                            |
| `tool.outputSchema` | The API's output schema at this version, closed at every level.                                    |
| `policies`          | None. A model performs no database operation.                                                      |

`config` is read only: it describes the version. An MCP server annotates a model as open world, `openWorldHint: true`, since each call sends its input outside Twisp. See [Annotations](https://www.twisp.com/docs/reference/actions/mcp.md#annotations).

## `decision/1`

**Input.** `state` is required: a string, an object, an array, or null. `questions` is required: a map from a question ID, which is not empty, to a question, with at least one entry. No other field is accepted.

| Question type | `criteria`                                            | Rule                                                                                                  |
| ------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `choice`      | A map from option to its description.                 | Required, with at least one option. A description can be null.                                        |
| `score`       | An ordered array of level descriptions, from level 0. | Required, with at least two levels, none null.                                                        |
| `noul`        | An object with `true` and `false` descriptions.       | Optional. Without a `true` or `false` description that is not null, `instructions` must not be empty. |

`instructions` and each description is a string, an object, an array, or null, except a score level, which is never null. An input that breaks these rules fails with `action_input_invalid`, naming the path, before anything is sent. The provider enforces its own limits on the number of options and levels. The input limit of 1 MiB bounds the state and the questions.

```json
{
  "state": { "subject": "Payouts failing", "body": "My payouts have failed for 3 days." },
  "questions": {
    "owner": {
      "type": "choice",
      "instructions": "Which team owns this ticket?",
      "criteria": { "payments": "Payouts or transfers.", "access": "Login or keys.", "other": null }
    },
    "urgent": { "type": "noul", "instructions": "Does the customer report an active loss?" }
  }
}
```

**Output.** The output has exactly these fields:

| Field     | Meaning                                                                   |
| --------- | ------------------------------------------------------------------------- |
| `model`   | The release that answered. It always equals the version's `config.model`. |
| `answers` | One answer per question, keyed by question ID, with its question's type.  |
| `usage`   | `input_tokens` and `output_tokens`.                                       |

| Answer   | Fields beyond `type`                                                                                                                                                  |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `choice` | `choice`, one of the options; `probabilities`, a probability for each option; `confidence`.                                                                           |
| `score`  | `score`, a position on the scale, such as `1.34`, rather than a whole level; `legend` and `probabilities`, keyed by level number from zero as a string; `confidence`. |
| `noul`   | `noul`, the probability of yes.                                                                                                                                       |

A field the provider adds to its response is not in the output until a version names it. The call's [transcript](https://www.twisp.com/docs/reference/actions/models.md#the-call-transcript) keeps the response as received.

A caller's [reference](https://www.twisp.com/docs/reference/actions/tools.md#tool-references) can fix `questions`, so an agent supplies only `state`. Fixed input is a CEL expression, so the questions are written as a CEL map literal:

```graphql
tools: [
  {
    name: "typesafe_jev"
    input: {
      questions: """
      {
        "personal": {"type": "noul", "instructions": "Is this a personal purchase?"}
      }
      """
    }
  }
]
```

## Versions and retirement

Version N of a model is its Nth release: it fixes `config.model`, the schemas, and the descriptions.

| Model          | Version | `config.model` | `config.api` | Status  |
| -------------- | ------- | -------------- | ------------ | ------- |
| `typesafe_jev` | 1       | `jev-1.13.0`   | `decision/1` | Current |

Within a model's name, a new version only adds: a new release, an optional input field, or an output field. An input an earlier version accepts is still accepted, and every field it returns is still returned. A change that breaks either ships under a new name, such as `typesafe_jev2`. A reference with a `version` calls that release, always; one without a `version` calls the latest, resolved when each execution of the caller starts, as for every [tool](https://www.twisp.com/docs/reference/actions/tools.md#versions). A new release can answer differently, so pin a version where answers must not move without a change of your own.

A version whose release the provider no longer serves is retired: it reads with `enabled: false` and `config.retired`, and is still listed. A call to it, from any caller, fails with `action_not_callable`, whose hint names the release, the retirement date, and the latest version. An MCP server leaves it out of `tools/list`. A definition can keep or add a pin to it. The latest version can be retired, and then a reference without a `version` fails the same way until a new release ships.

## Errors

The call is made once, with a 10-second timeout, capped by the transaction's deadline; the caller decides whether to call again. Each failure below is a call error, which `check=False` catches and an agent shows to its model.

| Code                       | When                                                                                                                                 | Details and hint                                                                                                                                      |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action_input_invalid`     | The input fails the API's rules. Nothing is sent.                                                                                    | The path and the expected shape.                                                                                                                      |
| `model_not_served`         | This deployment does not offer the model. A caller that declares it still runs; only its calls to the model fail.                    | `details.model`. Ask your Twisp operator to enable it.                                                                                                |
| `action_not_callable`      | The version is retired.                                                                                                              | The release, the date, and the latest version.                                                                                                        |
| `model_credential_missing` | The deployment has no API key for the provider. Nothing is sent.                                                                     | Ask your Twisp operator to set the key.                                                                                                               |
| `model_unauthorized`       | The provider refused the deployment's key.                                                                                           | `details.request_id`. Ask your Twisp operator to replace the key.                                                                                     |
| `model_request_invalid`    | The provider rejected the request, with a status in the 400s other than 401, 403, 408, and 429.                                      | `details.provider`, up to 2 KiB of the provider's message, and `details.request_id`. The input is the likely cause; a withdrawn release is the other. |
| `model_unavailable`        | The provider was rate limited, overloaded, failed, or did not answer within 10 seconds.                                              | `details.request_id` and `details.retry_after` when the provider sent them. Call again later.                                                         |
| `action_output_invalid`    | The answers do not match the questions, the response fails the output schema, or the response's `model` differs from `config.model`. | For a different model, `details.config_model` and `details.response_model`.                                                                           |

## The call transcript

A model call's execution records its input and output, as every call does. Its invocation's [transcript](https://www.twisp.com/docs/reference/actions/agents.md#transcripts) records the exchange with the provider in two events:

| Kind             | `data`                                                                                                                                                                |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model.request`  | `provider`, `method`, `path`, and `body`, the request body exactly as sent. Never the API key.                                                                        |
| `model.response` | `status`, `request_id`, the provider's request ID, `latency_ms`, and `body`, the body exactly as received. A request that got no response has `status` 0 and `error`. |

A body that is not valid UTF-8 is recorded as `body_base64` instead of `body`. The request is recorded before it is sent, and the response before it is used; if either cannot be recorded, nothing more is sent and the call fails and is retried. The events commit on their own, so they survive a call whose transaction rolls back, and `invocation.transcriptLastSequence` is the last of them.

A [replay](https://www.twisp.com/docs/reference/actions/executions.md#replays) or a preview of an execution that called a model calls the provider again and can get a different answer. A call repeated with the same `executionId` returns the recorded execution without calling the provider.
