# AUTO-GENERATED tier-A mirror spec — do not edit.
# Source: openapi/runtime/sessions.yaml (runtime 0.71.2)
#  + policy: openapi/mirror-policies/sessions.json
# Regenerate: node scripts/generate-mirror-specs.mjs
openapi: 3.0.3
info:
  title: naturali.ai — Sessions API
  version: 1.0.0
  description: >-
    Sessions: durable, resumable dialogues with an agent — messages, generations, tool outputs,
    forks and tags. A fully runtime-backed module — this spec is generated verbatim from the
    runtime's own, re-rooted under /v1/projects/{project_id}. The project in the path is authorized
    by naturali and enforced upstream by the project's scoped credential.


    This module mirrors the upstream runtime verbatim (tier A, #304): paths are the runtime's own
    re-rooted under /v1/projects/{project_id}, and every field, method, status code and error shape
    passes through unchanged. Errors raised by the runtime arrive in its envelope; errors raised by
    naturali itself (authentication, project resolution, an unreachable runtime) use naturali's.
  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: Sessions
    description: Manage agent sessions
security:
  - bearerAuth: []
  - oauth2:
      - mcp:access
paths:
  /v1/projects/{project_id}/sessions:
    post:
      tags:
        - Sessions
      summary: Create a session
      description: >
        Creates a new session for the specified agent, along with the underlying conversation, so
        the caller only needs this single call to start interacting with the agent. No actor is
        created: pass `actor_id` to attach an existing actor as the session's end user. When it is
        omitted the session has no actor, and generations in it carry no end-user attribution — they
        are not billed to an actor in the usage meter and they match no `actor`-scoped quota.
      operationId: createSession
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: An open session already exists for this actor (single_session_per_actor is enabled)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: SINGLE_SESSION_CONFLICT
                  message: An open session already exists for this actor.
                  meta:
                    session_id: sess_abc123
    get:
      tags:
        - Sessions
      summary: List sessions
      description: Returns sessions the caller can access, optionally filtered by agent, actor and status.
      operationId: listSessions
      parameters:
        - name: agent_id
          in: query
          required: false
          description: Filter by agent public ID
          schema:
            type: string
            example: agent_V1StGXR8Z5jdHi6B
        - name: actor_id
          in: query
          required: false
          description: Filter by actor public ID
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filter by session status (open, closed, or expired)
          schema:
            type: string
            enum:
              - open
              - closed
              - expired
        - $ref: "#/components/parameters/TagsQuery"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Paginated list of sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SessionRecord"
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}:
    get:
      tags:
        - Sessions
      summary: Get a session
      description: Returns details of a single session, including a `usage` roll-up of what its
        generations cost. The listing omits `usage`; for a window, a split by day or model, or one
        end user across every session, narrow `GET /v1/projects/{project_id}/usage` with
        `session_id` / `actor_id` instead.
      operationId: getSession
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - Sessions
      summary: Update a session
      description: Updates the session name and/or status.
      operationId: updateSession
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSessionRequest"
      responses:
        "200":
          description: Updated session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags:
        - Sessions
      summary: Delete a session
      description: >
        Deletes the session and its underlying conversation and messages. The session's actor is not
        deleted. Generations and traces produced by the session are not deleted either, since they
        are not linked to the session or conversation.
      operationId: deleteSession
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "204":
          description: Session deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}/messages:
    post:
      tags:
        - Sessions
      summary: Add a user message
      description: >
        Saves a user message to the session. When autoGenerate is enabled on the session and no
        generation is currently in progress, generation is triggered automatically and the response
        mirrors GenerateSessionResponse. Otherwise returns the saved user message.
      operationId: addSessionMessage
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddSessionMessageRequest"
      responses:
        "200":
          description: Duplicate request — original message returned (idempotency_key matched)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddSessionMessageSaved"
        "201":
          description: User message saved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddSessionMessageResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          description: "`QUOTA_EXCEEDED`: the session auto-generates and an `enforce`-mode generation quota is
            exhausted. `error.meta` carries `quota_id`, `metric`, `limit`, `window` and `resets_at`,
            with a `Retry-After` header. The message is saved. A session with
            `message_delay_seconds` answers `201` before the quota is read."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}/generate:
    post:
      tags:
        - Sessions
      summary: Trigger agent generation
      description: >
        Triggers the agent to generate a response based on the current conversation. Background by
        default: returns `202 Accepted` immediately while the generation runs. Pass ?wait=true to
        block and receive the assistant reply (or a requires_action status if the agent needs client
        tool outputs) in the response.
      operationId: generateSessionResponse
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: wait
          in: query
          required: false
          x-naturali-tool-forced: true
          description: When omitted or `false` (default), generation runs in the background and `202 Accepted`
            is returned immediately. Pass `true` to block until the generation settles and receive
            the result. An MCP tool call always waits.
          schema:
            type: boolean
            default: false
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateSessionRequest"
      responses:
        "200":
          description: Agent reply or requires_action (only when `?wait=true`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenerateSessionResponse"
        "202":
          description: Generation accepted and running in the background (default, when `wait` is omitted or
            `false`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - accepted
                  session_id:
                    type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Generation already in progress
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Session has expired due to inactivity
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: "`QUOTA_EXCEEDED`: an `enforce`-mode generation quota is exhausted. `error.meta`
            carries `quota_id`, `metric`, `limit`, `window` and `resets_at`, with a `Retry-After`
            header. Only with `?wait=true`: a background call is answered `202` before the quota is
            read."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: >
            Upstream AI provider error (AI_PROVIDER_ERROR). The error `meta` includes the
            `generation_id` and `trace_id` of the failed generation for post-mortem debugging via
            GET /v1/projects/{project_id}/generations/{generation_id}.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}/tool-outputs:
    post:
      tags:
        - Sessions
      summary: Submit tool outputs
      description: >
        Submits client tool outputs for a generation that returned requires_action. The agent
        continues its loop and returns the final or next requires_action result.
      operationId: submitSessionToolOutputs
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubmitSessionToolOutputsRequest"
      responses:
        "200":
          description: Generation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendSessionMessageResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: >
            The generation is not paused on client tool calls
            (GENERATION_NOT_AWAITING_TOOL_OUTPUTS): it never paused, or its outputs were already
            submitted. Each pause accepts outputs once.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}/fork:
    post:
      tags:
        - Sessions
      summary: Fork a session
      description: >
        Branches a new session from a point in this session's history: same context, different
        continuation.


        The fork gets its own conversation whose messages **reference the same documents** as the
        parent rather than copying them, so there is one stored copy of the content and a retention
        purge erases it from both. Recorded tool results ride along on those messages and are
        **replayed** as model input on the fork's next turn — forking never re-invokes a tool, so
        exploring a "what if" cannot send an email or charge a card a second time. The consequence
        to accept is that a forked turn sees the tool data as it was, not as it is now.


        The fork is created **inert**: `auto_generate` is false and no generation is triggered.
        Drive it with the normal message and generate endpoints. The fork has no actor — attach one
        only if the branch is meant to be driven by the same end user, since
        `single_session_per_actor` agents allow one open session per actor.
      operationId: forkSession
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForkSessionRequest"
      responses:
        "201":
          description: Fork created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionRecord"
        "400":
          description: "`fork_at_position` names no message in the parent conversation, or `agent_id` is
            unknown or belongs to another project"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: VALIDATION_FAILED
                  message: fork_at_position 9 does not exist in the parent conversation.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}/forks:
    get:
      tags:
        - Sessions
      summary: List a session's forks
      description: >
        Returns the sessions forked directly from this one. One level of lineage: a fork of a fork
        is listed under its own parent.
      operationId: listSessionForks
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Paginated list of forks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SessionRecord"
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/sessions/{session_id}/tags:
    get:
      tags:
        - Sessions
      summary: Get session tags
      description: Returns the session's tags object.
      operationId: getSessionTags
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagBag"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags:
        - Sessions
      summary: Replace session tags
      description: Replaces all tags on the session.
      operationId: replaceSessionTags
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TagBag"
      responses:
        "200":
          description: Updated tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagBag"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - Sessions
      summary: Merge session tags
      description: Merges the provided tags into the session's existing tags.
      operationId: mergeSessionTags
      x-naturali-resource:
        kind: session
        from: session_id
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TagBag"
      responses:
        "200":
          description: Updated tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagBag"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
components:
  schemas:
    SessionRecord:
      type: object
      properties:
        id:
          type: string
          description: Session public ID
          example: sess_V1StGXR8Z5jdHi6B
        agent_id:
          x-naturali-ref: agents
          type: string
          description: |
            Agent public ID, kept after a shared agent is deleted by its owner.
          example: agent_V1StGXR8Z5jdHi6B
        conversation_id:
          x-naturali-ref: conversations
          type: string
          description: Underlying conversation public ID
          example: conv_V1StGXR8Z5jdHi6B
        status:
          type: string
          enum:
            - open
            - closed
            - expired
          example: open
        name:
          type: string
          nullable: true
          example: Support chat
        actor_id:
          x-naturali-ref: actors
          type: string
          nullable: true
          description: |
            Public ID of the user actor, or null when the session was created without one
          example: actor_V1StGXR8Z5jdHi6B
        tags:
          $ref: "#/components/schemas/TagBag"
        auto_generate:
          type: boolean
          default: false
          description: When true, automatically triggers generation after each user message (if no generation
            is in progress).
        generating_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the current generation started, or null if not generating.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        inactivity_ttl_seconds:
          type: integer
          default: 0
          description: Number of seconds of inactivity after which the session expires. 0 means the session
            never expires.
          example: 300
        message_delay_seconds:
          type: integer
          nullable: true
          default: null
          description: >
            Number of seconds to wait after the last user message before sending to the LLM. Acts as
            a debounce: each new message resets the timer. null or absent means no delay (immediate
            processing).
          example: 3
        last_activity_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the last activity on the session (message added or response generated).
        forked_from_session_id:
          x-naturali-ref: sessions
          type: string
          nullable: true
          description: >
            Public ID of the session this one was forked from, or null when it was not forked. Also
            null once that parent is deleted — a fork survives its parent and keeps its own history.
          example: sess_V1StGXR8Z5jdHi6B
        forked_from_position:
          type: integer
          nullable: true
          description: >
            The parent conversation position this session branched after, or null when it is not a
            fork or was forked at the tip.
          example: 7
        usage:
          allOf:
            - $ref: "#/components/schemas/UsageTotals"
          description: >-
            What the session's generations cost: token counts and `cost_usd` summed across every
            metered generation dispatched through it. Present on the single-session read; omitted
            from session and fork list responses.


            A fork is a session of its own, so it starts at zero rather than inheriting what the
            history it copied cost — summing `usage` across a session and its forks therefore never
            double-counts. Work the platform does around a session without a generation of its own
            (a memory extraction pass, for instance) is metered on the project, not here.
    ForkSessionRequest:
      type: object
      additionalProperties: false
      properties:
        fork_at_position:
          type: integer
          minimum: 0
          description: >
            The parent conversation `position` to branch after. Messages at positions 0..N are
            carried into the fork. Omit it to branch at the tip (the whole history).
          example: 7
        agent_id:
          x-naturali-ref: agents
          type: string
          description: >
            Agent the fork runs against. Defaults to the parent session's agent; overriding it is
            the point of forking — same context, a different agent or agent version. Must belong to
            the same project as the session being forked.
          example: agent_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional name for the forked session
          example: retry with stricter system prompt
        tags:
          $ref: "#/components/schemas/TagBag"
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >
            Overrides the parent's `tool_context` on the fork. Omit it and the fork inherits the
            parent's, so the branch is faithful to the run it came from.
    CreateSessionRequest:
      type: object
      required:
        - agent_id
      additionalProperties: false
      properties:
        agent_id:
          x-naturali-ref: agents
          type: string
          description: >
            Agent this session belongs to. With a credential scoped to a project, it may also be an
            agent another project shares with that project; the session is then that project's.
          example: agent_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional session name
          example: Support chat
        actor_id:
          x-naturali-ref: actors
          type: string
          description: >
            Optional public ID of an existing actor to use as the user actor. Actors are created
            separately (POST /actors); this field only links one. Omit it and the session has no end
            user, so its generations match no actor-scoped quota.
          example: actor_V1StGXR8Z5jdHi6B
        auto_generate:
          type: boolean
          default: false
          description: When true, automatically triggers generation after each user message.
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: "Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and
            `mcp` tool call in this session. The header name is the deployment's configured context
            prefix (`X-Naturali-Context-` by default) plus the key verbatim — no character is
            re-cased. Keys are stored exactly as sent and never case-converted. A key that is not a
            valid HTTP header name, or two keys that map to the same header, are rejected with `400
            INVALID_TOOL_CONTEXT_KEY`. `session_id`, `actor_id` and `actor_external_id` are
            server-derived and dropped from the stored bag, in any casing. Write-only: no read of a
            session returns it. It also does not outlive the session: closing or expiring it clears
            the bag."
        inactivity_ttl_seconds:
          type: integer
          default: 0
          description: Number of seconds of inactivity after which the session expires. 0 means the session
            never expires.
          example: 300
        message_delay_seconds:
          type: integer
          nullable: true
          default: null
          description: >
            Number of seconds to wait after the last user message before sending to the LLM. Acts as
            a debounce: each new message resets the timer. null or absent means no delay (immediate
            processing).
          example: 3
    UpdateSessionRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          nullable: true
          description: Session name (set to null to clear)
        status:
          type: string
          enum:
            - open
            - closed
            - expired
          description: Session status
        auto_generate:
          type: boolean
          description: Enable or disable automatic generation after user messages.
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: "Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and
            `mcp` tool call in this session. The header name is the deployment's configured context
            prefix (`X-Naturali-Context-` by default) plus the key verbatim — no character is
            re-cased. Keys are stored exactly as sent and never case-converted. A key that is not a
            valid HTTP header name, or two keys that map to the same header, are rejected with `400
            INVALID_TOOL_CONTEXT_KEY`. `session_id`, `actor_id` and `actor_external_id` are
            server-derived and dropped from the stored bag, in any casing. Write-only: no read of a
            session returns it. It also does not outlive the session: closing or expiring it clears
            the bag."
        inactivity_ttl_seconds:
          type: integer
          description: >
            Number of seconds of inactivity after which the session expires. 0 means the session
            never expires. Updates the stored TTL; the inactivity clock continues from the last
            activity timestamp.
          example: 300
        message_delay_seconds:
          type: integer
          nullable: true
          description: >
            Number of seconds to wait after the last user message before sending to the LLM. Acts as
            a debounce: each new message resets the timer. Set to null to disable the delay.
          example: 3
    AddSessionMessageRequest:
      oneOf:
        - type: object
          additionalProperties: false
          required:
            - message
          properties:
            message:
              type: string
              description: User message text
              example: Hello, how can I deploy my app?
            tool_context:
              type: object
              additionalProperties:
                type: string
              nullable: true
              description: Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and
                `mcp` tool call in this generation. The header name is the deployment's configured
                context prefix (`X-Naturali-Context-` by default) plus the key verbatim — no
                character is re-cased. Keys are never case-converted — they round-trip exactly as
                sent. An invalid or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
            idempotency_key:
              type: string
              description: >
                Optional deduplication key scoped to this session. If a message with the same key
                already exists in the session, the original message is returned with HTTP 200 and no
                new message or generation is triggered.
              example: wamid.HBgLNTUxMTk4...
        - type: object
          additionalProperties: false
          required:
            - document_id
          properties:
            document_id:
              x-naturali-ref: documents
              type: string
              description: Public ID of a document used as the user message content.
            tool_context:
              type: object
              additionalProperties:
                type: string
              nullable: true
              description: Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and
                `mcp` tool call in this generation. The header name is the deployment's configured
                context prefix (`X-Naturali-Context-` by default) plus the key verbatim — no
                character is re-cased. Keys are never case-converted — they round-trip exactly as
                sent. An invalid or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
            idempotency_key:
              type: string
              description: >
                Optional deduplication key scoped to this session. If a message with the same key
                already exists in the session, the original message is returned with HTTP 200 and no
                new message or generation is triggered.
              example: wamid.HBgLNTUxMTk4...
    AddSessionMessageSaved:
      type: object
      description: Message saved; auto-generate is off or a generation is already in progress.
      properties:
        role:
          type: string
          enum:
            - user
        content:
          type: string
        document_id:
          x-naturali-ref: documents
          type: string
          nullable: true
    AddSessionMessageResponse:
      anyOf:
        - $ref: "#/components/schemas/AddSessionMessageSaved"
        - $ref: "#/components/schemas/GenerateSessionResponse"
    GenerateSessionRequest:
      type: object
      additionalProperties: false
      properties:
        model:
          type: string
          description: Optional model override
          example: gpt-4o
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and
            `mcp` tool call in this generation. The header name is the deployment's configured
            context prefix (`X-Naturali-Context-` by default) plus the key verbatim — no character
            is re-cased. Keys are never case-converted — they round-trip exactly as sent. An invalid
            or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
    GenerateSessionResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - completed
            - requires_action
        message:
          type: object
          properties:
            role:
              type: string
            content:
              type: string
            model:
              type: string
        generation_id:
          x-naturali-ref: generations
          type: string
        trace_id:
          x-naturali-ref: traces
          type: string
        required_action:
          type: object
          description: Present when status is requires_action
          properties:
            tool_calls:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  tool_name:
                    type: string
                  args:
                    type: object
    SendSessionMessageResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - completed
            - requires_action
        message:
          type: object
          properties:
            role:
              type: string
            content:
              type: string
            model:
              type: string
        generation_id:
          x-naturali-ref: generations
          type: string
        trace_id:
          x-naturali-ref: traces
          type: string
        required_action:
          type: object
          description: Present when status is requires_action
          properties:
            tool_calls:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  tool_name:
                    type: string
                  args:
                    type: object
    SubmitSessionToolOutputsRequest:
      type: object
      required:
        - generation_id
        - tool_outputs
      additionalProperties: false
      properties:
        generation_id:
          x-naturali-ref: generations
          type: string
          description: The generation ID from the requires_action response
        tool_outputs:
          type: array
          items:
            type: object
            required:
              - tool_call_id
              - output
            additionalProperties: false
            properties:
              tool_call_id:
                type: string
              output:
                description: The tool output value
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          description: Structured error. Every error response uses this shape, so `code` can be read without
            first checking the type of `error`.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: "Machine-readable error code: lower_snake for an error naturali raises
                (`access_denied`), UPPER_SNAKE for one the runtime reports (`RESOURCE_NOT_FOUND`)."
              example: access_denied
            message:
              type: string
              description: Human-readable explanation.
              example: Your role in this project does not carry this action.
            details:
              type: object
              additionalProperties: true
              description: Structured context for an error naturali raises, such as the `resource` and `limit` of
                a `plan_limit_reached`.
            meta:
              type: object
              description: Structured context for an error the runtime reports.
    TagBag:
      type: object
      additionalProperties:
        type: string
      x-cli-flag-name: tags
      description: >-
        Key-value labels on a resource. A flat object of string values — an array, a nested object
        or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and
        stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB
        containment wherever tags are read: the `?tags=` filter and knowledge search.


        Keys beginning `system.` are reserved: the platform writes them to record which
        conversation, actor, agent and role a row came from, and a write naming one is refused with
        `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.


        The bag is bounded, because every pair reaches the IAM context of every access check on the
        resource: at most 50 keys, each key at most 128 characters and each value at most 256. A
        write past a bound — including a merge that would grow the stored bag past the key count —
        is `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed. `system.*` keys
        are the platform's and do not count against the 50.
      example:
        team: finance
        env: prod
    UsageTotals:
      type: object
      description: >
        Token counts and cost for one slice of the meter. Tokens and cost only: a `compute_second`
        or `gb_day` meter is reported by the aggregate's `components` array.


        The three input dimensions partition the prompt — `input_tokens` = `uncached_input_tokens` +
        `cached_tokens` + `cache_write_tokens` — and are separate because they are separately
        priced. Note that the `input_tokens` **component** on a usage event is the uncached figure
        alone; here `input_tokens` is the whole prompt.
      properties:
        cost_usd:
          type: number
          nullable: true
          description: Sum of priced component costs; null when nothing in the slice was priced. Always null
            at step altitude — the ledger prices one event per generation at write time, and a price
            read back per step would disagree with it after any price change.
        input_tokens:
          type: integer
          description: Full prompt tokens, reconstructed from the components.
        uncached_input_tokens:
          type: integer
          description: Prompt tokens priced at the plain input rate — the prompt minus cache reads and cache
            writes.
        output_tokens:
          type: integer
        cached_tokens:
          type: integer
          description: Prompt tokens served from the provider's prompt cache.
        cache_write_tokens:
          type: integer
          description: Prompt tokens written into the provider's prompt cache.
        reasoning_tokens:
          type: integer
          description: A non-billable subset of `output_tokens`, reported where the provider breaks it out.
  parameters:
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
    SessionId:
      name: session_id
      in: path
      required: true
      description: Session public ID
      schema:
        type: string
        example: sess_V1StGXR8Z5jdHi6B
    TagsQuery:
      name: tags
      in: query
      required: false
      description: >
        Filter by tag pairs, written `key:value` (split on the first colon, so a value may contain
        colons). Repeat the parameter for several pairs; **all** must be present with exactly that
        value.
      schema:
        type: array
        items:
          type: string
      style: form
      explode: true
      example:
        - env:prod
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
    oauth2:
      type: oauth2
      description: "A connected app's OAuth access token, issued by this API's authorization server
        (discovery: /.well-known/oauth-authorization-server). Its one scope carries every operation,
        confined to the projects the user chose when approving the app."
      flows:
        authorizationCode:
          authorizationUrl: https://api.naturali.ai/authorize
          tokenUrl: https://api.naturali.ai/token
          refreshUrl: https://api.naturali.ai/token
          scopes:
            mcp:access: Every operation this API serves, on the projects the grant covers.
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: The credential is scoped to a different project, or the caller's role in the project
        does not carry this action.
      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"
