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

# Agent skill

> Drop-in Cursor/Claude agent skill for calling the Levanto Sage decision API.

# Levanto Sage Decision Model

HTTP API for structured decisions over content plus a question.

<Card title="Download this skill" icon="download" href="/levanto-sage-decide.md">
  Drop this file into `.cursor/skills/`, `.claude/skills/`, or your agent's skill directory. Raw Markdown, no rendering.
</Card>

## Product Context

Levanto Sage is for workflows where software needs to **decide, not write**. It turns content plus a question into a structured decision with a probability or confidence signal.

Use Sage when an application needs to route, approve, block, score, rank, tag, or escalate. LLMs return text and only provide confidence if asked; that confidence is simulated. Sage always returns a decision signal, so machines can act when confidence is high and escalate to a human when confidence is low.

Supported decision kinds:

* **Yes/No** — answer a binary question: `probability` (P(yes)), `answer` (`"yes"`/`"no"`), and `confidence` (decisiveness from tie).
* **Choice** — pick the best option, with independent per-option probabilities (raw; do not sum to 1).
* **Scale** — score a rubric, with expectation and confidence.
* **Sort** — rank a list of items by a criterion.
* **Tags** — apply independent yes/no labels to the same content.

Good use cases:

* **Agentic workflows** — Sage as the decision layer for agents: automate routing, branching, and escalation across agentic workflows and pipelines.
* **Operations** — triage urgency and route work across DevOps, SecOps, and customer support.
* **Agentic guardrails** — check tool calls and responses on the hot path to keep agents secure and aligned with the business goals of the workflow.
* **Content screening & moderation** — tag, rank, score, and sort content in data pipelines, including moderation, abuse flagging, and policy-risk severity.
* **Risk & fraud** — assess risk and opportunity and detect fraud across transactions, asset trades, and agentic actions (e.g. refunds).

Use public-facing language:

* Sage decides; it does not write paragraphs.
* Supported decision kinds are **Yes/No**, **Choice**, **Scale**, **Sort**, and **Tags**.
* Do not explain internals, architecture, training details, infrastructure, or implementation details unless the user explicitly asks for private engineering context.
* Use **decision kind** in prose. Keep the JSON wire fields exactly as shown in code examples because clients need the real format.

## Endpoint

Default hosted endpoint:

```text theme={null}
https://sage.levanto.ai
```

The user may supply a different base URL. Do not invent an endpoint if one is missing; ask for it.

## Authentication

**A valid Levanto API key is always required for decision calls.** Every `POST` to `/decide` and `/decide/batch` must carry a key; requests without one are rejected with **HTTP 401** (`{"detail": "API key required. Provide a valid
Levanto API key."}`). Send it in the Authorization header:

```text theme={null}
Authorization: Bearer lv_live_...
```

* Keys look like `lv_live_...`. If the user has not provided one, **ask for it** — do not proceed to `/decide` without a key.
* **401** = missing or invalid key; **402** = key valid but account balance too low.
* **`/ready`** does **not** require a key — poll readiness before you have one.
* **Never hardcode, log, echo, or commit the key.** Read it from an environment variable (e.g. `SAGE_API_KEY` / `API_KEY`) and pass it only in the request header.

## Readiness

Poll **`GET /ready`** until HTTP **200** (**503** while the model is still loading). The response has **no JSON body** — readiness is indicated by the status code only. Cold starts can take several minutes.

```bash theme={null}
endpoint="https://sage.levanto.ai"
ua='-A your-app/1.0'

# Wait for model readiness (503 while loading, 200 when ready)
curl -s $ua -o /dev/null -w '%{http_code}\n' "$endpoint/ready"
```

Always send a normal `User-Agent` header when calling the hosted endpoint.

## Endpoints

| Method | Path            | Purpose                                                     |
| ------ | --------------- | ----------------------------------------------------------- |
| GET    | `/ready`        | Model readiness — **503** while loading, **200** when ready |
| POST   | `/decide`       | One decision                                                |
| POST   | `/decide/batch` | Many decisions                                              |

All decision calls (`/decide`, `/decide/batch`) require a valid API key (see [Authentication](#authentication)); `/ready` does not.

## Request Shape

```json theme={null}
{
  "content": "<content to evaluate>",
  "question": {
    "id": "<stable result id>",
    "kind": "<yesno | choice | scale | sort | tags>",
    "instructions": "<question to answer>"
  }
}
```

Use `content` for the thing being evaluated — a bare string (shorthand for `{ "kind": "text", "value": "..." }`), `{ "kind": "text", "value": "..." }`, or `{ "kind": "list", "value": [...] }` for sort. Use `question.kind` to select the decision kind and `question.instructions` for the criterion. The response envelope is always `{ id, kind, result, meta }`.

## Yes/No

Use Yes/No for binary questions.

```json theme={null}
{
  "content": "Marketing email draft promotes \"guaranteed 40% returns\" and describes the product as \"risk-free\" for accredited investors.",
  "question": {
    "id": "needs_compliance_review",
    "kind": "yesno",
    "instructions": "Does this copy require compliance review before send?"
  }
}
```

Response:

```json theme={null}
{
  "id": "needs_compliance_review",
  "kind": "yesno",
  "result": {
    "probability": 0.92,
    "confidence": 0.84,
    "answer": "yes"
  },
  "meta": { "model": "levanto-sage-v0.5", "latency_ms": 97.4 }
}
```

* `probability` — P(yes), in `[0, 1]`.
* `answer` — `"yes"` if `probability ≥ 0.5`, else `"no"`.
* `confidence` — decisiveness from a tie: `2 × |probability − 0.5|` (`0` at `0.5`, `1` at certainty). Grounding compares this to `confidence_floor`.

## Choice

Use Choice when Sage must pick one option.

```json theme={null}
{
  "content": "Vendor uploaded a contract PDF that mentions automatic renewal and a 90-day termination clause.",
  "question": {
    "id": "contract_disposition",
    "kind": "choice",
    "instructions": "How should legal handle this contract?",
    "options": [
      {"option": "approve", "description": "Standard terms, no red flags — proceed to signature"},
      {"option": "revise", "description": "Acceptable with specific clause changes before signing"},
      {"option": "reject", "description": "Terms are unacceptable — do not proceed"},
      {"option": "escalate", "description": "Unusual or high-risk terms — partner review required"}
    ]
  }
}
```

Response:

```json theme={null}
{
  "id": "contract_disposition",
  "kind": "choice",
  "result": {
    "chosen": "revise",
    "confidence": 0.88,
    "probabilities": [
      {"option": "approve", "probability": 0.11},
      {"option": "revise", "probability": 0.88},
      {"option": "reject", "probability": 0.04},
      {"option": "escalate", "probability": 0.22}
    ]
  },
  "meta": { "model": "levanto-sage-v0.5", "latency_ms": 216.8 }
}
```

* `probabilities` — raw P(correct) per option (independent sigmoids; **do not sum to 1**).
* `confidence` — P(chosen is correct); use for thresholds and grounding.
* `chosen` — argmax of `probabilities`.
* **2–120 options** per question (see [Limits](#limits)).

## Scale

Use Scale for rubric scoring. Always send **exactly 5 levels** with integer values **`0`, `1`, `2`, `3`, `4`** — the model is trained on this fixed rubric only. Subsets, sparse scales, or any other level values are rejected with HTTP 400.

```json theme={null}
{
  "content": "Candidate explained our product roadmap clearly but struggled to answer questions about database scaling tradeoffs.",
  "question": {
    "id": "interview_score",
    "kind": "scale",
    "instructions": "How strong was this candidate in the technical interview?",
    "levels": [
      {"level": 0, "description": "Not qualified — fundamental gaps in required skills"},
      {"level": 1, "description": "Weak — significant concerns about core competencies"},
      {"level": 2, "description": "Mixed — some strengths but notable gaps remain"},
      {"level": 3, "description": "Solid — meets the bar with minor areas to probe"},
      {"level": 4, "description": "Strong hire — clearly exceeds expectations for the role"}
    ]
  }
}
```

Response:

```json theme={null}
{
  "id": "interview_score",
  "kind": "scale",
  "result": { "expectation": 2.4, "confidence": 0.71 },
  "meta": { "model": "levanto-sage-v0.5", "latency_ms": 79.8 }
}
```

`expectation` is the expected level; `confidence` reflects how peaked the level distribution is (independent of the mean).

## Sort

Use Sort to rank a list of items by a criterion. `content` must be a list with **2–120 items** (see [Limits](#limits)).

```json theme={null}
{
  "content": {
    "kind": "list",
    "value": [
      {"id": "typo", "content": "Spelling mistake on the FAQ page."},
      {"id": "db_down", "content": "Primary production database is unreachable."},
      {"id": "pricing", "content": "Prospect asking about Enterprise pricing."}
    ]
  },
  "question": {
    "id": "triage",
    "kind": "sort",
    "instructions": "Sort by operational urgency, most urgent first."
  }
}
```

Response:

```json theme={null}
{
  "id": "triage",
  "kind": "sort",
  "result": { "sorted": ["db_down", "pricing", "typo"], "confidence": 0.92 },
  "meta": { "model": "levanto-sage-v0.5", "latency_ms": 312.0 }
}
```

The sort request is minimal — just `instructions` and the item list. Scoring strategy and ranking are chosen by the server, not the request: `SortQuestion` accepts only `kind`, `id`, `instructions` (any other field is rejected).

The result carries a calibrated, list-level `confidence` in `[0,1]` — how trustworthy the **whole order**.

## Tags

Use Tags to apply several independent yes/no labels to the same content. **1–120 tags** per question (see [Limits](#limits)). Each tag returns a `probability` and a `confidence` (`= 2|probability − 0.5|`, the same decisiveness metric as yes/no: 0 at a coin-flip, 1 at certainty); when a tag has a `threshold`, it also returns `applies`. There is no document-level tags confidence — read each tag's own.

```json theme={null}
{
  "content": "Buy cheap followers now!!! click here -> bit.ly/deal",
  "question": {
    "id": "moderate",
    "kind": "tags",
    "tags": [
      {"id": "spam", "threshold": 0.5},
      {"id": "promotion"}
    ]
  }
}
```

Response:

```json theme={null}
{
  "id": "moderate",
  "kind": "tags",
  "result": {
    "tags": [
      {"id": "spam", "probability": 0.95, "confidence": 0.90, "applies": true},
      {"id": "promotion", "probability": 0.88, "confidence": 0.76}
    ]
  },
  "meta": { "model": "levanto-sage-v0.5", "latency_ms": 241.0 }
}
```

## Grounding (optional)

Attach a `grounding` block to a Yes/No, Choice, Scale, or Tags request to let Sage fetch fresh web evidence before deciding (useful for recency- or fact-heavy questions). For Tags, `low_confidence` triggers when **any** tag's `confidence` is below the floor (weakest-link); Sage then re-scores all tags on the expanded content.

```json theme={null}
{
  "content": "Has there been a major Cloudflare incident this month?",
  "question": {"id": "incident", "kind": "yesno", "instructions": "Is this likely true?"},
  "grounding": {"trigger": "low_confidence", "confidence_floor": 0.8}
}
```

* `trigger`: `never`, `low_confidence` (default — search when `result.confidence` is below `confidence_floor`), or `always`.
* For **yesno**, `confidence` = `2|p − 0.5|` (decisiveness; 0 at a tie). Default floor `0.80` requires roughly `p ≤ 0.10` or `p ≥ 0.90` to skip search.
* The response gains `grounding_meta` with `triggered`, `queries`, `sources`, and timing.
* Requires the server to be configured for grounding; if it is not, omit the block.

## Batch

Use `/decide/batch` for many decisions in one call. Each entry in `requests` is a **group**: one `content` plus a list of `questions` about it — so asking several questions about the same content sends that content only once, and a single call can still mix groups with different content.

```json theme={null}
{
  "requests": [
    {
      "content": "User post: 'Selling verified Instagram accounts, DM for bulk pricing.'",
      "questions": [
        {"id": "policy_violation", "kind": "yesno", "instructions": "Does this post violate marketplace policy?"},
        {
          "id": "harm_level",
          "kind": "scale",
          "instructions": "How harmful is this content to the platform?",
          "levels": [
            {"level": 0, "description": "No meaningful harm — safe to leave published"},
            {"level": 1, "description": "Low harm — minor policy edge case"},
            {"level": 2, "description": "Moderate harm — misleading or low-grade abuse"},
            {"level": 3, "description": "High harm — fraud, spam, or user safety risk"},
            {"level": 4, "description": "Severe harm — active scam or imminent user damage"}
          ]
        }
      ]
    }
  ]
}
```

Each question can carry its own optional `grounding` (grounding is no longer part of the group; `sort` questions still reject it). To batch decisions about *different* content, add more groups:

```json theme={null}
{
  "requests": [
    {"content": "doc A", "questions": [{"id": "a1", "kind": "yesno", "instructions": "urgent?"}]},
    {"content": "doc B", "questions": [{"id": "b1", "kind": "yesno", "instructions": "urgent?"}]}
  ]
}
```

Batch responses are nested by group, then by question:

* `results`: one item per **group** (matches `requests` 1:1);
* `results[i].answers`: one item per question in that group's `questions`, in order;
* `results[i].answers[j].ok`: success flag for that question (`.result` holds the decision, or `.error` the message);
* `meta.request_count`: number of groups; `meta.question_count`: total questions across all groups.

## Errors

| HTTP          | Meaning                                     |
| ------------- | ------------------------------------------- |
| `400`         | Invalid request (schema validation)         |
| `401` / `402` | Missing/invalid API key, or balance too low |
| `503`         | Service loading or temporarily unavailable  |

Invalid requests return a standard schema message, for example:

```json theme={null}
{"detail": "content: Field required"}
```

```json theme={null}
{"detail": "scale levels must be integers in 0..4 (model trained on 5 levels); got [9]"}
```

```json theme={null}
{"detail": "Service is still loading."}
```

## Limits

Per-kind counts (HTTP 400 outside these ranges):

| Kind       | Field                 | Min |                     Max |
| ---------- | --------------------- | --: | ----------------------: |
| **choice** | `options`             |   2 |                     120 |
| **scale**  | `levels`              |   5 | 5 (values `0`–`4` only) |
| **sort**   | `content.value` items |   2 |                     120 |
| **tags**   | `tags`                |   1 |                     120 |

**Scale** — always send all five levels `0` through `4`. No sparse rubrics, no alternate ranges, no fewer or more levels. Each `level` must be a distinct integer in that set.

**Content and question length** — the full rendered request must fit in roughly **32K tokens** total. That budget includes `content`, `question.instructions`, option descriptions, scale rubric lines, sort item bodies, tag specs, and any grounding-added search context. Oversized requests fail rather than silently truncating. Very long content can also hit HTTP 503 before the hard token cap; keep documents well under the 32K ceiling in practice.

## Threshold Guidance

Sage always returns a decision signal. The application decides what to do next.

Example:

* high confidence: automate;
* medium confidence: review;
* low confidence: escalate to a human or ask for more context.

Do not say Sage "abstains." Say it supports low-confidence routing.

## Smoke Test

```bash theme={null}
endpoint="https://sage.levanto.ai"
ua='-A your-app/1.0'
# API key is REQUIRED for /decide — read it from the environment, never hardcode.
auth="-H Authorization: Bearer ${SAGE_API_KEY:?set SAGE_API_KEY to your lv_live_ key}"

# /ready needs no key — empty body; 200 means ready, 503 means still loading.
curl -s $ua -o /dev/null -w '%{http_code}\n' "$endpoint/ready" | grep -qx 200

curl -s $ua $auth -H 'Content-Type: application/json' "$endpoint/decide" -d '{
  "content": "Marketing email draft promotes \"guaranteed 40% returns\" and describes the product as \"risk-free\" for accredited investors.",
  "question": {"id": "needs_compliance_review", "kind": "yesno", "instructions": "Does this copy require compliance review before send?"}
}' | jq -e '.kind == "yesno" and .result.probability != null and .result.confidence != null and .result.answer != null'

curl -s $ua $auth -H 'Content-Type: application/json' "$endpoint/decide/batch" -d '{
  "requests": [
    {
      "content": "same doc",
      "questions": [
        {"id": "a", "kind": "yesno", "instructions": "urgent?"},
        {"id": "b", "kind": "yesno", "instructions": "positive?"}
      ]
    }
  ]
}' | jq -e '.meta.request_count == 1 and .meta.question_count == 2 and (.results | length) == 1 and (.results[0].answers | length) == 2'
```
