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

# Select the records worth sending to a model

> Send every record you are considering plus the task they must serve.
Needlepath returns the subset it selected — together with a rendered
block, per-record scores and reasons, and the token arithmetic.

**Selection, not rewriting.** Nothing is paraphrased or summarised into
new prose and no model writes replacement text; an excerpt is built from
the record's own content. It is **not** guaranteed byte-identical to a
span of your input — see `SelectedRecord.excerpt`. If you need exact
spans, join on `record_id` and use your own copy.

**Stateless.** Records ride in on every request; nothing is persisted
between calls. The same request under the same `operating_point` gets
the same answer — with two caveats worth knowing before you assert on
it in a test: record **order** breaks score ties, and a record sent
**without an `id`** comes back under a freshly generated one, so
`selected[].record_id` will differ between two otherwise identical
calls. Send ids.

**A stand-down is a success.** If the engine judges that trimming would
not pay, it returns your context essentially intact with
`fallback_used = true` and, when a gate verdict ran, `gate.engaged =
false` plus a `reason`. Treat it as a normal 200 and send what you were
going to send. Do not treat a low `tokens_saved` as an error.

**Fail open.** If this call times out, returns non-2xx, or returns an
empty selection, send your original context unmodified. It is a binding
rule for every integration, first-party or yours.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/context/select
openapi: 3.1.0
info:
  title: Needlepath Context Selection API
  version: 1.0.0
  summary: Select which context records enter a model call, verbatim.
  description: |
    Needlepath decides **which of your records go into a prompt**. You send the
    records you hold — documents, tool results, prior turns — plus the task they
    have to serve and a token budget. Needlepath returns the subset worth
    sending, verbatim, with a rendered block you can paste straight into your
    prompt.

    Three properties shape everything else in this document:

    - **Nothing is stored.** Records ride in on every request. There is no
      session, no upload step, no retention. The service holds no database on
      the request path and its execution role grants log-writing and nothing
      else. See *Retention and trust*.
    - **The same request gets the same answer.** Behaviour is pinned by an
      opaque, immutable `operating_point` label. Retuning mints a *new* label;
      a published label never changes meaning.
    - **It can decline.** When trimming would not pay, the engine stands down
      and hands back a selection that is effectively your input. That is a
      designed outcome, not a failure — see `fallback_used` and `gate`.

    ### Versioning

    URL versions are **major-only**: `/v1`, `/v2`, …. A breaking change mints a
    new URL version; optional additions — a new response field, a new optional
    request field with a safe default, a new operating-point label — ship at the
    existing one. Nothing is served unversioned and there is no implicit
    "latest". A version keeps serving until it is **formally deprecated**, which
    requires a published notice naming the replacement and a sunset date, a
    written migration path, and a notice period that has elapsed before removal.

    ### Forward compatibility, required of clients

    Two rules, both from `INTERFACE.md`, and a client that ignores them will
    break on a change this contract considers non-breaking:

    1. **Ignore response fields you do not recognise.** New optional fields ship
       within `/v1`.
    2. **Do not exhaustively match `gate.reason` or the gate outcome.** The gate
       outcome is an open, extensible enum. Today it takes two values — a
       *select* outcome (`gate.engaged = true`) and a *stand_down* outcome
       (`gate.engaged = false`) — with the specific trigger in `reason`
       (`engage:needle`, `standdown:high_drift`, …). A third outcome,
       *escalate*, is a planned additive change.
  contact:
    name: Needlepath support
    url: https://nextmoca.com
  x-logo:
    altText: Needlepath
servers:
  - url: https://api.nextmoca.com
    description: Needlepath hosted API.
security:
  - bearerAuth: []
tags:
  - name: Selection
    description: The one endpoint that does work.
  - name: Registry
    description: Public, unchanging facts about the deployment.
paths:
  /v1/context/select:
    post:
      tags:
        - Selection
      summary: Select the records worth sending to a model
      description: |
        Send every record you are considering plus the task they must serve.
        Needlepath returns the subset it selected — together with a rendered
        block, per-record scores and reasons, and the token arithmetic.

        **Selection, not rewriting.** Nothing is paraphrased or summarised into
        new prose and no model writes replacement text; an excerpt is built from
        the record's own content. It is **not** guaranteed byte-identical to a
        span of your input — see `SelectedRecord.excerpt`. If you need exact
        spans, join on `record_id` and use your own copy.

        **Stateless.** Records ride in on every request; nothing is persisted
        between calls. The same request under the same `operating_point` gets
        the same answer — with two caveats worth knowing before you assert on
        it in a test: record **order** breaks score ties, and a record sent
        **without an `id`** comes back under a freshly generated one, so
        `selected[].record_id` will differ between two otherwise identical
        calls. Send ids.

        **A stand-down is a success.** If the engine judges that trimming would
        not pay, it returns your context essentially intact with
        `fallback_used = true` and, when a gate verdict ran, `gate.engaged =
        false` plus a `reason`. Treat it as a normal 200 and send what you were
        going to send. Do not treat a low `tokens_saved` as an error.

        **Fail open.** If this call times out, returns non-2xx, or returns an
        empty selection, send your original context unmodified. It is a binding
        rule for every integration, first-party or yours.
      operationId: selectContext
      requestBody:
        required: true
        description: |
          `ContextRequest`. Maximum body size is **6 MB** — the AWS Lambda
          synchronous payload cap. API Gateway rejects an oversized body before
          the service is invoked, so guard it client-side rather than relying on
          a readable error.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContextRequest'
            examples:
              fixed:
                summary: Fixed budget, three records
                value:
                  request_id: req-2026-08-01-0001
                  records:
                    - id: r-812
                      kind: tool_result
                      title: get_invoice(4471)
                      source: billing-api
                      text: >-
                        {"invoice_id": 4471, "total_cents": 128400, "status":
                        "open"}
                    - id: r-813
                      kind: external_data
                      title: Refund policy v4
                      source: handbook
                      text: >-
                        Refunds are issued to the original payment method within
                        10 business days.
                    - id: r-814
                      kind: llm_response
                      text: I have pulled up the account. One moment.
                  task:
                    prompt: Can this invoice still be refunded?
                    tool_name: answer_customer
                  budget:
                    max_context_tokens: 4000
                    operating_point: np-2026-07-r2
                  render: true
                  render_format: plain
                  return_per_record: true
              adaptive:
                summary: Adaptive budget with an escalation ladder
                value:
                  request_id: req-2026-08-01-0002
                  records:
                    - id: r-001
                      kind: external_data
                      text: …
                  task:
                    prompt: Which clause governs early termination?
                    recent_prompts:
                      - Summarise the master services agreement.
                  budget:
                    max_context_tokens: 8000
                    operating_point: np-2026-07-r2
                    mode: adaptive
                    adaptive:
                      initial_tokens: 2000
                      escalation_tokens:
                        - 4000
                        - 8000
                      allow_full_context_fallback: true
                  render: true
                  return_per_record: true
      responses:
        '200':
          description: |
            A selection. **Includes stand-downs and engine-internal failures** —
            see `fallback_used` and `selection_error`. This service returns 200
            with a usable body rather than a 5xx when its own selection pass
            fails, because the correct client behaviour in that case is to send
            the original context, which the body already contains.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContextResponse'
              examples:
                engaged:
                  summary: Engaged — two of three records selected
                  value:
                    request_id: req-2026-08-01-0001
                    rendered_context: |-
                      [tool_result] get_invoice(4471)
                      {"invoice_id": 4471, …}

                      [external_data] Refund policy v4
                      Refunds are issued to …
                    policy_version: np-2026-07-r2
                    selected:
                      - record_id: r-812
                        kind: tool_result
                        title: get_invoice(4471)
                        source: billing-api
                        score: 0.91
                        reason: keyword_and_entity_match
                        excerpt: >-
                          {"invoice_id": 4471, "total_cents": 128400, "status":
                          "open"}
                        excerpt_format: plain
                        selected_tokens: 31
                      - record_id: r-813
                        kind: external_data
                        title: Refund policy v4
                        source: handbook
                        score: 0.74
                        reason: keyword_overlap
                        excerpt: >-
                          Refunds are issued to the original payment method
                          within 10 business days.
                        excerpt_format: plain
                        selected_tokens: 18
                    tokens_before: 96
                    tokens_after: 49
                    tokens_saved: 47
                    records_available: 3
                    records_selected: 2
                    fallback_used: false
                    selection_error: null
                    engine_latency_ms: 11.4
                    budget_tokens: 4000
                    attempted_budget_tokens: []
                    reduction_ratio: 0.4895833333333333
                    safety: null
                    gate:
                      engaged: true
                      reason: engage:needle
                      signals: {}
                    format_metrics: {}
                stood_down:
                  summary: Stood down — trimming would not have paid
                  value:
                    request_id: req-2026-08-01-0003
                    rendered_context: …your context, effectively intact…
                    policy_version: np-2026-07-r2
                    selected: []
                    tokens_before: 96
                    tokens_after: 96
                    tokens_saved: 0
                    records_available: 3
                    records_selected: 3
                    fallback_used: true
                    selection_error: null
                    engine_latency_ms: 3.2
                    budget_tokens: 4000
                    attempted_budget_tokens: []
                    reduction_ratio: 0
                    safety: null
                    gate:
                      engaged: false
                      reason: standdown:high_drift
                      signals: {}
                    format_metrics:
                      engine_fallback_reason: nothing_cleared_the_selection_floor
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/RequestTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/EngineError'
      security:
        - bearerAuth: []
components:
  schemas:
    ContextRequest:
      type: object
      required:
        - request_id
        - records
        - task
        - budget
      properties:
        request_id:
          type: string
          description: >
            Your correlation id for this unit of work. **The response echoes it

            back byte-for-byte**, and the published client raises if it does not

            match — so it is the key you join a response back to a request on.


            Keep it to **≤ 128 characters from `[A-Za-z0-9._:-]`**. An id
            outside

            that bound is still echoed to you unchanged, but the *internal*

            correlation copy is truncated and de-fanged, and the response then

            carries `format_metrics.request_id_sanitized: true` so you can see
            it

            happened. Two consequences worth designing for: an id that differs

            only past character 128 is indistinguishable in our logs, and

            **anything you put in this field reaches our logs** — so do not put

            record content, customer text or credentials in it.
          examples:
            - req-2026-08-01-0001
        records:
          type: array
          description: |
            The state Needlepath may keep, drop or excerpt. An empty array is
            accepted and yields an empty selection.

            **Order is a tie-breaker.** Records that score equally are ranked by
            the order they arrived in, so reordering an otherwise identical
            request can change which records survive a tight budget. Send a
            stable order if you want stable results.

            Record **count** is capped per request alongside the token ceiling,
            because selection cost scales with count as well as size. See
            *Limits*.
          items:
            $ref: '#/components/schemas/ContextRecord'
        task:
          $ref: '#/components/schemas/TaskSpec'
        budget:
          $ref: '#/components/schemas/BudgetSpec'
        render:
          type: boolean
          default: true
          description: |
            Ask for `rendered_context`, a model-facing string assembled from the
            selection. Set `false` if you assemble the prompt yourself from
            `selected[]`.

            **Currently ignored** — the rendered block is always built and
            returned. Discard it client-side until this is wired.
        render_format:
          type: string
          enum:
            - plain
            - hybrid
          default: plain
          description: >
            A neutral rendering hint, not a method knob.


            `plain` (the default) leaves excerpts as text. `hybrid` additionally

            allows a structured excerpt — JSON, typically — to be re-encoded
            into

            a more compact form when that would save tokens; `excerpt_format` on

            each selected record reports which form was used. **Send `plain` if

            you intend to re-parse excerpts as JSON.**
        return_per_record:
          type: boolean
          default: true
          description: |
            Include the `selected[]` detail — per-record score, reason, excerpt
            and token count. Set `false` and you get `rendered_context` plus the
            token arithmetic only, which is a materially smaller response body.
    ContextResponse:
      type: object
      description: >
        The selection.


        **Required by the contract** (the published client raises if any is

        missing, or if `request_id` does not echo): `request_id`,

        `rendered_context`, `tokens_before`, `tokens_after`, `tokens_saved`,

        `records_available`, `records_selected`, `fallback_used`,

        `engine_latency_ms`.


        Everything else is optional and **new optional fields ship within

        `/v1`** — ignore what you do not recognise.


        The first field that will exercise that rule is already designed and is

        documented in this file as the `Usage` schema. It is deliberately
        **not**

        listed as a property below, because it is not serving: listing it would

        make every generated client carry a field that never arrives.
      required:
        - request_id
        - rendered_context
        - tokens_before
        - tokens_after
        - tokens_saved
        - records_available
        - records_selected
        - fallback_used
        - engine_latency_ms
      properties:
        request_id:
          type: string
          description: Your `request_id`, echoed byte-for-byte.
        rendered_context:
          type: string
          description: |
            The model-facing block, assembled from the selection. Empty when you
            sent `render: false`.
        policy_version:
          type:
            - string
            - 'null'
          description: |
            The frozen version behind the `operating_point` that actually ran.
            Record it with every result: it is what makes a result file
            self-describing, and it is how you detect that you inherited a
            default you did not choose.
        selected:
          type: array
          default: []
          description: |
            Per-record detail, in selection order. Empty when you sent
            `return_per_record: false`, **and also empty on some stand-downs** —
            an empty `selected[]` is not by itself evidence that nothing was
            selected. Read `records_selected` for that.
          items:
            $ref: '#/components/schemas/SelectedRecord'
        tokens_before:
          type: integer
          description: |
            The engine's own accounting of the context available before
            filtering.

            On this deployment it works out to exactly
            `Σ estimate_tokens(record.text)`. It is **still not the billing
            quantity**: the meter additionally counts `record.title`,
            `task.prompt` and `task.recent_prompts`, so `input_tokens` is the
            larger number and the two will not match.

            It is also an engine-internal quantity by definition rather than by
            promise — it is a per-record sum taking a maximum against the
            summary the engine holds, which on this deployment is your text.
            Budget against it; do not reconcile an invoice with it.
        tokens_after:
          type: integer
          description: >-
            Context size after selection, on the same engine-internal basis as
            `tokens_before`.
        tokens_saved:
          type: integer
          description: |
            `tokens_before − tokens_after`. Zero on a stand-down, and that is a
            correct outcome rather than a failed one.
        records_available:
          type: integer
          description: How many records the engine considered.
        records_selected:
          type: integer
          description: How many it selected.
        fallback_used:
          type: boolean
          description: >
            The engine stood aside and returned your context essentially intact.


            **`fallback_used = true` does not tell you why**, and three
            different

            causes produce it: a deliberate gate stand-down, an empty or

            unusable record set, and an engine-internal exception. Read `gate`

            and `selection_error` to tell them apart, and see

            `format_metrics.engine_fallback_reason`.
        selection_error:
          type:
            - string
            - 'null'
          description: >
            Set when the engine's own selection pass raised. **The HTTP status
            is

            still 200** — the body carries a usable full-context result, which
            is

            what a client should send anyway. If you monitor Needlepath, alarm
            on

            this field, not on 5xx: an availability metric defined on status
            code

            reports health straight through an engine outage.
        engine_latency_ms:
          type: number
          format: double
          description: >
            Server-measured selection time. It excludes network transit, so it
            is

            not your round trip, and the two are different measurements that
            must

            not be compared or added together casually.


            **Example values in this document are illustrative and are not a

            performance commitment.** Measure your own end-to-end latency from

            your own client.
        budget_tokens:
          type: integer
          description: The budget the returned selection was produced under.
        attempted_budget_tokens:
          type: array
          items:
            type: integer
          default: []
          description: >
            Every rung attempted on the adaptive ladder, in order. Empty on the

            fixed path. This is the field to watch if you want to know whether
            an

            adaptive workload is doing several selection passes per request.
        reduction_ratio:
          type: number
          format: double
          description: |
            `tokens_saved / tokens_before`, on the engine-internal basis. Zero
            when `tokens_before` is zero.
        safety:
          oneOf:
            - $ref: '#/components/schemas/SafetySummary'
            - type: 'null'
          description: |
            Present only when a coverage verdict actually ran. **`null` on the
            plain fixed-budget path**, so a null `safety` means "no verdict was
            computed", never "the verdict was fine".
        gate:
          oneOf:
            - $ref: '#/components/schemas/GateSummary'
            - type: 'null'
          description: Present when a gate decision was made.
        format_metrics:
          type: object
          additionalProperties: true
          default: {}
          description: >
            Free-form, additive rendering and diagnostic telemetry. Treat
            unknown

            keys as informational. Keys you may see today:


            - `engine_fallback_reason` — present **only** when the engine
              actually stood aside, carrying its own reason. This is the field
              that distinguishes "nothing cleared the selection floor" from other
              stand-downs on the fixed path, where `safety` is null.
            - `request_id_sanitized: true` — your `request_id` violated the
              length or charset bound and the internal copy was truncated and
              de-fanged. The echo you received is still your bytes.
            - `client_latency_ms` — **added by the client, not by the server.**
              The published Python client records its own round trip here. Do not
              read it as a server measurement.
    ContextRecord:
      type: object
      required:
        - text
      description: >
        One unit of state. `text` is the only required field; everything else is

        a signal that improves selection.


        **Derived quantities are not sent.** Token counts and the like are

        computed server-side — there is no field for you to pre-compute one
        into,

        deliberately, because a caller-supplied count would be a caller-supplied

        bill.
      properties:
        text:
          type: string
          description: |
            The content. Selection keeps or excerpts it and never rewrites it
            into new prose — see `SelectedRecord.excerpt` for the exact
            guarantee, which is narrower than byte-identity.
        kind:
          type: string
          default: external_data
          enum:
            - user_input
            - llm_response
            - tool_call
            - tool_result
            - external_data
            - error
            - artifact
            - tool_schema
          description: >
            The record's role. This is the published set; a value outside it is

            rejected. Roles carry different selection priors, so labelling

            honestly is worth more than labelling everything `external_data`.


            `tool_schema` is reserved for tool and function schemas. A

            protected-kind behaviour exists for it — never dropped, never
            lossily

            excerpted — but it is **not enabled on the operating points served

            today**, so today a `tool_schema` record is selected like any other.

            Label it correctly anyway: when protection is enabled it is enabled

            by label, and mislabelled records will not benefit.
        id:
          type:
            - string
            - 'null'
          description: >
            Your id for this record. Echoed as `selected[].record_id`. Supply
            one

            if you intend to map the selection back onto your own objects —

            without it, a server-generated id is returned and it will not match

            anything you hold.
        source:
          type:
            - string
            - 'null'
          description: Where the record came from. Echoed on the selection.
        title:
          type:
            - string
            - 'null'
          description: >-
            A short human label. Echoed on the selection, and **metered** — see
            *How to recompute your own bill*.
        step_id:
          type:
            - string
            - 'null'
          description: >-
            The step in your workflow that produced this record. Used for
            recency and drift signals.
        importance:
          type: number
          format: double
          default: 0
          description: |
            Your own relevance prior, if you have one. A hint, not an override:
            a high `importance` does not pin a record into the selection. Use
            `task.required_record_ids` for that.
        keywords:
          type: array
          items:
            type: string
          default: []
          description: >-
            Structurally bounded and **not metered**. Bounded by the published
            size cap; values beyond it are outside the contract and may be
            rejected. See *Limits*.
        tags:
          type: array
          items:
            type: string
          default: []
          description: >-
            Structurally bounded and not metered. Bounded by the published size
            cap; values beyond it are outside the contract and may be rejected.
        attributes:
          type: object
          additionalProperties: true
          default: {}
          description: >
            Opaque per-kind passthrough. Structurally bounded and **not
            metered**.


            It is **not ignored**: the engine folds these values into the text
            it

            infers entities, keywords and tags from, so a well-chosen attribute

            can help. That also means it is not a way to smuggle bulk content

            past the meter: the field is bounded by the published size cap, and

            values beyond it are outside the contract and may be rejected. Put
            content in `text`, where selection actually

            reads it, and keep this for short structured facts.
    TaskSpec:
      type: object
      required:
        - prompt
      description: The query the selected context has to serve.
      properties:
        prompt:
          type: string
          description: |
            What the model is being asked to do. **Metered** — this is
            caller-controlled and unbounded, so it counts toward both your
            per-request ceiling and your bill.
        tool_name:
          type:
            - string
            - 'null'
          description: The tool about to be called, when the selection serves a tool call.
        required_record_ids:
          type: array
          items:
            type: string
          default: []
          description: >
            Records that must appear in the selection.


            **Does not pin today.** Ids here are matched against the engine's
            own

            internal record identity rather than against `records[].id`, so they

            currently match nothing. If a record must reach your prompt, keep it

            out of `records[]` and concatenate it around `rendered_context`

            yourself.
        parent_record_ids:
          type: array
          items:
            type: string
          default: []
          description: |
            Records this task descends from, for lineage signals. **Matched the
            same way as `required_record_ids`, and inert for the same reason.**
        keywords:
          type: array
          items:
            type: string
          default: []
        tags:
          type: array
          items:
            type: string
          default: []
        step_id:
          type:
            - string
            - 'null'
        recent_prompts:
          type: array
          items:
            type: string
          default: []
          description: >
            Prior-step prompts, oldest to newest, intended as drift signals.


            **Not used for drift today** — but they *are* counted against your

            per-request ceiling, and will be counted by the meter. Sending a
            long

            history therefore costs allowance and buys nothing until this is

            wired. Send a few, or none.
        output_mode:
          type:
            - string
            - 'null'
          description: A hint about the shape of the answer being generated.
        output_token_budget:
          type:
            - integer
            - 'null'
          description: How many tokens the downstream answer is expected to occupy.
    BudgetSpec:
      type: object
      required:
        - max_context_tokens
      description: |
        The operating point, in arm-neutral quantities only.

        **No method-tuning knobs appear here or anywhere else in the request.**
        Thresholds, weightings, tier rules and gate parameters are resolved
        server-side from the `operating_point` label. That is a contract
        property, not an omission: it is what makes a published label
        reproducible and what keeps a benchmark comparison honest.
      properties:
        max_context_tokens:
          type: integer
          description: |
            The token allowance the selection must fit inside.

            Set it from what your downstream call can actually accept and
            leave it there. Selections at different budgets are different
            computations — a larger allowance is not guaranteed to contain, or
            to beat, a smaller one — so do not sweep it looking for a quality
            optimum, and do not wire it to a user-facing quality dial.
          examples:
            - 4000
        operating_point:
          type:
            - string
            - 'null'
          description: >
            An opaque, versioned, immutable label naming a frozen configuration.


            **Always send one.** Omitting it silently inherits the server's

            current default, and that default moves when a new label is minted —

            which is exactly how results end up attributed to a configuration
            the

            caller never chose. Read the registry at `/v1/operating-points`.
          examples:
            - np-2026-07-r2
        max_records:
          type:
            - integer
            - 'null'
          description: >-
            Cap the number of records in the selection. Distinct from the tier's
            per-request record ceiling, which rejects the request instead.
        max_excerpt_tokens_per_record:
          type:
            - integer
            - 'null'
          description: |
            Cap the excerpt taken from any single record.

            **Currently ignored** — per-record excerpt size is resolved from the
            operating point.
        mode:
          type: string
          enum:
            - fixed
            - adaptive
          default: fixed
          description: |
            `fixed` selects once against `max_context_tokens`.

            `adaptive` walks an escalation ladder: it selects at
            `adaptive.initial_tokens`, and escalates only if that selection is
            judged insufficient. `attempted_budget_tokens[]` in the response
            reports exactly how far it went. Requires `adaptive` to be set —
            `mode: "adaptive"` with a null `adaptive` object falls back to the
            fixed path.
        adaptive:
          oneOf:
            - $ref: '#/components/schemas/AdaptiveBudget'
            - type: 'null'
          description: |
            The ladder. Optional in the schema, and **`mode: "adaptive"` with
            this absent silently runs as a fixed-budget request** rather than
            erroring — so send it whenever you mean adaptive, and assert on
            `attempted_budget_tokens` if it matters.
        require_evidence_coverage:
          type:
            - boolean
            - 'null'
          description: |
            Ask the engine to run its coverage/answerability check and to stand
            down to full context rather than return a selection that does not
            cover the task's evidence. When set, the verdict is summarised in
            `safety`.

            **Only takes effect with `mode: "adaptive"`.** On the fixed-budget
            path no coverage verdict runs, which is also why `safety` is `null`
            there.
    SelectedRecord:
      type: object
      required:
        - record_id
      description: One selected record.
      properties:
        record_id:
          type: string
          description: >-
            Your `records[].id` when you supplied one; a server-generated id
            otherwise.
        kind:
          type: string
          default: external_data
        title:
          type:
            - string
            - 'null'
        source:
          type:
            - string
            - 'null'
        score:
          type: number
          format: double
          default: 0
          description: >
            Relevance score. Comparable **within one response**; not calibrated

            across requests or across operating points, so do not threshold on
            an

            absolute value.
        reason:
          type: string
          default: ''
          description: >-
            Why this record was selected. Human-readable, open-ended — do not
            exhaustively match it.
        excerpt:
          type: string
          default: ''
          description: |
            The text that entered the selection, built from the record's own
            content. Never paraphrased or summarised into new prose.

            **Not guaranteed byte-identical to a span of your input.** Under a
            tight excerpt budget it may be a whitespace-normalised,
            relevance-ordered set of lines from the record rather than one
            contiguous span, and may be preceded by a short generated
            `Entities: key=value` line. A truncated widest-tier excerpt carries
            a `...[truncated]` marker. If you sent `render_format: "hybrid"`, a
            JSON excerpt may be re-encoded compactly — `excerpt_format` says
            which form this is.

            Join on `record_id` and use your own copy when you need exact bytes.
        excerpt_format:
          type: string
          default: plain
          description: |
            The form `excerpt` is in. `plain` is text. Other values indicate a
            re-encoding — for example a compact tabular form for JSON, which is
            only reachable when you send `render_format: "hybrid"`. Treat this
            as an open set and fall back to handling the value as text.
        selected_tokens:
          type: integer
          default: 0
          description: Tokens this record contributed to the selection.
    SafetySummary:
      type: object
      description: |
        The neutral subset of a coverage/answerability verdict. The method's
        internal obligation taxonomy and repair internals stay server-side; only
        the outcome crosses the boundary.
      properties:
        selection_safe:
          type: boolean
          default: true
        fallback_required:
          type: boolean
          default: false
        fallback_reason:
          type: string
          default: ''
          description: |
            The *coverage verdict's* reason. It is not the general stand-down
            reason — on the fixed-budget path no verdict runs at all and this
            object is absent. See `format_metrics.engine_fallback_reason`.
        coverage_score:
          type: number
          format: double
          default: 0
        evidence_shape:
          type: string
          default: unknown
        evidence_terms:
          type:
            - object
            - 'null'
          additionalProperties:
            type: array
            items:
              type: string
        repair_reasons:
          type: array
          items:
            type: string
          default: []
    GateSummary:
      type: object
      description: |
        The neutral engage / stand-down summary.

        **The outcome is an open, extensible enum — do not exhaustively match
        it.** Today `engaged: true` is the *select* outcome and `engaged: false`
        is the *stand_down* outcome, with the trigger in `reason`. A third
        outcome, *escalate*, is planned and will be introduced additively (a new
        `reason` prefix and/or an optional outcome field), so a client that
        tolerates unrecognised prefixes will not break.
      properties:
        engaged:
          type: boolean
          default: false
        reason:
          type: string
          default: ''
          examples:
            - engage:needle
            - standdown:high_drift
        signals:
          type: object
          additionalProperties: true
          default: {}
          description: >-
            JSON-safe telemetry about the decision. Informational; the key set
            is not contractual.
    Error:
      type: object
      required:
        - error
      description: |
        The **service's** error envelope. Distinguishable at a glance from a
        gateway-generated error, which carries a `message`/`Message` key instead
        and never an `error` key — see the individual responses.
      properties:
        error:
          type: string
          description: A stable machine-readable code. Match on this, not on `detail`.
          enum:
            - bad_request
            - request_too_large
            - engine_error
            - not_found
        detail:
          type: string
          description: Human-readable. **Not stable** — do not parse it.
    GatewayError:
      type: object
      description: |
        Generated by API Gateway, not by the service. The key is capitalised
        inconsistently between cases (`message` vs `Message`) because these are
        AWS's own bodies passed through unchanged — **match on the status code,
        not on this body.**
      properties:
        message:
          type: string
        Message:
          type: string
    RequestTooLargeError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          required:
            - limit_name
            - limit
            - observed
          properties:
            limit_name:
              type: string
              description: Which ceiling was exceeded.
              enum:
                - input_tokens
                - records
            limit:
              type: integer
              description: Your tier's ceiling for that quantity.
            observed:
              type: integer
              description: >-
                What this request measured. The pair is what lets a client
                resize precisely instead of guessing.
    AdaptiveBudget:
      type: object
      required:
        - initial_tokens
      description: The escalation ladder for adaptive mode.
      properties:
        initial_tokens:
          type: integer
          description: The first rung. Selection is attempted here first.
        escalation_tokens:
          type: array
          items:
            type: integer
          default: []
          description: |
            Further rungs, ascending. Each rung that is attempted is a full
            selection pass, so a workload that always escalates to the top does
            several times the work of one that succeeds on the first rung.
        allow_full_context_fallback:
          type: boolean
          default: true
          description: >
            Whether the ladder may end in passing the whole context through. Set

            `false` only if your downstream call has a hard cap that full
            context

            would exceed — with it off, a request that cannot be satisfied
            within

            the ladder returns the best selection it found rather than
            everything.
  responses:
    BadRequest:
      description: |
        The body is not a well-formed `ContextRequest` — malformed JSON, a
        missing required field, or a field of the wrong type. `detail` is the
        parser's own message about structure.

        It is returned to you and deliberately **not** written to our logs,
        because a type error can quote the offending value — so `detail` may
        contain a fragment of your own payload. Show it to a developer; think
        before you write it to *your* logs.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: bad_request
            detail: '''prompt'''
    Unauthorized:
      description: |
        **No `Authorization` header at all.** Rejected at the gateway before
        anything of ours is invoked, so the body is the platform's, not ours.

        A header that is present but not `Bearer`-shaped is **not** a 401 — it
        reaches authorization and comes back `403`. Wire the two separately:
        **401 = the header never arrived** (fix the client), **403 = it arrived
        and was refused** (fix the credential).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GatewayError'
          example:
            message: Unauthorized
    PaymentRequired:
      description: |
        **Reserved.** Not returned by the current service. Documented so
        clients reserve the case and surface "out of credit" distinctly from
        an auth failure if it ever appears.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GatewayError'
    Forbidden:
      description: >
        A header arrived and authorization refused it. The reason is
        deliberately

        not returned — a caller learns only that the credential does not work,

        because an error that distinguishes "no such key" from "suspended org"
        is

        an oracle.


        Causes: an `Authorization` header that is present but not
        `Bearer`-shaped;

        an unknown, revoked or malformed key; a suspended org; a control-plane

        projection too stale to vouch for.


        Not retryable. Retrying a 403 will not resolve any of its causes.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GatewayError'
          example:
            Message: >-
              User is not authorized to access this resource with an explicit
              deny
    RequestTooLarge:
      description: >
        Well-formed, but over a ceiling. **This is the one exact, pre-request
        cost

        control** — it is checked before the engine runs and before anything is

        billable.


        413 rather than 400 on purpose: your payload is *valid*, so the fix is
        to

        shrink or split it, not to correct it. `limit_name`, `limit` and

        `observed` tell you which axis and by how much.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RequestTooLargeError'
          example:
            error: request_too_large
            detail: input_tokens 143820 exceeds the limit of 100000
            limit_name: input_tokens
            limit: 100000
            observed: 143820
    TooManyRequests:
      description: |
        A usage-plan limit was hit — either the per-second rate/burst, or the
        daily quota. Both surface as 429 with an AWS body: `Too Many Requests`
        for throttling, `Limit Exceeded` for the daily quota. **The distinction
        matters for retry**: a throttle clears in milliseconds, a daily quota
        does not clear until the quota period rolls.

        Retry throttles with exponential backoff and jitter. Do not retry a
        `Limit Exceeded` in a tight loop.

        Your per-minute and per-day figures are **targets, not hard ceilings** —
        see *Limits* for why, and for what actually is exact.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GatewayError'
          examples:
            throttled:
              value:
                message: Too Many Requests
            quota:
              value:
                message: Limit Exceeded
    EngineError:
      description: >
        The service could not produce a response at all — a transport-level

        failure, an unresolvable `operating_point`, or a record `kind` outside
        the

        published enum.


        **Note the asymmetry, because it decides how you monitor us.** A failure

        of the *selection pass itself* does not reach here: it returns **200**

        with `selection_error` populated and a usable full-context body. So 5xx

        is not a complete picture of engine health, and `selection_error` is the

        signal to alarm on.


        Two shapes. **`{"error": "engine_error", …}`** is ours; a body with no

        `error` key is the platform's, produced when something in front of the

        service fails.


        Retry only the second, with capped exponential backoff. The two rough

        edges named above are **deterministic** — the same unresolvable label or

        the same unknown `kind` fails identically every time, so retrying an

        unchanged request cannot help.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: engine_error
            detail: 'ValueError: unknown operating_point: ''np-2026-07-r9'''
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer np_live_…`

        One credential form. There is no query-parameter, cookie or
        `x-api-key` alternative — the legacy `x-api-key` header stopped being
        accepted at the Bearer cutover and now fails authorization.

        Keys are **org-scoped**, never user-scoped, and the secret is shown
        exactly once at mint. Rotation is create-then-revoke.

````