openapi: 3.0.3
info:
  title: naturali.ai — Agent Generations API
  version: 1.0.0
  description: >
    Run an agent, and read what it ran (API.md §5 — Agents; §2 — Trace /
    Generation). A generation sends messages to an agent, resolves its tools, and
    runs the model loop, returning the final text (and a structured object when
    the agent has an output schema). Generations are a pass-through to the
    backing runtime agent: the cost is metered on the runtime and surfaced through
    GET /v1/projects/{project_id}/usage, so naturali persists no generation row
    of its own.

    The record is still readable. `POST …/generations` and a session reply both
    hand back a generation id, and the two GETs here resolve one — its lifecycle
    status, why it stopped, the structured error when it failed, and the
    `action_id` it was labelled with. Grouped by the execution that produced
    them, the same records are served by
    [`traces.yaml`](./traces.yaml).

    **Two shapes, deliberately.** `GenerationResult` is what running one returns
    (the model's output — text, object, pending tool calls). `Generation` is the
    stored record (lifecycle, timing, error, metadata). A read returns the
    record; it does not replay the output.
  contact:
    name: naturali.ai
    url: https://naturali.ai
servers:
  - url: '{baseUrl}'
    description: Host of your naturali.ai deployment; every path carries the /v1 prefix.
    variables:
      baseUrl:
        description: Base host URL.
        default: https://api.naturali.ai
tags:
  - name: Generations
    description: Run an agent and get its response; read the resulting records.
security:
  - bearerAuth: []
paths:
  /v1/projects/{project_id}/agents/{agent_id}/generations:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/AgentId'
    post:
      tags: [Generations]
      summary: Run an agent generation
      description: >
        Sends messages to the agent, resolves its tools, and runs the model
        loop.


        Background by default: this returns `202` immediately with a
        `generation_id`, and the turn runs on. Poll
        [`GET /v1/projects/{project_id}/generations/{generation_id}`](/docs/api/generations/get-generation)
        until its `status` leaves `in_progress`.


        Pass `?wait=true` to block instead and receive the turn itself — the
        final text when `status` is `completed` (plus `object` when the agent
        has an output schema), or the pending `tool_calls` when `status` is
        `requires_action`.


        With `stream: true` the response is a Server-Sent Events stream
        (Content-Type text/event-stream) proxied from the runtime. A stream
        holds the request open by definition, so it always waits; combining it
        with an explicit `wait=false` is a `400`.
      operationId: createGeneration
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/Wait'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerationCreate'
      responses:
        '202':
          description: >
            Accepted — the default. The generation is running in the
            background; poll the returned `generation_id`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedGeneration'
        '200':
          description: >
            The settled generation — returned only with `wait=true` or, as a
            Server-Sent Events stream, with `stream: true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationResult'
            text/event-stream:
              schema:
                type: string
                description: >
                  A Server-Sent Events stream proxied from the runtime; each
                  event is a chunk of the generation as it is produced.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
    get:
      tags: [Generations]
      summary: List an agent's generations
      description: >
        Lists the generation records the agent has produced, newest first.
        Filter by lifecycle `status` to find the failures without paging
        everything the agent has ever run.
      operationId: listAgentGenerations
      parameters:
        - $ref: '#/components/parameters/GenerationStatus'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: A page of generation records.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/generations/{generation_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/GenerationId'
    get:
      tags: [Generations]
      summary: Get a generation
      description: >
        Returns one generation record. Flat rather than nested under the agent,
        because the ids that need resolving arrive on their own — a session reply
        carries a `generation_id` with no agent in hand.

        A generation belonging to another project responds `404`, not `403` — the
        API never confirms that an id exists elsewhere.
      operationId: getGeneration
      responses:
        '200':
          description: The generation record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Generation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/generations/{generation_id}/content:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/GenerationId'
    delete:
      tags: [Generations]
      summary: Purge a generation's content
      description: >
        Clears the generation's content — `metadata`, `error`, `extraction` and
        the internal recovery state of a paused run — and stamps
        `content_redacted_at` as verifiable proof the content is gone.

        The billing and audit skeleton is preserved: ids, timestamps, status,
        stop reason and the attribution fields (`action_id`, `trigger_id`) the
        usage ledger reads. A purged generation still reads back with
        `GET /v1/projects/{project_id}/generations/{generation_id}` — a `404`
        there would prove nothing about what was erased.

        This is the narrow erasure, scoped to one model turn. It does **not**
        delete the parent trace's step payload, which holds this generation's
        content alongside its siblings'. To erase a whole run's content, purge
        the trace with
        `DELETE /v1/projects/{project_id}/traces/{trace_id}/content`, which
        cascades to every descendant trace and all of their generations.

        Idempotent: purging an already-purged generation succeeds and leaves the
        original `content_redacted_at` in place.

        A generation belonging to another project responds `404`, not `403`, and
        nothing is purged.
      operationId: purgeGenerationContent
      responses:
        '200':
          description: The purged generation skeleton.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Generation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/generations/{generation_id}/usage:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/GenerationId'
    get:
      tags: [Generations]
      summary: Get a generation's cost
      description: >
        What this one generation cost, and the tokens it was charged on — the
        billing-grade receipt the runtime froze at write time, per model line item.

        This is the per-generation grain that
        `GET /v1/projects/{project_id}/usage` cannot express: that meter buckets
        a whole project by model, agent, run, day or meter type, and a run can
        hold more than one generation. Use this to price a single turn, and the
        project meter to roll spend up.

        `cost_usd` is `null` when nothing was priced — never that the work was
        free. Only naturali-managed providers are priced; a BYOK generation runs
        on your own provider account, so it carries no LLM cost here (its tokens
        are still reported).

        A generation belonging to another project responds `404`, not `403`.
      operationId: getGenerationUsage
      responses:
        '200':
          description: The generation's usage receipt.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationUsage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Client-supplied key to make this mutating POST idempotent.
      schema:
        type: string
    Wait:
      name: wait
      in: query
      required: false
      description: >-
        When omitted or `false` (the default), the generation runs in the
        background and this returns `202` with a `generation_id` to poll. Pass
        `true` to block until the turn settles and receive it inline.
      schema:
        type: boolean
        default: false
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
    AgentId:
      name: agent_id
      in: path
      required: true
      description: Agent public ID (agent_ prefix).
      schema:
        type: string
        example: agent_V1StGXR8Z5jdHi6B
    GenerationId:
      name: generation_id
      in: path
      required: true
      description: Generation public ID (gen_ prefix).
      schema:
        type: string
        example: gen_V1StGXR8Z5jdHi6B
    GenerationStatus:
      name: status
      in: query
      required: false
      description: Filter to one lifecycle status.
      schema:
        type: string
        enum: [in_progress, requires_action, completed, failed]
      example: failed
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum items to return (1–100).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Offset:
      name: offset
      in: query
      required: false
      description: >
        Items to skip. These lists page by offset rather than by naturali's
        usual opaque cursor because the upstream ordering is offset-based;
        a cursor here would only imitate a keyset.
      schema:
        type: integer
        minimum: 0
        default: 0
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: The resource does not exist (existence is not leaked).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UpstreamUnavailable:
      description: The upstream runtime could not complete the operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    GenerationCreate:
      type: object
      description: The messages to run the agent over, plus optional metering hints.
      required:
        - messages
      properties:
        messages:
          type: array
          minItems: 1
          description: The conversation to run the model turn over.
          items:
            $ref: '#/components/schemas/Message'
        stream:
          type: boolean
          description: >
            When true, the response is a Server-Sent Events stream
            (text/event-stream) instead of a single JSON body.
          default: false
          example: false
        action_id:
          type: string
          description: >
            Logical action label recorded on the generation's usage meter, so
            spend can be rolled up per action.
          example: support.triage
        trace_id:
          type: string
          description: Optional trace ID to group related generations.
          example: trace_V1StGXR8Z5jdHi6B
        metadata:
          type: object
          additionalProperties: true
          description: Caller-supplied key/value metadata attached to the generation record.
    Message:
      type: object
      required:
        - role
        - content
      properties:
        role:
          type: string
          enum: [user, assistant]
          example: user
          description: >
            Who authored the message. A `system` role is not accepted here: an
            agent's system prompt is operator configuration, set once as the
            agent's `instructions`, not something a caller supplies per
            request. Sending one is rejected with `400 validation_failed`.
        content:
          type: string
          description: The message text.
          example: What is the capital of France?
    AcceptedGeneration:
      type: object
      description: >
        The handle returned when a generation is left to run in the background —
        the default. It names the turn, not its outcome: read
        [`GET /v1/projects/{project_id}/generations/{generation_id}`](/docs/api/generations/get-generation)
        for that.
      required:
        - status
        - generation_id
      properties:
        status:
          type: string
          enum: [accepted]
          description: Always `accepted` — the turn was queued, not settled.
          example: accepted
        generation_id:
          type: string
          description: Public generation ID (gen_ prefix) to poll.
          example: gen_V1StGXR8Z5jdHi6B
        agent_id:
          type: string
          description: The agent running the generation.
          example: agent_V1StGXR8Z5jdHi6B
        trace_id:
          type: string
          nullable: true
          description: The execution this generation belongs to.
          example: trace_V1StGXR8Z5jdHi6B
    GenerationResult:
      type: object
      description: >
        What running a generation returns — the model's output for this turn. The
        stored record is `Generation`; reading it back does not replay the output.
      properties:
        id:
          type: string
          nullable: true
          description: Public generation ID (gen_ prefix).
          example: gen_V1StGXR8Z5jdHi6B
        agent_id:
          type: string
          description: The agent that produced the generation.
          example: agent_V1StGXR8Z5jdHi6B
        status:
          type: string
          nullable: true
          enum: [completed, requires_action]
          description: >
            `completed` when the turn finished; `requires_action` when it paused
            on client tool calls (see tool_calls).
          example: completed
        text:
          type: string
          nullable: true
          description: The final text output, when completed.
          example: Paris
        object:
          type: object
          additionalProperties: true
          nullable: true
          description: >
            Structured object matching the agent's output schema, when completed
            and an output schema is set.
        tool_calls:
          type: array
          nullable: true
          description: Pending tool calls, when status is requires_action.
          items:
            type: object
            properties:
              tool_call_id:
                type: string
              tool_name:
                type: string
              args:
                type: object
                additionalProperties: true
      required:
        - agent_id
        - status
        - text
        - object
        - tool_calls
    Generation:
      type: object
      description: >
        A stored generation record — one model loop an agent ran, as the runtime
        recorded it. This is what a read returns; `GenerationResult` is what
        running one returns.

        The record carries no prompt or completion text: the transcript belongs
        to the session that ran the turn, and the step-level detail to the trace.
      properties:
        id:
          type: string
          nullable: true
          description: Public generation ID (gen_ prefix).
          example: gen_V1StGXR8Z5jdHi6B
        project_id:
          type: string
          nullable: true
          x-naturali-ref: project
          example: proj_V1StGXR8Z5jdHi6B
        agent_id:
          type: string
          nullable: true
          x-naturali-ref: agent
          description: The agent that ran this generation.
          example: agent_V1StGXR8Z5jdHi6B
        trace_id:
          type: string
          nullable: true
          x-naturali-ref: trace
          description: The execution this generation belongs to.
          example: trace_V1StGXR8Z5jdHi6B
        initiator_generation_id:
          type: string
          nullable: true
          x-naturali-ref: generation
          description: >
            The generation whose sub-agent call started this one. Null for a
            top-level generation.
          example: gen_9f2StGXR8Z5jdHi6
        status:
          type: string
          nullable: true
          enum: [in_progress, requires_action, completed, failed]
          description: >
            Lifecycle status. Wider than `GenerationResult.status`, which only
            ever reports the two states a returning call can be in.
          example: completed
        stop_reason:
          type: string
          nullable: true
          description: Why the generation stopped (e.g. `stop`, `error`).
          example: stop
        error:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            Structured error recorded when the generation failed — at least
            `message`, plus `code` for mapped errors (e.g. `AI_PROVIDER_ERROR`
            for an upstream provider failure). Null when it did not fail.
          example:
            { code: AI_PROVIDER_ERROR, message: upstream provider returned 429 }
        action_id:
          type: string
          nullable: true
          description: >
            The label supplied as `action_id` when the generation was run,
            recorded on its usage event so spend rolls up per action.
          example: support.triage
        trigger_id:
          type: string
          nullable: true
          description: The trigger that started this generation, when applicable.
          example: trg_V1StGXR8Z5jdHi6B
        extraction:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            Memory-extraction summary recorded for this generation — set when
            the agent's knowledge config produced one for this turn
            (`candidates`, `created`, `updated`, `skipped`).
          example: { candidates: 3, created: 2, updated: 0, skipped: 1 }
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            Caller-supplied key/value metadata attached to the generation
            record — nothing else. Server-owned state has its own top-level
            fields (`action_id`, `trigger_id`, `extraction`) and is never
            merged into this object.
          example: { ticket_id: ZD-4821 }
        content_redacted_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When this generation's content was purged, either directly via
            `DELETE /v1/projects/{project_id}/generations/{generation_id}/content`
            or by the cascade from its trace's purge. Non-null means `metadata`,
            `error` and `extraction` have been cleared while the billing and
            audit skeleton was preserved — which is what makes it the proof that
            an erasure happened, rather than a record that simply never carried
            any content. Null when nothing has been purged.
          example: null
        started_at:
          type: string
          format: date-time
          nullable: true
          example: '2026-07-14T09:00:00.000Z'
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: When the generation reached a terminal state; null until then.
          example: '2026-07-14T09:00:04.512Z'
        last_activity_at:
          type: string
          format: date-time
          nullable: true
          example: '2026-07-14T09:00:04.512Z'
        created_at:
          type: string
          format: date-time
          nullable: true
          example: '2026-07-14T09:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          nullable: true
          example: '2026-07-14T09:00:04.512Z'
      required:
        - id
        - project_id
        - agent_id
        - trace_id
        - initiator_generation_id
        - status
        - stop_reason
        - error
        - action_id
        - trigger_id
        - extraction
        - metadata
        - content_redacted_at
        - started_at
        - completed_at
        - last_activity_at
        - created_at
        - updated_at
    GenerationList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Generation'
        total:
          type: integer
          nullable: true
          description: Total generations matching the query, when the upstream reports it.
          example: 42
        limit:
          type: integer
          example: 20
        offset:
          type: integer
          example: 0
      required: [data, total, limit, offset]
    GenerationUsage:
      type: object
      description: >
        One generation's billing receipt — the cost, the tokens it was charged
        on, and a line item per metered event explaining how the charge was
        reached.

        Token names match `GET /v1/projects/{project_id}/usage` so the two cost
        surfaces read alike: `input_tokens` is the full prompt (uncached plus
        cached), and `total_tokens` is `input_tokens + output_tokens`.
      properties:
        generation_id:
          type: string
          x-naturali-ref: generation
          example: gen_V1StGXR8Z5jdHi6B
        currency:
          type: string
          description: Currency the costs are expressed in.
          example: USD
        cost_usd:
          type: number
          nullable: true
          description: >
            Billing-grade cost for the whole generation, frozen at write time;
            null when nothing was priced (never that it was free).
          example: 0.004231
        input_tokens:
          type: integer
          description: Full prompt tokens — uncached input plus cached.
          example: 1620
        output_tokens:
          type: integer
          example: 240
        cached_tokens:
          type: integer
          description: The cached portion of input_tokens.
          example: 1024
        reasoning_tokens:
          type: integer
          description: >
            Reasoning tokens, a non-billable subset of output_tokens reported for
            visibility — never priced, never double-counted into the total.
          example: 96
        total_tokens:
          type: integer
          description: input_tokens + output_tokens.
          example: 1860
        by_meter_type:
          type: array
          description: >
            Cost split per meter type — the "tokens plus infra" view. A
            single-type receipt has one entry whose cost equals cost_usd.
          items:
            type: object
            properties:
              meter_type:
                type: string
                nullable: true
                description: e.g. `llm_tokens`, `compute_execution`.
                example: llm_tokens
              cost_usd:
                type: number
                nullable: true
                example: 0.004231
            required: [meter_type, cost_usd]
        line_items:
          type: array
          description: One entry per metered event on this generation.
          items:
            type: object
            properties:
              event_id:
                type: string
                nullable: true
                description: Public ID of the usage event, for reconciliation.
                example: uev_V1StGXR8Z5jdHi6B
              meter_type:
                type: string
                nullable: true
                example: llm_tokens
              provider:
                type: string
                nullable: true
                description: The upstream provider that served the call.
                example: bedrock
              model:
                type: string
                nullable: true
                example: anthropic.claude-haiku-4-5-20251001-v1:0
              cost_usd:
                type: number
                nullable: true
                example: 0.004231
              components:
                type: array
                description: >
                  The measured quantities behind this line item's cost —
                  quantity × unit_price, per billable dimension.
                items:
                  type: object
                  properties:
                    component:
                      type: string
                      nullable: true
                      description: The measured dimension.
                      example: input_tokens
                    quantity:
                      type: integer
                      example: 1620
                    unit:
                      type: string
                      nullable: true
                      example: token
                    billable:
                      type: boolean
                      description: >
                        Whether this component contributes to cost. Non-billable
                        details (e.g. reasoning_tokens) are reported but never
                        priced.
                      example: true
                    unit_price:
                      type: number
                      nullable: true
                      description: USD per unit, frozen at write time.
                      example: 0.0000008
                    cost_usd:
                      type: number
                      nullable: true
                      example: 0.001296
                  required:
                    - component
                    - quantity
                    - unit
                    - billable
                    - unit_price
                    - cost_usd
            required:
              [event_id, meter_type, provider, model, cost_usd, components]
      required:
        - generation_id
        - currency
        - cost_usd
        - input_tokens
        - output_tokens
        - cached_tokens
        - reasoning_tokens
        - total_tokens
        - by_meter_type
        - line_items
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: upstream_unavailable
            message:
              type: string
              example: Could not run the generation on the upstream runtime.
            details:
              type: object
              additionalProperties: true
              description: Optional structured context for the error.
