openapi: 3.0.3
info:
  title: SOAT Agent Sessions API
  version: 1.0.0
  description: >
    Agent Sessions provide a simplified 1-user ↔ 1-agent conversational
    abstraction. Each session belongs to an agent (set via agent_id) and manages
    the underlying conversation and generation plumbing automatically. The end
    user behind the session is not created for you — pass actor_id to attach an
    existing actor.
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://your-soat.com or http://localhost:5047)
    variables:
      baseUrl:
        description: The base URL of your SOAT deployment
        default: http://localhost:5047
tags:
  - name: Sessions
    description: Manage agent sessions
security:
  - bearerAuth: []
paths:
  /api/v1/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.
                  hint: >-
                    The agent allows one open session per actor. Reuse the
                    session named in `meta.session_id`, or close it first.
                  docs_url: >-
                    https://soat.ttoss.dev/docs/error-codes#single_session_conflict
                  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]
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            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'

  /api/v1/sessions/{session_id}:
    get:
      tags:
        - Sessions
      summary: Get a session
      description: Returns details of a single session.
      operationId: getSession
      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
      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
      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'

  /api/v1/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
      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'

  /api/v1/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
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - name: wait
          in: query
          required: false
          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.
          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'
        '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 /api/v1/generations/{generation_id}.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/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
      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'

  /api/v1/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
      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.
                  hint: >-
                    Fix the request and retry. Unknown fields are rejected
                    outright, so compare the payload against the operation in
                    `/openapi.json`; `meta` names the offending field when the
                    check can identify one.
                  docs_url: 'https://soat.ttoss.dev/docs/error-codes#validation_failed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/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
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            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'

  /api/v1/sessions/{session_id}/tags:
    get:
      tags:
        - Sessions
      summary: Get session tags
      description: Returns the session's tags object.
      operationId: getSessionTags
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Session tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '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
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Updated tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '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
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Updated tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key

  parameters:
    SessionId:
      name: session_id
      in: path
      required: true
      description: Session public ID
      schema:
        type: string
        example: sess_V1StGXR8Z5jdHi6B

  schemas:
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          description: >-
            Structured error. Every error response uses this shape — 401, 403
            and the 500 catch-all included — so `code` can be read without
            first checking the type of `error`.
          required:
            - code
            - message
            - hint
            - docs_url
          properties:
            code:
              type: string
              description: A key from the server's ERROR_CODES registry.
              example: RESOURCE_NOT_FOUND
            message:
              type: string
              example: 'Resource not found'
            hint:
              type: string
              description: >-
                What to do about this error. Resolved per code, so a caller that
                has never seen the code before can act on the response without
                leaving it.
              example: >-
                Check the id, and check that the credential can see the project
                that owns the resource.
            docs_url:
              type: string
              format: uri
              description: The reference-page anchor documenting this code.
              example: >-
                https://soat.ttoss.dev/docs/error-codes#resource_not_found
            meta:
              type: object
              description: Optional structured context for the error.
    SessionRecord:
      type: object
      properties:
        id:
          type: string
          description: Session public ID
          example: sess_V1StGXR8Z5jdHi6B
        agent_id:
          x-soat-ref: agents
          type: string
          description: Agent public ID
          example: agent_V1StGXR8Z5jdHi6B
        conversation_id:
          x-soat-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-soat-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:
          type: object
          additionalProperties:
            type: string
        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
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every
            `http`, `mcp` and `builtin` tool call in this session. The header name is
            the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased.
            Keys are never case-converted — they round-trip exactly as sent.
            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`.
        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-soat-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

    ForkSessionRequest:
      type: object
      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-soat-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:
          type: object
          additionalProperties:
            type: string
          description: Optional tags for the forked session
        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
      properties:
        agent_id:
          x-soat-ref: agents
          type: string
          description: Agent this session belongs to
          example: agent_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional session name
          example: Support chat
        actor_id:
          x-soat-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-Soat-Context-<key>` headers on every
            `http`, `mcp` and `builtin` tool call in this session. The header name is
            the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased.
            Keys are never case-converted — they round-trip exactly as sent.
            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`.
        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
      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-Soat-Context-<key>` headers on every
            `http`, `mcp` and `builtin` tool call in this session. The header name is
            the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased.
            Keys are never case-converted — they round-trip exactly as sent.
            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`.
        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-Soat-Context-<key>` headers on every
                `http`, `mcp` and `builtin` tool call in this generation. The header name is
                the deployment's configured context prefix (`X-Soat-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-soat-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-Soat-Context-<key>` headers on every
                `http`, `mcp` and `builtin` tool call in this generation. The header name is
                the deployment's configured context prefix (`X-Soat-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-soat-ref: documents
          type: string
          nullable: true
    AddSessionMessageResponse:
      # anyOf, not oneOf: without a discriminator the two shapes are not
      # mutually exclusive (a saved-message body can also satisfy the looser
      # generation schema), and `oneOf` rejects a body that matches both.
      anyOf:
        - $ref: '#/components/schemas/AddSessionMessageSaved'
        - $ref: '#/components/schemas/GenerateSessionResponse'

    GenerateSessionRequest:
      type: object
      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-Soat-Context-<key>` headers on every
            `http`, `mcp` and `builtin` tool call in this generation. The header name is
            the deployment's configured context prefix (`X-Soat-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-soat-ref: generations
          type: string
        trace_id:
          x-soat-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-soat-ref: generations
          type: string
        trace_id:
          x-soat-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
      properties:
        generation_id:
          x-soat-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
            properties:
              tool_call_id:
                type: string
              output:
                description: The tool output value

  responses:
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
