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, actors, and generation plumbing automatically.
  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. Internally creates a
        conversation and two actors (agent + user) so the caller only needs
        this single call to start interacting with the agent.
      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]
        - 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. Returns the assistant reply or a requires_action status
        if the agent needs client tool outputs. Pass ?async=true for a
        202 accepted response when you do not need to wait for the result.
      operationId: generateSessionResponse
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - name: async
          in: query
          required: false
          description: When true, generation runs in the background and 202 is returned immediately
          schema:
            type: boolean
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateSessionRequest'
      responses:
        '200':
          description: Agent reply or requires_action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateSessionResponse'
        '202':
          description: Generation accepted (async mode)
          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}/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
      description: >
        Structured error envelope returned for all non-2xx responses.
        For DomainErrors (business-logic failures) the `error` property is an
        object with `code` and `message`; for other failures it is a string.
      properties:
        error:
          oneOf:
            - type: string
              description: Generic error message (e.g. "Internal Server Error")
            - type: object
              description: Structured domain error
              required:
                - code
                - message
              properties:
                code:
                  type: string
                  description: Machine-readable error code
                  example: RESOURCE_NOT_FOUND
                message:
                  type: string
                  description: Human-readable error description
                  example: Session not found
                meta:
                  type: object
                  additionalProperties: true
                  description: Optional structured context
      required:
        - 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
          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 injected as context headers into all tool call requests made during this session.
        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).

    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
          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 injected as context headers into all tool call requests made during this session.
        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 injected as context headers into all tool call requests made during this session.
        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 injected as context headers into all tool call requests made during this generation.
            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 injected as context headers into all tool call requests made during this generation.
            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:
      oneOf:
        - $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 injected as context headers into all tool call requests made during this generation.

    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'
