openapi: 3.0.3
info:
  title: Generations API
  version: 1.0.0
  description: >
    Generation records track individual LLM generation runs started by agents.
    Each record carries the lifecycle status ('in_progress', 'requires_action',
    'completed', or 'failed'), the stop reason, and — when the generation
    failed — a structured error payload for post-mortem debugging. Generations
    for a trace can be listed via GET /api/v1/generations?trace_id=.
  contact:
    name: SOAT API Support

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: Generations
    description: Inspect generation records

security:
  - bearerAuth: []

paths:
  /api/v1/generations:
    get:
      tags:
        - Generations
      summary: List generations
      description: >
        Returns generations the caller can access, optionally filtered by agent,
        trace, orchestration run, node, and status. Replaces the former
        per-trace generations endpoint (use the trace_id query filter).


        Filtering by `orchestration_run_id` is the supported way to get from an
        orchestration run to the generations its agent nodes produced: a node
        execution record carries no generation id, so the pointer lives here,
        alongside the run's other attribution columns.
      operationId: listGenerations
      parameters:
        - name: agent_id
          in: query
          required: false
          description: Filter by agent public ID
          schema:
            type: string
        - name: trace_id
          in: query
          required: false
          description: Filter by trace public ID
          schema:
            type: string
        - name: initiator_generation_id
          in: query
          required: false
          description: >
            Filter by the public ID of the parent generation. Returns all
            generations triggered by that generation — sub-agent invocations.
            Null-initiated (top-level) generations are not returned.
          schema:
            type: string
        - name: chain_id
          in: query
          required: false
          description: >
            Filter by the continuation chain the generation belongs to. This is
            how a chain is expanded into its members — the chain record carries
            only their count.
          schema:
            type: string
        - name: orchestration_run_id
          in: query
          required: false
          description: >
            Filter by the orchestration run that dispatched the generation.
            This is how a run is traced back to what its agent nodes did — a
            node execution record stores no generation id.
          schema:
            type: string
        - name: node_id
          in: query
          required: false
          description: >
            Filter by the orchestration node that dispatched the generation.
            Combine with `orchestration_run_id` to narrow to one node of one
            run; a retried node returns one generation per `node_attempt`.
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filter by lifecycle status
          schema:
            type: string
            enum: [in_progress, requires_action, completed, failed]
        - 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 generations
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Generation'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/generations/{generation_id}:
    get:
      tags:
        - Generations
      summary: Get a generation
      description: >
        Returns a single generation record by ID, including its status and the
        structured `error` payload when the generation failed (e.g. because the
        upstream AI provider returned an error).
      operationId: getGeneration
      parameters:
        - name: generation_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the generation
      responses:
        '200':
          description: Generation details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Generation'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Generations
      summary: Update generation metadata
      description: >
        Attaches caller-supplied key/value metadata to a generation record for
        per-run audit attribution (e.g. recording which knowledge-corpus version
        produced an AI action). The provided keys are shallow-merged over the
        existing `metadata`, so repeated patches accumulate. The bag is
        caller-owned and no key is reserved: server-owned state (usage
        attribution, the served agent version, the route's record, the
        extraction summary) lives in its own top-level fields and cannot be
        written from here.
      operationId: updateGeneration
      parameters:
        - name: generation_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the generation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateGenerationRequest'
            examples:
              audit:
                summary: Attach caller audit metadata
                value:
                  metadata:
                    team: payments
                    ticket_id: OPS-4821
      responses:
        '200':
          description: Updated generation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Generation'
        '400':
          description: Bad Request (e.g. metadata is not a JSON object)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/generations/{generation_id}/content:
    delete:
      tags:
        - Generations
      summary: Purge generation content
      description: >
        Clears the generation's content — `metadata`, `error`, `extraction`, and
        the internal recovery state of a paused run — and stamps
        `content_redacted_at`.


        The usage and audit skeleton is preserved: ids, timestamps, status, stop
        reason, and the attribution fields (`action_id`, `trigger_id`,
        `orchestration_run_id`, `node_id`, `node_attempt`, `agent_version`,
        `routing`) the billing
        ledger reads. A purged generation reads back as that skeleton, not a 404.


        This does **not** delete the parent trace's steps object, which holds
        this generation's content alongside its siblings'. To erase the run's
        content completely, purge the trace
        (`DELETE /api/v1/traces/{trace_id}/content`), which cascades here.


        Idempotent — purging an already-purged generation succeeds and leaves the
        original `content_redacted_at` in place.
      operationId: purgeGenerationContent
      parameters:
        - name: generation_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the generation
      responses:
        '200':
          description: The purged generation skeleton
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Generation'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/generations/{generation_id}/transcript:
    get:
      tags:
        - Generations
      summary: Get a generation's transcript
      description: >
        Returns one generation's turn read back as an ordered sequence of
        steps: what it was asked, each model step with its tool calls and
        results, and how it ended.


        The transcript is assembled at read time from the generation record and
        the trace's steps object; nothing is stored, so it cannot outlive the
        content it projects. Requires `traces:GetTrace` in addition to
        `generations:GetGeneration`, because the response merges content from
        both resources.


        A generation whose content is unavailable — never written under
        zero-retention, or cleared by a purge — returns `200` with the skeleton
        rather than an error: `input` and `output` are null, `steps` is empty,
        and the `content_redacted_*` fields say which happened.
        `content_redacted_by_principal_id` is `zero_retention` when the content
        was never stored, and the purging principal's ID when it was erased
        later. A generation that is still running returns the same shape with
        an empty `steps`; `status` disambiguates the two.
      operationId: getGenerationTranscript
      parameters:
        - name: generation_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the generation
      responses:
        '200':
          description: The generation's transcript
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationTranscript'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Generation:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the generation
          example: gen_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
        agent_id:
          x-soat-ref: agents
          type: string
          description: Public ID of the agent that ran this generation
        trace_id:
          x-soat-ref: traces
          type: string
          description: Public ID of the trace this generation belongs to
        initiator_generation_id:
          x-soat-ref: generations
          type: string
          nullable: true
          description: >
            Public ID of the generation that triggered this one. Set for
            sub-agent invocations. Null for top-level generations.
        chain_id:
          x-soat-ref: chains
          type: string
          nullable: true
          description: >
            Public ID of the continuation chain this generation belongs to.
            Set on every member of a chain — the continuations and the root they
            descend from — and null on a generation that is not part of one.
        started_by_principal_type:
          type: string
          nullable: true
          description: Type of the principal that started the generation
        started_by_principal_id:
          type: string
          nullable: true
          description: ID of the principal that started the generation
        status:
          type: string
          description: Lifecycle status of the generation
          enum: [in_progress, requires_action, completed, failed]
          example: failed
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: When the generation reached a terminal state
        last_activity_at:
          type: string
          format: date-time
          nullable: true
        stop_reason:
          type: string
          nullable: true
          description: >
            Why the generation stopped. Either the model provider's own finish
            reason relayed unchanged ('stop', 'tool-calls', 'length', …) or one
            the platform names itself: 'max_steps' when the turn spent its whole
            step budget on tool calls, 'depth_guard' when a nested call exceeded
            the call depth, 'chain_limit' when a continuation chain reached its
            generation budget, or 'error' when the turn failed.
          example: error
        error:
          type: object
          nullable: true
          description: >
            Structured error payload recorded when the generation failed.
            Contains at least `message`; `code` is set for mapped errors
            (e.g. AI_PROVIDER_ERROR for upstream provider failures).
          properties:
            code:
              type: string
              example: AI_PROVIDER_ERROR
            message:
              type: string
              example: 'Provider returned 402: insufficient credits'
            meta:
              type: object
        action_id:
          type: string
          nullable: true
          description: >
            Logical action label supplied on the generate request. Recorded on
            the generation's usage event for per-action spend rollups.
        trigger_id:
          x-soat-ref: triggers
          type: string
          nullable: true
          description: Trigger that initiated the generation, when applicable
        orchestration_run_id:
          x-soat-ref: orchestration-runs
          type: string
          nullable: true
          description: >
            Orchestration run that dispatched the generation. Null for a
            standalone generation.
        node_id:
          type: string
          nullable: true
          description: >
            Node within `orchestration_run_id` that dispatched the generation.
            Together with the run it forms the usage event's replay identity.
        node_attempt:
          type: integer
          nullable: true
          description: >
            The node's 1-based retry attempt, completing the
            run + node + attempt replay identity. A retried node produces one
            generation per attempt; this is what tells them apart. Null for a
            generation no orchestration node dispatched.
        agent_version:
          type: integer
          nullable: true
          description: >
            Agent config version that served this generation, resolved by the
            served-version resolver (see
            [agent versions](/docs/modules/agents#versions-and-releases)).
        extraction:
          type: object
          nullable: true
          description: >
            Memory-extraction summary recorded for this generation. Set when the
            agent's `knowledge_config.extraction` produced one for this turn.
          properties:
            candidates:
              type: integer
              description: Number of extraction candidates considered
            created:
              type: integer
              description: Number of new memory entries created
            updated:
              type: integer
              description: Number of existing memory entries updated
            skipped:
              type: integer
              description: Number of candidates skipped (e.g. duplicates)
        routing:
          type: object
          nullable: true
          description: >
            What the model route did for this generation. Present only when the
            agent resolves its model through a `model_route_id`.
          properties:
            route_id:
              x-soat-ref: model-routes
              type: string
              description: The route that resolved the model
            target_index:
              type: integer
              nullable: true
              description: >
                Position in the route's `targets` of the target that served the
                last LLM call; null when every attempt failed.
            fallbacks:
              type: integer
              description: >
                How many times the route moved past a target during this
                generation (cumulative across a multi-step run).
            attempts:
              type: array
              description: >
                Every attempt, in order, across every LLM call of the run. An
                attempt with no `error_class` succeeded.
              items:
                type: object
                properties:
                  target_index:
                    type: integer
                  ai_provider_id:
                    x-soat-ref: ai-providers
                    type: string
                  model:
                    type: string
                  error_class:
                    type: string
                    enum: [provider_error, timeout, rate_limited]
                    description: >
                      Why the attempt failed. Absent on the serving attempt, and
                      absent on a deterministic failure (which fails the
                      generation instead of failing over).
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            Caller-owned key/value annotations, attached at create time (via the
            `metadata` field on the create-agent-generation request) or
            afterwards (via the update-generation request), and returned
            verbatim. The server writes nothing here: every piece of state it
            owns — usage attribution, the served agent version, the route's
            record, the extraction summary, internal recovery state — is a field
            of its own, so no key written here can reach platform state. Keys are
            never transformed, and no key is reserved.
          example:
            team: payments
            ticket_id: OPS-4821
        content_redacted_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When the generation's content was purged. Non-null means `metadata`,
            `error`, `extraction` and the internal recovery state have been
            cleared, while the usage/audit skeleton — ids, timestamps, status,
            stop reason and the attribution fields — is preserved.
        content_redacted_by_principal_type:
          type: string
          nullable: true
          description: Principal kind that purged the content ('user' or 'api_key')
          example: user
        content_redacted_by_principal_id:
          type: string
          nullable: true
          description: >
            Public ID of the principal that purged the content — the API key's
            own id for key auth, so the record names which key acted.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    UpdateGenerationRequest:
      type: object
      required:
        - metadata
      properties:
        metadata:
          type: object
          additionalProperties: true
          description: >
            Caller-supplied key/value metadata to shallow-merge into the
            generation record's caller-owned `metadata` bag. No key is reserved:
            server-owned state lives in its own top-level fields and cannot be
            written from here.
          example:
            team: payments
            ticket_id: OPS-4821

    TranscriptUsage:
      type: object
      description: >
        Token counts for one step. A field is null when the provider did not
        report it — null rather than 0, so "not reported" stays distinguishable
        from "none used".
      properties:
        input_tokens:
          type: integer
          nullable: true
        output_tokens:
          type: integer
          nullable: true
        total_tokens:
          type: integer
          nullable: true

    TranscriptToolCall:
      type: object
      description: One tool call the model made during a step.
      properties:
        id:
          type: string
          nullable: true
          description: >
            The call's ID, as the model provider issued it (e.g. `call_…`),
            used to correlate it with an entry in `tool_results`. Null when the
            stored step did not record one.
        tool_name:
          type: string
          nullable: true
          example: weather
        args:
          description: >
            The arguments the model supplied, as a value. This payload is
            tool-owned: its keys are passed through exactly as they were
            recorded and are never inspected or rewritten by SOAT.
          nullable: true
          example:
            cityName: Paris

    TranscriptToolResult:
      type: object
      description: >
        One tool's answer to a call in the same step. A call that failed is
        reported here too, with `result` null and `error` set — so a reader sees
        successes and failures in one ordered list keyed by the call they answer.
      properties:
        tool_call_id:
          type: string
          nullable: true
          description: The `id` of the `tool_calls` entry this answers.
        tool_name:
          type: string
          nullable: true
          example: weather
        result:
          description: >
            What the tool returned, as a value. Tool-owned: keys are passed
            through verbatim. Null when the call errored.
          nullable: true
          example:
            tempC: 18
        error:
          description: The tool's failure, when the step recorded one.
          nullable: true
          example: null

    TranscriptStep:
      type: object
      description: >
        One model step. Projected from the stored step at read time — the stored
        shape is provider- and SDK-specific and is never put on the wire.
      properties:
        index:
          type: integer
          description: >
            Zero-based position of this step in the turn. Positional rather than
            the model's own step number, which restarts at zero when a paused
            turn resumes.
          example: 0
        text:
          type: string
          description: >
            The text this step produced. Empty for a step that only called
            tools.
          example: ''
        finish_reason:
          type: string
          nullable: true
          description: Why this step stopped.
          example: tool-calls
        tool_calls:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptToolCall'
        tool_results:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptToolResult'
        usage:
          allOf:
            - $ref: '#/components/schemas/TranscriptUsage'
          nullable: true
          description: Null when the step recorded no usage.

    GenerationTranscript:
      type: object
      description: >
        One generation's turn, read back step by step. Assembled at read time
        from the generation record and the trace's steps object — never stored,
        so it dies with the content it projects.
      properties:
        generation_id:
          x-soat-ref: generations
          type: string
          example: gen_V1StGXR8Z5jdHi6B
        trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          example: trace_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        agent_id:
          x-soat-ref: agents
          type: string
        agent_version:
          type: integer
          nullable: true
          description: Agent config version that served the turn.
        status:
          type: string
          description: >
            Lifecycle status of the generation. Disambiguates an empty `steps`
            caused by a run still in flight from one caused by erased content.
          enum: [in_progress, requires_action, completed, failed]
          example: completed
        stop_reason:
          type: string
          nullable: true
          description: >
            Why the generation stopped — the provider's finish reason, or one of
            the platform's own ('max_steps', 'depth_guard', 'chain_limit',
            'error').
          example: stop
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        step_count:
          type: integer
          description: >
            Number of steps this turn recorded. A counter rather than content,
            so it survives a purge and still reports the size of a turn whose
            steps are gone. Scoped to the generation, not the trace: a trace
            that groups several generations counts them all in its own
            `step_count`, while each transcript reports only its own.
          example: 2
        input:
          type: array
          nullable: true
          description: >
            The messages the turn was asked, as recorded. Message content is
            caller-owned and passed through verbatim. Null when the content was
            never stored or has been purged.
          items:
            type: object
        steps:
          type: array
          description: >
            The turn's steps in order. Empty for a run still in progress, and
            for one whose content is unavailable — `status` and
            `content_redacted_at` say which.
          items:
            $ref: '#/components/schemas/TranscriptStep'
        output:
          type: object
          nullable: true
          description: >
            The turn's final answer. Null when there are no steps to derive it
            from.
          properties:
            content:
              type: string
              nullable: true
              description: >
                The last step that produced text. Null for a turn that only
                called tools.
              example: It's 18°C in Paris right now.
            finish_reason:
              type: string
              nullable: true
              description: The finish reason of the actual last step.
              example: stop
        error:
          type: object
          nullable: true
          description: Structured error payload when the generation failed.
        content_redacted_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When the generation's content was erased; null while it is intact.
        content_redacted_by_principal_type:
          type: string
          nullable: true
          description: Principal kind that erased the content.
          example: system
        content_redacted_by_principal_id:
          type: string
          nullable: true
          description: >
            Public ID of that principal. `zero_retention` when the content was
            never stored, distinguishing it from content erased later.

    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.
