openapi: 3.0.3
info:
  title: SOAT REST API
  version: 0.37.1
  description: |-
    Complete REST surface of a SOAT deployment, merged from the per-module OpenAPI
    specs that also generate the TypeScript SDK, the `soat` CLI and the MCP tool
    surface. SOAT is self-hosted: point the `baseUrl` server variable at your own
    deployment — this documentation site serves the description only, not the API.

    Authentication is a bearer token: a user JWT from `POST /api/v1/users/login`,
    or a project key (`sk_…`). Request and response bodies are snake_case.

    Every error response — including the ones a client is most tempted to
    special-case (401, 403, 429, 500) — is JSON of the same shape:
    `{ "error": { "code": "RESOURCE_NOT_FOUND", "message": "…", "hint": "…",
    "docs_url": "…", "meta": { … } } }`.
    `error.code` is stable and safe to branch on, and `error.hint` says what to do
    about the failure; the full catalog of codes, their
    HTTP statuses and what they mean is in this document's `x-error-codes`
    extension and at https://soat.ttoss.dev/errors.json.
  license:
    name: Apache-2.0
    url: https://github.com/ttoss/soat/blob/main/LICENSE
  contact:
    name: SOAT on GitHub
    url: https://github.com/ttoss/soat
servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://soat.example.com or http://localhost:5047).
    variables:
      baseUrl:
        default: http://localhost:5047
        description: The base URL of your SOAT deployment.
security:
  - bearerAuth: []
tags:
  - name: Activity
    description: Read the autonomous-execution activity feed
  - name: Actors
    description: Manage actors associated with projects
  - name: Agents
    description: Manage AI agents
  - name: Agent Versions
    description: Agent config history and staged rollout
  - name: Agent Traces
    description: View agent traces
  - name: AI Providers
    description: Manage AI providers
  - name: API Keys
    description: Manage API keys
  - name: Approvals
    description: Manage the human-decision approval queue
  - name: Audit Log
    description: Query the append-only audit log
  - name: Chains
    description: Inspect continuation chains and how large they have grown
  - name: Chats
    description: Manage chats
  - name: Conversations
    description: Manage conversations
  - name: Documents
    description: Manage documents
  - name: Embeddings
    description: Generate text embeddings
  - name: Evaluations
    description: Datasets, evals, and eval runs
  - name: Exceptions
    description: Triage the failure/anomaly exception queue
  - name: Files
    description: Manage files
  - name: Formations
    description: Manage declarative formation stacks
  - name: Generations
    description: Inspect generation records
  - name: Guardrails
    description: Manage guardrails
  - name: Ingestion Rules
    description: Route content types to converter tools or agents during ingestion
  - name: Knowledge
    description: Unified search across documents and knowledge sources
  - name: Memories
    description: Manage memory configurations for document retrieval
  - name: Memory Entries
    description: Manage individual memory entries (the actual knowledge items stored in a memory)
  - name: Model Routes
    description: Manage ordered provider+model failover routes
  - name: OAuth
    description: OAuth 2.1 discovery, dynamic client registration, and the authorization code flow
  - name: Orchestrations
    description: Manage orchestrations and their runs
  - name: Policies
    description: Manage policies
  - name: Projects
    description: Manage projects
  - name: Quotas
    description: Manage quotas and rate limits
  - name: Secrets
    description: Manage secrets
  - name: Sessions
    description: Manage agent sessions
  - name: Tasks
    description: Manage tasks and their transitions
  - name: Tools
    description: Manage tools
  - name: Traces
    description: Inspect execution traces and trace trees
  - name: Triggers
    description: Manage triggers and inspect firings
  - name: Usage
    description: Inspect LLM token-usage meter rows
  - name: Users
    description: Manage users
  - name: User Policies
    description: Manage user policies
  - name: Webhooks
    description: Manage webhooks
  - name: Workflows
    description: Manage workflow definitions
paths:
  /api/v1/activity:
    get:
      tags:
        - Activity
      summary: List activity feed entries
      description: >-
        Returns activity entries for a project, newest first, filterable by kind and severity. Paginated with an opaque
        cursor rather than offset/limit — pass the previous page's `next_cursor` to fetch the next one; a `null`
        `next_cursor` means there is no more data.
      operationId: listActivity
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: kind
          in: query
          description: Filter by activity kind
          schema:
            type: string
            enum:
              - action_executed
              - approval_resolved
              - exception_created
              - schedule_fired
        - name: severity
          in: query
          description: Filter by severity
          schema:
            type: string
            enum:
              - info
              - warning
              - critical
        - name: cursor
          in: query
          description: Opaque cursor from a previous page's `next_cursor`
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Page of activity entries
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ActivityEntry'
                  next_cursor:
                    type: string
                    nullable: true
                    description: Pass as `cursor` to fetch the next page; `null` when this is the last page
        '400':
          description: Malformed cursor
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/actors:
    get:
      tags:
        - Actors
      summary: List actors
      description: >-
        Returns all actors the caller has access to. If projectId is provided, returns only actors in that project.
        project keys are scoped to a single project automatically. JWT users without projectId receive actors across all
        their accessible projects.
      operationId: listActors
      parameters:
        - name: project_id
          in: query
          required: false
          description: Project ID (optional)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: external_id
          in: query
          required: false
          description: External ID to filter by (e.g. WhatsApp phone number)
          schema:
            type: string
            example: '+15551234567'
        - name: agent_id
          in: query
          required: false
          description: Return only actors linked to this agent
          schema:
            type: string
            example: agent_V1StGXR8Z5jdHi6B
        - name: chat_id
          in: query
          required: false
          description: Return only actors linked to this chat
          schema:
            type: string
            example: chat_V1StGXR8Z5jdHi6B
        - name: conversation_id
          in: query
          required: false
          description: |
            Return only actors that participate in this conversation (derived from the conversation's messages).
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of actors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ActorRecord'
                  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'
    post:
      tags:
        - Actors
      summary: Create an actor
      description: >-
        Creates a new actor. project keys automatically infer the project from the key's scope; JWT callers must supply
        projectId.
      operationId: createActor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID. Required for JWT auth; omit when using an project key.
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  example: Alice
                external_id:
                  type: string
                  description: >-
                    Optional external identifier (e.g. WhatsApp phone number). If provided and an actor with this
                    externalId already exists in the project, the existing actor is returned (idempotent — 200 OK).
                  example: '+15551234567'
                instructions:
                  type: string
                  nullable: true
                  description: >-
                    Persona-specific instructions composed into the effective system prompt during conversation
                    generation.
                agent_id:
                  x-soat-ref: agents
                  type: string
                  description: Agent to link this actor to. Mutually exclusive with chat_id.
                  example: agent_V1StGXR8Z5jdHi6B
                chat_id:
                  x-soat-ref: chats
                  type: string
                  description: Chat to link this actor to. Mutually exclusive with agent_id.
                  example: chat_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Actor already exists — returned when externalId matches an existing actor in this project (idempotent)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActorRecord'
        '201':
          description: Actor created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActorRecord'
        '400':
          description: Invalid request body
          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'
  /api/v1/actors/{actor_id}:
    get:
      tags:
        - Actors
      summary: Get an actor by ID
      description: Returns an actor by its ID
      operationId: getActor
      parameters:
        - name: actor_id
          in: path
          required: true
          description: Actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Actor found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActorRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Actors
      summary: Delete an actor
      description: Deletes an actor by its ID
      operationId: deleteActor
      parameters:
        - name: actor_id
          in: path
          required: true
          description: Actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Actor deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Actors
      summary: Update an actor
      description: Updates an actor's properties
      operationId: updateActor
      parameters:
        - name: actor_id
          in: path
          required: true
          description: Actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: Alice Smith
                external_id:
                  type: string
                  description: External identifier (e.g. WhatsApp phone number)
                  example: '+15551234567'
                instructions:
                  type: string
                  description: Persona-specific instructions
                agent_id:
                  x-soat-ref: agents
                  type: string
                  nullable: true
                  description: Agent to link this actor to. Mutually exclusive with chat_id.
                chat_id:
                  x-soat-ref: chats
                  type: string
                  nullable: true
                  description: Chat to link this actor to. Mutually exclusive with agent_id.
                tags:
                  type: object
                  additionalProperties:
                    type: string
      responses:
        '200':
          description: Actor updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActorRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/actors/{actor_id}/tags:
    get:
      tags:
        - Actors
      summary: Get actor tags
      description: Returns all tags attached to the actor
      operationId: getActorTags
      parameters:
        - name: actor_id
          in: path
          required: true
          description: Actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Actor tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Actors
      summary: Replace actor tags
      description: Replaces all tags on the actor with the provided tags (not merged)
      operationId: replaceActorTags
      parameters:
        - name: actor_id
          in: path
          required: true
          description: Actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
              example:
                type: customer
                status: active
      responses:
        '200':
          description: Tags replaced
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Actors
      summary: Merge actor tags
      description: Merges provided tags with existing tags (existing tags are preserved unless overridden)
      operationId: mergeActorTags
      parameters:
        - name: actor_id
          in: path
          required: true
          description: Actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
              example:
                priority: high
      responses:
        '200':
          description: Tags merged
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents:
    post:
      tags:
        - Agents
      summary: Create an agent
      description: Creates a new agent bound to an AI provider.
      operationId: createAgent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAgentRequest'
            examples:
              minimal:
                summary: Minimal agent
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
              full:
                summary: Agent with tools and instructions
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  name: Research Assistant
                  instructions: You are a helpful research assistant.
                  model: gpt-4o
                  tool_bindings:
                    - tool_id: tool_abc123
                  max_steps: 10
                  temperature: 0.7
      responses:
        '201':
          description: Agent created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '400':
          description: Bad Request
          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: AI provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags:
        - Agents
      summary: List agents
      description: Returns all agents in the project.
      operationId: listAgents
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
          description: Project public ID to filter by
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of agents
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Agent'
                  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/agents/{agent_id}:
    get:
      tags:
        - Agents
      summary: Get an agent
      description: Returns a single agent by ID.
      operationId: getAgent
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Agent details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Agents
      summary: Update an agent
      description: Updates an existing agent. Identical to PATCH — both perform partial updates.
      operationId: updateAgent
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAgentRequest'
      responses:
        '200':
          description: Agent updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Agents
      summary: Partially update an agent
      description: Partially updates an existing agent. Identical to PUT — both perform partial updates.
      operationId: patchAgent
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAgentRequest'
      responses:
        '200':
          description: Agent updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Agents
      summary: Delete an agent
      description: >
        Deletes an agent by ID. Fails with `409` if the agent has dependent generations or traces, unless `force=true`
        is passed, in which case those generations and traces are deleted along with the agent.
      operationId: deleteAgent
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
        - name: force
          in: query
          required: false
          description: >
            When `true`, deletes the agent's dependent generations and traces instead of returning `409
            AGENT_HAS_DEPENDENTS`.
          schema:
            type: boolean
            default: false
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >
            Agent has dependent generations or traces (pass `force=true` to delete anyway). `error.meta` carries
            `generation_count` and `trace_count` so a caller can tell which one is nonzero.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/generate:
    post:
      tags:
        - Agents
      summary: Run an agent generation
      description: >
        Sends messages to the agent, resolves its tools, and runs the AI model loop. Background by default: returns `202
        Accepted` with a `generation_id` to poll via `GET /api/v1/generations/{generation_id}`. Pass `?wait=true` to
        block and receive the result inline, where client tools pause the generation and return `requires_action`.
        Streaming (`stream: true`) implies waiting.
      operationId: createAgentGeneration
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
        - name: wait
          in: query
          required: false
          x-soat-tool-forced: 'true'
          description: >-
            When omitted or `false` (default), the generation runs in the background and `202 Accepted` is returned
            immediately with a `generation_id` to poll. Pass `true` to block until the generation settles and receive
            the result. Mutually exclusive with `stream: true`. A `builtin` tool call always waits.
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAgentGenerationRequest'
            examples:
              basic:
                summary: Simple generation
                value:
                  messages:
                    - role: user
                      content: What is the weather in Tokyo?
              toolOutput:
                summary: Use a tool output as user message content
                value:
                  messages:
                    - role: user
                      content:
                        type: tool_output
                        tool_id: tool_audio_to_text
                        input:
                          url: https://example.com/audio.mp3
                        output_path: text
              streaming:
                summary: Streaming generation
                value:
                  messages:
                    - role: user
                      content: Summarize the latest report.
                  stream: true
      responses:
        '200':
          description: 'Generation result or SSE stream (only when `?wait=true` or `stream: true`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentGenerationResponse'
            text/event-stream:
              schema:
                type: string
                description: >
                  SSE stream of delta chunks ending with `data: [DONE]`.


                  The response headers are written before the provider is called, so a failure cannot become a status
                  code once the stream is open. It arrives instead as a terminal `data: {"error": "..."}` frame carrying
                  the same mapped message the non-streaming path returns in its `502` body (e.g. `Provider returned 404:
                  ...`), and the stream then ends **without** a `[DONE]` — the absence of that sentinel is how a caller
                  tells a truncated answer from a complete one. Chunks produced before the failure are still delivered,
                  and the generation is recorded as `failed`.
        '202':
          description: >-
            Generation accepted and running in the background (default, when `wait` is omitted or `false`). Poll `GET
            /api/v1/generations/{generation_id}` for the result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedGenerationResponse'
        '400':
          description: Bad Request
          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: Agent or AI provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >
            Upstream AI provider error (AI_PROVIDER_ERROR); model output that does not satisfy the agent's
            `output_schema` (OUTPUT_SCHEMA_VALIDATION_FAILED — the violated field is named in the message); or a model
            that wrote a tool invocation out as plain assistant text instead of calling the tool, so the tool never ran
            (TEXT_ENCODED_TOOL_CALL — `meta.tool_name` names the tool). 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}. Streaming requests report the provider error in a terminal SSE frame
            instead, since their status line is already on the wire.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/generate/{generation_id}/tool-outputs:
    post:
      tags:
        - Agents
      summary: Submit tool outputs for a paused generation
      description: |
        Resumes a generation that was paused due to client tool calls. Provide tool outputs for each pending tool call.
      operationId: submitAgentToolOutputs
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
        - name: generation_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitToolOutputsRequest'
      responses:
        '200':
          description: Generation result after resuming
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentGenerationResponse'
        '400':
          description: Bad Request
          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: Agent or generation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >
            Upstream AI provider error (AI_PROVIDER_ERROR); model output that does not satisfy the agent's
            `output_schema` (OUTPUT_SCHEMA_VALIDATION_FAILED); or a model that wrote a tool invocation out as plain
            assistant text instead of calling the tool (TEXT_ENCODED_TOOL_CALL — `meta.tool_name` names the tool). The
            resumed generation is recorded `failed`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/versions:
    get:
      tags:
        - Agent Versions
      summary: List an agent's config versions
      description: >
        Returns the agent's archived configurations, newest first. A version is written on create and on every
        subsequent write that changes the config — through the REST API or a formation apply alike. See [Versioning and
        Staged Rollout](/docs/modules/agents#versioning-and-staged-rollout).
      operationId: listAgentVersions
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of agent versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AgentVersion'
                  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'
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/versions/{version}:
    get:
      tags:
        - Agent Versions
      summary: Get an archived agent config version
      description: >
        Returns the exact configuration the agent held at a given version, so a generation can be traced back to the
        config that produced it.
      operationId: getAgentVersion
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Archived agent version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentVersion'
        '400':
          description: Bad Request — version is not a positive integer
          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: Agent or version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/versions/{version}/restore:
    post:
      tags:
        - Agent Versions
      summary: Restore an archived config as a new version
      description: >
        Copies the named version's configuration onto the agent as a **new** version rather than rewinding the counter,
        so history stays append-only and the versions in between remain retrievable. Restoring the config the agent
        already holds is a no-op and creates no version.


        The restored config fully replaces the current one: a field the archived version did not set is cleared, not
        merged. Restore re-validates the config, so a tool, provider, or guardrail deleted since the snapshot was taken
        fails the request instead of writing a broken agent.
      operationId: restoreAgentVersion
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreAgentVersionRequest'
      responses:
        '200':
          description: The agent, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '400':
          description: Bad Request — invalid version, or the archived config no longer validates
          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: Agent or version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/release:
    put:
      tags:
        - Agent Versions
      summary: Set or replace a staged rollout
      description: >
        Starts serving two archived versions side by side: `canary_percent` of traffic gets `canary_version`, the rest
        gets `stable_version`.


        Assignment is deterministic — it hashes the actor behind the request's session (falling back to the session
        itself), so one end user never flip-flops between configs mid-conversation. Requests with neither are split
        randomly.


        While a release is active the agent's live columns act as a **draft**: further edits archive new versions but do
        not disturb either side of the running split. End the rollout with `promote` or `abort`.
      operationId: setAgentRelease
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetAgentReleaseRequest'
      responses:
        '200':
          description: The agent, with its active release set
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '400':
          description: Bad Request — malformed input, or a version that does not exist
          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: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/release/promote:
    post:
      tags:
        - Agent Versions
      summary: Promote the canary and end the rollout
      description: >
        Makes the canary version's config the agent's live config and clears the release. The canary is pinned by
        version, so an edit that landed mid-rollout is not promoted in its place — it stays an unreleased draft in the
        version history.


        When the release carries a `promotion_gate`, the eval it names must have a run that finished `completed` with
        `passed: true` **and** was pinned to the canary version (`agent_version`); otherwise the call is a `409` and the
        rollout is left running untouched. The run that cleared the gate is recorded as `eval_run_id` on the version
        that goes live.
      operationId: promoteAgentRelease
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The agent, now serving the promoted config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            Conflict — the agent has no active release (`NO_ACTIVE_RELEASE`), or its `promotion_gate` has no passing
            eval run against the canary version (`PROMOTION_GATE_UNMET`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/agents/{agent_id}/release/abort:
    post:
      tags:
        - Agent Versions
      summary: Abort the rollout and roll back to stable
      description: >
        Restores the stable version's config as the agent's live config and clears the release, so all traffic returns
        to the configuration the rollout was measured against — not to whatever draft the live columns happened to hold.
      operationId: abortAgentRelease
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The agent, back on the stable config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict — the agent has no active release
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/ai-providers:
    get:
      tags:
        - AI Providers
      summary: List AI providers
      description: Returns a list of AI provider configurations for a project
      operationId: listAiProviders
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          description: Number of results per page
          schema:
            type: integer
            default: 25
        - name: offset
          in: query
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of AI providers
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        provider:
                          type: string
                          enum:
                            - openai
                            - anthropic
                            - google
                            - xai
                            - groq
                            - ollama
                            - azure
                            - bedrock
                            - vertex
                            - gateway
                            - custom
                        default_model:
                          type: string
                        secret_id:
                          x-soat-ref: secrets
                          type: string
                          nullable: true
                          description: Secret ID containing API credentials, or null when the record links none.
                        base_url:
                          type: string
                          description: Custom base URL for the provider. Absent when the record sets none.
                        config:
                          type: object
                          description: Additional provider-specific configuration. Absent when the record sets none.
                        project_id:
                          x-soat-ref: projects
                          type: string
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - AI Providers
      summary: Create an AI provider
      description: Creates a new LLM provider configuration
      operationId: createAiProvider
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - provider
                - default_model
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Provider configuration name
                  example: OpenAI Production
                provider:
                  type: string
                  enum:
                    - openai
                    - anthropic
                    - google
                    - xai
                    - groq
                    - ollama
                    - azure
                    - bedrock
                    - vertex
                    - gateway
                    - custom
                  description: LLM provider
                  example: openai
                default_model:
                  type: string
                  description: Default model to use
                  example: gpt-4
                secret_id:
                  x-soat-ref: secrets
                  type: string
                  description: Secret ID containing API credentials
                  example: sec_V1StGXR8Z5jdHi6B
                base_url:
                  type: string
                  description: Custom base URL for the provider
                config:
                  type: object
                  description: Additional provider-specific configuration
      responses:
        '201':
          description: AI provider created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  provider:
                    type: string
                  default_model:
                    type: string
                  project_id:
                    x-soat-ref: projects
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '400':
          description: Bad request (invalid provider or missing fields)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/ai-providers/{ai_provider_id}:
    get:
      tags:
        - AI Providers
      summary: Get an AI provider
      description: Returns a specific AI provider configuration
      operationId: getAiProvider
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: AI provider details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  provider:
                    type: string
                  default_model:
                    type: string
                  project_id:
                    x-soat-ref: projects
                    type: string
                  secret_id:
                    x-soat-ref: secrets
                    type: string
                    nullable: true
                  base_url:
                    type: string
                  config:
                    type: object
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
    patch:
      tags:
        - AI Providers
      summary: Update an AI provider
      description: Updates an AI provider configuration
      operationId: updateAiProvider
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                provider:
                  type: string
                  enum:
                    - openai
                    - anthropic
                    - google
                    - xai
                    - groq
                    - ollama
                    - azure
                    - bedrock
                    - vertex
                    - gateway
                    - custom
                  description: LLM provider
                  example: openai
                default_model:
                  type: string
                secret_id:
                  x-soat-ref: secrets
                  type: string
                  nullable: true
                base_url:
                  type: string
                config:
                  type: object
      responses:
        '200':
          description: AI provider updated successfully
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
    delete:
      tags:
        - AI Providers
      summary: Delete an AI provider
      description: >
        Deletes an AI provider configuration.


        Live references — chats, agents, and model routes whose targets name this provider — always block deletion with
        `409 AI_PROVIDER_HAS_DEPENDENTS`; `force` does not override them, so delete or repoint those resources first.
        Soft dependents — price overrides and usage/generation records — also block with `409` unless `force=true`,
        which deletes the provider's price overrides and unlinks (nulls) its usage history, preserving those rows. The
        `409` body's `error.meta` reports the counts, a sample of offending IDs, and a `forcible` flag that is `true`
        when a `force=true` retry would succeed.
      operationId: deleteAiProvider
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
        - name: force
          in: query
          required: false
          description: >
            When `true`, delete the provider's price overrides and unlink its usage history so a provider with only soft
            dependents can be removed. Has no effect on live references (chats, agents, model routes), which always
            block deletion.
          schema:
            type: boolean
            default: false
      responses:
        '204':
          description: AI provider deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
        '409':
          description: >
            The AI provider still has dependents. Live references always block; soft dependents block unless force=true.
            See error.meta for counts, offending IDs, and the forcible flag.
  /api/v1/ai-providers/{ai_provider_id}/models:
    get:
      tags:
        - AI Providers
      summary: List the models this provider can run
      description: >
        Asks the provider which models it can run, using this provider record's own credentials and configuration, and
        returns provider-native model ids — the same strings `default_model` and an agent's `model` carry.

        Which models are reachable is a property of the credential, not of the provider type: a Vertex provider sees
        only the publisher models its Google Cloud project and location serve, and a Bedrock provider only the
        foundation models enabled in its region. Reading the list is how a caller avoids pinning a model that fails at
        generation time.

        Not every provider type can answer. `azure` lists deployments an operator named rather than models, and `ollama`
        lists whatever was pulled onto that host, so both return `400 MODEL_LISTING_UNSUPPORTED`.

        Listing resolves credentials the same way generation does, so a record that can generate can list. The API-key
        types (`openai`, `groq`, `xai`, `gateway`, `custom`, `anthropic`, `google`) use the record's linked secret and
        cannot list without one. `bedrock` and `vertex` use the linked secret when there is one — IAM keys or a Bedrock
        API key, a Google service-account key — and otherwise fall back to the server environment (the AWS default
        credential chain, Google Application Default Credentials), so a record with no `secret_id` can still list.

        A Vertex record needs no `config.project` when its secret is a service-account key, since the key file names its
        own project. A Vertex record in express mode (API key) cannot list at all: the publisher-model listing rejects
        API keys and needs a credential that asserts a principal, so it returns `400 MODEL_LISTING_UNSUPPORTED`.

        The Vertex answer is the publisher catalogue the record's `config.location` region serves. The project behind
        the credential is billed and quota'd for the call but does not filter the result, so a listed model may still be
        unavailable to that project at generation time.
      operationId: listAiProviderModels
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: The models this provider can run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderModelsResponse'
        '400':
          description: >
            The provider type or authentication mode cannot enumerate models (including Vertex express mode), or the
            record is missing configuration the listing needs (a Vertex project from either `config.project` or the
            service-account key file, a Bedrock region, or — for the API-key provider types — a linked secret).
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
        '502':
          description: The provider rejected the listing request
  /api/v1/ai-providers/{ai_provider_id}/prices:
    get:
      tags:
        - AI Providers
      summary: List per-provider price overrides
      description: >
        Returns the per-provider price overrides for this AI provider instance. An override prices this specific
        provider (e.g. an enterprise-negotiated rate or a gateway with markup) and wins over the global default at cost
        time. Authorized by the caller's access to the provider's project — so, unlike the global price book, a
        project's own overrides are visible here.
      operationId: getAiProviderPrices
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: The provider's price overrides
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderPricesResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
    put:
      tags:
        - AI Providers
      summary: Upsert per-provider price overrides
      description: >
        Upserts price overrides for this AI provider instance, keyed on (model, effective_from). The provider slug is
        taken from the AI provider itself, so only the model, rates, and effective_from are supplied. Authorized by the
        caller's access to the provider's project. `effective_from` must be in the future once the (model, component)
        has a price row — past prices are immutable, so corrections ship as new future-dated rows. A first price for a
        (model, component) nothing prices yet may be dated now or earlier, so a new provider is never live and unpriced.
      operationId: updateAiProviderPrices
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertProviderPricesRequest'
      responses:
        '200':
          description: The upserted price overrides
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderPricesResponse'
        '400':
          description: Bad Request (e.g. non-future effective_from)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
  /api/v1/api-keys:
    get:
      tags:
        - API Keys
      summary: List API keys
      description: >
        Lists API keys accessible to the caller. - JWT admin: returns all API keys. - JWT regular user: returns only the
        user's own API keys. - Project-scoped credential (API key or OAuth token): returns only API keys scoped to that
        project.
      operationId: listApiKeys
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of API keys
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKeyRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
    post:
      tags:
        - API Keys
      summary: Create an API key
      description: >
        Creates a new API key for the authenticated user. - `project_id` is optional. When set, the key is scoped to
        that single project. When omitted or null, the key is **unscoped** and spans every project its owner can reach.
        - If `policy_ids` is provided, the key's effective permissions are the intersection of the user's policies and
        the key's policies. - Otherwise the key inherits the user's permissions (confined to the key's project when
        scoped). - When the request is authenticated with a **project-scoped credential**, the new key is confined to
        that same project: omitting `project_id` defaults to it, naming a different project returns `403
        API_KEY_PROJECT_SCOPE`, and `project_id: null` (an unscoped key) is likewise refused. Minting an unscoped key
        requires an unscoped credential.
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Key name for identification
                  example: CI/CD Pipeline
                project_id:
                  type: string
                  nullable: true
                  description: >-
                    Optional project ID to scope the key to. Omit or set null to create an unscoped key that spans
                    projects.
                  example: proj_V1StGXR8Z5jdHi6B
                  x-soat-ref: projects
                policy_ids:
                  x-soat-ref: policies
                  type: array
                  items:
                    type: string
                  description: >-
                    Optional list of policy IDs to attach. Key permissions become the intersection of user policies and
                    these policies.
                  example:
                    - pol_V1StGXR8Z5jdHi6B
      responses:
        '201':
          description: API key created successfully. The raw key value is only returned once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        '400':
          description: Bad request (missing name, invalid project or policy IDs)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (a project-scoped credential named a different project, or asked for an unscoped key)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/api-keys/{api_key_id}:
    get:
      tags:
        - API Keys
      summary: Get an API key
      description: >-
        Returns details of an API key. Only the owner or an admin can access it, and a project-scoped credential can
        only reach keys in its own project.
      operationId: getApiKey
      parameters:
        - name: api_key_id
          in: path
          required: true
          description: API key public ID (key_ prefix)
          schema:
            type: string
            example: key_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: API key details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRecord'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (not the key owner or admin, or the credential is scoped to a different project)
        '404':
          description: API key not found
    put:
      tags:
        - API Keys
      summary: Update an API key
      description: >-
        Updates an API key's name, project scope, or policies. The project scope can be changed to another project, set
        (scoping a previously unscoped key), or cleared with null (unscoping the key). Only the owner or an admin can
        update it. A project-scoped credential can only update keys in its own project, and cannot move a key to another
        project or unscope it.
      operationId: updateApiKey
      parameters:
        - name: api_key_id
          in: path
          required: true
          description: API key public ID (key_ prefix)
          schema:
            type: string
            example: key_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: Updated Key Name
                project_id:
                  x-soat-ref: projects
                  type: string
                  nullable: true
                  description: >-
                    Re-scope the key to a different project, or set null to clear the scope (unscoped key). Omit to
                    leave the scope unchanged.
                  example: proj_V1StGXR8Z5jdHi6B
                policy_ids:
                  x-soat-ref: policies
                  type: array
                  items:
                    type: string
                  description: Replace the key's policy list (empty array removes all)
                  example:
                    - pol_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: API key updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRecord'
        '400':
          description: Bad request (invalid project or policy IDs)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (not the key owner or admin, or the credential is scoped to a different project)
        '404':
          description: API key not found
    delete:
      tags:
        - API Keys
      summary: Delete an API key
      description: >-
        Deletes an API key. Only the owner or an admin can delete it, and a project-scoped credential can only delete
        keys in its own project.
      operationId: deleteApiKey
      parameters:
        - name: api_key_id
          in: path
          required: true
          description: API key public ID (key_ prefix)
          schema:
            type: string
            example: key_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: API key deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (not the key owner or admin, or the credential is scoped to a different project)
        '404':
          description: API key not found
  /api/v1/approvals:
    get:
      tags:
        - Approvals
      summary: List approval items
      description: Returns approval items for a project, filterable by status, origin, and expiry.
      operationId: listApprovals
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          description: Filter by lifecycle status
          schema:
            type: string
            enum:
              - pending
              - approved
              - rejected
              - expired
        - name: origin
          in: query
          description: Filter by producer origin
          schema:
            type: string
            enum:
              - node
              - tool_call
              - task_transition
        - name: expires_before
          in: query
          description: Return only items expiring at or before this timestamp
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of approval items
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApprovalItem'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '400':
          description: Invalid `status` or `origin` filter value
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/approvals/recurrences:
    get:
      tags:
        - Approvals
      summary: List recurring approval groups
      description: >-
        Read-only rollup answering "what keeps coming back?" — groups items by `dedup_key` and returns those recurring
        at least `min_count` times, most-recurrent first. Each group carries the ordered item chain (via
        `previous_item_id`) and the resolution reasons in order, so a human can read recurring rejections side by side
        and graduate the pattern into a guardrail `deny`. Exact-key grouping only; no cluster state is stored.
      operationId: listApprovalRecurrences
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          description: Lifecycle status the groups are built from (default `rejected`)
          schema:
            type: string
            enum:
              - pending
              - approved
              - rejected
              - expired
            default: rejected
        - name: min_count
          in: query
          description: Minimum items in a group for it to be returned
          schema:
            type: integer
            default: 2
            minimum: 1
        - name: limit
          in: query
          required: false
          description: Maximum number of groups to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of groups to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of recurrence groups
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApprovalRecurrenceGroup'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '400':
          description: Invalid `status` filter value
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/approvals/{approval_id}:
    get:
      tags:
        - Approvals
      summary: Get an approval item
      description: Returns a single approval item with its full evidence.
      operationId: getApproval
      parameters:
        - $ref: '#/components/parameters/approval_id'
      responses:
        '200':
          description: Approval item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalItem'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Approval item not found
  /api/v1/approvals/{approval_id}/approve:
    post:
      tags:
        - Approvals
      summary: Approve an approval item
      description: >-
        Approves the item. Optionally supply edited `arguments` to replace the proposed arguments (edit-then-approve);
        the original is preserved on the item. Expiry is re-checked at decision time — an expired item can never be
        approved.
      operationId: approveApproval
      parameters:
        - $ref: '#/components/parameters/approval_id'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                arguments:
                  type: object
                  description: Edited arguments to execute instead of the proposed ones
      responses:
        '200':
          description: Approval item approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalItem'
        '400':
          description: Edited arguments are not a JSON object
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Approval item not found
        '409':
          description: Item already resolved or expired
  /api/v1/approvals/{approval_id}/reject:
    post:
      tags:
        - Approvals
      summary: Reject an approval item
      description: Rejects the item. A reason is required and preserved on the item.
      operationId: rejectApproval
      parameters:
        - $ref: '#/components/parameters/approval_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  description: Why the item is being rejected (required)
                  example: Amount exceeds the approved monthly budget.
      responses:
        '200':
          description: Approval item rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalItem'
        '400':
          description: Reason missing
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Approval item not found
        '409':
          description: Item already resolved or expired
  /api/v1/audit-log:
    get:
      tags:
        - Audit Log
      summary: List audit entries
      description: >-
        Returns audit-log entries visible to the caller, newest first. All filters are optional and combine with AND.
        `resource_srn` is a prefix match (e.g. `srn:{project}:secret:` matches every secret action); every other filter
        is exact.
      operationId: listAuditEntries
      parameters:
        - name: project_id
          in: query
          description: Project ID (scopes results; required if not using project key auth for a specific project)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: action
          in: query
          description: Exact permission-action string, e.g. `secrets:DeleteSecret`
          schema:
            type: string
            example: secrets:DeleteSecret
        - name: principal_id
          in: query
          description: Public id of the principal (`user_…` or `key_…`)
          schema:
            type: string
        - name: resource_public_id
          in: query
          description: Exact target resource public id, e.g. `sec_…`
          schema:
            type: string
        - name: resource_srn
          in: query
          description: >-
            SRN prefix match, e.g. `srn:{project}:secret:`. The log is append-only, so a stored SRN is never rewritten;
            the filter matches it as stored.
          schema:
            type: string
        - name: from
          in: query
          description: Only entries created at or after this timestamp (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          description: Only entries created at or before this timestamp (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          description: Number of results per page (1–200, default 25)
          schema:
            type: integer
            default: 25
        - name: offset
          in: query
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: A page of audit entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AuditEntry'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '400':
          description: '`from` or `to` is present but not a valid ISO 8601 date'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/audit-log/export:
    get:
      tags:
        - Audit Log
      summary: Export audit entries as NDJSON
      description: >-
        Streams a project's audit-log entries as newline-delimited JSON — one entry object per line, oldest first — for
        archival before the retention window expires, or for shipping into an external system. `project_id` is required:
        the export is per-project by design. Filters behave exactly as they do on the list endpoint.
      operationId: exportAuditEntries
      x-soat-mcp-exclude: true
      parameters:
        - name: project_id
          in: query
          required: true
          description: Project whose entries are exported
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: action
          in: query
          description: Exact permission-action string, e.g. `secrets:DeleteSecret`
          schema:
            type: string
        - name: principal_id
          in: query
          description: Public id of the principal (`user_…` or `key_…`)
          schema:
            type: string
        - name: resource_public_id
          in: query
          description: Exact target resource public id, e.g. `sec_…`
          schema:
            type: string
        - name: resource_srn
          in: query
          description: >-
            SRN prefix match, e.g. `srn:{project}:secret:`. The log is append-only, so a stored SRN is never rewritten;
            the filter matches it as stored.
          schema:
            type: string
        - name: from
          in: query
          description: Only entries created at or after this timestamp (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          description: Only entries created at or before this timestamp (ISO 8601)
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: >-
            A newline-delimited stream of audit entries. Each line is a JSON object with the same fields as
            `AuditEntry`.
          content:
            application/x-ndjson:
              schema:
                type: string
        '400':
          description: '`project_id` is required, or `from`/`to` is present but not a valid ISO 8601 date'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/audit-log/{entry_id}:
    get:
      tags:
        - Audit Log
      summary: Get an audit entry
      description: Returns a single audit-log entry, including its `detail` payload
      operationId: getAuditEntry
      parameters:
        - name: entry_id
          in: path
          required: true
          description: Audit entry ID
          schema:
            type: string
            example: audit_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Audit entry details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEntry'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Audit entry not found
  /api/v1/chains:
    get:
      tags:
        - Chains
      summary: List continuation chains
      description: >-
        Returns the continuation chains in a project, newest first. Filter by `status` to find the chains that may still
        be spending (`active`) or the ones a budget stopped (`budget_exhausted`).
      operationId: listChains
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          description: Filter by chain status
          schema:
            type: string
            enum:
              - active
              - concluded
              - expired
              - budget_exhausted
        - name: agent_id
          in: query
          description: Filter by the agent whose continuation opened the chain
          schema:
            type: string
            example: agent_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of continuation chains
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Chain'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/chains/{chain_id}:
    get:
      tags:
        - Chains
      summary: Get a continuation chain
      description: Returns a single continuation chain. To read the generations in it, list generations filtered by `chain_id`.
      operationId: getChain
      parameters:
        - $ref: '#/components/parameters/chain_id'
      responses:
        '200':
          description: Continuation chain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chain'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Chain not found
  /api/v1/chats:
    post:
      tags:
        - Chats
      summary: Create a chat
      description: Creates a new chat resource bound to an AI provider.
      operationId: createChat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChatRequest'
            examples:
              basic:
                summary: Minimal chat
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
              with_instructions:
                summary: Chat with instructions
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  name: Support Bot
                  instructions: You are a helpful support assistant.
                  model: gpt-4o
      responses:
        '201':
          description: Chat created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '400':
          description: Bad Request
          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: AI provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags:
        - Chats
      summary: List chats
      description: Returns all chats in the project.
      operationId: listChats
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
          description: Project public ID to filter by
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of chats
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Chat'
                  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/chats/{chat_id}:
    get:
      tags:
        - Chats
      summary: Get a chat
      description: Returns a single chat by ID.
      operationId: getChat
      parameters:
        - name: chat_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Chat record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Chat not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Chats
      summary: Delete a chat
      description: Deletes a chat by ID.
      operationId: deleteChat
      parameters:
        - name: chat_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Chat deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Chat not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/chat/completions:
    post:
      tags:
        - Chats
      summary: Create a chat completion
      description: >
        OpenAI Chat Completions-compatible endpoint. Mirrors OpenAI's `POST /v1/chat/completions` path so an OpenAI SDK
        can target it by base URL alone.


        Names exactly one target. With `ai_provider_id` the completion is stateless: the provider's secret is decrypted
        and the appropriate Vercel AI SDK provider is called, with no server-side model fallback. With `chat_id` the
        stored chat supplies the provider (or the project's `default_model_route_id`), model and instructions.


        System content travels only in `instructions` — a `role: "system"` entry in `messages` is refused with `400
        SYSTEM_MESSAGE_NOT_ALLOWED`. With `chat_id`, a request `instructions` replaces the chat's stored one for this
        call only; the stored value applies when the request carries none, and the two are never merged.


        Messages may use `document_id` instead of `content` with either target. Chats hold no message history — send the
        full `messages` array every time.
      operationId: createChatCompletion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
            examples:
              basic:
                summary: Simple user message
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  messages:
                    - role: user
                      content: Hello, how are you?
              with_chat:
                summary: Against a stored chat
                value:
                  chat_id: chat_V1StGXR8Z5jdHi6B
                  messages:
                    - role: user
                      content: What can you help me with?
              with_document:
                summary: Message referencing a document
                value:
                  chat_id: chat_V1StGXR8Z5jdHi6B
                  messages:
                    - role: user
                      document_id: doc_V1StGXR8Z5jdHi6B
              with_provider:
                summary: With explicit AI provider and streaming
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  model: gpt-4o
                  instructions: You are a helpful assistant.
                  messages:
                    - role: user
                      content: What files do I have?
                  stream: true
      responses:
        '200':
          description: Chat completion result (JSON or SSE stream)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatCompletionResponse'
            text/event-stream:
              schema:
                type: string
                description: >
                  SSE stream of JSON objects, one per line, prefixed with `data: `. The stream ends with `data: [DONE]`.

                  The response headers are written before the provider is called, so a failure cannot become a status
                  code once the stream is open. It arrives instead as a terminal `data: {"error": "..."}` frame carrying
                  the same mapped message the non-streaming path returns in its `502` body (e.g. `Provider returned 404:
                  ...`), and the stream then ends without a `[DONE]`.
        '400':
          description: >-
            Bad Request — `messages` is missing or empty; a `role: "system"` entry appears in `messages`; neither
            `ai_provider_id` nor `chat_id` was given, or both were; or `chat_id` names a chat that does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: >-
            Forbidden — the caller lacks `chats:CreateChatCompletion` on the project the call belongs to: the chat's
            project for `chat_id`, the AI provider's project for `ai_provider_id`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: AI provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >
            Upstream AI provider error (AI_PROVIDER_ERROR) — the provider rejected the completion (an unavailable model,
            a refused credential) or could not be reached. The message names the provider's own status and message, so
            an unavailable model is distinguishable from a fault in the runtime itself. Streaming requests report this
            in a terminal SSE frame instead, since their status line is already on the wire.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/conversations:
    get:
      tags:
        - Conversations
      summary: List conversations
      description: >-
        Returns all conversations the caller has access to. If projectId is provided, returns only conversations in that
        project. project keys are scoped to a single project automatically.
      operationId: listConversations
      parameters:
        - name: project_id
          in: query
          required: false
          description: Project ID (optional)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: actor_id
          in: query
          required: false
          description: Filter by actor ID
          schema:
            type: string
            example: actor_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of conversations
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationRecord'
                  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'
    post:
      tags:
        - Conversations
      summary: Create a conversation
      description: >-
        Creates a new conversation. project keys automatically infer the project from the key's scope; JWT callers must
        supply projectId.
      operationId: createConversation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID. Required for JWT auth; omit when using an project key.
                  example: proj_V1StGXR8Z5jdHi6B
                status:
                  type: string
                  enum:
                    - open
                    - closed
                  default: open
                  description: Initial conversation status
                name:
                  type: string
                  nullable: true
                  description: Optional name for the conversation
                actor_id:
                  x-soat-ref: actors
                  type: string
                  nullable: true
                  description: Actor ID to associate with this conversation
                  example: actor_V1StGXR8Z5jdHi6B
      responses:
        '201':
          description: Conversation created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationRecord'
        '400':
          description: Invalid request body
          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'
  /api/v1/conversations/{conversation_id}:
    get:
      tags:
        - Conversations
      summary: Get a conversation by ID
      description: Returns a conversation by its ID
      operationId: getConversation
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Conversation found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Conversations
      summary: Update a conversation
      description: Updates the status of a conversation
      operationId: updateConversation
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum:
                    - open
                    - closed
                  description: New conversation status
                name:
                  type: string
                  nullable: true
                  description: New conversation name
      responses:
        '200':
          description: Conversation updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationRecord'
        '400':
          description: Invalid request body
          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: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Conversations
      summary: Delete a conversation
      description: Deletes a conversation by its ID
      operationId: deleteConversation
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Conversation deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/conversations/{conversation_id}/messages:
    get:
      tags:
        - Conversations
      summary: List conversation messages
      description: Returns all messages (documents) attached to a conversation, ordered by position
      operationId: listConversationMessages
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationMessageRecord'
                  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'
        '404':
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - Conversations
      summary: Add a message to a conversation
      description: >-
        Creates a document from the message text and attaches it to the conversation at the given position. If position
        is omitted, it is appended at the end.
      operationId: addConversationMessage
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - message
                - role
              properties:
                message:
                  type: string
                  description: Message text content to add to the conversation
                  example: Hello, how can I help you?
                role:
                  type: string
                  enum:
                    - user
                    - assistant
                  description: Role of the message sender
                  example: user
                actor_id:
                  x-soat-ref: actors
                  type: string
                  nullable: true
                  description: Optional actor ID to associate with this message (user identity)
                  example: actor_V1StGXR8Z5jdHi6B
                position:
                  type: integer
                  description: Zero-based position. Defaults to MAX+1 (append).
                  example: 0
                metadata:
                  type: object
                  description: >-
                    Optional structured metadata to attach to the message (e.g. phone number, channel). Stored as-is and
                    injected into the AI prompt context.
                  nullable: true
                  additionalProperties: true
                  example:
                    phone: '5511999998888'
                    channel: whatsapp
      responses:
        '201':
          description: Message added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationMessageRecord'
        '400':
          description: Invalid request body
          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: Conversation or actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/conversations/{conversation_id}/generate:
    post:
      tags:
        - Conversations
      summary: Generate the next message in a conversation
      description: |
        Generates the next message using the specified actor's linked agent or chat.
        Background by default: returns `202 Accepted` immediately and the reply
        lands as a new ConversationMessage when it completes — poll
        `GET /api/v1/conversations/{conversation_id}/messages` for it.
        Pass `?wait=true` to block and receive the result inline. On
        `completed`, the reply is persisted as a new ConversationMessage
        authored by that actor. On `requires_action`, nothing is persisted; the
        caller must submit tool outputs via the Agents module and re-invoke
        generate — so a flow using client tools should pass `?wait=true`.
      operationId: generateConversationMessage
      parameters:
        - name: conversation_id
          in: path
          required: true
          schema:
            type: string
        - name: wait
          in: query
          required: false
          x-soat-tool-forced: 'true'
          description: >-
            When omitted or `false` (default), the generation runs in the background and `202 Accepted` is returned
            immediately. Pass `true` to block until the generation settles and receive the result. A `builtin` tool call
            always waits.
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agent_id
              properties:
                agent_id:
                  x-soat-ref: agents
                  type: string
                  description: ID of the agent that will produce the next message.
                model:
                  type: string
                  description: Optional model override.
                stream:
                  type: boolean
                  description: If true, stream tokens via SSE. NOT IMPLEMENTED in v1 — returns 501.
                tool_context:
                  type: object
                  additionalProperties:
                    type: string
                  nullable: true
                  description: >-
                    Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin`
                    tool call in this generation. The header name is the deployment's configured context prefix
                    (`X-Soat-Context-` by default) plus the key verbatim — no character is re-cased and keys are never
                    case-converted, so they round-trip exactly as sent. An invalid or colliding key is rejected with
                    `400 INVALID_TOOL_CONTEXT_KEY`.
      responses:
        '200':
          description: Generation completed or requires action (only when `?wait=true`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateConversationMessageResponse'
        '202':
          description: >-
            Generation accepted and running in the background (default, when `wait` is omitted or `false`). The reply is
            persisted as a ConversationMessage when it completes.
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - conversation_id
                properties:
                  status:
                    type: string
                    enum:
                      - accepted
                    example: accepted
                  conversation_id:
                    type: string
                    example: conv_V1StGXR8Z5jdHi6B
        '400':
          description: Invalid request
          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: Conversation or actor not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '501':
          description: Streaming not implemented
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/conversations/{conversation_id}/messages/{document_id}:
    delete:
      tags:
        - Conversations
      summary: Remove a message from a conversation
      description: Removes a document from a conversation
      operationId: removeConversationMessage
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Message removed
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversation or message not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/conversations/{conversation_id}/tags:
    get:
      tags:
        - Conversations
      summary: Get conversation tags
      description: Returns all tags attached to the conversation
      operationId: getConversationTags
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Conversation tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Conversations
      summary: Replace conversation tags
      description: Replaces all tags on the conversation with the provided tags
      operationId: replaceConversationTags
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Tags replaced
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Conversations
      summary: Merge conversation tags
      description: Merges provided tags with existing tags
      operationId: mergeConversationTags
      parameters:
        - name: conversation_id
          in: path
          required: true
          description: Conversation ID
          schema:
            type: string
            example: conv_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Tags merged
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/documents:
    get:
      tags:
        - Documents
      summary: List documents
      description: >-
        Returns all documents the caller has access to. If projectId is provided, returns only documents in that
        project. project keys are scoped to a single project automatically. JWT users without projectId receive
        documents across all their accessible projects.
      operationId: listDocuments
      parameters:
        - name: project_id
          in: query
          required: false
          description: Project ID (optional)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: path_prefix
          in: query
          required: false
          description: >-
            Only documents filed under this directory. The prefix is a path boundary, not a substring: `/reports`
            returns `/reports/q1.txt` and never `/reports-archive/q1.txt`, and `/` selects the whole project. A leading
            slash is optional and a trailing one is ignored, so `reports`, `/reports` and `/reports/` are the same
            filter. `%` and `_` are literal characters, not wildcards.
          schema:
            type: string
            example: /reports/
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of documents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DocumentRecord'
                  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'
    post:
      tags:
        - Documents
      summary: Create a document
      description: >-
        Creates a new text document and generates an embedding vector for semantic search. project keys automatically
        infer the project from the key's scope; JWT callers must supply projectId.
      operationId: createDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID. Required for JWT auth; omit when using an project key.
                  example: proj_V1StGXR8Z5jdHi6B
                content:
                  type: string
                  example: The quick brown fox jumps over the lazy dog.
                path:
                  type: string
                  description: Logical path within the project (e.g. /reports/q1.txt). Defaults to /filename if omitted.
                  example: /reports/q1.txt
                filename:
                  type: string
                  example: my-doc.txt
                title:
                  type: string
                  description: Document title
                metadata:
                  type: object
                  description: >-
                    Arbitrary metadata object. Unlike other body fields, keys are stored and returned verbatim in the
                    casing supplied — they are not converted between snake_case and camelCase.
                tags:
                  type: object
                  additionalProperties:
                    type: string
                  description: Key-value tags
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: >-
                    How to split the content into embeddable chunks. `whole` (default) stores the content as a single
                    chunk; `size` splits into fixed-size character windows with overlap. `page` is equivalent to `whole`
                    for plain text.
                  default: whole
                chunk_size:
                  type: integer
                  description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
                  example: 1000
                chunk_overlap:
                  type: integer
                  description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults to 200.
                  example: 200
      responses:
        '201':
          description: Document created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentRecord'
        '400':
          description: Invalid request body
          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'
  /api/v1/documents/ingest:
    post:
      tags:
        - Documents
      summary: Ingest a file into a chunked document
      description: |
        Parses an already-uploaded file and creates one Document split into one or
        more embedded chunks. The source format is detected from the file's content
        type: PDFs are parsed page-by-page; `text/plain` and `text/markdown` files
        are read as a single source. How the source is chunked is controlled by
        `chunk_strategy`.

        A file can only back one Document — a second call with the same `file_id`
        returns `409 FILE_ALREADY_INGESTED`. To re-process an already-ingested file
        (e.g. with a different `chunk_strategy`), use
        `POST /documents/{document_id}/ingest`; to ingest the same source under a
        different path, upload a new copy of the file first.
      operationId: ingestDocument
      x-iam-action: documents:IngestDocument
      parameters:
        - name: wait
          in: query
          required: false
          description: >-
            When omitted or `false` (default), processing runs in the background and `202 Accepted` is returned
            immediately with `status=pending`. Pass `true` to block until processing completes and receive `201 Created`
            with `status=ready`.
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - file_id
              properties:
                file_id:
                  x-soat-ref: files
                  type: string
                  description: ID of the uploaded file. Must be one of application/pdf, text/plain, text/markdown.
                  example: file_V1StGXR8Z5jdHi6B
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID. Required for JWT auth; omit when using a project key.
                  example: proj_V1StGXR8Z5jdHi6B
                path_prefix:
                  type: string
                  description: Path prefix under which to store the document (e.g. /docs/). The filename is appended automatically.
                  example: /docs/
                tags:
                  type: object
                  additionalProperties:
                    type: string
                  description: Key-value tags to attach to the document.
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: >-
                    How to split the source into chunks. `page` (default) creates one chunk per non-empty page (PDF);
                    for non-paged sources it yields a single chunk. `whole` joins everything into one chunk. `size`
                    splits into fixed-size character windows with overlap.
                  default: page
                chunk_size:
                  type: integer
                  description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
                  example: 1000
                chunk_overlap:
                  type: integer
                  description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults to 200.
                  example: 200
      responses:
        '201':
          description: >-
            Ingestion completed synchronously (only when `?wait=true`). The document is fully indexed and ready for
            search.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestedDocumentRecord'
        '202':
          description: >-
            Ingestion accepted. The document record has been created with `status=pending` and processing runs in the
            background. Poll `GET /api/v1/documents/{document_id}` until `status` is `ready` or `failed`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestedDocumentRecord'
        '400':
          description: Invalid request, file not found, or unsupported content type
          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'
        '409':
          description: >-
            The file already backs a Document (a file can only be ingested once). Use `POST
            /documents/{document_id}/ingest` to re-process the existing document, or upload a new copy of the file to
            ingest it separately.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: >-
            The file is too large to ingest synchronously (`?wait=true`). Retry in background mode and poll the document
            status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/documents/{document_id}:
    get:
      tags:
        - Documents
      summary: Get a document by ID
      description: Returns a document with its text content
      operationId: getDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Document found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Documents
      summary: Delete a document
      description: Deletes a document and its underlying file
      operationId: deleteDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Document deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Documents
      summary: Update a document
      description: >-
        Updates document content, title, path, metadata, or tags. Supplying `path` moves the document to a new logical
        path within the project.
      operationId: updateDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  description: New text content
                title:
                  type: string
                  description: New title
                path:
                  type: string
                  nullable: true
                  description: Logical path within the project (e.g. /reports/q1.txt). Pass null to clear.
                  example: /reports/q1.txt
                metadata:
                  type: object
                  description: >-
                    Arbitrary metadata object. Unlike other body fields, keys are stored and returned verbatim in the
                    casing supplied — they are not converted between snake_case and camelCase.
                tags:
                  type: object
                  additionalProperties:
                    type: string
                  description: Key-value tags
      responses:
        '200':
          description: Document updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/documents/{document_id}/status:
    get:
      tags:
        - Documents
      summary: Get document ingestion status
      description: |
        Returns a lightweight ingestion status payload for polling — `status`,
        `chunk_count`, `total_pages`, and (when failed) `error`. Unlike
        `GET /documents/{document_id}`, it never returns the assembled chunk
        content, so it is cheap to poll on large documents. A document whose
        ingestion has stalled (no progress past the configured timeout) is
        transitioned to `failed` with `error=INGESTION_TIMEOUT` on read.
      operationId: getDocumentStatus
      x-iam-action: documents:GetDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Document ingestion status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentStatusRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/documents/{document_id}/ingest:
    post:
      tags:
        - Documents
      summary: Re-ingest an existing document
      description: |
        Re-runs ingestion for an existing document against its already-stored
        source file. Existing chunks are discarded and the document is reset to
        `status=pending` before re-processing. Use this to recover a document
        stuck in `processing`/`failed` or to re-chunk with a different strategy
        without re-uploading the file. Background by default (`202`); pass
        `?wait=true` to run synchronously (`201`).
      operationId: reingestDocument
      x-iam-action: documents:IngestDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
        - name: wait
          in: query
          required: false
          description: >-
            When omitted or `false` (default), processing runs in the background and `202 Accepted` is returned
            immediately with `status=pending`. Pass `true` to block until processing completes and receive `201 Created`
            with `status=ready`.
          schema:
            type: boolean
            default: false
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: How to split the source into chunks. Defaults to `page`.
                  default: page
                chunk_size:
                  type: integer
                  description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
                  example: 1000
                chunk_overlap:
                  type: integer
                  description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults to 200.
                  example: 200
      responses:
        '201':
          description: Re-ingestion completed synchronously (only when `?wait=true`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestedDocumentRecord'
        '202':
          description: >-
            Re-ingestion accepted. The document was reset to `status=pending` and processing runs in the background.
            Poll `GET /api/v1/documents/{document_id}/status`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestedDocumentRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: The file is too large to re-ingest synchronously (`?wait=true`). Retry in background mode.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/documents/{document_id}/ingestion-callback:
    post:
      tags:
        - Documents
      summary: Deliver an async converter result
      description: |
        Token-authed callback for a tool converter that deferred conversion by
        returning `{ "status": "pending" }` (see the Ingestion Rules module
        docs). Not IAM-gated — the external converter is not a SOAT principal,
        so it authenticates with the single-use token minted for this
        document and ingestion attempt (delivered as `callback.token` /
        embedded in `callback.url` in the original converter invocation).
        Accepted only while the document is still awaiting that exact
        attempt; rejected with `409` if the attempt already completed, timed
        out, or was superseded by a re-ingest.
      operationId: completeIngestionCallback
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
        - name: token
          in: query
          required: true
          description: Single-use signed token from the original `callback.token`
          schema:
            type: string
      requestBody:
        required: true
        description: >-
          The converter output contract, adapted for a JSON request body: a single page as `{ text }`, or `{ pages: [{
          text, page_number }] }` for multiple pages.
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required:
                    - text
                  properties:
                    text:
                      type: string
                - type: object
                  required:
                    - pages
                  properties:
                    pages:
                      type: array
                      items:
                        type: object
                        properties:
                          text:
                            type: string
                          page_number:
                            type: integer
      responses:
        '204':
          description: >-
            Conversion completed — the document was chunked and marked `ready` (or `failed` with `FILE_PARSE_FAILED` if
            the output produced no text).
        '401':
          description: The token is missing, invalid, or does not match this document.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            The document is no longer awaiting this conversion attempt (already completed, timed out, or superseded by a
            re-ingest).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: The output shape is unrecognized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/documents/{document_id}/tags:
    get:
      tags:
        - Documents
      summary: Get document tags
      description: Returns all tags attached to the document
      operationId: getDocumentTags
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Document tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Documents
      summary: Replace document tags
      description: Replaces all tags on the document with the provided tags (not merged)
      operationId: replaceDocumentTags
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
              example:
                category: research
                status: draft
      responses:
        '200':
          description: Tags replaced
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Documents
      summary: Merge document tags
      description: Merges provided tags with existing tags (existing tags are preserved unless overridden)
      operationId: mergeDocumentTags
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
              example:
                priority: high
      responses:
        '200':
          description: Tags merged
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Document not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/embeddings:
    post:
      tags:
        - Embeddings
      summary: Create embeddings
      description: |
        Generates embedding vectors for one or more text inputs using the server's configured embedding model.
        Provide `input` for a single text or `inputs` for a batch. At least one is required.
        Returns `embedding` when `input` is used, and `embeddings` when `inputs` is used.
      operationId: createEmbeddings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                input:
                  type: string
                  description: Single text to embed.
                  example: The quick brown fox jumps over the lazy dog.
                inputs:
                  type: array
                  description: Batch of texts to embed.
                  items:
                    type: string
                  example:
                    - The quick brown fox.
                    - Pack my box with five dozen liquor jugs.
      responses:
        '200':
          description: Embeddings generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbeddingsResponse'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Embedding service not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/datasets:
    get:
      tags:
        - Evaluations
      summary: List datasets
      description: Returns the datasets defined in a project
      operationId: listDatasets
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of datasets
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Dataset'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Evaluations
      summary: Create a dataset
      description: >-
        Creates a project-scoped dataset — a named collection of test cases an eval runs an agent against. Names are
        unique per project.


        Datasets are operator-owned **fixtures**. The platform's content purge never deletes or mutates a dataset item,
        so erasing a generation cannot silently stop a test suite from being runnable.
      operationId: createDataset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Unique name within the project
                  example: billing-regressions
                description:
                  type: string
                  nullable: true
                  description: What this suite covers
                  example: Questions the billing agent regressed on in Q2
      responses:
        '201':
          description: Dataset created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Dataset'
        '400':
          description: Bad request (missing or invalid name)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A dataset with that name already exists in the project
        '500':
          description: Internal server error
  /api/v1/datasets/{dataset_id}:
    get:
      tags:
        - Evaluations
      summary: Get a dataset
      description: Returns a specific dataset
      operationId: getDataset
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Dataset details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Dataset'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset not found
    put:
      tags:
        - Evaluations
      summary: Update a dataset
      description: Updates a dataset's name and/or description
      operationId: updateDataset
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: billing-regressions
                description:
                  type: string
                  nullable: true
                  example: Questions the billing agent regressed on in Q2
      responses:
        '200':
          description: Dataset updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Dataset'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset not found
        '409':
          description: A dataset with that name already exists in the project
    delete:
      tags:
        - Evaluations
      summary: Delete a dataset
      description: >-
        Deletes a dataset, its items, and every eval bound to it. Results of runs that already scored those items keep
        their frozen copies of the input and expected output.
      operationId: deleteDataset
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Dataset deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset not found
  /api/v1/datasets/{dataset_id}/items:
    get:
      tags:
        - Evaluations
      summary: List dataset items
      description: Returns the test cases in a dataset, oldest first
      operationId: listDatasetItems
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of dataset items
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DatasetItem'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset not found
    post:
      tags:
        - Evaluations
      summary: Add a dataset item
      description: >-
        Adds one test case. `input` is replayed verbatim as the generation's messages, so it must be a non-empty array
        of `{ role, content }`.
      operationId: createDatasetItem
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - input
              properties:
                input:
                  $ref: '#/components/schemas/DatasetItemInput'
                expected_output:
                  type: string
                  nullable: true
                  description: Reference answer for exact_match / embedding_similarity / llm_judge scorers
                  example: Your invoice is issued on the first of each month.
                metadata:
                  type: object
                  nullable: true
                  additionalProperties: true
                  description: Free-form tags, opaque to the platform
                  example:
                    topic: billing
      responses:
        '201':
          description: Dataset item created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetItem'
        '400':
          description: Bad request (input is not message-shaped)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset not found
  /api/v1/datasets/{dataset_id}/items/from-generation:
    post:
      tags:
        - Evaluations
      summary: Curate a dataset item from a generation
      description: >-
        Promotes a real, completed generation into a test case: its input messages become the item's `input`, and its
        own answer becomes `expected_output` unless you supply one. Use it to build an evaluation set out of production
        traffic rather than hand-authoring fixtures.


        The item is a **copy**, not a view. It keeps working after the source generation's content is purged, and
        `source_generation_id` goes null if that generation is deleted — a purge can never quietly stop a suite from
        being runnable.


        Requires both `evaluations:CreateDataset` and `generations:GetGeneration`: the call copies content out of a
        generation, so a principal that may not read that generation may not curate it either.


        Only a **completed** generation can be promoted (`409 GENERATION_NOT_COMPLETED`), and only while its content is
        still available: an agent or project running with `trace_content_mode: none` never stored the input, and a
        purged or expired generation no longer has it (`409 GENERATION_CONTENT_UNAVAILABLE`). Generations that predate
        input recording answer the same way.
      operationId: createDatasetItemFromGeneration
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - generation_id
              properties:
                generation_id:
                  type: string
                  description: The completed generation to promote. Must belong to the same project as the dataset.
                  example: gen_V1StGXR8Z5jdHi6B
                expected_output:
                  type: string
                  nullable: true
                  description: >-
                    Reference answer. Omit to use the generation's own answer; pass `null` to store the item with no
                    reference answer.
                  example: Your invoice is issued on the first of each month.
                metadata:
                  type: object
                  nullable: true
                  additionalProperties: true
                  description: Free-form tags, opaque to the platform
                  example:
                    topic: billing
      responses:
        '201':
          description: Dataset item created from the generation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetItem'
        '400':
          description: Bad request (generation_id missing, or the generation belongs to a different project than the dataset)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset or generation not found
        '409':
          description: The generation has not completed, or its content was never stored or has been purged
  /api/v1/datasets/{dataset_id}/items/{item_id}:
    put:
      tags:
        - Evaluations
      summary: Update a dataset item
      description: >-
        Updates a test case. Runs that already scored it are unaffected — each result carries its own frozen copy of the
        input and expected output.
      operationId: updateDatasetItem
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
        - name: item_id
          in: path
          required: true
          description: Dataset item ID
          schema:
            type: string
            example: dsit_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                input:
                  $ref: '#/components/schemas/DatasetItemInput'
                expected_output:
                  type: string
                  nullable: true
                  example: Your invoice is issued on the first of each month.
                metadata:
                  type: object
                  nullable: true
                  additionalProperties: true
                  example:
                    topic: billing
      responses:
        '200':
          description: Dataset item updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetItem'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset or item not found
    delete:
      tags:
        - Evaluations
      summary: Delete a dataset item
      description: Deletes a test case. Results of runs that already scored it stay readable; their `dataset_item_id` becomes null.
      operationId: deleteDatasetItem
      parameters:
        - name: dataset_id
          in: path
          required: true
          description: Dataset ID
          schema:
            type: string
            example: dset_V1StGXR8Z5jdHi6B
        - name: item_id
          in: path
          required: true
          description: Dataset item ID
          schema:
            type: string
            example: dsit_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Dataset item deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Dataset or item not found
  /api/v1/evals:
    get:
      tags:
        - Evaluations
      summary: List evals
      description: Returns the evals defined in a project
      operationId: listEvals
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of evals
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Eval'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Evaluations
      summary: Create an eval
      description: >-
        Binds an agent under test to a dataset and a list of scorers. The agent and the dataset must belong to the same
        project as the eval; a cross-project reference is rejected with 400.


        Scorer config is frozen here rather than read from the agent at run time, so two runs of the same eval are
        always judged by the same criteria and their comparison measures the agent instead of the config drifting
        underneath it. Each scorer `type` may appear at most once.
      operationId: createEval
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - agent_id
                - dataset_id
                - scorers
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Unique name within the project
                  example: billing-regression-suite
                agent_id:
                  x-soat-ref: agents
                  type: string
                  description: The agent under test
                  example: agent_V1StGXR8Z5jdHi6B
                dataset_id:
                  type: string
                  description: The dataset to run it against
                  example: dset_V1StGXR8Z5jdHi6B
                scorers:
                  $ref: '#/components/schemas/Scorers'
                pass_threshold:
                  type: number
                  nullable: true
                  description: >-
                    0–1. The run passes iff its pass rate — passed items over non-errored items — is at least this. Null
                    reports scores without gating on them.
                  example: 0.8
      responses:
        '201':
          description: Eval created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Eval'
        '400':
          description: Bad request (unknown scorer type, cross-project reference, invalid threshold)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: An eval with that name already exists in the project
        '500':
          description: Internal server error
  /api/v1/evals/{eval_id}:
    get:
      tags:
        - Evaluations
      summary: Get an eval
      description: Returns a specific eval
      operationId: getEval
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Eval details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Eval'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval not found
    put:
      tags:
        - Evaluations
      summary: Update an eval
      description: >-
        Updates an eval. Changing `agent_id` re-validates the scorers against the new agent, since an `output_schema`
        scorer that was legal against the old one may not be.
      operationId: updateEval
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: billing-regression-suite
                agent_id:
                  x-soat-ref: agents
                  type: string
                  example: agent_V1StGXR8Z5jdHi6B
                dataset_id:
                  type: string
                  example: dset_V1StGXR8Z5jdHi6B
                scorers:
                  $ref: '#/components/schemas/Scorers'
                pass_threshold:
                  type: number
                  nullable: true
                  example: 0.8
      responses:
        '200':
          description: Eval updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Eval'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval not found
        '409':
          description: An eval with that name already exists in the project
    delete:
      tags:
        - Evaluations
      summary: Delete an eval
      description: Deletes an eval, its runs, and their results
      operationId: deleteEval
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Eval deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval not found
  /api/v1/evals/{eval_id}/runs:
    get:
      tags:
        - Evaluations
      summary: List eval runs
      description: Returns an eval's runs, newest first
      operationId: listEvalRuns
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of eval runs
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EvalRun'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval not found
    post:
      tags:
        - Evaluations
      summary: Start an eval run
      description: >-
        Runs the eval against its dataset, creating one real agent generation per item and scoring the outputs.


        `wait: true` executes the run synchronously and returns it terminal, with its scores. The dataset is capped at
        25 items for a synchronous run; a larger one is rejected with 400 rather than partially scored.


        `wait: false` (the default) enqueues one task per item and returns immediately with `status: "queued"`. A worker
        executes the items and the run settles itself; poll `GET /evals/{eval_id}/runs/{eval_run_id}` for the terminal
        status, or subscribe to the `eval_run.completed` webhook. There is no item cap on a queued run.


        The whole run is pinned to **one** agent version, stamped on `agent_version`: pass one explicitly to evaluate a
        canary before promoting it, or omit it to use the active release's stable version (or the live draft when no
        release is in effect). Without the pin, release assignment would bucket each item independently and blend two
        configs into a single score.


        With `baseline_run_id`, the finished run's `aggregate_scores.baseline` carries per-scorer deltas against that
        run, computed over the items present and scorable in **both** runs, with the divergence counted. A delta over a
        shifted dataset is therefore never presented as a clean comparison.
      operationId: startEvalRun
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                wait:
                  type: boolean
                  default: false
                  description: >-
                    True runs the eval synchronously (25-item cap) and returns a terminal run with its scores. False —
                    the default — enqueues the items and returns a `queued` run immediately.
                  example: true
                agent_version:
                  type: integer
                  nullable: true
                  description: >-
                    An archived agent version to evaluate. Defaults to the active release's stable version, or the live
                    draft version when no release is in effect.
                  example: 3
                baseline_run_id:
                  type: string
                  nullable: true
                  description: >-
                    A terminal run of the same eval to compare against. The finished run's `aggregate_scores.baseline`
                    reports per-scorer deltas over the item intersection. A run of a different eval is rejected with
                    400.
                  example: evrun_V1StGXR8Z5jdHi6B
                metadata:
                  type: object
                  additionalProperties: true
                  description: >-
                    Caller-supplied key/value metadata attached to the run record for attribution — what this
                    measurement was of (the commit or release candidate being scored, the CI job that asked for it).
                    Round-trips verbatim on every read of the run, the list included.


                    The bag is caller-owned and no key is reserved: everything the platform decides about a run
                    (`status`, `agent_version`, `baseline_run_id`, `aggregate_scores`, `passed`, the counts) is a field
                    of its own and cannot be written from here. Nothing in the scoring path reads it. A non-object is
                    rejected with `400 VALIDATION_FAILED` and no run is created.
                  example:
                    commit_sha: 9f2c1ab
                    ci_job: nightly-evals
                tool_context:
                  type: object
                  additionalProperties:
                    type: string
                  description: >-
                    Key/value context forwarded to every item's generation, so an agent whose tools authorize through
                    `tool_context` is scored against the configuration it runs in production rather than with an empty
                    bag. Each key is forwarded as one `X-Soat-Context-<key>` header and resolves any `{{context:<key>}}`
                    token in a bound tool's headers or `preset_parameters`.


                    Stored on the run and re-read per item, since a queued run (the default) is driven by a worker with
                    no request behind it. **Write-only**: no read of the run returns it, unlike `metadata` — a run is a
                    report other people read, and a credential in it is not theirs to see. Cleared once the run reaches
                    a terminal state.


                    An eval generation has no session, so the reserved keys `session_id`, `actor_id` and
                    `actor_external_id` are dropped (in any casing) rather than forwarded. Every other key becomes an
                    HTTP header name and must match that grammar, or the request is rejected with `400
                    INVALID_TOOL_CONTEXT_KEY` and no run is created.
                  example:
                    ocaToken: eyJhbGciOiJIUzI1NiJ9.abc
                    tenant: acme
      responses:
        '201':
          description: 'Eval run finished (`wait: true`) or queued (`wait: false`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EvalRun'
        '400':
          description: >-
            Bad request (non-boolean wait, dataset empty or over the synchronous cap, unknown agent_version, invalid
            baseline, scorers no longer valid against the agent, a `tool_context` key that cannot become a header)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval not found
        '500':
          description: Internal server error
  /api/v1/evals/{eval_id}/runs/{eval_run_id}:
    get:
      tags:
        - Evaluations
      summary: Get an eval run
      description: Returns a run's status, counts, and aggregate scores
      operationId: getEvalRun
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
        - name: eval_run_id
          in: path
          required: true
          description: Eval run ID
          schema:
            type: string
            example: evrun_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Eval run details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EvalRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval or run not found
  /api/v1/evals/{eval_id}/runs/{eval_run_id}/results:
    get:
      tags:
        - Evaluations
      summary: List eval run results
      description: Returns the per-item results of a run, oldest first
      operationId: listEvalResults
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
        - name: eval_run_id
          in: path
          required: true
          description: Eval run ID
          schema:
            type: string
            example: evrun_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of eval results
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EvalResult'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval or run not found
  /api/v1/evals/{eval_id}/runs/{eval_run_id}/cancel:
    post:
      tags:
        - Evaluations
      summary: Cancel an eval run
      description: >-
        Cancels a queued or running run: its outstanding item tasks are dropped so it stops consuming provider budget,
        and the run settles as `canceled`.


        Results already written are kept — they are real measurements of generations that were really paid for — and
        `completed_count` / `errored_count` report what ran. `aggregate_scores` is deliberately left null: a partial
        roll-up in the same field a completed run uses would read as a whole-dataset verdict.


        A run that has already finished is rejected with 400.
      operationId: cancelEvalRun
      parameters:
        - name: eval_id
          in: path
          required: true
          description: Eval ID
          schema:
            type: string
            example: eval_V1StGXR8Z5jdHi6B
        - name: eval_run_id
          in: path
          required: true
          description: Eval run ID
          schema:
            type: string
            example: evrun_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Eval run canceled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EvalRun'
        '400':
          description: The run has already finished
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Eval or run not found
        '500':
          description: Internal server error
  /api/v1/exceptions:
    get:
      tags:
        - Exceptions
      summary: List exception items
      description: Returns exception items for a project, filterable by status, severity, and kind.
      operationId: listExceptions
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          description: Filter by triage status
          schema:
            type: string
            enum:
              - open
              - acknowledged
              - resolved
        - name: severity
          in: query
          description: Filter by severity
          schema:
            type: string
            enum:
              - info
              - warning
              - critical
        - name: kind
          in: query
          description: Filter by how the exception was filed
          schema:
            type: string
            enum:
              - run_failed
              - guardrail_tripwire
              - approval_expired
              - quota_unpriced
              - event_trigger_loop
              - chain_limit
              - manual
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of exception items
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExceptionItem'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/exceptions/{exception_id}:
    get:
      tags:
        - Exceptions
      summary: Get an exception item
      description: Returns a single exception item with its full detail.
      operationId: getException
      parameters:
        - $ref: '#/components/parameters/exception_id'
      responses:
        '200':
          description: Exception item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExceptionItem'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Exception item not found
  /api/v1/exceptions/{exception_id}/acknowledge:
    post:
      tags:
        - Exceptions
      summary: Acknowledge an exception item
      description: >-
        Moves the item to `acknowledged` ("someone is on it"), recording who. A no-op that returns the item unchanged
        when already acknowledged; rejected when already resolved.
      operationId: acknowledgeException
      parameters:
        - $ref: '#/components/parameters/exception_id'
      responses:
        '200':
          description: Exception item acknowledged
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExceptionItem'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Exception item not found
        '409':
          description: Item already resolved
  /api/v1/exceptions/{exception_id}/resolve:
    post:
      tags:
        - Exceptions
      summary: Resolve an exception item
      description: Moves the item to `resolved` ("fixed"), recording who and an optional note.
      operationId: resolveException
      parameters:
        - $ref: '#/components/parameters/exception_id'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  description: Optional resolution note
                  example: Root cause fixed; retried the run successfully.
      responses:
        '200':
          description: Exception item resolved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExceptionItem'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Exception item not found
        '409':
          description: Item already resolved
  /api/v1/files:
    get:
      tags:
        - Files
      summary: List all files
      description: Returns a list of all stored files
      operationId: listFiles
      parameters:
        - name: project_id
          in: query
          required: false
          description: Filter files by project ID
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of files returned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FileRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - Files
      summary: Create a file
      description: Creates a new file record in the system
      operationId: createFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: >-
                    Public ID of the project. Optional when authenticating with a project-scoped API key, which defaults
                    to the key's project; required otherwise.
                  example: proj_V1StGXR8Z5jdHi6B
                prefix:
                  type: string
                  description: >-
                    Directory within the project (e.g. /images). Optional; defaults to / (root). Combined with filename
                    to form the file's key (path).
                  example: /images
                filename:
                  type: string
                  description: Original / download name and the key's leaf segment (e.g. logo.png).
                  example: logo.png
                content_type:
                  type: string
                  description: MIME type of the file
                  example: application/pdf
                size:
                  type: integer
                  nullable: true
                  description: File size in bytes
                  example: 1024
                metadata:
                  type: string
                  description: JSON string with additional metadata
                  example: '{"author":"John"}'
      responses:
        '201':
          description: File created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/files/upload:
    post:
      tags:
        - Files
      summary: Upload a file
      description: Uploads a file to the server and stores it in the configured storage directory
      operationId: uploadFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: File content
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: >-
                    Project ID to associate the file with. Optional when authenticating with a project-scoped API key,
                    which defaults to the key's project; required otherwise.
                  example: proj_V1StGXR8Z5jdHi6B
                prefix:
                  type: string
                  description: Directory within the project (e.g. /images). Optional; defaults to / (root).
                  example: /images
                filename:
                  type: string
                  description: Original / download name. Optional; defaults to the uploaded file's name.
                  example: logo.png
                metadata:
                  type: string
                  description: Additional metadata as a JSON string
                  example: '{"author":"John"}'
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '400':
          description: Missing file or invalid project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/files/upload/base64:
    post:
      tags:
        - Files
      summary: Upload a file using base64 encoding
      description: Uploads a file to the server using base64-encoded content
      operationId: uploadFileBase64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadFileBase64Request'
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '400':
          description: Missing content or invalid project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/files/presigned-url:
    post:
      tags:
        - Files
      summary: Create a presigned upload URL
      description: >-
        Creates a short-lived, single-use presigned upload URL — the local-storage equivalent of an S3 presigned URL.
        The client then uploads the file content directly to the returned `upload_url` via `POST
        /api/v1/files/upload/{token}`, bypassing MCP payload size limits. When the server is configured with
        `SOAT_BASE_URL`, `upload_url` is a fully-qualified absolute URL so MCP agents and other clients can POST to it
        without knowing the server base URL in advance.
      operationId: createPresignedUrl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PresignedUrlRequest'
      responses:
        '201':
          description: Presigned URL created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresignedUrlResponse'
        '400':
          description: Missing or invalid project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/files/upload/{token}:
    post:
      tags:
        - Files
      summary: Upload a file using an upload token
      description: >-
        Uploads file content authorized by a single-use token from `POST /api/v1/files/presigned-url`. No bearer
        credential is required — the token is the credential. Accepts either multipart/form-data (field `file`) or JSON
        with a base64-encoded `content` field.
      operationId: uploadFileWithToken
      security: []
      parameters:
        - name: token
          in: path
          required: true
          description: The single-use upload token (e.g. upt_...)
          schema:
            type: string
            example: upt_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: File content
                metadata:
                  type: string
                  description: Additional metadata as a JSON string
                  example: '{"author":"John"}'
          application/json:
            schema:
              $ref: '#/components/schemas/UploadFileWithTokenRequest'
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '400':
          description: Missing content
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Upload token not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Upload token already used
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '410':
          description: Upload token expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}:
    get:
      tags:
        - Files
      summary: Get a file by ID
      description: Returns the data and metadata of a specific file
      operationId: getFile
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
            example: abc123
      responses:
        '200':
          description: File found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Files
      summary: Delete a file
      description: Removes a file from the system by ID
      operationId: deleteFile
      parameters:
        - name: file_id
          in: path
          required: true
          description: ID of the file to delete
          schema:
            type: string
            example: abc123
      responses:
        '204':
          description: File deleted successfully
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/download:
    get:
      tags:
        - Files
      summary: Download a file
      description: Streams the file content to the client
      operationId: downloadFile
      x-soat-mcp-exclude: true
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
            example: file_abc123
      responses:
        '200':
          description: File content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/metadata:
    patch:
      tags:
        - Files
      summary: Update file metadata
      description: Updates the metadata field of a file
      operationId: updateFileMetadata
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
            example: file_abc123
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                metadata:
                  type: string
                  description: New metadata as a JSON string
                  example: '{"author":"Jane","tags":["report"]}'
                prefix:
                  type: string
                  description: >-
                    New directory — moves the file. The resulting path (prefix + filename) must be unique within the
                    project.
                  example: /reports
                filename:
                  type: string
                  description: New filename — renames the key's leaf and the download name.
                  example: renamed-file.txt
      responses:
        '200':
          description: Metadata updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A file already exists at the target path in this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/download/base64:
    get:
      tags:
        - Files
      summary: Download file as base64
      description: Returns the file content encoded as base64
      operationId: downloadFileBase64
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      responses:
        '200':
          description: File content as base64
          content:
            application/json:
              schema:
                type: object
                properties:
                  content:
                    type: string
                    description: Base64-encoded file content
                  filename:
                    type: string
                    description: Original filename
                  content_type:
                    type: string
                    description: MIME type of the file
                  size:
                    type: integer
                    nullable: true
                    description: File size in bytes
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/tags:
    get:
      tags:
        - Files
      summary: Get file tags
      description: Returns all tags attached to the file
      operationId: getFileTags
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      responses:
        '200':
          description: File tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Files
      summary: Replace file tags
      description: Replaces all tags on the file with the provided tags
      operationId: replaceFileTags
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Tags replaced
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Files
      summary: Merge file tags
      description: Merges provided tags with existing tags
      operationId: mergeFileTags
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Tags merged
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/formations/validate:
    post:
      tags:
        - Formations
      summary: Validate a formation template
      description: >
        Validates a formation template without creating any resources. Returns a list of errors and warnings. Accepts
        the template as a JSON object or as a YAML/JSON string.
      operationId: validateFormation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                template:
                  $ref: '#/components/schemas/FormationTemplateInput'
                parameters:
                  type: object
                  additionalProperties:
                    type: string
                  description: >
                    Runtime parameter values that override or supply template parameter defaults. Keys must match
                    parameter names declared in `template.parameters`. When provided, the validation result also reports
                    required parameters that are still missing after applying these values.
                  nullable: true
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'
        '401':
          description: Unauthorized
  /api/v1/formations/plan:
    post:
      tags:
        - Formations
      summary: Plan a formation deployment
      description: >
        Computes a diff between the desired template and the current stack state without making any changes. Returns the
        list of planned actions.
      operationId: planFormation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - template
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: >-
                    Project ID. Optional when authenticating with a project-scoped API key, which defaults to the key's
                    project; required otherwise.
                  example: proj_V1StGXR8Z5jdHi6B
                formation_id:
                  x-soat-ref: formations
                  type: string
                  description: Existing formation ID to compare against. Omit for new formation planning.
                template:
                  $ref: '#/components/schemas/FormationTemplateInput'
                parameters:
                  type: object
                  additionalProperties:
                    type: string
                  description: >
                    Runtime parameter values that override or supply template parameter defaults. Keys must match
                    parameter names declared in `template.parameters`. A parameter declared with `use_previous_value:
                    true` may be omitted to reuse its stored value.
                  nullable: true
      responses:
        '200':
          description: Plan result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanResult'
        '400':
          description: Bad Request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/formations:
    get:
      tags:
        - Formations
      summary: List formations
      description: Returns all formation stacks for a project
      operationId: listFormations
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of formations
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Formation'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      tags:
        - Formations
      summary: Create a new formation
      description: >
        Validates the template, creates the formation record, then provisions all declared resources in dependency
        order.


        A **template-shape** error is refused with `400`. A **deploy** failure is not: the operation ran, so the
        formation is returned with `201` and `status: "failed"`, and `error` explains why (the resources created before
        the failure are rolled back). Read `status` — a `2xx` here means the deploy was attempted, not that it worked.
        The `builtin` CLI exits non-zero on that body so `create-formation && …` does not lie.
      operationId: createFormation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - template
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: >-
                    Project ID. Optional when authenticating with a project-scoped API key, which defaults to the key's
                    project; required otherwise.
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Human-readable name for the formation stack
                  example: my-agent-stack
                template:
                  $ref: '#/components/schemas/FormationTemplateInput'
                parameters:
                  type: object
                  additionalProperties:
                    type: string
                  description: >
                    Runtime parameter values that override or supply template parameter defaults. Keys must match
                    parameter names declared in `template.parameters`. Required parameters (those without a default)
                    must be provided here.
                  nullable: true
                metadata:
                  type: object
                  additionalProperties: true
                  nullable: true
                  description: >
                    Static annotations stored on the formation record. This field is NOT a substitution site:
                    `sub`/`param`/`ref` expressions are rejected with 400 (`FORMATION_INVALID_METADATA`). For
                    deploy-time substitution use the template's top-level `metadata` block, which is resolved into
                    `resolved_metadata`.
      responses:
        '201':
          description: Formation created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Formation'
        '400':
          description: Bad Request
        '401':
          description: Unauthorized
        '403':
          description: >-
            Forbidden — either the caller may not operate on formations in the project, or it lacks an action a resource
            this template declares requires. `error.meta.denied_actions` names every missing action; nothing is applied.
        '409':
          description: Formation with this name already exists
  /api/v1/formations/{formation_id}:
    get:
      tags:
        - Formations
      summary: Get a specific formation
      description: Returns the formation stack including its current resources.
      operationId: getFormation
      parameters:
        - name: formation_id
          in: path
          required: true
          schema:
            type: string
          example: form_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Formation details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Formation'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not Found
    put:
      tags:
        - Formations
      summary: Update an formation
      description: >
        Applies a new template to the formation. Resources are created, updated, or deleted to reconcile the current
        state with the desired state.


        A **template-shape** error is refused with `400`. A **deploy** failure is not: the operation ran, so the
        formation is returned with `200` and `status: "failed"`, and `error` explains why. Read `status` — a `2xx` here
        means the deploy was attempted, not that it worked. The `builtin` CLI exits non-zero on that body so
        `update-formation && …` does not lie.


        A deploy that replaced a resource and could not delete the superseded one answers `status: "active"` with
        `error.code: "FORMATION_REPLACE_CLEANUP_FAILED"` — the desired state is realised, and `error.meta.failures`
        names every resource still live. The next deploy retries the disposal.
      operationId: updateFormation
      parameters:
        - name: formation_id
          in: path
          required: true
          schema:
            type: string
          example: form_V1StGXR8Z5jdHi6B
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                template:
                  $ref: '#/components/schemas/FormationTemplateInput'
                parameters:
                  type: object
                  additionalProperties:
                    type: string
                  description: >
                    Runtime parameter values that override or supply template parameter defaults. Keys must match
                    parameter names declared in `template.parameters`. Required parameters (those without a default)
                    must be provided here, unless the parameter is declared with `use_previous_value: true`, in which
                    case omitting it reuses the previously stored value.
                  nullable: true
                metadata:
                  type: object
                  additionalProperties: true
                  nullable: true
                  description: >
                    Static annotations stored on the formation record. This field is NOT a substitution site:
                    `sub`/`param`/`ref` expressions are rejected with 400 (`FORMATION_INVALID_METADATA`). For
                    deploy-time substitution use the template's top-level `metadata` block, which is resolved into
                    `resolved_metadata`.
      responses:
        '200':
          description: Updated formation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Formation'
        '400':
          description: Bad Request
        '401':
          description: Unauthorized
        '403':
          description: >-
            Forbidden — either the caller may not operate on formations in the project, or it lacks an action a resource
            this template declares requires. `error.meta.denied_actions` names every missing action; nothing is applied.
        '404':
          description: Not Found
    delete:
      tags:
        - Formations
      summary: Delete an formation
      description: >
        Deletes the formation stack and all its managed resources in reverse dependency order.


        A resource the platform refuses to delete on its own — most often an agent that has generation or trace history
        — fails the teardown with `409 FORMATION_DELETE_FAILED`, naming every blocking resource in
        `error.meta.failures`. Resolve the blockers (for an agent, `DELETE /api/v1/agents/{agent_id}?force=true` also
        removes its generations and traces, and `deletion_policy: retain` exempts it from teardown entirely) and delete
        the formation again.


        A refusal the platform can foresee is found by a pre-flight, before the first delete: nothing is removed, and
        the formation stays `active` and intact for the retry. An unforeseeable error surfaces mid-teardown instead,
        where resources deleted before the blocker stay deleted and the formation is left in `delete_failed`. The error
        message states which happened.
      operationId: deleteFormation
      parameters:
        - name: formation_id
          in: path
          required: true
          schema:
            type: string
          example: form_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                required:
                  - success
        '401':
          description: Unauthorized
        '403':
          description: >-
            Forbidden — either the caller may not operate on formations in the project, or it lacks an action a resource
            this template declares requires. `error.meta.denied_actions` names every missing action; nothing is applied.
        '404':
          description: Not Found
        '409':
          description: >
            One or more resources could not be deleted (`FORMATION_DELETE_FAILED`). `error.meta.failures` lists each one
            as `{ logical_id, resource_type, error }`. The `message` says whether the pre-flight caught it (nothing
            deleted, formation still `active`) or it surfaced mid-teardown (formation left in `delete_failed`).
  /api/v1/formations/{formation_id}/events:
    get:
      tags:
        - Formations
      summary: List formation operation events
      description: >
        Returns all operations (create, update, delete) with their event logs for the formation, ordered
        chronologically.
      operationId: listFormationEvents
      parameters:
        - name: formation_id
          in: path
          required: true
          schema:
            type: string
          example: form_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of operations
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FormationOperation'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not Found
  /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'
  /api/v1/guardrails:
    post:
      tags:
        - Guardrails
      summary: Create a guardrail
      description: >
        Creates a new guardrail in the project, archiving its document as version 1. The `document` is validated on
        write: `class` must be a literal (A/B/C/D) or a JSON Logic expression, and every variable it (and `guard`)
        reference must resolve to the `args.*` / `context.*` / `runtime.*` namespaces — an out-of-catalog `runtime.*`
        key is rejected with 400.
      operationId: createGuardrail
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGuardrailRequest'
            examples:
              always_approve:
                summary: Always require human sign-off
                value:
                  name: Sign-off Guardrail
                  document:
                    class: C
              budget_threshold:
                summary: Class B below a threshold, C at or above, guarded by 24h spend
                value:
                  name: Budget Update Guardrail
                  document:
                    default_class: C
                    class:
                      if:
                        - <:
                            - var: args.amount
                            - 500
                        - B
                        - C
                    guard:
                      <:
                        - var: runtime.usage.cost_usd_24h
                        - 1000
      responses:
        '201':
          description: Guardrail created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Guardrail'
        '400':
          description: Bad Request — invalid document or variable reference
          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'
    get:
      tags:
        - Guardrails
      summary: List guardrails
      description: Returns all guardrails in the project.
      operationId: listGuardrails
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
          description: Project public ID to filter by
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of guardrails
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Guardrail'
                  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/guardrails/{guardrail_id}:
    get:
      tags:
        - Guardrails
      summary: Get a guardrail
      description: Returns a single guardrail by ID.
      operationId: getGuardrail
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Guardrail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Guardrail'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Guardrails
      summary: Update a guardrail
      description: >
        Updates an existing guardrail. A `document` write that actually changes the policy increments `version` and
        archives the new document as a GuardrailVersion; metadata-only edits (name / description / context), and
        re-writing the document the guardrail already holds, leave the version untouched.
      operationId: updateGuardrail
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateGuardrailRequest'
      responses:
        '200':
          description: Guardrail updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Guardrail'
        '400':
          description: Bad Request — invalid document or variable reference
          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: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Guardrails
      summary: Delete a guardrail
      description: Deletes a guardrail and its archived versions by ID.
      operationId: deleteGuardrail
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/guardrails/{guardrail_id}/versions:
    get:
      tags:
        - Guardrails
      summary: List a guardrail's config versions
      description: >
        Returns the guardrail's archived configurations, newest first. A version is written on create and on every
        subsequent write that changes the policy `document` — through the REST API or a formation apply alike.
        Metadata-only edits (name, description, context binding) do not archive a version. See
        [Versioning](/docs/modules/guardrails#versioning).
      operationId: listGuardrailVersions
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of guardrail versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/GuardrailVersion'
                  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'
        '404':
          description: Guardrail not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/guardrails/{guardrail_id}/versions/{version}:
    get:
      tags:
        - Guardrails
      summary: Fetch an archived guardrail version
      description: >
        Returns the exact configuration — and so the exact `document` — that governed at a given version. Approval
        items, activity entries, and exceptions record the version that governed them, so the audit chain survives
        edits.
      operationId: getGuardrailVersion
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Archived guardrail version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GuardrailVersion'
        '400':
          description: Bad Request — version is not a positive integer
          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: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/guardrails/{guardrail_id}/versions/{version}/restore:
    post:
      tags:
        - Guardrails
      summary: Restore an archived guardrail config
      description: >
        Writes an archived version's `document` back as the guardrail's live policy, which archives it again as a
        **new** version rather than rewinding the counter — so an approval item or exception citing any version in
        between still resolves.


        The restore runs through the ordinary update path, so the archived document is re-validated; restoring the
        policy the guardrail already holds is a no-op and archives nothing.
      operationId: restoreGuardrailVersion
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreGuardrailVersionRequest'
      responses:
        '200':
          description: The guardrail, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Guardrail'
        '400':
          description: Bad Request — invalid version, or the archived document no longer validates
          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: Guardrail or version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/guardrails/{guardrail_id}/evaluate:
    post:
      tags:
        - Guardrails
      summary: Dry-run evaluate a guardrail
      description: >
        Runs the full evaluation pipeline — the `class` expression, the guard, the context tool per `context_mode`, live
        `runtime.*` resolution — against caller-supplied `args` and `guardrail_context`, and returns the exact
        `guardrail_evaluation` record a real call would produce. Nothing executes, no approval item is filed, and no
        activity entry is written. This is the adoption path: preview a document's decisions against production-shaped
        calls before attaching it — or before editing a widely-attached one. Pass an optional `tool_id` to resolve
        `runtime.tool.*`.
      operationId: evaluateGuardrail
      parameters:
        - name: guardrail_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                args:
                  type: object
                  additionalProperties: true
                  description: The proposed call's arguments (the `args.*` namespace).
                guardrail_context:
                  type: object
                  additionalProperties: true
                  description: >
                    The caller-supplied guardrail context (the `context.*` namespace), combined with the context tool
                    per `context_mode`.
                tool_id:
                  type: string
                  description: Optional tool to resolve `runtime.tool.*` against.
      responses:
        '200':
          description: The would-be evaluation record (nothing executed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GuardrailEvaluation'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/ingestion-rules:
    get:
      tags:
        - Ingestion Rules
      summary: List ingestion rules
      description: Returns the ingestion rules for a project
      operationId: listIngestionRules
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          description: Number of results per page
          schema:
            type: integer
            default: 25
        - name: offset
          in: query
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of ingestion rules
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/IngestionRule'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Ingestion Rules
      summary: Create an ingestion rule
      description: Creates a rule mapping a content_type glob to a converter. Exactly one of tool_id or agent_id must be set.
      operationId: createIngestionRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content_type_glob
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                content_type_glob:
                  type: string
                  description: MIME type glob matched against a file's content_type
                  example: image/*
                tool_id:
                  x-soat-ref: tools
                  type: string
                  description: Converter tool id (mutually exclusive with agent_id)
                  example: tool_V1StGXR8Z5jdHi6B
                agent_id:
                  x-soat-ref: agents
                  type: string
                  description: Converter agent id (mutually exclusive with tool_id)
                  example: agent_V1StGXR8Z5jdHi6B
                action:
                  type: string
                  description: Operation id, required for builtin/mcp tool converters
                preset_parameters:
                  type: object
                  description: Merged into the tool input before invocation (tool converters only)
                native_extraction:
                  type: string
                  enum:
                    - first
                    - skip
                  description: >-
                    For native types (PDF/text): `first` (default) converts only when native extraction yields no text;
                    `skip` always converts.
                file_delivery:
                  type: string
                  enum:
                    - base64
                    - download_url
                  description: How the file reaches a tool converter (default base64)
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: Default chunk strategy, overridable per ingest request
                chunk_size:
                  type: integer
                  description: Default window size in characters for the size strategy
                chunk_overlap:
                  type: integer
                  description: Default overlap in characters for the size strategy
                metadata:
                  type: object
                  description: Arbitrary JSON metadata
      responses:
        '201':
          description: Ingestion rule created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestionRule'
        '400':
          description: Validation failed (e.g. tool_id and agent_id both set or both missing)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A rule for this content_type_glob already exists in the project
        '500':
          description: Internal server error
  /api/v1/ingestion-rules/{ingestion_rule_id}:
    get:
      tags:
        - Ingestion Rules
      summary: Get an ingestion rule
      description: Returns a specific ingestion rule
      operationId: getIngestionRule
      parameters:
        - name: ingestion_rule_id
          in: path
          required: true
          description: Ingestion rule ID
          schema:
            type: string
            example: igr_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Ingestion rule details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestionRule'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Ingestion rule not found
    patch:
      tags:
        - Ingestion Rules
      summary: Update an ingestion rule
      description: Updates fields of an ingestion rule
      operationId: updateIngestionRule
      parameters:
        - name: ingestion_rule_id
          in: path
          required: true
          description: Ingestion rule ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content_type_glob:
                  type: string
                tool_id:
                  x-soat-ref: tools
                  type: string
                  nullable: true
                agent_id:
                  x-soat-ref: agents
                  type: string
                  nullable: true
                action:
                  type: string
                  nullable: true
                preset_parameters:
                  type: object
                  nullable: true
                native_extraction:
                  type: string
                  enum:
                    - first
                    - skip
                file_delivery:
                  type: string
                  enum:
                    - base64
                    - download_url
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                    - null
                  nullable: true
                  description: Send `null` to clear the rule's override and fall back to the per-request default.
                chunk_size:
                  type: integer
                  nullable: true
                chunk_overlap:
                  type: integer
                  nullable: true
                metadata:
                  type: object
                  nullable: true
      responses:
        '200':
          description: Ingestion rule updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestionRule'
        '400':
          description: Validation failed
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Ingestion rule not found
        '409':
          description: A rule for this content_type_glob already exists in the project
    delete:
      tags:
        - Ingestion Rules
      summary: Delete an ingestion rule
      description: Deletes an ingestion rule
      operationId: deleteIngestionRule
      parameters:
        - name: ingestion_rule_id
          in: path
          required: true
          description: Ingestion rule ID
          schema:
            type: string
      responses:
        '204':
          description: Ingestion rule deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Ingestion rule not found
  /api/v1/knowledge/search:
    post:
      tags:
        - Knowledge
      summary: Search knowledge
      description: >-
        Searches across documents and memory entries using semantic search, file paths, document IDs, or memory
        IDs/tags. At least one of `query`, `document_paths`, `document_ids`, `memory_ids`, or `memory_tags` must be
        provided.
      operationId: searchKnowledge
      x-iam-action: knowledge:SearchKnowledge
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Limit search to a specific project
                  example: proj_V1StGXR8Z5jdHi6B
                query:
                  type: string
                  description: Semantic search query text
                  example: customer communication preferences
                min_score:
                  type: number
                  description: >-
                    Minimum `score` a result must reach to be returned. Filters on the implementation-defined `score`,
                    not on `similarity_score`, so the cutoff follows the ranking. Only applies when `query` is provided.
                    Because the scale behind `score` is not part of the contract, treat a tuned value as tied to the
                    deployment rather than portable.
                  minimum: 0
                  maximum: 1
                  example: 0.5
                limit:
                  type: integer
                  description: Maximum number of results to return (default 10)
                  minimum: 1
                  example: 10
                memory_ids:
                  x-soat-ref: memories
                  type: array
                  description: Search entries within these specific memories
                  items:
                    type: string
                  example:
                    - mem_V1StGXR8Z5jdHi6B
                memory_tags:
                  type: array
                  description: Search entries in memories whose tags match any of these patterns (glob supported)
                  items:
                    type: string
                  example:
                    - customer*
                    - support
                document_paths:
                  type: array
                  description: Filter results to documents whose file path starts with one of these prefixes
                  items:
                    type: string
                  example:
                    - /sales/
                    - /hr/
                document_ids:
                  x-soat-ref: documents
                  type: array
                  description: Filter results to specific document IDs
                  items:
                    type: string
                  example:
                    - doc_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                type: object
                required:
                  - results
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/KnowledgeResult'
        '400':
          description: Bad request — at least one search parameter is required
          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'
  /api/v1/memories:
    get:
      tags:
        - Memories
      summary: List memories
      description: Returns a list of memory configurations for a project
      operationId: listMemories
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: tags
          in: query
          description: >
            Filter memories by tag patterns. Supports glob syntax (`*` matches any substring, `?` matches any single
            character). Multiple values are ORed — a memory is returned if any of its tags match any of the provided
            patterns. Omit to return all memories.
          schema:
            type: array
            items:
              type: string
          style: form
          explode: true
          example:
            - customer*
            - support
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of memories
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Memory'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Memories
      summary: Create a memory
      description: Creates a new memory configuration in a project
      operationId: createMemory
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Memory name
                  example: Product Documentation
                description:
                  type: string
                  description: Optional description
                  example: Retrieves product docs for support queries
                tags:
                  type: array
                  items:
                    type: string
                  description: Optional list of tags for filtering in knowledge search
                  example:
                    - projectA
                    - customer-support
      responses:
        '201':
          description: Memory created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
        '400':
          description: Bad request (missing required fields or invalid config)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/memories/{memory_id}:
    get:
      tags:
        - Memories
      summary: Get a memory
      description: Returns a single memory configuration by ID
      operationId: getMemory
      parameters:
        - name: memory_id
          in: path
          required: true
          schema:
            type: string
            example: mem_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Memory found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory not found
        '500':
          description: Internal server error
    put:
      tags:
        - Memories
      summary: Update a memory
      description: Updates an existing memory configuration
      operationId: updateMemory
      parameters:
        - name: memory_id
          in: path
          required: true
          schema:
            type: string
            example: mem_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Memory name
                description:
                  type: string
                  nullable: true
                  description: Optional description
                tags:
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: Optional list of tags for filtering in knowledge search
      responses:
        '200':
          description: Memory updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory not found
        '500':
          description: Internal server error
    delete:
      tags:
        - Memories
      summary: Delete a memory
      description: Deletes a memory configuration
      operationId: deleteMemory
      parameters:
        - name: memory_id
          in: path
          required: true
          schema:
            type: string
            example: mem_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Memory deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory not found
        '500':
          description: Internal server error
  /api/v1/memory-entries:
    get:
      tags:
        - Memory Entries
      summary: List memory entries
      description: Returns all entries in a memory container
      operationId: listMemoryEntries
      x-iam-action: memories:ListMemoryEntries
      parameters:
        - name: memory_id
          in: query
          required: true
          description: Memory container to list entries from (mem_...)
          schema:
            type: string
            example: mem_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
        - name: include_invalidated
          in: query
          required: false
          description: >-
            Include invalidated (superseded) entries. They are excluded by default; set this to audit the supersede
            history.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: List of memory entries
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MemoryEntry'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory not found
        '500':
          description: Internal server error
    post:
      tags:
        - Memory Entries
      summary: Create a memory entry
      description: >-
        Creates a new entry in the specified memory container. Automatically generates an embedding for semantic search,
        and skips the write when an existing entry is a near-duplicate (see `duplicate_threshold`). A merely similar
        fact is stored as its own entry: this path has no agent context and therefore no model to consolidate two facts
        into one.
      operationId: createMemoryEntry
      x-iam-action: memories:CreateMemoryEntry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - memory_id
                - content
              properties:
                memory_id:
                  x-soat-ref: memories
                  type: string
                  description: Memory container to add the entry to (mem_...)
                  example: mem_V1StGXR8Z5jdHi6B
                content:
                  type: string
                  description: The text content of the memory entry
                  example: The customer prefers email communication over phone calls
                source_type:
                  type: string
                  enum:
                    - manual
                    - agent
                    - extraction
                    - orchestration
                  description: How this entry was created
                  default: manual
                  example: manual
                tags:
                  type: array
                  items:
                    type: string
                  description: Per-entry tag strings, used for entry-granularity filtering in search-knowledge (memory_tags)
                  example:
                    - role:traffic-manager
                    - source:rejected_approval
                metadata:
                  type: object
                  additionalProperties: true
                  description: Arbitrary structured metadata attached to the entry
                  example:
                    evidence: high
                    quarter: Q3
                duplicate_threshold:
                  type: number
                  description: >-
                    Cosine similarity score at or above which the incoming content is considered a duplicate of an
                    existing entry and skipped (default 0.95). Below it the entry is always created.
                  default: 0.95
                  minimum: 0
                  maximum: 1
      responses:
        '200':
          description: Memory entry deduplicated (action is "skipped")
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryEntryWriteResult'
        '201':
          description: Memory entry created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryEntryWriteResult'
        '400':
          description: Bad request (missing required fields)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory not found
        '500':
          description: Internal server error
  /api/v1/memory-entries/{entry_id}:
    get:
      tags:
        - Memory Entries
      summary: Get a memory entry
      description: Returns a single memory entry by ID
      operationId: getMemoryEntry
      x-iam-action: memories:GetMemoryEntry
      parameters:
        - name: entry_id
          in: path
          required: true
          schema:
            type: string
            example: mem_entry_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Memory entry found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryEntry'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory or entry not found
        '500':
          description: Internal server error
    put:
      tags:
        - Memory Entries
      summary: Update a memory entry
      description: Updates an existing memory entry. Regenerates the embedding if content changes.
      operationId: updateMemoryEntry
      x-iam-action: memories:UpdateMemoryEntry
      parameters:
        - name: entry_id
          in: path
          required: true
          schema:
            type: string
            example: mem_entry_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                  description: Updated text content
                tags:
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: Replaces the entry's tags. Pass null or an empty array to clear.
                  example:
                    - role:traffic-manager
                metadata:
                  type: object
                  nullable: true
                  additionalProperties: true
                  description: Replaces the entry's metadata. Pass null to clear.
      responses:
        '200':
          description: Memory entry updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryEntry'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory or entry not found
        '500':
          description: Internal server error
    delete:
      tags:
        - Memory Entries
      summary: Delete a memory entry
      description: Deletes a memory entry
      operationId: deleteMemoryEntry
      x-iam-action: memories:DeleteMemoryEntry
      parameters:
        - name: entry_id
          in: path
          required: true
          schema:
            type: string
            example: mem_entry_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Memory entry deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Memory or entry not found
        '500':
          description: Internal server error
  /api/v1/model-routes:
    get:
      tags:
        - Model Routes
      summary: List model routes
      description: Returns the model routes defined in a project
      operationId: listModelRoutes
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of model routes
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelRoute'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Model Routes
      summary: Create a model route
      description: >-
        Creates a project-scoped model route: a named, ordered list of provider+model targets tried in array order.
        Every target must reference an AI provider in the same project (400 otherwise), and the total attempt budget —
        the sum of `1 + max_retries` over all targets — may not exceed 10 (400 naming the computed total). A duplicate
        `name` in the project is rejected with 409.
      operationId: createModelRoute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - targets
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Human-readable name, unique per project
                  example: primary-with-fallback
                targets:
                  type: array
                  minItems: 1
                  description: >-
                    Ordered failover targets. Position is priority: target 0 is tried first, and a retryable failure
                    falls through to the next target.
                  items:
                    $ref: '#/components/schemas/ModelRouteTarget'
                retry_on:
                  type: array
                  description: >-
                    Which failure classes are failover-eligible. A failure whose class is not listed (and every
                    deterministic failure — 400-class, auth, content policy) fails the generation immediately instead of
                    spending another target's budget.
                  items:
                    type: string
                    enum:
                      - provider_error
                      - timeout
                      - rate_limited
                  default:
                    - provider_error
                    - timeout
                    - rate_limited
                failure_threshold:
                  type: integer
                  default: 3
                  description: >-
                    Consecutive retryable failures after which a target is skipped for `cooldown_seconds`. Breaker state
                    is in-process per node and keyed by (provider, model), so it is shared by every route pointing at
                    the same backend.
                  example: 3
                cooldown_seconds:
                  type: integer
                  default: 60
                  description: How long a tripped target is skipped before being probed again
                  example: 60
      responses:
        '201':
          description: Model route created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModelRoute'
        '400':
          description: Bad request (invalid targets, attempt cap exceeded, unknown provider)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A model route with this name already exists in the project
        '500':
          description: Internal server error
  /api/v1/model-routes/{route_id}:
    get:
      tags:
        - Model Routes
      summary: Get a model route
      description: Returns a specific model route
      operationId: getModelRoute
      parameters:
        - name: route_id
          in: path
          required: true
          description: Model route ID
          schema:
            type: string
            example: route_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Model route details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModelRoute'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Model route not found
    put:
      tags:
        - Model Routes
      summary: Update a model route
      description: >-
        Updates a model route's name, targets, retry classes, or breaker configuration. Omitted fields are left
        unchanged.
      operationId: updateModelRoute
      parameters:
        - name: route_id
          in: path
          required: true
          description: Model route ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: New name (unique per project)
                targets:
                  type: array
                  minItems: 1
                  description: Replacement target list (ordered)
                  items:
                    $ref: '#/components/schemas/ModelRouteTarget'
                retry_on:
                  type: array
                  items:
                    type: string
                    enum:
                      - provider_error
                      - timeout
                      - rate_limited
                failure_threshold:
                  type: integer
                cooldown_seconds:
                  type: integer
      responses:
        '200':
          description: Model route updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModelRoute'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Model route not found
        '409':
          description: A model route with this name already exists in the project
    delete:
      tags:
        - Model Routes
      summary: Delete a model route
      description: >-
        Deletes a model route. Returns 409 when an agent still references it — a routed agent has no pinned provider to
        fall back on, so the reference must be repointed or the agent deleted first.
      operationId: deleteModelRoute
      parameters:
        - name: route_id
          in: path
          required: true
          description: Model route ID
          schema:
            type: string
      responses:
        '204':
          description: Model route deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Model route not found
        '409':
          description: The model route is still referenced by one or more agents
  /.well-known/oauth-authorization-server:
    get:
      tags:
        - OAuth
      summary: Get authorization server metadata
      description: |
        Returns the [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) Authorization Server Metadata
        document: where `/authorize`, `/token` and `/register` are, which grants and PKCE methods are
        supported, and which scopes exist.

        Discovery precedes authentication by definition, so this endpoint takes no credential.
      operationId: getOauthAuthorizationServerMetadata
      security: []
      responses:
        '200':
          description: Authorization server metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthAuthorizationServerMetadata'
  /.well-known/oauth-protected-resource:
    get:
      tags:
        - OAuth
      summary: Get protected resource metadata
      description: |
        Returns the [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) Protected Resource Metadata
        document, naming the authorization server that guards `/mcp`. An unauthenticated request to
        `/mcp` answers `401` with a `WWW-Authenticate: Bearer resource_metadata="…"` header pointing
        here, which is how an OAuth-aware MCP client bootstraps the flow.
      operationId: getOauthProtectedResourceMetadata
      security: []
      responses:
        '200':
          description: Protected resource metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthProtectedResourceMetadata'
  /register:
    post:
      tags:
        - OAuth
      summary: Register an OAuth client
      description: |
        [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) Dynamic Client Registration — the
        self-serve half of the flow, which lets a client onboard itself with no operator step.

        A client that registers with `token_endpoint_auth_method: none` is public and receives no
        secret; any other method yields a `client_secret` that does not expire.
      operationId: registerOauthClient
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OauthClientRegistrationRequest'
      responses:
        '201':
          description: Client registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthClientRegistrationResponse'
        '400':
          description: '`redirect_uris` is missing or is not an array of strings'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthErrorResponse'
  /authorize:
    get:
      tags:
        - OAuth
      summary: Start the authorization code flow
      description: |
        The authorization endpoint. PKCE is mandatory — `code_challenge` is required and
        `code_challenge_method` must be `S256`; OAuth 2.1 forbids `plain`.

        A user who has not yet consented is redirected to the consent screen at
        `/app/oauth/consent`; once consent is recorded, a second call to this endpoint redirects to
        `redirect_uri` with a single-use `code`. Parameter errors that can be attributed to a
        registered `redirect_uri` are reported as a redirect carrying `error`, per RFC 6749;
        an unknown `client_id` or an unregistered `redirect_uri` answers `400` directly, because
        there is no trustworthy place to send the user.
      operationId: authorizeOauthClient
      security: []
      parameters:
        - name: client_id
          in: query
          required: true
          description: The registered client identifier.
          schema:
            type: string
        - name: redirect_uri
          in: query
          required: true
          description: Must exactly match one of the client's registered redirect URIs.
          schema:
            type: string
            format: uri
        - name: response_type
          in: query
          required: true
          description: Must be `code`.
          schema:
            type: string
            enum:
              - code
        - name: code_challenge
          in: query
          required: true
          description: PKCE challenge, the base64url-encoded SHA-256 of the verifier.
          schema:
            type: string
        - name: code_challenge_method
          in: query
          required: true
          description: Must be `S256`.
          schema:
            type: string
            enum:
              - S256
        - name: scope
          in: query
          required: false
          description: Space-separated scopes, e.g. `mcp:access`.
          schema:
            type: string
        - name: state
          in: query
          required: false
          description: Opaque value echoed back on the redirect. Optional.
          schema:
            type: string
      responses:
        '302':
          description: |
            Redirect to `redirect_uri` with `code` and `state`, to the consent screen when consent is
            still needed, or back to `redirect_uri` with `error` for a parameter the client can fix.
          headers:
            Location:
              description: Where the user agent is sent next.
              schema:
                type: string
                format: uri
        '400':
          description: '`client_id` is missing or unknown, or `redirect_uri` is not registered for it'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthErrorResponse'
  /token:
    post:
      tags:
        - OAuth
      summary: Exchange a code or refresh token for an access token
      description: |
        The token endpoint, serving the `authorization_code` and `refresh_token` grants. The client
        authenticates with `client_secret_basic`, `client_secret_post`, or `none` for a public client.

        The access token it returns is a SOAT-issued JWT: send it as `Authorization: Bearer <token>`
        to `/mcp` and to the REST API. Refresh tokens are single-use and rotated on every exchange.
      operationId: createOauthToken
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OauthTokenRequest'
      responses:
        '200':
          description: Access token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthTokenResponse'
        '400':
          description: Invalid grant, missing parameter, or unsupported `grant_type`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthErrorResponse'
        '401':
          description: Client authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OauthErrorResponse'
  /api/v1/orchestrations:
    post:
      tags:
        - Orchestrations
      summary: Create an orchestration
      description: Creates a new orchestration (pipeline) definition in the project.
      operationId: createOrchestration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrchestrationRequest'
      responses:
        '201':
          description: Orchestration created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    get:
      tags:
        - Orchestrations
      summary: List orchestrations
      description: Returns orchestrations accessible to the caller.
      operationId: listOrchestrations
      parameters:
        - in: query
          name: project_id
          schema:
            type: string
          description: Filter by project public ID
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of orchestrations
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Orchestration'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/orchestrations/validate:
    post:
      tags:
        - Orchestrations
      summary: Validate an orchestration graph
      description: >
        Statically validates an orchestration graph without persisting anything. Checks that every node has its required
        field, node ids are unique, edges reference existing nodes, the graph is acyclic (unless it contains a loop
        node), and every `input_mapping` `{"var": "..."}` reference resolves to a state key written by an upstream node
        or seeded by `input_schema`. Returns blocking `errors` and non-blocking `warnings` (e.g. a state key only
        written on a conditional branch). The same `errors` checks are enforced on create and update, which fail with
        `400` when any error is present.
      operationId: validateOrchestration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateOrchestrationRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'
        '401':
          description: Unauthorized
  /api/v1/orchestrations/queue/stats:
    get:
      tags:
        - Orchestrations
      summary: Get orchestration queue stats
      description: >
        Returns a point-in-time snapshot of the orchestration run queue: how many tasks are waiting to be claimed
        (`queue_depth`), how many are currently claimed with a valid lease (`claimed_tasks`), the age of the oldest
        waiting task, recent claim-latency percentiles over a rolling in-process window, and a per-project breakdown.
        Intended for admin/operator policies; guarded by `orchestrations:GetQueueStats`. A project-scoped caller sees
        only their own projects under `per_project`.
      operationId: getQueueStats
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Queue stats snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueStats'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/orchestrations/{orchestration_id}:
    get:
      tags:
        - Orchestrations
      summary: Get an orchestration
      description: Returns the orchestration with nodes and edges.
      operationId: getOrchestration
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
      responses:
        '200':
          description: Orchestration details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
    patch:
      tags:
        - Orchestrations
      summary: Update an orchestration
      description: Partially updates an orchestration definition.
      operationId: updateOrchestration
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrchestrationRequest'
      responses:
        '200':
          description: Updated orchestration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
    delete:
      tags:
        - Orchestrations
      summary: Delete an orchestration
      description: Deletes an orchestration definition and all its runs.
      operationId: deleteOrchestration
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
  /api/v1/orchestrations/{orchestration_id}/versions:
    get:
      tags:
        - Orchestrations
      summary: List an orchestration's graph versions
      description: >
        Returns the orchestration's archived graphs, newest first. A version is written on create and on every
        subsequent write that changes the graph (`nodes`, `edges`, `state_schema`, `input_schema`) — through the REST
        API or a formation apply alike. Metadata-only edits (name, description) do not archive a version. See
        [Versioning](/docs/modules/orchestrations#versioning).
      operationId: listOrchestrationVersions
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of orchestration versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrchestrationVersion'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Orchestration not found
  /api/v1/orchestrations/{orchestration_id}/versions/{version}:
    get:
      tags:
        - Orchestrations
      summary: Fetch an archived orchestration version
      description: >
        Returns the exact graph a given version describes. Every run records the version it started on in
        `orchestration_version` and executes that graph for its whole life, so this is how you read the topology a run
        actually took — including a run whose orchestration has been rewired since.
      operationId: getOrchestrationVersion
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Archived orchestration version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationVersion'
        '400':
          description: Bad Request — version is not a positive integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
  /api/v1/orchestrations/{orchestration_id}/versions/{version}/restore:
    post:
      tags:
        - Orchestrations
      summary: Restore an archived orchestration graph
      description: >
        Writes an archived version's graph back as the orchestration's live definition, which archives it again as a
        **new** version rather than rewinding the counter — so a run pinned to any version in between still resolves the
        graph it started on.


        The restore runs through the ordinary update path, so the archived graph goes through the same static validation
        as an authored one. Node resource references (`agent_id`, `tool_id`, `orchestration_id`) resolve when a run
        reaches the node, so a target deleted since the snapshot was taken restores cleanly and surfaces as a failed run
        rather than a `400`. Restoring the graph the orchestration already holds is a no-op and archives nothing. Runs
        already in flight are unaffected either way — a restore is an ordinary edit, and pinning is what keeps it from
        reaching them.
      operationId: restoreOrchestrationVersion
      parameters:
        - $ref: '#/components/parameters/orchestration_id'
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreOrchestrationVersionRequest'
      responses:
        '200':
          description: The orchestration, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Orchestration'
        '400':
          description: Bad Request — version is not a positive integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
  /api/v1/orchestration-runs:
    post:
      tags:
        - Orchestrations
      summary: Start an orchestration run
      description: >-
        Creates a new run for the orchestration named by orchestration_id. By default the run executes durably in the
        background: the response returns immediately with status "queued" (a worker then claims it and moves it to
        "running") and progress is observed via get-orchestration-run or run lifecycle webhook events
        (orchestration_runs.started/awaiting_input/succeeded/failed). Delay and poll waits park the run as "sleeping"
        and are woken by a background scheduler, surviving restarts. Pass wait=true to block until the run reaches a
        terminal or awaiting_input state.
      operationId: startOrchestrationRun
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartRunRequest'
      responses:
        '201':
          description: Run created and executed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '400':
          description: >-
            Validation error (e.g. a `tool_context` key that cannot become a header, or `metadata` that is not a JSON
            object). No run is created.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Orchestration not found
    get:
      tags:
        - Orchestrations
      summary: List orchestration runs
      description: >-
        Returns orchestration runs the caller can access, optionally filtered by orchestration, by parent run, or by
        whether the run has a parent at all.


        Note when aggregating: a run's `usage` covers its whole subtree, so summing it over a list that contains both a
        parent and its children counts the children more than once. Pass `nested=false` to sum over runs a caller
        started.
      operationId: listOrchestrationRuns
      parameters:
        - name: orchestration_id
          in: query
          required: false
          description: Filter by orchestration public ID (orch_...)
          schema:
            type: string
        - name: parent_orchestration_run_id
          in: query
          required: false
          description: >-
            Filter to the runs one specific parent run's `loop` / `sub_orchestration` nodes started (run_...). This is
            how a caller holding a parent names the individual children behind its `usage`.
          schema:
            type: string
        - name: nested
          in: query
          required: false
          description: >-
            Filter by whether the run was started by another run. `false` returns only the runs a caller started (no
            parent), which is the set to sum `usage` over; `true` returns only the runs a `loop` / `sub_orchestration`
            node started, across every parent. Omit to return both.


            Contradicting `parent_orchestration_run_id` with `nested=false` is a `400`; any value other than `true` or
            `false` is a `400`.
          schema:
            type: boolean
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of runs
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrchestrationRun'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/orchestration-runs/{orchestration_run_id}/cancel:
    post:
      tags:
        - Orchestrations
      summary: Cancel an orchestration run
      description: Cancels a run that has not yet reached a terminal state.
      operationId: cancelOrchestrationRun
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      responses:
        '200':
          description: Cancelled run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run is already in a terminal state
  /api/v1/orchestration-runs/{orchestration_run_id}/human-input:
    post:
      tags:
        - Orchestrations
      summary: Submit human input
      description: Provides human input to a run that is awaiting_input at a human node.
      operationId: submitHumanInput
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HumanInputRequest'
      responses:
        '200':
          description: Run after processing human input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '400':
          description: Invalid input
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run is not awaiting input
  /api/v1/orchestration-runs/{orchestration_run_id}/resume:
    post:
      tags:
        - Orchestrations
      summary: Resume an orchestration run
      description: >-
        Re-drives an awaiting_input orchestration run from its last checkpoint. This does not satisfy the pause itself —
        it carries no node_id or payload, so a run parked on a human or webhook-receive node re-parks on the same node.
        Use submit-human-input to supply the awaited payload and advance the run.
      operationId: resumeOrchestrationRun
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      responses:
        '200':
          description: Resumed run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '409':
          description: Run is not awaiting input
  /api/v1/orchestration-runs/{orchestration_run_id}:
    get:
      tags:
        - Orchestrations
      summary: Get an orchestration run
      description: Returns the status, state, and artifacts of a specific run.
      operationId: getOrchestrationRun
      parameters:
        - $ref: '#/components/parameters/orchestration_run_id'
      responses:
        '200':
          description: Run details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrationRun'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
  /api/v1/policies:
    get:
      tags:
        - Policies
      summary: List all policies
      description: >
        Returns global policies. Requires admin role. Pass user_id to list only the policies attached to that user
        (replaces the former per-user policies endpoint).
      operationId: listPolicies
      parameters:
        - name: user_id
          in: query
          required: false
          description: Return only policies attached to this user (user_...)
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of policies
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PolicyRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
    post:
      tags:
        - Policies
      summary: Create a policy
      description: Creates a new global policy. Requires admin role.
      operationId: createPolicy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - document
              properties:
                name:
                  type: string
                  example: ReadOnlyAccess
                description:
                  type: string
                  example: Allows read-only access to all resources
                document:
                  $ref: '#/components/schemas/PolicyDocument'
      responses:
        '201':
          description: Policy created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyRecord'
        '400':
          description: Bad request (invalid policy document)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
  /api/v1/policies/{policy_id}:
    get:
      tags:
        - Policies
      summary: Get a policy
      description: Returns details of a specific policy. Requires admin role.
      operationId: getPolicy
      parameters:
        - name: policy_id
          in: path
          required: true
          description: Policy public ID (pol_ prefix)
          schema:
            type: string
            example: pol_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Policy details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyRecord'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '404':
          description: Policy not found
    put:
      tags:
        - Policies
      summary: Update a policy
      description: Updates an existing global policy. Requires admin role.
      operationId: updatePolicy
      parameters:
        - name: policy_id
          in: path
          required: true
          description: Policy public ID (pol_ prefix)
          schema:
            type: string
            example: pol_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - document
              properties:
                name:
                  type: string
                description:
                  type: string
                document:
                  $ref: '#/components/schemas/PolicyDocument'
      responses:
        '200':
          description: Policy updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyRecord'
        '400':
          description: Bad request (invalid policy document)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '404':
          description: Policy not found
    delete:
      tags:
        - Policies
      summary: Delete a policy
      description: Deletes a global policy. Requires admin role.
      operationId: deletePolicy
      parameters:
        - name: policy_id
          in: path
          required: true
          description: Policy public ID (pol_ prefix)
          schema:
            type: string
            example: pol_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Policy deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '404':
          description: Policy not found
  /api/v1/projects:
    get:
      tags:
        - Projects
      summary: List projects
      description: >
        Lists projects accessible to the caller. - JWT admin: returns all projects. - JWT regular user: returns only
        projects granted by the user's policies. - API key scoped to a project: returns only that project.
      operationId: listProjects
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of projects
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProjectRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
    post:
      tags:
        - Projects
      summary: Create a project
      description: Creates a new project. Requires admin role.
      operationId: createProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  example: My Project
      responses:
        '201':
          description: Project created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: proj_V1StGXR8Z5jdHi6B
                  name:
                    type: string
                  created_at:
                    type: string
                    format: date-time
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '500':
          description: Internal server error
  /api/v1/projects/{project_id}:
    get:
      tags:
        - Projects
      summary: Get a project
      description: Returns details of a specific project.
      operationId: getProject
      parameters:
        - name: project_id
          in: path
          required: true
          description: Project public ID (proj_ prefix)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Project details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRecord'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Project not found
    patch:
      tags:
        - Projects
      summary: Update a project
      description: >-
        Updates a project's name, its attached guardrails (`guardrail_ids` — the project-scope baseline governing every
        tool call by every agent in the project), its orchestration concurrency limit (`max_concurrent_runs`), its
        continuation-chain ceiling (`max_chain_generations`), its orchestration nesting bound (`max_run_depth`), its
        inherited model route (`default_model_route_id`), its read-auditing opt-in (`audit_reads_enabled`), its
        trace-content retention window (`trace_content_retention_days`), and/or its zero-retention setting
        (`trace_content_mode`). At least one field is required. Requires admin role. Detaching a guardrail (removing an
        id) additionally requires guardrails:DetachGuardrail.
      operationId: updateProject
      parameters:
        - name: project_id
          in: path
          required: true
          description: Project public ID (proj_ prefix)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: Renamed Project
                guardrail_ids:
                  x-soat-ref: guardrails
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: Guardrails attached at the project scope.
                max_concurrent_runs:
                  type: integer
                  nullable: true
                  minimum: 1
                  example: 5
                  description: >-
                    Maximum orchestration runs of this project driven at once. `null` clears the limit (unlimited);
                    otherwise an integer >= 1. Enforced at queue claim time — excess runs stay queued until a slot
                    frees.
                max_chain_generations:
                  type: integer
                  nullable: true
                  minimum: 1
                  example: 25
                  description: >-
                    Generations one continuation chain in this project may hold before the platform stops resuming it.
                    `null` clears the project's ceiling, leaving the deployment-wide
                    `MAX_CONTINUATION_CHAIN_GENERATIONS`; otherwise an integer >= 1. The effective budget is the
                    smallest of the deployment's ceiling, this one, and the agent's own `max_chain_generations` stop
                    condition, so an agent author can be stricter than this number but never exceed it.
                max_run_depth:
                  type: integer
                  nullable: true
                  minimum: 1
                  example: 5
                  description: >-
                    `loop` / `sub_orchestration` nesting levels a run tree in this project may reach before the engine
                    refuses to start the next child. `null` clears the project's bound, leaving the deployment-wide
                    `MAX_ORCHESTRATION_RUN_DEPTH` (default 10); otherwise an integer >= 1. The effective bound is the
                    smaller of the two, so this can only be stricter than the deployment's — which is what makes it a
                    bound a graph author cannot opt out of. A refused child fails its parent run with
                    `ORCHESTRATION_RUN_DEPTH_LIMIT`.
                default_model_route_id:
                  x-soat-ref: model-routes
                  type: string
                  nullable: true
                  example: route_V1StGXR8Z5jdHi6B
                  description: >-
                    Model route inherited by consumers in this project that bind neither `model_route_id` nor
                    `ai_provider_id`. The route must belong to this project (`400` otherwise). `null` clears the
                    default, which is refused with `409` while any consumer inherits it — repointing it to another route
                    is always allowed and immediately changes which targets those consumers use.
                audit_reads_enabled:
                  type: boolean
                  example: true
                  description: >-
                    Opt the project into read auditing. When true, `GET` requests that name this project are recorded in
                    the audit log alongside mutations. Defaults to `false`.
                trace_content_retention_days:
                  type: integer
                  nullable: true
                  minimum: 1
                  example: 90
                  description: >-
                    How long trace and generation content is kept before the daily retention sweep content-purges it.
                    `null` (the default) disables retention; otherwise an integer >= 1. The sweep uses the same purge
                    path as `DELETE /traces/{trace_id}/content`, so the row survives as an auditable skeleton with
                    `content_redacted_at` set.
                trace_content_mode:
                  type: string
                  enum:
                    - full
                    - none
                  example: none
                  description: >-
                    Whether trace and generation content is persisted at all. `full` (the default) stores it; `none` is
                    zero-retention — content is never written, for every agent in the project. An agent may tighten this
                    to `none` on its own but cannot loosen a `none` project back to `full`.
      responses:
        '200':
          description: Project updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRecord'
        '400':
          description: Invalid request body
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '404':
          description: Project not found
        '409':
          description: '`default_model_route_id` cannot be cleared while consumers inherit it (PROJECT_DEFAULT_ROUTE_INHERITED).'
    delete:
      tags:
        - Projects
      summary: Delete a project
      description: >
        Deletes a project. Requires admin role. Fails with `409` if the project has any dependent resource, unless
        `force=true` is passed, in which case those resources are deleted along with the project.


        Every project-scoped resource counts: agents, ai providers, model routes, tools, ingestion rules, actors, chats,
        conversations, sessions, generations, traces, datasets, evals, workflows, tasks, triggers, orchestrations and
        their runs, formations, memories, secrets, files, guardrails, quotas, usage history — and the activity,
        approval, exception and guardrail-evaluation records a project accumulates while it runs. Audit log entries are
        the one exception: they outlive the project, keeping their record with `project_id` cleared.
      operationId: deleteProject
      parameters:
        - name: project_id
          in: path
          required: true
          description: Project public ID (proj_ prefix)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: force
          in: query
          required: false
          description: >
            When `true`, deletes all of the project's dependent resources instead of returning `409
            PROJECT_HAS_DEPENDENTS`.
          schema:
            type: boolean
            default: false
      responses:
        '204':
          description: Project deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '404':
          description: Project not found
        '409':
          description: Project has dependent resources (pass `force=true` to delete anyway)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/projects/{project_id}/prices:
    get:
      tags:
        - Projects
      summary: List a project's price rows
      description: >
        Returns the project's per-provider-slug price rows — the middle pricing tier that covers every one of the
        project's instances of a given provider slug. At cost time a per-provider-instance override wins over these, and
        these win over the global default. Authorized by the caller's access to the project.
      operationId: getProjectPrices
      parameters:
        - name: project_id
          in: path
          required: true
          description: Project public ID (proj_ prefix)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: The project's price rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPricesResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Project not found
    put:
      tags:
        - Projects
      summary: Upsert a project's price rows
      description: >
        Upserts project + provider-slug price rows, keyed on (provider, model, effective_from). A row covers all of the
        project's instances of that provider slug. Authorized by the caller's access to the project. `effective_from`
        must be in the future once the (provider, model, component) is priced — past prices are immutable, so
        corrections ship as new future-dated rows. A first price, with no row for the project or the global default, may
        be dated now or earlier.
      operationId: updateProjectPrices
      parameters:
        - name: project_id
          in: path
          required: true
          description: Project public ID (proj_ prefix)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertProjectPricesRequest'
      responses:
        '200':
          description: The upserted price rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPricesResponse'
        '400':
          description: Bad Request (e.g. non-future effective_from)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Project not found
  /api/v1/quotas:
    get:
      tags:
        - Quotas
      summary: List quotas
      description: Returns the quotas defined in a project
      operationId: listQuotas
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of quotas
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Quota'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Quotas
      summary: Create a quota
      description: >-
        Creates a project-scoped quota. `requests` is valid for `scope: project`/`api_key`; `tokens` and `cost_usd` are
        valid for `scope: project`/`agent`/`actor`. Any other scope/metric pair is rejected with 400 (no attribution
        exists to enforce it). An `actor` quota caps one end user's spend, matched from the generation's session; a null
        `scope_ref` means one budget *per* actor rather than a pooled project total. A duplicate quota (same project,
        scope, scope_ref, metric, window) is rejected with 409.
      operationId: createQuota
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scope
                - metric
                - window
                - limit
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                scope:
                  type: string
                  enum:
                    - project
                    - api_key
                    - agent
                    - actor
                  description: The scope the quota applies to
                scope_ref:
                  type: string
                  nullable: true
                  description: >-
                    Public id of the api key / agent / actor the quota applies to. For `api_key` and `agent` scope, NULL
                    means all entities of that scope type in the project. For `actor` scope, NULL means one budget *per*
                    actor — each end user gets their own allowance — rather than a pooled total across all actors.
                  example: key_V1StGXR8Z5jdHi6B
                metric:
                  type: string
                  enum:
                    - requests
                    - tokens
                    - cost_usd
                  description: The metric being capped
                  example: requests
                window:
                  type: string
                  enum:
                    - rolling_1m
                    - rolling_1h
                    - rolling_24h
                    - calendar_month
                  description: The window over which the metric is aggregated
                limit:
                  type: number
                  description: The cap. Must be a positive integer for requests/tokens; fractional values are allowed for cost_usd.
                  example: 600
                mode:
                  type: string
                  enum:
                    - enforce
                    - monitor
                  default: enforce
                  description: >-
                    enforce blocks with 429 (requests at the middleware, tokens/cost_usd at the pre-generation check);
                    monitor observes without blocking — a breach fires the quota.exceeded webhook and writes a
                    quotas:MonitorBreach audit entry, but the request is let through.
                  example: enforce
                on_unpriced:
                  type: string
                  enum:
                    - block
                    - allow
                  default: block
                  description: >-
                    Only for metric cost_usd (400 on any other metric). What an enforce quota does when the current
                    window is a pricing blackout — several metered llm_tokens events, none of them priced, so the
                    aggregate is 0 however much was actually spent. Platform meters such as compute_execution are read
                    for the aggregate but never for this verdict. block (the default) refuses new generations with 409
                    QUOTA_UNENFORCEABLE until pricing is configured; allow accepts the unmeasurable spend explicitly.
                    Either way a quota_unpriced exception is filed. monitor-mode quotas never block regardless.
                  example: block
      responses:
        '201':
          description: Quota created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quota'
        '400':
          description: Bad request (invalid scope/metric/window/mode/limit)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A matching quota already exists
        '500':
          description: Internal server error
  /api/v1/quotas/{quota_id}:
    get:
      tags:
        - Quotas
      summary: Get a quota
      description: Returns a specific quota, including current window usage
      operationId: getQuota
      parameters:
        - name: quota_id
          in: path
          required: true
          description: Quota ID
          schema:
            type: string
            example: quota_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Quota details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quota'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Quota not found
    patch:
      tags:
        - Quotas
      summary: Update a quota
      description: Updates a quota's limit and/or mode. Other fields are immutable.
      operationId: updateQuota
      parameters:
        - name: quota_id
          in: path
          required: true
          description: Quota ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                limit:
                  type: number
                  description: New limit
                  example: 1000
                mode:
                  type: string
                  enum:
                    - enforce
                    - monitor
                  description: New mode
                  example: monitor
                on_unpriced:
                  type: string
                  enum:
                    - block
                    - allow
                  description: >-
                    New pricing posture. Only for metric cost_usd (400 on any other metric); see the create operation
                    for what block and allow mean.
                  example: allow
      responses:
        '200':
          description: Quota updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quota'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Quota not found
    delete:
      tags:
        - Quotas
      summary: Delete a quota
      description: Deletes a quota and drops its window counters
      operationId: deleteQuota
      parameters:
        - name: quota_id
          in: path
          required: true
          description: Quota ID
          schema:
            type: string
      responses:
        '204':
          description: Quota deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Quota not found
  /api/v1/secrets:
    get:
      tags:
        - Secrets
      summary: List secrets
      description: Returns a list of secrets for a project
      operationId: listSecrets
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          description: Number of results per page
          schema:
            type: integer
            default: 25
        - name: offset
          in: query
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of secrets
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        has_value:
                          type: boolean
                          description: Whether an encrypted value is stored for this secret
                        project_id:
                          x-soat-ref: projects
                          type: string
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Secrets
      summary: Create a secret
      description: Creates a new encrypted secret in a project
      operationId: createSecret
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - value
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Secret name
                  example: DATABASE_PASSWORD
                value:
                  type: string
                  description: Secret value (will be encrypted)
                  example: supersecretpassword
      responses:
        '201':
          description: Secret created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  has_value:
                    type: boolean
                    description: Whether an encrypted value is stored for this secret
                  project_id:
                    x-soat-ref: projects
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '400':
          description: Bad request (missing required fields)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/secrets/{secret_id}:
    get:
      tags:
        - Secrets
      summary: Get a secret
      description: Returns a specific secret
      operationId: getSecret
      parameters:
        - name: secret_id
          in: path
          required: true
          description: Secret ID
          schema:
            type: string
            example: sec_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Secret details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  has_value:
                    type: boolean
                    description: Whether an encrypted value is stored for this secret
                  project_id:
                    x-soat-ref: projects
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Secret not found
    patch:
      tags:
        - Secrets
      summary: Update a secret
      description: Updates a secret's name and/or value
      operationId: updateSecret
      parameters:
        - name: secret_id
          in: path
          required: true
          description: Secret ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: New secret name
                value:
                  type: string
                  description: New secret value
      responses:
        '200':
          description: Secret updated successfully
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Secret not found
    delete:
      tags:
        - Secrets
      summary: Delete a secret
      description: Deletes a secret
      operationId: deleteSecret
      parameters:
        - name: secret_id
          in: path
          required: true
          description: Secret ID
          schema:
            type: string
      responses:
        '200':
          description: Secret deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Secret not found
  /api/v1/sessions:
    post:
      tags:
        - Sessions
      summary: Create a session
      description: >
        Creates a new session for the specified agent, along with the underlying conversation, so the caller only needs
        this single call to start interacting with the agent. No actor is created: pass `actor_id` to attach an existing
        actor as the session's end user. When it is omitted the session has no actor, and generations in it carry no
        end-user attribution — they are not billed to an actor in the usage meter and they match no `actor`-scoped
        quota.
      operationId: createSession
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSessionRequest'
      responses:
        '201':
          description: Session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRecord'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: An open session already exists for this actor (single_session_per_actor is enabled)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: SINGLE_SESSION_CONFLICT
                  message: An open session already exists for this actor.
                  hint: >-
                    The agent allows one open session per actor. Reuse the session named in `meta.session_id`, or close
                    it first.
                  docs_url: https://soat.ttoss.dev/docs/error-codes#single_session_conflict
                  meta:
                    session_id: sess_abc123
    get:
      tags:
        - Sessions
      summary: List sessions
      description: Returns sessions the caller can access, optionally filtered by agent, actor and status.
      operationId: listSessions
      parameters:
        - name: agent_id
          in: query
          required: false
          description: Filter by agent public ID
          schema:
            type: string
            example: agent_V1StGXR8Z5jdHi6B
        - name: actor_id
          in: query
          required: false
          description: Filter by actor public ID
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filter by session status (open, closed, or expired)
          schema:
            type: string
            enum:
              - open
              - closed
              - expired
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated list of sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SessionRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/sessions/{session_id}:
    get:
      tags:
        - Sessions
      summary: Get a session
      description: Returns details of a single session.
      operationId: getSession
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Session details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRecord'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Sessions
      summary: Update a session
      description: Updates the session name and/or status.
      operationId: updateSession
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSessionRequest'
      responses:
        '200':
          description: Updated session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRecord'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
        - Sessions
      summary: Delete a session
      description: >
        Deletes the session and its underlying conversation and messages. The session's actor is not deleted.
        Generations and traces produced by the session are not deleted either, since they are not linked to the session
        or conversation.
      operationId: deleteSession
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '204':
          description: Session deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/sessions/{session_id}/messages:
    post:
      tags:
        - Sessions
      summary: Add a user message
      description: >
        Saves a user message to the session. When autoGenerate is enabled on the session and no generation is currently
        in progress, generation is triggered automatically and the response mirrors GenerateSessionResponse. Otherwise
        returns the saved user message.
      operationId: addSessionMessage
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddSessionMessageRequest'
      responses:
        '200':
          description: Duplicate request — original message returned (idempotency_key matched)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddSessionMessageSaved'
        '201':
          description: User message saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddSessionMessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/sessions/{session_id}/generate:
    post:
      tags:
        - Sessions
      summary: Trigger agent generation
      description: >
        Triggers the agent to generate a response based on the current conversation. Background by default: returns `202
        Accepted` immediately while the generation runs. Pass ?wait=true to block and receive the assistant reply (or a
        requires_action status if the agent needs client tool outputs) in the response.
      operationId: generateSessionResponse
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - name: wait
          in: query
          required: false
          description: >-
            When omitted or `false` (default), generation runs in the background and `202 Accepted` is returned
            immediately. Pass `true` to block until the generation settles and receive the result.
          schema:
            type: boolean
            default: false
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateSessionRequest'
      responses:
        '200':
          description: Agent reply or requires_action (only when `?wait=true`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateSessionResponse'
        '202':
          description: Generation accepted and running in the background (default, when `wait` is omitted or `false`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - accepted
                  session_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Generation already in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '410':
          description: Session has expired due to inactivity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >
            Upstream AI provider error (AI_PROVIDER_ERROR). The error `meta` includes the `generation_id` and `trace_id`
            of the failed generation for post-mortem debugging via GET /api/v1/generations/{generation_id}.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/sessions/{session_id}/tool-outputs:
    post:
      tags:
        - Sessions
      summary: Submit tool outputs
      description: >
        Submits client tool outputs for a generation that returned requires_action. The agent continues its loop and
        returns the final or next requires_action result.
      operationId: submitSessionToolOutputs
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitSessionToolOutputsRequest'
      responses:
        '200':
          description: Generation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendSessionMessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/sessions/{session_id}/fork:
    post:
      tags:
        - Sessions
      summary: Fork a session
      description: >
        Branches a new session from a point in this session's history: same context, different continuation.


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


        The fork is created **inert**: `auto_generate` is false and no generation is triggered. Drive it with the normal
        message and generate endpoints. The fork has no actor — attach one only if the branch is meant to be driven by
        the same end user, since `single_session_per_actor` agents allow one open session per actor.
      operationId: forkSession
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForkSessionRequest'
      responses:
        '201':
          description: Fork created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRecord'
        '400':
          description: >-
            `fork_at_position` names no message in the parent conversation, or `agent_id` is unknown or belongs to
            another project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: VALIDATION_FAILED
                  message: fork_at_position 9 does not exist in the parent conversation.
                  hint: >-
                    Fix the request and retry. Unknown fields are rejected outright, so compare the payload against the
                    operation in `/openapi.json`; `meta` names the offending field when the check can identify one.
                  docs_url: https://soat.ttoss.dev/docs/error-codes#validation_failed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/sessions/{session_id}/forks:
    get:
      tags:
        - Sessions
      summary: List a session's forks
      description: >
        Returns the sessions forked directly from this one. One level of lineage: a fork of a fork is listed under its
        own parent.
      operationId: listSessionForks
      parameters:
        - $ref: '#/components/parameters/SessionId'
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated list of forks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SessionRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/sessions/{session_id}/tags:
    get:
      tags:
        - Sessions
      summary: Get session tags
      description: Returns the session's tags object.
      operationId: getSessionTags
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Session tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
        - Sessions
      summary: Replace session tags
      description: Replaces all tags on the session.
      operationId: replaceSessionTags
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Updated tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
        - Sessions
      summary: Merge session tags
      description: Merges the provided tags into the session's existing tags.
      operationId: mergeSessionTags
      parameters:
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        '200':
          description: Updated tags
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/tasks:
    get:
      description: >-
        Lists tasks (the board query). Filter by workflow, state, status, or assignee — `GET
        /tasks?workflow_id=...&state=...` is one board column.
      tags:
        - Tasks
      summary: List tasks
      operationId: listTasks
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
        - name: workflow_id
          in: query
          required: false
          schema:
            type: string
        - name: state
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - open
              - closed
        - name: assignee
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: A list of tasks
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      description: >-
        Creates a task bound to a workflow. By default the task is placed in the workflow's initial state; passing
        `state` places it directly in that named state instead — an alternate entry point for starting a task mid-flow
        (e.g. "a new recorte for an existing theme by id"), rather than re-submitting from the initial state and hoping
        a guard or similarity gate recognizes it. Entering the resulting state, initial or named, behaves identically:
        that state's `on_enter` automation fires and its `stalled_after` clock arms.
      tags:
        - Tasks
      summary: Create a task
      operationId: createTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
      responses:
        '201':
          description: Task created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          description: >-
            Bad request — invalid payload (`TASK_PAYLOAD_INVALID`), or `state` does not name a declared state of the
            workflow (`TASK_STATE_NOT_FOUND`)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
  /api/v1/tasks/{task_id}:
    get:
      description: Retrieves a task, including its active dispatch and automation status.
      tags:
        - Tasks
      summary: Get a task
      operationId: getTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Task details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Task not found
    patch:
      description: >-
        Updates a task's payload, title, or assignee. `state` is never directly writable — move it with a transition;
        sending a `state` field is rejected as an unknown field (`VALIDATION_FAILED`). `payload` is shallow-merged over
        the existing payload (PATCH semantics): keys the request omits are preserved. The payload is caller-owned; the
        automation result lives in the read-only `last_result` field, which no patch can reach. The merged payload is
        validated against the workflow's `payload_schema`.
      tags:
        - Tasks
      summary: Update a task
      operationId: updateTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskRequest'
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          description: Bad request (invalid payload)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Task not found
    delete:
      description: Deletes a task. Its transition history cascades.
      tags:
        - Tasks
      summary: Delete a task
      operationId: deleteTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Task deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Task not found
  /api/v1/tasks/{task_id}/transitions:
    post:
      description: >-
        Fires a named transition on a task. The transition must exist in the workflow and be valid from the task's
        current state; its guard must pass. This is the single path every state change routes through. A transition
        declaring `requires_approval` does not move the task — it parks a pending ApprovalItem and returns the task with
        `pending_transition` set; the move applies only when the approval is approved.
      tags:
        - Tasks
      summary: Transition a task
      operationId: transitionTask
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransitionTaskRequest'
      responses:
        '200':
          description: The task after the transition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          description: The transition does not exist, is not valid, or its guard rejected the move
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Task not found
        '409':
          description: A concurrent transition made this one invalid, or the task is closed
  /api/v1/tasks/{task_id}/history:
    get:
      description: Returns the append-only transition history of a task.
      tags:
        - Tasks
      summary: Get task history
      operationId: getTaskHistory
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The task's transition history, oldest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskTransition'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Task not found
  /api/v1/tools:
    post:
      tags:
        - Tools
      summary: Create a tool
      description: Creates a new tool in the project.
      operationId: createTool
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateToolRequest'
            examples:
              http:
                summary: HTTP tool
                value:
                  name: get-weather
                  type: http
                  description: Fetches current weather for a city
                  parameters:
                    type: object
                    properties:
                      city:
                        type: string
                    required:
                      - city
                  execute:
                    url: https://api.weather.example/v1/current?city={city}
              http_with_output_mapping:
                summary: HTTP tool reshaping its result with output_mapping
                value:
                  name: transcribe-audio
                  type: http
                  description: Transcribes an audio file and returns the bare text
                  parameters:
                    type: object
                    properties:
                      file:
                        type: string
                    required:
                      - file
                  execute:
                    url: https://api.x.ai/v1/stt
                    method: POST
                    body_mode: multipart
                  output_mapping:
                    var: output.text
              http_aws_sigv4:
                summary: HTTP tool signed with AWS Signature Version 4
                value:
                  name: get-s3-object
                  type: http
                  description: Reads an object from an S3 bucket
                  parameters:
                    type: object
                    properties:
                      key:
                        type: string
                    required:
                      - key
                  execute:
                    url: https://my-bucket.s3.us-east-1.amazonaws.com/{key}
                    method: GET
                    auth:
                      type: aws_sigv4
                      region: us-east-1
                      service: s3
                      access_key_id: '{{secret:sec_awsKeyId}}'
                      secret_access_key: '{{secret:sec_awsSecret}}'
              http_gcp_service_account:
                summary: HTTP tool authenticated as a GCP service account
                value:
                  name: create-bigquery-job
                  type: http
                  description: Submits a BigQuery job
                  parameters:
                    type: object
                    properties:
                      query:
                        type: string
                    required:
                      - query
                  execute:
                    url: https://bigquery.googleapis.com/bigquery/v2/projects/my-gcp-project/jobs
                    method: POST
                    auth:
                      type: gcp_service_account
                      credentials: '{{secret:sec_gcpServiceAccount}}'
                      scopes:
                        - https://www.googleapis.com/auth/bigquery
              client:
                summary: Client tool
                value:
                  name: show-dialog
                  type: client
                  description: Displays a confirmation dialog to the user
                  parameters:
                    type: object
                    properties:
                      message:
                        type: string
              builtin:
                summary: SOAT platform tool
                value:
                  name: builtin-files
                  type: builtin
                  actions:
                    - files:ListFiles
                    - files:GetFile
              mcp_scoped:
                summary: Read-only MCP tool (allowlisted to a subset of actions)
                value:
                  name: oneclick
                  type: mcp
                  mcp:
                    url: https://mcp.oneclick.example/sse
                  actions:
                    - list_campaigns
                    - get_campaign
              mcp_denylist:
                summary: Read-only MCP tool (whole surface minus write actions)
                value:
                  name: oneclick
                  type: mcp
                  mcp:
                    url: https://mcp.oneclick.example/sse
                  denied_actions:
                    - create_optimization
                    - update_optimization
                    - deactivate_all_optimizations
              pipeline:
                summary: Pipeline tool (compute → persist)
                value:
                  name: compute-and-save
                  type: pipeline
                  description: Computes a sum and persists the result
                  parameters:
                    type: object
                    properties:
                      x:
                        type: number
                      'y':
                        type: number
                    required:
                      - x
                      - 'y'
                  pipeline:
                    steps:
                      - id: compute
                        tool_id: tool_calc
                        action: add
                        input:
                          a:
                            var: input.x
                          b:
                            var: input.y
                      - id: persist
                        tool_id: tool_save_record
                        input:
                          value:
                            var: steps.compute.sum
                    output:
                      saved_id:
                        var: steps.persist.id
      responses:
        '201':
          description: Tool created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tool'
        '400':
          description: Bad Request
          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'
    get:
      tags:
        - Tools
      summary: List tools
      description: Returns all tools in the project.
      operationId: listTools
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
          description: Project public ID to filter by
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of tools
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tool'
                  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/tools/{tool_id}:
    get:
      tags:
        - Tools
      summary: Get a tool
      description: Returns a single tool by ID.
      operationId: getTool
      parameters:
        - name: tool_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Tool
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tool'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Tools
      summary: Update a tool
      description: Updates an existing tool.
      operationId: updateTool
      parameters:
        - name: tool_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateToolRequest'
      responses:
        '200':
          description: Tool updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tool'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Tools
      summary: Delete a tool
      description: Deletes a tool by ID.
      operationId: deleteTool
      parameters:
        - name: tool_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/tools/{tool_id}/call:
    post:
      tags:
        - Tools
      summary: Call a tool
      description: >
        Directly invokes a tool and returns its output. Supported for `http`, `builtin`, `mcp`, and `pipeline` tools.
        `client` tools cannot be invoked server-side and will return 422. A `pipeline` tool runs its declared steps in
        order and returns the mapped `output` (or the last step's output); `action` is ignored and `input` is the
        pipeline input.

        For `builtin` and `mcp` tools the `action` field is required and identifies which action (SOAT) or tool name
        (MCP) to invoke. For `http` tools `action` is ignored. When a `builtin` or `mcp` tool declares an `actions`
        allowlist, an action outside it is rejected with `400 VALIDATION_FAILED` ("not available on this tool") before
        any outbound request is made.

        `preset_parameters` stored on the tool are pinned over the caller-supplied `input` before execution: a key the
        tool presets keeps its preset value even when `input` sets it. Keys the presets do not name are taken from
        `input` as sent.
      operationId: callTool
      parameters:
        - name: tool_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallToolRequest'
            examples:
              builtin_list_documents:
                summary: Call a SOAT list-documents action
                value:
                  action: list-documents
                  input:
                    project_id: proj_abc123
              http_tool:
                summary: Call an HTTP tool
                value:
                  input:
                    city: London
              mcp_tool:
                summary: Call an MCP tool
                value:
                  action: get_weather
                  input:
                    location: Paris
      responses:
        '200':
          description: Tool output
          content:
            application/json:
              schema:
                description: >-
                  The raw output returned by the tool — any JSON value (object, array, string, number, boolean). `null`
                  when the tool produced no output — a `builtin` action answering `204 No Content`, for instance.
        '400':
          description: Bad Request — invalid input or unknown action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: >
            Forbidden — the caller lacks permission, or the tool's target is blocked by the deployment's egress policy
            (TOOL_EGRESS_BLOCKED). An `http`/`mcp` tool may only reach publicly routable addresses unless the
            destination is listed in the server's TOOL_EGRESS_ALLOWED_HOSTS; the check runs against the resolved address
            and on every redirect hop, so `meta.tool_address` names the address that was refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Tool not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Unprocessable — tool type cannot be invoked server-side
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >
            Upstream tool target error (TOOL_HTTP_ERROR). Returned when an `http`-type tool's target responds with a
            non-2xx status. The error `meta` carries the real upstream `tool_status_code`, `tool_response_body`,
            `tool_url`, and `tool_method`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/traces:
    get:
      tags:
        - Traces
      summary: List traces
      description: Returns a paginated list of execution traces for the project.
      operationId: listTraces
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
          description: Project public ID to filter by
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of traces
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Trace'
                  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/traces/{trace_id}:
    get:
      tags:
        - Traces
      summary: Get a trace
      description: Returns a single trace by ID.
      operationId: getTrace
      parameters:
        - name: trace_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the trace
      responses:
        '200':
          description: Trace details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Trace'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Trace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/traces/{trace_id}/tree:
    get:
      tags:
        - Traces
      summary: Get trace tree
      description: >
        Returns the full execution tree rooted at the given trace (or its root if the given trace is a child). Each node
        represents one agent's execution session. The `children` array contains traces triggered by sub-agent tool calls
        from that trace.
      operationId: getTraceTree
      parameters:
        - name: trace_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of any trace in the tree (root or child)
        - name: include
          in: query
          required: false
          description: >
            Comma-separated list of related resources to embed on each node. Supported value: `generations` — attaches
            all generations that belong to each trace node (including sub-agent generations linked via
            `initiator_generation_id`).
          schema:
            type: string
            example: generations
      responses:
        '200':
          description: Trace tree rooted at the resolved root trace
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraceTreeNode'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Trace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/traces/{trace_id}/content:
    delete:
      tags:
        - Traces
      summary: Purge trace content
      description: >
        Deletes the trace's steps object from storage and clears its content columns (`file_id`, `error`), cascading to
        every descendant trace and to all of their generations. A descendant holds its own steps object covering the
        same run, so the cascade is what makes the erasure complete rather than merely partial.


        The rows survive as auditable skeletons with `content_redacted_at` set — ids, timestamps, step counts, and the
        generations' usage-attribution fields are preserved, because the billing and audit ledger must outlive a
        tenant's erasure of the content. A purged trace therefore reads back as a skeleton, not a 404: a 404 would prove
        nothing.


        Idempotent — purging an already-purged trace succeeds and leaves the original `content_redacted_at` in place.
      operationId: purgeTraceContent
      parameters:
        - name: trace_id
          in: path
          required: true
          schema:
            type: string
          description: Public ID of the trace
      responses:
        '200':
          description: The purged trace skeleton
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Trace'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Trace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/triggers:
    get:
      description: Lists triggers. Filter by project, starter type, or target type.
      tags:
        - Triggers
      summary: List triggers
      operationId: listTriggers
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum:
              - manual
              - webhook
              - schedule
              - event
        - name: target_type
          in: query
          required: false
          schema:
            type: string
            enum:
              - orchestration
              - agent
              - tool
              - eval
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: A list of triggers
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Trigger'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      description: Creates a new trigger for a project
      tags:
        - Triggers
      summary: Create a trigger
      operationId: createTrigger
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTriggerRequest'
      responses:
        '201':
          description: Trigger created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerWithSecret'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/triggers/{trigger_id}:
    get:
      description: Retrieves the details of a specific trigger
      tags:
        - Triggers
      summary: Get a trigger
      operationId: getTrigger
      parameters:
        - name: trigger_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Trigger details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Trigger'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
    patch:
      description: Updates an existing trigger's configuration. The type is immutable.
      tags:
        - Triggers
      summary: Update a trigger
      operationId: updateTrigger
      parameters:
        - name: trigger_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTriggerRequest'
      responses:
        '200':
          description: Trigger updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Trigger'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
    delete:
      description: Deletes a trigger
      tags:
        - Triggers
      summary: Delete a trigger
      operationId: deleteTrigger
      parameters:
        - name: trigger_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Trigger deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
  /api/v1/triggers/{trigger_id}/fire:
    post:
      description: >-
        Fires a trigger synchronously and returns the terminal firing record. The firing itself always settles here; an
        `eval` target's run is queued rather than executed inline, so the record names a `queued` run to poll.
      tags:
        - Triggers
      summary: Fire a trigger
      operationId: fireTrigger
      parameters:
        - name: trigger_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FireTriggerRequest'
      responses:
        '200':
          description: Terminal firing record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerFiring'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
        '409':
          description: Trigger inactive or creator unavailable
  /api/v1/triggers/{trigger_id}/secret:
    get:
      description: Retrieves the signing secret for a webhook trigger
      tags:
        - Triggers
      summary: Get trigger secret
      operationId: getTriggerSecret
      parameters:
        - name: trigger_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Trigger secret
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerSecretResponse'
        '400':
          description: Trigger is not a webhook trigger
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
  /api/v1/triggers/{trigger_id}/rotate-secret:
    post:
      description: Rotates the signing secret for a webhook trigger
      tags:
        - Triggers
      summary: Rotate trigger secret
      operationId: rotateTriggerSecret
      parameters:
        - name: trigger_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Secret rotated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerWithSecret'
        '400':
          description: Trigger is not a webhook trigger
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
  /api/v1/trigger-firings:
    get:
      description: Lists firings for a trigger (trigger_id is required).
      tags:
        - Triggers
      summary: List trigger firings
      operationId: listTriggerFirings
      parameters:
        - name: trigger_id
          in: query
          required: true
          description: Trigger to list firings for (trg_...)
          schema:
            type: string
        - 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: A list of firings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerFiringListResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Trigger not found
  /api/v1/trigger-firings/{firing_id}:
    get:
      description: Retrieves the details of a specific trigger firing
      tags:
        - Triggers
      summary: Get a trigger firing
      operationId: getTriggerFiring
      parameters:
        - name: firing_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Firing details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerFiring'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Firing not found
  /api/v1/usage/meters:
    get:
      tags:
        - Usage
      summary: List usage meters
      description: >
        Returns the raw usage-meter rows the caller can access, most recent first, optionally filtered by agent,
        generation, trace, actor, session, or `source`. Each row is the per-generation token usage as reported by the
        provider, for audit and reconciliation.
      operationId: listUsageMeters
      parameters:
        - name: agent_id
          in: query
          required: false
          description: Filter by agent public ID
          schema:
            type: string
        - name: generation_id
          in: query
          required: false
          description: Filter by generation public ID
          schema:
            type: string
        - name: trace_id
          in: query
          required: false
          description: Filter by trace public ID
          schema:
            type: string
        - name: actor_id
          in: query
          required: false
          description: >
            Filter by the actor (end user) the usage is attributed to. An actor that does not exist in scope yields an
            empty page.
          schema:
            type: string
        - name: session_id
          in: query
          required: false
          description: >
            Filter by the session the usage was produced in. A session that does not exist in scope yields an empty
            page.
          schema:
            type: string
        - name: trigger_id
          in: query
          required: false
          description: Filter by the trigger that initiated the generation
          schema:
            type: string
        - name: action_id
          in: query
          required: false
          description: Filter by logical action id
          schema:
            type: string
        - name: meter_type
          in: query
          required: false
          description: |
            Filter by meter type (e.g. `llm_tokens`, `compute_execution`, `api_request`, `storage`)
          schema:
            type: string
        - name: source
          in: query
          required: false
          description: >
            Filter by what the spend was incurred for. `eval` is an eval run's item generations and `eval_judge` an
            `llm_judge` scorer's own completion, so verification spend is `source` in (`eval`, `eval_judge`). Ordinary
            agent traffic carries no source and is matched by neither.
          schema:
            type: string
            example: eval
        - 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 usage-meter rows
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UsageEvent'
                  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/usage:
    get:
      tags:
        - Usage
      summary: Get aggregated usage for a project
      description: >
        Returns a project's usage rolled up over an optional `[from, to]` time window, bucketed by a single dimension
        and optionally narrowed to one `meter_type`. Each group and the grand total carry summed token counts, a
        measured `quantity` per component (so infra meters report their real amount, not zeros), and `cost_usd` (null
        when no event in the bucket was priced). This is the per-project cost-by-range/by-category query — a monthly
        figure without scanning raw meter rows client-side.
      operationId: getUsage
      parameters:
        - name: project_id
          in: query
          required: true
          description: Project public ID to aggregate usage for
          schema:
            type: string
        - name: group_by
          in: query
          required: true
          description: >
            Dimension to bucket by. `day` buckets on the event's UTC calendar day; the others bucket on the matching
            column. `ai_provider` buckets on the provider the spend was billed against — a routed generation's serving
            target, or the agent's pinned provider. `model` buckets on the model id *and* the provider that served it,
            so one model name served by two providers is two groups (see `ai_provider_id`). `source` buckets by what the
            spend was incurred for (`eval`, `eval_judge`), which is how verification spend is priced apart from the
            traffic serving real users; unlabelled traffic collapses into the single `null` bucket.
          schema:
            type: string
            enum:
              - model
              - ai_provider
              - agent
              - run
              - day
              - meter_type
              - actor
              - session
              - source
        - name: from
          in: query
          required: false
          description: |
            Inclusive lower bound (ISO-8601 timestamp) on the event created_at. Omit for no lower bound.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: |
            Inclusive upper bound (ISO-8601 timestamp) on the event created_at. Omit for no upper bound.
          schema:
            type: string
            format: date-time
        - name: meter_type
          in: query
          required: false
          description: >
            Narrow the rollup to one meter type (e.g. `llm_tokens`, `compute_execution`, `api_request`, `storage`). Omit
            to include every meter. An unknown type yields an empty rollup, not an error.
          schema:
            type: string
      responses:
        '200':
          description: The aggregated usage rollup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageAggregate'
        '400':
          description: Bad Request (missing project_id, invalid group_by or timestamp)
          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'
  /api/v1/usage/thresholds:
    get:
      tags:
        - Usage
      summary: List usage thresholds
      description: >
        Lists the usage alert thresholds the caller can access, optionally filtered by project_id. Each threshold fires
        the `usage.threshold_crossed` webhook when a project's cost or token usage over a calendar-month or rolling-24h
        window crosses the configured value.
      operationId: listUsageThresholds
      parameters:
        - name: project_id
          in: query
          required: false
          description: Filter by project public ID
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: The usage thresholds
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UsageThreshold'
                  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'
    post:
      tags:
        - Usage
      summary: Create a usage threshold
      description: >
        Creates a usage alert threshold on a project. Thresholds are immutable apart from deletion — to change one,
        delete and recreate it (which resets its fire state).
      operationId: createUsageThreshold
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUsageThresholdRequest'
      responses:
        '201':
          description: The created threshold
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageThreshold'
        '400':
          description: Bad Request (missing/invalid metric, window, or threshold)
          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'
  /api/v1/usage/thresholds/{threshold_id}:
    delete:
      tags:
        - Usage
      summary: Delete a usage threshold
      description: >
        Deletes a usage threshold, resetting its fire state. Recreating a threshold starts its once-per-window /
        hysteresis state fresh.
      operationId: deleteUsageThreshold
      parameters:
        - name: threshold_id
          in: path
          required: true
          description: Threshold public ID
          schema:
            type: string
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Threshold not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/usage/receipt:
    get:
      tags:
        - Usage
      summary: Get a generation or run billing receipt
      description: >
        Returns a billing receipt. Pass generation_id for a per-generation receipt, or orchestration_run_id for a
        per-run receipt summed across the orchestration run's meters — both share the same shape (per-model line items
        with tokens, the price-book version that priced them, and cost, plus totals). Exactly one of generation_id or
        orchestration_run_id must be supplied.
      operationId: getUsageReceipt
      parameters:
        - name: generation_id
          in: query
          required: false
          description: |
            Generation public ID. Mutually exclusive with orchestration_run_id.
          schema:
            type: string
        - name: orchestration_run_id
          in: query
          required: false
          description: >
            Orchestration run public ID. Returns the receipt summed across every generation the run metered. Mutually
            exclusive with generation_id.
          schema:
            type: string
      responses:
        '200':
          description: The generation or run usage receipt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageReceipt'
        '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/usage/prices:
    get:
      tags:
        - Usage
      summary: Get the price book
      description: >
        Returns the global price book — the versioned per-provider/model unit prices used to compute usage cost at write
        time. Readable by any authenticated user.
      operationId: getPriceBook
      responses:
        '200':
          description: The current price book
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceBookResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Usage
      summary: Upsert price-book rows
      description: >
        Upserts price rows keyed on (provider, model, effective_from). Admin only. `effective_from` must be in the
        future once the (provider, model, component) is priced — past prices are immutable so recorded costs stay
        explainable; ship corrections as new future-dated rows. A first price, with no row in this scope or any broader
        one, may be dated now or earlier.
      operationId: upsertPriceBook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertPricesRequest'
      responses:
        '200':
          description: The upserted price rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceBookResponse'
        '400':
          description: Bad Request (e.g. non-future effective_from)
          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'
  /api/v1/users/me:
    get:
      tags:
        - Users
      summary: Get the current authenticated user
      description: Returns the profile of the currently authenticated user
      operationId: getCurrentUser
      responses:
        '200':
          description: Current user returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserRecord'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users:
    get:
      tags:
        - Users
      summary: List all users
      description: Returns a list of all users
      operationId: listUsers
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of users returned successfully
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - Users
      summary: Create a user
      description: Creates a new user in the system
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - username
                - password
              properties:
                username:
                  type: string
                  example: johndoe
                password:
                  type: string
                  format: password
                  example: supersecret
                role:
                  type: string
                  enum:
                    - admin
                    - user
                  example: user
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserRecord'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/{user_id}:
    get:
      tags:
        - Users
      summary: Get a user by ID
      description: Returns the data of a specific user
      operationId: getUser
      parameters:
        - name: user_id
          in: path
          required: true
          description: User ID
          schema:
            type: string
            example: user_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserRecord'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Users
      summary: Delete a user by ID
      description: Deletes a specific user
      operationId: deleteUser
      parameters:
        - name: user_id
          in: path
          required: true
          description: User ID
          schema:
            type: string
            example: user_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: User deleted successfully
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/bootstrap:
    post:
      tags:
        - Users
      summary: Create the first admin user
      description: Creates the first admin user. Returns 409 if any user already exists.
      operationId: bootstrapUser
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - username
                - password
              properties:
                username:
                  type: string
                  example: admin
                password:
                  type: string
                  format: password
                  example: supersecret
      responses:
        '201':
          description: Admin user created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserRecord'
        '409':
          description: Users already exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/login:
    post:
      tags:
        - Users
      summary: Login user
      description: Authenticates a user and returns a JWT token
      operationId: loginUser
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - username
                - password
              properties:
                username:
                  type: string
                  example: admin
                password:
                  type: string
                  format: password
                  example: supersecret
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/v1/users/{user_id}/policies:
    put:
      tags:
        - Users
      summary: Attach policies to a user
      description: Replaces the user's policy list with the provided policy IDs. Requires admin role.
      operationId: attachUserPolicies
      parameters:
        - name: user_id
          in: path
          required: true
          description: User public ID (user_ prefix)
          schema:
            type: string
            example: user_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - policy_ids
              properties:
                policy_ids:
                  x-soat-ref: policies
                  type: array
                  items:
                    type: string
                  description: List of policy public IDs to attach (replaces existing)
                  example:
                    - pol_V1StGXR8Z5jdHi6B
      responses:
        '204':
          description: Policies attached successfully
        '400':
          description: Bad request (policy_ids must be an array)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (non-admin user)
        '404':
          description: User or policy not found
  /api/v1/webhooks:
    get:
      description: Lists webhooks. Use the project_id query parameter to filter by project.
      tags:
        - Webhooks
      summary: List webhooks
      operationId: listWebhooks
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: A list of webhooks
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Webhook'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      description: Creates a new webhook for a project
      tags:
        - Webhooks
      summary: Create a webhook
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookWithSecret'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  /api/v1/webhooks/{webhook_id}:
    get:
      description: Retrieves the details of a specific webhook
      tags:
        - Webhooks
      summary: Get a webhook
      operationId: getWebhook
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Webhook details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Webhook not found
    put:
      description: Updates an existing webhook's configuration
      tags:
        - Webhooks
      summary: Update a webhook
      operationId: updateWebhook
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
      responses:
        '200':
          description: Webhook updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Webhook not found
    delete:
      description: Deletes a webhook and stops all event deliveries
      tags:
        - Webhooks
      summary: Delete a webhook
      operationId: deleteWebhook
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Webhook deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Webhook not found
  /api/v1/webhook-deliveries:
    get:
      description: Lists event deliveries for a webhook (webhook_id is required).
      tags:
        - Webhooks
      summary: List webhook deliveries
      operationId: listWebhookDeliveries
      parameters:
        - name: webhook_id
          in: query
          required: true
          description: Webhook to list deliveries for (wh_...)
          schema:
            type: string
        - 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: A list of deliveries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeliveryListResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Webhook not found
  /api/v1/webhook-deliveries/{delivery_id}:
    get:
      description: Retrieves the details of a specific webhook delivery
      tags:
        - Webhooks
      summary: Get a delivery
      operationId: getWebhookDelivery
      parameters:
        - name: delivery_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Delivery details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Delivery not found
  /api/v1/webhook-deliveries/{delivery_id}/redeliver:
    post:
      description: |
        Queues the stored payload of an existing delivery to be sent again.

        A new delivery record is created rather than the original being reset,
        so the original attempt stays in the history. The send happens in the
        background: poll the returned delivery to observe its outcome.
      tags:
        - Webhooks
      summary: Redeliver a webhook delivery
      operationId: redeliverWebhookDelivery
      parameters:
        - name: delivery_id
          in: path
          required: true
          description: Delivery to send again (wh_deliv_...)
          schema:
            type: string
      responses:
        '202':
          description: Redelivery queued; poll the returned delivery for its outcome
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Delivery not found
  /api/v1/webhooks/{webhook_id}/secret:
    get:
      description: Retrieves the signing secret for the specified webhook
      tags:
        - Webhooks
      summary: Get webhook secret
      operationId: getWebhookSecret
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Webhook secret
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSecretResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Webhook not found
  /api/v1/webhooks/{webhook_id}/rotate-secret:
    post:
      description: Rotates the secret key for the specified webhook
      tags:
        - Webhooks
      summary: Rotate webhook secret
      operationId: rotateWebhookSecret
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Secret rotated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookWithSecret'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Webhook not found
  /api/v1/workflows:
    get:
      description: Lists workflow definitions in a project.
      tags:
        - Workflows
      summary: List workflows
      operationId: listWorkflows
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: A list of workflows
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workflow'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      description: Creates a new workflow definition. The definition is statically validated.
      tags:
        - Workflows
      summary: Create a workflow
      operationId: createWorkflow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWorkflowRequest'
      responses:
        '201':
          description: Workflow created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          description: Bad request (invalid definition)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A workflow with this name already exists
  /api/v1/workflows/{workflow_id}:
    get:
      description: Retrieves a workflow definition.
      tags:
        - Workflows
      summary: Get a workflow
      operationId: getWorkflow
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Workflow details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
    patch:
      description: >-
        Updates a workflow definition. Structural changes (states/transitions) are re-validated. Existing tasks in a
        removed state stay put but can only leave via transitions valid in the new definition.
      tags:
        - Workflows
      summary: Update a workflow
      operationId: updateWorkflow
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWorkflowRequest'
      responses:
        '200':
          description: Workflow updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          description: Bad request (invalid definition)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
    delete:
      description: Deletes a workflow. Rejected while open tasks exist.
      tags:
        - Workflows
      summary: Delete a workflow
      operationId: deleteWorkflow
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Workflow deleted
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
        '409':
          description: The workflow has open tasks and cannot be deleted
  /api/v1/workflows/{workflow_id}/versions:
    get:
      description: >
        Returns the workflow's archived state machines, newest first. A version is written on create and on every
        subsequent write that changes the definition (`states`, `transitions`, `payload_schema`) — through the REST API
        or a formation apply alike. Metadata-only edits (name, description) do not archive a version. See
        [Versioning](/docs/modules/workflows#versioning).
      tags:
        - Workflows
      summary: List a workflow's versions
      operationId: listWorkflowVersions
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of workflow versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WorkflowVersion'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Workflow not found
  /api/v1/workflows/{workflow_id}/versions/{version}:
    get:
      description: >
        Returns the exact state machine a given version describes. Every task records the version it entered on in
        `workflow_version` and runs on that machine for its whole life, so this is how you read the definition a task is
        actually being validated against — including a task whose workflow has been rewired since.
      tags:
        - Workflows
      summary: Fetch an archived workflow version
      operationId: getWorkflowVersion
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Archived workflow version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowVersion'
        '400':
          description: Bad request — version is not a positive integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
  /api/v1/workflows/{workflow_id}/versions/{version}/restore:
    post:
      description: >
        Writes an archived version's state machine back as the workflow's live definition, which archives it again as a
        **new** version rather than rewinding the counter — so a task pinned to any version in between still runs on the
        machine it entered on.


        The restore runs through the ordinary update path, so the archived definition goes through the same validation
        as an authored one. That includes resolving every `on_enter` dispatch target, so restoring a version whose agent
        or orchestration has since been deleted fails with `400` rather than writing a definition that would strand a
        task on entry. Restoring the definition the workflow already holds is a no-op and archives nothing. Tasks
        already in flight are unaffected either way — a restore is an ordinary edit, and pinning is what keeps it from
        reaching them.
      tags:
        - Workflows
      summary: Restore an archived workflow state machine
      operationId: restoreWorkflowVersion
      parameters:
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          description: The archived version number
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestoreWorkflowVersionRequest'
      responses:
        '200':
          description: The workflow, at its new version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          description: Bad request — version is not a positive integer, or the restored definition is invalid
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
components:
  schemas:
    ActivityEntry:
      type: object
      properties:
        id:
          type: string
          example: acte_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        kind:
          type: string
          enum:
            - action_executed
            - approval_resolved
            - exception_created
            - schedule_fired
          description: How the entry was produced
        severity:
          type: string
          enum:
            - info
            - warning
            - critical
        summary:
          type: string
          description: One-line, human-readable description
        detail:
          type: object
          nullable: true
          description: Kind-specific structured context (tool, args digest, node id, generation id, guardrail policy version)
        orchestration_run_id:
          type: string
          nullable: true
          description: Originating orchestration run, if any
        agent_id:
          type: string
          nullable: true
          description: Associated agent, if any
        ref_id:
          type: string
          nullable: true
          description: >-
            Producer-specific reference (the approval, exception, or trigger firing id the entry came from, or the
            executed tool's id)
        created_at:
          type: string
          format: date-time
    ActorRecord:
      type: object
      properties:
        id:
          type: string
          description: Actor ID
          example: actor_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Project ID
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: Alice
        external_id:
          type: string
          nullable: true
          description: External identifier (e.g. WhatsApp phone number)
          example: '+15551234567'
        instructions:
          type: string
          nullable: true
          description: Persona-specific instructions composed into the effective system prompt during conversation generation.
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: Agent this actor is linked to (mutually exclusive with chatId).
        chat_id:
          x-soat-ref: chats
          type: string
          nullable: true
          description: Chat this actor is linked to (mutually exclusive with agentId).
        tags:
          type: object
          additionalProperties:
            type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    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.
    Agent:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the agent
          example: agent_V1StGXR8Z5jdHi6B
        project_id:
          type: string
          description: Public ID of the owning project
          x-soat-ref: projects
        ai_provider_id:
          type: string
          nullable: true
          description: >-
            Public ID of the pinned AI provider. Null when the agent resolves its model through `model_route_id`
            instead.
          x-soat-ref: ai-providers
        model_route_id:
          type: string
          nullable: true
          description: >-
            Public ID of the model route that resolves this agent's completion model. Null when the agent pins a
            provider through `ai_provider_id`. Mutually exclusive with `ai_provider_id` and `model`.
          x-soat-ref: model-routes
        name:
          type: string
          nullable: true
          description: Display name
        instructions:
          type: string
          nullable: true
          description: System instructions guiding behavior
        model:
          type: string
          nullable: true
          description: Model identifier
        tool_bindings:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/ToolBinding'
          description: >-
            Tools attached to this agent, one binding object per tool — the canonical attachment field. See [Tool
            Bindings](/docs/modules/agents#tool-bindings).
        max_steps:
          type: integer
          nullable: true
          description: >-
            Maximum agent loop steps before stopping. The budget bounds a **turn**: a generation that pauses at
            `requires_action` and resumes after `submit-tool-outputs` continues the same turn and spends what is left of
            it, so a turn that arrives with nothing left completes with `stop_reason: "max_steps"` instead of calling
            the model again.
        tool_choice:
          nullable: true
          description: >-
            Tool choice strategy. Accepts a string (`"auto"`, `"required"`) or an object (`{ "type": "tool",
            "tool_name": "my_tool" }`). A forcing value (`"required"` or the object form) forbids a final assistant
            message on every step of every turn, including a resumed or continued one, so it requires a `has_tool_call`
            entry in `stop_conditions` — otherwise the write is refused with `FORCED_TOOL_CHOICE_CANNOT_STOP`.
        stop_conditions:
          type: array
          nullable: true
          items:
            type: object
          description: >-
            Conditions that end the agent's work early, on top of `max_steps` — turn-scoped (`has_tool_call`) or
            chain-scoped (`max_chain_generations`). See the create request body for the accepted shapes.
        active_tool_ids:
          x-soat-ref: tools
          type: array
          nullable: true
          items:
            type: string
          description: Subset of the bound tools that are active
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the agent scope, governing every tool call the agent makes.
        step_rules:
          type: array
          nullable: true
          items:
            type: object
          description: >-
            Per-step overrides of `tool_choice` and `active_tool_ids`. Steps are numbered from the first step of the
            **turn**, and that numbering spans a `requires_action` pause — a rule fires once per turn, not once per
            resumption.
        boundary_policy:
          type: object
          nullable: true
          description: Allowed/denied SOAT actions
        temperature:
          type: number
          nullable: true
          description: Sampling temperature
        knowledge_config:
          type: object
          nullable: true
          description: Knowledge retrieval config injected before every generation
          properties:
            memory_ids:
              x-soat-ref: memories
              type: array
              items:
                type: string
            memory_tags:
              type: array
              items:
                type: string
            document_ids:
              x-soat-ref: documents
              type: array
              items:
                type: string
            document_paths:
              type: array
              items:
                type: string
            min_score:
              type: number
            limit:
              type: integer
            write_memory_id:
              x-soat-ref: memories
              type: string
              nullable: true
              description: >-
                Public ID of the memory the agent can write to during generation. When set, a write_memory tool is
                automatically available to the agent.
            extraction:
              description: >-
                Automatic fact extraction from completed generation turns (requires write_memory_id). Pass `true` to
                enable with defaults, or an object to customize the provider, model, and prompt used for the extraction
                completion. Extracted facts are written to the write memory through the standard dedup/merge/skip
                algorithm.
              oneOf:
                - type: boolean
                - type: object
                  properties:
                    enabled:
                      type: boolean
                      description: >-
                        Defaults to true when the object form is used. Set false to keep the configuration but disable
                        extraction.
                    ai_provider_id:
                      x-soat-ref: ai-providers
                      type: string
                      description: >-
                        AI provider override for extraction calls. Must belong to the agent's project. Its default_model
                        becomes the model fallback.
                    model:
                      type: string
                      description: Model override for extraction calls.
                    prompt:
                      type: string
                      description: >-
                        Replaces the default task instructions. The JSON response contract and the conversation
                        transcript are always appended by the server.
        output_schema:
          type: object
          nullable: true
          description: >-
            JSON Schema describing the structured object the model must return. When set, non-streaming generations
            constrain output to this schema and the parsed value is returned as `output.object`. The schema is enforced
            on the way back, not just sent to the model: an object that violates it fails the generation with 502
            `OUTPUT_SCHEMA_VALIDATION_FAILED`, naming the violated field. Constraints beyond `required`/`type`
            (`minLength`, `enum`, `pattern`, `minItems`) are honored and are what reject a structurally valid but
            degenerate answer. See the Structured Output section in the Agents module docs.
        max_context_messages:
          type: integer
          nullable: true
          description: >-
            Maximum number of recent messages to include in the context window sent to the model. When null, all
            messages are included.
        single_session_per_actor:
          type: boolean
          description: >-
            When true, only one open session per actor_id is allowed for this agent. Creating a second open session for
            the same actor returns 409.
        trace_content_mode:
          type: string
          nullable: true
          enum:
            - full
            - none
            - null
          description: >-
            Agent-scope zero-retention setting. `null` (the default) inherits the project's `trace_content_mode`; `none`
            means this agent's trace and generation content is never persisted. An agent may tighten a storing project
            to `none` but cannot loosen a `none` project back to `full`.
        on_approval_expiry:
          type: string
          nullable: true
          enum:
            - terminate
            - react
            - null
          description: >-
            What happens when one of this agent's held tool calls expires un-approved. `null` (the default) and
            `terminate` end the chain there — the expired approval, its `approvals.expired` event and the auto-filed
            `approval_expired` exception are the whole record. `react` spawns a continuation that reports the staleness
            to the agent, for an agent that acts on it.
        version:
          type: integer
          description: >-
            Current config version. Starts at 1 and increments on every write that changes the config; each increment
            archives the new config as an `AgentVersion`. A write that changes nothing leaves it untouched.
          example: 3
        active_release:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/AgentRelease'
          description: Staged rollout in progress, or null when all traffic serves this config.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AgentRelease:
      type: object
      description: >-
        A staged rollout splitting traffic between two archived versions. See [Versioning and Staged
        Rollout](/docs/modules/agents#versioning-and-staged-rollout).
      required:
        - stable_version
        - canary_version
        - canary_percent
      properties:
        stable_version:
          type: integer
          minimum: 1
          description: Version served to traffic not assigned to the canary
          example: 3
        canary_version:
          type: integer
          minimum: 1
          description: Version under trial. Must differ from `stable_version`.
          example: 4
        canary_percent:
          type: integer
          minimum: 0
          maximum: 100
          description: Percentage of traffic assigned to `canary_version`
          example: 20
        promotion_gate:
          x-soat-ref: evals
          type: string
          nullable: true
          description: >-
            Eval that must have a passing run against `canary_version` before `promote` is allowed, or null for an
            ungated rollout. The gate constrains only how the rollout ends — traffic is split the same way either way.
            See [Eval-gated promotion](/docs/modules/agents#eval-gated-promotion).
          example: eval_V1StGXR8Z5jdHi6B
    AgentVersion:
      type: object
      description: An immutable archive of an agent's configuration at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: agver_V1StGXR8Z5jdHi6B
        agent_id:
          x-soat-ref: agents
          type: string
          description: Public ID of the agent this version belongs to
          example: agent_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The agent's configuration as it stood at this version: every mutable field of the `Agent` schema
            (`instructions`, `model`, `tool_bindings`, `max_steps`, `tool_choice`, `stop_conditions`, `active_tool_ids`,
            `step_rules`, `boundary_policy`, `temperature`, `knowledge_config`, `output_schema`, `max_context_messages`,
            `single_session_per_actor`, `on_approval_expiry`, `guardrail_ids`, `ai_provider_id`, `model_route_id`,
            `name`), and none of its identity or bookkeeping fields (`id`, `project_id`, `version`, `active_release`,
            timestamps).


            Deliberately open rather than a fixed schema: an archive written by an earlier release of SOAT reflects the
            agent surface **of its own time**, so it may carry fields the current schema no longer defines, or lack ones
            it has since gained. Knowledge retrieval is not part of the snapshot — a version records which
            `knowledge_config` applied, while the documents and memories it resolves keep their own histories and are
            pinned at generation time.
        label:
          type: string
          nullable: true
          description: >-
            Optional human tag, set with `version_label` on the write that created this version. Restore, promote and
            abort set one automatically (e.g. `restored from v1`).
          example: pre-tone-change
        eval_run_id:
          x-soat-ref: eval-runs
          type: string
          nullable: true
          description: >-
            The eval run that cleared the release's `promotion_gate` when this version was promoted. Null for every
            version that did not go live through a gated promotion — which is most of them.
          example: evrun_V1StGXR8Z5jdHi6B
        created_by:
          x-soat-ref: users
          type: string
          nullable: true
          description: >-
            Public ID of the user whose action produced this version. A formation apply is attributed to the project's
            owning identity; null when no principal could be resolved.
        created_at:
          type: string
          format: date-time
    RestoreAgentVersionRequest:
      type: object
      properties:
        label:
          type: string
          description: Tag for the version this restore creates. Defaults to `restored from v{version}`.
          example: rollback-incident-42
    SetAgentReleaseRequest:
      type: object
      required:
        - stable_version
        - canary_version
        - canary_percent
      properties:
        stable_version:
          type: integer
          minimum: 1
          description: An existing version to serve as the baseline
          example: 3
        canary_version:
          type: integer
          minimum: 1
          description: An existing version to trial. Must differ from `stable_version`.
          example: 4
        canary_percent:
          type: integer
          minimum: 0
          maximum: 100
          description: Percentage of traffic to assign to `canary_version`
          example: 20
        promotion_gate:
          x-soat-ref: evals
          type: string
          nullable: true
          description: >-
            Eval to gate promotion on. It must belong to this project and evaluate this agent; anything else is a `400`.
            Omit it, or send null, for a rollout that can be promoted at will.
          example: eval_V1StGXR8Z5jdHi6B
    ToolBinding:
      type: object
      description: >-
        One agent↔tool attachment. Exactly one of `tool_id` (persisted tool reference) or `tool` (inline ephemeral
        definition) per entry. Tool-call gating is owned by [Guardrails](/docs/modules/guardrails), attached via
        `guardrail_ids` on the project, agent, or tool — not on the binding.
      properties:
        tool_id:
          x-soat-ref: tools
          type: string
          description: Public ID of a persisted tool. Exactly one of `tool_id`/`tool`.
          example: tool_V1StGXR8Z5jdHi6B
        tool:
          $ref: '#/components/schemas/CreateToolRequest'
    CreateAgentRequest:
      type: object
      description: >-
        Exactly one of `ai_provider_id` or `model_route_id` must be set (400 otherwise). `model` names the model on a
        pinned provider and cannot be combined with `model_route_id`, whose targets each name their own model.
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: Public ID of the AI provider to pin. Mutually exclusive with `model_route_id`.
        model_route_id:
          x-soat-ref: model-routes
          type: string
          description: >-
            Public ID of a model route in the same project. The agent's completion model is then resolved through the
            route's ordered targets with failover. Mutually exclusive with `ai_provider_id` and `model`.
        name:
          type: string
        instructions:
          type: string
        model:
          type: string
        tool_bindings:
          type: array
          items:
            $ref: '#/components/schemas/ToolBinding'
          description: >-
            Tools to attach, one binding object per tool — the only attachment field. An entry is either a reference (`{
            "tool_id": … }`) or an inline definition (`{ "tool": … }`). See [Tool
            Bindings](/docs/modules/agents#tool-bindings).
        max_steps:
          type: integer
        tool_choice:
          description: >-
            Tool choice strategy. Accepts a string (`"auto"`, `"required"`) or an object (`{ "type": "tool",
            "tool_name": "my_tool" }`). A forcing value (`"required"` or the object form) forbids a final assistant
            message on every step of every turn, including a resumed or continued one, so it requires a `has_tool_call`
            entry in `stop_conditions` — otherwise the write is refused with `FORCED_TOOL_CHOICE_CANNOT_STOP`.
        stop_conditions:
          type: array
          items:
            type: object
          description: >-
            Conditions that end the agent's work early, on top of `max_steps`. Two scopes:


            `{"type": "has_tool_call", "tool_name": "<resolved tool name>"}` ends the **turn** after the step that calls
            the named tool. It narrows when the loop ends — it never lets it run past `max_steps`.


            `{"type": "max_chain_generations", "max_generations": <n>}` bounds the **continuation chain** instead: once
            the chain has spawned that many generations, further resumptions stop with `chain_limit` rather than
            extending it. It never shortens a turn. The effective ceiling is the smaller of this and the deployment's
            `MAX_CONTINUATION_CHAIN_GENERATIONS`, so an agent can be stricter than the platform but never looser.


            An unknown `type`, a `has_tool_call` without a `tool_name`, a `max_chain_generations` whose
            `max_generations` is not a positive integer, or a non-object entry is rejected with 400.
        active_tool_ids:
          x-soat-ref: tools
          type: array
          items:
            type: string
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the agent scope.
        step_rules:
          type: array
          items:
            type: object
        boundary_policy:
          type: object
        temperature:
          type: number
        knowledge_config:
          type: object
          properties:
            memory_ids:
              x-soat-ref: memories
              type: array
              items:
                type: string
            memory_tags:
              type: array
              items:
                type: string
            document_ids:
              x-soat-ref: documents
              type: array
              items:
                type: string
            document_paths:
              type: array
              items:
                type: string
            min_score:
              type: number
            limit:
              type: integer
            write_memory_id:
              x-soat-ref: memories
              type: string
              nullable: true
              description: >-
                Public ID of the memory the agent can write to during generation. When set, a write_memory tool is
                automatically available to the agent.
            extraction:
              description: >-
                Automatic fact extraction from completed generation turns (requires write_memory_id). Pass `true` to
                enable with defaults, or an object to customize the provider, model, and prompt used for the extraction
                completion. Extracted facts are written to the write memory through the standard dedup/merge/skip
                algorithm.
              oneOf:
                - type: boolean
                - type: object
                  properties:
                    enabled:
                      type: boolean
                      description: >-
                        Defaults to true when the object form is used. Set false to keep the configuration but disable
                        extraction.
                    ai_provider_id:
                      x-soat-ref: ai-providers
                      type: string
                      description: >-
                        AI provider override for extraction calls. Must belong to the agent's project. Its default_model
                        becomes the model fallback.
                    model:
                      type: string
                      description: Model override for extraction calls.
                    prompt:
                      type: string
                      description: >-
                        Replaces the default task instructions. The JSON response contract and the conversation
                        transcript are always appended by the server.
        output_schema:
          type: object
          nullable: true
          description: >-
            JSON Schema describing the structured object the model must return. When set, non-streaming generations
            constrain output to this schema and the parsed value is returned as `output.object`. The schema is enforced
            on the way back, not just sent to the model: an object that violates it fails the generation with 502
            `OUTPUT_SCHEMA_VALIDATION_FAILED`, naming the violated field. Constraints beyond `required`/`type`
            (`minLength`, `enum`, `pattern`, `minItems`) are honored and are what reject a structurally valid but
            degenerate answer. See the Structured Output section in the Agents module docs.
        max_context_messages:
          type: integer
          description: Maximum number of recent messages included in the context window. Null means no limit.
        single_session_per_actor:
          type: boolean
          description: When true, only one open session per actor_id is allowed for this agent.
        trace_content_mode:
          type: string
          nullable: true
          enum:
            - full
            - none
            - null
          description: >-
            Zero-retention opt-in for this agent. `null` inherits the project's setting; `none` means trace and
            generation content is never written. Setting `full` under a project whose own mode is `none` is refused with
            400 — the project is a floor an agent may only tighten.
        on_approval_expiry:
          type: string
          nullable: true
          enum:
            - terminate
            - react
            - null
          description: >-
            What happens when one of this agent's held tool calls expires un-approved. `null` (the default) and
            `terminate` end the chain there — the expired approval, its `approvals.expired` event and the auto-filed
            `approval_expired` exception are the whole record. `react` spawns a continuation that reports the staleness
            to the agent, for an agent that acts on it.
        version_label:
          type: string
          nullable: true
          description: >-
            Optional tag for the config version this write archives (e.g. `initial`). Annotates the version only — it is
            not stored on the agent and is not part of the config, so labelling a change is never itself a change.
          example: initial
    UpdateAgentRequest:
      type: object
      description: >-
        The post-update state must still set exactly one of `ai_provider_id` or `model_route_id`. To switch a pinned
        agent to a route, send `model_route_id` together with `ai_provider_id: null` in the same request (and vice
        versa).
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
        model_route_id:
          x-soat-ref: model-routes
          type: string
          nullable: true
          description: Model route in the same project. Mutually exclusive with `ai_provider_id` and `model`; set to null to clear.
        name:
          type: string
          nullable: true
        instructions:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        tool_bindings:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/ToolBinding'
          description: >-
            Tools attached to the agent — the only attachment field. Replaces the whole binding list; set to `null` to
            clear. See [Tool Bindings](/docs/modules/agents#tool-bindings).
        max_steps:
          type: integer
          nullable: true
        tool_choice:
          nullable: true
          description: >-
            Tool choice strategy. Accepts a string (`"auto"`, `"required"`) or an object (`{ "type": "tool",
            "tool_name": "my_tool" }`). A forcing value (`"required"` or the object form) forbids a final assistant
            message on every step of every turn, including a resumed or continued one, so it requires a `has_tool_call`
            entry in `stop_conditions` — otherwise the write is refused with `FORCED_TOOL_CHOICE_CANNOT_STOP`.
        stop_conditions:
          type: array
          nullable: true
          items:
            type: object
          description: >-
            Conditions that end the agent's work early, on top of `max_steps`. Two scopes:


            `{"type": "has_tool_call", "tool_name": "<resolved tool name>"}` ends the **turn** after the step that calls
            the named tool. It narrows when the loop ends — it never lets it run past `max_steps`.


            `{"type": "max_chain_generations", "max_generations": <n>}` bounds the **continuation chain** instead: once
            the chain has spawned that many generations, further resumptions stop with `chain_limit` rather than
            extending it. It never shortens a turn. The effective ceiling is the smaller of this and the deployment's
            `MAX_CONTINUATION_CHAIN_GENERATIONS`, so an agent can be stricter than the platform but never looser.


            An unknown `type`, a `has_tool_call` without a `tool_name`, a `max_chain_generations` whose
            `max_generations` is not a positive integer, or a non-object entry is rejected with 400.
        active_tool_ids:
          x-soat-ref: tools
          type: array
          nullable: true
          items:
            type: string
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the agent scope.
        step_rules:
          type: array
          nullable: true
          items:
            type: object
        boundary_policy:
          type: object
          nullable: true
        temperature:
          type: number
          nullable: true
        knowledge_config:
          type: object
          nullable: true
          properties:
            memory_ids:
              x-soat-ref: memories
              type: array
              items:
                type: string
            memory_tags:
              type: array
              items:
                type: string
            document_ids:
              x-soat-ref: documents
              type: array
              items:
                type: string
            document_paths:
              type: array
              items:
                type: string
            min_score:
              type: number
            limit:
              type: integer
            write_memory_id:
              x-soat-ref: memories
              type: string
              nullable: true
              description: >-
                Public ID of the memory the agent can write to during generation. When set, a write_memory tool is
                automatically available to the agent.
            extraction:
              description: >-
                Automatic fact extraction from completed generation turns (requires write_memory_id). Pass `true` to
                enable with defaults, or an object to customize the provider, model, and prompt used for the extraction
                completion. Extracted facts are written to the write memory through the standard dedup/merge/skip
                algorithm.
              oneOf:
                - type: boolean
                - type: object
                  properties:
                    enabled:
                      type: boolean
                      description: >-
                        Defaults to true when the object form is used. Set false to keep the configuration but disable
                        extraction.
                    ai_provider_id:
                      x-soat-ref: ai-providers
                      type: string
                      description: >-
                        AI provider override for extraction calls. Must belong to the agent's project. Its default_model
                        becomes the model fallback.
                    model:
                      type: string
                      description: Model override for extraction calls.
                    prompt:
                      type: string
                      description: >-
                        Replaces the default task instructions. The JSON response contract and the conversation
                        transcript are always appended by the server.
        output_schema:
          type: object
          nullable: true
          description: >-
            JSON Schema describing the structured object the model must return. When set, non-streaming generations
            constrain output to this schema and the parsed value is returned as `output.object`. The schema is enforced
            on the way back, not just sent to the model: an object that violates it fails the generation with 502
            `OUTPUT_SCHEMA_VALIDATION_FAILED`, naming the violated field. Constraints beyond `required`/`type`
            (`minLength`, `enum`, `pattern`, `minItems`) are honored and are what reject a structurally valid but
            degenerate answer. See the Structured Output section in the Agents module docs.
        max_context_messages:
          type: integer
          nullable: true
          description: Maximum number of recent messages included in the context window. Null means no limit.
        single_session_per_actor:
          type: boolean
          nullable: true
          description: When true, only one open session per actor_id is allowed for this agent.
        trace_content_mode:
          type: string
          nullable: true
          enum:
            - full
            - none
            - null
          description: >-
            Zero-retention opt-in for this agent. `null` inherits the project's setting; `none` means trace and
            generation content is never written. Setting `full` under a project whose own mode is `none` is refused with
            400.
        on_approval_expiry:
          type: string
          nullable: true
          enum:
            - terminate
            - react
            - null
          description: >-
            What happens when one of this agent's held tool calls expires un-approved. `null` (the default) and
            `terminate` end the chain there — the expired approval, its `approvals.expired` event and the auto-filed
            `approval_expired` exception are the whole record. `react` spawns a continuation that reports the staleness
            to the agent, for an agent that acts on it.
        version_label:
          type: string
          nullable: true
          description: >-
            Optional tag for the config version this write archives (e.g. `pre-tone-change`). Annotates the version only
            — it is not stored on the agent and is not part of the config, so labelling a change is never itself a
            change. Ignored when the write changes nothing, since no version is created.
          example: pre-tone-change
    CreateAgentGenerationRequest:
      type: object
      required:
        - messages
      properties:
        messages:
          type: array
          minItems: 1
          items:
            type: object
            additionalProperties: false
            required:
              - role
              - content
            properties:
              role:
                type: string
                enum:
                  - user
                  - assistant
              content:
                oneOf:
                  - type: string
                  - $ref: '#/components/schemas/ToolOutputMessageContent'
                  - $ref: '#/components/schemas/DocumentMessageContent'
        stream:
          type: boolean
          default: false
          x-soat-tool-unsupported: true
          description: When true the response is an SSE stream
        trace_id:
          x-soat-ref: traces
          type: string
          x-soat-server-managed: true
          description: >-
            Optional trace ID to group generations. Each generation appends its own steps to the trace's steps object,
            and `step_count` covers them all.
        parent_trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          x-soat-server-managed: true
          description: The trace ID of the parent agent generation that triggered this one (for agent-to-agent calls)
        root_trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          x-soat-server-managed: true
          description: The trace ID of the root generation in the call chain; if omitted, this generation is the root
        max_call_depth:
          type: integer
          minimum: 0
          default: 10
          x-soat-server-managed: true
          description: Maximum nested agent-call depth; 0 short-circuits with a depth-guard response
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            in this generation. The header name is the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased. Keys are never case-converted — they round-trip
            exactly as sent. An invalid or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
        action_id:
          type: string
          description: >-
            Logical action label recorded on the generation's usage meter, so spend can be rolled up per action (e.g. an
            A/B/C/D operating action).
        guardrail_context:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            Caller-supplied guardrail context (the `context.*` namespace guard and class expressions read at
            tool-dispatch time). Free-form and never interpreted by the platform; a guardrail may combine it with a
            `context_tool` per its `context_mode`. See the guardrails module.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            Caller-supplied key/value metadata attached to the generation record for per-run audit attribution (e.g. the
            knowledge-corpus version that produced this action). Round-trips verbatim when the generation is fetched via
            the generations API. The bag is caller-owned and no key is reserved: server-owned state (usage attribution,
            the served agent version, the model route's record, the extraction summary) lives in its own top-level
            generation fields and cannot be written from here. Use the request's own `action_id` field to set the
            usage-attribution label.
        extract:
          type: boolean
          description: >-
            Per-turn override of the agent's `knowledge_config.extraction` default. Omit to follow the agent's stored
            config. Set `false` to suppress automatic memory extraction for this turn (e.g. an operational or
            tool-listing turn that would only add noise to a curated memory). Set `true` to force extraction on for this
            turn even when the agent does not enable it by default, provided the agent has a `write_memory_id`. Has no
            effect on streaming or `requires_action` turns, which never extract.
        knowledge_config:
          type: object
          nullable: true
          description: >-
            Per-generation knowledge retrieval override. Array filters (memory_ids, memory_tags, document_ids,
            document_paths) are unioned with the agent's stored knowledge_config; scalar fields (min_score, limit) use
            the per-generation value when present.
          properties:
            memory_ids:
              x-soat-ref: memories
              type: array
              items:
                type: string
            memory_tags:
              type: array
              items:
                type: string
            document_ids:
              x-soat-ref: documents
              type: array
              items:
                type: string
            document_paths:
              type: array
              items:
                type: string
            min_score:
              type: number
            limit:
              type: integer
    ToolOutputMessageContent:
      type: object
      required:
        - type
        - tool_id
      properties:
        type:
          type: string
          enum:
            - tool_output
        tool_id:
          x-soat-ref: tools
          type: string
          description: Public ID of the tool to execute before generation.
        action:
          type: string
          nullable: true
          description: Optional action name for tools that require action selection (for example builtin and mcp tools).
        input:
          type: object
          nullable: true
          additionalProperties: true
          description: Input payload passed to the tool call.
        output_path:
          type: string
          nullable: true
          description: Optional dot-notation path used to extract a value from the tool output.
    DocumentMessageContent:
      type: object
      required:
        - type
        - document_id
      properties:
        type:
          type: string
          enum:
            - document
        document_id:
          x-soat-ref: documents
          type: string
          description: Public ID of a document to use as the message content.
    SubmitToolOutputsRequest:
      type: object
      required:
        - tool_outputs
      properties:
        tool_outputs:
          type: array
          minItems: 1
          items:
            type: object
            required:
              - tool_call_id
              - output
            properties:
              tool_call_id:
                type: string
                description: ID of the tool call to respond to
              output:
                description: Result of the tool execution
    AcceptedGenerationResponse:
      type: object
      description: >
        Handle for a generation running in the background. The generation record already exists when this is returned,
        so the id is immediately pollable via `GET /api/v1/generations/{generation_id}`.
      required:
        - status
        - generation_id
        - trace_id
      properties:
        status:
          type: string
          enum:
            - accepted
          example: accepted
        generation_id:
          type: string
          example: gen_V1StGXR8Z5jdHi6B
        trace_id:
          type: string
          example: trace_V1StGXR8Z5jdHi6B
    AgentGenerationResponse:
      type: object
      description: >
        Result of an agent generation. Mirrors the server's `GenerationResult`. When `status` is `completed` the model
        output is under `output`; when it is `requires_action` the pending client tool calls are under
        `required_action`.
      required:
        - id
        - trace_id
        - status
      properties:
        id:
          type: string
          description: Public ID of the generation
          example: gen_V1StGXR8Z5jdHi6B
        trace_id:
          type: string
          description: Public ID of the trace for this generation
          example: trace_V1StGXR8Z5jdHi6B
        status:
          type: string
          enum:
            - completed
            - requires_action
          description: Generation status
        ai_provider_id:
          type: string
          x-soat-ref: ai-providers
          nullable: true
          description: >
            Public ID of the AI provider that served `output.model` — the target a model route picked, or the agent's
            pinned provider. A model string alone does not identify its provider: two providers in one project can serve
            byte-identical model names, so this is what makes the value safe to map back to a name a gateway in front of
            this runtime publishes. Null when the generation resolved no serving provider.
          example: aip_V1StGXR8Z5jdHi6B
        output:
          type: object
          nullable: true
          description: Model output (present when `status` is `completed`).
          required:
            - model
            - content
            - finish_reason
          properties:
            model:
              type: string
              description: Model that produced the output
            content:
              type: string
              description: Final text output
            finish_reason:
              type: string
              description: Reason the model stopped generating
            response_messages:
              type: array
              nullable: true
              description: Full AI SDK response messages (tool calls, tool results, final text)
              items:
                type: object
            object:
              type: object
              nullable: true
              description: Structured object matching the agent's `output_schema` (when `output_schema` is set)
        required_action:
          type: object
          nullable: true
          description: Pending action the caller must satisfy (present when `status` is `requires_action`).
          required:
            - type
            - tool_calls
          properties:
            type:
              type: string
              enum:
                - submit_tool_outputs
              description: The kind of action required
            tool_calls:
              type: array
              description: Pending tool calls to execute and submit outputs for
              items:
                type: object
                properties:
                  id:
                    type: string
                    description: Tool call ID
                  tool_name:
                    type: string
                    description: Name of the tool to invoke
                  args:
                    type: object
                    description: Arguments for the tool call
    ProviderPrice:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the price row
          example: price_V1StGXR8Z5jdHi6B
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: The AI provider instance this override prices
          example: aip_V1StGXR8Z5jdHi6B
        meter_type:
          type: string
          description: Always `llm_tokens` for provider price overrides
        provider:
          type: string
          description: Provider slug (taken from the AI provider instance)
          example: openai
        model:
          type: string
          example: gpt-4o
        component:
          type: string
          description: The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`)
        unit:
          type: string
          description: Always `token` for provider price overrides
        unit_price:
          type: number
          description: USD per token for this component
        effective_from:
          type: string
          format: date-time
          description: The row with the latest effective_from <= now() prices a call
        created_at:
          type: string
          format: date-time
    ProviderPricesResponse:
      type: object
      properties:
        prices:
          type: array
          items:
            $ref: '#/components/schemas/ProviderPrice'
    UpsertProviderPricesRequest:
      type: object
      required:
        - prices
      properties:
        prices:
          type: array
          items:
            type: object
            required:
              - model
              - component
              - unit
              - unit_price
              - effective_from
            properties:
              model:
                type: string
                example: gpt-4o
              component:
                type: string
                description: The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`)
              unit:
                type: string
                description: Always `token` for token pricing
              unit_price:
                type: number
                description: USD per token for this component
              effective_from:
                type: string
                format: date-time
                description: Must be in the future; past prices are immutable
    ProviderModelsResponse:
      type: object
      required:
        - provider
        - models
      properties:
        provider:
          type: string
          description: The provider slug the listing came from
          example: vertex
        models:
          type: array
          items:
            type: object
            required:
              - id
            properties:
              id:
                type: string
                description: |
                  The provider-native model id, ready to use as `default_model` or an agent's `model`
                example: gemini-2.5-flash
              display_name:
                type: string
                description: The provider's own human-readable name, when it reports one
                example: Gemini 2.5 Flash
              vendor:
                type: string
                description: Who makes the model, when the provider reports it
                example: google
              input_modalities:
                type: array
                description: Lowercased input modalities, when the provider reports them
                items:
                  type: string
                example:
                  - text
                  - image
              output_modalities:
                type: array
                description: Lowercased output modalities, when the provider reports them
                items:
                  type: string
                example:
                  - text
              streaming:
                type: boolean
                description: Whether the model supports streaming responses
              lifecycle:
                type: string
                description: >
                  `active`, `legacy` or `deprecated`, as the provider reports it. A model that is not `active` still
                  serves today but should not be pinned by anything new.
                enum:
                  - active
                  - legacy
                  - deprecated
              inference_types:
                type: array
                description: >
                  Lowercased inference types the model supports, when reported. A Bedrock model offering only
                  `inference_profile` must be invoked through a cross-region profile id rather than the bare model id.
                items:
                  type: string
                example:
                  - on_demand
                  - inference_profile
    ApiKeyRecord:
      type: object
      properties:
        id:
          type: string
          description: Public API key ID (key_ prefix)
          example: key_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: CI/CD Pipeline
        key_prefix:
          type: string
          description: First 8 characters of the raw key for identification
          example: sk_a1b2c3
        user_id:
          x-soat-ref: users
          type: string
          description: Owner user public ID
          example: user_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          nullable: true
          description: The project this key is scoped to, or null for an unscoped key that spans projects
          example: proj_V1StGXR8Z5jdHi6B
        policy_ids:
          x-soat-ref: policies
          type: array
          items:
            type: string
          description: Public IDs of policies attached to this key
          example:
            - pol_V1StGXR8Z5jdHi6B
        created_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKeyRecord'
        - type: object
          properties:
            key:
              type: string
              description: The raw API key value (only returned once at creation). Use as Bearer token.
              example: sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
    ApprovalRecurrenceGroup:
      type: object
      description: >-
        A set of approval items sharing a `dedup_key` — the same proposed action recurring — with the ordered chain and
        its resolution reasons.
      properties:
        dedup_key:
          type: string
          description: The shared dedup key that defines the group
        agent_id:
          type: string
          nullable: true
          description: Proposing agent (shared across the group)
        tool_id:
          type: string
          nullable: true
          description: Proposed tool (shared across the group)
        count:
          type: integer
          description: Number of items in the group
        chain:
          type: array
          description: The items oldest → newest (the `previous_item_id` chain)
          items:
            type: object
            properties:
              id:
                type: string
                example: apr_V1StGXR8Z5jdHi6B
              status:
                type: string
                enum:
                  - pending
                  - approved
                  - rejected
                  - expired
              resolution_reason:
                type: string
                nullable: true
              created_at:
                type: string
                format: date-time
        reasons:
          type: array
          description: The chain's resolution reasons in order (empty entries omitted)
          items:
            type: string
    ApprovalItem:
      type: object
      properties:
        id:
          type: string
          example: apr_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        origin:
          type: string
          enum:
            - node
            - tool_call
            - task_transition
          description: How the item was produced (analytics/filtering only)
        status:
          type: string
          enum:
            - pending
            - approved
            - rejected
            - expired
        proposed_action:
          type: object
          nullable: true
          description: >-
            The frozen proposed action. Null for producers whose proposal is not a tool call (a `task_transition` item
            gates a workflow transition, named by `task_transition`).
          properties:
            tool_id:
              type: string
            action:
              type: string
              description: >-
                Resolved action name (the builtin/mcp action) for `tool_call`-origin items — always present there, even
                for single-action tools. Omitted for `node`-origin items, whose downstream execution is wired by a
                separate `tool` node in the graph.
            arguments:
              type: object
        reasoning:
          type: string
          nullable: true
          description: The proposing agent's rationale
        evidence:
          type: object
          nullable: true
          description: Supporting structured data
        predicted_impact:
          type: string
          nullable: true
          description: Expected execution effect
        expires_at:
          type: string
          format: date-time
          description: Server-enforced hard gate; the item can never execute after this
        dedup_key:
          type: string
          nullable: true
          description: Set on tool-call items to suppress duplicate proposals
        orchestration_run_id:
          type: string
          nullable: true
          description: Originating orchestration run (node producer)
        node_id:
          type: string
          nullable: true
          description: Originating node id within the run's graph
        generation_id:
          type: string
          nullable: true
          description: Originating generation (tool-call producer)
        session_id:
          type: string
          nullable: true
          description: Session the originating generation ran in (tool-call producer)
        agent_id:
          type: string
          nullable: true
          description: Proposing agent
        task_id:
          type: string
          nullable: true
          description: Gated task (task_transition producer)
        task_transition:
          type: string
          nullable: true
          description: Transition fired on approval (task_transition producer)
        policy_version:
          type: string
          nullable: true
        previous_item_id:
          type: string
          nullable: true
          description: >-
            Prior item's ID when this proposal was re-filed after an earlier matching item (same dedup_key) had been
            rejected
          example: apr_V1StGXR8Z5jdHi6B
        resolved_by:
          type: string
          nullable: true
          description: Resolving user's public ID; null on expiry
        resolution_reason:
          type: string
          nullable: true
          description: Required on rejection
        edited_arguments:
          type: object
          nullable: true
          description: Set on edit-then-approve
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AuditEntry:
      type: object
      properties:
        id:
          type: string
          example: audit_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          nullable: true
          description: Project the action targeted; null for global actions
        principal_type:
          type: string
          nullable: true
          enum:
            - user
            - api_key
            - null
          description: >-
            Principal kind; null for platform-originated entries (those are identified by their `action`, e.g.
            `quotas:MonitorBreach`)
        principal_id:
          type: string
          nullable: true
          description: Public id of the principal (`user_…` or `key_…`); null for platform-originated entries
        action:
          type: string
          description: The permission-action string that authorized the request
          example: secrets:DeleteSecret
        resource_srn:
          type: string
          nullable: true
          description: SRN the action targeted (type-level `srn:{project}:{type}:*` on creates)
          example: srn:proj_V1StGXR8Z5jdHi6B:secret:sec_V1StGXR8Z5jdHi6B
        resource_public_id:
          type: string
          nullable: true
          description: Target resource public id (from the SRN, or the response body on creates)
        status:
          type: integer
          description: HTTP status of the response
          example: 200
        request_id:
          type: string
          nullable: true
          description: Per-request correlation id (also returned in the X-Request-Id header)
        ip:
          type: string
          nullable: true
        user_agent:
          type: string
          nullable: true
        detail:
          type: object
          nullable: true
          description: >-
            Kind-specific payload. Multi-check routes record the remaining checks under `additional_checks`.
            Platform-originated entries set a `detail.kind` discriminator, e.g. `quota_monitor_breach` or
            `guardrail_evaluation`.
          additionalProperties: true
        created_at:
          type: string
          format: date-time
    Chain:
      type: object
      properties:
        id:
          type: string
          example: chain_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: >-
            The agent whose continuation opened the chain. A chain can span agents, so this names its origin rather than
            an owner. Held as a plain id, not a reference the platform maintains — deleting the agent leaves the chain
            record intact.
        status:
          type: string
          enum:
            - active
            - concluded
            - expired
            - budget_exhausted
          description: >-
            `active` — hops are still being spawned. `concluded` — a member finished with nothing left pending; not
            terminal, since a decision months later can spawn another hop and put the chain back to `active`. `expired`
            — a held approval lapsed and the agent does not react to expiry, so nothing resumed it. `budget_exhausted` —
            a hop was refused by the chain budget.
        generation_count:
          type: integer
          description: >-
            Generations in the chain, the root included — the same population `GET /api/v1/generations?chain_id=<id>`
            returns. Re-derived on every hop, so it is a description of the chain, never the thing the budget is
            enforced against.
        last_generation_at:
          type: string
          format: date-time
          nullable: true
          description: When the chain last gained a generation
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Chat:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the chat
          example: chat_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project
          example: proj_V1StGXR8Z5jdHi6B
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >-
            Public ID of the pinned AI provider, or `null` when the chat pins none and inherits its project's
            `default_model_route_id`.
          example: aip_V1StGXR8Z5jdHi6B
        name:
          type: string
          nullable: true
          description: Optional human-readable name
          example: Support Bot
        instructions:
          type: string
          nullable: true
          description: Optional system message sent with every completion
          example: You are a helpful support assistant.
        model:
          type: string
          nullable: true
          description: Optional model override for this chat
          example: gpt-4o
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateChatRequest:
      type: object
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: >-
            Public ID of the AI provider to pin. Optional: omit it to inherit the project's `default_model_route_id`,
            which gives the chat provider failover. Omitting it returns `400` when the project has no default, and
            cannot be combined with `model` — each route target names its own.
          example: aip_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: |
            Public ID of the project. Required when the user belongs to multiple projects and no project key is used.
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional human-readable name
          example: Support Bot
        instructions:
          type: string
          description: Optional system message applied to all completions on this chat
          example: You are a helpful support assistant.
        model:
          type: string
          description: Optional default model override
          example: gpt-4o
    ChatMessageInput:
      type: object
      required:
        - role
      properties:
        role:
          type: string
          enum:
            - user
            - assistant
          example: user
        content:
          type: string
          description: Text content of the message (mutually exclusive with documentId)
          example: What can you help me with?
        document_id:
          x-soat-ref: documents
          type: string
          description: >
            Public ID of a document whose content is used as the message body (mutually exclusive with content). Only
            valid for user/assistant roles.
          example: doc_V1StGXR8Z5jdHi6B
    ChatCompletionRequest:
      type: object
      required:
        - messages
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: >
            Public ID of the AI provider to run the completion against. Mutually exclusive with `chat_id`; exactly one
            of the two is required.
          example: aip_V1StGXR8Z5jdHi6B
        chat_id:
          x-soat-ref: chats
          type: string
          description: >
            Public ID of a stored chat supplying the provider, model and instructions. Mutually exclusive with
            `ai_provider_id`; exactly one of the two is required.
          example: chat_V1StGXR8Z5jdHi6B
        model:
          type: string
          description: |
            Model identifier. Overrides the provider's `default_model`, or the chat's `model`, when specified.
          example: gpt-4o
        instructions:
          type: string
          description: >
            System instructions for this call. Sent to the provider as its `instructions` argument rather than as a
            message, which is the only place the AI SDK accepts system content (`allowSystemInMessages` defaults to
            false). This field is the only channel — a `role: "system"` entry in `messages` is refused with `400
            SYSTEM_MESSAGE_NOT_ALLOWED`. With `chat_id`, this replaces the chat's stored `instructions` for this call
            only; the stored value applies when the request carries none, and the two are never merged.
          example: You are a helpful assistant.
        messages:
          type: array
          minItems: 1
          description: Ordered list of chat messages
          items:
            $ref: '#/components/schemas/ChatMessageInput'
        stream:
          type: boolean
          default: false
          description: >
            When `true` the response is an SSE stream of delta chunks. When `false` (default) a single JSON object is
            returned.
    ChatCompletionResponseMessage:
      type: object
      properties:
        role:
          type: string
          example: assistant
        content:
          type: string
          example: Hello! I am doing well, thank you.
    ChatCompletionChoice:
      type: object
      properties:
        index:
          type: integer
          example: 0
        message:
          $ref: '#/components/schemas/ChatCompletionResponseMessage'
        finish_reason:
          type: string
          example: stop
    ChatCompletionResponse:
      type: object
      properties:
        object:
          type: string
          example: chat.completion
        model:
          type: string
          example: gpt-4o
        choices:
          type: array
          items:
            $ref: '#/components/schemas/ChatCompletionChoice'
    ConversationRecord:
      type: object
      properties:
        id:
          type: string
          description: Conversation ID
          example: conv_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Project ID
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          nullable: true
          description: Optional human-readable name for the conversation.
        status:
          type: string
          enum:
            - open
            - closed
          description: Conversation status
          example: open
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: Actor ID associated with this conversation
          example: actor_V1StGXR8Z5jdHi6B
    ConversationMessageRecord:
      type: object
      properties:
        document_id:
          x-soat-ref: documents
          type: string
          description: Document ID
          example: doc_V1StGXR8Z5jdHi6B
        role:
          type: string
          enum:
            - user
            - assistant
            - system
          description: Role of the message sender
          example: user
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: Optional actor ID associated with this message
          example: actor_V1StGXR8Z5jdHi6B
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: Optional agent ID that generated this message (set for assistant messages produced by generate)
          example: agent_V1StGXR8Z5jdHi6B
        position:
          type: integer
          description: Zero-based position in the conversation
          example: 0
        metadata:
          type: object
          description: Optional structured metadata attached to the message
          nullable: true
          additionalProperties: true
          example:
            phone: '5511999998888'
            channel: whatsapp
        content:
          type: string
          nullable: true
          description: Full text content of the message
    GenerateConversationMessageCompleted:
      type: object
      required:
        - status
        - content
        - message
        - generation_id
        - trace_id
      properties:
        status:
          type: string
          enum:
            - completed
          description: Indicates generation finished successfully.
        content:
          type: string
          description: |
            The AI-generated text of the reply. This is the canonical field for the assistant's response text.
          example: Hello! How can I help you today?
        message:
          $ref: '#/components/schemas/ConversationMessageRecord'
        generation_id:
          x-soat-ref: generations
          type: string
          description: ID of the underlying generation record.
          example: gen_V1StGXR8Z5jdHi6B
        trace_id:
          x-soat-ref: traces
          type: string
          description: Trace ID for observability.
          example: trace_V1StGXR8Z5jdHi6B
        model:
          type: string
          description: Model used for generation.
          example: gpt-4o
    GenerateConversationMessageRequiresAction:
      type: object
      required:
        - status
        - generation_id
        - trace_id
        - required_action
      properties:
        status:
          type: string
          enum:
            - requires_action
          description: |
            Indicates the agent requires tool-call outputs before it can produce a reply. No message is persisted yet.
        generation_id:
          x-soat-ref: generations
          type: string
          description: ID of the paused generation. Pass to the tool-outputs endpoint.
          example: gen_V1StGXR8Z5jdHi6B
        trace_id:
          x-soat-ref: traces
          type: string
          description: Trace ID for observability.
          example: trace_V1StGXR8Z5jdHi6B
        required_action:
          type: object
          description: Tool-call information the client must resolve.
    GenerateConversationMessageResponse:
      oneOf:
        - $ref: '#/components/schemas/GenerateConversationMessageCompleted'
        - $ref: '#/components/schemas/GenerateConversationMessageRequiresAction'
      discriminator:
        propertyName: status
        mapping:
          completed: '#/components/schemas/GenerateConversationMessageCompleted'
          requires_action: '#/components/schemas/GenerateConversationMessageRequiresAction'
    DocumentRecord:
      type: object
      properties:
        id:
          type: string
          description: Document ID
          example: doc_V1StGXR8Z5jdHi6B
        file_id:
          x-soat-ref: files
          type: string
          description: Underlying file ID
          example: file_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Project ID
          example: proj_V1StGXR8Z5jdHi6B
        path:
          type: string
          nullable: true
          description: Logical path of the document within the project (e.g. /reports/q1.txt)
          example: /reports/q1.txt
        filename:
          type: string
          description: Original filename
          example: my-doc.txt
        content_type:
          type: string
          description: Media type of the source file the document was ingested from. Absent when the underlying file is gone.
          example: application/pdf
        size:
          type: integer
          description: File size in bytes
          example: 42
        status:
          type: string
          enum:
            - pending
            - processing
            - ready
            - failed
          description: >-
            Ingestion lifecycle state. `pending` — enqueued; `processing` — chunks being extracted and embedded; `ready`
            — fully indexed; `failed` — processing error (see the `error` field on `GET /documents/{id}/status`).
          example: ready
        content:
          type: string
          nullable: true
          description: Text content (only present on getDocument, and only when status is ready)
          example: The quick brown fox jumps over the lazy dog.
        chunk_strategy:
          type: string
          enum:
            - page
            - whole
            - size
          description: >-
            The chunk strategy the document was last (re-)ingested with. Absent when the default (`whole`) was used —
            the mapper omits the key rather than sending `null`.
          example: size
        chunk_size:
          type: integer
          description: Window size in characters used when `chunk_strategy=size`. Absent otherwise.
          example: 800
        chunk_overlap:
          type: integer
          description: Overlap in characters between consecutive windows used when `chunk_strategy=size`. Absent otherwise.
          example: 120
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    IngestedDocumentRecord:
      allOf:
        - $ref: '#/components/schemas/DocumentRecord'
        - type: object
          properties:
            chunk_count:
              type: integer
              description: Number of chunks created from the file.
              example: 10
    DocumentStatusRecord:
      type: object
      properties:
        id:
          type: string
          description: Document ID
          example: doc_V1StGXR8Z5jdHi6B
        status:
          type: string
          enum:
            - pending
            - processing
            - ready
            - failed
          description: Ingestion lifecycle state.
          example: ready
        chunk_count:
          type: integer
          description: >-
            Number of chunks **currently indexed** for this document (a live count). Grows while `status=processing` and
            equals the final total once `ready`; `0` while `pending`.
          example: 10
        total_chunks:
          type: integer
          nullable: true
          description: >-
            Planned total number of chunks, known once chunking begins. `null` until then. Used as the denominator for
            `progress`.
          example: 12
        total_pages:
          type: integer
          nullable: true
          description: >-
            Number of source pages extracted. Only known after extraction, so it is `null` until `status` is `ready` or
            `failed` (not the same as zero pages).
          example: 12
        progress:
          type: integer
          nullable: true
          description: >-
            Ingestion progress as a percentage (`chunk_count / total_chunks`). `0` while `pending`, climbs while
            `processing` (capped at 99), `100` when `ready`, and `null` when `failed` or not yet computable.
          example: 100
        error:
          type: string
          nullable: true
          description: Failure reason when `status` is `failed` (e.g. `FILE_PARSE_FAILED`, `INGESTION_TIMEOUT`).
          example: INGESTION_TIMEOUT
    EmbeddingsResponse:
      type: object
      description: Response containing generated embeddings. Fields present depend on whether `input` or `inputs` was provided.
      properties:
        embedding:
          type: array
          description: Embedding vector for the single `input` text.
          items:
            type: number
          example:
            - 0.123
            - -0.456
            - 0.789
        embeddings:
          type: array
          description: Embedding vectors for each item in the `inputs` batch.
          items:
            type: array
            items:
              type: number
          example:
            - - 0.123
              - -0.456
              - 0.789
            - - 0.321
              - -0.654
              - 0.987
    DatasetItemInput:
      type: array
      description: Messages replayed verbatim as the generation's input
      items:
        type: object
        required:
          - role
          - content
        properties:
          role:
            type: string
            example: user
          content:
            description: Message content — a string, or AI SDK content parts
            example: When is my invoice issued?
    Scorers:
      type: array
      description: >-
        Scorer configs, a discriminated union on `type`. Each type may appear at most once. Every scorer produces `{
        score: 0–1, passed: boolean }`; binary scorers emit 0 or 1.


        `exact_match` compares the trimmed output text to `expected_output`. `contains` looks for `value` in the output
        text. `json_logic` evaluates `expression` over `{ input, output, object, expected, item.metadata }`, where
        `object` is the structured output (absent when the agent has no `output_schema`). `output_schema` validates the
        structured output against the scorer's own `schema`, falling back to the agent's; it requires the agent to carry
        an `output_schema`, because without one the platform emits no structured output and every item would score 0.


        `llm_judge` grades the output with a model completion, returning a continuous score plus its `reasoning`. Its
        `pass_threshold` is required: a continuous score says nothing about where "good enough" is, and a defaulted
        cutoff would silently decide the gate.


        `embedding_similarity` embeds the output text and `expected_output` with the platform's configured embedding
        model (`EMBEDDING_PROVIDER` / `EMBEDDING_MODEL` — the same stack document ingestion uses) and scores their
        cosine similarity, clamped to 0-1. Its `pass_threshold` is required for the same reason as the judge's. An item
        without an `expected_output` scores 0; an embedding backend failure marks the **item** errored, never a score of
        0.


        `tool` runs a custom scoring algorithm: a server-callable project tool the engine invokes once per item with the
        item's context. Unlike the built-in types it may appear several times, each under a distinct `name` — outcomes
        and aggregates key on the name.
      items:
        oneOf:
          - $ref: '#/components/schemas/ExactMatchScorer'
          - $ref: '#/components/schemas/ContainsScorer'
          - $ref: '#/components/schemas/JsonLogicScorer'
          - $ref: '#/components/schemas/OutputSchemaScorer'
          - $ref: '#/components/schemas/EmbeddingSimilarityScorer'
          - $ref: '#/components/schemas/LlmJudgeScorer'
          - $ref: '#/components/schemas/ToolScorer'
    ExactMatchScorer:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - exact_match
    ContainsScorer:
      type: object
      required:
        - type
        - value
      properties:
        type:
          type: string
          enum:
            - contains
        value:
          type: string
          example: invoice
        case_sensitive:
          type: boolean
          default: false
    JsonLogicScorer:
      type: object
      required:
        - type
        - expression
      properties:
        type:
          type: string
          enum:
            - json_logic
        expression:
          type: object
          additionalProperties: true
          description: A JSON Logic expression; a truthy result scores 1
    OutputSchemaScorer:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - output_schema
        schema:
          type: object
          additionalProperties: true
          description: >-
            JSON Schema the structured output is validated against. Frozen here so two runs stay comparable; falls back
            to the agent's `output_schema` when omitted.
    EmbeddingSimilarityScorer:
      type: object
      required:
        - type
        - pass_threshold
      properties:
        type:
          type: string
          enum:
            - embedding_similarity
        pass_threshold:
          type: number
          minimum: 0
          maximum: 1
          description: >-
            The item passes this scorer when the cosine similarity between the embeddings of the output text and
            `expected_output` is greater than or equal to this value. Required.
          example: 0.85
    LlmJudgeScorer:
      type: object
      required:
        - type
        - prompt
        - pass_threshold
      properties:
        type:
          type: string
          enum:
            - llm_judge
        prompt:
          type: string
          description: >-
            The judge prompt. `{{input}}`, `{{output}}` and `{{expected}}` are replaced with the item's input messages,
            the agent's output text, and the item's `expected_output`. Slots are filled in one pass, so a slot value
            that itself contains `{{output}}` is not re-expanded. The judge must answer with a JSON object carrying a
            numeric `score` between 0 and 1 and an optional `reasoning` string; a reply that does not marks the **item**
            errored, never the run failed and never a score of 0.
          example: >-
            Rate 0-1 how well the answer matches the reference. Answer with {"score": <0-1>, "reasoning": "<why>"}.
            Question: {{input}} Answer: {{output}} Reference: {{expected}}
        pass_threshold:
          type: number
          minimum: 0
          maximum: 1
          description: The item passes this scorer when the judge's score is greater than or equal to this value. Required.
          example: 0.7
        ai_provider_id:
          type: string
          nullable: true
          description: >-
            The AI provider that runs the judge; it must belong to the eval's project. Omit to use the project's default
            model route.
          example: aip_V1StGXR8Z5jdHi6B
        model:
          type: string
          nullable: true
          description: >-
            Overrides the provider's default model. Pinned per scorer, because deltas between runs judged by different
            models are not comparable.
          example: gpt-4o-mini
    ToolScorer:
      type: object
      description: >-
        A custom scoring algorithm — a server-callable project tool the engine invokes once per item. The tool receives
        the same variables a `json_logic` expression reads — `input`, `output`, `object` (when the agent has an
        `output_schema`), `expected`, and `item.metadata` — with `preset_parameters` merged in at the top level, and
        must answer with a JSON object carrying a numeric `score` between 0 and 1, an optional boolean `passed`, and an
        optional `reasoning` string. A malformed answer or a failed call marks the **item** errored, never the run
        failed and never a score of 0.
      required:
        - type
        - name
        - tool_id
      properties:
        type:
          type: string
          enum:
            - tool
        name:
          type: string
          description: >-
            Keys this scorer's outcomes and aggregate scores, so it must be unique within the eval and must not shadow a
            built-in scorer type. Unlike the built-in types, several `tool` scorers may coexist under distinct names.
          example: toxicity
        tool_id:
          x-soat-ref: tools
          type: string
          description: >-
            The tool that scores each item. It must belong to the eval's project and be server-callable (`http`, `mcp`,
            `builtin`, or `pipeline` — a `client` tool pauses for a calling client an eval run does not have).
          example: tool_V1StGXR8Z5jdHi6B
        action:
          type: string
          nullable: true
          description: The operation to invoke; required when the tool type is `builtin` or `mcp`.
          example: score-toxicity
        preset_parameters:
          type: object
          nullable: true
          additionalProperties: true
          description: >-
            Fixed values merged into every call's input at the top level. The engine-injected keys (`input`, `output`,
            `object`, `expected`, `item`) are reserved and rejected.
        pass_threshold:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >-
            Fallback verdict cutoff when the tool answers without a `passed` flag: the item passes this scorer when
            `score` is greater than or equal to this value. A tool-returned `passed` always wins. When the tool omits
            `passed` and no threshold is set, the item is recorded as errored — the scorer produced no verdict.
          example: 0.5
    ScorerResult:
      type: object
      properties:
        scorer:
          type: string
          description: The scorer that produced this entry — the scorer type, or for a `tool` scorer its `name`
          example: contains
        score:
          type: number
          example: 1
        passed:
          type: boolean
        reasoning:
          type: string
          description: The stated rationale; present for `llm_judge` and for `tool` scorers whose tool returned one
    Dataset:
      type: object
      properties:
        id:
          type: string
          example: dset_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DatasetItem:
      type: object
      properties:
        id:
          type: string
          example: dsit_V1StGXR8Z5jdHi6B
        dataset_id:
          type: string
          example: dset_V1StGXR8Z5jdHi6B
        input:
          $ref: '#/components/schemas/DatasetItemInput'
        expected_output:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
          additionalProperties: true
        source_generation_id:
          type: string
          nullable: true
          description: >-
            The generation this item was curated from. A curated item is a deliberate fixture: erasing the source
            generation neither deletes nor mutates it.
          example: gen_V1StGXR8Z5jdHi6B
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Eval:
      type: object
      properties:
        id:
          type: string
          example: eval_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        name:
          type: string
        agent_id:
          x-soat-ref: agents
          type: string
          example: agent_V1StGXR8Z5jdHi6B
        dataset_id:
          type: string
          example: dset_V1StGXR8Z5jdHi6B
        scorers:
          $ref: '#/components/schemas/Scorers'
        pass_threshold:
          type: number
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AggregateScores:
      type: object
      nullable: true
      description: Per-scorer rollup plus the run-level pass rate; null until the run is terminal
      properties:
        scorers:
          type: object
          additionalProperties:
            type: object
            properties:
              mean:
                type: number
              pass_rate:
                type: number
        pass_rate:
          type: number
          nullable: true
          description: Passed items over non-errored items; null when nothing was scorable
        scored_item_count:
          type: integer
          description: Items that produced a score — errored items are excluded
        baseline:
          $ref: '#/components/schemas/BaselineComparison'
    BaselineComparison:
      type: object
      nullable: true
      description: >-
        Comparison against the run named by `baseline_run_id`; absent when the run named none.


        Every number here is computed over the **item intersection** — the dataset items present and scorable in both
        runs — because a delta only means something when both sides answered the same question. The
        compared/added/removed counts make any dataset drift visible instead of letting it read as agent regression.
        Positive deltas mean this run scored higher than the baseline.
      properties:
        run_id:
          type: string
          example: evrun_V1StGXR8Z5jdHi6B
        compared_item_count:
          type: integer
          description: Items scorable in both runs — the basis of every delta
        added_item_count:
          type: integer
          description: Scorable here but not in the baseline (added, or errored there)
        removed_item_count:
          type: integer
          description: Scorable in the baseline but not here (removed, or errored here)
        pass_rate_delta:
          type: number
          nullable: true
          description: Run-level pass-rate delta over the intersection; null when the two runs share no comparable item
        scorers:
          type: object
          description: >-
            Per-scorer deltas, keyed by scorer type. A scorer only one of the two runs ran is omitted rather than
            compared against nothing.
          additionalProperties:
            type: object
            properties:
              mean_delta:
                type: number
              pass_rate_delta:
                type: number
    EvalRun:
      type: object
      properties:
        id:
          type: string
          example: evrun_V1StGXR8Z5jdHi6B
        eval_id:
          type: string
          example: eval_V1StGXR8Z5jdHi6B
        agent_version:
          type: integer
          description: The one agent version every item in this run executed against
          example: 3
        status:
          type: string
          enum:
            - queued
            - running
            - completed
            - failed
            - canceled
        baseline_run_id:
          type: string
          nullable: true
          example: evrun_V1StGXR8Z5jdHi6B
        trigger_id:
          type: string
          nullable: true
          description: >-
            The trigger that started this run — set when a schedule (or a manual trigger fire) started it, null for a
            run started through this API. Kept even if the trigger is later deleted.
          example: trg_V1StGXR8Z5jdHi6B
        aggregate_scores:
          $ref: '#/components/schemas/AggregateScores'
        passed:
          type: boolean
          nullable: true
          description: Null when the eval declares no pass_threshold, and until the run is terminal
        item_count:
          type: integer
        completed_count:
          type: integer
        errored_count:
          type: integer
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            The caller-owned key/value metadata supplied when the run was started, returned verbatim. Null when the run
            was started without any (a trigger-started run included — see `trigger_id` for that provenance). The server
            writes nothing here.
          example:
            commit_sha: 9f2c1ab
            ci_job: nightly-evals
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    EvalResult:
      type: object
      properties:
        id:
          type: string
          example: evres_V1StGXR8Z5jdHi6B
        eval_run_id:
          type: string
          example: evrun_V1StGXR8Z5jdHi6B
        dataset_item_id:
          type: string
          nullable: true
          description: Null once the dataset item has been deleted
          example: dsit_V1StGXR8Z5jdHi6B
        input:
          $ref: '#/components/schemas/DatasetItemInput'
        expected_output:
          type: string
          nullable: true
        generation_id:
          type: string
          nullable: true
          example: gen_V1StGXR8Z5jdHi6B
        output:
          type: string
          nullable: true
          description: >-
            The agent's final output text. Cleared when the linked generation's content is purged; the scores and the
            frozen input survive.
        scores:
          type: array
          items:
            $ref: '#/components/schemas/ScorerResult'
        passed:
          type: boolean
          description: AND over the per-scorer passed flags
        error:
          type: string
          nullable: true
          description: >-
            Item-level failure reason. A generation that did not complete — a `requires_action` pause, a provider
            failure — is recorded here and excluded from the aggregates rather than scored 0.
        created_at:
          type: string
          format: date-time
    ExceptionItem:
      type: object
      properties:
        id:
          type: string
          example: exc_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        status:
          type: string
          enum:
            - open
            - acknowledged
            - resolved
        severity:
          type: string
          enum:
            - info
            - warning
            - critical
        kind:
          type: string
          enum:
            - run_failed
            - guardrail_tripwire
            - approval_expired
            - quota_unpriced
            - event_trigger_loop
            - chain_limit
            - manual
          description: How the exception was filed
        title:
          type: string
          description: Human-readable one-line summary
        detail:
          type: object
          nullable: true
          description: Structured context (tool, args digest, error message, guardrail version)
        occurrence_count:
          type: integer
          description: How many times this exact failure has been observed while open
        last_seen_at:
          type: string
          format: date-time
          description: Timestamp of the most recent occurrence
        orchestration_run_id:
          type: string
          nullable: true
          description: Originating orchestration run
        node_id:
          type: string
          nullable: true
          description: Originating node id within the run's graph
        agent_id:
          type: string
          nullable: true
          description: Associated agent
        guardrail_version:
          type: string
          nullable: true
          description: '`<guardrailId>@<version>` for a guardrail_tripwire exception'
        acknowledged_by:
          type: string
          nullable: true
          description: Acknowledging user's public ID
        resolved_by:
          type: string
          nullable: true
          description: Resolving user's public ID
        resolution_note:
          type: string
          nullable: true
          description: Optional note recorded at resolution
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    UploadFileBase64Request:
      type: object
      required:
        - content
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
          example: proj_V1StGXR8Z5jdHi6B
        content:
          type: string
          description: Base64-encoded file content
          example: SGVsbG8gV29ybGQ=
        prefix:
          type: string
          description: Directory within the project (e.g. /documents). Optional; defaults to / (root).
          example: /documents
        filename:
          type: string
          description: Original / download name and the key's leaf segment.
          example: document.txt
        content_type:
          type: string
          description: MIME type of the file
          example: text/plain
        metadata:
          type: string
          description: JSON string with additional metadata
          example: '{"author":"John"}'
    PresignedUrlRequest:
      type: object
      required:
        - project_id
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
          example: proj_V1StGXR8Z5jdHi6B
        prefix:
          type: string
          description: Directory within the project (e.g. /documents). Optional; defaults to / (root).
          example: /documents
        filename:
          type: string
          description: Original / download name and the key's leaf segment.
          example: report.pdf
        content_type:
          type: string
          description: MIME type of the file to be uploaded
          example: application/pdf
    PresignedUrlResponse:
      type: object
      properties:
        upload_token:
          type: string
          description: Single-use token value
          example: upt_V1StGXR8Z5jdHi6B
        upload_url:
          type: string
          description: >-
            Fully-qualified URL to POST the file content to (absolute when SOAT_BASE_URL is configured, relative
            otherwise).
          example: https://api.example.com/api/v1/files/upload/upt_V1StGXR8Z5jdHi6B
        expires_at:
          type: string
          format: date-time
          description: Token expiration timestamp
    UploadFileWithTokenRequest:
      type: object
      properties:
        content:
          type: string
          description: Base64-encoded file content (alternative to multipart `file`)
          example: SGVsbG8gV29ybGQ=
        filename:
          type: string
          description: Original / download name (overrides the token's filename)
          example: document.txt
        content_type:
          type: string
          description: MIME type of the file (overrides the token's content type)
          example: text/plain
        metadata:
          type: string
          description: JSON string with additional metadata
          example: '{"author":"John"}'
    FileRecord:
      type: object
      description: Stored file metadata
      properties:
        id:
          type: string
          description: Unique file identifier
          example: abc123
        prefix:
          type: string
          readOnly: true
          description: Directory of the file (the `path` without its last segment). Read-only — set it via `prefix` on write.
          example: /images
        filename:
          type: string
          description: Original / download name and the key's leaf segment.
          example: logo.png
        path:
          type: string
          nullable: true
          readOnly: true
          description: >-
            Full key of the file within the project — `prefix` + `/` + `filename` (e.g. /images/logo.png). Read-only;
            unique per project; the file's identity and policy-SRN target.
          example: /images/logo.png
        content_type:
          type: string
          nullable: true
          description: MIME type of the file
          example: application/pdf
        size:
          type: integer
          nullable: true
          description: File size in bytes
          example: 1024
        metadata:
          type: string
          nullable: true
          description: JSON string with additional metadata
          example: '{"author":"John"}'
        tags:
          type: object
          additionalProperties:
            type: string
          description: Key-value tags attached to the file.
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
    FormationTemplateInput:
      description: >
        A formation template supplied as either a JSON object or a YAML/JSON string. When a string is provided the
        server parses it with a YAML parser (JSON is valid YAML) before processing.
      oneOf:
        - $ref: '#/components/schemas/FormationTemplate'
        - type: string
          description: YAML or JSON string representation of a FormationTemplate
    FormationTemplate:
      type: object
      required:
        - resources
      properties:
        parameters:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/ParameterDeclaration'
          description: >
            Declared parameters for this template. Each parameter may have a default value and an optional description.
            Parameters without a default must be supplied in the `parameters` field of the deploy request.
          nullable: true
        resources:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/ResourceDeclaration'
          description: Map of logical resource IDs to resource declarations
        outputs:
          type: object
          additionalProperties: true
          description: >
            Map of output names to values. Values may use `{ "ref": "logicalId" }` to reference physical IDs of created
            resources, or `{ "param": "ParamName" }` and `{ "sub": "text ${ParamName}" }` to embed parameter values.
          nullable: true
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >
            Arbitrary metadata attached to the template. Supports the same substitution as `outputs`: `{ "ref":
            "logicalId" }` resolves to a created resource's physical ID, and `{ "param": "ParamName" }` / `{ "sub":
            "text ${ParamName}" }` embed parameter values. The raw expressions are preserved here; the resolved values
            from the last deploy are exposed on the formation's `resolved_metadata` field.
    ParameterDeclaration:
      type: object
      properties:
        type:
          type: string
          description: Parameter type (currently only 'string' is supported)
          example: string
        default:
          type: string
          description: Default value used when the parameter is not supplied at deploy time
          nullable: true
        description:
          type: string
          description: Human-readable description of what this parameter represents
          nullable: true
        no_echo:
          type: boolean
          description: >
            When true, the parameter value should be treated as sensitive and not echoed in logs or UI. Analogous to
            NoEcho in CloudFormation.
          nullable: true
        use_previous_value:
          type: boolean
          description: >
            When true, omitting this parameter on update reuses its previously stored value instead of failing the
            required-parameter check — analogous to CloudFormation's UsePreviousValue, declared in the template. An
            explicitly supplied value still overrides. Has no effect on create (there is no previous value yet). The
            value is reused only where the underlying resource retains it (e.g. a secret's encrypted value); otherwise
            the last-applied value is used.
          nullable: true
    AgentResourceProperties:
      description: >-
        Creates an AI agent backed by a provider. The agent handles requests, runs tools, and can be attached to actors.
        Exactly one of `ai_provider_id` or `model_route_id` must be declared. Switching an existing agent between the
        two declares the new field together with an explicit `null` for the old one.
      type: object
      additionalProperties: false
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: Public ID of the AI provider to pin. Mutually exclusive with `model_route_id`.
        model_route_id:
          x-soat-ref: model-routes
          type: string
          nullable: true
          description: >-
            Public ID of a model route in the same project — the agent's completion model is resolved through the
            route's ordered targets with failover. Mutually exclusive with `ai_provider_id` and `model`.
        name:
          type: string
          nullable: true
          description: Agent display name
        instructions:
          type: string
          nullable: true
          description: System instructions for the agent
        model:
          type: string
          nullable: true
          description: Model identifier (overrides provider default)
        tool_bindings:
          type: array
          nullable: true
          items:
            type: object
          description: >-
            Tools to attach, one binding object per tool: `{ tool_id }`. Tool-call gating is owned by guardrails
            (attached via `guardrail_ids` on the project, agent, or tool), not by the binding. Inline `tool` entries are
            not supported in templates; declare a tool resource and reference it via `tool_id` (a `{ "ref": … }` to a
            tool resource in the same template resolves at deploy time).
        max_steps:
          type: integer
          nullable: true
          description: Maximum number of agentic steps per generation
        tool_choice:
          nullable: true
          description: >-
            Controls how the model selects tools. Accepts a string (`"auto"`, `"required"`) or an object (`{ "type":
            "tool", "tool_name": "my_tool" }`).
        stop_conditions:
          type: array
          nullable: true
          description: >-
            Conditions that stop the agent's work early — turn-scoped (`has_tool_call`) or chain-scoped
            (`max_chain_generations`).
          items:
            type: object
            properties:
              type:
                type: string
                description: Condition type — `has_tool_call` or `max_chain_generations`
              tool_name:
                type: string
                nullable: true
                description: Tool name to match when type is `has_tool_call`
              max_generations:
                type: integer
                nullable: true
                description: Generations the continuation chain may reach when type is `max_chain_generations`
        active_tool_ids:
          x-soat-ref: tools
          type: array
          nullable: true
          items:
            type: string
          description: Subset of the bound tools that are active
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the agent scope.
        step_rules:
          type: array
          nullable: true
          description: Per-step overrides applied during multi-step generation. Steps not covered by a rule use the agent defaults.
          items:
            type: object
            properties:
              step:
                type: integer
                description: 1-indexed step number this rule applies to
              tool_choice:
                type: object
                nullable: true
                description: 'Tool choice override for this step, e.g. `auto`, `required`, or `{ type: tool, tool_name: search }`'
              active_tool_ids:
                x-soat-ref: tools
                type: array
                nullable: true
                items:
                  type: string
                description: Tool IDs active on this step
        boundary_policy:
          type: object
          nullable: true
          description: >-
            Restricts which SOAT actions the agent may invoke. Evaluated as the intersection with the caller's own
            policy.
          properties:
            statement:
              type: array
              description: List of IAM policy statements
              items:
                type: object
                properties:
                  effect:
                    type: string
                    description: Effect — `Allow` or `Deny`
                  action:
                    type: array
                    items:
                      type: string
                    description: IAM action strings, e.g. `memories:*` or `agents:DeleteAgent`
                  resource:
                    type: array
                    nullable: true
                    items:
                      type: string
                    description: Resource SRN patterns (optional; omit to match all resources)
        temperature:
          type: number
          nullable: true
          description: Sampling temperature
        max_context_messages:
          type: integer
          nullable: true
          description: >-
            Maximum number of recent messages to include in the context window sent to the model. When null, all
            messages are included.
        single_session_per_actor:
          type: boolean
          nullable: true
          description: When true, only one open session per actor_id is allowed for this agent.
        trace_content_mode:
          type: string
          nullable: true
          description: >-
            Agent-scope zero-retention setting (`full` or `none`). `null` inherits the project's setting. `full` is
            refused when the project's own mode is `none`.
        on_approval_expiry:
          type: string
          nullable: true
          description: >-
            What happens when a held tool call expires un-approved: `terminate` (the default when null) ends the chain,
            `react` spawns a continuation that reports the staleness to the agent.
        knowledge_config:
          type: object
          nullable: true
          description: >-
            Knowledge retrieval configuration. When set, relevant documents and memory entries are injected into every
            generation.
          properties:
            memory_ids:
              x-soat-ref: memories
              type: array
              items:
                type: string
              description: Public IDs of memories to retrieve from
            memory_tags:
              type: array
              items:
                type: string
              description: Retrieve from all memories matching these tags
            document_ids:
              x-soat-ref: documents
              type: array
              items:
                type: string
              description: Public IDs of documents to retrieve from
            document_paths:
              type: array
              items:
                type: string
              description: Retrieve from all documents matching these path prefixes
            min_score:
              type: number
              description: Minimum similarity score (0–1) for retrieved chunks
            limit:
              type: integer
              description: Maximum number of chunks to inject
            write_memory_id:
              x-soat-ref: memories
              type: string
              nullable: true
              description: >-
                Public ID of the memory the agent can write to. When set, a `write_memory` tool is automatically
                available to the agent.
            extraction:
              description: >-
                Automatic fact extraction from completed generation turns (requires write_memory_id). Pass `true` to
                enable with defaults, or an object to customize the provider, model, and prompt used for the extraction
                completion.
              oneOf:
                - type: boolean
                - type: object
                  properties:
                    enabled:
                      type: boolean
                      description: >-
                        Defaults to true when the object form is used. Set false to keep the configuration but disable
                        extraction.
                    ai_provider_id:
                      x-soat-ref: ai-providers
                      type: string
                      description: >-
                        AI provider override for extraction calls. Must belong to the agent's project. Its default_model
                        becomes the model fallback.
                    model:
                      type: string
                      description: Model override for extraction calls.
                    prompt:
                      type: string
                      description: >-
                        Replaces the default task instructions. The JSON response contract and the conversation
                        transcript are always appended by the server.
        output_schema:
          type: object
          nullable: true
          description: >-
            JSON Schema describing the structured object the model must return. Non-streaming generations are
            constrained to this schema; the parsed value is returned as `output.object`.
    ActorResourceProperties:
      description: >-
        Creates a stateful conversation actor that wraps an agent or chat session and optionally links to a memory
        store.
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: Actor display name
        external_id:
          type: string
          nullable: true
          description: External identifier for idempotent actor creation
        instructions:
          type: string
          nullable: true
          description: Persona-specific instructions
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: Linked agent ID (mutually exclusive with chat_id)
        chat_id:
          x-soat-ref: chats
          type: string
          nullable: true
          description: Linked chat ID (mutually exclusive with agent_id)
    AiProviderResourceProperties:
      description: Configures an LLM provider connection (API key, model, endpoint) that agents use to generate responses.
      type: object
      additionalProperties: false
      required:
        - name
        - provider
        - default_model
      properties:
        name:
          type: string
          description: Provider display name
        provider:
          type: string
          enum:
            - openai
            - anthropic
            - google
            - xai
            - groq
            - ollama
            - azure
            - bedrock
            - vertex
            - gateway
            - custom
          description: Provider type
        default_model:
          type: string
          description: Default model identifier (e.g. gpt-4o, claude-3-7-sonnet)
        secret_id:
          x-soat-ref: secrets
          type: string
          nullable: true
          description: Public ID of the secret containing the API key
        base_url:
          type: string
          nullable: true
          description: Custom base URL for the provider API (self-hosted or proxy)
        config:
          type: object
          nullable: true
          description: Provider-specific extra configuration
    ToolResourceProperties:
      description: Defines a tool (HTTP endpoint, MCP server, SOAT action, or pipeline) that agents can invoke during a generation.
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: Tool display name
        type:
          type: string
          nullable: true
          description: Tool type hint (e.g. http, mcp, builtin, pipeline)
        description:
          type: string
          nullable: true
          description: Tool description shown to the model
        parameters:
          type: object
          nullable: true
          description: JSON Schema describing the tool's input parameters (free-form, user-defined)
        execute:
          type: object
          nullable: true
          description: HTTP execution configuration. Required for `http` tools.
          properties:
            url:
              type: string
              description: Endpoint URL. Supports `{param}` placeholders resolved from tool arguments.
            method:
              type: string
              nullable: true
              description: 'HTTP method (default: `POST`)'
            headers:
              type: object
              nullable: true
              description: Static headers included in every request
            body_mode:
              type: string
              nullable: true
              description: >-
                Request body encoding for `POST`/`PUT`/`PATCH`: `json` (default) or `multipart`. Incompatible with
                `auth.type: aws_sigv4`.
            auth:
              type: object
              nullable: true
              description: >-
                Computed request credential. `type` is `aws_sigv4` (with `region`, `service`, `access_key_id`,
                `secret_access_key` and optional `session_token`) or `gcp_service_account` (with `credentials` and
                `scopes`). Credential fields accept `{{secret:...}}` references.
        mcp:
          type: object
          nullable: true
          description: MCP server connection configuration. Required for `mcp` tools.
          properties:
            url:
              type: string
              description: MCP server URL
            headers:
              type: object
              nullable: true
              description: Headers included in every MCP request
        actions:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Allowlist of actions the tool exposes. For `builtin` tools: SOAT platform action names. For `mcp` tools: an
            optional allowlist of MCP tool names to scope the server surface (`null` exposes every tool).
        denied_actions:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            For `mcp` tools: an optional denylist of MCP tool names to hide. Applied after `actions` and taking
            precedence over it — the ergonomic way to scope a read+write MCP server read-only by denying just the write
            tools. `null` denies nothing.
        context_keys:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Optional allowlist of `tool_context` keys forwarded to this tool as prefixed context headers. `null` or
            omitted forwards every key; `[]` forwards none. The server-pinned identity keys (`session_id`, `actor_id`,
            `actor_external_id`) are always forwarded, and a key consumed by a `{{context:<key>}}` token in this tool's
            own headers is substituted regardless of this list.
        preset_parameters:
          type: object
          nullable: true
          description: Pre-filled parameter values injected at execution time
        pipeline:
          type: object
          nullable: true
          description: >-
            Pipeline definition for `pipeline` tools: an ordered `steps` array, each invoking another tool by `tool_id`
            (optional `action`) with an `input` built from earlier results via JSON Logic over `{ input, steps }`, plus
            an optional `output` mapping. Step `input` keys and `var` paths use camelCase (the runtime form). Free-form,
            user-defined.
        output_mapping:
          type: object
          nullable: true
          description: >-
            Universal JSON Logic mapping applied to the tool's raw result, for every tool type. Evaluated over `{
            output: <raw result> }`, e.g. `{ "var": "output.text" }`. For `pipeline` tools this runs after the
            pipeline's own `output` mapping.
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the tool scope.
    DatasetResourceProperties:
      description: >-
        Declares an evaluation dataset — the named fixture suite an eval runs an agent against. Its test cases are
        declared separately as `dataset_item` resources, so an item curated through the API is never collateral of a
        formation apply. Deleting the dataset deletes its items and the evals bound to it.
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: Dataset name, unique within the project
        description:
          type: string
          nullable: true
          description: Optional description
    DatasetItemResourceProperties:
      description: >-
        One test case in a dataset: the messages sent to the agent under test and, optionally, the reference answer
        scorers compare against. Editing or removing an item never rewrites a run that already scored it — each result
        froze its own copy.
      type: object
      additionalProperties: false
      required:
        - dataset_id
        - input
      properties:
        dataset_id:
          x-soat-ref: datasets
          type: string
          description: Public ID of the parent dataset (or ref expression)
        input:
          type: array
          items:
            type: object
          description: The messages sent to the agent, as `{role, content}` objects
        expected_output:
          type: string
          nullable: true
          description: Reference answer for exact_match / contains / embedding_similarity / llm_judge scorers
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: 'Free-form tags on the case, e.g. `{"topic": "billing"}`'
    EvalResourceProperties:
      description: >-
        Binds an agent under test to a dataset and the scorers its outputs are judged by. `pass_threshold` is the pass
        rate a run must reach for its `passed` verdict — the gate an agent-version promotion consumes.
      type: object
      additionalProperties: false
      required:
        - name
        - agent_id
        - dataset_id
        - scorers
      properties:
        name:
          type: string
          description: Eval name, unique within the project
        agent_id:
          x-soat-ref: agents
          type: string
          description: Public ID of the agent under test (or ref expression)
        dataset_id:
          x-soat-ref: datasets
          type: string
          description: Public ID of the dataset to run against (or ref expression)
        scorers:
          type: array
          items:
            type: object
          description: >-
            Scorer configs — `exact_match`, `contains`, `json_logic`, `output_schema`, `embedding_similarity`,
            `llm_judge`, or `tool`. Same shape as the evals REST contract.
        pass_threshold:
          type: number
          nullable: true
          description: >-
            0–1. A run passes when its pass rate over non-errored items reaches this. Omit for a run that reports scores
            without a verdict.
    DocumentResourceProperties:
      description: Stores a text document in a project, optionally indexing it for knowledge retrieval.
      type: object
      additionalProperties: false
      required:
        - content
      properties:
        content:
          type: string
          description: Document text content
        path:
          type: string
          nullable: true
          description: Virtual path for organising the document
        filename:
          type: string
          nullable: true
          description: Original filename
        title:
          type: string
          nullable: true
          description: Document title
        metadata:
          type: object
          nullable: true
          description: Arbitrary metadata key-value pairs
        tags:
          type: object
          nullable: true
          description: Tag key-value pairs for filtering
        chunk_strategy:
          type: string
          enum:
            - page
            - whole
            - size
          description: >-
            How to split the content into embeddable chunks, matching `POST /documents`. `whole` (default) stores the
            content as a single chunk; `size` splits into fixed-size character windows with overlap. `page` is
            equivalent to `whole` for plain text.
          default: whole
        chunk_size:
          type: integer
          description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
        chunk_overlap:
          type: integer
          description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults to 200.
    MemoryResourceProperties:
      description: Creates a named memory store that actors can read from and write to across conversations.
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: Memory display name
        description:
          type: string
          nullable: true
          description: What this memory stores
        tags:
          type: array
          nullable: true
          items:
            type: string
          description: Tag strings for filtering
    MemoryEntryResourceProperties:
      description: Adds a single text entry to a memory store.
      type: object
      additionalProperties: false
      required:
        - memory_id
        - content
      properties:
        memory_id:
          x-soat-ref: memories
          type: string
          description: Public ID of the parent memory (or ref expression)
        content:
          type: string
          description: Text content of the memory entry
        source_type:
          type: string
          enum:
            - manual
            - agent
            - extraction
            - orchestration
          description: How this entry was created (defaults to manual)
        tags:
          type: array
          nullable: true
          items:
            type: string
          description: Per-entry tag strings for entry-granularity filtering
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Arbitrary structured metadata attached to the entry
    ModelRouteResourceProperties:
      description: >-
        Declares a model route within the formation's project: a named, ordered list of provider+model failover targets
        with retry and circuit-breaker configuration. Consumers reference it through their own `model_route_id`, or
        inherit it as the project's `default_model_route_id`.
      type: object
      additionalProperties: false
      required:
        - name
        - targets
      properties:
        name:
          type: string
          description: Route name, unique within the project
        targets:
          type: array
          description: >-
            Ordered failover targets, tried in array order. Each entry is `{ ai_provider_id, model, timeout_seconds?,
            max_retries? }`; every provider must belong to this project, and the total attempt budget (sum of `1 +
            max_retries`) is capped at 10.
          items:
            type: object
        retry_on:
          type: array
          description: >-
            Which failure classes fail over: any of `provider_error`, `timeout`, `rate_limited`. Defaults to all three.
            Deterministic rejections (400-class, auth, content policy) never fail over.
          items:
            type: string
        failure_threshold:
          type: integer
          nullable: true
          description: Consecutive retryable failures before a target is skipped (default 3)
        cooldown_seconds:
          type: integer
          nullable: true
          description: How long a tripped target is skipped before being probed again (default 60)
    WebhookResourceProperties:
      description: Registers an HTTPS endpoint to receive SOAT platform event notifications.
      type: object
      additionalProperties: false
      required:
        - name
        - url
        - events
      properties:
        name:
          type: string
          description: Webhook display name
        description:
          type: string
          nullable: true
          description: Optional description
        url:
          type: string
          description: HTTPS endpoint that receives event payloads
        events:
          type: array
          items:
            type: string
          description: Event types to subscribe to (e.g. memory.updated)
    TriggerResourceProperties:
      description: >-
        Binds a starter (manual, webhook, schedule, or event) to an executable target (orchestration, agent, tool, or
        eval). Firings run under the confined run-as identity of the caller who deployed the formation, so a firing
        never exceeds what that caller could do directly.
      type: object
      additionalProperties: false
      required:
        - name
        - type
        - target_type
        - target_id
      properties:
        name:
          type: string
          description: Trigger display name (unique within the project)
        description:
          type: string
          nullable: true
          description: Optional description
        type:
          type: string
          enum:
            - manual
            - webhook
            - schedule
            - event
          description: Starter type. Immutable after creation
        target_type:
          type: string
          enum:
            - orchestration
            - agent
            - tool
            - eval
          description: The kind of resource this trigger activates
        target_id:
          type: string
          description: >-
            Public ID of the target resource. Use { "ref": "LogicalId" } to reference an orchestration, agent, tool, or
            eval defined in the template.
        action:
          type: string
          nullable: true
          description: Tool targets only — the action for builtin/mcp tools
        input:
          type: object
          nullable: true
          description: Static input shallow-merged under each firing's runtime input
        cron:
          type: string
          nullable: true
          description: 5-field cron expression (UTC). Required when type is schedule
        event_pattern:
          type: string
          nullable: true
          description: >-
            Internal-event subscription pattern (`*`, `prefix.*`, or an exact event name). Required when type is event,
            rejected otherwise
        active:
          type: boolean
          description: Whether the trigger fires (default true)
        policy_id:
          x-soat-ref: policies
          type: string
          nullable: true
          description: Optional boundary policy that further confines the run-as identity
    ApiKeyResourceProperties:
      description: >-
        Creates an API key scoped to the formation's project and optionally restricted by a set of policies. The key is
        owned by the caller who deployed the formation, exactly as one created through the API is, so it never carries
        more access than they already have.
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: Human-readable label for the API key
        policy_ids:
          x-soat-ref: policies
          type: array
          items:
            type: string
          description: Optional list of policy public IDs that further restrict the key's permissions
    ChatResourceProperties:
      description: >-
        Creates a chat within the formation's project, connected either to an AI provider or — by declaring no provider
        — to the project's `default_model_route_id`.
      type: object
      additionalProperties: false
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >-
            Public ID of the AI provider to use for this chat. Omit (or declare `null`) to inherit the project's
            `default_model_route_id`, which requires the project to have one and cannot be combined with `model`.
        name:
          type: string
          nullable: true
          description: Human-readable label for the chat
        instructions:
          type: string
          nullable: true
          description: System message to set the assistant behaviour
        model:
          type: string
          nullable: true
          description: Model override; defaults to the AI provider's default model
    ConversationResourceProperties:
      description: Creates a conversation within the formation's project.
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          nullable: true
          description: Human-readable label for the conversation
        status:
          type: string
          description: Initial status of the conversation (open or closed)
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: Public ID of an actor to associate with this conversation
    FileResourceProperties:
      description: Registers a file record within the formation's project.
      type: object
      additionalProperties: false
      properties:
        prefix:
          type: string
          nullable: true
          description: >-
            Directory within the project. Optional; defaults to / (root). Combined with filename to form the file's key
            (path).
        filename:
          type: string
          nullable: true
          description: Original / download name and the key's leaf segment.
        content_type:
          type: string
          nullable: true
          description: MIME type of the file
        size:
          type: integer
          nullable: true
          description: File size in bytes
        metadata:
          type: string
          nullable: true
          description: JSON string with additional metadata
    PolicyResourceProperties:
      description: Creates an access-control policy within the formation's project.
      type: object
      additionalProperties: false
      required:
        - document
      properties:
        name:
          type: string
          nullable: true
          description: Human-readable label for the policy
        description:
          type: string
          nullable: true
          description: Description of what the policy grants
        document:
          type: object
          additionalProperties: true
          description: Policy document containing an array of statements
    SecretResourceProperties:
      description: Creates an encrypted secret within the formation's project.
      type: object
      additionalProperties: false
      required:
        - name
        - value
      properties:
        name:
          type: string
          description: Human-readable label for the secret
        value:
          type: string
          description: The secret value to encrypt and store
    ProjectPriceResourceProperties:
      description: >-
        Upserts a project-scoped price row so a deployed stack produces billing-grade usage cost with no out-of-band
        pricing step. The row is keyed on (provider, model, component, effective_from) within the formation's project —
        the middle pricing tier that covers every one of the project's instances of a given provider slug. When
        `effective_from` is omitted the price takes effect at deploy time, so generations run right after deploy are
        priced.
      type: object
      additionalProperties: false
      required:
        - provider
        - model
        - component
        - unit
        - unit_price
      properties:
        provider:
          type: string
          description: SKU vendor slug the price applies to (e.g. openai, anthropic, soat)
        model:
          type: string
          description: SKU identifier — the model id for LLM SKUs, the platform unit otherwise
        component:
          type: string
          description: The billable component this row prices (input_tokens, output_tokens, cached_tokens, compute_second, …)
        unit:
          type: string
          description: Unit the unit_price is denominated in (token, compute_second, …); must match the metered component's unit
        unit_price:
          type: number
          description: USD per unit. Must be a non-negative number
        meter_type:
          type: string
          description: Meter type this SKU belongs to (defaults to llm_tokens)
        effective_from:
          type: string
          format: date-time
          description: >-
            Timestamp from which this price applies. Omit to take effect at deploy time. The row with the latest
            effective_from at or before now() prices a call.
    SessionResourceProperties:
      description: Creates a session attached to an agent within the formation's project.
      type: object
      additionalProperties: false
      required:
        - agent_id
      properties:
        agent_id:
          x-soat-ref: agents
          type: string
          description: Public ID of the agent that owns this session
        name:
          type: string
          nullable: true
          description: Human-readable label for the session
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: Public ID of an actor to associate with this session
        auto_generate:
          type: boolean
          description: Whether to automatically generate a response when messages are sent
        inactivity_ttl_seconds:
          type: integer
          description: Number of seconds of inactivity after which the session expires. 0 means never expires.
        tool_context:
          type: object
          additionalProperties: true
          nullable: true
          description: Optional context object passed to tool calls
    IngestionRuleResourceProperties:
      description: >-
        Routes a file content_type to a converter (tool or agent) so ingestion can turn non-native files (images, audio,
        scanned PDFs) into Documents. See the Ingestion Rules module docs for the matching and converter-invocation
        model.
      type: object
      additionalProperties: false
      required:
        - content_type_glob
      properties:
        content_type_glob:
          type: string
          description: MIME type glob matched against a file's content_type (e.g. image/*, audio/mpeg, application/pdf)
        tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
          description: Converter tool ID (mutually exclusive with agent_id)
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: Converter agent ID (mutually exclusive with tool_id)
        action:
          type: string
          nullable: true
          description: Operation id, required for builtin/mcp tool converters
        preset_parameters:
          type: object
          nullable: true
          description: Merged into the tool input before invocation (tool converters only)
        native_extraction:
          type: string
          nullable: true
          description: >-
            For native types (PDF/text): `first` (default) converts only when native extraction yields no text; `skip`
            always converts.
        file_delivery:
          type: string
          nullable: true
          description: How the file reaches a tool converter — base64 (default) or download_url
        chunk_strategy:
          type: string
          nullable: true
          description: Default chunk strategy (page/whole/size), overridable per ingest request
        chunk_size:
          type: integer
          nullable: true
          description: Default window size in characters for the size strategy
        chunk_overlap:
          type: integer
          nullable: true
          description: Default overlap in characters for the size strategy
        metadata:
          type: object
          nullable: true
          description: Arbitrary JSON metadata
    OrchestrationResourceProperties:
      description: >-
        Creates a DAG orchestration that wires agents, tools, and knowledge lookups into a repeatable pipeline within
        the formation's project. Node resource references (`agent_id`, `tool_id`, `memory_id`, `orchestration_id`)
        accept `{ "ref": "LogicalId" }` expressions to point at other resources declared in the same template — the
        basis for deploying an agent "squad" (a team of agents plus the flow that coordinates them) as a single stack.
      type: object
      additionalProperties: false
      required:
        - name
        - nodes
        - edges
      properties:
        name:
          type: string
          description: Human-readable name for the orchestration
        description:
          type: string
          nullable: true
          description: Optional description of what the orchestration does
        nodes:
          type: array
          description: >-
            Ordered list of node definitions. A node's resource references (`agent_id`, `tool_id`, `memory_id`,
            `orchestration_id`) may use `{ "ref": "LogicalId" }` to bind to other resources in the template.
          items:
            type: object
            additionalProperties: true
        edges:
          type: array
          description: Directed connections between nodes
          items:
            type: object
            additionalProperties: true
        state_schema:
          type: object
          nullable: true
          additionalProperties: true
          description: Optional JSON Schema describing the run state
        input_schema:
          type: object
          nullable: true
          additionalProperties: true
          description: Optional JSON Schema describing the run input
    WorkflowResourceProperties:
      description: >-
        Creates a workflow — a state-machine definition (named states, allowed transitions, guards, and per-state
        automation) that tasks live in. State and transition dispatch references (`agent_id`, `orchestration_id`,
        `tool_id` inside an `on_enter` block) accept `{ "ref": "LogicalId" }` expressions to point at agents,
        orchestrations or tools declared in the same template, so a workflow plus the agents and tools that service its
        states can deploy as one stack. Mirrors the workflows REST contract (`states`, `transitions`, `payload_schema`).
      type: object
      additionalProperties: false
      required:
        - name
        - states
        - transitions
      properties:
        name:
          type: string
          description: Human-readable name for the workflow, unique within the project
        description:
          type: string
          nullable: true
          description: Optional description of what the workflow models
        states:
          type: array
          description: >-
            Named states. Exactly one must be `initial: true`; any number may be `terminal: true`. A `kind: human` state
            parks the task until a transition fires; an `on_enter` block dispatches one agent generation or
            orchestration run on entry.
          items:
            type: object
            additionalProperties: true
        transitions:
          type: array
          description: >-
            Named, directional moves between states. Each has `from` (source states) and `to` (one target), an optional
            JSON Logic `guard`, and an optional `requires_approval` gate.
          items:
            type: object
            additionalProperties: true
        payload_schema:
          type: object
          nullable: true
          additionalProperties: true
          description: Optional JSON Schema describing a task's payload
    QuotaResourceProperties:
      description: >-
        Creates a quota — a project-scoped cap that blocks (`enforce`) or reports (`monitor`) when a windowed aggregate
        is exceeded. `requests` quotas are enforced by the request middleware; `tokens`/`cost_usd` quotas at the
        pre-generation check. Mirrors the quotas REST contract; `scope`, `metric`, and `window` are immutable after
        creation (only `limit`, `mode`, and `on_unpriced` update).
      type: object
      additionalProperties: false
      required:
        - scope
        - metric
        - window
        - limit
      properties:
        scope:
          type: string
          enum:
            - project
            - api_key
            - agent
            - actor
          description: The scope the quota applies to
        scope_ref:
          type: string
          nullable: true
          description: >-
            Public id of the api key / agent / actor the quota applies to. For `api_key` and `agent` scope, NULL means
            all entities of that scope type in the project. For `actor` scope, NULL means one budget *per* actor rather
            than a pooled total across all actors.
        metric:
          type: string
          enum:
            - requests
            - tokens
            - cost_usd
          description: The metric being capped
        window:
          type: string
          enum:
            - rolling_1m
            - rolling_1h
            - rolling_24h
            - calendar_month
          description: The window over which the metric is aggregated
        limit:
          type: number
          description: The cap. Positive integer for requests/tokens; fractional allowed for cost_usd.
        mode:
          type: string
          enum:
            - enforce
            - monitor
          description: enforce blocks with 429; monitor fires the webhook only
        on_unpriced:
          type: string
          enum:
            - block
            - allow
          description: >-
            Only for metric cost_usd. What an enforce quota does over a pricing blackout — block (the default) refuses
            generations with 409 QUOTA_UNENFORCEABLE, allow accepts the unmeasurable spend. See the quotas REST
            contract.
    GuardrailResourceProperties:
      description: >-
        Creates a guardrail — an action-class document (`class`/`guard`) that gates tool-call autonomy. Attach it to a
        tool or agent via that resource's `guardrail_ids` (a `{ "ref": … }` to this resource in the same template
        resolves to its physical id at deploy time). Mirrors the guardrails REST contract;
        `class`/`default_class`/`guard`/`escalate` are flattened here from the REST API's single `document` object.
      type: object
      additionalProperties: false
      required:
        - name
        - class
      properties:
        name:
          type: string
          description: Human-readable name
        description:
          type: string
          nullable: true
          description: Optional description
        class:
          description: >-
            A class literal (`A` / `B` / `C` / `D`) or a JSON Logic expression returning one. An invalid result resolves
            to `default_class`.
          oneOf:
            - type: string
              enum:
                - A
                - B
                - C
                - D
            - type: object
        default_class:
          type: string
          enum:
            - A
            - B
            - C
            - D
          description: >-
            Applied when the `class` expression returns anything other than a valid class. Defaults to `C`
            (fail-closed).
        guard:
          type: object
          nullable: true
          description: A single JSON Logic expression; when the call classifies as `B` it executes only if this evaluates truthy.
        escalate:
          type: boolean
          nullable: true
          description: When true, a passing guard still files an approval item.
        context_tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
          description: Optional tool the platform calls at evaluation time to fetch fresh guardrail context.
        context_mode:
          type: string
          nullable: true
          enum:
            - merge
            - replace
            - null
          description: How tool-fetched context combines with the caller-supplied context.
    ResourceDeclaration:
      type: object
      required:
        - type
        - properties
      properties:
        type:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          description: >
            Resource type. The built-in types are `ai_provider`, `tool`, `agent`, `actor`, `api_key`, `chat`,
            `conversation`, `dataset`, `dataset_item`, `document`, `file`, `guardrail`, `ingestion_rule`, `memory`,
            `memory_entry`, `model_route`, `eval`, `orchestration`, `policy`, `project_price`, `quota`, `secret`,
            `session`, `webhook`, `trigger` and `workflow`.


            This is deliberately not an enum: a deployment operator can register additional resource types backed by
            their own handler, and those are declared here exactly like a built-in one. The set a given deployment
            accepts is authoritative in the server, which rejects an unregistered type with `VALIDATION_FAILED` and
            lists what it does support.
        properties:
          type: object
          additionalProperties: true
          description: >
            Resource properties, as authored in the template and echoed back verbatim. The allowed fields, required
            fields, and field types for each resource `type` are defined by the corresponding `<Type>ResourceProperties`
            schema in this document (e.g. `model_route` → `ModelRouteResourceProperties`), which the server enforces at
            validate/deploy time. The declaration itself is free-form here because property values may be substitution
            expressions rather than final values: `{ "ref": "logicalId" }` references another resource's physical ID, `{
            "param": "ParamName" }` substitutes a parameter value, and `{ "sub": "text ${ParamName}" }` interpolates
            parameters into a string.
        depends_on:
          type: array
          items:
            type: string
          description: Explicit dependency list. In addition to implicit `ref` dependencies.
          nullable: true
        deletion_policy:
          type: string
          enum:
            - delete
            - retain
          description: >
            Controls what happens to the physical resource when it is removed from the stack. `delete` (default) deletes
            the physical resource. `retain` keeps the physical resource alive and only removes the formation record.
            Omit it to get `delete`; an explicit `null` is rejected.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
    FormationResource:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the resource record
        logical_id:
          type: string
          description: Logical identifier from the template
        resource_type:
          type: string
          description: Resource type (e.g. agent, memory)
        physical_resource_id:
          type: string
          nullable: true
          description: Public ID of the physical SOAT resource
        status:
          type: string
          enum:
            - pending
            - created
            - updated
            - deleted
            - failed
          description: Current resource status
    Formation:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the formation
          example: form_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Project public ID
        name:
          type: string
          description: Human-readable formation name
        template:
          $ref: '#/components/schemas/FormationTemplate'
        outputs:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: Resolved output values after stack deployment
        status:
          type: string
          enum:
            - creating
            - active
            - updating
            - failed
            - deleting
            - deleted
            - delete_failed
          description: Formation status
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >
            Static annotations stored on the formation record (supplied at create/update). Not a substitution site —
            `sub`/`param`/`ref` expressions are rejected. Use the template's top-level `metadata` block for deploy-time
            substitution (see `resolved_metadata`).
        resolved_metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >
            The template's top-level `metadata` block after parameter (`sub`/`param`) and resource (`ref`) substitution
            at the last deploy. Null when the template declares no metadata.
        resolved_parameters:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >
            Parameter values applied at the last deploy, for auditability. `no_echo` parameters are masked (`***`). Null
            when the template declares no parameters.
        error:
          allOf:
            - $ref: '#/components/schemas/FormationError'
          nullable: true
          description: >
            Why the formation is `failed` or `delete_failed`, in the same `{ code, message, meta }` shape as an error
            response. Null in every other status, and cleared by the next successful deploy. This is the reason a `2xx`
            deploy response can report `status: "failed"` without a second call to `list-formation-events`.


            One case carries an error while the formation is `active`: `FORMATION_REPLACE_CLEANUP_FAILED`, when a deploy
            replaced a resource and the superseded one could not be deleted. The desired state is realised, so the
            deploy succeeded — but the old resource is still live, and `meta.failures` names it. It stays on the
            formation as pending cleanup and is retried on the next deploy or teardown, which clears the error once it
            is gone.
        resources:
          type: array
          items:
            $ref: '#/components/schemas/FormationResource'
          description: Resources managed by this formation (present on get/create/update)
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ValidationError:
      type: object
      properties:
        path:
          type: string
          description: Location of the issue (e.g. nodes[1].input_mapping.val).
        message:
          type: string
          description: Human-readable description of the issue.
    ValidationResult:
      type: object
      required:
        - valid
        - errors
        - warnings
      properties:
        valid:
          type: boolean
          description: True when there are no blocking errors.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
    PlanChange:
      type: object
      properties:
        logical_id:
          type: string
        resource_type:
          type: string
        action:
          type: string
          enum:
            - create
            - update
            - delete
            - no-op
        physical_resource_id:
          type: string
          description: The existing resource's physical ID. Present for update / no-op / delete actions, absent for create.
        diff:
          type: object
          description: >-
            Resolved desired-state properties (post parameter/ref substitution) and, when available, the current live or
            last-applied properties they were compared against. Omitted when neither side could be computed (e.g. an
            unregistered resource type).
          properties:
            desired:
              type: object
              additionalProperties: true
            current:
              type: object
              additionalProperties: true
              nullable: true
    PlanResult:
      type: object
      properties:
        changes:
          type: array
          items:
            $ref: '#/components/schemas/PlanChange'
        unauthorized_actions:
          type: array
          description: >-
            The per-resource actions the caller may not perform. A formation may only do what the caller could do
            directly, so applying this template would be refused while any of these remain. Absent when the caller may
            perform every action the plan implies. A plan itself changes nothing, so it reports them rather than
            failing.
          items:
            $ref: '#/components/schemas/UnauthorizedFormationAction'
    UnauthorizedFormationAction:
      type: object
      required:
        - logical_id
        - resource_type
        - action
      properties:
        logical_id:
          type: string
          description: The template's own name for the resource.
          example: MyGuardrail
        resource_type:
          type: string
          description: The declared resource type.
          example: guardrail
        action:
          type: string
          description: The action the caller lacks.
          example: guardrails:CreateGuardrail
    FormationError:
      type: object
      description: >-
        Why a deploy or teardown failed, in the one error shape the API has. Carried on the formation itself and on the
        operation that failed.
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: >-
            The failing operation's error code (`VALIDATION_FAILED`, `RESOURCE_NOT_FOUND`, `FORMATION_DELETE_FAILED`,
            `FORMATION_REPLACE_CLEANUP_FAILED`, …), or `UNKNOWN` when the underlying failure carried no code.
          example: VALIDATION_FAILED
        message:
          type: string
          description: The failure, as reported by the resource that raised it.
          example: >-
            dataset_id is immutable: item 'dsit_V1StGXR8Z5jdHi6B' belongs to 'dset_V1StGXR8Z5jdHi6B'. Declare a new
            dataset_item instead.
        meta:
          type: object
          additionalProperties: true
          description: >-
            Context for the failure. A failed apply names the resource that broke it (`logical_id`, `resource_type`); a
            failed teardown lists every blocker under `failures`, and so does a succeeded deploy that could not dispose
            of a replaced resource — there each entry adds the `physical_resource_id` still live.
          example:
            logical_id: case1
            resource_type: dataset_item
    FormationEvent:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        logical_id:
          type: string
        resource_type:
          type: string
        action:
          type: string
          description: >-
            What the deploy did to the resource: `create`, `update`, `delete`, `no-op`, `rollback` (a resource created
            earlier in this deploy that was walked back after a later failure), or `rollback-skipped` (a
            `deletion_policy: retain` resource left standing by that unwind).
          example: rollback
        status:
          type: string
          enum:
            - succeeded
            - failed
        physical_resource_id:
          type: string
          nullable: true
        error:
          type: string
          nullable: true
    FormationOperation:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the operation
        operation_type:
          type: string
          enum:
            - validate
            - plan
            - create
            - update
            - delete
        status:
          type: string
          enum:
            - pending
            - running
            - succeeded
            - failed
        events:
          type: array
          items:
            $ref: '#/components/schemas/FormationEvent'
          nullable: true
        plan:
          allOf:
            - $ref: '#/components/schemas/PlanResult'
          nullable: true
        error:
          allOf:
            - $ref: '#/components/schemas/FormationError'
          nullable: true
          description: >-
            Why this operation failed. Null for a succeeded or running operation. The same bag the formation itself
            carries while that failure is its current state.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    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.
    GuardrailDocument:
      type: object
      required:
        - class
      additionalProperties: true
      description: >
        The action-class document. `class` maps a call to an action class; `guard` gates class-B autonomy. Both are
        single JSON Logic expressions over the `args.*` / `context.*` / `runtime.*` namespaces.
      properties:
        class:
          description: >
            A class literal (`A` / `B` / `C` / `D`) or a JSON Logic expression returning one. An invalid result resolves
            to `default_class`.
          oneOf:
            - type: string
              enum:
                - A
                - B
                - C
                - D
            - type: object
        default_class:
          type: string
          enum:
            - A
            - B
            - C
            - D
          description: >
            Applied when the `class` expression returns anything other than a valid class. Defaults to `C`
            (fail-closed).
        guard:
          type: object
          description: >
            A single JSON Logic expression; when the call classifies as `B` it must evaluate truthy to execute
            autonomously. Compose multiple conditions with `{ "and": [...] }`.
        escalate:
          type: boolean
          description: |
            When `true`, a failing guard routes to approval instead of tripping fail-closed.
        expires_in:
          type: integer
          minimum: 1
          description: >
            Default approval window in seconds for a class-C approval this guardrail files. Omitted → the platform's 24h
            default. When several guardrails apply, the governing (strictest-matching) one's value is used.
    Guardrail:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the guardrail
          example: guard_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Human-readable name
          example: Budget Update Guardrail
        description:
          type: string
          nullable: true
          description: Optional description
        version:
          type: integer
          description: Incremented on every document write; prior versions are archived
          example: 1
        document:
          $ref: '#/components/schemas/GuardrailDocument'
        context_tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
          description: |
            Optional tool the platform calls at evaluation time to fetch fresh guardrail context.
        context_mode:
          type: string
          nullable: true
          enum:
            - merge
            - replace
            - null
          description: |
            How tool-fetched context combines with the caller-supplied context.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    GuardrailEvaluation:
      type: object
      description: >
        The record produced by evaluating a guardrail against one call — written to the audit trail at dispatch time
        (one per applying guardrail) and returned verbatim by the dry-run endpoint.
      properties:
        kind:
          type: string
          description: Always `guardrail_evaluation`.
        guardrail_id:
          x-soat-ref: guardrails
          type: string
          example: guard_V1StGXR8Z5jdHi6B
        guardrail_version:
          type: integer
          nullable: true
          description: The governing version; null for a dangling reference (fail-closed C).
        scope:
          type: string
          enum:
            - project
            - agent
            - tool
        tool:
          type: string
          nullable: true
        action:
          type: string
          nullable: true
        class:
          type: string
          description: The resolved class (or the applied default_class).
          enum:
            - A
            - B
            - C
            - D
        decision:
          type: string
          enum:
            - execute
            - route_to_approval
            - blocked
            - tripwire
        guard_result:
          type: boolean
          nullable: true
          description: The guard outcome; null when the call did not classify as B.
        context_source:
          type: string
          enum:
            - caller
            - tool
            - merged
            - none
        context_snapshot:
          type: object
          additionalProperties: true
          description: >
            Flat map of only the vars the class/guard expressions referenced, keyed by fully-qualified path, frozen at
            evaluation-time values.
        agent_id:
          type: string
          nullable: true
        orchestration_run_id:
          type: string
          nullable: true
        generation_id:
          type: string
          nullable: true
    GuardrailVersion:
      type: object
      description: An immutable archive of a guardrail's configuration at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: guard_ver_V1StGXR8Z5jdHi6B
        guardrail_id:
          x-soat-ref: guardrails
          type: string
          description: Public ID of the guardrail this version belongs to
          example: guard_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The guardrail's versioned surface as it stood at this version. Today that is the policy `document` and
            nothing else: name, description and the context binding are metadata, and bumping the version when one of
            them changes would make two version numbers denote the same policy — which is exactly what an evaluation
            record cites.


            Deliberately open rather than a fixed schema: an archive written by an earlier release of SOAT reflects the
            guardrail surface **of its own time**, so it may carry fields the current API no longer documents.
          properties:
            document:
              $ref: '#/components/schemas/GuardrailDocument'
        label:
          type: string
          nullable: true
          description: >-
            Optional human tag for this version, e.g. `pre-tightening`. Set from the `label` field of a restore, or
            generated for one.
          example: restored from v2
        created_by:
          x-soat-ref: users
          type: string
          nullable: true
          description: Public ID of the user whose action produced this version. Null for writes with no request user behind them.
        created_at:
          type: string
          format: date-time
    RestoreGuardrailVersionRequest:
      type: object
      properties:
        label:
          type: string
          description: Optional tag for the version the restore creates. Defaults to `restored from v<version>`.
          example: rollback to pre-incident policy
    CreateGuardrailRequest:
      type: object
      required:
        - name
        - document
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
        name:
          type: string
          description: Human-readable name
          example: Budget Update Guardrail
        description:
          type: string
          nullable: true
        document:
          $ref: '#/components/schemas/GuardrailDocument'
        context_tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
        context_mode:
          type: string
          enum:
            - merge
            - replace
        version_label:
          type: string
          description: >-
            Optional tag for the config version this write archives (e.g. `initial`). Annotates the version only — it is
            not stored on the guardrail and is not part of the config, so labelling a change is never itself a change.
          example: initial
    UpdateGuardrailRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        document:
          $ref: '#/components/schemas/GuardrailDocument'
        context_tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
        context_mode:
          type: string
          nullable: true
          enum:
            - merge
            - replace
            - null
        version_label:
          type: string
          description: >-
            Optional tag for the config version this write archives (e.g. `pre-tightening`). Annotates the version only
            — it is not stored on the guardrail and is not part of the config, so labelling a change is never itself a
            change. Ignored when the write changes no policy, since no version is created.
          example: pre-tightening
    IngestionRule:
      type: object
      properties:
        id:
          type: string
          example: igr_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        content_type_glob:
          type: string
          example: image/*
        tool_id:
          x-soat-ref: tools
          type: string
          nullable: true
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
        action:
          type: string
          nullable: true
        preset_parameters:
          type: object
          nullable: true
        native_extraction:
          type: string
          enum:
            - first
            - skip
        file_delivery:
          type: string
          enum:
            - base64
            - download_url
        chunk_strategy:
          type: string
          nullable: true
        chunk_size:
          type: integer
          nullable: true
        chunk_overlap:
          type: integer
          nullable: true
        metadata:
          type: object
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    KnowledgeResult:
      oneOf:
        - $ref: '#/components/schemas/DocumentKnowledgeResult'
        - $ref: '#/components/schemas/MemoryKnowledgeResult'
      discriminator:
        propertyName: source_type
        mapping:
          document: '#/components/schemas/DocumentKnowledgeResult'
          memory: '#/components/schemas/MemoryKnowledgeResult'
    DocumentKnowledgeResult:
      type: object
      required:
        - source_type
        - document_id
        - content
        - created_at
        - updated_at
      properties:
        source_type:
          type: string
          enum:
            - document
          description: The type of knowledge source this result comes from
          example: document
        document_id:
          x-soat-ref: documents
          type: string
          description: Public ID of the document
          example: doc_V1StGXR8Z5jdHi6B
        chunk_id:
          type: string
          description: Public ID of the document chunk that matched the query
          example: dchunk_V1StGXR8Z5jdHi6B
        page:
          type: integer
          nullable: true
          description: Page number within the source PDF (1-indexed). Null for plain-text documents.
          example: 3
        file_id:
          x-soat-ref: files
          type: string
          description: Public ID of the underlying file
          example: file_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project the document belongs to
          example: proj_V1StGXR8Z5jdHi6B
        path:
          type: string
          description: Logical path of the file within the project
          example: /sales/policies.txt
        filename:
          type: string
          description: Filename of the underlying file
          example: policies.txt
        size:
          type: integer
          description: File size in bytes
          example: 1024
        title:
          type: string
          description: Document title
          example: Sales Communication Policy
        metadata:
          type: object
          description: >-
            Arbitrary metadata attached to the document, returned verbatim in the casing it was written with at
            create/update time (e.g. a key written as `strapiDocumentId` is returned as `strapiDocumentId`, not
            `strapi_document_id`) — it is not converted between snake_case and camelCase like other response fields.
        tags:
          type: object
          additionalProperties:
            type: string
          description: Key-value tags
          example:
            department: sales
        content:
          type: string
          nullable: true
          description: Full text content of the document
        score:
          type: number
          description: >-
            Implementation-defined relevance ranking — higher is better. The **ordering** it produces is the contract;
            the absolute value is not, and the formula behind it may change (a future hybrid ranking would fuse several
            signals here). It is the field `min_score` filters on and the field results are sorted by. Only present when
            `query` was provided. Use `similarity_score` when you need the raw cosine value.
          example: 0.82
        similarity_score:
          type: number
          description: >-
            Raw cosine similarity (0–1) between the query and this result. Pinned to that meaning — unlike `score`, it
            is never redefined. Only present when `query` was provided.
          minimum: 0
          maximum: 1
          example: 0.82
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          description: Last updated timestamp
    MemoryKnowledgeResult:
      type: object
      required:
        - source_type
        - entry_id
        - memory_id
        - memory_name
        - content
        - created_at
        - updated_at
      properties:
        source_type:
          type: string
          enum:
            - memory
          description: The type of knowledge source this result comes from
          example: memory
        entry_id:
          type: string
          description: Public ID of the memory entry
          example: mem_entry_V1StGXR8Z5jdHi6B
        memory_id:
          x-soat-ref: memories
          type: string
          description: Public ID of the parent memory
          example: mem_V1StGXR8Z5jdHi6B
        memory_name:
          type: string
          description: Human-readable name of the parent memory
          example: Customer Preferences
        content:
          type: string
          description: Text content of the memory entry
        score:
          type: number
          description: >-
            Implementation-defined relevance ranking — higher is better. The **ordering** it produces is the contract;
            the absolute value is not, and the formula behind it may change (a future hybrid ranking would fuse several
            signals here). It is the field `min_score` filters on and the field results are sorted by. Only present when
            `query` was provided. Use `similarity_score` when you need the raw cosine value.
          example: 0.79
        similarity_score:
          type: number
          description: >-
            Raw cosine similarity (0–1) between the query and this result. Pinned to that meaning — unlike `score`, it
            is never redefined. Only present when `query` was provided.
          minimum: 0
          maximum: 1
          example: 0.79
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          description: Last updated timestamp
    Memory:
      type: object
      properties:
        id:
          type: string
          example: mem_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: Product Documentation
        description:
          type: string
          nullable: true
          example: Retrieves product docs for support queries
        tags:
          type: array
          nullable: true
          items:
            type: string
          description: List of tags for filtering in knowledge search
          example:
            - projectA
            - customer-support
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    MemoryEntry:
      type: object
      properties:
        id:
          type: string
          example: mem_entry_V1StGXR8Z5jdHi6B
        memory_id:
          x-soat-ref: memories
          type: string
          example: mem_V1StGXR8Z5jdHi6B
        content:
          type: string
          example: The customer prefers email communication over phone calls
        source_type:
          type: string
          enum:
            - manual
            - agent
            - extraction
            - orchestration
          example: manual
        tags:
          type: array
          nullable: true
          items:
            type: string
          description: Per-entry tag strings
          example:
            - role:traffic-manager
            - source:rejected_approval
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Arbitrary structured metadata attached to the entry
          example:
            evidence: high
        source_generation_id:
          x-soat-ref: generations
          type: string
          nullable: true
          description: >-
            The generation whose turn produced this entry. Set for entries written by the `write_memory` tool and by
            automatic extraction; null for manual and orchestration writes. Recorded when the entry is created and never
            rewritten by a later merge.
          example: gen_V1StGXR8Z5jdHi6B
        source_conversation_id:
          x-soat-ref: conversations
          type: string
          nullable: true
          description: >-
            The conversation the producing turn belonged to. Null when the entry did not come from a conversation (a
            direct agent generation, a manual write, or an orchestration write).
          example: conv_V1StGXR8Z5jdHi6B
        invalidated_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            When the entry was superseded. Null means the entry is currently valid. Invalidated entries are excluded
            from listing, from write deduplication, and from knowledge search, but remain readable by ID for audit.
        superseded_by_entry_id:
          type: string
          nullable: true
          description: The entry that replaced this one, when it was superseded. Null for valid entries.
          example: mem_entry_V1StGXR8Z5jdHi6B
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    MemoryEntryWriteResult:
      allOf:
        - $ref: '#/components/schemas/MemoryEntry'
        - type: object
          properties:
            action:
              type: string
              enum:
                - created
                - updated
                - skipped
                - superseded
              description: >-
                The outcome of the write operation. `updated` means an existing entry was rewritten to absorb the
                incoming fact — it is produced only by the LLM consolidation on agent write paths (the `write_memory`
                tool and automatic extraction), never by this endpoint, which creates instead of merging. `superseded`
                means the incoming content contradicted an existing entry, which was invalidated and replaced — it is
                produced by the LLM-arbitrated write path and does not occur until that ships.
    ModelRouteTarget:
      type: object
      required:
        - ai_provider_id
        - model
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: AI provider in the route's project
          example: aip_V1StGXR8Z5jdHi6B
        model:
          type: string
          description: Model name to call on that provider
          example: gpt-4o-mini
        timeout_seconds:
          type: integer
          description: >-
            Per-attempt deadline, enforced with an AbortSignal composed with the caller's signal. A timeout classifies
            as `timeout`; the caller's own signal firing aborts the run without failover. Omitted means no per-target
            deadline.
          example: 30
        max_retries:
          type: integer
          default: 0
          description: >-
            Retries on this target before falling through to the next one. The route is the only retry authority —
            routed calls pass `maxRetries: 0` to the AI SDK so its own retry loop cannot multiply these.
          example: 1
    ModelRoute:
      type: object
      properties:
        id:
          type: string
          example: route_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        name:
          type: string
          example: primary-with-fallback
        targets:
          type: array
          items:
            $ref: '#/components/schemas/ModelRouteTarget'
        retry_on:
          type: array
          items:
            type: string
            enum:
              - provider_error
              - timeout
              - rate_limited
        failure_threshold:
          type: integer
          example: 3
        cooldown_seconds:
          type: integer
          example: 60
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    OauthAuthorizationServerMetadata:
      type: object
      description: RFC 8414 Authorization Server Metadata.
      required:
        - issuer
        - authorization_endpoint
        - token_endpoint
        - response_types_supported
        - grant_types_supported
        - code_challenge_methods_supported
        - token_endpoint_auth_methods_supported
      properties:
        issuer:
          type: string
          format: uri
          description: The deployment's base URL, which every endpoint below is relative to.
          example: http://localhost:5047
        authorization_endpoint:
          type: string
          format: uri
          example: http://localhost:5047/authorize
        token_endpoint:
          type: string
          format: uri
          example: http://localhost:5047/token
        registration_endpoint:
          type: string
          format: uri
          description: RFC 7591 dynamic client registration.
          example: http://localhost:5047/register
        response_types_supported:
          type: array
          items:
            type: string
          example:
            - code
        grant_types_supported:
          type: array
          items:
            type: string
          example:
            - authorization_code
            - refresh_token
        code_challenge_methods_supported:
          type: array
          description: PKCE is mandatory in OAuth 2.1, so `plain` is not offered.
          items:
            type: string
          example:
            - S256
        token_endpoint_auth_methods_supported:
          type: array
          items:
            type: string
          example:
            - client_secret_basic
            - client_secret_post
            - none
        scopes_supported:
          type: array
          description: The scopes a client may request. `mcp:access` grants use of the MCP endpoint.
          items:
            type: string
          example:
            - mcp:access
    OauthProtectedResourceMetadata:
      type: object
      description: RFC 9728 Protected Resource Metadata.
      required:
        - resource
        - authorization_servers
      properties:
        resource:
          type: string
          format: uri
          description: The protected resource's identifier.
          example: http://localhost:5047
        authorization_servers:
          type: array
          description: Issuers of the authorization servers that guard this resource.
          items:
            type: string
            format: uri
          example:
            - http://localhost:5047
    OauthClientRegistrationRequest:
      type: object
      description: RFC 7591 client metadata. Fields beyond those listed are stored as sent.
      required:
        - redirect_uris
      properties:
        redirect_uris:
          type: array
          description: At least one redirect URI. `/authorize` accepts only an exact match.
          items:
            type: string
            format: uri
          example:
            - https://client.example.com/callback
        client_name:
          type: string
          description: Human-readable name, shown on the consent screen.
          example: My MCP Client
        token_endpoint_auth_method:
          type: string
          description: '`none` registers a public client, which receives no secret. Defaults to `client_secret_basic`.'
          example: none
        grant_types:
          type: array
          description: Defaults to `authorization_code` and `refresh_token`.
          items:
            type: string
        response_types:
          type: array
          description: Defaults to `code`.
          items:
            type: string
    OauthClientRegistrationResponse:
      type: object
      description: The registered client, echoing the metadata sent plus the server-assigned fields.
      required:
        - client_id
        - redirect_uris
        - token_endpoint_auth_method
        - grant_types
        - response_types
        - client_id_issued_at
      properties:
        client_id:
          type: string
          example: 8f14e45fceea167a
        client_secret:
          type: string
          description: 'Absent for a public client (`token_endpoint_auth_method: none`).'
        client_secret_expires_at:
          type: integer
          description: '`0` means the secret does not expire. Absent for a public client.'
          example: 0
        client_id_issued_at:
          type: integer
          description: Unix time, in seconds, at which the client was registered.
          example: 1735689600
        redirect_uris:
          type: array
          items:
            type: string
            format: uri
        client_name:
          type: string
        token_endpoint_auth_method:
          type: string
          example: none
        grant_types:
          type: array
          items:
            type: string
          example:
            - authorization_code
            - refresh_token
        response_types:
          type: array
          items:
            type: string
          example:
            - code
    OauthTokenRequest:
      type: object
      description: |
        Form-encoded token request. `code`, `redirect_uri` and `code_verifier` belong to the
        `authorization_code` grant; `refresh_token` to the `refresh_token` grant.
      required:
        - grant_type
      properties:
        grant_type:
          type: string
          enum:
            - authorization_code
            - refresh_token
          example: authorization_code
        code:
          type: string
          description: The single-use authorization code from the `/authorize` redirect.
        redirect_uri:
          type: string
          format: uri
          description: Must match the `redirect_uri` the code was issued for.
        code_verifier:
          type: string
          description: The PKCE verifier whose SHA-256 is the `code_challenge` sent to `/authorize`.
        refresh_token:
          type: string
          description: The refresh token to exchange. Single-use — a new one is returned.
        scope:
          type: string
          description: Space-separated scopes, to narrow those of the refreshed token.
        client_id:
          type: string
          description: Required for a public client, which sends no secret.
        client_secret:
          type: string
          description: For `client_secret_post`. With `client_secret_basic`, send it in the `Authorization` header instead.
    OauthTokenResponse:
      type: object
      required:
        - access_token
        - token_type
        - scope
      properties:
        access_token:
          type: string
          description: 'A SOAT-issued JWT. Send it as `Authorization: Bearer <token>`.'
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          description: Lifetime of the access token, in seconds.
          example: 3600
        refresh_token:
          type: string
          description: Single-use; rotated on every exchange.
        scope:
          type: string
          description: Space-separated scopes actually granted.
          example: mcp:access
    OauthErrorResponse:
      type: object
      description: |
        RFC 6749 error. These endpoints answer in the OAuth error shape rather than SOAT's
        `{ code, message, hint, docs_url }`, because an OAuth client parses `error` to decide what to
        do next.
      required:
        - error
      properties:
        error:
          type: string
          description: RFC 6749 error code.
          enum:
            - invalid_request
            - invalid_client
            - invalid_grant
            - invalid_redirect_uri
            - unsupported_grant_type
            - unsupported_response_type
            - access_denied
            - server_error
        error_description:
          type: string
          description: Human-readable detail.
          example: PKCE verification failed
    QueueStats:
      type: object
      description: A point-in-time snapshot of the orchestration run queue.
      properties:
        driver:
          type: string
          enum:
            - postgres
            - sqs
          description: >-
            The active queue driver (`ORCHESTRATION_QUEUE_DRIVER`). Under `sqs`, `oldest_queued_age_seconds` is always
            `null` and `per_project` is always empty — SQS exposes neither.
          example: postgres
        queue_depth:
          type: integer
          description: >-
            Tasks waiting to be claimed now (unclaimed and past their `available_at`). Backoff-delayed tasks are
            excluded.
          example: 12
        claimed_tasks:
          type: integer
          description: Tasks currently claimed with a valid (unexpired) lease.
          example: 3
        oldest_queued_age_seconds:
          type: number
          nullable: true
          description: Age in seconds of the oldest claimable-now task, or `null` when none are waiting.
          example: 4.2
        claim_latency_ms:
          type: object
          description: >-
            Claim-latency percentiles (time from a task becoming available to being claimed) over a rolling in-process
            window. `p50`/`p95` are `null` when no claim happened in the window.
          properties:
            p50:
              type: number
              nullable: true
              example: 18
            p95:
              type: number
              nullable: true
              example: 240
            window_seconds:
              type: integer
              example: 300
        per_project:
          type: array
          description: One row per project with any queued or claimed task.
          items:
            type: object
            properties:
              project_id:
                type: string
                description: Public project ID (proj_ prefix).
                example: proj_V1StGXR8Z5jdHi6B
              queued:
                type: integer
                example: 5
              claimed:
                type: integer
                example: 1
    OrchestrationNode:
      type: object
      description: A single execution unit in the orchestration graph.
      required:
        - id
        - type
      properties:
        id:
          type: string
          description: Unique node identifier within this orchestration.
        type:
          type: string
          description: >-
            Node execution type. Known types: agent, tool, transform, knowledge, memory_write, condition, human,
            approval, loop, poll, delay, webhook, emit_event, sub_orchestration. Open set — new types may be added in
            minor releases, and an unrecognized type is accepted at create time (the run fails when the node
            dispatches), so clients must tolerate unknown values.
        agent_id:
          x-soat-ref: agents
          type: string
          description: For agent nodes — public ID of the agent to invoke.
        tool_id:
          x-soat-ref: tools
          type: string
          description: For tool and poll nodes — public ID of the tool to call.
        operation_id:
          type: string
          description: For tool and poll nodes — specific operation/action on MCP/SOAT tools.
        expression:
          description: >-
            For transform/condition nodes — JSON Logic rule (https://jsonlogic.com) evaluated against the run state. A
            rule may be any JSON value (object, string, number, boolean, array), so no type is constrained.
        exit_condition:
          description: >
            For poll nodes — JSON Logic stop condition, evaluated each attempt against the run state augmented with
            `response` (the latest tool result) and `attempt` (1-based count); a truthy result stops polling.
        prompt:
          type: string
          description: For human nodes — prompt shown to the human reviewer.
        options:
          type: array
          items:
            type: string
          description: For human nodes — constrained choices.
        memory_id:
          x-soat-ref: memories
          type: string
          description: For memory_write nodes — public ID of the target memory.
        arguments:
          type: object
          additionalProperties: true
          description: >
            For approval nodes — input-mapping-style object (JSON Logic values) resolved against run state into the
            proposed tool call's arguments, frozen onto the created approval item.
        expires_in:
          type: integer
          description: >
            For approval nodes — seconds until the created approval item expires. Defaults to 86400 (24h) when omitted.
            An expired item can never execute; the run routes down its `on_expired` edge.
        instructions:
          type: string
          description: For approval nodes — optional guidance shown to the approver.
        reasoning:
          description: For approval nodes — JSON Logic (any JSON value) resolved into the item's reasoning.
        evidence:
          description: For approval nodes — JSON Logic (any JSON value) resolved into the item's evidence.
        predicted_impact:
          description: For approval nodes — JSON Logic (any JSON value) resolved into the item's predicted impact.
        input_mapping:
          type: object
          additionalProperties: true
          description: >
            Maps node input keys to values. Each value is JSON Logic (https://jsonlogic.com), the same evaluator used by
            transform and condition nodes. A single-key object is evaluated against the run state — `{"var": "key"}`
            reads `state.key`, `{"cat": [...]}` and `{">": [...]}` compute derived values. Any other value (string,
            number, boolean, array, multi-key object) is passed through as a literal.
        state_mapping:
          type: object
          additionalProperties: true
          description: >
            Maps state write paths to values. Each key is a `state.<path>` destination (the `state.` prefix is
            optional); each value is JSON Logic (https://jsonlogic.com) evaluated against `{ "output": <node artifact>,
            "state": <run state> }` — e.g. `{ "summary": {"var": "output.content"} }` writes the artifact's `content`
            field to `state.summary`. The same evaluator as input_mapping/transform/condition; only the context differs.
        output_schema:
          type: object
          description: For agent nodes — JSON Schema for structured output parsing.
        collection:
          type: string
          description: For loop nodes — state path to the collection to iterate over.
        item_variable:
          type: string
          description: For loop nodes — variable name injected into state for each item.
        parallelism:
          type: integer
          description: For loop nodes — number of items to process in parallel.
        context_keys:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            For loop and sub_orchestration nodes — allowlist of the run's `tool_context` keys the child run inherits.
            When `null` (the default), the child inherits the parent's whole bag — the behavior of every graph authored
            before this field existed. When set, only the listed keys are handed down, so a run holding a broad
            credential can delegate one step to a shared sub-graph without passing on what that sub-graph does not need;
            `[]` hands down nothing. Matching is case-insensitive, since an entry names a key that becomes an HTTP
            header name; an entry outside that grammar is rejected at write time with `INVALID_TOOL_CONTEXT_KEY`. The
            server-derived identity keys (`session_id`, `actor_id`, `actor_external_id`) are unaffected — they are
            re-derived per generation in the child regardless of this list. Ignored for other node types.
        interval:
          type: string
          description: >
            For poll nodes — wait between attempts. Accepts a friendly suffix form (`5s`, `30s`, `5m`, `2h`, `500ms`) or
            ISO 8601 (e.g. PT5S).
        fail_on_timeout:
          type: boolean
          description: >
            For poll nodes — when max_iterations is reached without the exit condition becoming true, fail the run
            (true) instead of completing with condition_met=false (default false).
        duration:
          type: string
          description: >
            For delay nodes — how long to wait. Accepts a friendly suffix form (`5s`, `30s`, `5m`, `2h`, `500ms`) or ISO
            8601 (e.g. PT5S).
        mode:
          type: string
          enum:
            - receive
          description: >
            For webhook nodes — parks the run awaiting an inbound callback. `receive` is the only mode; to send a
            notification out of a graph, use an `emit_event` node instead.
        event_type:
          type: string
          description: >
            For emit_event nodes — the internal event type to emit (e.g. `guardrail.exception`). The node's
            input_mapping becomes the event `data`. Any Webhook subscribed to this event type in the run's project then
            delivers it — signed, retried, and tracked by the Webhooks module — so the graph holds no URL or secret of
            its own.
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: >
            Public ID of the orchestration this node runs — the child orchestration for sub_orchestration nodes, and the
            orchestration run once per item for loop nodes.
        max_iterations:
          type: integer
          description: >
            Maximum iterations before the node is aborted. For poll nodes this is the maximum number of attempts
            (default 10, ceiling 1000).
        retry:
          type: object
          description: >
            Retry-on-failure policy. When the node throws a transient error (unexpected/infrastructure errors and
            upstream 5xx) and attempts remain, the run parks as `sleeping` and re-executes the node after the backoff
            delay. Terminal errors (4xx business errors) fail immediately. Absent or `max_attempts <= 1` means
            fail-fast.
          properties:
            max_attempts:
              type: integer
              description: |
                Total attempts including the first (default 1, ceiling 20).
            backoff:
              type: object
              properties:
                strategy:
                  type: string
                  enum:
                    - fixed
                    - exponential
                  description: >
                    `fixed` waits `delay_ms` between every attempt; `exponential` doubles per prior attempt. Default
                    `fixed`.
                delay_ms:
                  type: integer
                  description: Base delay between attempts in ms (default 1000).
                max_delay_ms:
                  type: integer
                  description: |
                    Cap on the computed backoff delay in ms (default 300000).
    OrchestrationEdge:
      type: object
      description: A directed connection between two nodes.
      required:
        - from
        - to
      properties:
        from:
          type: string
          description: Source node ID.
        to:
          type: string
          description: Target node ID.
        condition:
          type: string
          description: For condition node routing — label to match against condition output.
        activation_group:
          type: string
          description: Groups edges for join semantics.
        activation_condition:
          type: string
          enum:
            - all
            - any
          description: Whether all or any edges in the activation group must fire.
    Orchestration:
      type: object
      required:
        - id
        - project_id
        - name
        - version
        - nodes
        - edges
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: Public ID (orch_...).
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project.
        name:
          type: string
          description: Human-readable name.
        description:
          type: string
          nullable: true
          description: Optional description.
        version:
          type: integer
          description: >
            Incremented on every write that changes the graph; prior versions are archived. A run pins the version it
            started on, so these fields are a draft for runs started from now on rather than a live rewrite of the ones
            already executing.
          example: 1
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
          description: Optional JSON Schema for state validation.
        input_schema:
          type: object
          nullable: true
          description: Schema for run inputs (initial state).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateOrchestrationRequest:
      type: object
      required:
        - name
        - nodes
        - edges
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: >-
            Public ID of the project. Optional when authenticating with a project-scoped API key, which defaults to the
            key's project; required otherwise.
        name:
          type: string
          description: Human-readable name.
        description:
          type: string
          nullable: true
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
        input_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: Optional tag for the version this create archives, e.g. `initial`.
          example: initial
    UpdateOrchestrationRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        state_schema:
          type: object
          nullable: true
        input_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: >-
            Optional tag for the version this write archives, e.g. `pre-rewire`. Ignored when the write changes no graph
            field, since no version is archived.
          example: pre-rewire
    OrchestrationVersion:
      type: object
      description: An immutable archive of an orchestration's graph at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: orch_ver_V1StGXR8Z5jdHi6B
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: Public ID of the orchestration this version belongs to
          example: orch_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The orchestration's versioned surface as it stood at this version: `nodes`, `edges`, `state_schema` and
            `input_schema`. Name and description are metadata — bumping the version when one of them changes would make
            two version numbers denote the same topology, which is exactly what a run cites.


            Deliberately open rather than a fixed schema: an archive written by an earlier release of SOAT reflects the
            orchestration surface **of its own time**, so it may carry fields the current API no longer documents.
          properties:
            nodes:
              type: array
              items:
                $ref: '#/components/schemas/OrchestrationNode'
            edges:
              type: array
              items:
                $ref: '#/components/schemas/OrchestrationEdge'
            state_schema:
              type: object
              nullable: true
            input_schema:
              type: object
              nullable: true
        label:
          type: string
          nullable: true
          description: >-
            Optional human tag for this version, e.g. `pre-rewire`. Set from the `version_label` field of a write, the
            `label` field of a restore, or generated for one.
          example: restored from v2
        created_by:
          x-soat-ref: users
          type: string
          nullable: true
          description: Public ID of the user whose action produced this version. Null for writes with no request user behind them.
        created_at:
          type: string
          format: date-time
    RestoreOrchestrationVersionRequest:
      type: object
      properties:
        label:
          type: string
          description: Optional tag for the version the restore creates. Defaults to `restored from v<version>`.
          example: rollback to pre-incident graph
    OrchestrationRun:
      type: object
      required:
        - id
        - orchestration_id
        - project_id
        - status
        - state
        - active_nodes
        - artifacts
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: Public ID (run_...).
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: Public ID of the parent orchestration.
        orchestration_version:
          type: integer
          nullable: true
          description: >
            The orchestration version this run executes, fixed when the run started. Every later step of the run — the
            first drive, a wake from `sleeping`, a human or approval resume, a redrive after a crash — resolves the
            graph from this version, so editing the orchestration never re-shapes a run already in flight. Fetch the
            graph it names at `GET /api/v1/orchestrations/{orchestration_id}/versions/{version}`.


            Null for runs created before pinning existed, which execute the live graph.
          example: 3
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project.
        status:
          type: string
          description: >-
            Run lifecycle state. `queued` awaits a worker; `running` is actively executing; `sleeping` is parked on a
            delay/poll wait (no worker); `awaiting_input` is parked on a human node; `succeeded`/`failed`/ `cancelled`
            are terminal; `expired` is a wait that passed its deadline.
          enum:
            - queued
            - running
            - sleeping
            - awaiting_input
            - succeeded
            - failed
            - cancelled
            - expired
        state:
          type: object
          description: Current accumulated state.
        active_nodes:
          type: array
          items:
            type: string
          description: Node IDs currently active.
        artifacts:
          type: object
          description: Map of node ID to output artifact.
        error:
          type: object
          nullable: true
          description: Error details when status is failed.
        trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
        input:
          type: object
          nullable: true
          description: Initial input provided at run creation.
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            The `tool_context` supplied at run creation, forwarded as `X-Soat-Context-<key>` headers on every tool call
            the run's agent nodes make. Null when the run was started without one.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            The caller-owned key/value metadata supplied at run creation, returned verbatim. Null when the run was
            started without any. The server writes nothing here and no key is reserved; the bag is never merged into
            `state`, so nothing in it reaches the graph.
          example:
            tenant_account_id: '42'
            dispatch_batch: nightly-2026-08-25
        parent_orchestration_run_id:
          x-soat-ref: orchestration-runs
          type: string
          nullable: true
          description: >-
            The run whose node started this one — set only on a child a `loop` or `sub_orchestration` node spawned, null
            for a run a caller started. A child is its own run with its own usage events, so this is what makes a
            delegated run's spend attributable to the run that ordered it.
        parent_node_id:
          type: string
          nullable: true
          description: >-
            The node within `parent_orchestration_run_id` that started this run. Null when `parent_orchestration_run_id`
            is null.
        run_depth:
          type: integer
          minimum: 0
          description: >-
            `loop` / `sub_orchestration` edges between this run and the run a caller started: `0` for a caller-started
            run, one more than its parent's for a child. Starting a child past the effective bound — the smaller of the
            deployment's `MAX_ORCHESTRATION_RUN_DEPTH` (default 10) and the project's `max_run_depth` — is refused with
            `ORCHESTRATION_RUN_DEPTH_LIMIT`, which fails the run that tried to descend. That bounds a graph whose
            `sub_orchestration` node names itself, directly or through a cycle of two graphs, which the intra-graph
            cycle check cannot see.
          example: 0
        output:
          type: object
          nullable: true
          description: Terminal node artifact(s) when the run has succeeded.
        node_executions:
          type: array
          description: >-
            Per-node execution records in chronological order. Each entry captures the resolved input, output, status,
            and error for a single node execution — the orchestration analogue of an LLM trace.
          items:
            $ref: '#/components/schemas/NodeExecution'
        usage:
          allOf:
            - $ref: '#/components/schemas/RunUsageTotals'
          description: >-
            What the run cost: token counts and `cost_usd` summed across every metered generation it produced **and
            every run it started** through `loop` / `sub_orchestration` nodes, at any depth. Present on the single-run
            read; omitted from run list responses.


            A nested child is a run record of its own, so this figure spans several of them. Two consequences: summing
            `usage` across a list that mixes parents and children double-counts (filter with `nested=false`), and the
            per-event receipt at `/api/v1/usage/receipt` stays scoped to one run — its line items carry a `node_id` from
            one graph only.
        usage_own:
          allOf:
            - $ref: '#/components/schemas/RunUsageTotals'
          description: >-
            The same roll-up restricted to **this run's own nodes**, excluding every nested run it started. Equal to
            `usage` for a run with no children; below it for a run that delegates. Present on the single-run read;
            omitted from run list responses.


            This is the field to read to see where cost sits in a run tree — own versus subtree — without walking the
            children.
        required_action:
          allOf:
            - $ref: '#/components/schemas/RequiredAction'
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    RunUsageTotals:
      type: object
      description: Token and cost roll-up for an orchestration run, summed across every usage event the run's generations produced.
      properties:
        total_input_tokens:
          type: integer
        total_output_tokens:
          type: integer
        total_cached_tokens:
          type: integer
        total_reasoning_tokens:
          type: integer
        total_cost_usd:
          type: number
          nullable: true
          description: Sum of the run's priced component costs in USD. Null when nothing on the run was priced.
    NodeExecution:
      type: object
      description: >-
        Record of a single node execution within a run, used to debug which node failed, what input it received, and
        what it produced.
      required:
        - node_id
        - attempt
        - status
        - created_at
      properties:
        node_id:
          type: string
          description: ID of the executed node.
        node_type:
          type: string
          nullable: true
          description: Type of the executed node (e.g. agent, transform).
        attempt:
          type: integer
          description: >
            1-based attempt number. A node with a retry policy produces one record per attempt (failed attempts followed
            by a final record).
        status:
          type: string
          enum:
            - running
            - completed
            - failed
            - requires_action
            - skipped
          description: >-
            Node execution status. `running` marks an execution record whose node is still in flight. Open set — new
            statuses may be added in minor releases; clients must tolerate unknown values.
        input:
          type: object
          nullable: true
          description: Resolved input_mapping the node received.
        output:
          type: object
          nullable: true
          description: Output artifact the node produced (null when failed).
        error:
          type: object
          nullable: true
          description: Error details when status is failed.
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    RequiredAction:
      type: object
      description: Details for an awaiting_input run waiting for human input.
      required:
        - type
        - node_id
        - prompt
        - context
      properties:
        type:
          type: string
          enum:
            - human_input
            - webhook_receive
            - approval
          description: >-
            Discriminator identifying the kind of pause. Open enum — new pause kinds may be added in minor releases;
            clients must tolerate unknown values.
        node_id:
          type: string
        prompt:
          type: string
        context:
          type: object
        options:
          type: array
          items:
            type: string
          nullable: true
        approval_spec:
          type: object
          description: >-
            Present only for `approval` pauses — the frozen tool proposal the engine emits as an ApprovalItem when the
            run parks. Copied as a value; inner keys stay exactly as authored.
        approval_id:
          x-soat-ref: approvals
          type: string
          description: Present once the approval item is emitted.
        expires_at:
          type: string
          format: date-time
          description: Present for `approval` pauses — when the item expires.
    HumanInputRequest:
      type: object
      required:
        - node_id
      properties:
        node_id:
          type: string
          description: ID of the human node to satisfy.
        output:
          type: object
          description: Output/response provided by the human reviewer.
    StartRunRequest:
      type: object
      required:
        - orchestration_id
      properties:
        orchestration_id:
          x-soat-ref: orchestrations
          type: string
          description: Orchestration to run (orch_...).
          example: orch_V1StGXR8Z5jdHi6B
        input:
          type: object
          description: Initial state for the run (merged with orchestration defaults).
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            made by an agent node of this run — including the agents of any child run a `loop` or `sub_orchestration`
            node starts. The header name is `X-Soat-Context-` plus the key verbatim; no character is re-cased.


            The bag is stored on the run and re-read on every step, so it survives an `awaiting_input` pause, a
            `sleeping` wait, a background worker drive and a crash redrive. A key that is not a valid HTTP header name,
            or two keys that map to the same header, are rejected with `400 INVALID_TOOL_CONTEXT_KEY` and no run is
            created.


            The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped at generation time —
            a caller cannot address them from here.
          example:
            ocaToken: eyJhbGciOiJIUzI1NiJ9.abc
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Caller-supplied key/value metadata attached to the run record for per-run attribution (e.g. which of your
            own tenants this run belongs to, or the dispatch batch that started it). Round-trips verbatim on every read
            of the run, on the list as well as the single read.


            The bag is caller-owned and no key is reserved: server-owned state (status, the pinned orchestration
            version, the trace, usage, artifacts, the run's own `input` and accumulated `state`) lives in its own
            top-level field and cannot be written from here.


            It is **not** merged into run state: no graph node sees it, and an `input_schema` never has to tolerate it —
            which is what makes it the place for an infrastructural label, rather than `input`. Keys are never
            transformed. It is not inherited by the child runs a `loop` or `sub_orchestration` node starts; each child
            carries whatever the graph gives it, which today is nothing.
          example:
            tenant_account_id: '42'
            dispatch_batch: nightly-2026-08-25
        wait:
          type: boolean
          default: false
          description: >-
            When true, block until the run reaches a terminal (succeeded/failed) or awaiting_input state and return the
            settled run. When false (default), return immediately with status "queued" and execute the run in the
            background.
    ValidateOrchestrationRequest:
      type: object
      properties:
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/OrchestrationEdge'
        input_schema:
          type: object
          nullable: true
          description: Optional JSON Schema for run inputs; its top-level properties seed state.
    PolicyStatement:
      type: object
      required:
        - effect
        - action
      properties:
        effect:
          type: string
          enum:
            - Allow
            - Deny
          example: Allow
        action:
          type: array
          items:
            type: string
          example:
            - files:ListFiles
            - files:CreateFile
        resource:
          type: array
          items:
            type: string
          example:
            - srn:proj_abc:files:*
        condition:
          type: object
          additionalProperties: true
          description: >
            Optional condition block. Keys are condition operators (e.g. StringEquals) mapping to context-key/value
            maps; evaluated by the policy compiler. Free-form — keys are dynamic, not validated. Operator and context
            keys are matched by exact string and are never case-converted, so they round-trip exactly as written:
            `soat:ResourceTag/cost_center` selects the tag `cost_center`, not `costCenter`.
          example:
            StringEquals:
              soat:ResourceTag/env: prod
    PolicyDocument:
      type: object
      required:
        - statement
      properties:
        statement:
          type: array
          items:
            $ref: '#/components/schemas/PolicyStatement'
    PolicyRecord:
      type: object
      properties:
        id:
          type: string
          description: Public policy ID (pol_ prefix)
          example: pol_V1StGXR8Z5jdHi6B
        name:
          type: string
          nullable: true
          example: ReadOnlyAccess
        description:
          type: string
          nullable: true
        document:
          $ref: '#/components/schemas/PolicyDocument'
        created_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
    ProjectRecord:
      type: object
      properties:
        id:
          type: string
          description: Public project ID (proj_ prefix)
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: My Project
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Guardrails attached at the project scope — the baseline governing every tool call by every agent in the
            project.
          example: []
        max_concurrent_runs:
          type: integer
          nullable: true
          minimum: 1
          description: >-
            Maximum orchestration runs of this project driven at once. `null` means unlimited (the default). Enforced at
            queue claim time.
          example: null
        max_chain_generations:
          type: integer
          nullable: true
          minimum: 1
          description: >-
            Generations one continuation chain in this project may hold before the platform stops resuming it. `null`
            means no project ceiling (the default), leaving the deployment-wide one. The effective budget is the
            smallest of the deployment's ceiling, this one, and the agent's own `max_chain_generations` stop condition.
          example: null
        max_run_depth:
          type: integer
          nullable: true
          minimum: 1
          description: >-
            `loop` / `sub_orchestration` nesting levels a run tree in this project may reach before the engine refuses
            to start the next child. `null` means no project bound (the default), leaving the deployment-wide one. The
            effective bound is the smaller of the two.
          example: null
        default_model_route_id:
          x-soat-ref: model-routes
          type: string
          nullable: true
          description: >-
            Model route inherited by consumers in this project that bind neither `model_route_id` nor `ai_provider_id`.
            `null` means no default, so every consumer must bind explicitly.
          example: null
        audit_reads_enabled:
          type: boolean
          description: >-
            Whether `GET` requests naming this project are recorded in the audit log. `false` by default — v1 audits
            mutations only.
          example: false
        trace_content_retention_days:
          type: integer
          nullable: true
          minimum: 1
          description: >-
            Days of trace/generation content retention before the daily sweep purges it. `null` means retention is
            disabled (the default) and content is kept until purged on demand.
          example: null
        trace_content_mode:
          type: string
          enum:
            - full
            - none
          description: >-
            Whether trace/generation content is persisted. `full` by default; `none` is zero-retention — content is
            never written for any agent in this project.
          example: full
        created_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
    ProjectPrice:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the price row
          example: price_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: The project this price is scoped to
          example: proj_V1StGXR8Z5jdHi6B
        ai_provider_id:
          type: string
          nullable: true
          description: Always null for a project + provider-slug price
        meter_type:
          type: string
          description: Always `llm_tokens` for project prices
        provider:
          type: string
          example: openai
        model:
          type: string
          example: gpt-4o
        component:
          type: string
          description: The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`)
        unit:
          type: string
          description: Always `token` for token pricing
        unit_price:
          type: number
          description: USD per token for this component
        effective_from:
          type: string
          format: date-time
          description: The row with the latest effective_from <= now() prices a call
        created_at:
          type: string
          format: date-time
    ProjectPricesResponse:
      type: object
      properties:
        prices:
          type: array
          items:
            $ref: '#/components/schemas/ProjectPrice'
    UpsertProjectPricesRequest:
      type: object
      required:
        - prices
      properties:
        prices:
          type: array
          items:
            type: object
            required:
              - provider
              - model
              - component
              - unit
              - unit_price
              - effective_from
            properties:
              provider:
                type: string
                example: openai
              model:
                type: string
                example: gpt-4o
              component:
                type: string
                description: The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`)
              unit:
                type: string
                description: Always `token` for token pricing
              unit_price:
                type: number
                description: USD per token for this component
              effective_from:
                type: string
                format: date-time
                description: Must be in the future; past prices are immutable
    Quota:
      type: object
      properties:
        id:
          type: string
          example: quota_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        scope:
          type: string
          enum:
            - project
            - api_key
            - agent
            - actor
        scope_ref:
          type: string
          nullable: true
          description: >-
            Public id of the api key / agent / actor the quota applies to. For `api_key` and `agent` scope, NULL means
            all entities of that scope type in the project. For `actor` scope, NULL means one budget *per* actor rather
            than a pooled total across all actors.
        metric:
          type: string
          enum:
            - requests
            - tokens
            - cost_usd
        window:
          type: string
          enum:
            - rolling_1m
            - rolling_1h
            - rolling_24h
            - calendar_month
        limit:
          type: number
        mode:
          type: string
          enum:
            - enforce
            - monitor
        on_unpriced:
          type: string
          enum:
            - block
            - allow
            - null
          nullable: true
          description: >-
            Pricing posture of a cost_usd quota over an unpriced blackout — block refuses generations, allow lets them
            through (the quota_unpriced exception is filed either way). Null for metrics with no pricing dependency.
        current_usage:
          type: object
          nullable: true
          description: >-
            Current fixed-window usage for the requests metric. Null for token/cost quotas (which aggregate the usage
            meter at check time rather than keeping a counter) and in list responses.
          properties:
            window_key:
              type: string
              example: 2026-07-07T12:31Z
            count:
              type: integer
              example: 42
            resets_at:
              type: string
              format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SessionRecord:
      type: object
      properties:
        id:
          type: string
          description: Session public ID
          example: sess_V1StGXR8Z5jdHi6B
        agent_id:
          x-soat-ref: agents
          type: string
          description: Agent public ID
          example: agent_V1StGXR8Z5jdHi6B
        conversation_id:
          x-soat-ref: conversations
          type: string
          description: Underlying conversation public ID
          example: conv_V1StGXR8Z5jdHi6B
        status:
          type: string
          enum:
            - open
            - closed
            - expired
          example: open
        name:
          type: string
          nullable: true
          example: Support chat
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: |
            Public ID of the user actor, or null when the session was created without one
          example: actor_V1StGXR8Z5jdHi6B
        tags:
          type: object
          additionalProperties:
            type: string
        auto_generate:
          type: boolean
          default: false
          description: When true, automatically triggers generation after each user message (if no generation is in progress).
        generating_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the current generation started, or null if not generating.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            in this session. The header name is the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased. Keys are never case-converted — they round-trip
            exactly as sent. A key that is not a valid HTTP header name, or two keys that map to the same header, are
            rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
        inactivity_ttl_seconds:
          type: integer
          default: 0
          description: Number of seconds of inactivity after which the session expires. 0 means the session never expires.
          example: 300
        message_delay_seconds:
          type: integer
          nullable: true
          default: null
          description: >
            Number of seconds to wait after the last user message before sending to the LLM. Acts as a debounce: each
            new message resets the timer. null or absent means no delay (immediate processing).
          example: 3
        last_activity_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the last activity on the session (message added or response generated).
        forked_from_session_id:
          x-soat-ref: sessions
          type: string
          nullable: true
          description: >
            Public ID of the session this one was forked from, or null when it was not forked. Also null once that
            parent is deleted — a fork survives its parent and keeps its own history.
          example: sess_V1StGXR8Z5jdHi6B
        forked_from_position:
          type: integer
          nullable: true
          description: >
            The parent conversation position this session branched after, or null when it is not a fork or was forked at
            the tip.
          example: 7
    ForkSessionRequest:
      type: object
      properties:
        fork_at_position:
          type: integer
          minimum: 0
          description: >
            The parent conversation `position` to branch after. Messages at positions 0..N are carried into the fork.
            Omit it to branch at the tip (the whole history).
          example: 7
        agent_id:
          x-soat-ref: agents
          type: string
          description: >
            Agent the fork runs against. Defaults to the parent session's agent; overriding it is the point of forking —
            same context, a different agent or agent version. Must belong to the same project as the session being
            forked.
          example: agent_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional name for the forked session
          example: retry with stricter system prompt
        tags:
          type: object
          additionalProperties:
            type: string
          description: Optional tags for the forked session
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >
            Overrides the parent's `tool_context` on the fork. Omit it and the fork inherits the parent's, so the branch
            is faithful to the run it came from.
    CreateSessionRequest:
      type: object
      required:
        - agent_id
      properties:
        agent_id:
          x-soat-ref: agents
          type: string
          description: Agent this session belongs to
          example: agent_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional session name
          example: Support chat
        actor_id:
          x-soat-ref: actors
          type: string
          description: >
            Optional public ID of an existing actor to use as the user actor. Actors are created separately (POST
            /actors); this field only links one. Omit it and the session has no end user, so its generations match no
            actor-scoped quota.
          example: actor_V1StGXR8Z5jdHi6B
        auto_generate:
          type: boolean
          default: false
          description: When true, automatically triggers generation after each user message.
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            in this session. The header name is the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased. Keys are never case-converted — they round-trip
            exactly as sent. A key that is not a valid HTTP header name, or two keys that map to the same header, are
            rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
        inactivity_ttl_seconds:
          type: integer
          default: 0
          description: Number of seconds of inactivity after which the session expires. 0 means the session never expires.
          example: 300
        message_delay_seconds:
          type: integer
          nullable: true
          default: null
          description: >
            Number of seconds to wait after the last user message before sending to the LLM. Acts as a debounce: each
            new message resets the timer. null or absent means no delay (immediate processing).
          example: 3
    UpdateSessionRequest:
      type: object
      properties:
        name:
          type: string
          nullable: true
          description: Session name (set to null to clear)
        status:
          type: string
          enum:
            - open
            - closed
            - expired
          description: Session status
        auto_generate:
          type: boolean
          description: Enable or disable automatic generation after user messages.
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            in this session. The header name is the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased. Keys are never case-converted — they round-trip
            exactly as sent. A key that is not a valid HTTP header name, or two keys that map to the same header, are
            rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
        inactivity_ttl_seconds:
          type: integer
          description: >
            Number of seconds of inactivity after which the session expires. 0 means the session never expires. Updates
            the stored TTL; the inactivity clock continues from the last activity timestamp.
          example: 300
        message_delay_seconds:
          type: integer
          nullable: true
          description: >
            Number of seconds to wait after the last user message before sending to the LLM. Acts as a debounce: each
            new message resets the timer. Set to null to disable the delay.
          example: 3
    AddSessionMessageRequest:
      oneOf:
        - type: object
          additionalProperties: false
          required:
            - message
          properties:
            message:
              type: string
              description: User message text
              example: Hello, how can I deploy my app?
            tool_context:
              type: object
              additionalProperties:
                type: string
              nullable: true
              description: >-
                Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool
                call in this generation. The header name is the deployment's configured context prefix
                (`X-Soat-Context-` by default) plus the key verbatim — no character is re-cased. Keys are never
                case-converted — they round-trip exactly as sent. An invalid or colliding key is rejected with `400
                INVALID_TOOL_CONTEXT_KEY`.
            idempotency_key:
              type: string
              description: >
                Optional deduplication key scoped to this session. If a message with the same key already exists in the
                session, the original message is returned with HTTP 200 and no new message or generation is triggered.
              example: wamid.HBgLNTUxMTk4...
        - type: object
          additionalProperties: false
          required:
            - document_id
          properties:
            document_id:
              x-soat-ref: documents
              type: string
              description: Public ID of a document used as the user message content.
            tool_context:
              type: object
              additionalProperties:
                type: string
              nullable: true
              description: >-
                Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool
                call in this generation. The header name is the deployment's configured context prefix
                (`X-Soat-Context-` by default) plus the key verbatim — no character is re-cased. Keys are never
                case-converted — they round-trip exactly as sent. An invalid or colliding key is rejected with `400
                INVALID_TOOL_CONTEXT_KEY`.
            idempotency_key:
              type: string
              description: >
                Optional deduplication key scoped to this session. If a message with the same key already exists in the
                session, the original message is returned with HTTP 200 and no new message or generation is triggered.
              example: wamid.HBgLNTUxMTk4...
    AddSessionMessageSaved:
      type: object
      description: Message saved; auto-generate is off or a generation is already in progress.
      properties:
        role:
          type: string
          enum:
            - user
        content:
          type: string
        document_id:
          x-soat-ref: documents
          type: string
          nullable: true
    AddSessionMessageResponse:
      anyOf:
        - $ref: '#/components/schemas/AddSessionMessageSaved'
        - $ref: '#/components/schemas/GenerateSessionResponse'
    GenerateSessionRequest:
      type: object
      properties:
        model:
          type: string
          description: Optional model override
          example: gpt-4o
        tool_context:
          type: object
          additionalProperties:
            type: string
          nullable: true
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            in this generation. The header name is the deployment's configured context prefix (`X-Soat-Context-` by
            default) plus the key verbatim — no character is re-cased. Keys are never case-converted — they round-trip
            exactly as sent. An invalid or colliding key is rejected with `400 INVALID_TOOL_CONTEXT_KEY`.
    GenerateSessionResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - completed
            - requires_action
        message:
          type: object
          properties:
            role:
              type: string
            content:
              type: string
            model:
              type: string
        generation_id:
          x-soat-ref: generations
          type: string
        trace_id:
          x-soat-ref: traces
          type: string
        required_action:
          type: object
          description: Present when status is requires_action
          properties:
            tool_calls:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  tool_name:
                    type: string
                  args:
                    type: object
    SendSessionMessageResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - completed
            - requires_action
        message:
          type: object
          properties:
            role:
              type: string
            content:
              type: string
            model:
              type: string
        generation_id:
          x-soat-ref: generations
          type: string
        trace_id:
          x-soat-ref: traces
          type: string
        required_action:
          type: object
          description: Present when status is requires_action
          properties:
            tool_calls:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  tool_name:
                    type: string
                  args:
                    type: object
    SubmitSessionToolOutputsRequest:
      type: object
      required:
        - generation_id
        - tool_outputs
      properties:
        generation_id:
          x-soat-ref: generations
          type: string
          description: The generation ID from the requires_action response
        tool_outputs:
          type: array
          items:
            type: object
            required:
              - tool_call_id
              - output
            properties:
              tool_call_id:
                type: string
              output:
                description: The tool output value
    Task:
      type: object
      properties:
        id:
          type: string
        project_id:
          type: string
        workflow_id:
          type: string
        workflow_version:
          type: integer
          nullable: true
          description: >-
            The workflow version this task runs on, fixed when the task was created. Transitions, approval gates and
            payload validation all resolve through it, so editing the workflow never re-shapes a task already in flight.
            `null` for tasks created before pinning existed, which run on the live definition.
          example: 1
        title:
          type: string
        state:
          type: string
        status:
          type: string
          enum:
            - open
            - closed
        payload:
          type: object
          description: >-
            Caller-owned task data; input to guards (as `task.payload`) and dispatch mappings. The engine never writes
            into it except the workflow's declared `payload_writes`.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            The caller-owned key/value metadata supplied when the task was created, returned verbatim. Null when the
            task was created without any. Unlike `payload` it is invisible to guards and to `payload_writes`, so it is
            the place for an attribution label rather than task data.
          example:
            tenant_account_id: '42'
            source: zendesk
        last_result:
          nullable: true
          description: >-
            Server-owned. The result of the current state's last completed dispatch, overwritten on every dispatch.
            Read-only — exposed to transition guards and `on_complete`/`payload_writes` expressions as
            `task.last_result`, a namespace a caller cannot write.
        assignee:
          type: string
          nullable: true
        active_dispatch:
          type: object
          nullable: true
          description: >-
            { kind, id, status } of the current state's dispatch, if any. `kind` is `generation`, `orchestration_run` or
            `tool_call`; a `tool_call` always carries a null `id`, since a direct tool call leaves no addressable
            record. Carries an additional `attempt` (1-based) while the state's `on_enter.retry` policy is in effect.
        automation_status:
          type: string
          nullable: true
          enum:
            - running
            - completed
            - failed
            - unrouted
            - null
          description: Status of the current state's dispatch. `null` until a state with an automation is entered.
        automation_chain_depth:
          type: integer
          description: >-
            Server-owned. How many machine-driven transitions have run back-to-back with no outside intervention — a
            dispatch outcome routed through `on_complete`/`on_failure`, or a `transition-task` call made by a dispatched
            run or agent with its run-as token. Any move by a person, a plain API key, or an approval resolution resets
            it to `0`. Once it would exceed the server's limit (`TASK_AUTOMATION_CHAIN_LIMIT`, default 50) the next such
            transition is refused with `TASK_AUTOMATION_CHAIN_LIMIT`, bounding a cycle composed across workflows and
            orchestrations.
        pending_transition:
          type: string
          nullable: true
          description: >-
            The name of a `requires_approval` transition parked awaiting a human decision. Non-null while an
            ApprovalItem gates the move; the task stays in its current state and no other transition may fire until the
            approval resolves.
        entered_state_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TaskTransition:
      type: object
      properties:
        id:
          type: string
        task_id:
          type: string
        from_state:
          type: string
          nullable: true
        to_state:
          type: string
        transition:
          type: string
          nullable: true
        principal_kind:
          type: string
          description: >
            Who made the move. `user` and `api_key` are authenticated principals; `automation` (the engine acting on an
            `on_enter` dispatch outcome) and `approval` (an approval resolution) are system principals. Named
            `principal_*`, not `actor_*`: these ids never reference the Actors module.
          enum:
            - user
            - api_key
            - automation
            - approval
        principal_id:
          type: string
          nullable: true
          description: >
            Public id of the principal that made the move — the user (`user_...`), or for `api_key` auth the key's own
            id (`key_...`), distinguishing which key acted. Null for `automation`, which has no principal: the cause is
            carried by `generation_id` / `orchestration_run_id` / `tool_id`, one per dispatch kind — exactly one of
            which is set on an automation move.
        generation_id:
          type: string
          nullable: true
          description: Set when an `agent` dispatch's generation caused the move.
        orchestration_run_id:
          type: string
          nullable: true
          description: Set when an `orchestration` dispatch's run caused the move.
        tool_id:
          type: string
          nullable: true
          description: >
            Set when a `tool` dispatch caused the move. A tool call produces no addressable record of its own, so the
            tool it called is what records why the task moved.
        note:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    CreateTaskRequest:
      type: object
      required:
        - workflow_id
        - title
      properties:
        project_id:
          type: string
        workflow_id:
          type: string
        title:
          type: string
        payload:
          type: object
        assignee:
          type: string
          nullable: true
        state:
          type: string
          description: >-
            Name of a declared workflow state to create the task in directly, instead of the workflow's `initial` state.
            Must name a state declared on the workflow, or the request is rejected with `TASK_STATE_NOT_FOUND` (400).
            Defaults to the `initial` state.
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >-
            Key-value pairs forwarded as `X-Soat-Context-<key>` headers on every `http`, `mcp` and `builtin` tool call
            made by this task's automation dispatches — the agent generations a state's `on_enter` starts, and the agent
            nodes of any orchestration run it starts. The header name is `X-Soat-Context-` plus the key verbatim; no
            character is re-cased.

            Creation is the task's first move, so this is the bag the entry state's `on_enter` runs with. Each
            transition may replace it (see `TransitionTaskRequest.tool_context`).

            The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped in any casing and
            re-derived server-side, so a task-dispatched generation cannot forge them. A key outside the HTTP
            header-name grammar is rejected with `INVALID_TOOL_CONTEXT_KEY` (400).

            Write-only: the stored bag is never returned by any task read, and it is cleared when the task reaches a
            terminal state.
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Caller-supplied key/value metadata attached to the task record for attribution — which of your own tenants
            the task belongs to, the ticket that raised it, the import batch that created it. Round-trips verbatim on
            every read of the task, the list included, and survives every transition (a transition supplies no metadata
            of its own).


            The bag is caller-owned and no key is reserved: everything the engine decides about a task (`state`,
            `status`, `workflow_version`, `last_result`, `active_dispatch`, the automation fields) is a field of its own
            and cannot be written from here.


            Prefer this over `payload` for anything that is not task data: `payload` is read by every guard as
            `task.payload` and may be written by the workflow's declared `payload_writes`, so a label parked there is
            neither invisible to the state machine nor safe from it. A non-object is rejected with `400
            VALIDATION_FAILED` and no task is created.
          example:
            tenant_account_id: '42'
            source: zendesk
    UpdateTaskRequest:
      type: object
      properties:
        title:
          type: string
        payload:
          type: object
          description: >-
            Partial payload, shallow-merged over the existing payload. Omitted keys are preserved; provided keys
            overwrite. The merged result must satisfy the workflow's payload_schema.
        assignee:
          type: string
          nullable: true
    TransitionTaskRequest:
      type: object
      required:
        - transition
      properties:
        transition:
          type: string
        note:
          type: string
          nullable: true
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >-
            Caller context for the automation dispatches the task makes from here on, forwarded as
            `X-Soat-Context-<key>` headers on their tool calls.

            Supplying it **replaces** the task's stored bag wholesale; omitting it keeps the current one, so the context
            follows whoever last moved the task and survives every move that does not speak about it — including an
            approval gate, a retry, and an automation hop. Send an empty object to clear it without closing the task.

            The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped in any casing and
            re-derived server-side. A key outside the HTTP header-name grammar is rejected with
            `INVALID_TOOL_CONTEXT_KEY` (400).

            Write-only: never returned by a task read, and cleared when the transition closes the task.
    Tool:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the tool
          example: tool_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Tool name
          example: get-weather
        type:
          type: string
          enum:
            - http
            - client
            - mcp
            - builtin
            - pipeline
          description: Tool type
          example: http
        description:
          type: string
          nullable: true
          description: What the tool does (sent to the model)
        parameters:
          type: object
          nullable: true
          description: JSON Schema for tool input
        execute:
          type: object
          nullable: true
          description: >
            Execution config for http tools. Supported fields: `url` (required), `method` (default `POST`), `headers`,
            and `body_mode`. The `url` may contain `{paramName}` placeholders (e.g. `/users/{userId}`) that are replaced
            at call time with the corresponding tool argument value (URL-encoded). Arguments consumed as path parameters
            are excluded from the query string and request body. `body_mode` is `json` (default) or `multipart`. In
            `multipart` mode the merged tool arguments are sent as a `multipart/form-data` body: scalar fields become
            plain form fields and a field shaped like `{ content_type, filename, data_base64 }` is decoded from base64
            and attached as a file part (the hardcoded `Content-Type: application/json` is dropped so `fetch` sets the
            multipart boundary itself).


            `auth` adds a computed request credential, for targets whose `Authorization` value cannot be expressed as a
            static header. Supported `auth.type` values:


            - `aws_sigv4` — signs the request with AWS Signature Version 4. Requires `region`, `service`,
            `access_key_id` and `secret_access_key`; `session_token` is optional (temporary credentials). Incompatible
            with `body_mode: multipart`, whose body bytes are not known at signing time.

            - `gcp_service_account` — mints a Google OAuth 2.0 access token from a signed service account assertion and
            sends it as a bearer token. Requires `credentials` (the service account key file JSON, as a string) and
            `scopes` (a non-empty array). Tokens are cached per service account and scope set until shortly before they
            expire.


            Credential fields accept `{{secret:...}}` references and should use them — a tool is readable by anyone who
            can `GET /tools`, and the stored reference is what is echoed back, never the resolved value.


            `headers` values additionally accept `{{context:<key>}}` references, resolved per call from the caller's
            `tool_context`, so a per-user credential can be placed in the real header the target expects
            (`Authorization: Bearer {{context:ocaToken}}`) instead of only in a prefixed context header. Valid **only**
            inside `headers` — a context value is caller-supplied, so it may not steer the `url` — and a key missing
            from the `tool_context` at call time fails the tool call with `MISSING_TOOL_CONTEXT_KEY` rather than sending
            an empty credential. See the Tool Context reference.
        mcp:
          type: object
          nullable: true
          description: >-
            MCP server config (`url`, `headers`). `headers` values accept `{{secret:...}}` and `{{context:<key>}}`
            references, resolved right before the outbound MCP request; `url` accepts `{{secret:...}}` only.
        actions:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Allowlist of actions to expose. For `builtin` tools: SOAT platform action names (required). For `mcp` tools:
            an optional allowlist of MCP tool names — when set, only those tools are exposed to the model and callable
            via `/call`; when `null`, the entire MCP server surface is exposed. Ignored for other tool types.
        denied_actions:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            For `mcp` tools: an optional denylist of MCP tool names to hide. Applied after `actions` and taking
            precedence over it — a name in both lists is denied. This is the ergonomic way to scope a read+write MCP
            server read-only: deny just the write tools instead of enumerating every read tool in `actions`. Names not
            listed are exposed. `null` (default) denies nothing. Ignored for other tool types.
        context_keys:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Optional allowlist of `tool_context` keys that may be forwarded to this tool as prefixed context headers
            (`X-Soat-Context-<key>` by default). When `null`, every key in the caller's `tool_context` is forwarded —
            the behavior of every tool created before this field existed. When set, only the listed keys are, so a
            per-user credential in `tool_context` can be confined to the tools that need it; `[]` forwards none. The
            server-pinned identity keys (`session_id`, `actor_id`, `actor_external_id`) are always forwarded. A key
            consumed by a `{{context:<key>}}` token in this tool's own headers is substituted regardless of this list —
            the tool declared that header itself.
        preset_parameters:
          type: object
          nullable: true
          description: >-
            Fixed parameters pinned on every call this tool makes, whatever its type. Keys matching fields in the input
            schema are removed from the schema shown to the model, and a pinned value wins over one the model or a
            direct caller supplies for the same key.


            Values accept `{{context:<key>}}` references, resolved per call from the caller's `tool_context`, so a pin
            can be the run's own value — the one account this run may act on — rather than one fixed when the tool was
            created. A resolved value is retyped to the parameter's declared schema type; a key missing from the call's
            `tool_context` fails the call with `MISSING_TOOL_CONTEXT_KEY` rather than sending the literal placeholder.
            `{{secret:...}}` is not resolved here. See the Tool Context reference.
        pipeline:
          type: object
          nullable: true
          description: >-
            Pipeline definition for `pipeline` tools: an ordered `steps` array, each step invoking a tool (optional
            `action`) and building its `input` from earlier results via JSON Logic evaluated over `{ input, steps }`. A
            step references its tool either by `tool_id` (an existing, persisted tool) or by an inline `tool` definition
            — the same shape as `CreateToolRequest` minus `project_id` — executed directly without a Tool row, but never
            both. An inline step `tool` cannot itself be of type `pipeline`. An optional `output` maps the final result.
        output_mapping:
          type: object
          nullable: true
          description: >-
            Universal JSON Logic mapping applied to the tool's raw result, for every tool type (`http`, `mcp`,
            `builtin`, `pipeline`, `client`). Evaluated over `{ output: <raw result>, input: <merged input> }`, so `{
            "var": "output.text" }` extracts a bare scalar field instead of requiring a wrapping `pipeline` tool, and `{
            "var": "input.title" }` echoes back a field of the request that produced the response. For `pipeline` tools
            this runs *after* the pipeline's own `output` mapping, over that mapping's result.
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the tool scope, governing this tool wherever it is used, by any agent.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateToolRequest:
      type: object
      required:
        - name
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
        name:
          type: string
          description: Tool name
        type:
          type: string
          enum:
            - http
            - client
            - mcp
            - builtin
            - pipeline
          description: Tool type (default http)
        description:
          type: string
          description: What the tool does
        parameters:
          type: object
          description: JSON Schema for tool input
        execute:
          type: object
          description: >
            Execution config for http tools. Supported fields: `url` (required), `method` (default `POST`), `headers`,
            and `body_mode`. The `url` may contain `{paramName}` placeholders (e.g. `/users/{userId}`) that are replaced
            at call time with the corresponding tool argument value (URL-encoded). Arguments consumed as path parameters
            are excluded from the query string and request body. `body_mode` is `json` (default) or `multipart`. In
            `multipart` mode the merged tool arguments are sent as a `multipart/form-data` body: scalar fields become
            plain form fields and a field shaped like `{ content_type, filename, data_base64 }` is decoded from base64
            and attached as a file part (the hardcoded `Content-Type: application/json` is dropped so `fetch` sets the
            multipart boundary itself).


            `auth` adds a computed request credential, for targets whose `Authorization` value cannot be expressed as a
            static header. Supported `auth.type` values:


            - `aws_sigv4` — signs the request with AWS Signature Version 4. Requires `region`, `service`,
            `access_key_id` and `secret_access_key`; `session_token` is optional (temporary credentials). Incompatible
            with `body_mode: multipart`, whose body bytes are not known at signing time.

            - `gcp_service_account` — mints a Google OAuth 2.0 access token from a signed service account assertion and
            sends it as a bearer token. Requires `credentials` (the service account key file JSON, as a string) and
            `scopes` (a non-empty array). Tokens are cached per service account and scope set until shortly before they
            expire.


            Credential fields accept `{{secret:...}}` references and should use them — a tool is readable by anyone who
            can `GET /tools`, and the stored reference is what is echoed back, never the resolved value.


            `headers` values additionally accept `{{context:<key>}}` references, resolved per call from the caller's
            `tool_context`, so a per-user credential can be placed in the real header the target expects
            (`Authorization: Bearer {{context:ocaToken}}`) instead of only in an `X-Soat-Context-<key>` header. Valid
            **only** inside `headers` — a context value is caller-supplied, so it may not steer the `url` — and a key
            missing from the `tool_context` at call time fails the tool call with `MISSING_TOOL_CONTEXT_KEY` rather than
            sending an empty credential. See the Tool Context reference.
        mcp:
          type: object
          description: >-
            MCP server config (`url`, `headers`). `headers` values accept `{{secret:...}}` and `{{context:<key>}}`
            references, resolved right before the outbound MCP request; `url` accepts `{{secret:...}}` only.
        actions:
          type: array
          items:
            type: string
          description: >-
            Allowlist of actions. For `builtin` tools: SOAT platform action names (required). For `mcp` tools: an
            optional allowlist of MCP tool names to scope the server surface — omit or set `null` to expose every tool
            the MCP server offers. Ignored for other tool types.
        denied_actions:
          type: array
          items:
            type: string
          description: >-
            For `mcp` tools: an optional denylist of MCP tool names to hide. Applied after `actions` and taking
            precedence over it. Use it to scope a read+write MCP server read-only by denying just the write tools. Omit
            or set `null` to deny nothing. Ignored for other tool types.
        context_keys:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Optional allowlist of `tool_context` keys that may be forwarded to this tool as prefixed context headers
            (`X-Soat-Context-<key>` by default). When `null` or omitted, every key in the caller's `tool_context` is
            forwarded — the behavior of every tool created before this field existed. When set, only the listed keys
            are, so a per-user credential in `tool_context` can be confined to the tools that need it; `[]` forwards
            none. The server-pinned identity keys (`session_id`, `actor_id`, `actor_external_id`) are always forwarded.
            A key consumed by a `{{context:<key>}}` token in this tool's own headers is substituted regardless of this
            list — the tool declared that header itself.
        preset_parameters:
          type: object
          description: >-
            Fixed parameters pinned on every call this tool makes, whatever its type. Keys matching fields in the input
            schema are removed from the schema shown to the model, and a pinned value wins over one the model or a
            direct caller supplies for the same key.


            Values accept `{{context:<key>}}` references, resolved per call from the caller's `tool_context`, so a pin
            can be the run's own value — the one account this run may act on — rather than one fixed when the tool was
            created. A resolved value is retyped to the parameter's declared schema type; a key missing from the call's
            `tool_context` fails the call with `MISSING_TOOL_CONTEXT_KEY` rather than sending the literal placeholder.
            `{{secret:...}}` is not resolved here. See the Tool Context reference.
        pipeline:
          type: object
          description: >-
            Pipeline definition for `pipeline` tools. See the `pipeline` field on the Tool schema for the full
            structure.
        output_mapping:
          type: object
          description: >-
            Universal JSON Logic mapping applied to the tool's raw result. See the `output_mapping` field on the Tool
            schema for details.
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the tool scope.
    UpdateToolRequest:
      type: object
      properties:
        name:
          type: string
        type:
          type: string
          enum:
            - http
            - client
            - mcp
            - builtin
            - pipeline
        description:
          type: string
          nullable: true
        parameters:
          type: object
          nullable: true
        execute:
          type: object
          nullable: true
          description: >
            Execution config for http tools. Supported fields: `url` (required), `method` (default `POST`), `headers`,
            and `body_mode`. The `url` may contain `{paramName}` placeholders (e.g. `/users/{userId}`) that are replaced
            at call time with the corresponding tool argument value (URL-encoded). Arguments consumed as path parameters
            are excluded from the query string and request body. `body_mode` is `json` (default) or `multipart`. In
            `multipart` mode the merged tool arguments are sent as a `multipart/form-data` body: scalar fields become
            plain form fields and a field shaped like `{ content_type, filename, data_base64 }` is decoded from base64
            and attached as a file part (the hardcoded `Content-Type: application/json` is dropped so `fetch` sets the
            multipart boundary itself).


            `auth` adds a computed request credential, for targets whose `Authorization` value cannot be expressed as a
            static header. Supported `auth.type` values:


            - `aws_sigv4` — signs the request with AWS Signature Version 4. Requires `region`, `service`,
            `access_key_id` and `secret_access_key`; `session_token` is optional (temporary credentials). Incompatible
            with `body_mode: multipart`, whose body bytes are not known at signing time.

            - `gcp_service_account` — mints a Google OAuth 2.0 access token from a signed service account assertion and
            sends it as a bearer token. Requires `credentials` (the service account key file JSON, as a string) and
            `scopes` (a non-empty array). Tokens are cached per service account and scope set until shortly before they
            expire.


            Credential fields accept `{{secret:...}}` references and should use them — a tool is readable by anyone who
            can `GET /tools`, and the stored reference is what is echoed back, never the resolved value.


            `headers` values additionally accept `{{context:<key>}}` references, resolved per call from the caller's
            `tool_context`, so a per-user credential can be placed in the real header the target expects
            (`Authorization: Bearer {{context:ocaToken}}`) instead of only in an `X-Soat-Context-<key>` header. Valid
            **only** inside `headers` — a context value is caller-supplied, so it may not steer the `url` — and a key
            missing from the `tool_context` at call time fails the tool call with `MISSING_TOOL_CONTEXT_KEY` rather than
            sending an empty credential. See the Tool Context reference.
        mcp:
          type: object
          nullable: true
          description: >-
            MCP server config (`url`, `headers`). `headers` values accept `{{secret:...}}` and `{{context:<key>}}`
            references, resolved right before the outbound MCP request; `url` accepts `{{secret:...}}` only.
        actions:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Allowlist of actions. For `builtin` tools: SOAT platform action names. For `mcp` tools: an optional
            allowlist of MCP tool names to scope the server surface (`null` exposes every tool). Ignored for other tool
            types.
        denied_actions:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            For `mcp` tools: an optional denylist of MCP tool names to hide. Applied after `actions` and taking
            precedence over it. Use it to scope a read+write MCP server read-only by denying just the write tools.
            `null` denies nothing. Ignored for other tool types.
        context_keys:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Optional allowlist of `tool_context` keys that may be forwarded to this tool as prefixed context headers
            (`X-Soat-Context-<key>` by default). When `null` or omitted, every key in the caller's `tool_context` is
            forwarded — the behavior of every tool created before this field existed. When set, only the listed keys
            are, so a per-user credential in `tool_context` can be confined to the tools that need it; `[]` forwards
            none. The server-pinned identity keys (`session_id`, `actor_id`, `actor_external_id`) are always forwarded.
            A key consumed by a `{{context:<key>}}` token in this tool's own headers is substituted regardless of this
            list — the tool declared that header itself.
        preset_parameters:
          type: object
          nullable: true
          description: >-
            Fixed parameters pinned on every call this tool makes, whatever its type. Keys matching fields in the input
            schema are removed from the schema shown to the model, and a pinned value wins over one the model or a
            direct caller supplies for the same key.


            Values accept `{{context:<key>}}` references, resolved per call from the caller's `tool_context`, so a pin
            can be the run's own value — the one account this run may act on — rather than one fixed when the tool was
            created. A resolved value is retyped to the parameter's declared schema type; a key missing from the call's
            `tool_context` fails the call with `MISSING_TOOL_CONTEXT_KEY` rather than sending the literal placeholder.
            `{{secret:...}}` is not resolved here. See the Tool Context reference.
        pipeline:
          type: object
          nullable: true
          description: >-
            Pipeline definition for `pipeline` tools. See the `pipeline` field on the Tool schema for the full
            structure.
        output_mapping:
          type: object
          nullable: true
          description: >-
            Universal JSON Logic mapping applied to the tool's raw result. See the `output_mapping` field on the Tool
            schema for details.
        guardrail_ids:
          x-soat-ref: guardrails
          type: array
          nullable: true
          items:
            type: string
          description: Guardrails attached at the tool scope.
    CallToolRequest:
      type: object
      properties:
        action:
          type: string
          description: >
            For `builtin` tools: the action name (must be in the tool's `actions` list). For `mcp` tools: the MCP tool
            name to invoke (must be in the tool's `actions` allowlist when one is set, and must not be in its
            `denied_actions` denylist). Ignored for `http` tools.
        input:
          type: object
          description: >
            Input parameters for the tool call. These are merged with the tool's `preset_parameters` before execution; a
            preset value wins over the same key sent here.
          additionalProperties: true
        tool_context:
          type: object
          additionalProperties:
            type: string
          description: >
            Key/value context for this call, forwarded to the tool as `X-Soat-Context-<key>` request headers and
            resolving any `{{context:<key>}}` token in the tool's `execute.headers`, `mcp.headers` or
            `preset_parameters`. Narrowed by the tool's `context_keys` allowlist when it sets one.

            This route has no session, so it stamps no server-derived identity: the reserved keys `session_id`,
            `actor_id` and `actor_external_id` are dropped from this bag (in any casing) rather than forwarded, so a
            downstream tool can still trust that a context header naming one is server-derived. Every other key becomes
            an HTTP header name and must match that grammar, or the call fails with `INVALID_TOOL_CONTEXT_KEY`.
          example:
            tenantId: acme
            userToken: tok_abc123
    Trace:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the trace
          example: trace_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 produced this trace
        file_id:
          x-soat-ref: files
          type: string
          nullable: true
          description: >
            Public ID of the File containing the full serialized steps JSON. Null if the trace has not been saved yet
            (save is fire-and-forget).
          example: file_xyz789
        step_count:
          type: integer
          description: Number of steps recorded in this trace
          example: 2
        parent_trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          description: >
            Public ID of the parent trace. Null if this trace is the root (i.e., it was not triggered by a sub-agent
            call from another trace).
        root_trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          description: |
            Public ID of the root trace for the entire execution tree. Null if this trace is itself the root.
        error:
          type: object
          nullable: true
          description: >
            Structured error payload recorded when a generation in this trace failed (e.g. an upstream AI provider
            error). Null if no failure has been recorded.
          properties:
            code:
              type: string
              example: AI_PROVIDER_ERROR
            message:
              type: string
              example: 'Provider returned 402: insufficient credits'
            meta:
              type: object
        content_redacted_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When the trace's content was purged. Non-null means the steps object has been deleted from storage and the
            content columns cleared, while this row survives as an auditable skeleton (ids, timestamps, step count). A
            purged trace still reads back as a skeleton with this marker set rather than as a 404, so the erasure is
            provable.
        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
    TraceTreeNode:
      type: object
      description: A trace node in the execution tree, with nested children.
      properties:
        id:
          type: string
          description: Public ID of the trace
        project_id:
          x-soat-ref: projects
          type: string
        agent_id:
          x-soat-ref: agents
          type: string
        file_id:
          x-soat-ref: files
          type: string
          nullable: true
        step_count:
          type: integer
        parent_trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
        root_trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
        error:
          type: object
          nullable: true
          description: Structured error payload recorded when a generation in this trace failed
        created_at:
          type: string
          format: date-time
        content_redacted_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When the trace's content was purged. Non-null means the steps object has been deleted from storage and the
            content columns cleared, while this row survives as an auditable skeleton (ids, timestamps, step count). A
            purged trace still reads back as a skeleton with this marker set rather than as a 404, so the erasure is
            provable.
        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.
        children:
          type: array
          description: Child traces triggered by sub-agent calls from this trace
          items:
            $ref: '#/components/schemas/TraceTreeNode'
        generations:
          type: array
          description: >
            Generations that belong to this trace node. Only present when `include=generations` is requested. Includes
            top-level generations and sub-agent child generations linked via `initiator_generation_id`.
          items:
            $ref: '#/components/schemas/Generation'
    Trigger:
      type: object
      properties:
        id:
          type: string
        project_id:
          x-soat-ref: projects
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        type:
          type: string
          enum:
            - manual
            - webhook
            - schedule
            - event
        target_type:
          type: string
          enum:
            - orchestration
            - agent
            - tool
            - eval
        target_id:
          type: string
          description: Public ID of the target resource (orchestration, agent, tool, or eval)
        action:
          type: string
          nullable: true
          description: Tool targets only — the action for builtin/mcp tools
        input:
          type: object
          nullable: true
          description: >-
            Static input, shallow-merged under fire-time input. For `eval` targets the effective input may carry
            `agent_version` and `baseline_run_id`, which are passed to the queued run.
        cron:
          type: string
          nullable: true
          description: 5-field cron expression (UTC). Present only for schedule triggers
        event_pattern:
          type: string
          nullable: true
          description: >-
            Internal-event subscription pattern. Present only for event triggers: `*`, `prefix.*`, or an exact event
            name
        active:
          type: boolean
        policy_id:
          x-soat-ref: policies
          type: string
          nullable: true
          description: Optional boundary policy that further restricts firings
        next_fire_at:
          type: string
          format: date-time
          nullable: true
          description: Read-only, schedule triggers only. Server-computed next fire time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TriggerWithSecret:
      allOf:
        - $ref: '#/components/schemas/Trigger'
        - type: object
          properties:
            secret:
              type: string
              description: Webhook triggers only. Returned only on create and rotate
    CreateTriggerRequest:
      type: object
      required:
        - name
        - type
        - target_type
        - target_id
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: >-
            Public ID of the project. Optional when authenticating with a project-scoped API key, which defaults to the
            key's project; required otherwise.
        name:
          type: string
        description:
          type: string
        type:
          type: string
          enum:
            - manual
            - webhook
            - schedule
            - event
        target_type:
          type: string
          enum:
            - orchestration
            - agent
            - tool
            - eval
        target_id:
          type: string
        action:
          type: string
          description: Tool targets only — the action for builtin/mcp tools
        input:
          type: object
        cron:
          type: string
          description: 5-field cron expression (UTC). Required when type is schedule
        event_pattern:
          type: string
          description: >-
            Internal-event subscription pattern. Required when type is event, rejected otherwise. `*` matches every
            event, `prefix.*` a namespace, or give an exact event name such as `documents.ingested`
        active:
          type: boolean
          default: true
        policy_id:
          x-soat-ref: policies
          type: string
    UpdateTriggerRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        target_type:
          type: string
          enum:
            - orchestration
            - agent
            - tool
            - eval
        target_id:
          type: string
        action:
          type: string
          nullable: true
        input:
          type: object
          nullable: true
        cron:
          type: string
          nullable: true
        event_pattern:
          type: string
          nullable: true
        active:
          type: boolean
        policy_id:
          x-soat-ref: policies
          type: string
          nullable: true
    FireTriggerRequest:
      type: object
      properties:
        input:
          type: object
          description: >-
            Fire-time input, shallow-merged over the trigger's static input. For `eval` targets it may carry
            `agent_version` and `baseline_run_id`.
    TriggerSecretResponse:
      type: object
      properties:
        secret:
          type: string
    TriggerFiring:
      type: object
      properties:
        id:
          type: string
        trigger_id:
          x-soat-ref: triggers
          type: string
        project_id:
          x-soat-ref: projects
          type: string
        source:
          type: string
          enum:
            - manual
            - webhook
            - schedule
            - event
        status:
          type: string
          enum:
            - pending
            - running
            - succeeded
            - failed
        input:
          type: object
          nullable: true
        result:
          type: object
          nullable: true
          description: '{ target_type, result_id, status, output } — output truncated'
        error:
          type: object
          nullable: true
          description: '{ code, message, meta }'
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TriggerFiringListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TriggerFiring'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    UsageComponent:
      type: object
      description: >
        One priced dimension of a usage event. Every meter type is expressed as components, so tokens and infra are
        uniform: an `llm_tokens` event has `input_tokens` / `output_tokens` (+ `cached_tokens`, and a non-billable
        `reasoning_tokens` detail), a `compute_execution` event has one `compute_second` component.
      properties:
        component:
          type: string
          description: The measured dimension (e.g. `input_tokens`, `compute_second`)
        quantity:
          type: number
          description: The measured amount, expressed in `unit`
        unit:
          type: string
          description: Unit `quantity` is measured in (`token`, `compute_second`, …)
        billable:
          type: boolean
          description: >
            Whether this component contributes to cost. Non-billable details (e.g. `reasoning_tokens`, a subset of
            `output_tokens`) are never priced and never double-counted into billable totals.
        unit_price:
          type: number
          nullable: true
          description: USD per `unit`, frozen at write time; null when unpriced
        cost_usd:
          type: number
          nullable: true
          description: quantity × unit_price, frozen at write time; null when unpriced
        price_id:
          type: string
          nullable: true
          description: Public ID of the price-book row that priced this component
    UsageEvent:
      type: object
      description: >
        One metered occurrence (a completed LLM call, a node execution, …). Attribution and total cost live here; the
        measured quantities live in `components`.
      properties:
        id:
          type: string
          description: Public ID of the usage event
          example: ue_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project the usage is attributed to
        orchestration_run_id:
          type: string
          nullable: true
          description: |
            Public ID of the orchestration run that initiated the occurrence. Null for standalone events.
        node_id:
          type: string
          nullable: true
          description: Orchestration node ID within the run, when applicable
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: Public ID of the agent that ran the generation
        generation_id:
          x-soat-ref: generations
          type: string
          nullable: true
          description: Public ID of the generation this usage was recorded for
        trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          description: Public ID of the trace this usage belongs to
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: >
            Public ID of the actor (end user) the metered occurrence was produced for, frozen at write time. Null when
            no end user is behind the work — orchestration runs, triggers, direct API generations.
        session_id:
          x-soat-ref: sessions
          type: string
          nullable: true
          description: >
            Public ID of the session the metered occurrence ran in, frozen at write time. Null for work not dispatched
            through a session.
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >
            Public ID of the AI provider instance billed — the target a model route picked for the turn, or the agent's
            pinned provider. Null if the provider was later deleted; the `provider`/`model` snapshot still records what
            was billed.
        trigger_id:
          x-soat-ref: triggers
          type: string
          nullable: true
          description: >
            Public ID of the trigger that initiated the generation (agent-target triggers). Null when not started by a
            trigger.
        action_id:
          type: string
          nullable: true
          description: >
            Caller-supplied logical action label, for rolling spend up per action. Null when the caller did not label
            the action.
        meter_type:
          type: string
          description: |
            What the event measures: `llm_tokens`, `compute_execution`, `api_request`, or `storage`.
        source:
          type: string
          nullable: true
          description: >
            What the spend was incurred for. `eval` is an eval run's item generations and `eval_judge` an `llm_judge`
            scorer's own completion, so verification spend is `source` in (`eval`, `eval_judge`) and the two are
            distinct so running a suite can be priced apart from grading it. Null for ordinary agent traffic.
          example: eval
        provider:
          type: string
          description: |
            As-billed SKU vendor slug, retained even if the AI provider is deleted. `soat` for platform meter types.
          example: openai
        model:
          type: string
          description: Model id, or the billable SKU for platform meter types
          example: gpt-4o
        cost_usd:
          type: number
          nullable: true
          description: >
            Total USD cost — the sum of the priced component costs, frozen at write time. Null when no component was
            priced.
        components:
          type: array
          items:
            $ref: '#/components/schemas/UsageComponent'
        created_at:
          type: string
          format: date-time
    UsageAggregateComponent:
      type: object
      description: >
        One measured dimension of a bucket, summed across its events. This is what makes the rollup uniform over meter
        types: the token fields only describe `llm_tokens`, so an infra meter's amount is read here.
      properties:
        component:
          type: string
          description: >
            The measured dimension: `input_tokens`, `cached_tokens`, `output_tokens`, `reasoning_tokens`,
            `compute_second`, `request`, `gb_day`, …
        unit:
          type: string
          description: |
            Unit `quantity` is measured in: `token`, `compute_second`, `request`, `gb_day`.
        quantity:
          type: number
          description: |
            Summed measured amount in `unit`. Non-integer for fractional measures (GB-days, compute seconds).
          example: 0.4
        cost_usd:
          type: number
          nullable: true
          description: >
            Sum of the priced component costs; null when no price row covered the component — the quantity is still
            captured, it does not mean the usage was free.
    UsageAggregateTotals:
      type: object
      description: |
        Summed token counts, measured component quantities, and cost for a bucket (or the grand total).
      properties:
        cost_usd:
          type: number
          nullable: true
          description: Sum of priced event costs in the bucket; null when nothing is priced
        input_tokens:
          type: integer
          description: Full prompt tokens (uncached input + cached), reconstructed from components
        output_tokens:
          type: integer
        cached_tokens:
          type: integer
        reasoning_tokens:
          type: integer
        components:
          type: array
          description: >
            Every component measured in the bucket, sorted by `component` then `unit`. The token fields above cover
            `llm_tokens` only, so this is where a `storage` / `api_request` / `compute_execution` bucket reports what it
            actually measured.
          items:
            $ref: '#/components/schemas/UsageAggregateComponent'
    UsageAggregate:
      type: object
      properties:
        project_id:
          x-soat-ref: projects
          type: string
        from:
          type: string
          format: date-time
          nullable: true
          description: Lower bound applied, echoed back; null when unbounded
        to:
          type: string
          format: date-time
          nullable: true
          description: Upper bound applied, echoed back; null when unbounded
        group_by:
          type: string
          enum:
            - model
            - ai_provider
            - agent
            - run
            - day
            - meter_type
            - actor
            - session
            - source
        meter_type:
          type: string
          nullable: true
          description: Meter-type filter applied, echoed back; null when unfiltered
        groups:
          type: array
          description: One entry per distinct value in the chosen dimension.
          items:
            allOf:
              - type: object
                properties:
                  key:
                    type: string
                    nullable: true
                    description: >
                      The bucket's value in the chosen dimension (a model id, meter type, AI provider / agent / run /
                      actor / session public id, or `YYYY-MM-DD` UTC day). Null when the dimension does not apply to an
                      event — under `actor`/`session` this is the bucket holding everything with no end user behind it.
                  ai_provider_id:
                    type: string
                    x-soat-ref: ai-providers
                    nullable: true
                    description: >
                      Under `group_by=model`, the provider that served the bucket's model; null on every other
                      dimension. A model id does not identify its provider on its own — one project can hold two
                      providers serving byte-identical model names — so the model dimension buckets on (model,
                      `ai_provider_id`) and two groups may repeat one `key` with different providers. The groups still
                      sum to `totals`.
              - $ref: '#/components/schemas/UsageAggregateTotals'
        totals:
          $ref: '#/components/schemas/UsageAggregateTotals'
    UsageThreshold:
      type: object
      description: >
        A per-project alert rule on windowed usage. When the project's `metric` over `window` crosses `threshold`, a
        `usage.threshold_crossed` webhook fires. Fire state is enforced with once-per-window (calendar) / 10% re-arm
        (rolling) hysteresis.
      properties:
        id:
          type: string
          description: Public ID of the threshold
          example: uthr_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        metric:
          type: string
          enum:
            - cost_usd
            - tokens
          description: |
            What is measured: `cost_usd` (across all meter types) or `tokens` (input + output + cached).
        window:
          type: string
          enum:
            - calendar_month
            - rolling_24h
          description: |
            The evaluation window: the current UTC calendar month, or the trailing 24 hours.
        threshold:
          type: number
          description: The value the windowed aggregate must cross to fire
        last_fired_at:
          type: string
          format: date-time
          nullable: true
          description: When the threshold last fired; null until first fire
        fired_window_key:
          type: string
          nullable: true
          description: >
            The `YYYY-MM` window key of the last fire (calendar_month hysteresis); null for rolling_24h and before the
            first fire.
        created_at:
          type: string
          format: date-time
    CreateUsageThresholdRequest:
      type: object
      required:
        - project_id
        - metric
        - window
        - threshold
      properties:
        project_id:
          x-soat-ref: projects
          type: string
        metric:
          type: string
          enum:
            - cost_usd
            - tokens
        window:
          type: string
          enum:
            - calendar_month
            - rolling_24h
        threshold:
          type: number
          description: Must be greater than 0
    UsageReceipt:
      type: object
      properties:
        generation_id:
          description: |
            Present on a per-generation receipt; absent on a per-run receipt.
          x-soat-ref: generations
          type: string
        orchestration_run_id:
          description: |
            Present on a per-run receipt (summed across the run's meters); absent on a per-generation receipt.
          x-soat-ref: orchestration-runs
          type: string
        currency:
          type: string
          example: USD
        line_items:
          type: array
          description: >
            One line per usage event — for a generation receipt, the events on that generation; for a run receipt, every
            event across the run.
          items:
            type: object
            properties:
              event_id:
                type: string
              meter_type:
                type: string
              provider:
                type: string
              model:
                type: string
              node_id:
                type: string
                nullable: true
                description: >
                  Orchestration node that produced the event. On a run receipt every line carries it — an `agent` node's
                  `llm_tokens` line and the `compute_execution` line of each node execution alike — so grouping the
                  lines by `node_id` gives the per-node cost the receipt total alone hides. A retried node contributes
                  one line per attempt, all under the same `node_id` — the event records no attempt number — which is
                  the intended reading for spend: a retry is real money. Null when no node produced the event (a
                  standalone generation, a run-level meter).
              cost_usd:
                type: number
                nullable: true
              components:
                type: array
                items:
                  $ref: '#/components/schemas/UsageComponent'
        by_meter_type:
          type: array
          description: >
            Per-meter-type cost rollup — the "tokens + infra" split. A single-type receipt has one entry whose cost
            equals the receipt total.
          items:
            type: object
            properties:
              meter_type:
                type: string
              cost_usd:
                type: number
                nullable: true
        total_input_tokens:
          type: integer
          description: Full prompt tokens (uncached input + cached), reconstructed from components
        total_output_tokens:
          type: integer
        total_cached_tokens:
          type: integer
        total_reasoning_tokens:
          type: integer
        total_cost_usd:
          type: number
          nullable: true
          description: Sum of priced component costs; null when nothing is priced
    Price:
      type: object
      description: >
        A versioned unit price for one billable component of a SKU. Cost is uniform across meter types — quantity ×
        unit_price.
      properties:
        id:
          type: string
          description: Public ID of the price row
          example: price_V1StGXR8Z5jdHi6B
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >
            Null for a global default price. Set when this row is a per-provider override for a specific AI provider
            instance.
        meter_type:
          type: string
          description: Meter type this SKU belongs to (`llm_tokens`, `compute_execution`, …)
        provider:
          type: string
          description: SKU vendor slug (`openai`, or `soat` for platform SKUs)
          example: openai
        model:
          type: string
          description: Model id, or the billable SKU for platform meter types
          example: gpt-4o
        component:
          type: string
          description: The component this row prices (`input_tokens`, `compute_second`, …)
        unit:
          type: string
          description: Unit `unit_price` is denominated in (`token`, `compute_second`, …)
        unit_price:
          type: number
          description: USD per `unit` (for token components, USD per token)
        effective_from:
          type: string
          format: date-time
          description: The row with the latest effective_from <= now() prices a call
        created_at:
          type: string
          format: date-time
    PriceBookResponse:
      type: object
      properties:
        prices:
          type: array
          items:
            $ref: '#/components/schemas/Price'
    UpsertPricesRequest:
      type: object
      required:
        - prices
      properties:
        prices:
          type: array
          items:
            type: object
            required:
              - provider
              - model
              - component
              - unit
              - unit_price
              - effective_from
            description: >
              A price row for one component of a SKU. For LLM SKUs supply the `input_tokens` / `output_tokens` (and
              optionally `cached_tokens`) components with unit `token`; for platform SKUs supply the platform component
              (e.g. `compute_second`).
            properties:
              ai_provider_id:
                x-soat-ref: ai-providers
                type: string
                nullable: true
                description: |
                  Omit for a global default price; set to a provider public ID to record a per-provider override.
              meter_type:
                type: string
                default: llm_tokens
                description: |
                  Defaults to `llm_tokens`. Set to a platform meter type (e.g. `compute_execution`) for a platform SKU.
              provider:
                type: string
              model:
                type: string
              component:
                type: string
                description: The component this row prices (`input_tokens`, `compute_second`, …)
              unit:
                type: string
                description: Unit `unit_price` is denominated in (`token`, `compute_second`, …)
              unit_price:
                type: number
                description: USD per `unit`
              effective_from:
                type: string
                format: date-time
                description: Must be in the future; past prices are immutable
    UserRecord:
      type: object
      properties:
        id:
          type: string
          description: Public user ID (user_ prefix)
          example: user_V1StGXR8Z5jdHi6B
        username:
          type: string
          example: johndoe
        role:
          type: string
          enum:
            - admin
            - user
          example: user
        created_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2024-01-01T00:00:00.000Z'
    LoginResponse:
      type: object
      properties:
        id:
          type: string
          description: Public user ID (user_ prefix)
          example: user_V1StGXR8Z5jdHi6B
        username:
          type: string
          example: admin
        role:
          type: string
          enum:
            - admin
            - user
          example: admin
        token:
          type: string
          description: JWT bearer token
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    Webhook:
      type: object
      properties:
        id:
          type: string
        project_id:
          x-soat-ref: projects
          type: string
        policy_id:
          x-soat-ref: policies
          type: string
          nullable: true
        name:
          type: string
        description:
          type: string
          nullable: true
        url:
          type: string
        events:
          type: array
          items:
            type: string
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    WebhookWithSecret:
      allOf:
        - $ref: '#/components/schemas/Webhook'
        - type: object
          properties:
            secret:
              type: string
    CreateWebhookRequest:
      type: object
      required:
        - name
        - url
        - events
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: >-
            Public ID of the project. Optional when authenticating with a project-scoped API key, which defaults to the
            key's project; required otherwise.
        name:
          type: string
        description:
          type: string
        url:
          type: string
        events:
          type: array
          items:
            type: string
        policy_id:
          x-soat-ref: policies
          type: string
    UpdateWebhookRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        url:
          type: string
        events:
          type: array
          items:
            type: string
        active:
          type: boolean
        policy_id:
          x-soat-ref: policies
          type: string
          nullable: true
    WebhookSecretResponse:
      type: object
      properties:
        secret:
          type: string
    Delivery:
      type: object
      properties:
        id:
          type: string
        webhook_id:
          x-soat-ref: webhooks
          type: string
          description: Public ID of the webhook this delivery belongs to
        event_type:
          type: string
        payload:
          type: object
        status:
          type: string
          enum:
            - pending
            - success
            - failed
        status_code:
          type: integer
          nullable: true
        attempts:
          type: integer
        last_attempt_at:
          type: string
          format: date-time
          nullable: true
        next_attempt_at:
          type: string
          format: date-time
          nullable: true
          description: |
            When the delivery becomes eligible for its next attempt. Set while
            the delivery is pending and a retry is still owed; null once it has
            succeeded or exhausted its attempts.
        response_body:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DeliveryListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Delivery'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    WorkflowState:
      type: object
      description: >-
        A named state. Exactly one state must be `initial: true`; any number may be `terminal: true`. A `kind: human`
        state never dispatches — the task parks until a transition fires. `on_enter` (§5) dispatches exactly one of an
        agent generation (`kind: agent`, `agent_id`), an orchestration run (`kind: orchestration`, `orchestration_id`)
        or a tool call (`kind: tool`, `tool_id`, optional `operation_id`) on entry, optionally under a `retry` policy
        (`max_attempts` 1-10, `backoff_seconds`, `backoff_multiplier`) that re-runs execution failures before
        `on_failure` applies. A `tool` dispatch settles within the dispatch and is adjudicated by the same guardrails as
        an orchestration `tool` node; for anything that must wait (a delay, a poll, a multi-step pipeline, or an
        approval-gated tool), dispatch an orchestration instead.
      additionalProperties: true
      required:
        - name
      properties:
        name:
          type: string
        initial:
          type: boolean
        terminal:
          type: boolean
        kind:
          type: string
          description: >-
            `human` marks a human-in-the-loop parking state: the state never dispatches (declaring `on_enter` on it is
            rejected at validation) and the task parks until a principal fires a transition.
        stalled_after:
          type: integer
          nullable: true
          description: >-
            Seconds a task may sit in this state before the stall sweeper emits a `tasks.stalled` event (once per stall
            episode, re-armed on the next transition). Must be a positive integer when set. Omit or null to never stall.
            The event does not move the task — route on it with a webhook/trigger.
        on_enter:
          type: object
          nullable: true
          additionalProperties: true
    WorkflowTransition:
      type: object
      description: >-
        A named, directional move. `from` is a list of source states; `to` is one target state. `guard` is a JSON Logic
        expression over `{task, transition, principal}` that must be truthy for the move to apply.
      additionalProperties: true
      required:
        - name
        - from
        - to
      properties:
        name:
          type: string
        from:
          type: array
          items:
            type: string
        to:
          type: string
        guard:
          type: object
          nullable: true
        requires_approval:
          type: boolean
          description: >-
            Gate the transition behind a human approval. When `true`, firing the transition (by anyone other than the
            approval resolution itself) parks a pending `ApprovalItem` instead of moving the task; the task exposes
            `pending_transition` until the item resolves. Approval fires the transition as the `approval` principal (its
            guard re-evaluated at resolution time); rejection or expiry clears the gate and appends a history note.
    Workflow:
      type: object
      properties:
        id:
          type: string
        project_id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        version:
          type: integer
          description: >
            Incremented on every write that changes the state machine; prior versions are archived. A task pins the
            version it entered on, so these fields are a draft for tasks created from now on rather than a live rewrite
            of the ones already in flight.
          example: 1
        states:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowState'
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowTransition'
        payload_schema:
          type: object
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateWorkflowRequest:
      type: object
      required:
        - name
        - states
        - transitions
      properties:
        project_id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        states:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowState'
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowTransition'
        payload_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: Optional tag for the version this create archives, e.g. `initial`.
          example: initial
    UpdateWorkflowRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        states:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowState'
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowTransition'
        payload_schema:
          type: object
          nullable: true
        version_label:
          type: string
          description: >-
            Optional tag for the version this write archives, e.g. `pre-rewire`. Ignored when the write changes no
            definition field, since no version is archived.
          example: pre-rewire
    WorkflowVersion:
      type: object
      description: An immutable archive of a workflow's state machine at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: wfl_ver_V1StGXR8Z5jdHi6B
        workflow_id:
          x-soat-ref: workflows
          type: string
          description: Public ID of the workflow this version belongs to
          example: wfl_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The workflow's versioned surface as it stood at this version: `states`, `transitions` and `payload_schema`.
            Name and description are metadata — bumping the version when one of them changes would make two version
            numbers denote the same state machine, which is exactly what a task cites.


            Deliberately open rather than a fixed schema: an archive written by an earlier release of SOAT reflects the
            workflow surface **of its own time**, so it may carry fields the current API no longer documents.
          properties:
            states:
              type: array
              items:
                $ref: '#/components/schemas/WorkflowState'
            transitions:
              type: array
              items:
                $ref: '#/components/schemas/WorkflowTransition'
            payload_schema:
              type: object
              nullable: true
        label:
          type: string
          nullable: true
          description: >-
            Optional human tag for this version, e.g. `pre-rewire`. Set from the `version_label` field of a write, the
            `label` field of a restore, or generated for one.
          example: restored from v2
        created_by:
          x-soat-ref: users
          type: string
          nullable: true
          description: Public ID of the user whose action produced this version. Null for writes with no request user behind them.
        created_at:
          type: string
          format: date-time
    RestoreWorkflowVersionRequest:
      type: object
      properties:
        label:
          type: string
          description: Optional tag for the new version the restore archives. Defaults to `restored from vN`.
          example: rollback to pre-rewire
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    approval_id:
      name: approval_id
      in: path
      required: true
      description: Approval item ID
      schema:
        type: string
        example: apr_V1StGXR8Z5jdHi6B
    chain_id:
      name: chain_id
      in: path
      required: true
      description: Continuation chain ID
      schema:
        type: string
        example: chain_V1StGXR8Z5jdHi6B
    exception_id:
      name: exception_id
      in: path
      required: true
      description: Exception item ID
      schema:
        type: string
        example: exc_V1StGXR8Z5jdHi6B
    orchestration_id:
      in: path
      name: orchestration_id
      required: true
      schema:
        type: string
      description: Public ID of the orchestration (orch_...)
    orchestration_run_id:
      in: path
      name: orchestration_run_id
      required: true
      schema:
        type: string
      description: Public ID of the run (run_...)
    SessionId:
      name: session_id
      in: path
      required: true
      description: Session public ID
      schema:
        type: string
        example: sess_V1StGXR8Z5jdHi6B
  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'
x-error-codes:
  ACTIVITY_INVALID_CURSOR:
    http_status: 400
    description: The activity feed cursor is malformed or does not decode to a valid (created_at, id) position.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#activity_invalid_cursor
  ACTOR_HAS_MESSAGES:
    http_status: 409
    description: The actor has linked session messages and cannot be deleted.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#actor_has_messages
  ACTOR_NOT_FOUND:
    http_status: 400
    description: A referenced actor does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#actor_not_found
  AGENT_AND_CHAT_EXCLUSIVE:
    http_status: 400
    description: An actor cannot have both an agent_id and a chat_id assigned simultaneously. Set one or the other, not both.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#agent_and_chat_exclusive
  AGENT_HAS_DEPENDENTS:
    http_status: 409
    description: >-
      The agent is referenced by one or more generations or traces and cannot be deleted. Delete the dependent records
      first.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#agent_has_dependents
  AGENT_NOT_FOUND:
    http_status: 400
    description: A referenced agent does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#agent_not_found
  AI_PROVIDER_ERROR:
    http_status: 502
    description: >-
      The upstream AI provider returned an error (e.g. insufficient credits, rate limit, or the provider is
      unreachable).
    resolution: >-
      The upstream model provider failed, not this server. Check the provider credential and the account behind it
      (credits, rate limits, model access), then retry with backoff.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ai_provider_error
  AI_PROVIDER_HAS_DEPENDENTS:
    http_status: 409
    description: >-
      The AI provider still has dependents. Live references (chats, agents, model-route targets) always block deletion
      and must be deleted or repointed first. Soft dependents (price overrides, usage records) block only until
      force=true, which drops the overrides and unlinks usage history. The error meta reports the counts, a sample of
      offending IDs, and a `forcible` flag.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ai_provider_has_dependents
  AI_PROVIDER_MISCONFIGURED:
    http_status: 400
    description: >-
      The AI provider record is missing configuration the provider type requires — for example a `vertex` provider with
      no Google Cloud project in `config.project` and no service-account key file linked as its secret.
    resolution: >-
      The provider record is missing something it needs to make a call — usually the API key secret or the base URL.
      Update the AI provider and retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ai_provider_misconfigured
  AI_PROVIDER_NOT_FOUND:
    http_status: 400
    description: A referenced AI provider does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ai_provider_not_found
  API_KEY_PROJECT_SCOPE:
    http_status: 403
    description: >-
      The credential (API key or OAuth token) is bound to one project and the request targets a different one. This
      project binding is a hard boundary that the owner’s admin role does not lift — admin only relaxes the project
      create/delete gate, never the per-credential project scope for resource operations. Mint a key scoped to the
      target project (or an unscoped key) to operate there. The error meta reports `scoped_project` and
      `requested_project`.
    resolution: >-
      This API key is scoped to a single project and the request targets another one (or none). Omit `project_id` to use
      the key's own project, or use an unscoped credential.
    docs_url: https://soat.ttoss.dev/docs/error-codes#api_key_project_scope
  APPROVAL_ALREADY_RESOLVED:
    http_status: 409
    description: The approval item has already been resolved (approved, rejected, or expired) and cannot be resolved again.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#approval_already_resolved
  APPROVAL_EXPIRED:
    http_status: 409
    description: >-
      The approval item has expired and can never execute. Its supporting evidence is stale; a fresh proposal is
      required.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#approval_expired
  APPROVAL_INVALID_EDIT:
    http_status: 400
    description: The edited arguments supplied on edit-then-approve must be a JSON object.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#approval_invalid_edit
  APPROVAL_NOT_FOUND:
    http_status: 404
    description: The approval item does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#approval_not_found
  APPROVAL_REASON_REQUIRED:
    http_status: 400
    description: A reason is required when rejecting an approval item.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#approval_reason_required
  BOOTSTRAP_ALREADY_COMPLETED:
    http_status: 409
    description: The instance already has at least one user, so the one-time bootstrap of the first admin cannot run again.
    resolution: >-
      The first admin already exists, so bootstrap is closed for good. Sign in instead, and create further users with
      `POST /api/v1/users`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#bootstrap_already_completed
  CHAIN_NOT_FOUND:
    http_status: 404
    description: The continuation chain does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#chain_not_found
  CHAT_NOT_FOUND:
    http_status: 400
    description: A referenced chat does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#chat_not_found
  CONVERTER_FAILED:
    http_status: 422
    description: >-
      The ingestion converter (tool or agent) failed to run, an agent converter returned an async deferral (unsupported
      — agent converters are always awaited inline), or a tool converter returned an async deferral during synchronous
      ingestion (`?wait=true`, which cannot wait for a callback).
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#converter_failed
  CONVERTER_OUTPUT_INVALID:
    http_status: 422
    description: >-
      The ingestion converter returned an unrecognized output shape. Expected a string, `{ pages: [{ text, page_number
      }] }`, or `{ status: "pending" }`.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#converter_output_invalid
  EMBEDDING_NOT_CONFIGURED:
    http_status: 503
    description: The embedding service is not configured. Set EMBEDDING_PROVIDER and EMBEDDING_MODEL environment variables.
    resolution: >-
      Knowledge search needs an embedding model. Configure one on the project or the AI provider before ingesting or
      searching.
    docs_url: https://soat.ttoss.dev/docs/error-codes#embedding_not_configured
  EXCEPTION_ALREADY_RESOLVED:
    http_status: 409
    description: The exception item has already been resolved and cannot be acknowledged or resolved again.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#exception_already_resolved
  EXCEPTION_NOT_FOUND:
    http_status: 404
    description: The exception item does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#exception_not_found
  FILE_ALREADY_INGESTED:
    http_status: 409
    description: >-
      The file already backs a Document (a file can only be ingested once). Use POST /documents/{document_id}/ingest to
      re-process the existing document, or upload a new copy of the file to ingest it under a different path/strategy.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_already_ingested
  FILE_DOWNLOAD_URL_NOT_CONFIGURED:
    http_status: 500
    description: >-
      An ingestion rule with file_delivery: download_url requires SOAT_BASE_URL to be set — the URL is fetched by an
      external converter that cannot resolve a relative or localhost address.
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_download_url_not_configured
  FILE_HAS_DEPENDENTS:
    http_status: 409
    description: The file is referenced by one or more records and cannot be deleted. Delete the dependent records first.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_has_dependents
  FILE_NOT_FOUND:
    http_status: 400
    description: The referenced file does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_not_found
  FILE_PARSE_FAILED:
    http_status: 400
    description: The referenced file contains no extractable text.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_parse_failed
  FILE_STORAGE_MISCONFIGURED:
    http_status: 500
    description: >-
      The file storage backend is not configured correctly — an unknown FILES_STORAGE_PROVIDER, an unsupported storage
      type, or a missing required variable (e.g. FILES_STORAGE_DIR for local, FILES_S3_BUCKET for s3).
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_storage_misconfigured
  FILE_TOO_LARGE_FOR_SYNC:
    http_status: 413
    description: >-
      The file is too large to ingest synchronously. Retry without `?wait=true` to run ingestion in the background and
      poll the document status.
    resolution: >-
      The file is too large to parse inside the request. Call the same operation without `wait=true` and poll the handle
      it returns.
    docs_url: https://soat.ttoss.dev/docs/error-codes#file_too_large_for_sync
  FORBIDDEN:
    http_status: 403
    description: The authenticated user does not have permission to perform this action.
    resolution: >-
      The credential authenticated but is not allowed this action. Attach an IAM policy granting the action to the
      caller, or use a credential that has it.
    docs_url: https://soat.ttoss.dev/docs/error-codes#forbidden
  FORCED_TOOL_CHOICE_CANNOT_STOP:
    http_status: 400
    description: >-
      The agent forces tool use on every step (`tool_choice: "required"`, or the `{ type: "tool" }` object form) but
      declares no `has_tool_call` stop condition, so no turn it runs could ever end by answering — only by exhausting
      `max_steps`. Declare the terminal condition, or drop the forcing to `"auto"` and force a specific step with
      `step_rules` instead.
    resolution: >-
      Add `{ "type": "has_tool_call", "tool_name": "<your done tool>" }` to `stop_conditions`, or set `tool_choice` to
      `"auto"` and force the step you actually care about with `step_rules`. `max_chain_generations` does not satisfy
      this — it bounds a chain, it never ends a turn.
    docs_url: https://soat.ttoss.dev/docs/error-codes#forced_tool_choice_cannot_stop
  FORMATION_DELETE_FAILED:
    http_status: 409
    description: >-
      One or more of the formation's resources could not be deleted, so the stack is left in `delete_failed`.
      `meta.failures` names each blocking resource (its logical id, type, and the underlying error) — the common case is
      a resource the platform refuses to delete on its own, such as an agent that has generation or trace history.
      Resolve those, then delete the formation again.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#formation_delete_failed
  FORMATION_HANDLER_FAILED:
    http_status: 502
    description: >-
      A resource type registered by the deployment operator delegates its lifecycle to an external HTTP handler, and
      that handler did not answer successfully — it returned a non-2xx status, timed out, was unreachable, or replied
      with a body the protocol does not allow (a create with no `physical_resource_id`, for instance).
      `meta.resource_type` and `meta.request_type` name the type and the operation, and the handler's own message is
      relayed when it sent one. This is an operator-side failure: the template is not at fault and re-deploying it
      unchanged will fail the same way until the handler is fixed.
    resolution: >-
      A resource type this deployment registered delegates to an external handler, and that handler did not answer. The
      template is not at fault, so re-deploying it unchanged will fail identically — the deployment operator has to fix
      the handler (`meta.resource_type` names the type, `meta.request_type` the operation it failed on).
    docs_url: https://soat.ttoss.dev/docs/error-codes#formation_handler_failed
  FORMATION_INVALID_METADATA:
    http_status: 400
    description: >-
      The formation-level `metadata` field contains a `sub`/`param`/`ref`/`ref_attr` substitution expression. That field
      is a static annotation bag and is never resolved; put deploy-time substitutions in the template top-level
      `metadata` block instead.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#formation_invalid_metadata
  FORMATION_MISSING_PARAMETERS:
    http_status: 400
    description: >-
      The formation template declares one or more required parameters (no default, not satisfied by use_previous_value)
      that were not supplied or were provided as an empty string.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#formation_missing_parameters
  FORMATION_REPLACE_CLEANUP_FAILED:
    http_status: 500
    description: >-
      A deploy replaced a resource — the handler answered a new `physical_resource_id` — and the superseded resource
      could not be deleted, so it is still live while the formation's ledger already points at the replacement. The
      deploy itself succeeded: the desired state is realised, and this is carried on the formation (and on its
      operation) rather than thrown, so it never appears as a response status. `meta.failures` names each un-deleted
      resource as `{ logical_id, resource_type, physical_resource_id, error }`. Each one stays on the formation as
      pending cleanup and is retried on the next deploy and on teardown; resolve whatever refused the delete and deploy
      again.
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#formation_replace_cleanup_failed
  GENERATION_ALREADY_IN_PROGRESS:
    http_status: 409
    description: A generation is already in progress for this session. Wait for it to complete before starting a new one.
    resolution: A generation is already running for this target. Poll it to completion, or cancel it, before starting another.
    docs_url: https://soat.ttoss.dev/docs/error-codes#generation_already_in_progress
  GENERATION_CONTENT_UNAVAILABLE:
    http_status: 409
    description: >-
      The generation's content was never stored (the agent or project runs with trace_content_mode 'none') or has since
      been purged, so the turn cannot be replayed or curated.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#generation_content_unavailable
  GENERATION_FAILED:
    http_status: 500
    description: An unexpected error occurred during agent generation. Check the trace for details.
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#generation_failed
  GENERATION_NOT_COMPLETED:
    http_status: 409
    description: >-
      The generation has not completed, so it has no finished turn to promote. Only a completed generation can be
      curated into a dataset item.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#generation_not_completed
  GENERATION_NOT_FOUND:
    http_status: 404
    description: The generation does not exist or is not in a pending state for tool output submission.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#generation_not_found
  GUARDRAIL_HAS_REFERENCES:
    http_status: 409
    description: >-
      The guardrail is still attached to one or more tools, agents, or projects (`guardrail_ids`) and cannot be deleted.
      Detach every reference first (requires guardrails:DetachGuardrail). The error meta lists the referencing resources
      by scope.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#guardrail_has_references
  GUARDRAIL_NOT_FOUND:
    http_status: 400
    description: A guardrail referenced by a `guardrail_ids` attachment does not exist in the project.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#guardrail_not_found
  HOOK_INVALID_JSON:
    http_status: 400
    description: The inbound hook request body is not valid JSON.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#hook_invalid_json
  HOOK_PAYLOAD_TOO_LARGE:
    http_status: 413
    description: The inbound hook request body exceeds the 1 MiB limit.
    resolution: The payload exceeds the accepted size. Send less in one request, or use the upload flow for large bodies.
    docs_url: https://soat.ttoss.dev/docs/error-codes#hook_payload_too_large
  INGESTION_CALLBACK_CONFLICT:
    http_status: 409
    description: >-
      The document is no longer awaiting this conversion attempt — it already completed, timed out, or was superseded by
      a re-ingest.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ingestion_callback_conflict
  INGESTION_CALLBACK_INVALID_TOKEN:
    http_status: 401
    description: The ingestion-callback token is missing, malformed, expired, or does not match the target document.
    resolution: 'Authenticate the request: send a valid bearer token or project API key in the `Authorization` header.'
    docs_url: https://soat.ttoss.dev/docs/error-codes#ingestion_callback_invalid_token
  INGESTION_RULE_GLOB_CONFLICT:
    http_status: 409
    description: An ingestion rule for this content_type_glob already exists in the project.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ingestion_rule_glob_conflict
  INGESTION_RULE_VALIDATION_FAILED:
    http_status: 400
    description: >-
      The ingestion rule configuration is invalid (e.g. tool_id and agent_id are both set or both missing, the converter
      tool is a client tool, a soat/mcp converter tool is missing an action, content_type_glob is not a valid MIME type
      glob, or preset_parameters contains the reserved key "file" or "callback").
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#ingestion_rule_validation_failed
  INTERNAL_ERROR:
    http_status: 500
    description: >-
      An unhandled server-side failure. The message is always the constant "Internal Server Error" — the underlying
      exception is logged, never returned, so this error carries no detail a caller can act on beyond retrying.
    resolution: >-
      Retry with backoff. The underlying exception is logged server-side and deliberately never forwarded, so the
      response carries nothing further to act on.
    docs_url: https://soat.ttoss.dev/docs/error-codes#internal_error
  INVALID_CRON_EXPRESSION:
    http_status: 400
    description: The cron expression is invalid. Triggers require a strict 5-field cron expression evaluated in UTC.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#invalid_cron_expression
  INVALID_EVENT_PATTERN:
    http_status: 400
    description: >-
      The event pattern is invalid. An event trigger subscribes with '*', 'prefix.*', or an exact event name, and a
      pattern in a platform namespace must match a registered event.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#invalid_event_pattern
  INVALID_OUTPUT_SCHEMA:
    http_status: 400
    description: The agent output_schema is invalid — it must be a JSON Schema object.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#invalid_output_schema
  INVALID_TEMPLATE_TOKEN:
    http_status: 400
    description: >-
      A {{...}} double-curly token was found that is not a {{secret:sec_...}} or {{context:<key>}} reference, or a
      {{context:<key>}} token was found outside execute.headers / mcp.headers. Double curly braces are reserved for
      those two reference kinds; use single braces ({param}) for URL path parameters.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#invalid_template_token
  INVALID_TOOL_CONTEXT_KEY:
    http_status: 400
    description: >-
      A tool_context key cannot be used as an HTTP header name. Keys are forwarded verbatim as <prefix><key> headers,
      where the prefix is the deployment's TOOL_CONTEXT_HEADER_PREFIX (X-Soat-Context- by default), so a key may only
      contain letters, digits and the characters !#$%&'*+-.^_`|~, and two keys must not map to the same header name
      (header names are case-insensitive).
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#invalid_tool_context_key
  MISSING_TOOL_CONTEXT_KEY:
    http_status: 400
    description: >-
      A tool header references a {{context:<key>}} token whose key is not present in the tool_context for this call. The
      call is failed rather than sending the header with an empty value.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#missing_tool_context_key
  MODEL_LISTING_FAILED:
    http_status: 502
    description: >-
      The provider rejected the model listing request, or answered it with something other than JSON. The provider's own
      status and message are carried in the error message.
    resolution: An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.
    docs_url: https://soat.ttoss.dev/docs/error-codes#model_listing_failed
  MODEL_LISTING_UNSUPPORTED:
    http_status: 400
    description: >-
      The AI provider type cannot enumerate the models it can run. Azure lists deployments an operator named rather than
      models, and Ollama lists whatever was pulled onto that host, so neither answers the question.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#model_listing_unsupported
  MODEL_ROUTE_HAS_DEPENDENTS:
    http_status: 409
    description: >-
      The model route is referenced by one or more agents, or is a project's default_model_route_id, and cannot be
      deleted. Repoint or delete the referencing consumers first; the error meta reports the counts and a sample of
      agent IDs.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#model_route_has_dependents
  MODEL_ROUTE_NOT_FOUND:
    http_status: 400
    description: A referenced model route does not exist in the project.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#model_route_not_found
  NAME_CONFLICT:
    http_status: 409
    description: A resource with this name already exists in the project. Use a different name.
    resolution: >-
      A resource with this name already exists in the project. Pick another name, or update the existing resource
      instead of creating a second one.
    docs_url: https://soat.ttoss.dev/docs/error-codes#name_conflict
  NOT_IMPLEMENTED:
    http_status: 501
    description: The request is well-formed but names an option this API version does not implement.
    resolution: >-
      The operation exists in the contract but is not implemented on this deployment. Retrying will not change the
      outcome.
    docs_url: https://soat.ttoss.dev/docs/error-codes#not_implemented
  NO_ACTIVE_RELEASE:
    http_status: 409
    description: The agent has no active release to promote or abort. Set one with PUT /agents/{agent_id}/release first.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#no_active_release
  ORCHESTRATION_CYCLE_DETECTED:
    http_status: 422
    description: The orchestration graph contains a cycle. Cycles are not supported.
    resolution: >-
      The graph has a cycle, so no topological order exists. Break the cycle named in `meta` — a loop belongs in a
      workflow, not a DAG.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_cycle_detected
  ORCHESTRATION_DISPATCH_FAILED:
    http_status: 422
    description: >-
      A workflow on_enter dispatch's orchestration run settled in a non-success terminal status (failed, cancelled, or
      expired).
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_dispatch_failed
  ORCHESTRATION_HUMAN_NODE_MISMATCH:
    http_status: 400
    description: The specified node is not the active human node waiting for input.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_human_node_mismatch
  ORCHESTRATION_MAX_ITERATIONS_EXCEEDED:
    http_status: 422
    description: The orchestration run exceeded the maximum number of iterations.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_max_iterations_exceeded
  ORCHESTRATION_NESTED_RUN_FAILED:
    http_status: 422
    description: >-
      A `loop` / `sub_orchestration` child run settled in a non-success terminal status (failed, cancelled, or expired)
      carrying no code of its own, so the node that started it failed too. A child that carries a code fails its parent
      under that code instead, so the cause reaches the run a caller reads (#1185). Matches
      ORCHESTRATION_DISPATCH_FAILED, the same rule for a workflow on_enter dispatch's run.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_nested_run_failed
  ORCHESTRATION_NODE_FAILED:
    http_status: 422
    description: A node in the orchestration run failed to execute.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_node_failed
  ORCHESTRATION_NOT_FOUND:
    http_status: 404
    description: The orchestration does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_not_found
  ORCHESTRATION_POLL_EXHAUSTED:
    http_status: 422
    description: >-
      A poll node reached its attempt limit without its exit condition becoming true (only raised when fail_on_timeout
      is set).
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_poll_exhausted
  ORCHESTRATION_RUN_DEPTH_LIMIT:
    http_status: 409
    description: >-
      Starting the next `loop` / `sub_orchestration` child run would nest past the depth bound, so it was refused. This
      bounds a cycle no intra-graph validator can see — a graph whose `sub_orchestration` node names itself, directly or
      through a cycle of two graphs — which the orchestration cycle validator (intra-graph, and excluding loop nodes
      deliberately) cannot detect. The bound is the smaller of MAX_ORCHESTRATION_RUN_DEPTH (default 10) and the
      project's `max_run_depth`; the error meta names the depth reached, the limit, and which of the two set it (#1185).
    resolution: >-
      Nesting is recursing: follow `parent_orchestration_run_id` up from the failed run to find the `sub_orchestration`
      (or `loop`) node that names a graph already in the chain, and repoint it. If the composition is legitimately this
      deep, raise the project bound with `PATCH /api/v1/projects/{project_id}` (`max_run_depth`) — `meta.limit_source`
      says whether the project or the deployment set the number that refused it.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_run_depth_limit
  ORCHESTRATION_RUN_NOT_AWAITING_INPUT:
    http_status: 409
    description: The orchestration run is not awaiting input.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_run_not_awaiting_input
  ORCHESTRATION_RUN_NOT_CANCELLABLE:
    http_status: 409
    description: The orchestration run is already in a terminal state and cannot be cancelled.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_run_not_cancellable
  ORCHESTRATION_RUN_NOT_FOUND:
    http_status: 404
    description: The orchestration run does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_run_not_found
  ORCHESTRATION_VALIDATION_FAILED:
    http_status: 400
    description: >-
      The orchestration graph is invalid (e.g. a dangling edge, a node missing a required field, a cycle, or an
      input_mapping that references a state key no upstream node writes).
    resolution: >-
      The orchestration graph is not runnable as declared. `meta` names the failing node or edge; `POST
      /api/v1/orchestrations/{orchestration_id}/validate` reports every problem at once.
    docs_url: https://soat.ttoss.dev/docs/error-codes#orchestration_validation_failed
  OUTPUT_SCHEMA_STREAMING_UNSUPPORTED:
    http_status: 400
    description: >-
      Streaming generation does not support output_schema. Set stream to false, or remove output_schema from the
      agent/request.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#output_schema_streaming_unsupported
  OUTPUT_SCHEMA_VALIDATION_FAILED:
    http_status: 502
    description: >-
      The model returned an object that does not satisfy the agent output_schema (or was not valid JSON at all).
      Upstream-caused like AI_PROVIDER_ERROR: the request was well-formed, the model output was not.
    resolution: An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.
    docs_url: https://soat.ttoss.dev/docs/error-codes#output_schema_validation_failed
  PDF_PARSE_FAILED:
    http_status: 400
    description: The uploaded file could not be parsed as a PDF. Ensure the file is a valid PDF document.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#pdf_parse_failed
  PIPELINE_DEPTH_EXCEEDED:
    http_status: 422
    description: The pipeline tool exceeded the maximum nested execution depth (pipelines calling pipelines).
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#pipeline_depth_exceeded
  PIPELINE_INVALID_STEP:
    http_status: 400
    description: >-
      The pipeline tool configuration is invalid (e.g. no steps, a duplicate or malformed step id, a missing tool
      reference, a forward reference to a later step, or a step that targets a client tool).
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#pipeline_invalid_step
  PIPELINE_STEP_FAILED:
    http_status: 422
    description: A step in the pipeline tool failed to execute.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#pipeline_step_failed
  POLICY_NOT_FOUND:
    http_status: 400
    description: A referenced policy does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#policy_not_found
  PROJECT_DEFAULT_ROUTE_INHERITED:
    http_status: 409
    description: >-
      A project's default_model_route_id cannot be cleared while consumers that bind neither an AI provider nor a model
      route inherit it — clearing it would leave them with no resolvable model. Repointing the default to another route
      is always allowed; the error meta reports the count and a sample of inheriting resource IDs.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#project_default_route_inherited
  PROJECT_HAS_DEPENDENTS:
    http_status: 409
    description: >-
      The project has one or more dependent resources (agents, ai providers, tools, etc.) and cannot be deleted. Use
      force=true to delete dependents as well.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#project_has_dependents
  PROMOTION_GATE_UNMET:
    http_status: 409
    description: >-
      The release's promotion_gate has no passing eval run against the canary version. Run the eval with agent_version
      pinned to the canary, or abort the rollout.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#promotion_gate_unmet
  QUEUE_DRIVER_MISCONFIGURED:
    http_status: 500
    description: >-
      The orchestration queue driver is not configured correctly — an unknown ORCHESTRATION_QUEUE_DRIVER, or a missing
      required variable for the selected driver (e.g. ORCHESTRATION_QUEUE_SQS_QUEUE_URL for sqs).
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#queue_driver_misconfigured
  QUOTA_CONFLICT:
    http_status: 409
    description: >-
      A quota with the same (project, scope, scope_ref, metric, window) already exists. The all-enforce precedence rule
      makes duplicates pure redundancy, so a duplicate is rejected instead of stored.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#quota_conflict
  QUOTA_EXCEEDED:
    http_status: 429
    description: >-
      An enforced quota has been exceeded for the request scope. The response carries a `Retry-After` header (seconds
      until the window resets) and a `meta` block naming the breached quota, metric, limit, window, and reset time.
    resolution: >-
      Wait until the window resets — `Retry-After` (seconds) and `meta.resets_at` both carry the time — or raise the
      quota with `PATCH /api/v1/quotas/{quota_id}`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#quota_exceeded
  QUOTA_UNENFORCEABLE:
    http_status: 409
    description: >-
      An enforced `cost_usd` quota with `on_unpriced: "block"` (the default) cannot be evaluated: the current window
      holds a pricing blackout — several metered events, none of them priced — so the aggregate is `0` however much was
      actually spent. The cap refuses the generation rather than waving through spend it cannot measure. No
      `Retry-After` is sent — the window resetting changes nothing; configure pricing for the models in use, or set the
      quota's `on_unpriced` to `"allow"` to accept unmeasurable spend explicitly.
    resolution: >-
      Add price book entries covering the models this project runs (`POST /api/v1/prices`), so the cost quota has
      something to aggregate. To accept unmeasurable spend instead, set `on_unpriced: "allow"` on the quota with `PATCH
      /api/v1/quotas/{quota_id}` (or switch it to `monitor` mode to stop blocking entirely).
    docs_url: https://soat.ttoss.dev/docs/error-codes#quota_unenforceable
  REQUEST_REJECTED:
    http_status: 400
    description: >-
      The HTTP framework rejected the request before it reached a handler — a payload over the size limit, an
      unsupported method, a malformed request line. The accompanying message carries the specific reason, and the
      response status is the one the framework chose rather than this default.
    resolution: >-
      The HTTP layer rejected the request before it reached a route — method, content type, or size. The message names
      the reason.
    docs_url: https://soat.ttoss.dev/docs/error-codes#request_rejected
  RESOURCE_NOT_FOUND:
    http_status: 404
    description: The requested resource does not exist or is not accessible.
    resolution: >-
      Check the id, and check that the credential can see the project that owns the resource — a resource in another
      project is indistinguishable from one that does not exist. List the collection to confirm.
    docs_url: https://soat.ttoss.dev/docs/error-codes#resource_not_found
  SECRET_HAS_DEPENDENTS:
    http_status: 409
    description: >-
      The secret is referenced by one or more AI providers and cannot be deleted. Use force=true to delete dependents as
      well.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#secret_has_dependents
  SECRET_NOT_DECRYPTABLE:
    http_status: 500
    description: >-
      A stored trigger or webhook secret could not be decrypted — SECRETS_ENCRYPTION_KEY has changed since it was
      written. Rotating the secret replaces it; restoring the original key recovers every secret written under it.
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#secret_not_decryptable
  SECRET_NOT_FOUND:
    http_status: 400
    description: A secret referenced by a {{secret:...}} token does not exist in this project.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#secret_not_found
  SESSION_CLOSED:
    http_status: 409
    description: The session is closed and does not accept new messages or generation requests. Open a new session to continue.
    resolution: The session is closed and accepts no further messages. Open a new one, or fork this one to keep its history.
    docs_url: https://soat.ttoss.dev/docs/error-codes#session_closed
  SESSION_EXPIRED:
    http_status: 410
    description: The session has expired due to inactivity. Open a new session to continue.
    resolution: >-
      Open a new session; an expired one cannot be revived. Raise the agent's session TTL if this happens sooner than
      the workload needs.
    docs_url: https://soat.ttoss.dev/docs/error-codes#session_expired
  SINGLE_SESSION_CONFLICT:
    http_status: 409
    description: An open session already exists for this actor. Use the existing session or close it first.
    resolution: The agent allows one open session per actor. Reuse the session named in `meta.session_id`, or close it first.
    docs_url: https://soat.ttoss.dev/docs/error-codes#single_session_conflict
  SYSTEM_MESSAGE_NOT_ALLOWED:
    http_status: 400
    description: >-
      A `role: "system"` entry was supplied in `messages`. System content never travels as a message on any SOAT surface
      — it goes in the `instructions` field instead — the same name on every surface (a completion request, a Chat, an
      Agent). Mirrors the AI SDK, whose `allowSystemInMessages` defaults to false because a system entry in a
      caller-supplied array is a prompt-injection vector.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#system_message_not_allowed
  TASK_AUTOMATION_CHAIN_LIMIT:
    http_status: 409
    description: >-
      The task has run too many machine-driven transitions back-to-back with no outside intervention, and the next one
      was refused. This bounds a cycle composed across modules — a workflow state dispatching work that transitions the
      task back into that same state — which neither the orchestration cycle validator (intra-graph) nor the workflow
      itself (where revisiting states is deliberate) can see. Any move by a person, a plain API key, or an approval
      resolution resets the chain. The ceiling is TASK_AUTOMATION_CHAIN_LIMIT (default 50) (#885).
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_automation_chain_limit
  TASK_AUTOMATION_PROVENANCE_MISSING:
    http_status: 500
    description: >-
      An automation-caused transition was about to be persisted with no recorded cause: principal_id, generation_id, and
      orchestration_run_id would all be null. This is a writer bug, not a valid degraded record — the transition is
      rejected instead of being silently written (#792).
    resolution: Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_automation_provenance_missing
  TASK_GUARD_REJECTED:
    http_status: 400
    description: The transition guard evaluated to false for this task.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_guard_rejected
  TASK_NOT_FOUND:
    http_status: 404
    description: The task does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_not_found
  TASK_PAYLOAD_INVALID:
    http_status: 400
    description: The task payload does not satisfy the workflow's payload_schema.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_payload_invalid
  TASK_STATE_NOT_FOUND:
    http_status: 400
    description: A task create request's `state` does not name a declared state of the workflow.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_state_not_found
  TASK_TRANSITION_CONFLICT:
    http_status: 409
    description: A concurrent change made the requested transition invalid from the current state, or the task is already closed.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_transition_conflict
  TASK_TRANSITION_NOT_FOUND:
    http_status: 400
    description: The named transition does not exist in the workflow.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#task_transition_not_found
  TEXT_ENCODED_TOOL_CALL:
    http_status: 502
    description: >-
      The model wrote a tool invocation as plain assistant text (e.g. a ```json block containing {"name": "<a bound
      tool>", "arguments": {…}}) instead of making a structured tool call, so the tool never ran. Upstream-caused like
      AI_PROVIDER_ERROR: returning that text as the answer would be a silent data-integrity failure, so the generation
      fails instead.
    resolution: An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.
    docs_url: https://soat.ttoss.dev/docs/error-codes#text_encoded_tool_call
  TOOL_AUTH_FAILED:
    http_status: 502
    description: >-
      An http-type tool's `execute.auth` credentials could not be turned into a request credential — malformed service
      account JSON, an unusable private key, or a token endpoint that rejected the assertion. The error `meta` carries
      the token endpoint's status and body when it responded. Distinct from `TOOL_HTTP_ERROR`, which is the tool's own
      target rejecting the call.
    resolution: An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.
    docs_url: https://soat.ttoss.dev/docs/error-codes#tool_auth_failed
  TOOL_CALL_NOT_SUPPORTED:
    http_status: 422
    description: This tool type cannot be invoked server-side. Client tools must be executed by the calling client.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#tool_call_not_supported
  TOOL_DISPATCH_FAILED:
    http_status: 422
    description: >-
      A workflow on_enter `tool` dispatch did not return a result. The tool call was settled before it ran — blocked by
      a guardrail (class D or a class-B tripwire), or routed to human approval (class C), which a task dispatch cannot
      park on. The error `meta` carries the `tool_id` and the `outcome` that settled it. Dispatch an orchestration
      instead when the tool is approval-gated: its engine can park and resume.
    resolution: >-
      The request was accepted and then could not be carried out. The failure is in the work, not the request shape —
      read `meta` and the trace for which step failed before retrying.
    docs_url: https://soat.ttoss.dev/docs/error-codes#tool_dispatch_failed
  TOOL_EGRESS_BLOCKED:
    http_status: 403
    description: >-
      An http- or mcp-type tool's target is not publicly routable — a loopback, private, link-local (cloud metadata),
      CGNAT or IPv6 ULA address — and the deployment's TOOL_EGRESS_ALLOWED_HOSTS does not list it. Also returned when a
      hostname resolves to such an address, when a redirect leads to one, when the scheme is not http/https, or when the
      redirect chain is too long. The error `meta` carries `tool_url` and, when known, the offending `tool_address`.
    resolution: >-
      The credential is authenticated but lacks this action, or is scoped to another project. Grant the action in an IAM
      policy, or use a credential that has it.
    docs_url: https://soat.ttoss.dev/docs/error-codes#tool_egress_blocked
  TOOL_HTTP_ERROR:
    http_status: 502
    description: >-
      An http-type tool call returned a non-2xx response. The error `meta` carries the real upstream status code,
      response body, URL, and method.
    resolution: An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.
    docs_url: https://soat.ttoss.dev/docs/error-codes#tool_http_error
  TOOL_NOT_FOUND:
    http_status: 400
    description: A referenced tool does not exist.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#tool_not_found
  TRIGGER_ACTION_NOT_ALLOWED:
    http_status: 400
    description: >-
      A trigger field is incompatible with its type: `action` is only valid for tool targets (and required for soat/mcp
      tools), and `cron` is required for schedule triggers and rejected otherwise.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_action_not_allowed
  TRIGGER_CAUSATION_LIMIT:
    http_status: 409
    description: >-
      An event trigger refused to extend the causal chain that reached it — it is already in the chain, or the chain has
      run too deep.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_causation_limit
  TRIGGER_CREATOR_UNAVAILABLE:
    http_status: 409
    description: The trigger creator no longer exists, so the firing identity cannot be resolved.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_creator_unavailable
  TRIGGER_INPUT_INVALID:
    http_status: 400
    description: The effective input is invalid for the target (empty agent input, or an orchestration input_schema violation).
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_input_invalid
  TRIGGER_NOT_ACTIVE:
    http_status: 409
    description: The trigger is inactive and cannot fire.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_not_active
  TRIGGER_RECURSION_FORBIDDEN:
    http_status: 403
    description: A trigger-scoped credential cannot fire a trigger, preventing unbounded trigger→trigger loops.
    resolution: >-
      The credential is authenticated but lacks this action, or is scoped to another project. Grant the action in an IAM
      policy, or use a credential that has it.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_recursion_forbidden
  TRIGGER_TARGET_NOT_FOUND:
    http_status: 400
    description: The trigger target does not exist in the project, or its type does not match target_type.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_target_not_found
  TRIGGER_TYPE_IMMUTABLE:
    http_status: 400
    description: A trigger’s type cannot be changed after creation.
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#trigger_type_immutable
  UNAUTHORIZED:
    http_status: 401
    description: Authentication is required or the token is invalid.
    resolution: >-
      Send `Authorization: Bearer <token>` — a user token from `POST /api/v1/users/login`, a project API key (`sk_…`),
      or an OAuth access token. Discovery metadata for the OAuth flow is at `/.well-known/oauth-authorization-server`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#unauthorized
  UNSUPPORTED_FILE_TYPE:
    http_status: 400
    description: >-
      The referenced file has a content type that cannot be ingested. Supported types: application/pdf, text/plain,
      text/markdown.
    resolution: >-
      The file's media type has no parser on this deployment. Convert it to a supported type, or add an ingestion rule
      that does.
    docs_url: https://soat.ttoss.dev/docs/error-codes#unsupported_file_type
  UPLOAD_TOKEN_EXPIRED:
    http_status: 410
    description: The upload token has expired. Request a new one.
    resolution: Request a fresh upload token; tokens are single-use and short-lived by design.
    docs_url: https://soat.ttoss.dev/docs/error-codes#upload_token_expired
  UPLOAD_TOKEN_NOT_FOUND:
    http_status: 404
    description: The upload token does not exist.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#upload_token_not_found
  UPLOAD_TOKEN_USED:
    http_status: 409
    description: The upload token has already been used.
    resolution: This upload token has already been redeemed. Request a new one for another upload.
    docs_url: https://soat.ttoss.dev/docs/error-codes#upload_token_used
  VALIDATION_FAILED:
    http_status: 400
    description: The request body failed validation.
    resolution: >-
      Fix the request and retry. Unknown fields are rejected outright, so compare the payload against the operation in
      `/openapi.json`; `meta` names the offending field when the check can identify one.
    docs_url: https://soat.ttoss.dev/docs/error-codes#validation_failed
  WORKFLOW_HAS_OPEN_TASKS:
    http_status: 409
    description: The workflow has one or more open tasks and cannot be deleted.
    resolution: >-
      The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named
      in the message, then retry.
    docs_url: https://soat.ttoss.dev/docs/error-codes#workflow_has_open_tasks
  WORKFLOW_NOT_FOUND:
    http_status: 404
    description: The workflow does not exist or is not accessible.
    resolution: >-
      Check the id and the project the credential can see, then retry. Listing the collection confirms whether the
      resource exists.
    docs_url: https://soat.ttoss.dev/docs/error-codes#workflow_not_found
  WORKFLOW_VALIDATION_FAILED:
    http_status: 400
    description: >-
      The workflow definition is invalid (e.g. duplicate state names, not exactly one initial state, a transition
      referencing an unknown state, a malformed guard, or an on_enter dispatch missing its target).
    resolution: >-
      Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against
      the operation in `/openapi.json`.
    docs_url: https://soat.ttoss.dev/docs/error-codes#workflow_validation_failed
