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 ('running', '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. The generations of one
        trace are the `trace_id` 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: session_id
          in: query
          required: false
          description: >
            Return only the generations dispatched through one session — the
            turns behind that conversation's spend. A session that does not
            exist in scope yields an empty page.
          schema:
            type: string
        - name: actor_id
          in: query
          required: false
          description: >
            Return only the generations one end user started, across every
            session they appear in. An actor that does not exist in scope
            yields an empty page.
          schema:
            type: string
        - name: initiator_generation_id
          in: query
          required: false
          description: >
            Filter by the public ID of the parent generation. Returns every
            generation started by that generation: sub-agent invocations,
            approval continuations, client-tool re-handoffs and memory-rule
            handler turns. 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. Repeat the parameter to OR values. A
            value outside the enum, empty string included, is a `400`.
          schema:
            type: array
            items:
              type: string
              enum: [running, requires_action, completed, failed]
          style: form
          explode: true
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      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: './errors.yaml#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/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
      x-soat-resource:
        kind: generation
        from: generation_id
      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: './errors.yaml#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/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. the ticket or case an AI action belongs
        to). 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, what knowledge
        retrieval served) lives in its own top-level fields and cannot be
        written from here.
      operationId: updateGeneration
      x-soat-resource:
        kind: generation
        from: generation_id
      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: './errors.yaml#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/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
      x-soat-resource:
        kind: generation
        from: generation_id
      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: './errors.yaml#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'

  /api/v1/generations/{generation_id}/memory-assertions:
    get:
      tags:
        - Generations
      summary: List the memory writes a generation made
      description: >
        Every memory write this turn made, oldest first: each memory rule's
        firings and the agent's own `write_memory` calls alike. The rows behind
        the generation's per-rule `extraction` counts. Reading them is reading
        the generation (`generations:GetGeneration`).
      operationId: listGenerationMemoryAssertions
      x-soat-resource:
        kind: generation
        from: generation_id
      parameters:
        - name: generation_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the generation
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: The generation's memory writes
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: './memory-assertions.yaml#/components/schemas/MemoryAssertion'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/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
      x-soat-resource:
        kind: generation
        from: generation_id
      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: './errors.yaml#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/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 started this one: a sub-agent
            invocation, an approval continuation, a client-tool re-handoff or
            a memory-rule handler turn. 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.
        conversation_id:
          x-soat-ref: conversations
          type: string
          nullable: true
          description: >
            Public ID of the conversation this turn served, or null for a
            generation started outside one — a direct call, a trigger, an
            orchestration node. This is the edge a memory assertion walks up
            to reach the conversation a fact was learned in.
        session_id:
          x-soat-ref: sessions
          type: string
          nullable: true
          description: >
            Public ID of the session this generation was dispatched through, or
            null for a generation started outside one. This is the link a
            per-session cost reading follows; the session's own `usage` field
            reports the roll-up directly.
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: >
            Public ID of the end-user actor the generation was attributed to, or
            null when none was. Copied onto the generation's usage event, so it
            is the same dimension `group_by=actor` buckets on.
        started_by_principal_type:
          nullable: true
          allOf:
            - $ref: './principals.yaml#/components/schemas/PrincipalType'
          description: Who started the generation
        started_by_principal_id:
          type: string
          nullable: true
          description: >-
            Public id of that principal: the user (`user_…`), or for API key
            auth the key itself (`key_…`), so the record names which key acted.
          example: user_V1StGXR8Z5jdHi6B
        status:
          type: string
          description: Lifecycle status of the generation
          enum: [running, 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:
          allOf:
            - $ref: './errors.yaml#/components/schemas/StoredError'
          nullable: true
          description: >-
            Why the generation failed: `AI_PROVIDER_ERROR` for an upstream
            provider failure, `GENERATION_FAILED` for any other failure of the
            turn.
        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: >
            What each [memory rule](/docs/modules/memories#memory-rules) bound
            to `generations.completed` wrote for this turn, keyed by the
            rule's id — a store may have several rules, and one flat pair of
            counts could not say which produced them. Absent when no rule fired.
            Rules bound to `conversation_messages.generated` are recorded only
            as memory assertions. The rows behind every count are listed by
            `GET /api/v1/generations/{generation_id}/memory-assertions`, so the
            summary and what it summarizes can be reconciled.
          additionalProperties:
            type: object
            properties:
              candidates:
                type: integer
                description: Number of candidates the rule's handler proposed
              created:
                type: integer
                description: Number of new memories created
              superseded:
                type: integer
                description: >
                  Number of candidates that restated a known fact that had
                  changed, retiring the memory holding it
              skipped:
                type: integer
                description: Number of candidates skipped (e.g. duplicates)
        tool_surface:
          type: object
          nullable: true
          description: >-
            What this turn's tool definitions cost to send — the one part of a
            prompt no provider reports and no caller can derive, since the tool
            block is a constant inside the reported totals and identical on
            every step.


            Null when the surface was never measured: a generation from before
            the field, or a path that resolves none. That is **not** what
            `tools: 0` means, which is a measured agent with nothing bound.


            Not a meter. `estimated_tokens` is an estimate and is never
            priced — `usage` and the usage events are the billing record.
          properties:
            tools:
              type: integer
              description: >-
                Tools resolved for the turn, before any per-step narrowing by
                `step_rules`.
              example: 61
            bytes:
              type: integer
              description: >-
                Serialized length of those definitions — name, description and
                input schema — in canonical JSON, not the provider's wire
                format. Exact, and comparable across providers, which is what
                makes two agents' surfaces worth comparing.
              example: 152161
            estimated_tokens:
              type: integer
              description: >-
                `bytes` over a measured bytes-per-token ratio. An estimate, and
                named one.
              example: 45148
        retrieval:
          type: array
          nullable: true
          description: >-
            What `knowledge_config` retrieval injected into this turn, in the
            order it was injected: which document, at which version, and which
            chunk, or which memory. Written by the server at turn start and
            never writable; pointers only, never the text.


            Null when no retrieval ran — the agent has no `knowledge_config`,
            or the turn had neither a query nor a filter to search with. An
            empty array is a retrieval that ran and matched nothing.


            Not content: a content purge leaves it standing, and zero retention
            still writes it. `document_version` names an archived version, so
            the text the turn read stays readable after the document changes.
          items:
            oneOf:
              - $ref: '#/components/schemas/GenerationRetrievedDocument'
              - $ref: '#/components/schemas/GenerationRetrievedMemory'
            discriminator:
              propertyName: source_type
              mapping:
                document: '#/components/schemas/GenerationRetrievedDocument'
                memory: '#/components/schemas/GenerationRetrievedMemory'
        idempotency_key:
          type: string
          nullable: true
          description: >-
            The deduplication key the generation was started under, unique
            within the project and claimed for as long as the generation record
            exists. Null for a generation started without one.
          example: discord-1287654321098765432
        usage:
          allOf:
            - $ref: './usage-totals.yaml#/components/schemas/UsageTotals'
          nullable: true
          description: >-
            What the turn cost: token counts and `cost_usd` for every event
            metered against this generation. Present on the single-generation
            read; omitted from the listing, where it would be a query per row.


            Null when nothing has been metered yet — a turn still in flight, or
            one whose metering failed. A turn is metered once at the end, so
            this is one event's figures in the ordinary case.
        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:
          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
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'
        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:
          nullable: true
          allOf:
            - $ref: './principals.yaml#/components/schemas/PrincipalType'
          description: >-
            Who purged the content: `user` or `api_key` for a purge request,
            `system` for the retention sweep and for content never stored under
            zero retention.
        content_redacted_by_principal_id:
          type: string
          nullable: true
          description: >-
            Public id of that principal — the API key's own id for key auth, so
            the record names which key acted. `retention_sweep` or
            `zero_retention` for `system`.
          example: user_V1StGXR8Z5jdHi6B
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    GenerationRetrievedDocument:
      type: object
      required:
        - source_type
        - document_id
        - document_version
        - chunk_id
        - page
        - similarity_score
      properties:
        source_type:
          type: string
          enum:
            - document
          example: document
        document_id:
          x-soat-ref: documents
          type: string
          description: Public ID of the document the chunk belongs to
          example: 'doc_V1StGXR8Z5jdHi6B'
        document_version:
          type: integer
          nullable: true
          description: >-
            The document version the chunk was read from. Read it back with the
            document's versions to see the exact text the turn was given.
          example: 3
        chunk_id:
          type: string
          description: >-
            Public ID of the injected chunk. Stops resolving once the document
            is re-chunked; `document_version` is the durable pointer.
          example: 'dchunk_V1StGXR8Z5jdHi6B'
        page:
          type: integer
          nullable: true
          description: Page within the source PDF (1-indexed). Null for plain text.
          example: 2
        similarity_score:
          type: number
          nullable: true
          description: >-
            Raw cosine similarity (0–1) between the turn's query and the
            result. Null when the turn had no query, or the search answered
            from the lexical channel alone. The rank is the array order.
          example: 0.82
    GenerationRetrievedMemory:
      type: object
      required:
        - source_type
        - memory_store_id
        - memory_id
        - similarity_score
      properties:
        source_type:
          type: string
          enum:
            - memory
          example: memory
        memory_store_id:
          x-soat-ref: memory-stores
          type: string
          description: Public ID of the memory store the memory belongs to
          example: 'mstore_V1StGXR8Z5jdHi6B'
        memory_id:
          type: string
          description: Public ID of the injected memory
          example: 'mem_V1StGXR8Z5jdHi6B'
        similarity_score:
          type: number
          nullable: true
          description: >-
            Raw cosine similarity (0–1) between the turn's query and the
            result. Null when the turn had no query, or the search answered
            from the lexical channel alone. The rank is the array order.
          example: 0.82
    UpdateGenerationRequest:
      type: object
      required:
        - metadata
      additionalProperties: false
      properties:
        metadata:
          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
          allOf:
            - $ref: './metadata.yaml#/components/schemas/MetadataBag'

    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.
          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.
          example:
            tempC: 18
        error:
          allOf:
            - $ref: './errors.yaml#/components/schemas/StoredError'
          nullable: true
          description: >-
            The tool's failure, when the step recorded one. A failure the tool
            raised with a code keeps it; any other is `TOOL_CALL_FAILED`, with
            the fields the thrown error carried (`name`, `status`, …) in
            `meta`.
          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: './usage-totals.yaml#/components/schemas/UsageTotals'
          nullable: true
          description: >-
            What this step reported, in the shape every usage surface uses.
            Null when the step recorded no usage at all; within a step a
            dimension the provider did not report reads 0. `cost_usd` is always
            null here — the ledger prices one event per generation, so a
            per-step price would disagree with it after any price change.

    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: [running, 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:
          allOf:
            - $ref: './errors.yaml#/components/schemas/StoredError'
          nullable: true
          description: Why 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:
          nullable: true
          allOf:
            - $ref: './principals.yaml#/components/schemas/PrincipalType'
          description: Who erased the content.
        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.
